@clear-capabilities/agentic-security-scanner 0.185.0 → 0.186.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/bin/agentic-security.js +66 -18
  3. package/dist/1128.index.js +25 -21
  4. package/dist/{5817.index.js → 1602.index.js} +378 -29
  5. package/dist/2584.index.js +1 -1
  6. package/dist/4171.index.js +2 -2
  7. package/dist/{0b301d45b231f144a1b4.json → 4d06c8944ae121d7085c.json} +8 -8
  8. package/dist/5752.index.js +466 -0
  9. package/dist/7405.index.js +1 -1
  10. package/dist/8158.index.js +1713 -0
  11. package/dist/8798.index.js +1 -1
  12. package/dist/9382.index.js +2 -2
  13. package/dist/9560.index.js +1 -1
  14. package/dist/9579.index.js +1 -1
  15. package/dist/agentic-security.mjs +29 -29
  16. package/dist/agentic-security.mjs.sha256 +1 -1
  17. package/package.json +8 -8
  18. package/src/dataflow/CLAUDE.md +6 -3
  19. package/src/dataflow/access-paths.js +12 -2
  20. package/src/dataflow/demand-query.js +279 -40
  21. package/src/dataflow/dep-fingerprint.js +1 -1
  22. package/src/dataflow/exploit-prover.js +6 -2
  23. package/src/dataflow/ir-cache.js +268 -0
  24. package/src/dataflow/privacy-taint.js +7 -0
  25. package/src/engine.js +62 -5
  26. package/src/ir/CLAUDE.md +4 -0
  27. package/src/ir/callgraph.js +36 -8
  28. package/src/ir/index.js +18 -4
  29. package/src/ir/parser-js.js +10 -0
  30. package/src/language/CLAUDE.md +13 -0
  31. package/src/language/nix-option-types.js +170 -0
  32. package/src/language/nix-script-taint.js +46 -3
  33. package/src/language/nix-secrets.js +20 -2
  34. package/src/lineage/CLAUDE.md +2 -1
  35. package/src/lineage/smt-orchestrator.js +47 -11
  36. package/src/lineage/smt-z3-backend.js +294 -0
  37. package/src/posture/CLAUDE.md +5 -1
  38. package/src/posture/business-logic.js +16 -7
  39. package/src/posture/dag-commands.js +442 -0
  40. package/src/posture/finding-defaults.js +19 -0
  41. package/src/posture/git-history.js +91 -1
  42. package/src/posture/proof-fabric/cli.js +1 -1
  43. package/src/posture/proof-fabric/container.js +2 -2
  44. package/src/posture/proof-fabric/generate-haskell.js +205 -0
  45. package/src/posture/proof-fabric/generate.js +6 -3
  46. package/src/posture/proof-fabric/reproducer.js +4 -2
  47. package/src/posture/proof-fabric/runner.js +4 -4
  48. package/src/sast/CLAUDE.md +2 -0
  49. package/src/sast/_call-args.js +70 -0
  50. package/src/sast/init-scope.js +87 -0
  51. package/src/sast/listen-bind-address.js +49 -0
  52. package/src/sast/redirect-target.js +106 -0
  53. package/src/sast/reflected-input.js +76 -0
  54. package/src/sast/send-file-root.js +27 -0
  55. package/src/sast/sensitive-data-logging.js +38 -1
  56. package/dist/119.index.js +0 -197
  57. package/dist/128.index.js +0 -129
  58. package/dist/1293.index.js +0 -5689
  59. package/dist/1336.index.js +0 -16331
  60. package/dist/1351.index.js +0 -133
  61. package/dist/1387.index.js +0 -271
  62. package/dist/1437.index.js +0 -23
  63. package/dist/1445.index.js +0 -110
  64. package/dist/1505.index.js +0 -151
  65. package/dist/1557.index.js +0 -293
  66. package/dist/1621.index.js +0 -311
  67. package/dist/1751.index.js +0 -160
  68. package/dist/1765.index.js +0 -727
  69. package/dist/1861.index.js +0 -690
  70. package/dist/1906.index.js +0 -433
  71. package/dist/208.index.js +0 -1631
  72. package/dist/2223.index.js +0 -159
  73. package/dist/2238.index.js +0 -295
  74. package/dist/2530.index.js +0 -152
  75. package/dist/2565.index.js +0 -116
  76. package/dist/2574.index.js +0 -127
  77. package/dist/2620.index.js +0 -345
  78. package/dist/2773.index.js +0 -98
  79. package/dist/2775.index.js +0 -32
  80. package/dist/2966.index.js +0 -171
  81. package/dist/2972.index.js +0 -87
  82. package/dist/2991.index.js +0 -131
  83. package/dist/3063.index.js +0 -833
  84. package/dist/3075.index.js +0 -227
  85. package/dist/3121.index.js +0 -297
  86. package/dist/3314.index.js +0 -263
  87. package/dist/3480.index.js +0 -1822
  88. package/dist/3481.index.js +0 -24
  89. package/dist/3506.index.js +0 -268
  90. package/dist/3533.index.js +0 -591
  91. package/dist/3653.index.js +0 -450
  92. package/dist/3740.index.js +0 -722
  93. package/dist/3822.index.js +0 -131
  94. package/dist/3868.index.js +0 -292
  95. package/dist/3918.index.js +0 -192
  96. package/dist/4090.index.js +0 -288
  97. package/dist/4113.index.js +0 -1286
  98. package/dist/4121.index.js +0 -1190
  99. package/dist/4142.index.js +0 -690
  100. package/dist/4168.index.js +0 -509
  101. package/dist/4281.index.js +0 -993
  102. package/dist/4336.index.js +0 -436
  103. package/dist/4387.index.js +0 -532
  104. package/dist/4413.index.js +0 -302
  105. package/dist/4476.index.js +0 -4510
  106. package/dist/4491.index.js +0 -133
  107. package/dist/4538.index.js +0 -185
  108. package/dist/4684.index.js +0 -319
  109. package/dist/4740.index.js +0 -165
  110. package/dist/4780.index.js +0 -209
  111. package/dist/4802.index.js +0 -225
  112. package/dist/4812.index.js +0 -379
  113. package/dist/4835.index.js +0 -126
  114. package/dist/494.index.js +0 -226
  115. package/dist/4991.index.js +0 -549
  116. package/dist/5199.index.js +0 -305
  117. package/dist/5216.index.js +0 -109
  118. package/dist/5439.index.js +0 -301
  119. package/dist/5443.index.js +0 -97
  120. package/dist/5496.index.js +0 -377
  121. package/dist/5738.index.js +0 -240
  122. package/dist/5801.index.js +0 -588
  123. package/dist/5858.index.js +0 -223
  124. package/dist/5933.index.js +0 -143
  125. package/dist/601.index.js +0 -23
  126. package/dist/6173.index.js +0 -273
  127. package/dist/6291.index.js +0 -1871
  128. package/dist/6312.index.js +0 -666
  129. package/dist/634.index.js +0 -16182
  130. package/dist/644.index.js +0 -87
  131. package/dist/6508.index.js +0 -283
  132. package/dist/6560.index.js +0 -645
  133. package/dist/6576.index.js +0 -422
  134. package/dist/6579.index.js +0 -271
  135. package/dist/6655.index.js +0 -3221
  136. package/dist/6745.index.js +0 -305
  137. package/dist/6818.index.js +0 -21642
  138. package/dist/6932.index.js +0 -1190
  139. package/dist/6972.index.js +0 -167
  140. package/dist/6995.index.js +0 -163
  141. package/dist/7249.index.js +0 -191
  142. package/dist/7372.index.js +0 -1197
  143. package/dist/7546.index.js +0 -541
  144. package/dist/76.index.js +0 -448
  145. package/dist/7879.index.js +0 -163
  146. package/dist/8025.index.js +0 -130
  147. package/dist/8054.index.js +0 -248
  148. package/dist/808.index.js +0 -1330
  149. package/dist/8097.index.js +0 -201
  150. package/dist/8126.index.js +0 -191
  151. package/dist/8178.index.js +0 -78
  152. package/dist/8382.index.js +0 -93
  153. package/dist/8408.index.js +0 -261
  154. package/dist/8540.index.js +0 -174
  155. package/dist/8635.index.js +0 -802
  156. package/dist/8688.index.js +0 -410
  157. package/dist/8699.index.js +0 -779
  158. package/dist/8891.index.js +0 -457
  159. package/dist/8916.index.js +0 -885
  160. package/dist/9146.index.js +0 -170
  161. package/dist/9168.index.js +0 -841
  162. package/dist/9292.index.js +0 -369
  163. package/dist/9422.index.js +0 -866
  164. package/dist/9440.index.js +0 -453
  165. package/dist/9479.index.js +0 -152
  166. package/dist/9481.index.js +0 -220
  167. package/dist/9539.index.js +0 -100
  168. package/dist/9547.index.js +0 -103
  169. package/dist/9692.index.js +0 -90
  170. package/dist/9800.index.js +0 -462
  171. package/dist/9899.index.js +0 -138
  172. package/dist/9981.index.js +0 -206
  173. package/dist/999.index.js +0 -117
  174. package/dist/9999.index.js +0 -1698
