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

File.match? parses the pattern itself, so character-class errors come back from Crystal with its own wording. Brace groups it accepts and then never matches (*.{rb matches nothing at all), so the {/} balance is checked here — skipping over character classes, inside which a brace is an ordinary character.


[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]