Top Level Namespace
Defined in:
Constant Summary
-
ALLOWED_HTTP_METHODS =
["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "TRACE", "CONNECT", "QUERY"] -
The real HTTP methods an endpoint can carry.
QUERY(RFC 10008) is accepted core-wide, but a framework analyzer adds it to its own verb table only when the upstream framework actually routes the verb — adding it speculatively would report endpoints the framework itself answers with 405. -
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>). -
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. -
SAFE_HTTP_METHODS =
Set {"GET", "HEAD", "OPTIONS", "QUERY"} -
The verbs the read-vs-write heuristics treat as reads: GET/HEAD/OPTIONS plus QUERY (safe + idempotent per RFC 10008). TRACE, though RFC-safe, is excluded — it echoes the request rather than reading a resource, and no consumer ever counted it as a read. One shared set so the admin/webhook taggers, the ai_context unsafe-method signal, and the probe path-filler cannot drift on which verbs are safe. Callers normalize case themselves.
-
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"] -
The concrete verbs a wildcard route (
ANY/ALL/*) fans out to.QUERY(RFC 10008) is deliberately absent: fanning every wildcard route out to a QUERY endpoint would add one endpoint per catch-all route in every framework — reported, probed, and exported — for a verb almost no deployed app intends to serve. AQUERYendpoint is emitted only where a route declares the verb explicitly.
Method Summary
-
analysis_endpoints(options : Hash(String, YAML::Any), techs, logger : NoirLogger, failures : Array(AnalyzerFailure) | Nil = nil)
failuresis an out-parameter rather than a second return value so the existing signature keeps working: library embedders and specs call this positionally and have no interest in the failure list. - any_to_bool(any) : Bool
-
apply_cfml_components_only!(options : Hash(String, YAML::Any), selected_techs : Array(String)) : Nil
Publishes the narrowing decision into the options hash, which is how
Analyzer::Cfml::Purereceives it. - build_detector_list(options : Hash(String, YAML::Any)) : Array(Detector)
-
cfml_components_only?(selected_techs : Array(String)) : Bool
Whether the generic CFML analyzer should narrow to components-only mode.
- detect_techs(base_paths : Array(String), options : Hash(String, YAML::Any), passive_scans : Array(PassiveScan), logger : NoirLogger)
- 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)
-
filter_redundant_generic_techs(techs : Array(String)) : Array(String)
Drops techs that another detected tech supersedes.
- get_allowed_methods
- get_relative_path(base_path : String, path : String) : String
- get_symbol(method : String)
- initialize_analyzers(logger : NoirLogger)
-
json_any?(content : String) : JSON::Any | Nil
Strict
JSON.parse-or-nil. -
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_bounded?(regex : Regex, input : String) : Bool
Matches
regexagainstinput, treating a backtracking blow-up as "no match" rather than an exception. - 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
-
yaml_any?(content : String) : YAML::Any | Nil
Strict
YAML.parse-or-nil.
Method Detail
failures is an out-parameter rather than a second return value so the
existing signature keeps working: library embedders and specs call this
positionally and have no interest in the failure list. Callers that do care
(the CLI, via NoirRunner) pass an array and get every tech that raised.
Publishes the narrowing decision into the options hash, which is how
Analyzer::Cfml::Pure receives it.
Assigns on every pass, never only when true. The key is written into the
caller's options hash and nothing clears it — CodeLocator's lifecycle
resets cannot reach it, because it is not a locator key. Written
conditionally (the previous if cfml_components_only? ... = true shape), a
true from one scan survived into the next analysis_endpoints call on the
same hash: a library embedder that scanned a ColdBox app and then a
plain-CFML app got the second scan silently narrowed to components-only and
lost its entire .cfm page surface. Same shape as the scan-lifecycle leak
#2503 fixed, in the one remaining channel that carries state between passes.
The CLI never hit it — cli/commands/scan.cr dups the hash before the diff
scan — so only embedders reusing one hash were affected.
Extracted from analysis_endpoints for the same reason
cfml_components_only? was: in place, the only way to exercise it was to
run every analyzer.
Whether the generic CFML analyzer should narrow to components-only mode.
This is a narrowing, not a supersede, which is why it stays here rather
than moving to the catalog's :supersedes alongside the four
framework-shadows-framework rules. A CFML framework owns the .cfm page
surface, so dropping cfml_pure outright would lose the half no framework
analyzer covers — the access="remote" methods on a ColdBox app's proxy
components, which are HTTP-callable whatever framework fronts them.
Narrowing keeps those and still leaves the page surface to its owner.
Extracted from analysis_endpoints so it can be tested at all: in place
it sat inside the function that runs every analyzer.
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"
Drops techs that another detected tech supersedes. The rules live on the
superseding tech's catalog entry as :supersedes; see the doc on
NoirTechs::SUPERSEDES for what belongs there and — more importantly —
what must never be added.
Presence is evaluated against the input list rather than the array being
filtered, so the result no longer depends on the order the rules happen to
be written in. SUPERSEDES chains are rejected by the tech-registry
integrity spec, which keeps that equivalence true.
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.
Matches regex against input, treating a backtracking blow-up as "no
match" rather than an exception.
The bound is PCRE2's own match limit, which is the only mechanism that can
actually interrupt a running match. When a pattern backtracks past it,
Regex#matches? raises Regex::Error; that is the ReDoS ceiling, and it
applies whether or not anything here wraps the call.
This used to spawn a fiber and race it against select ... when timeout.
That could not work: Regex#matches? is a single pcre2_match FFI call
with no yield point, and noir has no preview_mt build, so the timeout
branch could never be reached while the match was running. Measured
directly — with timeout set to 10ms, the wrapper returned after 17ms,
having run the match to completion. It bounded nothing, and cost a fiber
spawn plus a channel per line scanned on the one hot path that used it.
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.