Top Level Namespace

Defined in:

Constant Summary

AI_CONTEXT_FEATURES = ["guards", "sinks", "validators", "signals", "callee", "all"]

--ai-context accepts 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?" is intentionally tight: lowercase comma-separated words drawn from a fixed vocabulary. This keeps noir scan --ai-context ./app working as a positional path.

AI_CONTEXT_VALID_FEATURES = {"guards", "sinks", "validators", "signals", "callee", "all"}

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

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 / .java file that sits inside an Android app's source set is normally scoped to mobile detectors only (an incidental import ...SpringApplication must 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 under app/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"}
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"}
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 clutter noir 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 bare VAL → all-types) and route it into the right slot in noir_options. --pvalue may be repeated to set multiple values across different types.

Method Summary

Macro Summary

Method Detail

def analysis_endpoints(options : Hash(String, YAML::Any), techs, logger : NoirLogger) #

[View source]
def any_to_bool(any) : Bool #

[View source]
def apply_ai_context(noir_options : Hash(String, YAML::Any), spec : String) #

[View source]
def apply_include_list(noir_options : Hash(String, YAML::Any), spec : String) #

[View source]
def banner(io : IO = STDERR) #

[View source]
def detect_techs(base_paths : Array(String), options : Hash(String, YAML::Any), passive_scans : Array(PassiveScan), logger : NoirLogger) #

[View source]
def detector_add_android_source_prefixes_from_dir(dir : String, prefixes : Array(String)) #

[View source]
def detector_android_source_file?(path : String, prefixes : Array(String)) : Bool #

[View source]
def detector_android_source_prefixes_for_manifest(manifest_path : String) : Array(String) #

[View source]
def detector_mobile_detector?(name : String) : Bool #

[View source]
def escape_glob_path(path : String) : String #

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"


[View source]
def extract_hidden_prompt_flags(noir_options : Hash(String, YAML::Any)) : Array(String) #

[View source]
def extract_legacy_aliases(args : Array(String), noir_options : Hash(String, YAML::Any)) : Array(String) #

[View source]
def filter_redundant_generic_techs(techs : Array(String)) : Array(String) #

[View source]
def generate_bash_completion_script #

[View source]
def generate_elvish_completion_script #

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.


[View source]
def generate_fish_completion_script #

[View source]
def generate_zsh_completion_script #

[View source]
def get_allowed_methods #

[View source]
def get_home #

[View source]
def get_relative_path(base_path : String, path : String) : String #

[View source]
def get_symbol(method : String) #

[View source]
def handle_pvalue(noir_options : Hash(String, YAML::Any), spec : String) #

[View source]
def initialize_analyzers(logger : NoirLogger) #

[View source]
def join_path(*segments : String) : String #

[View source]
def join_paths(*paths : String) : String #

[View source]
def normalize_ai_context_flag(args : Array(String)) : Array(String) #

[View source]
def parse_body(body : String, params : Array(Param)) #

[View source]
def parse_params(query : String | Nil, params : Array(Param), type : String) #

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

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.


[View source]
def 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. This helps mitigate ReDoS (Regular Expression Denial of Service) attacks.


[View source]
def remove_start_slash(input_path : String) : String #

[View source]
def run_options_parser #

[View source]
def valid_json?(content : String) : Bool #

[View source]
def valid_yaml?(content : String) : Bool #

[View source]

Macro Detail

macro define_analyzers(analyzers) #

[View source]
macro define_detectors(detectors) #

[View source]