From f170fa461ffcb6f40b2951ff991fd9498e5f48e1 Mon Sep 17 00:00:00 2001 From: root-Manas Date: Sun, 27 Sep 2026 19:52:14 +0530 Subject: [PATCH 1/4] Preserve storage override paths Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- cmd/macaron/main.go | 6 +++--- cmd/macaron/main_test.go | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/cmd/macaron/main.go b/cmd/macaron/main.go index fe30259..4f7ba13 100644 --- a/cmd/macaron/main.go +++ b/cmd/macaron/main.go @@ -768,11 +768,11 @@ func applyProfile(profile string, mode *string, rate *int, threads *int, stages } func macaronHome(override string) (string, error) { - if strings.TrimSpace(override) != "" { - return filepath.Clean(override), nil + if value := strings.TrimSpace(override); value != "" { + return value, nil } if env := strings.TrimSpace(os.Getenv("MACARON_HOME")); env != "" { - return filepath.Clean(env), nil + return env, nil } if cwd, err := os.Getwd(); err == nil { legacy := filepath.Join(cwd, "storage") diff --git a/cmd/macaron/main_test.go b/cmd/macaron/main_test.go index d053195..7fde6b7 100644 --- a/cmd/macaron/main_test.go +++ b/cmd/macaron/main_test.go @@ -69,4 +69,12 @@ func TestMacaronHomeOverride(t *testing.T) { if got != "/tmp/test-storage" { t.Fatalf("expected /tmp/test-storage, got %s", got) } + + got, err = macaronHome("~/test-storage") + if err != nil { + t.Fatal(err) + } + if got != "~/test-storage" { + t.Fatalf("expected ~/test-storage to remain literal, got %s", got) + } } From eb6cc4bf7144eb1ef53da8ba0e27469af97c3ce6 Mon Sep 17 00:00:00 2001 From: root-Manas Date: Sun, 27 Sep 2026 19:54:46 +0530 Subject: [PATCH 2/4] Remove unnecessary test workflow Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 45 ---------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 6cce375..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Download modules - run: go mod download - - - name: Run tests - run: go test ./... -v - - - name: Build CLI - run: mkdir -p bin && go build -o bin/macaron ./cmd/macaron - - - name: CLI smoke tests - run: | - ./bin/macaron --version - ./bin/macaron - - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Vet - run: go vet ./... From ef8b094e8c7636e9e9b9c78763f07ff034a8284a Mon Sep 17 00:00:00 2001 From: root-Manas Date: Sun, 27 Sep 2026 19:56:02 +0530 Subject: [PATCH 3/4] Add bounded parallel target scans Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 2 + cmd/macaron/main.go | 36 +++++++++++------ internal/app/app.go | 98 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 106 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index b98e4a0..32e6bbb 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Targets may be domain names, IP addresses, or bare origin URLs such as `https:// ```sh ./macaron scan -t example.com -t example.org +./macaron scan -t example.com -t example.org --target-workers 2 ./macaron scan --file targets.txt cat targets.txt | ./macaron scan --stdin ``` @@ -53,6 +54,7 @@ cat targets.txt | ./macaron scan --stdin | `--stages` | `all` | Comma-separated `subdomains,http,ports,urls,vulns` | | `--rate` | `150` | Built-in probe dispatches per second, capped at 10,000 | | `--threads` | `30` | Concurrent workers, maximum 500 | +| `--target-workers` | `1` | Number of targets scanned concurrently, maximum 50 | | `-q, --quiet` | off | Suppress progress display | | `--storage` | user config directory | Override the data directory | diff --git a/cmd/macaron/main.go b/cmd/macaron/main.go index 4f7ba13..fedf158 100644 --- a/cmd/macaron/main.go +++ b/cmd/macaron/main.go @@ -77,16 +77,17 @@ func run() int { func runScan(args []string) int { fs := pflag.NewFlagSet("scan", pflag.ContinueOnError) var ( - targets []string - file string - stdin bool - mode string - rate int - threads int - stages string - profile string - quiet bool - storage string + targets []string + file string + stdin bool + mode string + rate int + threads int + targetWorkers int + stages string + profile string + quiet bool + storage string ) fs.StringArrayVarP(&targets, "target", "t", nil, "Target domain(s) (repeatable)") fs.StringVarP(&file, "file", "f", "", "Read targets from file (one per line)") @@ -94,6 +95,7 @@ func runScan(args []string) int { fs.StringVarP(&mode, "mode", "m", "wide", "Probe discovered hosts (wide) or target only (narrow)") fs.IntVar(&rate, "rate", 150, "Built-in probe dispatches per second (max 10,000)") fs.IntVar(&threads, "threads", 30, "Concurrent workers") + fs.IntVar(&targetWorkers, "target-workers", 1, "Targets to scan concurrently") fs.StringVar(&stages, "stages", "all", "Comma-separated stages: subdomains,http,ports,urls,vulns") fs.StringVarP(&profile, "profile", "p", "balanced", "Workflow profile: passive|balanced|aggressive") fs.BoolVarP(&quiet, "quiet", "q", false, "Suppress progress output") @@ -167,6 +169,14 @@ func runScan(args []string) int { cliui.Err("--threads must be 500 or fewer") return 2 } + if targetWorkers <= 0 { + cliui.Err("--target-workers must be > 0") + return 1 + } + if targetWorkers > 50 { + cliui.Err("--target-workers must be 50 or fewer") + return 2 + } if rate > 10000 { cliui.Err("--rate must be 10,000 or fewer") return 2 @@ -181,14 +191,15 @@ func runScan(args []string) int { if !quiet { renderer = cliui.NewLiveRenderer(os.Stdout) defer renderer.Close() - cliui.Info("profile=%s mode=%s stages=%s rate=%d threads=%d targets=%d", - profile, mode, stages, rate, threads, len(allTargets)) + cliui.Info("profile=%s mode=%s stages=%s rate=%d threads=%d target-workers=%d targets=%d", + profile, mode, stages, rate, threads, targetWorkers, len(allTargets)) } res, err := application.Scan(ctx, app.ScanArgs{ Targets: allTargets, Mode: modeVal, Rate: rate, Threads: threads, + TargetWorkers: targetWorkers, Quiet: quiet, EnabledStages: app.ParseStages(stages), APIKeys: config.APIKeys, @@ -666,6 +677,7 @@ scan flags: --stages LIST subdomains,http,ports,urls,vulns (default: all) --rate N request rate hint (default: 150) --threads N workers (default: 30) + --target-workers N targets scanned concurrently (default: 1) -q, --quiet suppress progress output --storage DIR custom storage root diff --git a/internal/app/app.go b/internal/app/app.go index 78cae26..18bfb48 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -15,6 +15,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" "github.com/jedib0t/go-pretty/v6/table" @@ -51,6 +52,7 @@ type ScanArgs struct { Mode model.Mode Rate int Threads int + TargetWorkers int Quiet bool EnabledStages map[string]bool APIKeys map[string]string @@ -69,31 +71,91 @@ func (a *App) Scan(ctx context.Context, args ScanArgs) ([]model.ScanResult, erro if len(args.Targets) == 0 { return nil, errors.New("no valid targets provided") } - results := make([]model.ScanResult, 0, len(args.Targets)) - for _, t := range args.Targets { - if err := ctx.Err(); err != nil { - return results, err + workers := args.TargetWorkers + if workers <= 0 { + workers = 1 + } + if workers > len(args.Targets) { + workers = len(args.Targets) + } + + scanCtx, cancel := context.WithCancel(ctx) + defer cancel() + + results := make([]model.ScanResult, len(args.Targets)) + jobs := make(chan int) + var wg sync.WaitGroup + var firstErr error + var errMu sync.Mutex + + worker := func() { + defer wg.Done() + for index := range jobs { + if scanCtx.Err() != nil { + return + } + target := args.Targets[index] + res, err := a.Engine.ScanTarget(scanCtx, target, engine.Options{ + Mode: args.Mode, + Rate: args.Rate, + Threads: args.Threads, + Quiet: args.Quiet, + EnabledStages: args.EnabledStages, + APIKeys: args.APIKeys, + Progress: args.Progress, + }) + if err == nil { + err = a.Store.SaveScan(res) + } + if err != nil { + errMu.Lock() + if firstErr == nil { + firstErr = fmt.Errorf("scan %s: %w", target, err) + cancel() + } + errMu.Unlock() + return + } + results[index] = res } - res, err := a.Engine.ScanTarget(ctx, t, engine.Options{ - Mode: args.Mode, - Rate: args.Rate, - Threads: args.Threads, - Quiet: args.Quiet, - EnabledStages: args.EnabledStages, - APIKeys: args.APIKeys, - Progress: args.Progress, - }) - if err != nil { - return results, fmt.Errorf("scan %s: %w", t, err) + } + + wg.Add(workers) + for i := 0; i < workers; i++ { + go worker() + } + for index := range args.Targets { + select { + case jobs <- index: + case <-scanCtx.Done(): + break } - if err := a.Store.SaveScan(res); err != nil { - return results, fmt.Errorf("save scan %s: %w", t, err) + if scanCtx.Err() != nil { + break } - results = append(results, res) + } + close(jobs) + wg.Wait() + + errMu.Lock() + err := firstErr + errMu.Unlock() + if err != nil { + return compactResults(results), err } return results, nil } +func compactResults(results []model.ScanResult) []model.ScanResult { + out := make([]model.ScanResult, 0, len(results)) + for _, result := range results { + if result.ID != "" { + out = append(out, result) + } + } + return out +} + func (a *App) ShowStatus(limit int) (string, error) { summaries, err := a.Store.Summaries(limit) if err != nil { From 3055524095d29da3ec75d941effb6103cf08e4c8 Mon Sep 17 00:00:00 2001 From: root-Manas Date: Sun, 27 Sep 2026 19:57:27 +0530 Subject: [PATCH 4/4] Preserve storage path whitespace Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- cmd/macaron/main.go | 7 ++++--- cmd/macaron/main_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/cmd/macaron/main.go b/cmd/macaron/main.go index fedf158..4ab6c7d 100644 --- a/cmd/macaron/main.go +++ b/cmd/macaron/main.go @@ -780,10 +780,11 @@ func applyProfile(profile string, mode *string, rate *int, threads *int, stages } func macaronHome(override string) (string, error) { - if value := strings.TrimSpace(override); value != "" { - return value, nil + if strings.TrimSpace(override) != "" { + return override, nil } - if env := strings.TrimSpace(os.Getenv("MACARON_HOME")); env != "" { + env := os.Getenv("MACARON_HOME") + if strings.TrimSpace(env) != "" { return env, nil } if cwd, err := os.Getwd(); err == nil { diff --git a/cmd/macaron/main_test.go b/cmd/macaron/main_test.go index 7fde6b7..a38fc9a 100644 --- a/cmd/macaron/main_test.go +++ b/cmd/macaron/main_test.go @@ -77,4 +77,29 @@ func TestMacaronHomeOverride(t *testing.T) { if got != "~/test-storage" { t.Fatalf("expected ~/test-storage to remain literal, got %s", got) } + + override := " /tmp/test-storage " + got, err = macaronHome(override) + if err != nil { + t.Fatal(err) + } + if got != override { + t.Fatalf("expected %q to remain unchanged, got %q", override, got) + } +} + +func TestMacaronHomeEnvironmentOverride(t *testing.T) { + for _, want := range []string{"/tmp/macaron-home ", "~/macaron-home"} { + t.Run(want, func(t *testing.T) { + t.Setenv("MACARON_HOME", want) + + got, err := macaronHome("") + if err != nil { + t.Fatal(err) + } + if got != want { + t.Fatalf("expected %q, got %q", want, got) + } + }) + } }