Noir::CLI::ScanCommand
noir scan [paths...] [flags]
Discovers endpoints across one or more code bases. Positional paths
augment any -b PATH flags so both v0 and v1 invocation styles work:
noir scan ./app # v1 positional noir scan ./api ./worker # v1 multi-path positional noir -b ./app # v0 (router default-routes to scan) noir scan -b ./app --passive # v1 explicit + flags
Constants
Every spelling of each tech-selection flag. They all store a
comma-separated string whose "never passed" value is "" — the same
string --flag '' produces, and the same one the generated config
template ships — so the blank case can only be read off argv.
--only-techs '' was the sharpest of these: it asked to restrict the
scan and silently widened it back to everything, which is what a CI job
doing --only-techs "$TECHS" gets when $TECHS is unset.
ANSI 256-color orange used for the protocol-missing warning. Kept as a named constant so the call site reads as "warning color" rather than a bare magic number.
Class methods
Long-flag tokens present on the command line, with --flag=value
reduced to --flag. The only question asked of it is "did the user
type this flag?", which is why the values are dropped.
Detect + analyze with no report, no diff and no exit: the endpoint list
the query verbs filter. Deliver flags (--probe, --export-*) still run
inside analyze, exactly as on a scan.
Returns nil for a usable glob, otherwise the reason it is broken.
Both malformations are found by walking the pattern rather than by
asking File.match?. Crystal's matcher is lazy: it only raises
BadPatternError for an unterminated [ once it actually reaches
that [, and it stops at the first literal that does not match the
subject. Probing with one fixed string therefore reported an
unterminated character set for a[b (whose a happens to be a prefix
of nothing in particular) and passed z[b — the identical
malformation — straight through, where it then excluded nothing and
matched no file loudly enough to raise either. Which of the two a user
got depended on the first character of their pattern.
Brace groups have the same problem in the other direction: *.{rb is
accepted by the matcher and then never matches anything.
Inside a character class a brace is an ordinary character, and the
first character of a class is a literal member — []] is the class
containing ], exactly as Crystal's matcher reads it.
Why -u needs an authority, not just a scheme: it is the base URL
every discovered path is appended to. URI.parse is happy without a
host — -u http:// parses with an empty one — and the concatenation
then promotes the first discovered path segment to the authority, so
/a becomes http://a. With --probe or --status-codes that
fires real HTTP requests at a host the user never named.
Whitespace and control characters are rejected for the same reason
normalize_probe_via! rejects a missing host: -u "not a url" and
-u $'http://a\nb/' are accepted today and baked into every endpoint
of the JSON/OAS/Postman output as an unusable URL.
Returns nil when the host is usable, otherwise the reason (phrased to
slot into the -u/--url <reason> in <url> message).
Stage ARGV through OptionParser (positional path discovery happens
inside run_options_parser). Dup argv upfront because callers
commonly pass ARGV itself, which we are about to clear. The v0
deliver/probe flag tokens (--send-req, --use-matchers, etc.)
are rewritten to their v1 equivalents before the parser runs,
so the LEGACY surface never appears in scan -h and the parser
itself only needs to know about one set of names.
Returns the translated argv alongside the options: it, not the hash, is
what says which settings the user typed on this command line — by the
time the parser returns, a config-file value and a CLI flag are the same
entry in the same hash. The query verbs (trace/reach/query) share
this and prepare! so they accept every scan flag with scan's semantics.
Normalizes and validates parsed options in place; dies on the first problem. Everything a scan checks before it starts, minus the banner.
Exit code for a scan that produced its report.
Exit 2 rather than 1 so a CI gate can tell "scan ran, coverage incomplete" from the usage/validation errors that already exit 1.
degraded used to read !app.analyzer_failures.empty? when that list
held tech-level analyzer failures and nothing else, so --strict — whose
documented contract is "exit 2 if any analyzer failed or skipped a
file" — reported green on a scan that lost a whole subtree to an
unlistable directory, dropped every symlinked package, exported nothing,
or ran zero passive rules. Every one of those now feeds the same list, so
the test is unchanged and finally means what it says.