Skip to content

map: adapter summaries — argument roles and narrow candidate families - #125

Merged
patchstackdave merged 2 commits into
mainfrom
feat/map-argument-roles
Aug 13, 2026
Merged

map: adapter summaries — argument roles and narrow candidate families#125
patchstackdave merged 2 commits into
mainfrom
feat/map-argument-roles

Conversation

@patchstackdave

Copy link
Copy Markdown
Contributor

Track 2, step 1. The review recommended small per-library adapter summaries before any whole-program dataflow engine, because which argument received the value decides which mitigation class applies. This is the gate that lets ruleGeneratable ever be true.

Stacked on #124 (which stacks on #123). Merge order: #123#124 → this.

Argument roles

Every flow now reports argumentRole, from a table keyed by sink kind so an overloaded name (get) can't be read as the wrong thing:

exec(command) · execFile(file, args) · spawn(command, args)
fetch(url, init) · axios.get(url, options) · axios.post(url, body)
fs.readFile(path) · fs.writeFile(path, content)
pool.query(sql, values) · .insert(values) · .eq(column, value)
eval(code)

Narrow candidate families

candidateFamily is assigned only where a request value arriving is inherently dangerous and a rule can express the mitigation:

sink + role family
http + url ssrf
exec + command/file/args command-injection
fs + path path-traversal
db + sql sql-injection
eval + code code-injection

ruleGeneratable: true requires all of: precise evidence · non-null runtimeParameter · a local sink call site · a modelled role · a candidate family.

The refusals matter as much as the acceptances

Each of these is a proven flow — genuine reachability signal — but not a blockable pattern by itself. All are tested explicitly:

fs.writeFileSync("/tmp/x", req.body.contents)   role=content → NOT generatable
db.from(t).insert({ title: req.body.title })    role=values  → NOT generatable
axios.post(url, req.body.payload)               role=body    → NOT generatable
pool.query(sql, [req.body.id])                  role=values  → NOT generatable

That's the over-reach the review warned about by name (path vs contents; generic database values). The reason is reported, not the flow dropped — so it stays useful for reachability while being ineligible for a blocking rule.

Illustrative

The real reference app yields 0 candidates (every flow lands in a db values argument). An Express fixture with the high-risk sinks yields exactly ssrf + path-traversal + command-injection. A generatable candidate carries everything a compiler needs: route + method, runtimeParameter, sink package, evidence span, and the endpoint fingerprint for staleness.

811 tests green; typecheck clean.

Still ahead (Track 2)

The finer confidence taxonomy (exact/transformed/interprocedural), the candidate lifecycle (dry-run → observe → promote → retire on map drift), and corpus metrics — plan §10.

@coderbuds

coderbuds Bot commented Aug 13, 2026

Copy link
Copy Markdown

Comprehensive adapter summaries integrate argument roles into dataflow analysis.

🎯 Quality: 82% Excellent · 📦 Size: Extra Large — strongly consider breaking this down

🛡️ Standards: no pre-flight fit check ran for this change — wire assess-change-fit into your coding agents to catch size before opening.

📈 This month: Your 57th PR — above team average · Averaging Good

See how your team is trending →

@patchstackdave

Copy link
Copy Markdown
Contributor Author

/review

@patchstackdave
patchstackdave force-pushed the feat/map-argument-roles branch from 1e08458 to 7ef18c9 Compare August 13, 2026 13:13
patchstackdave and others added 2 commits August 13, 2026 15:16
Track 2, step 1: the review's recommendation to use small per-library summaries BEFORE
building a whole-program dataflow engine. Which ARGUMENT received the value decides which
mitigation class applies, so this is the gate that lets `ruleGeneratable` ever be true.

Every flow now reports `argumentRole` (command | file | args | url | init | body | options |
path | content | sql | values | columns | column | value | code | unknown), taken from a
per-sink-kind summary table so an overloaded name (`get`) can't be read as the wrong thing:
  exec(command) · execFile(file, args) · spawn(command, args)
  fetch(url, init) · axios.get(url, options) · axios.post(url, body)
  fs.readFile(path) · fs.writeFile(path, content)
  pool.query(sql, values) · .insert(values) · .eq(column, value)
  eval(code)

`candidateFamily` is then assigned only for pairs where a request value arriving is
inherently dangerous AND a rule can express it: http+url → ssrf, exec+command →
command-injection, fs+path → path-traversal, db+sql → sql-injection, eval+code →
code-injection. `ruleGeneratable` becomes true only with precise evidence, a non-null
runtime parameter, a local sink call site, a modelled role, and such a family.

The refusals matter as much as the acceptances, and are tested explicitly:
  fs.writeFileSync("/tmp/x", req.body.contents)   role=content → NOT generatable
  db.from(t).insert({ title: req.body.title })    role=values  → NOT generatable
  axios.post(url, req.body.payload)               role=body    → NOT generatable
  pool.query(sql, [req.body.id])                  role=values  → NOT generatable
Each is a PROVEN flow — real reachability signal — but not a blockable pattern on its own,
which is exactly the over-reach the review warned against (path vs contents, generic db
values). The reason is reported rather than the flow being dropped.

Illustrative: the real reference app yields 0 candidates (all its flows are db `values`),
while an Express fixture with the high-risk sinks yields exactly ssrf + path-traversal +
command-injection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… under tests/map/

The corpus is what makes candidate generation measurable rather than merely tested. Six
declarative cases across the stacks AI builders actually emit — Lovable/TanStack+Supabase
server fns, Express+axios+fs+child_process, Next App Router + a server action,
Fastify+pg (raw sql vs bound values), a Supabase Edge Function, and a case of shapes that
must yield nothing (route param, dynamic computed key, spread into sink).

Each case declares the candidates it expects as `family @ runtimeParameter` plus the proven
flows that must be REFUSED. Two failure modes are measured separately:
  - WRONG-INPUT: a candidate nobody declared — the rule would pin the wrong parameter.
    Asserted to be ZERO. This is the metric that governs auto-promotion.
  - MISSED: a declared candidate absent — a recall gap; loud, but a lesser sin than a
    wrong pin.
Plus two invariants across every case: a candidate never exists without a runtime
coordinate, and every refusal carries a reason (silence is what makes a map untrustworthy).

The corpus immediately earned its keep: a Next SERVER ACTION produced no inputs at all,
because its first parameter IS the payload (`export async function report(input) {
exec(input.job) }`) rather than a Request — so it could never yield a candidate. Payload-
style entries now treat that parameter as the input container (source `server-fn-data`),
which is also the shape TanStack server fns use without a validator.

Also groups the nine map test files under tests/map/, mirroring tests/protect/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@patchstackdave
patchstackdave force-pushed the feat/map-argument-roles branch from 7ef18c9 to aeba7a7 Compare August 13, 2026 13:16
@patchstackdave
patchstackdave merged commit 69215ed into main Aug 13, 2026
5 checks passed
@patchstackdave
patchstackdave deleted the feat/map-argument-roles branch August 13, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants