feat: major cleanup — remove dashboard, semantic CLI, banner, API management, uninstall, more tools#27
Conversation
…t, uninstall, more tools, new README Agent-Logs-Url: https://github.com/root-Manas/macaron/sessions/bd43ae69-d546-45ff-aed6-3450bdddd865 Co-authored-by: root-Manas <97402139+root-Manas@users.noreply.github.com>
…n validation, append bug, comments Agent-Logs-Url: https://github.com/root-Manas/macaron/sessions/bd43ae69-d546-45ff-aed6-3450bdddd865 Co-authored-by: root-Manas <97402139+root-Manas@users.noreply.github.com>
… buffer constant, recursive flattenAPIKeys comment, uppercase TLD support Agent-Logs-Url: https://github.com/root-Manas/macaron/sessions/bd43ae69-d546-45ff-aed6-3450bdddd865 Co-authored-by: root-Manas <97402139+root-Manas@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR is a major CLI- and pipeline-focused overhaul of macaron, removing the web dashboard/UI and replacing legacy flags with semantic subcommands while expanding tool support and introducing global API key management.
Changes:
- Removes the dashboard (
internal/ui) and related CLI surface area. - Reworks the CLI into subcommands (
scan/status/results/setup/export/config/api/uninstall) with a new banner + output helpers. - Expands pipeline/tooling (e.g., amass subdomains, naabu ports, gau/katana URLs) and adds API key import/bulk management with subfinder key injection.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Rewritten usage docs reflecting new subcommand-based CLI and pipeline/tool inventory. |
| internal/ui/server.go | Deletes the dashboard server implementation. |
| internal/ui/assets/index.html | Deletes dashboard frontend assets. |
| internal/engine/engine.go | Adds amass, naabu port scanning, gau/katana URL discovery, and subfinder API key injection. |
| internal/cliui/banner.go | Adds CLI banner + colored Info/OK/Warn/Err helpers with NO_COLOR support. |
| internal/cfg/config.go | Adds bulk load + import helpers for API keys and subfinder provider-config generation. |
| internal/app/app.go | Expands tool catalog/tool inventory output and updates status/setup rendering. |
| cmd/macaron/main.go | Replaces legacy flags with a subcommand dispatcher; adds api + uninstall flows. |
| cmd/macaron/main_test.go | Removes legacy arg-normalization tests; adds tests for profiles/domain detection/home override. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -46,6 +50,7 @@ func Save(storageRoot string, cfg *Config) error { | |||
| return os.WriteFile(filepath.Join(storageRoot, "config.yaml"), b, 0o644) | |||
There was a problem hiding this comment.
Save writes config.yaml with mode 0644, but this file contains API keys/secrets. This makes keys readable by other users on the system. Use a restrictive permission (e.g., 0600) when writing the config file (and ideally preserve permissions on updates).
| return os.WriteFile(filepath.Join(storageRoot, "config.yaml"), b, 0o644) | |
| path := filepath.Join(storageRoot, "config.yaml") | |
| mode := os.FileMode(0o600) | |
| if info, err := os.Stat(path); err == nil { | |
| mode = info.Mode().Perm() | |
| } else if !os.IsNotExist(err) { | |
| return err | |
| } | |
| return os.WriteFile(path, b, mode) |
| for _, candidate := range []string{ | ||
| filepath.Join(home, ".config", "amass", "config.yaml"), | ||
| filepath.Join(home, ".config", "amass", "config.ini"), | ||
| } { | ||
| b, err := os.ReadFile(candidate) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| // Best-effort: look for api_key or apikey patterns. | ||
| var raw map[string]any | ||
| if err := yaml.Unmarshal(b, &raw); err != nil { | ||
| continue | ||
| } |
There was a problem hiding this comment.
readAmassConfig attempts to parse ~/.config/amass/config.ini via yaml.Unmarshal, which will fail for INI format and silently skip importing keys from that file. Either parse INI properly (if you want to support it) or remove the .ini candidate to avoid a non-functional import path.
| for k, v := range bulk.APIKeys { | ||
| k = strings.ToLower(strings.TrimSpace(k)) | ||
| if k != "" && strings.TrimSpace(v) != "" { | ||
| cfg.APIKeys[k] = strings.TrimSpace(v) | ||
| } | ||
| } | ||
| return len(bulk.APIKeys), nil |
There was a problem hiding this comment.
BulkLoadFile returns len(bulk.APIKeys) even though empty/whitespace values are skipped during merge. This can report that N keys were loaded when fewer were actually applied. Consider counting only the keys that are actually inserted/updated in cfg.APIKeys and returning that count.
| for k, v := range bulk.APIKeys { | |
| k = strings.ToLower(strings.TrimSpace(k)) | |
| if k != "" && strings.TrimSpace(v) != "" { | |
| cfg.APIKeys[k] = strings.TrimSpace(v) | |
| } | |
| } | |
| return len(bulk.APIKeys), nil | |
| count := 0 | |
| for k, v := range bulk.APIKeys { | |
| k = strings.ToLower(strings.TrimSpace(k)) | |
| v = strings.TrimSpace(v) | |
| if k != "" && v != "" { | |
| cfg.APIKeys[k] = v | |
| count++ | |
| } | |
| } | |
| return count, nil |
| // writeSubfinderProviderConfig creates a temporary provider-config.yaml with | ||
| // macaron's API keys so subfinder uses them without changing the user's own | ||
| // subfinder config. Returns the temp file path, or "" if nothing to write. | ||
| // The caller is responsible for removing the returned file when done. | ||
| func writeSubfinderProviderConfig(apiKeys map[string]string) string { | ||
| if len(apiKeys) == 0 { | ||
| return "" | ||
| } | ||
| // macaron key name → subfinder provider name | ||
| mapping := map[string]string{ | ||
| "securitytrails": "securitytrails", | ||
| "virustotal": "virustotal", | ||
| "shodan": "shodan", | ||
| "binaryedge": "binaryedge", | ||
| "c99": "c99", | ||
| "chaos": "chaos", | ||
| "hunter": "hunter", | ||
| "intelx": "intelx", | ||
| "urlscan": "urlscan", | ||
| "zoomeye": "zoomeye", | ||
| "fullhunt": "fullhunt", | ||
| "github": "github", | ||
| "leakix": "leakix", | ||
| "netlas": "netlas", | ||
| "quake": "quake", | ||
| } | ||
| providers := make(map[string][]string) | ||
| for macaronKey, providerName := range mapping { | ||
| v, ok := apiKeys[macaronKey] | ||
| if !ok || strings.TrimSpace(v) == "" { | ||
| continue | ||
| } | ||
| providers[providerName] = append(providers[providerName], v) | ||
| } | ||
| if len(providers) == 0 { | ||
| return "" | ||
| } | ||
| tmp, err := os.CreateTemp("", "macaron_sfprov_*.yaml") | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| // Write YAML manually — keep it simple. | ||
| for provider, keys := range providers { | ||
| fmt.Fprintf(tmp, "%s:\n", provider) | ||
| for _, k := range keys { | ||
| fmt.Fprintf(tmp, " - %s\n", k) | ||
| } | ||
| } |
There was a problem hiding this comment.
writeSubfinderProviderConfig writes YAML manually and emits raw key strings without quoting/escaping. API keys often contain characters that can break YAML (e.g., :, #, leading/trailing spaces), which would make subfinder fail to read the provider config. Prefer generating the YAML via yaml.Marshal (or reuse the existing provider-config writer in internal/cfg) so values are correctly escaped and the provider mapping logic lives in one place.
| func runScan(args []string) int { | ||
| fs := pflag.NewFlagSet("scan", pflag.ContinueOnError) | ||
| var ( | ||
| scanTargets []string | ||
| status bool | ||
| results bool | ||
| listTools bool | ||
| export bool | ||
| configCmd bool | ||
| pipeline bool | ||
| serve bool | ||
| filePath string | ||
| useStdin bool | ||
| domain string | ||
| scanID string | ||
| what string | ||
| mode string | ||
| fast bool | ||
| narrow bool | ||
| rate int | ||
| threads int | ||
| limit int | ||
| output string | ||
| quiet bool | ||
| showVersion bool | ||
| serveAddr string | ||
| storagePath string | ||
| stages string | ||
| setAPI []string | ||
| showAPI bool | ||
| setup bool | ||
| installTools bool | ||
| profile string | ||
| guide bool | ||
| targets []string | ||
| file string | ||
| stdin bool | ||
| mode string | ||
| rate int | ||
| threads 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)") | ||
| fs.BoolVar(&stdin, "stdin", false, "Read targets from stdin") | ||
| fs.StringVarP(&mode, "mode", "m", "wide", "Scan mode: wide|narrow|fast|deep|osint") | ||
| fs.IntVar(&rate, "rate", 150, "Request rate hint") | ||
| fs.IntVar(&threads, "threads", 30, "Concurrent workers") | ||
| 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") | ||
| fs.StringVar(&storage, "storage", "", "Storage root (default: ./storage)") | ||
| _ = fs.Parse(args) | ||
|
|
||
| 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.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 { | ||
| fmt.Printf("macaronV2 %s (Go %s, stable)\n", version, runtime.Version()) | ||
| return 0 | ||
| // Positional args are also targets. | ||
| for _, a := range fs.Args() { | ||
| targets = append(targets, a) | ||
| } |
There was a problem hiding this comment.
All subcommands ignore flag parsing errors (_ = fs.Parse(args)). With ContinueOnError, this means unknown flags / missing values / --help can be silently ignored and execution continues with partial/default values (e.g., treating leftover args as targets). Capture and handle the parse error (including pflag.ErrHelp) and return a non-zero exit code where appropriate.
| removeStorage = readYes() | ||
| } | ||
| } else { | ||
| removeStorage = storage != "" |
There was a problem hiding this comment.
uninstall --yes sets removeStorage = storage != "", so running macaron uninstall --yes will never remove the default storage directory even though the interactive flow offers to remove it. This makes --yes behave differently than “answer yes to prompts”. Consider either adding an explicit --remove-storage flag or making --yes apply to both prompts (and keep --storage as the path selector).
| removeStorage = storage != "" | |
| removeStorage = home != "" |
| latest.txt | ||
| ``` | ||
|
|
||
| Override with `--storage /path/to/dir` or `MACARON_HOME` (env not yet supported — use the flag). |
There was a problem hiding this comment.
The README states MACARON_HOME is “not yet supported”, but macaronHome() already honors the MACARON_HOME environment variable. Update the docs to reflect current behavior (or remove env support if it’s not intended).
| Override with `--storage /path/to/dir` or `MACARON_HOME` (env not yet supported — use the flag). | |
| Override with `--storage /path/to/dir` or the `MACARON_HOME` environment variable. |
Summary
Complete overhaul of macaron as described in the issue. All changes build and all tests pass.
Changes
Removed
internal/ui/(server.go + HTML assets),servecommand, all related flags and referencesCLI / UX
internal/cliui/banner.gowith ASCII art,NO_COLORsupport, andInfo/OK/Warn/Errcolor helpers-scn,-sts,-res,-sak, etc.) with clear subcommands and readable flags:macaron scan -t <domain> -p <profile> --stages ...macaron status,results,setup,export,config,guide,versionmacaron api list|set|unset|import|bulkmacaron uninstallmacaron uninstall— locates binary viaos.Executable(), removes it from PATH, optionally removes storage directory (with confirmation prompt or--yesflag)API Management
macaron api set key=value [key=value ...]— set one or more keysmacaron api unset key [key ...]— remove keysmacaron api list— show masked keysmacaron api import— pull keys from installed tool configs (subfinder~/.config/subfinder/provider-config.yaml, amass config)macaron api bulk -f keys.yaml— load many keys at once from a YAML filePipeline improvements
Documentation
Testing
TestApplyProfileBalanced,TestLooksLikeDomain,TestMacaronHomeOverridego build ./...andgo test ./...pass cleanly