module

Noir::CliValidation

Constants

MAX_CONCURRENCY = 256

Upper bound for --concurrency. A user asking for hundreds of thousands of fibers doesn't get more throughput — it just exhausts file descriptors / sockets and can take the process down. Cap at a value far above any useful setting and warn rather than silently honoring it.

Class methods

exit_with_error(message : String) : NoReturn
Source
validate!(options : Hash(String, YAML::Any))
Source
validate_acp_target!(provider : String)

Pre-flight rejection of an ACP target Noir will not exec, so an untrusted --ai-provider "acp:<cmd>" fails cleanly before a scan starts rather than raising from the exec sink partway through one. The list itself lives in LLM::ACPTargets and is shared with that sink — it used to be a second byte-identical literal here, kept in sync by a comment.

Source
validate_ai_integer_options!(options : Hash(String, YAML::Any))

--ai-max-token / --ai-agent-max-steps are bounded by positive_int_or_die! on the CLI side. Their config-file twins had no bound at all, so a negative or zero step budget went straight into the agent loop. ConfigInitializer has already coerced the value to an Int by this point; all that's left is the range.

Zero is meaningful for ai_max_token only — it is the shipped default and the generated template's own value, documented as "no limit" — so it is accepted here even though --ai-max-token 0 is not.

Source
validate_ai_native_tools_allowlist!(options : Hash(String, YAML::Any))

--ai-native-tools-allowlist entries that don't canonicalize to a provider Noir supports for native tool-calling will never match and the feature just stays off. Warn so a typo like opena is visible instead of silently disabling tool-calling.

Source
validate_ai_provider_pair!(options : Hash(String, YAML::Any))

--ai-provider and --ai-model need each other to take effect. Either flag set on its own silently skips the AI analyzer:

  • --ai-provider openai (no model) — ai_provider_active? is false, AI never runs, user just gets a plain scan
  • --ai-model gpt-4 (no provider) — same outcome The ACP family is the one exception: acp:claude / acp:codex carry their own default model, so no --ai-model is required.
Source
validate_base_paths!(options : Hash(String, YAML::Any))
Source
validate_concurrency!(options : Hash(String, YAML::Any))
Source
validate_config_file!(options : Hash(String, YAML::Any))

--config-file PATH needs to exist, be a file (not a directory), and parse as a YAML mapping. The previous shape let File.read / YAML.parse raise straight through to the user, producing a Crystal stack trace for what should be a one-line "wrong path" message.

Source
validate_output_format!(options : Hash(String, YAML::Any))
Source
validate_output_path!(options : Hash(String, YAML::Any))
Source
validate_passive_scan_paths!(options : Hash(String, YAML::Any))

--passive-scan-path PATH accepts multiple entries (repeatable), and each must exist + be a directory — NoirPassiveScan.load_rules walks PATH/**/*.{yml,yaml} and Dir.glob silently returns zero matches for non-existent paths. The result is a passive scan that ran with 0 rules and surfaced 0 findings, with no indication that the path was bogus. Surface the typo at CLI parse time instead.

Source
validate_passive_scan_severity!(options : Hash(String, YAML::Any))

--passive-scan-severity is checked by its own flag handler in options.cr, but the identical passive_scan_severity: config key reached PassiveScanSeverity untouched — and meets_threshold? treats an unrecognized threshold as "include everything" (return true if min_level.nil?). So a typo in the config file didn't loosen the filter by one level, it removed it, silently, in the direction the user least wanted. Gate both sources here, in the one place config and CLI meet.

Source
validate_tagger_names!(options : Hash(String, YAML::Any))
Source
validate_tech_names!(options : Hash(String, YAML::Any))

-t/--techs, --only-techs, --exclude-techs should reject unknown tech names eagerly. Pre-fix, a typo like --only-techs falsk (instead of flask) silently dropped through NoirTechs.similar_to_tech returning "" and the scan produced zero endpoints without explanation — indistinguishable from "no flask code found here". Surfacing the typo at CLI parse time saves the surprise.

Source
warn_about_contradictory_probe_filters(options : Hash(String, YAML::Any))

A value listed in both --probe-match and --probe-skip is contradictory: skip wins in the Deliver pipeline, so the matcher is silently defeated. Surface the overlap instead of quietly filtering it out.

Source
warn_about_unused_delivery_flags(options : Hash(String, YAML::Any))

--probe-match and --probe-skip only run inside the Deliver pipeline (the code that ships endpoints to --probe / --probe-via / --export-es / --export-webhook). With no delivery target configured they silently no-op — a real surprise for users who set the flags expecting stdout output to be filtered. Warn at CLI parse time so the gap is obvious.

Source

Nested types