module Noir::CLI::ScanCommand

Overview

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

Defined in:

cli/commands/scan.cr

Constant Summary

PROMPT_OVERRIDE_SETTERS = {"override_filter_prompt" => ->(v : String) do LLM::PromptOverrides.filter_prompt = v end, "override_analyze_prompt" => ->(v : String) do LLM::PromptOverrides.analyze_prompt = v end, "override_bundle_analyze_prompt" => ->(v : String) do LLM::PromptOverrides.bundle_analyze_prompt = v end, "override_llm_optimize_prompt" => ->(v : String) do LLM::PromptOverrides.llm_optimize_prompt = v end}
TECH_FLAG_SPELLINGS = {"-t/--techs" => ["-t", "--techs"], "--only-techs" => ["--only-techs"], "--exclude-techs" => ["--exclude-techs"]}

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.

WARNING_COLOR = Colorize::Color256.new(208)

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 Method Summary

Class Method Detail

def self.cli_flag_names(argv : Array(String)) : Set(String) #

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.


[View source]
def self.glob_error(pattern : String) : String | Nil #

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.


[View source]
def self.host_error(host : String | Nil) : String | Nil #

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).


[View source]
def self.run(argv : Array(String)) #

[View source]
def self.scan_exit_code(app : NoirRunner, app_diff : NoirRunner | Nil) : Int32 #

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.


[View source]