Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 136 additions & 69 deletions cmd/macaron/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func main() {
func run() int {
normalizeLegacyArgs()
normalizeCommandArgs()
normalizeCompactFlags()

var (
scanTargets []string
Expand Down Expand Up @@ -63,38 +64,38 @@ func run() int {
guide bool
)

pflag.StringArrayVarP(&scanTargets, "scan", "s", nil, "Scan target(s)")
pflag.BoolVarP(&status, "status", "S", false, "Show scan status")
pflag.BoolVarP(&results, "results", "R", false, "Show results")
pflag.BoolVarP(&listTools, "list-tools", "L", false, "List external tool availability")
pflag.BoolVarP(&export, "export", "E", false, "Export results to JSON")
pflag.BoolVarP(&configCmd, "config", "C", false, "Show config paths")
pflag.BoolVarP(&pipeline, "pipeline", "P", false, "Show pipeline path (v2 native pipeline is built-in)")
pflag.BoolVar(&serve, "serve", false, "Start web dashboard server")
pflag.StringArrayVar(&scanTargets, "scn", nil, "Scan target(s)")
pflag.BoolVar(&status, "sts", false, "Show scan status")
pflag.BoolVar(&results, "res", false, "Show results")
pflag.BoolVar(&listTools, "lst", false, "List external tool availability")
pflag.BoolVar(&export, "exp", false, "Export results to JSON")
pflag.BoolVar(&configCmd, "cfg", false, "Show config paths")
pflag.BoolVar(&pipeline, "pip", false, "Show pipeline path (v2 native pipeline is built-in)")
pflag.BoolVar(&serve, "srv", false, "Start web dashboard server")

pflag.StringVarP(&filePath, "file", "F", "", "Read targets from file")
pflag.BoolVar(&useStdin, "stdin", false, "Read targets from stdin")
pflag.StringVarP(&domain, "domain", "d", "", "Filter by domain")
pflag.StringVar(&scanID, "id", "", "Fetch specific scan ID")
pflag.StringVarP(&what, "what", "w", "all", "Result view: all|subdomains|live|ports|urls|js|vulns")
pflag.StringVarP(&mode, "mode", "m", "wide", "Mode: wide|narrow|fast|deep|osint")
pflag.BoolVarP(&fast, "fast", "f", false, "Shortcut for mode fast")
pflag.BoolVarP(&narrow, "narrow", "n", false, "Shortcut for mode narrow")
pflag.IntVar(&rate, "rate", 150, "Request rate hint")
pflag.IntVar(&threads, "threads", 30, "Worker threads")
pflag.IntVar(&limit, "limit", 50, "Output limit")
pflag.StringVarP(&output, "output", "o", "", "Output file")
pflag.BoolVarP(&quiet, "quiet", "q", false, "Quiet output")
pflag.BoolVar(&showVersion, "version", false, "Show version")
pflag.StringVar(&serveAddr, "addr", "127.0.0.1:8088", "Dashboard bind address")
pflag.StringVar(&storagePath, "storage", "", "Storage root directory (default: ./storage)")
pflag.StringVar(&stages, "stages", "all", "Comma-separated stages: subdomains,http,ports,urls,vulns")
pflag.StringArrayVar(&setAPI, "set-api", nil, "Set API key as name=value (repeatable). Use empty value to unset.")
pflag.BoolVar(&showAPI, "show-api", false, "Show configured API keys (masked)")
pflag.BoolVar(&setup, "setup", false, "Show setup screen with tool installation status")
pflag.BoolVar(&installTools, "install-tools", false, "Install missing supported tools (Linux)")
pflag.StringVar(&profile, "profile", "balanced", "Workflow profile: passive|balanced|aggressive")
pflag.BoolVar(&guide, "guide", false, "Show first-principles workflow guide")
pflag.StringVar(&filePath, "fil", "", "Read targets from file")
pflag.BoolVar(&useStdin, "inp", false, "Read targets from stdin")
pflag.StringVar(&domain, "dom", "", "Filter by domain")
pflag.StringVar(&scanID, "sid", "", "Fetch specific scan ID")
pflag.StringVar(&what, "wht", "all", "Result view: all|subdomains|live|ports|urls|js|vulns")
pflag.StringVar(&mode, "mod", "wide", "Mode: wide|narrow|fast|deep|osint")
pflag.BoolVar(&fast, "fst", false, "Shortcut for mode fast")
pflag.BoolVar(&narrow, "nrw", false, "Shortcut for mode narrow")
pflag.IntVar(&rate, "rte", 150, "Request rate hint")
pflag.IntVar(&threads, "thr", 30, "Worker threads")
pflag.IntVar(&limit, "lim", 50, "Output limit")
pflag.StringVar(&output, "out", "", "Output file")
pflag.BoolVar(&quiet, "qut", false, "Quiet output")
pflag.BoolVar(&showVersion, "ver", false, "Show version")
pflag.StringVar(&serveAddr, "adr", "127.0.0.1:8088", "Dashboard bind address")
pflag.StringVar(&storagePath, "str", "", "Storage root directory (default: ./storage)")
pflag.StringVar(&stages, "stg", "all", "Comma-separated stages: subdomains,http,ports,urls,vulns")
pflag.StringArrayVar(&setAPI, "sak", nil, "Set API key as name=value (repeatable). Use empty value to unset.")
pflag.BoolVar(&showAPI, "shk", false, "Show configured API keys (masked)")
pflag.BoolVar(&setup, "stp", false, "Show setup screen with tool installation status")
pflag.BoolVar(&installTools, "ins", false, "Install missing supported tools (Linux)")
pflag.StringVar(&profile, "prf", "balanced", "Workflow profile: passive|balanced|aggressive")
pflag.BoolVar(&guide, "gud", false, "Show first-principles workflow guide")
pflag.Parse()

if showVersion {
Expand Down Expand Up @@ -277,38 +278,38 @@ func printHelp() {
Usage:
macaron scan example.com
macaron status
macaron results -d example.com -w live
macaron serve --addr 127.0.0.1:8088
macaron results -dom example.com -wht live
macaron serve -adr 127.0.0.1:8088
macaron setup

Core flags:
-s, --scan TARGET Scan one or more targets
-F, --file FILE Read targets from file
--stdin Read targets from stdin
-m, --mode MODE wide|narrow|fast|deep|osint
-S, --status Show scan summaries
-R, --results Show scan details
-E, --export Export JSON
-L, --list-tools Show tool availability
--storage DIR Use custom storage root (default ./storage)
--stages LIST Choose stages: subdomains,http,ports,urls,vulns
--set-api k=v Save API keys to storage config.yaml
--show-api Show masked API keys
--setup Show setup screen with tool status
--install-tools Install missing supported tools (Linux)
--profile NAME passive|balanced|aggressive
--guide Show first-principles workflow guide
--serve Start browser dashboard
--version Show version`)
-scn TARGET Scan one or more targets
-fil FILE Read targets from file
-inp Read targets from stdin
-mod MODE wide|narrow|fast|deep|osint
-sts Show scan summaries
-res Show scan details
-exp Export JSON
-lst Show tool availability
-str DIR Use custom storage root (default ./storage)
-stg LIST Choose stages: subdomains,http,ports,urls,vulns
-sak k=v Save API keys to storage config.yaml
-shk Show masked API keys
-stp Show setup screen with tool status
-ins Install missing supported tools (Linux)
-prf NAME passive|balanced|aggressive
-gud Show first-principles workflow guide
-srv Start browser dashboard
-ver Show version`)
}

func normalizeLegacyArgs() {
for i, arg := range os.Args {
if arg == "-setup" {
os.Args[i] = "--setup"
os.Args[i] = "-stp"
}
if arg == "-install-tools" {
os.Args[i] = "--install-tools"
os.Args[i] = "-ins"
}
Comment on lines 306 to 313
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeLegacyArgs rewrites -setup/-install-tools to -stp/-ins (single-dash), which still requires a later normalizeCompactFlags pass to become parseable by pflag (otherwise -stp is treated as a shorthand cluster). Consider rewriting legacy args directly to the canonical long form (--stp / --ins) so parsing does not depend on normalization order or future refactors.

Copilot uses AI. Check for mistakes.
}
}
Expand All @@ -327,29 +328,95 @@ func normalizeCommandArgs() {
args = append(args, tok)
continue
}
args = append(args, "--scan", tok)
args = append(args, "--scn", tok)
}
if len(args) == 1 {
args = append(args, "--scan")
args = append(args, "--scn")
}
os.Args = args
case "status":
os.Args = append([]string{os.Args[0], "--status"}, rest...)
os.Args = append([]string{os.Args[0], "--sts"}, rest...)
case "results":
os.Args = append([]string{os.Args[0], "--results"}, rest...)
os.Args = append([]string{os.Args[0], "--res"}, rest...)
case "serve":
os.Args = append([]string{os.Args[0], "--serve"}, rest...)
os.Args = append([]string{os.Args[0], "--srv"}, rest...)
case "setup":
os.Args = append([]string{os.Args[0], "--setup"}, rest...)
os.Args = append([]string{os.Args[0], "--stp"}, rest...)
case "export":
os.Args = append([]string{os.Args[0], "--export"}, rest...)
os.Args = append([]string{os.Args[0], "--exp"}, rest...)
case "config":
os.Args = append([]string{os.Args[0], "--config"}, rest...)
os.Args = append([]string{os.Args[0], "--cfg"}, rest...)
case "guide":
os.Args = append([]string{os.Args[0], "--guide"}, rest...)
os.Args = append([]string{os.Args[0], "--gud"}, rest...)
}
}

