Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The scan engine always ends with exactly one done event, marked `Stopped` when the scan ended before testing any candidate (out of scope, recursion ceiling, failed preflight or wildcard check, a wildcard zone without `-force`, or Ctrl+C during those checks), so the CLI, the TUI and `pkg/subenum` handle one shape of ending instead of a channel that sometimes closes without one. `pkg/subenum`'s `Event` gains `Stopped`, and a stopped scan's `Stats` now include the queries its checks sent (#99).

### Fixed
- `-print-config` shows the `attempts` value a scan actually uses: `1` by default instead of the raw `0` sentinel, and the `-retries` value (marked `from retries`) when that is what wins (#153).
- `tools/wordlist-gen` parses flags that appear after a positional the same way `subenum` does, instead of silently ignoring both: a leftover argument is now a usage error (2) that points at `-domain`, and an invalid `-combine` value or an empty `-o` exits 2 instead of 1 (#120).

## [0.9.0] - 2026-10-06
Expand Down
20 changes: 15 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,17 @@ func scanTarget(ctx context.Context, f cliFlags, domain string, entries []string
// an env value over the config file); both at the same level is an error
// (#116).
func resolveAttempts(attempts, retries int, attemptsSrc, retriesSrc string) (int, error) {
n, fromRetries, err := effectiveAttempts(attempts, retries, attemptsSrc, retriesSrc)
if err == nil && fromRetries {
fmt.Fprintln(os.Stderr, "Warning: -retries is deprecated, use -attempts instead")
}
return n, err
}

// effectiveAttempts is resolveAttempts without the deprecation warning. It
// also reports whether the value came from -retries, so -print-config can
// show the value the scanner will actually use.
func effectiveAttempts(attempts, retries int, attemptsSrc, retriesSrc string) (n int, fromRetries bool, err error) {
attemptsSet := attempts != 0
retriesSet := retries != 0
if attemptsSet && retriesSet {
Expand All @@ -1183,13 +1194,12 @@ func resolveAttempts(attempts, retries int, attemptsSrc, retriesSrc string) (int

switch {
case attemptsSet && retriesSet:
return 0, fmt.Errorf("cannot use both -attempts and -retries; use -attempts only")
return 0, false, fmt.Errorf("cannot use both -attempts and -retries; use -attempts only")
case retriesSet:
fmt.Fprintln(os.Stderr, "Warning: -retries is deprecated, use -attempts instead")
return retries, nil
return retries, true, nil
case attemptsSet:
return attempts, nil
return attempts, false, nil
default:
return 1, nil
return 1, false, nil
}
}
31 changes: 30 additions & 1 deletion main_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)

Expand Down Expand Up @@ -212,10 +213,38 @@ func (d *defaultsLoader) printConfig(w io.Writer, fset *flag.FlagSet) {
if given[fl.Name] {
source = sourceFlag
}
_, _ = fmt.Fprintf(w, "%-14s = %-24q (%s)\n", fl.Name, fl.Value.String(), source)
value := fl.Value.String()
if fl.Name == "attempts" {
value, source = d.effectiveAttemptsSetting(fset, value, source)
}
_, _ = fmt.Fprintf(w, "%-14s = %-24q (%s)\n", fl.Name, value, source)
})
}

// effectiveAttemptsSetting resolves -attempts the way a scan does: 0 means
// "unset", so the effective value is 1 or whatever -retries supplies. When
// the two conflict the raw value is kept, since a scan would refuse to run.
func (d *defaultsLoader) effectiveAttemptsSetting(fset *flag.FlagSet, value, source string) (string, string) {
retriesFlag := fset.Lookup("retries")
if retriesFlag == nil {
return value, source
}
attempts, err1 := strconv.Atoi(value)
retries, err2 := strconv.Atoi(retriesFlag.Value.String())
if err1 != nil || err2 != nil {
return value, source
}
retriesSrc := d.source(fset, "retries")
n, fromRetries, err := effectiveAttempts(attempts, retries, source, retriesSrc)
if err != nil {
return value, source
}
if fromRetries {
source = retriesSrc + ", from retries"
}
return strconv.Itoa(n), source
}

// settings returns the effective value of every setting that came from the
// environment or the config file, keyed by flag name, for the TUI's form.
func (d *defaultsLoader) settings(fs *flag.FlagSet) map[string]string {
Expand Down
27 changes: 27 additions & 0 deletions main_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,30 @@ func TestFileFlagsAreComplete(t *testing.T) {
}
}
}

// -print-config shows the attempts value a scan will use, not the raw 0
// sentinel (#153).
func TestPrintConfigEffectiveAttempts(t *testing.T) {
for _, tc := range []struct {
args []string
env map[string]string
want string
}{
{nil, nil, `attempts = "1" (default)`},
{[]string{"-attempts", "4"}, nil, `attempts = "4" (flag)`},
{[]string{"-retries", "3"}, nil, `attempts = "3" (flag, from retries)`},
{nil, map[string]string{"SUBENUM_RETRIES": "2"}, `attempts = "2" (env, from retries)`},
{[]string{"-attempts", "5"}, map[string]string{"SUBENUM_RETRIES": "2"}, `attempts = "5" (flag)`},
} {
d := &defaultsLoader{getenv: func(k string) string { return tc.env[k] }}
_, _, fs, err := parseFlagsWith(append(tc.args, "example.com"), d.apply)
if err != nil {
t.Fatal(err)
}
var out bytes.Buffer
d.printConfig(&out, fs)
if !strings.Contains(out.String(), tc.want) {
t.Errorf("%v %v: -print-config lacks %q:\n%s", tc.args, tc.env, tc.want, out.String())
}
}
}