@clear-capabilities/agentic-security-scanner 0.186.1 → 0.187.0
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 +24 -1
- package/bin/agentic-security.js +42 -17
- package/dist/{2066.index.js → 1602.index.js} +477 -9
- package/dist/9560.index.js +62 -5
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/{ca62ee93079be4c24eee.json → deabc47d0e27e21bf99b.json} +5 -5
- package/package.json +5 -5
- package/src/capabilities/decide.js +7 -1
- package/src/dataflow/CLAUDE.md +2 -0
- package/src/dataflow/catalog-haskell.js +7 -1
- package/src/dataflow/engine.js +16 -0
- package/src/dataflow/privacy-catalog-haskell.js +2 -1
- package/src/language/CLAUDE.md +19 -0
- package/src/language/compliance-map.js +6 -0
- package/src/language/context.js +20 -0
- package/src/language/haskell-disclosure.js +16 -4
- package/src/language/haskell-guards.js +130 -3
- package/src/language/haskell-ir.js +182 -10
- package/src/language/haskell-models.js +93 -4
- package/src/language/haskell-security-rules.js +112 -34
- package/src/language/haskell-syntax.js +10 -1
- package/src/language/haskell-web.js +44 -8
- package/src/language/support-registry.js +3 -2
- package/src/mcp/tools.js +54 -3
- package/src/posture/attack-taxonomy.js +10 -0
- package/src/posture/entrypoint-inventory.js +6 -2
- 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 +454 -0
- package/src/posture/proof-fabric/generate.js +15 -5
- package/src/posture/proof-fabric/oracles.js +21 -0
- package/src/posture/proof-fabric/reproducer.js +4 -2
- package/src/posture/proof-fabric/runner.js +4 -4
- package/src/posture/stable-id.js +2 -1
- package/src/report/index.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,29 @@
|
|
|
9
9
|
> make the history less accurate, not more.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.187.0 - Haskell parity work: wider exploit proof, broader SAST and taint, new unseen measurement sets
|
|
13
|
+
|
|
14
|
+
- **Haskell SAST and taint, generalised beyond the shapes they were written against.** New sources and sinks (Scotty, Yesod, WAI, Servant, Req, conduit, http-client), guards recognised by what they do rather than by name (host allow-lists, hand-written HTML escapers, `sh -c` scripts), SSRF reported at the send instead of the parse, and Haskell-scoped engine fixes for branches handed to combinators, `words`, `>>=` and common lowering forms. On unseen-v3, now a development set, SAST and taint reach 100%; that is a regression result, not a generalisation claim.
|
|
15
|
+
- **Honest unseen measurement.** A new set, unseen-v4 (78 forms, 13 families, measured once before this release's engine work), gives SAST recall 45.8% to 54.2% and taint precision 73.3% to 84.6%; taint recall is unchanged at 73.3%. Several families remain at 0 to 33% recall.
|
|
16
|
+
- **Real-code precision did not move.** On four pinned real Haskell projects (`bench/dag/independent-haskell/`, one LLM labeler, 60 of the flagged sites), default-scan precision is 2.3% (95% interval 0.4% to 12.1%), against 2.0% before. The new engine flags additional sites that are unlabeled and are not counted.
|
|
17
|
+
- **Alias fix.** A point-free alias of a library function (`tidy = takeFileName`, `run = callCommand`) was treated as an unresolved call, so a sanitizer alias left a flow unsanitized and a sink alias went undetected. Aliases of library functions are now expanded; a local binding with the same name disables the expansion.
|
|
18
|
+
- **Exploit proof.** Widened Haskell reproducer coverage and oracles.
|
|
19
|
+
- **Gates.** 56 Haskell mutation cases and 8 synthetic corpus entries (verdict-flip 460 of 460, corpus 228 of 228); layer-recall maps `.hs` and `.lhs`, taint attribution 126 to 134.
|
|
20
|
+
- **Parity outside the engine.** Haskell on the MCP `query_taint`, context, compliance mapping, capability decisions, attack taxonomy, entrypoint inventory and reports, with `docs/language-parity-haskell.md` listing what remains open.
|
|
21
|
+
- **Not done.** The container proof path is untested, `prove_finding` and `prove_fix` have no `.hs` harness builder, and `haskell-web-auth` route fixtures need a host with scotty, wai, servant and yesod to compile.
|
|
22
|
+
|
|
23
|
+
## 0.186.2 - Haskell exploit proof, a default-scan drift gate, and the measurements behind earlier claims
|
|
24
|
+
|
|
25
|
+
- **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.
|
|
26
|
+
- **`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.
|
|
27
|
+
- **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.
|
|
28
|
+
- **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.
|
|
29
|
+
- **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.
|
|
30
|
+
- **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.
|
|
31
|
+
- **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.
|
|
32
|
+
- **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.
|
|
33
|
+
- **The `v0.186.0` tag was removed.** That release was never published to npm.
|
|
34
|
+
|
|
12
35
|
## 0.186.1 - Git history annotation blames once per file, which restores scan speed (fix)
|
|
13
36
|
|
|
14
37
|
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.
|
|
@@ -19,7 +42,7 @@
|
|
|
19
42
|
|
|
20
43
|
## 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)
|
|
21
44
|
|
|
22
|
-
Everything new is Preview, opt-in and off by default.
|
|
45
|
+
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.
|
|
23
46
|
|
|
24
47
|
- **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.
|
|
25
48
|
- **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.
|
package/bin/agentic-security.js
CHANGED
|
@@ -3587,6 +3587,32 @@ async function cmdDigest(args) {
|
|
|
3587
3587
|
console.error('digest --slack <url> OR digest --discord <url>'); return 4;
|
|
3588
3588
|
}
|
|
3589
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
|
+
|
|
3590
3616
|
async function cmdFix(args) {
|
|
3591
3617
|
const id = args.flags.finding;
|
|
3592
3618
|
const isPreview = !!args.flags.preview;
|
|
@@ -3636,7 +3662,21 @@ async function cmdFix(args) {
|
|
|
3636
3662
|
return 4;
|
|
3637
3663
|
}
|
|
3638
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
|
+
}
|
|
3639
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
|
+
}
|
|
3640
3680
|
if (!res.ok) { console.error(`No verified fix for this finding (${res.status}${res.tier ? `, ${res.tier}` : ''}): ${res.reason || 'unsupported'}`); return 4; }
|
|
3641
3681
|
console.log(`${res.label || 'FIX'}${res.tier ? ` (${res.tier})` : ''}: ${res.explanation || ''}`.trim());
|
|
3642
3682
|
console.log(res.diff);
|
|
@@ -3740,23 +3780,8 @@ async function cmdFix(args) {
|
|
|
3740
3780
|
// --require-proof: the patch is applied ONLY if the original exploit is confirmed beforehand and demonstrably blocked
|
|
3741
3781
|
// afterwards (posture/proof-fabric/fix-proof.js). Missing reproducer, unavailable environment or an incomplete replay all refuse.
|
|
3742
3782
|
if (args.flags['require-proof']) {
|
|
3743
|
-
const
|
|
3744
|
-
|
|
3745
|
-
const { verifyFixWithProof, formatFixProof } = await import('../src/posture/proof-fabric/fix-proof.js');
|
|
3746
|
-
const { verifyFix } = await import('../src/posture/fix-verify.js');
|
|
3747
|
-
const loaded = resolveReproducer(scanRoot, f, args.flags.reproducer);
|
|
3748
|
-
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; }
|
|
3749
|
-
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.`);
|
|
3750
|
-
const files = { [f.file]: newContent };
|
|
3751
|
-
const staticVerification = await verifyFix({ scanRoot, originalFindingStableId: f.stableId || null, files, recordMetrics: false });
|
|
3752
|
-
// equivalent sibling exposures: the engine's own other findings of this kind, plus other places with the same code shape
|
|
3753
|
-
const sib = findSiblings({ root: scanRoot, finding: f, scanFindings: last.findings || [] });
|
|
3754
|
-
const siblings = sib.siblings.map((x) => ({ id: x.id ?? null, file: x.file, line: x.line, source: x.source }));
|
|
3755
|
-
if (sib.truncated.length) console.error(`--require-proof: the sibling search stopped early (${sib.truncated.join(', ')}); absence of siblings is not shown.`);
|
|
3756
|
-
const fp = await verifyFixWithProof({ finding: f, reproducer: loaded.reproducer, files, scanRoot, staticVerification, siblings, siblingSearchTruncated: sib.truncated });
|
|
3757
|
-
console.log(formatFixProof(fp));
|
|
3758
|
-
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 */ }
|
|
3759
|
-
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;
|
|
3760
3785
|
}
|
|
3761
3786
|
let result = await applyVerifiedFix({
|
|
3762
3787
|
scanRoot,
|