package/CHANGELOG.md CHANGED
@@ -9,6 +9,40 @@
9
9
  > make the history less accurate, not more.
10
10
 
11
11
 
12
+ ## 0.186.2 - Haskell exploit proof, a default-scan drift gate, and the measurements behind earlier claims
13
+
14
+ - **Haskell in the exploit proof fabric (GA capability, new language).** `proof doctor` now lists `haskell` and `proof generate` builds reproducers for Haskell findings. Reproducers run in GHC's interpreter (`ghc -e`), so there is no build step. Command injection and path-traversal writes are proven; server-side request forgery is refused because no HTTP client ships with GHC; SQL, argument injection and deserialization are refused by name. Only `base` and the packages that ship with GHC are allowed (read from the installed GHC); a handler that needs a web framework is refused with the package named. Template Haskell, quasi-quotes, CPP, the foreign function interface and plugin options are refused as load-time code execution. 45 of 45 fixture reproductions across 15 classes, 0 false confirmations. Limits: exercised under local confinement on macOS with GHC 9.14.1 only; the container image is wired but was not run against a real engine, and Linux was not exercised.
15
+ - **`fix --apply --require-proof` now holds for Haskell and Nix fixes.** Their fixer returned before the proof gate, so the flag was silently ignored. The gate is shared now; the patch is previewed, proven, then written, and aborted if the written patch differs from the proven one.
16
+ - **Correction to 0.186.0: default scan output did change.** 0.186.0 said a default scan's output was unchanged. It was not. Findings from flow-pair and structural detectors now carry `line` (taken from the sink, then the source) where they had none; on the fixtures used by the new gate, 33 of 84 findings gained a line, and one command-injection finding is now reported by the structural detector under the title "Command Injection (User-Controlled Input)". The stable finding id does not use the line. New detectors (redirect target, reflected input, send-file root, init scope, listen bind address) and general precision rules for open redirect, reflected input, blocking I/O, file serving, logging by name and Nix option types changed what some projects report. Line pragmas now match findings that had no line.
17
+ - **New gate: default-scan drift (`bench/default-scan-drift`).** A default scan over eight committed fixtures is compared to a committed baseline; any added, removed or changed finding fails `npm test` and prints the difference. Re-baselining is explicit and belongs with a changelog entry naming the rule.
18
+ - **Independent precision measurement (`bench/dag/independent`).** On projects the rule work never touched (koa, hapi, nest samples and strapi server packages; five Nix configuration collections), a seeded random sample of 60 flagged sites per language was labeled from source by one LLM reader. Precision of the whole default scan: JavaScript and TypeScript 7 of 60, 11.7% (95% interval 5.8% to 22.2%); Nix 4 of 59 decided sites, 6.8% (2.7% to 16.2%). This is the whole scan, not the narrower classes the 0.186.0 precision work targeted, so it does not contradict those figures; it shows they do not describe a typical project. The sample is for measurement only.
19
+ - **Speed gate now matches the hosted runner.** The time-to-first-finding budget on the hosted runner is the median of ten passing release runs (11.1 s) with a 1.35x limit (15.0 s), and a new `ttff` job runs it on every pull request, so a slowdown like 0.186.0's is found before a tag.
20
+ - **Unmet targets are stated.** The capability registry, README and graph-precision page list the targets 0.186.x missed: incremental analysis 2.5x and 2.4x on the synthetic projects against 3x, 75 of 982 sink sites still on the full path, 3 of 8 on the multi-service fixture, the z3 backend not used by a default scan.
21
+ - **S7 test failures explained.** They ran the built bundle while it crashed at start-up on a statically imported optional package (the `z3-solver` import fixed in 0.186.0's follow-up). A new test fails if the bundle imports any optional package statically.
22
+ - **The `v0.186.0` tag was removed.** That release was never published to npm.
23
+
24
+ ## 0.186.1 - Git history annotation blames once per file, which restores scan speed (fix)
25
+
26
+ 0.186.0 was tagged but never published: its release workflow stopped at the time-to-first-finding gate (15 to 18 s against a 13 s limit), so the first npm release carrying that work is 0.186.1. Nothing in 0.186.0's content changed apart from the fix below.
27
+
28
+ - **Why it got slower.** Findings that used to carry no line number now carry one, so the git history step, which asks git who introduced each finding, ran for about twice as many findings. It ran one `git blame` per finding and one `git show` per commit, about 14 ms each. Measured on this repository's own source: 7.9 s on 0.185.0, 10.9 s on 0.186.0.
29
+ - **The fix.** One `git blame` per file for all of its lines and one `git log --no-walk` for all commit messages. A file whose batch fails is blamed line by line, so a bad line number cannot lose the others. The annotations are the same; `test/git-history-batch.test.js` checks the batched answers against per-line `git blame`. Measured after the change: 8.3 s locally.
30
+ - **Also in this release.** Two test fixes found by hosted CI: the artifact CLI tests now warm the OSV cache once, because a cold runner has none, and `owned-build/6` accepts a watchdog stop on a slow runner.
31
+
32
+ ## 0.186.0 - The graph-precision capabilities are reachable from the CLI, a z3 backend checks path feasibility, and demand queries and incremental analysis got faster (Preview)
33
+
34
+ Everything new is Preview, opt-in and off by default. The new commands and the solver do not run in a default scan. **Correction (added in 0.186.2):** this entry originally said a default scan's output was unchanged. That was wrong for the release as a whole: the precision work, new detectors and a line-number backfill changed default output. See 0.186.2 for the list and the check that now catches this. No GA capability changed its contract.
35
+
36
+ - **Reachable from the CLI.** `dataflow view`, `dataflow contracts`, `dataflow sink-query`, `dataflow plan-cuts`, `witness build` and `witness verify` expose the typed graph views, boundary contracts, demand reverse taint, min-cut control planner and signed witness bundles. Every report carries a completion status (`complete`, `partial`, `cancelled`, `unavailable`, `invalid`) and `negativeClaimAllowed`; "no modeled path" is never reported as "no vulnerability". `witness build` exits 1 and writes nothing when no modeled path exists. The graph views other than the dataflow view and a tool in the MCP server remain library-only.
37
+ - **A real z3 feasibility backend (`AGENTIC_SECURITY_LINEAGE_SMT=z3`).** Path conditions are translated to SMT-LIB and checked with a per-query timeout and memory cap. Only a proven `unsat` over a verified encoding can reduce a path's exploitability; a timeout, an unknown result, a missing solver or a condition the encoding cannot express changes nothing. The synchronous graph build does not call it: it is reached through the async path-evidence entry points. NaN and loose-equality coercion are not modeled.
38
+ - **Demand queries are faster.** A query with the index built per query is 23.4x faster than full analysis on this repository (was 1.9x), and 3.7x averaged over a 30-query sample that includes fallbacks. Instance-field reads in JS and TS no longer force a full fallback; 75 of 982 sink sites still use the full path, all at the caller-depth limit. Findings equal full analysis on all 982 sites. Call-graph name resolution got a name index that also speeds up the cold engine.
39
+ - **Incremental analysis is faster.** A per-file IR cache (JS and TS, under `AGENTIC_SECURITY_INCREMENTAL`) removes re-parsing. End to end it is now 4.3x on `scanner/src`; on the two synthetic projects it is 2.5x and 2.4x, still short of the 3x target. Projects above about 5,000 functions still run cold at the default summary budget.
40
+ - **Fewer false positives on the labeled samples.** Flagged sites that were true rose from 5 of 25 to 5 of 8 for JavaScript and from 6 of 17 to 6 of 8 for Nix, with recall unchanged (5 of 5 and 6 of 6). Part of the JavaScript gain is a measurement correction: line-less findings were credited to every same-class label in their file. The rules are general (a literal origin prefix on a redirect, request-input-only reflection, module-load blocking calls, confined file serving, bounded Nix option types) and each has a metamorphic test; the held-out split was not independent, which the benchmark summary now says. The multi-service fixture still finds 3 of 8: the misses need the cross-service join in a normal scan and a queue-consumer source model.
41
+ - **The state-machine heuristic is narrowed.** A `.status` write is compared only with status sets declared in the same file, and test files are skipped. Before, the first set found anywhere in a project was applied to every write.
42
+ - **Fixed.** The lineage deadline tests no longer assume which of two valid cancellations wins on a slow runner, and the regression report no longer embeds the exact bundle size, so a rebuild cannot make it stale. `@babel/core` and `z3-solver` moved to their latest releases and `@azure/msal-node` is pinned past an advisory in the extension's dev tooling.
43
+ - **Self-scan baseline.** The scanner's own source falls to 688 findings; the only increases are three low advisories in the two new files that read files from disk.
44
+ - **Not claimed.** Reachability, satisfiability and runtime observation are not exploit proof. A signature on a witness bundle proves integrity, not that the transfer models are right.
45
+
12
46
  ## 0.185.0 - Graph analysis says when it did not finish, and gains recursion handling, call-site correlation, demand queries, incremental invalidation, boundary contracts, control planning and replayable witnesses (Preview)
13
47
 
14
48
  All new behavior is Preview and is off by default or purely additive. No GA capability changed its contract.
@@ -206,6 +206,22 @@ Commands:
206
206
  dataflow plan-cuts [path] --candidates <file.json> --output <file> [--query <file.json>] [--format json|markdown]
207
207
  Plan a minimum-cost, hypothetical control placement (graph cut) over the
208
208
  already-scanned lineage graph. Never verifies a remediation.
209
+ The plan carries a completion contract (never a negative claim).
210
+ dataflow view [path] [--node <id> [--direction in|out|both] [--relations a,b] [--origins a,b]] [--output <file>]
211
+ Opt-in typed graph view over the already-scanned lineage graph: identity,
212
+ completion, SCC condensation of the data-flow relation, or one node's neighbours.
213
+ dataflow contracts [path] [--contracts <file>] [--output <file>]
214
+ Opt-in boundary contracts: bind declared HTTP/queue/storage edges to parsed
215
+ consumer sites and seed taint there. A declaration is a modeled input only.
216
+ dataflow sink-query [path] (--list | --file <rel> --line <n>) [--mode demand|full|compare] [--arg-index <n>]
217
+ [--sink-id <id>] [--access-path <p>] [--rule-family <f>] [--max-work <n>]
218
+ [--max-caller-depth <n>] [--on-limit full|partial] [--output <file>]
219
+ Opt-in demand-driven reverse taint for one sink site. Verdicts: flow-found,
220
+ no-flow-in-modeled-scope, unknown. There is no "safe".
221
+ witness build [path] --out <dir> [--sink <id>] | --list-sinks
222
+ witness verify <bundle.json> [--public-key <path>] [--artifacts <dir> --root <project>] [--json]
223
+ Opt-in signed, replayable path witness bundles (Ed25519, same key as attest).
224
+ A valid signature proves integrity only, never that the finding is real.
209
225
  dataflow scenario apply [path] --operations <file.json> --output <file> [--format json|markdown]
210
226
  Simulate a hypothetical architecture change against the
211
227
  already-scanned lineage graph — never mutates the real scan.
@@ -3571,6 +3587,32 @@ async function cmdDigest(args) {
3571
3587
  console.error('digest --slack <url> OR digest --discord <url>'); return 4;
3572
3588
  }
3573
3589
 
3590
+ /**
3591
+ * --require-proof gate shared by the generic fix path and the Haskell/Nix language fixer: the patch (`files`: path -> patched text) is
3592
+ * allowed only if the original exploit is confirmed beforehand and demonstrably blocked afterwards. Returns { ok, code }; nothing is written here.
3593
+ */
3594
+ async function requireProofGate(args, scanRoot, f, last, files) {
3595
+ // --require-proof: the patch is applied ONLY if the original exploit is confirmed beforehand and demonstrably blocked
3596
+ // afterwards (posture/proof-fabric/fix-proof.js). Missing reproducer, unavailable environment or an incomplete replay all refuse.
3597
+ const { resolveReproducer } = await import('../src/posture/proof-fabric/cli.js');
3598
+ const { findSiblings } = await import('../src/posture/proof-fabric/siblings.js');
3599
+ const { verifyFixWithProof, formatFixProof } = await import('../src/posture/proof-fabric/fix-proof.js');
3600
+ const { verifyFix } = await import('../src/posture/fix-verify.js');
3601
+ const loaded = resolveReproducer(scanRoot, f, args.flags.reproducer);
3602
+ if (!loaded.reproducer) { console.error(`--require-proof: no reproducer for ${f.id}, and none could be generated: ${loaded.reason}. Refusing to apply; nothing was executed. Supply --reproducer <file>.`); return { ok: false, code: 4 }; }
3603
+ if (loaded.generated) console.error(`--require-proof: using a reproducer generated from the finding (${loaded.generated.shape}, ${loaded.generated.class}); it counts only if executing it confirms the exploit.`);
3604
+ const staticVerification = await verifyFix({ scanRoot, originalFindingStableId: f.stableId || null, files, recordMetrics: false });
3605
+ // equivalent sibling exposures: the engine's own other findings of this kind, plus other places with the same code shape
3606
+ const sib = findSiblings({ root: scanRoot, finding: f, scanFindings: last.findings || [] });
3607
+ const siblings = sib.siblings.map((x) => ({ id: x.id ?? null, file: x.file, line: x.line, source: x.source }));
3608
+ if (sib.truncated.length) console.error(`--require-proof: the sibling search stopped early (${sib.truncated.join(', ')}); absence of siblings is not shown.`);
3609
+ const fp = await verifyFixWithProof({ finding: f, reproducer: loaded.reproducer, files, scanRoot, staticVerification, siblings, siblingSearchTruncated: sib.truncated });
3610
+ console.log(formatFixProof(fp));
3611
+ try { (await import('../src/posture/dnr/store.js')).saveRecord(scanRoot, f.stableId || f.id, 'fix', fp); } catch { /* recording is best effort; the refusal below does not depend on it */ }
3612
+ if (!fp.verified) { console.error('\nRefusing to apply: the fix is not proof-verified (--require-proof).'); return { ok: false, code: 4 }; }
3613
+ return { ok: true, code: 0 };
3614
+ }
3615
+
3574
3616
  async function cmdFix(args) {
3575
3617
  const id = args.flags.finding;
3576
3618
  const isPreview = !!args.flags.preview;
@@ -3620,7 +3662,21 @@ async function cmdFix(args) {
3620
3662
  return 4;
3621
3663
  }
3622
3664
  const proj = loadLanguageProject(scanRoot);
3665
+ // --require-proof applies to a Haskell fix exactly as to any other: the verified patch is previewed first, the exploit is
3666
+ // confirmed against the original and blocked against the patch, and only then is the same patch written.
3667
+ let provenFiles = null;
3668
+ if (isApply && args.flags['require-proof']) {
3669
+ const prev = await languageFixPreview(f, proj.files, { apply: false, root: scanRoot });
3670
+ if (!prev.ok) { console.error(`No verified fix for this finding (${prev.status}${prev.tier ? `, ${prev.tier}` : ''}): ${prev.reason || 'unsupported'}`); return 4; }
3671
+ provenFiles = prev.edits ? Object.fromEntries(prev.edits.map((e) => [e.file, e.after])) : { [prev.file]: prev.after };
3672
+ const gate = await requireProofGate(args, scanRoot, f, last, provenFiles);
3673
+ if (!gate.ok) return gate.code;
3674
+ }
3623
3675
  const res = await languageFixPreview(f, proj.files, { apply: isApply, root: scanRoot });
3676
+ if (provenFiles && res.ok) {
3677
+ const applied = res.edits ? Object.fromEntries(res.edits.map((e) => [e.file, e.after])) : { [res.file]: res.after };
3678
+ if (JSON.stringify(Object.entries(applied).sort()) !== JSON.stringify(Object.entries(provenFiles).sort())) { console.error('The patch written differs from the patch that was proof-verified. Run `agentic-security undo` and report this.'); return 1; }
3679
+ }
3624
3680
  if (!res.ok) { console.error(`No verified fix for this finding (${res.status}${res.tier ? `, ${res.tier}` : ''}): ${res.reason || 'unsupported'}`); return 4; }
3625
3681
  console.log(`${res.label || 'FIX'}${res.tier ? ` (${res.tier})` : ''}: ${res.explanation || ''}`.trim());
3626
3682
  console.log(res.diff);
@@ -3724,23 +3780,8 @@ async function cmdFix(args) {
3724
3780
  // --require-proof: the patch is applied ONLY if the original exploit is confirmed beforehand and demonstrably blocked
3725
3781
  // afterwards (posture/proof-fabric/fix-proof.js). Missing reproducer, unavailable environment or an incomplete replay all refuse.
3726
3782
  if (args.flags['require-proof']) {
3727
- const { resolveReproducer } = await import('../src/posture/proof-fabric/cli.js');
3728
- const { findSiblings } = await import('../src/posture/proof-fabric/siblings.js');
3729
- const { verifyFixWithProof, formatFixProof } = await import('../src/posture/proof-fabric/fix-proof.js');
3730
- const { verifyFix } = await import('../src/posture/fix-verify.js');
3731
- const loaded = resolveReproducer(scanRoot, f, args.flags.reproducer);
3732
- if (!loaded.reproducer) { console.error(`--require-proof: no reproducer for ${f.id}, and none could be generated: ${loaded.reason}. Refusing to apply; nothing was executed. Supply --reproducer <file>.`); return 4; }
3733
- if (loaded.generated) console.error(`--require-proof: using a reproducer generated from the finding (${loaded.generated.shape}, ${loaded.generated.class}); it counts only if executing it confirms the exploit.`);
3734
- const files = { [f.file]: newContent };
3735
- const staticVerification = await verifyFix({ scanRoot, originalFindingStableId: f.stableId || null, files, recordMetrics: false });
3736
- // equivalent sibling exposures: the engine's own other findings of this kind, plus other places with the same code shape
3737
- const sib = findSiblings({ root: scanRoot, finding: f, scanFindings: last.findings || [] });
3738
- const siblings = sib.siblings.map((x) => ({ id: x.id ?? null, file: x.file, line: x.line, source: x.source }));
3739
- if (sib.truncated.length) console.error(`--require-proof: the sibling search stopped early (${sib.truncated.join(', ')}); absence of siblings is not shown.`);
3740
- const fp = await verifyFixWithProof({ finding: f, reproducer: loaded.reproducer, files, scanRoot, staticVerification, siblings, siblingSearchTruncated: sib.truncated });
3741
- console.log(formatFixProof(fp));
3742
- try { (await import('../src/posture/dnr/store.js')).saveRecord(scanRoot, f.stableId || f.id, 'fix', fp); } catch { /* recording is best effort; the refusal below does not depend on it */ }
3743
- if (!fp.verified) { console.error('\nRefusing to apply: the fix is not proof-verified (--require-proof).'); return 4; }
3783
+ const gate = await requireProofGate(args, scanRoot, f, last, { [f.file]: newContent });
3784
+ if (!gate.ok) return gate.code;
3744
3785
  }
3745
3786
  let result = await applyVerifiedFix({
3746
3787
  scanRoot,
@@ -5162,6 +5203,8 @@ async function cmdDataflowPlanCuts(args) {
5162
5203
  if (!loaded.ok) { process.stderr.write(`agentic-security dataflow plan-cuts: ${loaded.message}\n`); return 1; }
5163
5204
  const { planCuts } = await import('../src/lineage/cut-planner.js');
5164
5205
  const plan = planCuts(loaded.graph, query, candidates);
5206
+ // Completion contract: a plan is `complete` only for a finite cut over a graph known complete; never a negative claim.
5207
+ plan.contract = (await import('../src/posture/dag-commands.js')).planContract(plan);
5165
5208
  let data;
5166
5209
  if (format === 'json') data = JSON.stringify(plan, null, 2);
5167
5210
  else {
@@ -5171,6 +5214,7 @@ async function cmdDataflowPlanCuts(args) {
5171
5214
  for (const r of plan.validation?.residualPaths ?? []) lines.push(`- to \`${r.sink}\` via ${r.nodes.map((n) => `\`${n}\``).join(' -> ')}`);
5172
5215
  lines.push('', '## Limitations', '');
5173
5216
  for (const l of plan.limitations) lines.push(`- ${l}`);
5217
+ lines.push('', `Completion: ${plan.contract.status}. A negative claim is never allowed for a hypothetical plan.`);
5174
5218
  data = lines.join('\n') + '\n';
5175
5219
  }
5176
5220
  try {
@@ -8012,6 +8056,7 @@ async function main() {
8012
8056
  }
8013
8057
  case 'boundaries': process.exit(await cmdBoundaries(args));
8014
8058
  case 'explore': process.exit(await cmdExplore(args));
8059
+ case 'witness': process.exit(await (await import('../src/posture/dag-commands.js')).cmdWitness(args));
8015
8060
  case 'dataflow': {
8016
8061
  const sub = args._[1];
8017
8062
  if (sub === 'export') { process.exit(await cmdDataflowExport(args)); }
@@ -8053,6 +8098,9 @@ async function main() {
8053
8098
  break;
8054
8099
  }
8055
8100
  else if (sub === 'plan-cuts') { process.exit(await cmdDataflowPlanCuts(args)); }
8101
+ else if (sub === 'view') { process.exit(await (await import('../src/posture/dag-commands.js')).cmdDataflowView(args)); }
8102
+ else if (sub === 'contracts') { process.exit(await (await import('../src/posture/dag-commands.js')).cmdDataflowContracts(args)); }
8103
+ else if (sub === 'sink-query') { process.exit(await (await import('../src/posture/dag-commands.js')).cmdDataflowSinkQuery(args)); }
8056
8104
  else if (sub === 'impact') {
8057
8105
  const impactSub = args._[2];
8058
8106
  if (impactSub === 'assess') { process.exit(await cmdDataflowImpactAssess(args)); }
@@ -8073,7 +8121,7 @@ async function main() {
8073
8121
  break;
8074
8122
  }
8075
8123
  else if (sub === 'twin') { process.exit(await cmdDataflowTwin(args)); }
8076
- process.stderr.write(`agentic-security dataflow: unknown subcommand "${sub}" — only "export", "diff", "watch", "scenario", "impact", "observations", and "twin" are supported.\n`);
8124
+ process.stderr.write(`agentic-security dataflow: unknown subcommand "${sub}" — only "export", "diff", "watch", "scenario", "plan-cuts", "view", "contracts", "sink-query", "impact", "observations", and "twin" are supported.\n`);
8077
8125
  process.exit(2);
8078
8126
  }
8079
8127
  case 'governance': {
@@ -31,10 +31,14 @@ function makeIdentityTokenizer() {
31
31
  // EXPORTS
32
32
  __webpack_require__.d(__webpack_exports__, {
33
33
  WITNESS_SCHEMA: () => (/* binding */ WITNESS_SCHEMA),
34
+ buildWitnessBundle: () => (/* binding */ buildWitnessBundle),
35
+ exportProvenanceArtifact: () => (/* binding */ exportProvenanceArtifact),
36
+ parseWitnessBundle: () => (/* binding */ parseWitnessBundle),
37
+ signWitnessBundle: () => (/* binding */ signWitnessBundle),
34
38
  verifyWitness: () => (/* binding */ verifyWitness)
35
39
  });
36
40
 
37
- // UNUSED EXPORTS: buildWitnessBundle, exportProvenanceArtifact, parseWitnessBundle, replayWitness, signWitnessBundle, transferModelFingerprints, verifyWitnessSignature
41
+ // UNUSED EXPORTS: replayWitness, transferModelFingerprints, verifyWitnessSignature
38
42
 
39
43
  // EXTERNAL MODULE: external "node:crypto"
40
44
  var external_node_crypto_ = __webpack_require__(77598);
@@ -118,7 +122,7 @@ function iso(v) {
118
122
  * dropped and only COUNTED (their names could themselves carry a value). Identities are tokenized with the
119
123
  * existing keyed-hash convention; a raw identity with no tokenizer is dropped, never kept.
120
124
  */
121
- function witness_runtime_sanitizeCausalGroups(groups, { tokenize = (0,identity_token.makeIdentityTokenizer)() } = {}) {
125
+ function sanitizeCausalGroups(groups, { tokenize = (0,identity_token.makeIdentityTokenizer)() } = {}) {
122
126
  const out = []; const rejected = []; let droppedFields = 0;
123
127
  const GROUP_KEYS = new Set(['id', 'sessionRef', 'sessionId', 'traceId', 'environment', 'deploymentRef', 'deploymentConfigDigest',
124
128
  'identity', 'identityToken', 'firstObservedAt', 'lastObservedAt', 'edgeObservations']);
@@ -218,7 +222,7 @@ function assessRuntimeSupport({ pathEdgeIds, edgeStates, causalGroups = [], expe
218
222
  if (!ids.length || !isObj(edgeStates) || !edgeStates.evaluated) {
219
223
  return { ...base, status: 'not_consulted', statements: ['No telemetry was consulted for this path. That is not an observation of absence.'] };
220
224
  }
221
- const sanitized = witness_runtime_sanitizeCausalGroups(causalGroups, { tokenize });
225
+ const sanitized = sanitizeCausalGroups(causalGroups, { tokenize });
222
226
  base.groupsConsidered = sanitized.groups.length; base.rejectedGroups = sanitized.rejected.length; base.droppedFields = sanitized.droppedFields;
223
227
 
224
228
  for (const id of ids) {
@@ -278,7 +282,7 @@ function assessRuntimeSupport({ pathEdgeIds, edgeStates, causalGroups = [], expe
278
282
  * R10.6: external proof references are bound to the sink, the value and the configuration. A reference that is
279
283
  * stale or mismatched is REJECTED with its reasons; it is never re-attached to the changed graph.
280
284
  */
281
- function witness_runtime_bindExploitProofs(proofs, expected) {
285
+ function bindExploitProofs(proofs, expected) {
282
286
  const bound = []; const rejected = [];
283
287
  for (const [i, p] of arr(proofs).entries()) {
284
288
  const id = isObj(p) && typeof p.id === 'string' && REF_RE.test(p.id) ? p.id : `proof-${i}`;
@@ -302,7 +306,7 @@ function witness_runtime_bindExploitProofs(proofs, expected) {
302
306
  }
303
307
 
304
308
  /** Validate the nullable DAG-04 slot. Returns { ok, errors }. */
305
- function witness_runtime_validateControlEvidence(ce, pathEdgeIds) {
309
+ function validateControlEvidence(ce, pathEdgeIds) {
306
310
  if (ce === null) return { ok: true, errors: [] };
307
311
  const errors = [];
308
312
  if (!isObj(ce)) return { ok: false, errors: ['control evidence must be an object or null'] };
@@ -321,7 +325,7 @@ function witness_runtime_validateControlEvidence(ce, pathEdgeIds) {
321
325
  * The four statuses, side by side. Returns exactly the four keys in EVIDENCE_STATUS_KEYS and nothing that
322
326
  * ranks, scores or combines them.
323
327
  */
324
- function witness_runtime_buildEvidenceStatuses({ pathComplete, controlEvidence, runtime, exploit }) {
328
+ function buildEvidenceStatuses({ pathComplete, controlEvidence, runtime, exploit }) {
325
329
  return {
326
330
  modeledPath: { status: pathComplete ? 'connected_complete' : 'connected_partial' },
327
331
  feasibility: { status: controlEvidence ? controlEvidence.status : 'not_assessed' },
@@ -420,14 +424,14 @@ const fileOfScope = (scope) => (typeof scope === 'string' ? scope.split('::')[0]
420
424
  function exportProvenanceArtifact({ hops, incomplete = [] }) {
421
425
  const seen = new Map();
422
426
  for (const h of witness_bundle_arr(hops)) {
423
- const norm = Object.fromEntries(HOP_FIELDS.map((f) => [f, h?.[f] ?? null]));
424
- seen.set(canonicalJson(norm), norm);
427
+ const norm = Object.fromEntries(path_store/* HOP_FIELDS */.KO.map((f) => [f, h?.[f] ?? null]));
428
+ seen.set((0,evidence_bundle/* canonicalJson */.dj)(norm), norm);
425
429
  }
426
430
  const hopList = [...seen.keys()].sort().map((k) => seen.get(k));
427
431
  const truncations = witness_bundle_arr(incomplete)
428
432
  .map((u) => ({ scope: u.qid ?? u.scope ?? null, context: u.context ?? null, reason: u.reasons?.[0] ?? u.reason ?? 'analysis-incomplete' }))
429
- .sort((a, b) => (canonicalJson(a) < canonicalJson(b) ? -1 : 1));
430
- return { schema: PROVENANCE_ARTIFACT_SCHEMA, hops: hopList, truncations, digest: witness_bundle_sha(canonicalJson({ hops: hopList, truncations })) };
433
+ .sort((a, b) => ((0,evidence_bundle/* canonicalJson */.dj)(a) < (0,evidence_bundle/* canonicalJson */.dj)(b) ? -1 : 1));
434
+ return { schema: PROVENANCE_ARTIFACT_SCHEMA, hops: hopList, truncations, digest: witness_bundle_sha((0,evidence_bundle/* canonicalJson */.dj)({ hops: hopList, truncations })) };
431
435
  }
432
436
 
433
437
  function artifactDigest(a) {
@@ -568,8 +572,8 @@ function buildWitnessBundle(o = {}) {
568
572
  }
569
573
  const budget = { ...result.budget }; delete budget.expansionsUsed;
570
574
  const config = { pathQuery: budget, storeLimits: storeLimits.limits, ...(witness_bundle_isObj(o.analysisConfig) ? { extra: o.analysisConfig } : {}) };
571
- const analysisConfigDigest = witness_bundle_sha(canonicalJson(config));
572
- const graphDigest = computeGraphDigest(graph);
575
+ const analysisConfigDigest = witness_bundle_sha((0,evidence_bundle/* canonicalJson */.dj)(config));
576
+ const graphDigest = (0,export_json.computeGraphDigest)(graph);
573
577
 
574
578
  const files = [...new Set([srcNode, sinkNode, ...edges.flatMap((e) => [store.getNode(e.fromNodeId), store.getNode(e.toNodeId)])].map((n) => fileOfScope(n?.scope)).filter(Boolean))].sort();
575
579
  const sources = []; const unboundSourceFiles = [];
@@ -608,7 +612,7 @@ function buildWitnessBundle(o = {}) {
608
612
  const ceCheck = validateControlEvidence(ce, edges.map((e) => e.edgeId));
609
613
  if (!ceCheck.ok) return { ok: false, reason: `control evidence: ${ceCheck.errors.join('; ')}` };
610
614
 
611
- const tokenize = o.tokenize ?? makeIdentityTokenizer();
615
+ const tokenize = o.tokenize ?? (0,identity_token.makeIdentityTokenizer)();
612
616
  let runtime;
613
617
  if (o.runtime) {
614
618
  const groups = sanitizeCausalGroups(o.runtime.causalGroups, { tokenize });
@@ -622,7 +626,7 @@ function buildWitnessBundle(o = {}) {
622
626
  const exploitInputs = witness_bundle_arr(o.exploitProofs).map((p) => sanitizeProof(p));
623
627
  const exploit = { inputs: exploitInputs, ...bindExploitProofs(exploitInputs, { sinkRef, dataElementId: edges[0].dataElementId, graphDigest, analysisConfigDigest }) };
624
628
 
625
- const sourcePath = redactString(srcNode.path ?? ''); const sinkPath = redactString(sinkNode.path ?? '');
629
+ const sourcePath = (0,redact/* redactString */.rd)(srcNode.path ?? ''); const sinkPath = (0,redact/* redactString */.rd)(sinkNode.path ?? '');
626
630
  const redactions = (srcNode.path && sourcePath !== srcNode.path ? 1 : 0) + (sinkNode.path && sinkPath !== sinkNode.path ? 1 : 0);
627
631
 
628
632
  const bundle = jsonSafe({
@@ -669,7 +673,7 @@ function sanitizeProof(p) {
669
673
  const ref = (v) => (typeof v === 'string' && /^[A-Za-z0-9._:@/-]{1,160}$/.test(v) ? v : null);
670
674
  return {
671
675
  id: ref(p.id), kind: ['proof', 'dnr-hunt', 'incident'].includes(p.kind) ? p.kind : null, sinkRef: ref(p.sinkRef), dataElementId: ref(p.dataElementId),
672
- graphDigest: ref(p.graphDigest), analysisConfigDigest: ref(p.analysisConfigDigest), note: typeof p.note === 'string' ? redactString(p.note).slice(0, 200) : null,
676
+ graphDigest: ref(p.graphDigest), analysisConfigDigest: ref(p.analysisConfigDigest), note: typeof p.note === 'string' ? (0,redact/* redactString */.rd)(p.note).slice(0, 200) : null,
673
677
  };
674
678
  }
675
679
 
@@ -693,9 +697,9 @@ function signedBytes(bundle) {
693
697
 
694
698
  /** Sign with the same Ed25519 key and self-issued trust basis as finding evidence bundles. */
695
699
  function signWitnessBundle(bundle, privateKeyPem) {
696
- const publicKeyPem = crypto.createPublicKey(privateKeyPem).export({ type: 'spki', format: 'pem' });
697
- const withIssuance = { ...bundle, issuance: buildLocalIssuance(publicKeyPem) };
698
- const sig = crypto.sign(null, signedBytes(withIssuance), privateKeyPem);
700
+ const publicKeyPem = external_node_crypto_.createPublicKey(privateKeyPem).export({ type: 'spki', format: 'pem' });
701
+ const withIssuance = { ...bundle, issuance: (0,evidence_bundle/* buildLocalIssuance */.hc)(publicKeyPem) };
702
+ const sig = external_node_crypto_.sign(null, signedBytes(withIssuance), privateKeyPem);
699
703
  return { ...withIssuance, signature: { algorithm: 'ed25519', canonicalisation: WITNESS_SCHEMA, value: sig.toString('base64') } };
700
704
  }
701
705
 
@@ -874,16 +878,16 @@ function replayWitness(bundle, artifacts, opts = {}) {
874
878
  const gEdges = new Set(witness_bundle_arr(artifacts.graph.edges).map((e) => e.id));
875
879
  if (gb.edgeIds.some((id) => !gEdges.has(id))) fail('graph-binding', 'a bound graph edge is not in the graph artifact');
876
880
  }
877
- const ceCheck = witness_runtime_validateControlEvidence(bundle.controlEvidence, edges.map((e) => e.edgeId));
881
+ const ceCheck = validateControlEvidence(bundle.controlEvidence, edges.map((e) => e.edgeId));
878
882
  if (!ceCheck.ok) fail('control-evidence', ceCheck.errors.join('; '));
879
883
  const rt = bundle.runtime;
880
884
  const reAssessed = witness_bundle_isObj(rt) ? runtimeAssessment(rt, gb ? gb.edgeIds : []) : null;
881
885
  if (!reAssessed || !same(reAssessed, rt.assessment)) fail('runtime-inconsistent', 'the recorded runtime assessment does not follow from its recorded inputs');
882
886
  if (rt?.inputs && !gb) fail('runtime-inconsistent', 'runtime inputs without a graph binding');
883
- const reBound = witness_runtime_bindExploitProofs(witness_bundle_arr(bundle.exploit?.inputs), { sinkRef: last, dataElementId: edges[0]?.dataElementId, graphDigest: bundle.graphRef.digest, analysisConfigDigest: bundle.graphRef.analysisConfigDigest });
887
+ const reBound = bindExploitProofs(witness_bundle_arr(bundle.exploit?.inputs), { sinkRef: last, dataElementId: edges[0]?.dataElementId, graphDigest: bundle.graphRef.digest, analysisConfigDigest: bundle.graphRef.analysisConfigDigest });
884
888
  if (!same({ status: reBound.status, bound: reBound.bound, rejected: reBound.rejected }, { status: bundle.exploit?.status, bound: bundle.exploit?.bound, rejected: bundle.exploit?.rejected })) fail('proof-binding', 'the recorded proof binding does not follow from the sink, value and configuration it is bound to');
885
889
  if (reAssessed && bundle.completion) {
886
- const ev = witness_runtime_buildEvidenceStatuses({ pathComplete: bundle.completion.path.complete && derived.overall === 'complete', controlEvidence: bundle.controlEvidence, runtime: reAssessed, exploit: reBound });
890
+ const ev = buildEvidenceStatuses({ pathComplete: bundle.completion.path.complete && derived.overall === 'complete', controlEvidence: bundle.controlEvidence, runtime: reAssessed, exploit: reBound });
887
891
  if (!same(ev, bundle.evidence)) fail('evidence-statuses', 'the four evidence statuses do not follow from their inputs');
888
892
  }
889
893
  return { ok: failures.length === 0, failures, warnings, derived };