@clear-capabilities/agentic-security-scanner 0.150.2 → 0.151.1

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 CHANGED
@@ -9,6 +9,141 @@
9
9
  > make the history less accurate, not more.
10
10
 
11
11
 
12
+ ## 0.151.1 - Adversarial premortem on the SARD benchmarking subsystem: 12 real findings, 12 real fixes
13
+
14
+ A structured adversarial premortem ("assume this subsystem has completely failed six months
15
+ from now — work backwards to why") was run against 0.151.0's SARD benchmarking work, producing
16
+ 19 findings across data/leakage, methodology, security, MLOps, and governance. All 12 findings
17
+ judged actionable were fixed and verified with real runs — none deferred, none papered over. Full
18
+ account, including one deliberate self-correction, in `bench/sard/IMPLEMENTATION_STATUS.md`'s
19
+ "Adversarial premortem + full remediation pass" section.
20
+
21
+ **Data & leakage:**
22
+ - Train/dev/test splits (`bench/sard/splits/*.json`) were computed and self-verified but never
23
+ consumed by scoring — every prior macro-F1 number was measured over the full corpus, not a
24
+ held-out split. `bench-realworld.js` now has a real `--split train|dev|test` flag filtering
25
+ both ground truth and actual findings symmetrically (8 new tests).
26
+ - `leakage-audit.mjs`'s own `CWE` term couldn't match Juliet's real fused naming convention
27
+ (`CWE89`, no separator) — `'CWE89'.match(/\bCWE\b/i)` returns null. Added a dedicated
28
+ `cwe-number` check independent of the plain term (2 new tests).
29
+ - PHP's identifier neutralization renamed exactly one hardcoded variable (`$tainted`) — an ad hoc
30
+ point-fix, not a designed protection. Generalized to the same hash-based rule pattern Java/C#
31
+ already use.
32
+
33
+ **Methodology:**
34
+ - The C# "26/32 CWE families, root-caused, not fixed" claim was re-investigated and found wrong
35
+ as stated: catalog entries already exist for several of the "uncovered" families. A first
36
+ re-investigation pass concluded the interprocedural taint engine was broken — before writing
37
+ that down, a controlled test matrix caught that the "evidence" for it was a coincidental
38
+ structural-detector hit, not real taint (a lowercase `request` parameter never matched the
39
+ catalog's case-sensitive source entry, in either the passing or failing fixture). With the
40
+ casing fixed, interprocedural taint propagation works correctly for this shape. The real
41
+ root cause of the C# corpus gap remains genuinely open. 3 new regression tests.
42
+ - Added the previously-missing adversarial (must-flip) mutation side to `mutate.mjs` —
43
+ `ADVERSARIAL_SOURCE_LITERALIZATION` replaces a tainted source's initializer with a hardcoded
44
+ literal and scores the opposite polarity from the existing metamorphic mutators. Found a real
45
+ detector precision gap on its first use: a structural Java SQL-injection detector fires on
46
+ "string built via concatenation" regardless of whether the value is genuinely tainted, while
47
+ the real taint engine correctly stays silent (5 new tests; the detector gap itself is
48
+ disclosed, not fixed here).
49
+ - `compare-baseline.mjs`'s flat 2-percentage-point regression tolerance either hid a real
50
+ regression on a high-support metric or failed on ordinary noise for a low-support one — exactly
51
+ the double-bind measured live this session (an unrelated holdout app's F1 moved between two
52
+ identical runs). Added `adaptiveTolerance()`, widening the band for low-support metrics only,
53
+ one-directionally (10 new tests, including the first automated end-to-end CLI proof for this
54
+ script in either direction).
55
+ - Headline metrics (100% Fully Verified Fix Rate, 100% Semantic Robustness Rate) now carry
56
+ explicit scope qualifiers in the implementation ledger, so neither can be quoted out of
57
+ context as a whole-corpus claim.
58
+
59
+ **Security:**
60
+ - PRD §56 "LLM Isolation" had zero implementation and zero test anywhere in this repo. Added
61
+ `test/sard-llm-isolation.test.js` against the real LLM-validator prompt builder: an
62
+ already-neutralized input produces a leakage-clean prompt; the scanner's own CWE classification
63
+ is legitimate content, not a leak; and, disclosed rather than hidden, the prompt builder has no
64
+ independent redaction of its own — isolation is entirely inherited from upstream neutralization.
65
+
66
+ **MLOps/CI:**
67
+ - `realworld-bench`'s `needs: synthetic-bench` meant a real, unrelated regression in
68
+ `synthetic-bench` (91.3%→85.9% F1, first observed 2026-09-08) silently skipped
69
+ `realworld-bench` for 5+ consecutive scheduled runs with zero alert. Removed the dependency —
70
+ both are already independently-tiered informational jobs.
71
+ - `compare-baseline.mjs` was local-only by design (no committed scores), leaving no CI-enforced
72
+ regression gate at all. Wired a GitHub Actions cache (never a git commit) into `sard-blind-smoke`
73
+ so a genuine regression fails the job for real, without touching the "no scores committed"
74
+ policy.
75
+ - The external-holdout generalization gate could never fail: every existing curated app's ground
76
+ truth was bootstrapped from a past scanner run, not built independently. Added
77
+ `bench/holdout-independent/tinymart/` — a small, hand-written app whose ground truth was
78
+ authored from its own source before the scanner ever ran against it. Proved the gate has real
79
+ teeth end to end: clean baseline, deliberately removed a vulnerability (gate correctly failed,
80
+ naming the app and the exact regression), reverted (gate returned to clean, numbers matched
81
+ byte-for-byte). 5 new tests.
82
+
83
+ No detection-engine changes in this release — every fix above is to the benchmarking/CI
84
+ infrastructure itself, not to `scanner/src/`.
85
+
86
+ ## 0.151.0 - SARD/Juliet benchmarking: leakage-clean scoring, macro-F1, mutation testing, and fix verification (SARD_AGENTIC_SECURITY_PRD.md)
87
+
88
+ Builds a full benchmarking subsystem against NIST SARD's Juliet (Java/C#) and PHP Vulnerability
89
+ Test Suite corpora, extending the pre-existing `bench-realworld.js` harness rather than
90
+ duplicating it. New: `bench/sard/` (dataset lockfile, PHP ingestion, leakage audit, macro-F1 +
91
+ per-CWE + CWE-confusion-matrix + localization-accuracy scoring, structural train/dev/test
92
+ splitting with a duplicate-crossing audit, semantic mutation testing, and independent fix
93
+ verification), plus a `--cwe` targeted/smoke-run flag and a new CI job (`sard-blind-smoke`)
94
+ measuring genuine leakage-clean detection quality separately from the pre-existing
95
+ non-blind `sard-juliet-java` job (which tracks a different thing: whether the corpus-shape-aware
96
+ fallback code still works, not detection quality).
97
+
98
+ **Real, measured numbers** (all reproduced, none quoted without a fresh run — see
99
+ `bench/sard/IMPLEMENTATION_STATUS.md` for the full ledger and every command that produced them):
100
+ Java macro-F1 45.6% (P=66.5%, R=36.9%, leakage-clean, vulnerability-level scoring); C# macro-F1
101
+ 8.4% (26/32 CWE families found to have zero detector coverage at all — a real, root-caused gap,
102
+ not yet fixed); Semantic Robustness Rate 100% (84/84 real mutations survived, sanity-checked
103
+ against a deliberately-safe negative control); Fully Verified Fix Rate 100% (32/32 real Java
104
+ weak-hash fixes); leakage reduced from 907,516 to 3 residual hits (99.9997%).
105
+
106
+ **Real engine fixes found via this benchmarking work** (general capability improvements, not
107
+ SARD-specific shortcuts — each verified with `bench:layer-recall`/`test:dataflow` before and
108
+ after):
109
+ - PHP: `$_SESSION`/`$_ENV` added as taint sources (previously entirely absent from the catalog),
110
+ `mysql_query()` given a real taint-dataflow sink (previously only a same-line structural
111
+ regex, missing the dominant assign-then-call-later shape). `bench:layer-recall` PHP taint
112
+ recall moved 12→13; baseline re-recorded (a real improvement, not silently left stale — this
113
+ gate compares for equality, not a floor, precisely to prevent that).
114
+ - Java: `BufferedReader.readLine()`/`Console.readLine()` and `ResultSet.getString`/`getObject`
115
+ added as taint sources, root-caused from real, evidence-based error-cluster analysis (grouping
116
+ false negatives by Juliet's own filename descriptor) rather than guessed.
117
+ - `deterministic-fix.js`'s weak-hash rule had a real Java coverage gap: its `applies()` gate
118
+ already matched Java CWE-327/328/916 findings, but `transform()` had no Java branch at all, so
119
+ every Java weak-hash finding silently produced no fix. Added
120
+ `MessageDigest.getInstance("MD5"|"SHA1")` → `"SHA-256"`.
121
+ - `bench-realworld.js` and `leakage-audit.mjs` both had an unconditional top-level `main()` (and,
122
+ in `bench-realworld.js`'s case, a separate top-level usage-check) with no `import.meta.url`
123
+ guard — importing either file's helper functions as a module silently ran the ENTIRE CLI
124
+ against the *importer's* own `process.argv`. Found the hard way when a new mutation-testing
125
+ script's own `--app`/`--cwe` flags happened to also be valid `bench-realworld.js` flags,
126
+ triggering an unwanted ~160s benchmark run as a side effect of a function import. Both files
127
+ now guard their top-level side effects the same way every other multi-purpose script in
128
+ `bench/sard/scripts/` already did.
129
+ - Root `CLAUDE.md` described `bench:layer-recall:check` as "a FLOOR, not an equality check...
130
+ silent on a rise" — stale documentation of a design the gate itself had already moved past
131
+ (its own code comment says "PRD F12.2 — compares for EQUALITY, not against a floor"). Fixed.
132
+ - `src/lineage/source-registry.js`'s `NO_PROVENANCE_OVERRIDES` table (Data Flow Explorer) had no
133
+ entries for the new `java-io-readline`/`java-resultset-getstring`/`java-resultset-getobject`
134
+ catalog sources, or for the pre-existing `php-session`/`php-env` sources — all five failed the
135
+ registry's own completeness guard (`source-registry.test.js`), which fails loudly rather than
136
+ silently producing an uncategorized graph node. Added (`user-input`/`database-read`/
137
+ `http-cookie`/`env-value` respectively); pinned coverage counts re-measured and updated
138
+ (185 total source entries, 87 `candidate`). Two `bench/sard/scripts/` helpers
139
+ (`analyze-errors.mjs`, `score-php.mjs`) were also wired into `scanner/package.json` after
140
+ `no-orphan-scripts.test.js` flagged them as unreachable from any npm script.
141
+
142
+ 14 new automated tests (`java-taint-flow.test.js` +3, `deterministic-fix.test.js` +3, new
143
+ `sard-leakage-pipeline.test.js` +8 covering PRD §62's literal leakage-injection list and a
144
+ synthetic-fixture integration test through the real neutralization pipeline).
145
+
146
+
12
147
 
13
148
  ## 0.150.2 - Two adversarial-review passes on 0.150.0's Ollama support: redaction, disclosure, and robustness fixes
14
149
 
@@ -2864,6 +2864,7 @@ const _internals = { CACHE_DIR, CVE_RE, _stalenessTier };
2864
2864
 
2865
2865
  const JS_EXT = /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i;
2866
2866
  const PY_EXT = /\.py$/i;
2867
+ const JAVA_EXT = /\.java$/i;
2867
2868
 
2868
2869
  // Each rule gates on the finding's cwe/family, then rewrites the whole-file
2869
2870
  // content. transform() returns the new content, or null when nothing changed
@@ -2875,12 +2876,22 @@ const RULES = [
2875
2876
  // swapping them all is safe; the verifier confirms the weak-hash finding is
2876
2877
  // gone and nothing worse appeared.
2877
2878
  applies: (f) => /CWE-(?:327|328|916)/.test(f.cwe || '') || /weak.?hash/i.test(f.family || ''),
2879
+ // SARD_AGENTIC_SECURITY_PRD.md Phase 8 bench work found this rule's
2880
+ // `applies()` gate matched Java findings (CWE-327/328) by cwe/family, but
2881
+ // `transform()` had no Java branch at all — every Java weak-hash finding
2882
+ // silently produced `null` (no fix), a coverage gap invisible from the
2883
+ // gate alone. `MessageDigest.getInstance("MD5"|"SHA1"|"SHA-1")` ->
2884
+ // `"SHA-256"` is the same class of context-independent literal swap as
2885
+ // the existing JS/Python branches (the algorithm name is a string
2886
+ // literal, not something requiring surrounding-code understanding).
2878
2887
  transform: (content, file) => {
2879
2888
  let out = content;
2880
2889
  if (JS_EXT.test(file)) {
2881
2890
  out = out.replace(/(\bcreateHash\s*\(\s*['"`])(?:md5|sha1)(['"`])/gi, '$1sha256$2');
2882
2891
  } else if (PY_EXT.test(file)) {
2883
2892
  out = out.replace(/\bhashlib\.(?:md5|sha1)\s*\(/g, 'hashlib.sha256(');
2893
+ } else if (JAVA_EXT.test(file)) {
2894
+ out = out.replace(/(\bMessageDigest\.getInstance\s*\(\s*")(?:MD5|SHA-?1)(")/gi, '$1SHA-256$2');
2884
2895
  }
2885
2896
  return out !== content ? out : null;
2886
2897
  },