Top Level Namespace
Defined in:
Constant Summary
-
AI_CONTEXT_FEATURES =
NoirAIContext::ACCEPTED_FEATURES -
--ai-contextaccepts an optional comma-separated feature list. Crystal's OptionParser cannot express "optional positional value", so we rewrite the few well-defined ambiguous forms upfront:--ai-context → --ai-context= (bare, all features) --ai-context=guards,sinks → unchanged (explicit value) --ai-context guards,sinks → --ai-context=guards,sinks (heuristic) --ai-context ./app → --ai-context= (next token is a path)
The heuristic for "is the next token a feature list?": lowercase words joined by commas, where either (a) there's more than one comma-separated word, or (b) the single word matches the fixed vocabulary below exactly. A real filesystem path essentially never contains a literal comma, so any multi-word comma list — typo'd feature names included — is routed to
--ai-context=...and left for the vocabulary check inapply_ai_contextto reject with a precise "unknown feature" error, rather than silently falling through to "Base path does not exist:". A single bare word still has to match a known feature exactly, since that shape is genuinely ambiguous with a real one-word directory name ( noir scan --ai-context myappmust keep scanningmyapp). Both this and the flag's own validator used to spell the vocabulary out by hand, and both omittedsources. -
ALLOWED_HTTP_METHODS =
["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE", "CONNECT", "QUERY"] -
ANDROID_EMBEDDED_SERVER_MARKER =
/io\.ktor\.server\.|embeddedServer|\brouting\s*\{|\bfun\s+Route\.|org\.http4k\.routing|@RestController|@RequestMapping|@(?:Get|Post|Put|Delete|Patch)Mapping|\bRouterFunction\b/ -
Strong server-routing constructs. A
.kt/.javafile that sits inside an Android app's source set is normally scoped to mobile detectors only (an incidentalimport ...SpringApplicationmust not flag the project as a server). But an Android app can legitimately embed an on-device HTTP server — e.g. plain-app runs a local Ktor web server whose routes live underapp/src/main/java/.... When a file carries one of these markers it is a real server, so the Ktor / http4k / Spring detectors are allowed to run on it. Kept as a single precompiled constant — recompiling it per file would recreate the PCRE2 program on every read. -
ANDROID_SOURCE_SUBDIRS =
Set {"aidl", "assets", "cpp", "java", "jni", "kotlin", "res"} -
CFML_FRAMEWORK_TECHS =
Set {"cfml_taffy", "cfml_coldbox", "cfml_wheels", "cfml_fw1"} -
CFML frameworks that own their application's route table.
-
CLI_ENDPOINT_METHOD =
"CLI" -
The synthetic verb CLI entry points carry (
cli://<binary>/<subcommand>). -
DETECTOR_IGNORED_DIR_NAMES =
Set {".git", ".idea", ".vscode", ".claude", "node_modules", "vendor", "__pycache__", ".venv", "venv", ".pytest_cache", ".tox", ".gradle", ".bundle", ".dart_tool", ".cargo", ".terraform", ".zig-cache", "zig-cache", ".zig-out", "zig-out", "dist", "build", "target", "out", "tmp", ".cache", ".next", ".nuxt", ".svelte-kit", ".turbo", ".parcel-cache", ".serverless", ".expo", "coverage", ".coverage", "Pods", "__MACOSX"} -
DETECTOR_IGNORED_DIR_SUFFIXES =
Set {".xcassets"} -
DETECTOR_PATH_SEGMENT_PROBES =
["proj/grails-app/conf/application", "proj/supabase/migrations/001.sql", "proj/migrations/001.sql", "proj/supabase/config.toml", "proj/server/api/hello.js", "proj/server/routes/hello.js", "proj/pages/api/hello.js", "proj/app/api/hello/route.ts", "proj/routes/+server.ts", "proj/routes/index.dart", "proj/directus/snapshots/snap.json", "proj/wp-content/plugins/x.php", "proj/metadata/databases/tables.yaml", "proj/Magento/module.xml"] -
Paths that embed directory segments real detectors gate on (
/grails-app/,/migrations/,/server/api/, …). Used only to classify path-sensitive detectors — not as production file samples. -
DETECTOR_SPECIAL_BASENAMES =
Set {"package.json", "tsconfig.json", "composer.json", "composer.lock", "vercel.json", "now.json", "netlify.toml", "wrangler.toml", "Gemfile", "Gemfile.lock", "Package.swift", "Cargo.toml", "go.mod", "mix.exs", "pubspec.yaml", "pubspec.lock", "shard.yml", "shard.lock", "build.sbt", "pom.xml", "AndroidManifest.xml", "config.toml", "rebar.config", "erlang.mk", "project.clj", "deps.edn", "stack.yaml", "package.yaml", "gleam.toml", "manifest.toml", "paket.dependencies", "Caddyfile", "Dockerfile", "Makefile", "Rakefile", "serverless.yml", "serverless.yaml", "app.yaml", "openapi.yaml", "openapi.json", "swagger.json", "swagger.yaml"} -
Filenames that detectors match by exact basename (often with path constraints like "must sit at the project root"). These must not share an extension-only cache bucket — e.g.
vercel.jsonis not "any .json". -
ENDPOINT_METHODS =
(ALLOWED_HTTP_METHODS + SYNTHETIC_ENDPOINT_METHODS) + [CLI_ENDPOINT_METHOD] -
Every verb that can appear as
Endpoint#method. Consumers that have to tell "this token names a method" from "this token is part of a URL" (the probe matchers) read this instead of keeping their own list. -
INCLUDE_TARGETS =
{"path" => "include_path", "techs" => "include_techs", "callee" => "include_callee"} -
LEGACY_INCLUDE_TARGETS =
{"--include-path" => "include_path", "--include-techs" => "include_techs", "--include-callee" => "include_callee"} -
LEGACY_PVALUE_TARGETS =
{"--set-pvalue" => "set_pvalue", "--set-pvalue-header" => "set_pvalue_header", "--set-pvalue-cookie" => "set_pvalue_cookie", "--set-pvalue-query" => "set_pvalue_query", "--set-pvalue-form" => "set_pvalue_form", "--set-pvalue-json" => "set_pvalue_json", "--set-pvalue-path" => "set_pvalue_path"} -
Silently translate v0 flag spellings to their v1 storage. Keeping the work here (instead of
parser.on "--include-path") means the legacy names no longer clutternoir scan -h, while every v0 script keeps working untouched in v1.x. -
MOBILE_DETECTOR_NAMES =
Set {"android", "ios", "well_known_applinks"} -
PVALUE_TYPE_KEYS =
{"any" => "set_pvalue", "all" => "set_pvalue", "header" => "set_pvalue_header", "cookie" => "set_pvalue_cookie", "query" => "set_pvalue_query", "form" => "set_pvalue_form", "json" => "set_pvalue_json", "path" => "set_pvalue_path"} -
Split
TYPE=VAL(or bareVAL→ all-types) and route it into the right slot in noir_options.--pvaluemay be repeated to set multiple values across different types. -
SYNTHETIC_ANY_METHODS =
["ANY", "ALL", "*"] -
SYNTHETIC_ENDPOINT_METHODS =
["ANY", "PUBLISH", "SUBSCRIBE", "SEND", "RECEIVE"] -
Verbs an endpoint can carry that are not HTTP methods: the wildcard
ANY, and the AsyncAPI / messaging verbs the optimizer allow-lists so event-driven endpoints aren't downgraded toGET. -
WILDCARD_HTTP_METHODS =
["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "TRACE"]
Method Summary
- analysis_endpoints(options : Hash(String, YAML::Any), techs, logger : NoirLogger)
- any_to_bool(any) : Bool
-
apply_ai_context(noir_options : Hash(String, YAML::Any), spec : String)
--ai-context[=LIST]always enables AI context output. - apply_include_list(noir_options : Hash(String, YAML::Any), spec : String)
- banner(io : IO = STDERR)
-
build_detector_list(options : Hash(String, YAML::Any)) : Array(Detector)
The complete detector registry.
- detect_techs(base_paths : Array(String), options : Hash(String, YAML::Any), passive_scans : Array(PassiveScan), logger : NoirLogger)
- detector_add_android_source_prefixes_from_dir(dir : String, prefixes : Array(String))
- detector_android_source_file?(path : String, prefixes : Array(String)) : Bool
- detector_android_source_prefixes_for_manifest(manifest_path : String) : Array(String)
-
detector_build_applicable_lookup(detectors : Array(Detector)) : Proc(String, Array(Int32))
Build a lookup that turns a path into the list of detector indices whose
applicable?returns true — without re-walking every detector on every file. - detector_mobile_detector?(name : String) : Bool
-
detector_path_sensitive?(detector : Detector) : Bool
Whether
applicable?depends on more than the basename (root placement, directory segments, multi-hop path layout). - endpoint_method_token?(token : String) : Bool
-
escape_glob_path(path : String) : String
Escapes glob metacharacters in a path string.
- expand_synthetic_http_methods(method : String) : Array(String)
- extract_hidden_prompt_flags(noir_options : Hash(String, YAML::Any)) : Array(String)
- extract_legacy_aliases(args : Array(String), noir_options : Hash(String, YAML::Any)) : Array(String)
- filter_redundant_generic_techs(techs : Array(String)) : Array(String)
- generate_bash_completion_script
-
generate_elvish_completion_script
Native Elvish (https://elv.sh) completion.
- generate_fish_completion_script
- generate_zsh_completion_script
- get_allowed_methods
- get_home
- get_relative_path(base_path : String, path : String) : String
- get_symbol(method : String)
- handle_pvalue(noir_options : Hash(String, YAML::Any), spec : String)
- initialize_analyzers(logger : NoirLogger)
- join_path(*segments : String) : String
- join_paths(*paths : String) : String
-
json_any?(content : String) : JSON::Any | Nil
Strict
JSON.parse-or-nil. - normalize_ai_context_flag(args : Array(String)) : Array(String)
-
parse_yaml(content : String) : YAML::Any
Parses YAML, recovering from a stray-tab failure that libyaml (Crystal's YAML backend) is stricter about than most other parsers.
-
regex_matches_with_timeout?(regex : Regex, input : String, timeout : Time::Span = 500.milliseconds) : Bool
Safely checks if a regex matches a string within a given timeout.
- remove_start_slash(input_path : String) : String
- requestable_http_methods(method : String) : Array(String)
- run_options_parser
- synthetic_any_method?(method : String) : Bool
- valid_json?(content : String) : Bool
- valid_yaml?(content : String) : Bool
-
validate_ai_context_features(spec : String, origin : String)
Rejects AI-context bucket names outside the accepted vocabulary.
-
yaml_any?(content : String) : YAML::Any | Nil
Strict
YAML.parse-or-nil.
Method Detail
--ai-context[=LIST] always enables AI context output. An empty LIST
means "every category"; a non-empty LIST narrows the output to the
named categories.
The complete detector registry.
Derived from the classes themselves rather than a hand-maintained list.
Every detector under the Detector:: namespace is registered by existing;
there is no second place to add it to and therefore no way to write a
detector that silently never runs. That was a real failure mode — it is
how zap_sites_tree ended up detectable but unnameable (see
spec/unit_test/techs/registry_integrity_spec.cr, which still checks the
remaining hand-maintained lists it cannot yet be derived from).
The Detector:: filter is the production contract. crystal spec
compiles the suite into one binary, so all_subclasses also sees the
throwaway subclasses spec/unit_test/detector/detector_for_spec.cr
defines to exercise the base class; those must never join a real scan.
Sorted by class name so the order is a property of the source, not of
whatever sequence require "./detectors/**" happened to produce. Order is
not load-bearing — techs is accumulated concurrently across worker
fibers and the optimizer sorts endpoints before output — but a stable
order keeps debug logs and detector_list indices comparable between
runs.
Build a lookup that turns a path into the list of detector indices
whose applicable? returns true — without re-walking every detector
on every file. Most detectors only inspect extension / basename, so
their answers are memoized by basename. Detectors that look at path
segments or root placement are classified as path-sensitive and
always evaluated against the real path.
Whether applicable? depends on more than the basename (root
placement, directory segments, multi-hop path layout). Those
detectors must always see the real path in the hot loop.
Escapes glob metacharacters in a path string.
This is necessary when the path contains characters like { } [ ] * ?
which would otherwise be interpreted as glob patterns.
Example: "/path/{{cookiecutter}}/file" -> "/path/\{\{cookiecutter\}\}/file"
Native Elvish (https://elv.sh) completion. Wires the noir verb
surface into $edit:completion:arg-completer so noir <Tab> lists
the subcommands, noir <verb> <Tab> lists that verb's sub-actions,
and noir scan <Tab> falls back to filesystem path completion.
Install:
noir completion elvish > ~/.config/elvish/lib/noir.elv
then add use noir to ~/.config/elvish/rc.elv.
Strict JSON.parse-or-nil. Replaces the valid_json?(content) +
JSON.parse(content) idiom in detectors, which parsed the same
content twice per file.
Parses YAML, recovering from a stray-tab failure that libyaml (Crystal's YAML backend) is stricter about than most other parsers.
Real-world OpenAPI/Swagger documents occasionally carry a TAB character on an otherwise-blank line inside a block scalar (descriptions, examples, embedded code). libyaml rejects it with "found a tab character where an indentation space is expected", which drops the entire document — and with it every endpoint noir would have found — even though PyYAML, JS, and Go parsers accept it. As a last resort we blank out lines that consist solely of whitespace and retry. That transformation never touches real indentation, keys, or values, so a document that already parses is returned unchanged.
Safely checks if a regex matches a string within a given timeout. This helps mitigate ReDoS (Regular Expression Denial of Service) attacks.
Rejects AI-context bucket names outside the accepted vocabulary. origin
names the source in the error so a config-file typo doesn't read as a
command-line one.
Strict YAML.parse-or-nil. Replaces the valid_yaml?(content) +
YAML.parse(content) idiom in detectors, which paid for two full
libyaml passes over the same content per file.