Files
sif/sif_scantarget_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

79 lines
3.0 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package sif
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/vmfunc/sif/internal/config"
)
// headersOnlyApp builds an App that runs only the HTTP headers scan against
// live servers, with every network-touching scanner gated off. it isolates the
// scanTarget path to a single deterministic scan.
func headersOnlyApp() *App {
return &App{settings: &config.Settings{
Headers: true,
NoScan: true,
Dirlist: "none",
Dnslist: "none",
Ports: "none",
Timeout: 5 * time.Second,
}}
}
func okServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-Sif-Test", "1")
w.WriteHeader(http.StatusOK)
}))
}
// TestScanTargetIsolatesPerTargetState is the accumulator seam the worker pool
// depends on: each scanTarget call must own its scansRun rather than share a
// run-wide slice. two sequential scans of different targets must each report a
// single scan, not a growing total; a shared accumulator would make the second
// call report two.
func TestScanTargetIsolatesPerTargetState(t *testing.T) {
srvA := okServer()
defer srvA.Close()
srvB := okServer()
defer srvB.Close()
app := headersOnlyApp()
tsA, err := app.scanTarget(srvA.URL, "", false)
if err != nil {
t.Fatalf("scanTarget(A): %v", err)
}
if len(tsA.scansRun) != 1 || tsA.scansRun[0] != "HTTP Headers" {
t.Fatalf("target A scansRun = %v, want exactly [HTTP Headers]", tsA.scansRun)
}
tsB, err := app.scanTarget(srvB.URL, "", false)
if err != nil {
t.Fatalf("scanTarget(B): %v", err)
}
if len(tsB.scansRun) != 1 || tsB.scansRun[0] != "HTTP Headers" {
t.Fatalf("target B scansRun = %v, want exactly [HTTP Headers] (accumulator leaked across targets)", tsB.scansRun)
}
// no log dir configured, so no per-target log file should be recorded.
if len(tsA.logFiles) != 0 || len(tsB.logFiles) != 0 {
t.Errorf("logFiles should be empty without a log dir, got A=%v B=%v", tsA.logFiles, tsB.logFiles)
}
}