Commit f942492
map: build-time input-flow (attack-surface) map command (#116)
* map: build-time input-flow (attack-surface) map command
Adds `patchstack-connect map` — a build-time command that walks the app's source
and emits its input-flow map: entry points → the inputs each reads → the
sinks/dependencies they reach. It's both a user-facing attack-surface view and
the coordinate source precise (param-pinned) vPatch rules bind against.
Framework-AGNOSTIC by design — signal-driven, not stack-gated:
- entry points: createServerFn (TanStack), exported GET/POST/… handlers (Next
route handlers / SvelteKit), and app.post('/x', handler) route registrations
(Express/Fastify/Hono).
- inputs: zod z.object fields (name + type + min/max), and req.body/query/params
member accesses.
- sinks (provider-agnostic): db (supabase/knex .from().op, prisma, raw query),
fs, child_process exec, http/fetch, eval — followed one level into same-file
helpers. Add a stack by adding a recognizer.
Honesty is a first-class field: `coverage.notes` records that this is the
DETECTED surface (best-effort static analysis), never a guarantee.
The compiler is resolved at RUNTIME from the target app's own `typescript`
(marked external in tsup so it's never bundled into the CLI). Validated against
the TanStack+Supabase reference app and TanStack/Express/Next fixtures.
This is Leg 1 (extract + emit) of the build-time input-flow map; Leg 2 (POST to
a SaaS `site_input_map` store) is a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* map: tag each sink with the npm package behind it; recognize server actions
The link that lets a site's vulnerable dependency (from the manifest / TI) be
correlated to the exact input that reaches it: each sink now carries `package`,
resolved from the file's imports — precisely from the call's base identifier
(fs → node:fs, exec → node:child_process, axios → axios, and const-from-import /
require / new bindings), or inferred from the file's import of a known provider
for that sink kind when the client is built via a local factory (e.g.
`const supabase = getClient()` still resolves to @supabase/supabase-js).
Also fixes a dead branch: files with a `'use server'` directive passed the
pre-filter but had no recognizer — Next server actions are now extracted as
entry points (entryKind: server-action), and same-line const route-handler /
server-action recognizers are consolidated.
Validated on the reference app (all 7 supabase sinks tagged) and fixtures across
TanStack / Express / Next / server-action shapes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* map: harden the extractor — binding-gated sinks, richer input tracing, honesty markers
Recall fixes (all previously produced silent false negatives):
- the textual pre-filter and the route recognizer now derive from one list,
so files registering only .head()/.use() routes are no longer skipped
- inputs destructured from req.body/query/params and from destructured
handler params are extracted; fetch-style bodies are traced through
`const body = await request.json()` variables and destructuring
- router.route('/x').get(handler) chains and Fastify's object-form
app.route({method, url, handler}) are recognized (one endpoint per method)
- symlinked source directories are followed (with a realpath cycle guard)
Precision fixes (all previously produced false positives):
- sink recognizers are gated on module bindings: calls on plain local
objects/classes/functions are not dependency sinks; prisma-shaped ops
require a real prisma signal
- validator `.object({...})` is only read as a schema when its receiver
traces to a known validator package
- destructured handler params no longer wildcard-match unrelated
*.body/query/params member accesses
- bare builtin imports normalize to node:* (npm has a package named `fs`)
New signal:
- endpoints and sinks carry a 1-based source line (auditable coordinates)
- nested validator fields flatten to dotted paths (address.city, tags[].label)
with formats (.email(), …) and regex constraints captured
- inputsResolved: false marks endpoints whose declared validator could not be
parsed — inputs are unknown, not empty — and coverage notes now also report
per-run facts (skipped files, unresolved validators) instead of boilerplate
- per-file fail-open: one unparseable file no longer aborts the whole map
- binding resolution is transitive (const conn = pool.promise())
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* map: prove input→sink flows, stop over-claiming, widen + bound the walk
Addresses an external review of the map command. The headline problem: we emitted
endpoint-level inputs AND endpoint-level sinks but never established which input
reaches which sink — while the CLI advertised "inputs → sinks … precise rule
pinning". Anything consuming that for parameter pinning could pin the wrong input.
- FLOWS. Each endpoint now carries `flows: [{input, sink, confidence, line}]`.
A flow is `precise` only when the input identifier/path appears inside the sink
call's arguments (tainting the handler params + local aliases such as
`const body = await request.json()`); otherwise `heuristic` ("may reach"). On the
reference app: `title -> insert [precise]`, while the helper-reached select that
never receives the input is correctly `heuristic`. `inputs`/`sinks` are documented
as INVENTORIES; only flows assert reachability.
- FALSE POSITIVE. Sinks inside a declared-but-uncalled local function are no longer
attributed to the endpoint (an unused helper that shells out used to make the
endpoint look like it reaches exec). Inline callbacks / IIFEs still count.
- MISSED CODE. Walk the whole project (minus node_modules/dist/build/.next/…)
instead of `src` only, so root-level `server.ts` / `app/` / `functions/`
entrypoints are seen; adds .cjs/.cts/.mts.
- BOUNDARY. Symlinks are followed only while they stay inside the project;
--follow-symlinks opts out (a link to an external repo used to pull in its code).
- COVERAGE. `coverage` now reports filesDiscovered/filesParsed/filesSkipped + roots,
and notes when endpoints have inputs+sinks but no proven link.
Also fixes a pre-existing sink regression found while testing: a client built by a
LOCAL factory (`const supabase = getClient()`, the common AI-generated shape) looked
like a plain local, so every sink on it was dropped — the reference app reported ZERO
sinks. Bindings now follow a local factory's return value to the package it wraps
(fixpoint-resolved), restoring all 7 supabase sinks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: disclose the `map` capability and scope the "no source code" claim
Shipped docs must disclose every capability in dist/, and an overbroad privacy
claim is treated as misrepresentation by auditing agents. `map` reads the project's
source files, which made two claims inaccurate:
- AGENT-INSTALL said the package "reads the project's dependency list only".
That was a READ claim; it is now scoped to what is TRANSMITTED (still only
package names + versions), with an explicit line that `map` reads source
locally and transmits nothing.
- README's payload paragraph now notes the same.
Adds a `map` entry to both command references (what it walks, that it uses the
project's own TypeScript, that output is best-effort/detected-surface, that it
writes nothing but --out and is never invoked by scan/setup/guide/protect).
The install prompt is untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* map: derive the URL path of file-based route handlers
A file-based route carries its URL in its LOCATION, not its code, so these endpoints
had no `route` — meaning a generated rule could only be param-pinned, never
route-scoped (`when.path`). Derive it across the conventions AI builders emit:
Next App Router app/api/items/route.ts -> /api/items
app/api/items/[id]/route.ts -> /api/items/:id
app/(marketing)/api/x/route.ts -> /api/x (group stripped)
app/api/files/[...path]/route.ts -> /api/files/:path
Next Pages Router pages/api/items/index.ts -> /api/items
pages/api/[id].ts -> /api/:id
SvelteKit src/routes/api/items/+server.ts -> /api/items
Nuxt server/api/items.post.ts -> /api/items (method suffix dropped)
Dynamic segments become `:name` and set `routeDynamic: true`, so a consumer knows the
route is a PATTERN and must scope with a glob/regex `when.path` rather than treating
`/api/orders/:id` as a literal path.
Net effect: a Next route handler now yields FULL pinning coordinates — route +
method + inputs + a precise input→sink flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>1 parent 19428c7 commit f942492
10 files changed
Lines changed: 1657 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| 14 | + | |
13 | 15 | | |
14 | 16 | | |
15 | 17 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
72 | 81 | | |
73 | 82 | | |
74 | 83 | | |
| |||
189 | 198 | | |
190 | 199 | | |
191 | 200 | | |
192 | | - | |
| 201 | + | |
193 | 202 | | |
194 | 203 | | |
195 | 204 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
| |||
56 | 57 | | |
57 | 58 | | |
58 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
59 | 68 | | |
60 | 69 | | |
61 | 70 | | |
| |||
128 | 137 | | |
129 | 138 | | |
130 | 139 | | |
131 | | - | |
| 140 | + | |
132 | 141 | | |
133 | 142 | | |
134 | 143 | | |
| |||
193 | 202 | | |
194 | 203 | | |
195 | 204 | | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
196 | 240 | | |
197 | 241 | | |
198 | 242 | | |
| |||
771 | 815 | | |
772 | 816 | | |
773 | 817 | | |
| 818 | + | |
| 819 | + | |
774 | 820 | | |
775 | 821 | | |
776 | 822 | | |
| |||
0 commit comments