module Noir::CliValidation

Defined in:

cli_validation.cr

Constant Summary

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

Class Method Detail

def self.exit_with_error(message : String) : NoReturn #

[View source]
def self.validate!(options : Hash(String, YAML::Any)) #

[View source]
def self.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.


[View source]
def self.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.


[View source]
def self.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.


[View source]
def self.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.

[View source]
def self.validate_base_paths!(options : Hash(String, YAML::Any)) #

[View source]
def self.validate_concurrency!(options : Hash(String, YAML::Any)) #

[View source]
def self.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.


[View source]
def self.validate_output_format!(options : Hash(String, YAML::Any)) #

[View source]
def self.validate_output_path!(options : Hash(String, YAML::Any)) #

[View source]
def self.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.


[View source]
def self.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.


[View source]
def self.validate_tagger_names!(options : Hash(String, YAML::Any)) #

[View source]
def self.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.


[View source]
def self.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.


[View source]
def self.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.


[View source]