func normalizeCompactFlags() {
flagMap := map[string]string{
"scan": "scn", "s": "scn", "scn": "scn",
"status": "sts", "S": "sts", "sts": "sts",
"results": "res", "R": "res", "res": "res",
"list-tools": "lst", "L": "lst", "lst": "lst",
"export": "exp", "E": "exp", "exp": "exp",
"config": "cfg", "C": "cfg", "cfg": "cfg",
"pipeline": "pip", "P": "pip", "pip": "pip",
"serve": "srv", "srv": "srv",
"file": "fil", "F": "fil", "fil": "fil",
"stdin": "inp", "inp": "inp",
"domain": "dom", "d": "dom", "dom": "dom",
"id": "sid", "sid": "sid",
"what": "wht", "w": "wht", "wht": "wht",
"mode": "mod", "m": "mod", "mod": "mod",
"fast": "fst", "f": "fst", "fst": "fst",
"narrow": "nrw", "n": "nrw", "nrw": "nrw",
"rate": "rte", "rte": "rte",
"threads": "thr", "thr": "thr",
"limit": "lim", "lim": "lim",
"output": "out", "o": "out", "out": "out",
"quiet": "qut", "q": "qut", "qut": "qut",
"version": "ver", "ver": "ver",
"addr": "adr", "adr": "adr",
"storage": "str", "str": "str",
"stages": "stg", "stg": "stg",
"set-api": "sak", "sak": "sak",
"show-api": "shk", "shk": "shk",
"setup": "stp", "stp": "stp",
"install-tools": "ins", "ins": "ins",
"profile": "prf", "prf": "prf",
"guide": "gud", "gud": "gud",
}
args := make([]string, 0, len(os.Args))
args = append(args, os.Args[0])
for _, arg := range os.Args[1:] {
if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") && len(arg) > 2 {
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeCompactFlags does not normalize legacy single-letter flags (e.g. -s, -d, -m, -f, -q) because the single-dash branch only runs when len(arg) > 2. This will break previously supported pflag shorthands after removing *VarP declarations, and it also prevents use of the legacy keys already present in flagMap ("s", "d", "m", etc.). Adjust the condition to include 2-char args (and keep the map lookup to avoid changing unrelated - prefixed args).

Suggested change
if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") && len(arg) > 2 {
if strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") && len(arg) >= 2 {

Copilot uses AI. Check for mistakes.
key := strings.TrimPrefix(arg, "-")
val := ""
if i := strings.IndexRune(key, '='); i >= 0 {
val = key[i:]
key = key[:i]
}
if mapped, ok := flagMap[key]; ok {
args = append(args, "--"+mapped+val)
continue
}
}
if strings.HasPrefix(arg, "--") {
key := strings.TrimPrefix(arg, "--")
val := ""
if i := strings.IndexRune(key, '='); i >= 0 {
val = key[i:]
key = key[:i]
}
if mapped, ok := flagMap[key]; ok {
args = append(args, "--"+mapped+val)
continue
}
}
args = append(args, arg)
}
os.Args = args
}

