@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.
- package/CHANGELOG.md +34 -0
- package/bin/agentic-security.js +66 -18
- package/dist/1128.index.js +25 -21
- package/dist/{5817.index.js → 1602.index.js} +378 -29
- package/dist/2584.index.js +1 -1
- package/dist/4171.index.js +2 -2
- package/dist/{0b301d45b231f144a1b4.json → 4d06c8944ae121d7085c.json} +8 -8
- package/dist/5752.index.js +466 -0
- package/dist/7405.index.js +1 -1
- package/dist/8158.index.js +1713 -0
- package/dist/8798.index.js +1 -1
- package/dist/9382.index.js +2 -2
- package/dist/9560.index.js +1 -1
- package/dist/9579.index.js +1 -1
- package/dist/agentic-security.mjs +29 -29
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +8 -8
- package/src/dataflow/CLAUDE.md +6 -3
- package/src/dataflow/access-paths.js +12 -2
- package/src/dataflow/demand-query.js +279 -40
- package/src/dataflow/dep-fingerprint.js +1 -1
- package/src/dataflow/exploit-prover.js +6 -2
- package/src/dataflow/ir-cache.js +268 -0
- package/src/dataflow/privacy-taint.js +7 -0
- package/src/engine.js +62 -5
- package/src/ir/CLAUDE.md +4 -0
- package/src/ir/callgraph.js +36 -8
- package/src/ir/index.js +18 -4
- package/src/ir/parser-js.js +10 -0
- package/src/language/CLAUDE.md +13 -0
- package/src/language/nix-option-types.js +170 -0
- package/src/language/nix-script-taint.js +46 -3
- package/src/language/nix-secrets.js +20 -2
- package/src/lineage/CLAUDE.md +2 -1
- package/src/lineage/smt-orchestrator.js +47 -11
- package/src/lineage/smt-z3-backend.js +294 -0
- package/src/posture/CLAUDE.md +5 -1
- package/src/posture/business-logic.js +16 -7
- package/src/posture/dag-commands.js +442 -0
- package/src/posture/finding-defaults.js +19 -0
- package/src/posture/git-history.js +91 -1
- package/src/posture/proof-fabric/cli.js +1 -1
- package/src/posture/proof-fabric/container.js +2 -2
- package/src/posture/proof-fabric/generate-haskell.js +205 -0
- package/src/posture/proof-fabric/generate.js +6 -3
- package/src/posture/proof-fabric/reproducer.js +4 -2
- package/src/posture/proof-fabric/runner.js +4 -4
- package/src/sast/CLAUDE.md +2 -0
- package/src/sast/_call-args.js +70 -0
- package/src/sast/init-scope.js +87 -0
- package/src/sast/listen-bind-address.js +49 -0
- package/src/sast/redirect-target.js +106 -0
- package/src/sast/reflected-input.js +76 -0
- package/src/sast/send-file-root.js +27 -0
- package/src/sast/sensitive-data-logging.js +38 -1
- package/dist/119.index.js +0 -197
- package/dist/128.index.js +0 -129
- package/dist/1293.index.js +0 -5689
- package/dist/1336.index.js +0 -16331
- package/dist/1351.index.js +0 -133
- package/dist/1387.index.js +0 -271
- package/dist/1437.index.js +0 -23
- package/dist/1445.index.js +0 -110
- package/dist/1505.index.js +0 -151
- package/dist/1557.index.js +0 -293
- package/dist/1621.index.js +0 -311
- package/dist/1751.index.js +0 -160
- package/dist/1765.index.js +0 -727
- package/dist/1861.index.js +0 -690
- package/dist/1906.index.js +0 -433
- package/dist/208.index.js +0 -1631
- package/dist/2223.index.js +0 -159
- package/dist/2238.index.js +0 -295
- package/dist/2530.index.js +0 -152
- package/dist/2565.index.js +0 -116
- package/dist/2574.index.js +0 -127
- package/dist/2620.index.js +0 -345
- package/dist/2773.index.js +0 -98
- package/dist/2775.index.js +0 -32
- package/dist/2966.index.js +0 -171
- package/dist/2972.index.js +0 -87
- package/dist/2991.index.js +0 -131
- package/dist/3063.index.js +0 -833
- package/dist/3075.index.js +0 -227
- package/dist/3121.index.js +0 -297
- package/dist/3314.index.js +0 -263
- package/dist/3480.index.js +0 -1822
- package/dist/3481.index.js +0 -24
- package/dist/3506.index.js +0 -268
- package/dist/3533.index.js +0 -591
- package/dist/3653.index.js +0 -450
- package/dist/3740.index.js +0 -722
- package/dist/3822.index.js +0 -131
- package/dist/3868.index.js +0 -292
- package/dist/3918.index.js +0 -192
- package/dist/4090.index.js +0 -288
- package/dist/4113.index.js +0 -1286
- package/dist/4121.index.js +0 -1190
- package/dist/4142.index.js +0 -690
- package/dist/4168.index.js +0 -509
- package/dist/4281.index.js +0 -993
- package/dist/4336.index.js +0 -436
- package/dist/4387.index.js +0 -532
- package/dist/4413.index.js +0 -302
- package/dist/4476.index.js +0 -4510
- package/dist/4491.index.js +0 -133
- package/dist/4538.index.js +0 -185
- package/dist/4684.index.js +0 -319
- package/dist/4740.index.js +0 -165
- package/dist/4780.index.js +0 -209
- package/dist/4802.index.js +0 -225
- package/dist/4812.index.js +0 -379
- package/dist/4835.index.js +0 -126
- package/dist/494.index.js +0 -226
- package/dist/4991.index.js +0 -549
- package/dist/5199.index.js +0 -305
- package/dist/5216.index.js +0 -109
- package/dist/5439.index.js +0 -301
- package/dist/5443.index.js +0 -97
- package/dist/5496.index.js +0 -377
- package/dist/5738.index.js +0 -240
- package/dist/5801.index.js +0 -588
- package/dist/5858.index.js +0 -223
- package/dist/5933.index.js +0 -143
- package/dist/601.index.js +0 -23
- package/dist/6173.index.js +0 -273
- package/dist/6291.index.js +0 -1871
- package/dist/6312.index.js +0 -666
- package/dist/634.index.js +0 -16182
- package/dist/644.index.js +0 -87
- package/dist/6508.index.js +0 -283
- package/dist/6560.index.js +0 -645
- package/dist/6576.index.js +0 -422
- package/dist/6579.index.js +0 -271
- package/dist/6655.index.js +0 -3221
- package/dist/6745.index.js +0 -305
- package/dist/6818.index.js +0 -21642
- package/dist/6932.index.js +0 -1190
- package/dist/6972.index.js +0 -167
- package/dist/6995.index.js +0 -163
- package/dist/7249.index.js +0 -191
- package/dist/7372.index.js +0 -1197
- package/dist/7546.index.js +0 -541
- package/dist/76.index.js +0 -448
- package/dist/7879.index.js +0 -163
- package/dist/8025.index.js +0 -130
- package/dist/8054.index.js +0 -248
- package/dist/808.index.js +0 -1330
- package/dist/8097.index.js +0 -201
- package/dist/8126.index.js +0 -191
- package/dist/8178.index.js +0 -78
- package/dist/8382.index.js +0 -93
- package/dist/8408.index.js +0 -261
- package/dist/8540.index.js +0 -174
- package/dist/8635.index.js +0 -802
- package/dist/8688.index.js +0 -410
- package/dist/8699.index.js +0 -779
- package/dist/8891.index.js +0 -457
- package/dist/8916.index.js +0 -885
- package/dist/9146.index.js +0 -170
- package/dist/9168.index.js +0 -841
- package/dist/9292.index.js +0 -369
- package/dist/9422.index.js +0 -866
- package/dist/9440.index.js +0 -453
- package/dist/9479.index.js +0 -152
- package/dist/9481.index.js +0 -220
- package/dist/9539.index.js +0 -100
- package/dist/9547.index.js +0 -103
- package/dist/9692.index.js +0 -90
- package/dist/9800.index.js +0 -462
- package/dist/9899.index.js +0 -138
- package/dist/9981.index.js +0 -206
- package/dist/999.index.js +0 -117
- 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.
|
package/bin/agentic-security.js
CHANGED
|
@@ -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
|
|
3728
|
-
|
|
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': {
|
package/dist/1128.index.js
CHANGED
|
@@ -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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
697
|
-
const withIssuance = { ...bundle, issuance: buildLocalIssuance(publicKeyPem) };
|
|
698
|
-
const sig =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 };
|