Files
sif/sif_concurrency_test.go
TigahandGitHub d13622dc1c 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.
2026-07-12 14:23:35 -07:00

81 lines
3.1 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package sif
import (
"testing"
"github.com/vmfunc/sif/internal/output"
)
// TestScanAllTargetsConcurrentIsolation runs the target pool at concurrency 3
// against three separate servers and asserts each target's result is isolated:
// exactly one scan recorded per target, results in input order. Run under -race
// it also backstops scanTarget's concurrency safety and the serialized sink.
func TestScanAllTargetsConcurrentIsolation(t *testing.T) {
defer output.SetConcurrent(false)
srvA := okServer()
defer srvA.Close()
srvB := okServer()
defer srvB.Close()
srvC := okServer()
defer srvC.Close()
app := headersOnlyApp()
app.targets = []string{srvA.URL, srvB.URL, srvC.URL}
app.settings.Concurrency = 3
results, err := app.scanAllTargets("", false)
if err != nil {
t.Fatalf("scanAllTargets: %v", err)
}
if len(results) != len(app.targets) {
t.Fatalf("got %d results, want %d", len(results), len(app.targets))
}
for i, ts := range results {
if len(ts.scansRun) != 1 || ts.scansRun[0] != "HTTP Headers" {
t.Errorf("target %d scansRun = %v, want exactly [HTTP Headers] (accumulator leaked across targets)", i, ts.scansRun)
}
}
}
// TestScanAllTargetsSequentialMatchesInputOrder pins that concurrency 1 keeps the
// plain sequential path: one result per target, in order, no pool engaged.
func TestScanAllTargetsSequentialMatchesInputOrder(t *testing.T) {
srvA := okServer()
defer srvA.Close()
srvB := okServer()
defer srvB.Close()
app := headersOnlyApp()
app.targets = []string{srvA.URL, srvB.URL}
app.settings.Concurrency = 1
results, err := app.scanAllTargets("", false)
if err != nil {
t.Fatalf("scanAllTargets: %v", err)
}
if len(results) != 2 {
t.Fatalf("got %d results, want 2", len(results))
}
for i, ts := range results {
if len(ts.scansRun) != 1 || ts.scansRun[0] != "HTTP Headers" {
t.Errorf("target %d scansRun = %v, want exactly [HTTP Headers]", i, ts.scansRun)
}
}
if output.Concurrent() {
t.Error("concurrency 1 must not switch output into concurrent mode")
}
}