You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: finalize structured output only on success; docs facelift + em-dash normalize
Code:
- main.go: move out.Finish() off defer to just before the success "return 0".
Previously the deferred finalizer ran on every exit path, so an early error
(e.g. wildcard detection without -force) emitted an empty JSON array in
-format json. Text mode is unchanged (Finish is a no-op for text). The file
flush/close defer still runs after Finish, so buffered JSON/CSV is persisted
before the file closes.
- internal/output: add TestWriterJSONFinishGatesOutput documenting that
structured output is emitted only by Finish (no Finish = no spurious array).
Docs:
- docs/index.md: refreshed to the 0.6.0 feature set with cards for Output
Formats (-format), Rate Limiting (-rate), Record Types (-type), and Recursive
Enumeration (-recursive/-depth), matching the README Feature Matrix.
- Normalized em dashes to hyphens across docs/ (index, ARCHITECTURE,
DEVELOPER_GUIDE, _config.yml, default.html). Arrows and code identifiers
left intact; changelog history untouched.
Logged under CHANGELOG [Unreleased].
Co-authored-by: Cursor <cursoragent@cursor.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+9Lines changed: 9 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
8
+
## [Unreleased]
9
+
10
+
### Fixed
11
+
- Structured output (`-format json` and `-format csv`) is now finalized only on the successful scan path. The finalizer previously ran via `defer` on every exit, so an early error (such as wildcard detection without `-force`) emitted an empty JSON array. Text output behavior is unchanged.
12
+
13
+
### Docs
14
+
- GitHub Pages landing page (`docs/index.md`) refreshed to the 0.6.0 feature set, adding cards for Output Formats (`-format`), Rate Limiting (`-rate`), Record Types (`-type`), and Recursive Enumeration (`-recursive`/`-depth`).
15
+
- Normalized em dashes to hyphens across `docs/` for consistency with the no-em-dash convention.
* Function: `dns.Resolve(ctx, domain, timeout, dnsServer) ([]Record, time.Duration, error)` and `dns.ResolveTypes(..., types)` - perform the lookups and return typed `Record{Type, Value}` results. `ResolveTypes` issues per-type lookups (`LookupIP` ip4/ip6 for A/AAAA, `LookupCNAME` for CNAME) and filters to the requested types (default A,AAAA via `-type`).
72
72
* Function: `dns.ResolveDomain(ctx, domain, timeout, dnsServer, verbose) bool` - convenience wrapper returning a boolean, used by wildcard detection.
73
73
* Function: `dns.ResolveDomainWithRetry(ctx, domain, timeout, dnsServer, verbose, maxAttempts, types) ([]Record, bool)` - wraps the lookup with configurable retry logic and linear backoff between attempts, returning the resolved records.
74
-
* Function: `dns.CheckWildcard(ctx, domain, timeout, dnsServer) (bool, error)`— resolves two random subdomains to detect wildcard DNS records.
74
+
* Function: `dns.CheckWildcard(ctx, domain, timeout, dnsServer) (bool, error)`- resolves two random subdomains to detect wildcard DNS records.
75
75
*`net.Resolver{}`: A custom DNS resolver is configured.
76
76
*`PreferGo: true`: Instructs the resolver to use the pure Go DNS client.
77
77
*`Dial func(ctx context.Context, network, address string) (net.Conn, error)`: A custom dial function is provided to control the connection to the DNS server, using the user-specified `dnsServer` address.
***Purpose**: To efficiently perform DNS lookups for a large number of potential subdomains, `subenum` employs a worker pool pattern. This allows multiple DNS queries to be in flight concurrently, significantly speeding up the enumeration process compared to sequential lookups.
87
87
***Implementation**: The worker pool logic lives in `internal/scan/runner.go` as `scan.Run(ctx, cfg, events)`. Both the CLI (`run()` in `main.go`) and the TUI (`internal/tui`) call this function.
88
88
***`scan.Config`**: A struct carrying all scan parameters (domain, entries slice, concurrency, timeout, DNS server, simulate flag, etc.).
89
-
***`scan.Event` / `scan.EventKind`**: Typed events emitted on a `chan<- scan.Event`—`EventResult`, `EventProgress`, `EventWildcard`, `EventError`, `EventDone`.
89
+
***`scan.Event` / `scan.EventKind`**: Typed events emitted on a `chan<- scan.Event`-`EventResult`, `EventProgress`, `EventWildcard`, `EventError`, `EventDone`.
90
90
***Dispatcher and work queue**: A dispatcher goroutine owns an internal `jobs` channel, the queue of pending `job{domain, depth}` items, a visited set, and a pending-work counter. It seeds the queue from the wordlist slice and feeds workers. Workers submit newly discovered children back to the dispatcher over an `enqueue` channel and signal each finished job over a `completed` channel. The dispatcher closes `jobs` only when the pending counter reaches zero (or the context is cancelled). This lifecycle lets resolved subdomains enqueue children safely (recursive mode) without risking a send on a closed channel.
91
91
***`var wg sync.WaitGroup`**: A `sync.WaitGroup` waits for all worker goroutines to finish.
92
92
***Worker Goroutines Loop**: `cfg.Concurrency` goroutines are launched. Each reads a job from `jobs`, constructs nothing further (the job already holds the full domain), and calls `dns.ResolveDomainWithRetry()` (or `dns.SimulateResolve()` in simulate mode).
*`output.Writer` struct with mutex-protected methods:
104
104
*`Result(domain, records)` - in `text` format prints `Found: <domain>` to stdout (and the output file if configured); in `json` format buffers `{"subdomain", "records"}` objects and writes a single array at completion; in `csv` format streams `subdomain,type,value` rows with a header. The format is selected with `-format text|json|csv` (default `text`, which is byte-for-byte identical to prior behavior). The JSON array is buffered because it is a single document and does not stream; JSONL would be the streaming-friendly alternative if needed. Output formats are CLI-only for now (TUI-pending).
105
-
*`Progress(pct, processed, total, found)`— writes a carriage-return progress line to stderr.
106
-
*`Info(format, args...)`— writes an informational line to stderr.
107
-
*`Error(format, args...)`— writes an error line to stderr.
105
+
*`Progress(pct, processed, total, found)`- writes a carriage-return progress line to stderr.
106
+
*`Info(format, args...)`- writes an informational line to stderr.
107
+
*`Error(format, args...)`- writes an error line to stderr.
108
108
***Verbose Output** (when `-v` flag is enabled):
109
-
* Configuration summary, per-query DNS resolution info, and final scan statistics — all via `Info` to stderr.
109
+
* Configuration summary, per-query DNS resolution info, and final scan statistics - all via `Info` to stderr.
110
110
***Progress Reporting** (when `-progress` flag is enabled):
111
111
* A dedicated goroutine using a 1-second ticker calls `Progress` on stderr.
112
112
***Interactions**: All components route output through the `Writer`. Since results are the only thing on stdout, piping (`| cut -d' ' -f2`) works without `-progress=false`.
***Purpose**: Remember the last-used TUI form values across sessions so users don't have to re-type domain, wordlist path, and scan parameters every time.
131
131
***Implementation**:
132
132
*`savedConfig` struct mirrors `formValues` with JSON tags.
133
-
*`configPath()`— returns `os.UserConfigDir()/subenum/last.json` (e.g. `~/.config/subenum/last.json` on Linux/macOS, `%AppData%\subenum\last.json` on Windows).
134
-
*`saveConfig(fv formValues) error`— marshals `formValues` to JSON and writes it atomically with `os.WriteFile`. Called in `beginScan()` immediately before launching the scan goroutine. Errors are silently discarded so a write failure never blocks the scan.
135
-
*`loadSavedConfig() (savedConfig, bool)`— reads and unmarshals the file. Returns `false` if the file doesn't exist or is unreadable, causing `newFormModel` to fall back to hardcoded defaults.
133
+
*`configPath()`- returns `os.UserConfigDir()/subenum/last.json` (e.g. `~/.config/subenum/last.json` on Linux/macOS, `%AppData%\subenum\last.json` on Windows).
134
+
*`saveConfig(fv formValues) error`- marshals `formValues` to JSON and writes it atomically with `os.WriteFile`. Called in `beginScan()` immediately before launching the scan goroutine. Errors are silently discarded so a write failure never blocks the scan.
135
+
*`loadSavedConfig() (savedConfig, bool)`- reads and unmarshals the file. Returns `false` if the file doesn't exist or is unreadable, causing `newFormModel` to fall back to hardcoded defaults.
136
136
***Interactions**: `tui.New()` calls `loadSavedConfig()` on startup and passes the result to `newFormModel`. The `r` keybind (new scan) also calls `loadSavedConfig()` so the form is pre-filled with the values from the scan that just completed.
@@ -258,7 +258,7 @@ If you need to add a further dependency:
258
258
259
259
Areas for potential enhancement include:
260
260
261
-
* **Terminal UI**: An interactive TUI (`-tui` flag) built with Bubble Tea. Provides a form-based config screen and a live-scrolling results view — no arguments required to launch. Last-used values persist to `~/.config/subenum/last.json` across sessions.
261
+
* **Terminal UI**: An interactive TUI (`-tui` flag) built with Bubble Tea. Provides a form-based config screen and a live-scrolling results view - no arguments required to launch. Last-used values persist to `~/.config/subenum/last.json` across sessions.
262
262
* **Output Formats**: Supporting different output formats (JSON, CSV) in addition to the current plain text output file (`-o`).
263
263
* **Result Filtering**: Allowing users to filter results based on DNS record types.
264
264
* **Recursive Enumeration**: Adding support for recursive subdomain enumeration (e.g., finding subdomains of discovered subdomains).
Copy file name to clipboardExpand all lines: docs/index.md
+20-4Lines changed: 20 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,12 +9,12 @@ title: Home
9
9
10
10
## What it does
11
11
12
-
subenum brute-forces subdomains by resolving a wordlist against a target domain using a concurrent worker pool. Results stream to stdout — pipe-clean, no noise. Everything else (progress, diagnostics, errors) goes to stderr.
12
+
subenum brute-forces subdomains by resolving a wordlist against a target domain using a concurrent worker pool. Results stream to stdout - pipe-clean, no noise. Everything else (progress, diagnostics, errors) goes to stderr.
<figcaption>Interactive TUI - launch with <code>./subenum -tui</code> or <code>make tui</code></figcaption>
18
18
</figure>
19
19
</div>
20
20
@@ -37,7 +37,23 @@ subenum brute-forces subdomains by resolving a wordlist against a target domain
37
37
</div>
38
38
<divclass="feature-card">
39
39
<strong>Simulation Mode</strong>
40
-
<span>Generate synthetic DNS results at a configurable hit rate — zero network I/O. Safe for demos and testing.</span>
40
+
<span>Generate synthetic DNS results at a configurable hit rate - zero network I/O. Safe for demos and testing.</span>
41
+
</div>
42
+
<divclass="feature-card">
43
+
<strong>Output Formats</strong>
44
+
<span>Emit results as <code>text</code>, <code>json</code> (array of subdomain plus typed records), or <code>csv</code> via <code>-format</code>.</span>
45
+
</div>
46
+
<divclass="feature-card">
47
+
<strong>Rate Limiting</strong>
48
+
<span>Cap total DNS queries per second across the worker pool with <code>-rate</code> (context-aware, stays responsive to Ctrl+C).</span>
49
+
</div>
50
+
<divclass="feature-card">
51
+
<strong>Record Types</strong>
52
+
<span>Look up and filter by <code>A</code>, <code>AAAA</code>, or <code>CNAME</code> records with <code>-type</code>.</span>
53
+
</div>
54
+
<divclass="feature-card">
55
+
<strong>Recursive Enumeration</strong>
56
+
<span>Enumerate subdomains of discovered subdomains with <code>-recursive</code> and a <code>-depth</code> cap, with loop and duplicate protection.</span>
0 commit comments