NoirAIContext
NoirAIContext enriches each endpoint with an AIContext — the
guards / callees / sinks / validators / signals an LLM (or a human
triage pass) needs to reason about the route. The heavy lifting
lives in the collaborators required above:
PatternDefinition/Patterns— the declarative detection catalogs (sinks, validators, guards, parameter classes).SourceReader— cached source reads + snippet extraction.PatternMatcher— the stateless name/snippet detection engine.Builder— orchestrates every populate step per endpoint.
This file keeps only the public module surface: building context for
a batch of endpoints, and the --ai-context=… feature filter.
Constants
Every name --ai-context= accepts, including the all alias.
Authentication guard — "verifies who you are". Login required, JWT verification, session check.
Authorization guard — "verifies what you can do". Role, permission, ability, policy checks. Distinct from authentication because the common security gap is "logged in but no further check" → horizontal / vertical privilege escalation.
CORS open-with-credentials. Wildcard * origin combined with
credentials: true is the textbook CORS misconfiguration —
browsers actually block this combination at the spec level for
security reasons, so seeing it in code means either the
config is broken or the developer is reaching for something
the spec forbids. Either way, worth a sharp signal.
The check is order-independent across two regex windows so
origin: '*' followed by credentials: true and the reverse
both fire.
Negative protection: explicit CSRF bypass. Emit as a SIGNAL (not a guard) so the LLM knows protection is intentionally disabled here and the endpoint warrants extra scrutiny.
CSRF protection. Different layer from authn/authz — protects against cross-site request forgery via tokens / SameSite cookies / origin checks. Absent on state-changing endpoints is usually worth a review note.
The JVM receiver names no rule above can see (this.owners.save),
read off the declaration instead: a type named *Repository / *Dao,
or one extending such a type (every Spring Data base interface is a
*Repository). Only its data-access methods count, so getClass /
toString on a repository field stay out; a method without such a
name counts when it carries @Query.
Middleware that lists a directory to the client (Express serveIndex,
Go http.FileServer, which lists by default). A plain static mount
(express.static) is deliberately left out: it serves the same fixed
files a static dir expands into, and those asset rows carry no sink.
Matched only on the route's own statement, so the asset rows, anchored
at the asset file with no line, never reach it.
Accepted from the user as "every bucket". Not a bucket itself, so it is
kept out of FEATURES and added back only where user input is validated.
The --ai-context bucket vocabulary, in emission order.
There used to be four copies of this list — two in options.cr (the CLI
help text and the validator), one in NoirAIContext.parse_feature_set,
one in OutputBuilderCommon#ai_context_feature_filter — and they
disagreed. sources was in the two that decide what gets emitted and
missing from the two that decide what the CLI accepts, so
--ai-context=sources was rejected for a bucket the augmentor and every
output builder fully implement. The only way to see sources was to ask
for all of them, and --ai-context=guards,sources was unreachable.
Anything that needs the vocabulary reads it here.
What f in f.read() / f.Write(b) must be bound to for the call to
be file I/O: a call that opens a file (open(p), io.open,
Path(p).open(), File.open, os.Open / OpenFile / Create,
fs.createReadStream, new FileInputStream) or Go's *os.File type.
See Builder#file_handle_sink.
File-system calls as handler bodies spell them: Node's fs module and
its camelCase API (readFileSync, createReadStream, readdir),
os.ReadFile / os.Open, PHP readfile / scandir, res.sendFile /
send_file / send_from_directory, Express res.download,
http.ServeFile, gin's c.SaveUploadedFile.
An in-memory buffer: send_file(io.BytesIO(data)) streams bytes the
handler already holds, so no path reaches the file system.
JWT verification bypass — explicit "trust whatever we
received without checking" shapes across pyjwt / jsonwebtoken
/ java-jwt / ruby-jwt. Each pattern is a single-line dead
giveaway, surfaced as a sharp signal alongside csrf_exempt /
cors_open. The kind is jwt_unsafe so consumers can sort it
next to csrf_exempt.
Mobile code names its URL fetch flows download…; the bare word is
what Builder#normalize_mobile_sink turns into outbound_http. Kept
mobile-only like the kinds above: on an HTTP route it is the route
literal or a helper's name, not file I/O.
Sink kinds that only make sense on a mobile deep-link endpoint — a
WebView load or an intent launch fed by inbound deep-link data. They
live in the global SINK_PATTERNS so the catalog stays in one place,
but the Builder evaluates them only for endpoint.mobile? endpoints:
an HTTP route handler never opens a WebView or launches an Android
intent, so firing these against every route is conceptually wrong
(and a latent FP source as the name/source patterns broaden).
The non-mobile view of the sink catalog, computed once. Used for HTTP endpoints so the two mobile-only sinks above are never matched against server-side route/callee snippets.
Rate limiting / throttling. Often the only defence on credential-handling endpoints against credential stuffing / brute-force. Confidence is moderate because not every endpoint needs rate limiting (only credential / lookup-style ones).
Derived signals can depend on entries stored in another bucket. Keep
those evidence buckets visible whenever the signal is visible, and keep
only signals linked to the originally requested buckets. This preserves
relationships such as ssrf + outbound_http without exposing unrelated
signals from the same endpoint.
Bare query / execute are mostly request-input accessors
(c.Query, FastAPI Query(), params.query) or command objects
(useCase.execute), so they count as SQL only on a DB-ish
receiver. Raw SQL on any other receiver still surfaces through the
select ... from source pattern. select is the same: on a query
builder (knex.select, sqlx db.Select, jOOQ dsl.select) it runs
SQL, while _keys.Select(...) is LINQ and items.select { } Ruby's
Enumerable.
Class methods
Expands the requested features for signals present on this endpoint. Explicitly requesting signals keeps them all; otherwise only signals linked to the original selection are retained.
Parses a --ai-context=… value into the set of buckets that survive the
filter. An empty value, or one naming all, means every bucket.
Names are case-folded to match the CLI, which lowercases before storing:
a config-file ai_context_features: "Guards" reaches here unmodified and
would otherwise match no bucket at all.
Names in raw that are outside the accepted vocabulary, in the user's
original spelling so an error message can echo the typo as written.
--ai-context= validates through this; so does the effective option
value after config + CLI are merged. Without the second check a typo in
a config file's ai_context_features reached parse_feature_set,
matched no bucket, and silently emptied every endpoint's AI context —
the same value on the command line was a hard error.
Instance methods
Filters AIContext buckets while preserving evidence relationships for
derived signals. Mirrors the plain-text builder's feature filter so
JSON/YAML/SARIF/Postman/OAS and plain output show the same linked subset.
features holds bucket names from NoirAIContext::FEATURES. Selecting
every bucket is a no-op.