feat(scan): add -concurrency flag to scan targets in parallel (#367)

* refactor(cli): extract per-target scan into scanTarget

pull the target loop body into scanTarget, which returns a targetScan
holding that target's findings, report rows, scan labels and log files
instead of appending onto run-wide slices. the run loop merges those in
target order and finishRun handles the post-loop notify/silent/report/
summary steps. behavior is identical for the sequential run today; the
isolation is the seam a bounded worker pool needs next.

* feat(cli): scan targets in parallel with -concurrency

add -concurrency N (default 1) to run the per-target scan pool over
several targets at once. scanTarget already returns isolated
accumulators, so workers share only the console: output.SetConcurrent
serializes sink writes and de-animates spinners/progress so parallel
lines stay whole. results merge in input order, and concurrency 1 keeps
the sequential path unchanged.
This commit is contained in:
Tigah
2026-07-12 14:23:35 -07:00
committed by GitHub
parent 24f3481fb1
commit d13622dc1c
7 changed files with 717 additions and 404 deletions
+6 -3
View File
@@ -91,7 +91,9 @@ func (p *Progress) Resume() {
// Done clears the progress bar line
func (p *Progress) Done() {
if apiMode || !IsTTY {
// under -concurrency there is no live line to clear (render printed milestones),
// and clearing would wipe another target's output.
if apiMode || !IsTTY || concurrent {
return
}
ClearLine()
@@ -102,8 +104,9 @@ func (p *Progress) render() {
return
}
// In non-TTY mode, print progress at milestones only
if !IsTTY {
// In non-TTY mode, or under -concurrency, print progress at milestones only
// rather than redrawing a live line that parallel targets would corrupt.
if !IsTTY || concurrent {
current := atomic.LoadInt64(&p.current)
total := p.total
if total <= 0 {