func applyProfile(profile string, mode *string, rate *int, threads *int, stages *string) {
switch strings.ToLower(strings.TrimSpace(profile)) {
case "passive":
Expand Down Expand Up @@ -382,21 +449,21 @@ func printGuide() {

1) Setup once:
macaron setup
macaron --install-tools
macaron --set-api securitytrails=YOUR_KEY
macaron -ins
macaron -sak securitytrails=YOUR_KEY

2) Run intentional scans:
macaron scan target.com --profile passive
macaron scan target.com --profile balanced
macaron scan target.com --profile aggressive --stages subdomains,http,ports,urls,vulns
macaron scan target.com -prf passive
macaron scan target.com -prf balanced
macaron scan target.com -prf aggressive -stg subdomains,http,ports,urls,vulns

3) Inspect and decide:
macaron status
macaron results -d target.com -w live
macaron results -dom target.com -wht live
macaron serve

4) Export/share:
macaron export -o target.json
macaron export -out target.json

Profiles:
passive low-noise, low-rate, mostly passive collection
Expand Down
39 changes: 36 additions & 3 deletions cmd/macaron/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,50 @@ func withArgs(args []string, fn func()) {
func TestNormalizeLegacySetup(t *testing.T) {
withArgs([]string{"macaron", "-setup"}, func() {
normalizeLegacyArgs()
if osArgs()[1] != "--setup" {
t.Fatalf("expected --setup, got %s", osArgs()[1])
if osArgs()[1] != "-stp" {
t.Fatalf("expected -stp, got %s", osArgs()[1])
}
})
}

func TestNormalizeCommandScan(t *testing.T) {
withArgs([]string{"macaron", "scan", "example.com", "--fast"}, func() {
normalizeCompactFlags()
normalizeCommandArgs()
args := osArgs()
want := []string{"macaron", "--scan", "example.com", "--fast"}
want := []string{"macaron", "--scn", "example.com", "--fst"}
if len(args) != len(want) {
t.Fatalf("unexpected len: %#v", args)
}
for i := range want {
if args[i] != want[i] {
t.Fatalf("idx %d: got %q want %q", i, args[i], want[i])
}
}
})
}

func TestNormalizeLongToCompact(t *testing.T) {
withArgs([]string{"macaron", "--scan", "example.com", "--threads", "20"}, func() {
normalizeCompactFlags()
args := osArgs()
want := []string{"macaron", "--scn", "example.com", "--thr", "20"}
if len(args) != len(want) {
t.Fatalf("unexpected len: %#v", args)
}
for i := range want {
if args[i] != want[i] {
t.Fatalf("idx %d: got %q want %q", i, args[i], want[i])
}
}
})
}

func TestNormalizeSingleDashCompact(t *testing.T) {
withArgs([]string{"macaron", "-stp", "-ver"}, func() {
normalizeCompactFlags()
args := osArgs()
want := []string{"macaron", "--stp", "--ver"}
Comment on lines 24 to +61
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normalization tests cover long flags (--scan, --threads) and 3-letter single-dash flags (-stp, -ver), but they don’t cover the legacy single-letter forms that flagMap claims to support (e.g. -s, -d, -m, -f, -q, -o). Adding at least one test for a 1-letter legacy flag would prevent regressions where shorthands stop being accepted.

Copilot uses AI. Check for mistakes.
if len(args) != len(want) {
t.Fatalf("unexpected len: %#v", args)
}
Expand Down
Loading