@@ -362,6 +362,59 @@ _lifecycle_render_text_active() {
362362 return 0
363363}
364364
365+ # #
366+ # _lifecycle_list_stopped()
367+ # Enumerates stopped scans that have a valid checkpoint (resumable).
368+ # Returns 0 if any found (and prints text table), 1 if none.
369+ # Stopped scans are those with scan.meta.* state=stopped AND a matching
370+ # scan.checkpoint.* file — these can be resumed with --continue.
371+ # #
372+ _lifecycle_list_stopped () {
373+ local _scanid _state _found=0
374+ local _stopped_ids=" "
375+
376+ local _meta_file
377+ for _meta_file in " $sessdir " /scan.meta.* ; do
378+ [ -f " $_meta_file " ] || continue
379+ _scanid=" ${_meta_file##* scan.meta.} "
380+ case " $_scanid " in * .tmp) continue ;; esac
381+ _state=$( _lifecycle_detect_state " $_scanid " 2> /dev/null) || continue # safe: skip unreadable meta
382+ if [ " $_state " = " stopped" ] && [ -f " $sessdir /scan.checkpoint.$_scanid " ]; then
383+ if [ -z " $_stopped_ids " ]; then
384+ _stopped_ids=" $_scanid "
385+ else
386+ _stopped_ids=" $_stopped_ids " $' \n ' " $_scanid "
387+ fi
388+ _found=1
389+ fi
390+ done
391+
392+ if [ " $_found " -eq 0 ]; then
393+ return 1
394+ fi
395+
396+ local _count
397+ _count=$( printf ' %s\n' " $_stopped_ids " | command grep -c ' ^.' || echo 0)
398+ printf ' Stopped scans (%s):\n' " $_count "
399+ printf ' %-22s %-10s %-10s %-6s %-6s %-20s %s\n' \
400+ " SCANID" " STAGE" " FILES" " HITS" " WKRS" " STOPPED" " PATH"
401+
402+ while IFS= read -r _scanid; do
403+ [ -z " $_scanid " ] && continue
404+ _lifecycle_read_meta " $_scanid " || continue
405+
406+ local _stopped_display
407+ _stopped_display=$( echo " ${_meta_stopped_hr:- unknown} " | awk ' {print $1,$2,$3,$4}' )
408+ printf ' %-22s %-10s %-10s %-6s %-6s %-20s %s\n' \
409+ " $_scanid " " ${_meta_stage:- -} " " ${_meta_total_files:- -} " \
410+ " ${_meta_hits:- 0} " " ${_meta_workers:- -} " \
411+ " $_stopped_display " " ${_meta_path:- -} "
412+ done <<< " $_stopped_ids"
413+
414+ printf ' (resume: maldet --continue SCANID)\n'
415+ return 0
416+ }
417+
365418# #
366419# _lifecycle_render_json_active(scanids_newline_separated)
367420# JSON array output. Build manually with printf (no jq dependency).
@@ -1410,19 +1463,22 @@ _lifecycle_continue() {
14101463 eout " {lifecycle} warning: signature version changed since checkpoint (was: $_ckpt_sig_version , now: $_cur_sig_ver )" 1
14111464 fi
14121465
1413- # Apply checkpoint options as config overrides
1466+ # Apply checkpoint options as config overrides (gated by -co allowlist)
14141467 if [ -n " $_ckpt_options " ]; then
1415- local _opt
1468+ local _opt _co_allowed_pat
1469+ _build_co_allowed_pattern _co_allowed_pat
14161470 local _saved_ifs=" $IFS "
14171471 IFS=' ,'
14181472 for _opt in $_ckpt_options ; do
14191473 IFS=" $_saved_ifs "
14201474 # Each _opt is "key=value" — apply to shell environment
14211475 local _opt_key=" ${_opt%% =* } "
14221476 local _opt_val=" ${_opt#* =} "
1423- # Validate key is a safe identifier (word chars only) to prevent eval injection
1424- if [ -n " $_opt_key " ] && [[ " $_opt_key " =~ ^[a-zA-Z_][a-zA-Z0-9_]* $ ]]; then
1425- eval " $_opt_key =\"\$ _opt_val\" "
1477+ # Validate key against -co allowlist (rejects PATH, IFS, LD_PRELOAD, etc.)
1478+ if [ -n " $_opt_key " ] && [[ " $_opt_key " =~ $_co_allowed_pat ]]; then
1479+ printf -v " $_opt_key " ' %s' " $_opt_val "
1480+ else
1481+ eout " {lifecycle} warning: rejected unknown checkpoint option: $_opt_key " 1
14261482 fi
14271483 done
14281484 IFS=" $_saved_ifs "
0 commit comments