The declaration the compact row names when the optimizer merged several
files into one endpoint: the first one that is not under a copy
directory, else the first. Scan order is kept otherwise, since analyzers
put the declaring file first and its include chain after it (Django's
apps.py before urls.py).
ponytail: a directory-name list; declarations says when it mattered.
Noir::CLI::Inspect
Shared machinery of the query verbs: noir trace, noir reach,
noir query. Each is a view over the endpoint list a scan produces —
the verbs filter and project, they do not analyze.
The endpoints come from a live scan (every scan flag accepted, with
--ai-context all --include callee implied) or, with --from, from a
noir scan -f json / -f jsonl report, so an agent pays for one scan
and asks many questions.
Constants
Routers that match literal path text without regard to case by default:
ASP.NET (MVC, Web API, Razor Pages, minimal APIs, ...) and Express/Koa
(caseSensitive / sensitive default false). Rails, Django, Flask,
Spring, Laravel, Go routers and Fastify are case-sensitive.
ponytail: tech-name list; a router configured the other way round is not modelled.
Directories that hold copies of an app's code rather than the app:
juice-shop keeps a snippet of every route under data/static/codefixes/
and the optimizer merges it with the real server.ts declaration.
grep's convention, so a shell or an agent can branch without parsing.
A glob when it looks like one, a substring otherwise. The glob is the
agent's, not the shell's: * and ** match anything including /
(every Django URL ends in /, *users* has to find it), ? one
character, [...] a class, and {, } are literal so a route with
{id} in it can be matched as written.
Guards that gate who may call the route. Rate limiting and CSRF checks
are guards the augmentor records, but neither stops an anonymous caller,
and "no auth" is what --no-guard asks.
Where {*name} needs at least one segment: Next.js [...slug] (its
optional [[...slug]] is written as a second, bare route), axum and a
gRPC-transcoding {name=**}. Spring, ASP.NET, Armeria, salvo's
{*name} / {**name} and Rocket's <name..> take zero.
:id(\d+) (Express 4 / Koa / Fastify) and :id{[0-9]+} (Hono) carry a
regex constraint; a trailing ? makes the segment optional.
Play's $id<\w{8}> is one placeholder with its regex.
path-to-regexp v8 (Express 5) optional group: /posts{/:id}.
{id} :id <int:pk> [id] $id ${id} match one segment; *
** {path*} {...rest} [...slug] [[...slug]] *path :rest* <path:p>
{*path} (Spring, ASP.NET, Next.js and Starlette's {p:path} as the
optimizer writes them) and {path...} (Ktor, Go) match the rest
(Express's bare * is a catch-all, not one segment).
Proxy rules that serve every path under their own, by the path-type tag
the analyzer sets: nginx location /api/, haproxy path_beg and Istio
prefix compare strings (/api serves /apis); Ingress and Gateway
API Prefix compare whole segments. A Go ServeMux pattern ending in /
serves its subtree (gin's or Echo's /x/ is one route, and /x/{$}
only /x/).
A proxy rule merged into a code route (an Ingress Prefix folded into
POST /api/users) keeps its tag, but the code serves only its own
route, so the proxy tags count only on a row no code analyzer reported.
The rest placeholders that also take zero segments, so /files/:path*
serves /files too.
Class methods
any (or an empty bucket filter) is satisfied by a non-empty bucket.
Nearest declared routes for a miss: most shared leading segments
(case-insensitive, placeholders count), then the closest spelling of the
first segment that differs (/rest/baskets/3 → /rest/basket/:id), then
the closest length. Any method: the row names it, and "that path exists,
but as PATCH" is the answer a miss most often needs.
Signals worth filtering on: the route/detector/method ones restate
method, url and tech, and would make --signal any match everything.
Prints rows in common.format and returns the exit code. -f text
always prints the compact line; json/jsonl honour --full.
Filter values that no endpoint can satisfy, so a typo (--sink file
for file_io) or a report scanned without --ai-context is said out
loud instead of returning an empty answer that looks final.
The endpoint list to query: a saved report or a live scan.
Reads the endpoints of a -f json document or a -f jsonl stream.
Passive findings and failure lines (jsonl) are told apart by shape.
Whether query --method lists a route declared for declared. An
ANY row (nginx, Apache, router.all) serves every HTTP verb, so
--method POST lists it, as trace does; --method ANY lists only
the ANY rows.
Whether a route declared for declared serves a wanted request method.
Frameworks answer HEAD from the GET route, so a HEAD request traces to it.
Reported paths carry the scan root (corpus/app/routes/x.ts), so a glob
written from inside the app (routes/*) also matches from any /.
Component-wise suffix match: users.go and api/users.go both name
src/api/users.go; sers.go does not.
A project is named by its directory under the scan base, so services/orders
is that project and not services/orders-v2. A glob (same rules as above)
matches the whole name. An endpoint with no project matches nothing.
Every endpoint with evidence in file, each with its via list; with
line, evidence is ordered by distance and nearest_line is set.
ponytail: declaration lines only — no function spans, so :line orders
rather than filters. Tree-sitter spans would make it a filter.
Global output flags are already applied by the router, and remain valid when a verb reads a report instead of scanning a path.
The path part of an endpoint URL: -u prefixes a scheme+host. Any
other scheme (ws://, cli://shopctl/users, a deep link) names the
surface, so it stays: /users is not the CLI's users command.
The path a client sent, as a router sees it: no query or fragment, a
leading /, // collapsed, each segment percent-decoded (%2F stays
one segment, since a router splits before it decodes). A declared route
is written decoded, so /%C3%A9t%C3%A9 has to be read as /été.
The regex a declared route matches request paths against, or nil for a pattern Noir cannot turn into one.
How specific a route is: fewer catch-all segments, then fewer placeholders,
then more literal text. /u/:id outranks /*proxyPath for /u/5.
Among routes with a catch-all the literal text comes first:
/media/:id/*any is more specific than /*unmatched_route, though it
has one more param.
Splits rest into positionals and the scan flags (with their values).
Which flags consume a value is read off ScanFlags, the same table the
completions use; an unknown flag is passed through for scan's parser to
reject with its own message.
Routes matching a concrete request, exact matches first. A PHP front
controller also answers its routes under another spelling: /index.php/x
is /x (PATH_INFO routing, Nextcloud without pretty URLs), and Nextcloud
serves /ocs/v2.php/... as /ocs/v1.php/... too. Those are tried
against the PHP routes when the request itself matches nothing.
A Rails route also answers with an optional .format suffix
(/ai-llms.json is /ai-llms), so when only a catch-all matched, the
suffix-less path is tried against the Rails routes and its matches lead.
Flags scan's parser would reject. Caught here so the verb keeps its exit code 2 instead of scan's usage page and exit 1.
When only a catch-all answered (/*unmatched_route, an SPA fallback),
the request may be a typo of a real route: the nearest declared routes,
wildcard routes left out. Empty when any match is exact or a pattern.