@@ -27,6 +27,7 @@ SHOW_FIX=0
2727QUIET=0
2828NO_INTERACTIVE=0
2929REMEDIATE=0
30+ PARALLEL_SCANNERS=1
3031CONFIG_DIR=" "
3132
3233# ─── Usage ───────────────────────────────────────────────────────────────────
@@ -40,6 +41,7 @@ Options:
4041 --json Output findings as JSON array only
4142 --fix Show auto-fix commands in report
4243 --quiet Print summary line only
44+ --sequential Run scanners sequentially (default is parallel)
4345 --no-interactive Disable interactive post-scan menu
4446 --remediate Run scan then pipe findings to Claude for AI remediation
4547 --config-dir PATH Explicit path to openclaw config directory
@@ -60,6 +62,7 @@ while [[ $# -gt 0 ]]; do
6062 --json) JSON_OUTPUT=1; shift ;;
6163 --fix) SHOW_FIX=1; shift ;;
6264 --quiet) QUIET=1; shift ;;
65+ --sequential) PARALLEL_SCANNERS=0; shift ;;
6366 --no-interactive) NO_INTERACTIVE=1; shift ;;
6467 --remediate) REMEDIATE=1; NO_INTERACTIVE=1; shift ;;
6568 --config-dir)
@@ -143,6 +146,63 @@ if [[ "$JSON_OUTPUT" -eq 0 ]] && [[ "$QUIET" -eq 0 ]]; then
143146 print_init_message
144147fi
145148
149+ # ─── Parallel scanner execution function ────────────────────────────────────
150+
151+ run_scanners_parallel () {
152+ local temp_dir=" "
153+ temp_dir=" $( mktemp -d " ${TMPDIR:-/ tmp} /clawpinch.XXXXXX" ) "
154+
155+ # Use RETURN trap for cleanup — fires when function returns, doesn't
156+ # interfere with main script's own traps or Ctrl+C handling
157+ trap ' rm -rf "$temp_dir"' RETURN
158+
159+ # Track background job PIDs
160+ declare -a pids=()
161+
162+ # Launch all scanners in parallel
163+ for scanner in " ${scanners[@]} " ; do
164+ local scanner_name=" $( basename " $scanner " ) "
165+ local temp_file=" $temp_dir /${scanner_name} .json"
166+
167+ # Run scanner in background, redirecting output to temp file
168+ (
169+ # Initialize with empty array in case scanner fails to run
170+ echo ' []' > " $temp_file "
171+
172+ # Run scanner - exit code doesn't matter, we just need valid JSON output
173+ # (Scanners exit with code 1 when they find critical findings, but still output valid JSON)
174+ # Use command -v instead of has_cmd — bash functions aren't inherited by subshells
175+ if [[ " $scanner " == * .sh ]]; then
176+ bash " $scanner " > " $temp_file " 2> /dev/null || true
177+ elif [[ " $scanner " == * .py ]]; then
178+ # Python 3 only — scanners use f-strings and type hints that fail under Python 2
179+ if command -v python3 & > /dev/null; then
180+ python3 " $scanner " > " $temp_file " 2> /dev/null || true
181+ else
182+ echo " WARN: skipping $scanner_name (python3 not found)" >&2
183+ fi
184+ fi
185+ ) &
186+
187+ pids+=(" $! " )
188+ done
189+
190+ # Wait for all background jobs to complete
191+ for pid in " ${pids[@]} " ; do
192+ wait " $pid " 2> /dev/null || true
193+ done
194+
195+ # Merge all JSON outputs in a single jq command (avoids N jq calls in a loop)
196+ local json_files=(" $temp_dir " /* .json)
197+ if [[ -e " ${json_files[0]} " ]]; then
198+ ALL_FINDINGS=" $( jq -s ' add' " ${json_files[@]} " 2> /dev/null) " || ALL_FINDINGS=" []"
199+ else
200+ ALL_FINDINGS=" []"
201+ fi
202+
203+ # Temp directory cleaned up by RETURN trap
204+ }
205+
146206# ─── Discover scanner scripts ───────────────────────────────────────────────
147207
148208scanners=()
@@ -176,7 +236,32 @@ _SPINNER_PID=""
176236# Record scan start time
177237_scan_start=" ${EPOCHSECONDS:- $(date +% s)} "
178238
179- for scanner in " ${scanners[@]} " ; do
239+ # ─── Execute scanners (parallel or sequential) ──────────────────────────────
240+
241+ if [[ " $PARALLEL_SCANNERS " -eq 1 ]]; then
242+ # Parallel execution
243+ if [[ " $JSON_OUTPUT " -eq 0 ]] && [[ " $QUIET " -eq 0 ]]; then
244+ start_spinner " Running ${scanner_count} scanners in parallel..."
245+ fi
246+
247+ # Record parallel execution start time
248+ _parallel_start=" ${EPOCHSECONDS:- $(date +% s)} "
249+
250+ run_scanners_parallel
251+
252+ # Calculate parallel execution elapsed time
253+ _parallel_end=" ${EPOCHSECONDS:- $(date +% s)} "
254+ _parallel_elapsed=$(( _parallel_end - _parallel_start ))
255+
256+ # Count findings from merged results
257+ _parallel_count=" $( echo " $ALL_FINDINGS " | jq ' length' ) "
258+
259+ if [[ " $JSON_OUTPUT " -eq 0 ]] && [[ " $QUIET " -eq 0 ]]; then
260+ stop_spinner " Parallel scan" " $_parallel_count " " $_parallel_elapsed "
261+ fi
262+ else
263+ # Sequential execution
264+ for scanner in " ${scanners[@]} " ; do
180265 scanner_idx=$(( scanner_idx + 1 ))
181266 scanner_name=" $( basename " $scanner " ) "
182267 scanner_base=" ${scanner_name% .* } "
@@ -233,7 +318,8 @@ for scanner in "${scanners[@]}"; do
233318 if [[ " $JSON_OUTPUT " -eq 0 ]] && [[ " $QUIET " -eq 0 ]]; then
234319 stop_spinner " $local_name " " $local_count " " $_scanner_elapsed "
235320 fi
236- done
321+ done
322+ fi
237323
238324# Calculate total scan time
239325_scan_end=" ${EPOCHSECONDS:- $(date +% s)} "
0 commit comments