@clear-capabilities/agentic-security-scanner 0.139.1 → 0.141.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 +221 -0
- package/bin/agentic-security.js +40 -11
- package/dist/113.index.js +79 -3
- package/dist/178.index.js +1 -1
- package/dist/238.index.js +77 -1
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +12 -0
- package/dist/526.index.js +79 -3
- package/dist/637.index.js +1 -1
- package/dist/agentic-security.mjs +14 -14
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/ccpa.json +34 -7
- package/dist/compliance-frameworks/eu-ai-act.json +65 -14
- package/dist/compliance-frameworks/gdpr.json +56 -12
- package/dist/compliance-frameworks/hipaa-security-rule.json +68 -15
- package/dist/compliance-frameworks/nist-ai-600-1.json +57 -12
- package/dist/compliance-frameworks/nist-csf-2.json +78 -16
- package/dist/compliance-frameworks/nist-privacy-1-1.json +3 -0
- package/dist/compliance-frameworks/owasp-asvs-5.json +91 -20
- package/dist/compliance-frameworks/owasp-llm-top-10.json +89 -20
- package/package.json +16 -5
- package/src/dataflow/catalog.js +61 -0
- package/src/engine.js +281 -23
- package/src/mcp/tools.js +12 -0
- package/src/posture/accuracy-scorecard.js +57 -0
- package/src/posture/aibom.js +110 -1
- package/src/posture/auditor-walkthrough.js +137 -21
- package/src/posture/compliance-frameworks/ccpa.json +34 -7
- package/src/posture/compliance-frameworks/eu-ai-act.json +65 -14
- package/src/posture/compliance-frameworks/gdpr.json +56 -12
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +68 -15
- package/src/posture/compliance-frameworks/nist-ai-600-1.json +57 -12
- package/src/posture/compliance-frameworks/nist-csf-2.json +78 -16
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +3 -0
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +91 -20
- package/src/posture/compliance-frameworks/owasp-llm-top-10.json +89 -20
- package/src/posture/concurrency-checker.js +42 -5
- package/src/posture/coverage-strength.js +182 -0
- package/src/posture/epss.js +17 -1
- package/src/posture/family-registry.js +103 -0
- package/src/posture/family-resolve.js +47 -0
- package/src/posture/fix-coverage.js +113 -0
- package/src/posture/fix-metrics.js +76 -0
- package/src/posture/integrity.js +59 -8
- package/src/posture/mcp-rug-pull.js +144 -0
- package/src/posture/poc-generator.js +17 -1
- package/src/posture/poc-inprocess.js +217 -1
- package/src/posture/proof-coverage.js +162 -0
- package/src/posture/reachability-filter.js +44 -0
- package/src/posture/sbom.js +50 -7
- package/src/runScan.js +56 -5
- package/src/sast/CLAUDE.md +2 -2
- package/src/sast/claude-md-prompt-injection.js +47 -3
- package/src/sast/cloud-iam.js +23 -0
- package/src/sast/convention-deviation.js +66 -3
- package/src/sast/crypto-protocol.js +23 -0
- package/src/sast/dapp-frontend.js +20 -0
- package/src/sast/iac-cloud-templates.js +337 -0
- package/src/sast/k8s-admission.js +27 -0
- package/src/sast/ml-supply-chain.js +22 -0
- package/src/sast/ruby.js +132 -0
- package/src/sast/web3-advanced.js +26 -0
- package/src/sca/CLAUDE.md +21 -4
- package/src/sca/container.js +18 -1
- package/src/sca/dep-confusion.js +69 -3
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,227 @@
|
|
|
9
9
|
> make the history less accurate, not more.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
|
|
13
|
+
## 0.141.0 — Six new instruments, and the seven live bugs they found
|
|
14
|
+
|
|
15
|
+
The previous release fixed five bugs found by measuring. This one builds the
|
|
16
|
+
instruments that do the measuring for the surfaces that had none — SCA, secrets,
|
|
17
|
+
IaC, prompt injection, remediation, and the IDE/MCP surfaces — and then fixes
|
|
18
|
+
what they reported. Every number below is reproducible with a command in
|
|
19
|
+
`bench/*/README.md`.
|
|
20
|
+
|
|
21
|
+
None of the improvements came from tuning. They came from files that were never
|
|
22
|
+
read, patterns that could never fire, and versions that were silently rewritten.
|
|
23
|
+
|
|
24
|
+
### SCA never read a transitive dependency tree on any real project
|
|
25
|
+
|
|
26
|
+
`readTree` skipped **any file over 500 KB before deciding what kind of file it
|
|
27
|
+
was.** npm/cli's `package-lock.json` is 666 KB, next.js's `pnpm-lock.yaml` is
|
|
28
|
+
910 KB, magento2's `composer.lock` is 501 KB. On every project big enough for
|
|
29
|
+
supply-chain risk to matter, the lockfile was dropped and SCA fell back to
|
|
30
|
+
whatever exact versions appeared in `package.json` — direct dependencies only,
|
|
31
|
+
while the headline claim of the feature is transitive reachability.
|
|
32
|
+
|
|
33
|
+
Manifests now have their own, much larger cap. The 500 KB cap on **code** files
|
|
34
|
+
is unchanged: that one protects the analysis path and was never the problem.
|
|
35
|
+
|
|
36
|
+
Two more admission gaps in the same area: `go.sum` was never admitted though
|
|
37
|
+
`_parseGoSum` and its dispatch entry had always existed, and only the exact
|
|
38
|
+
basename `requirements.txt` was matched — so `requirements/dev.txt`, which is
|
|
39
|
+
what pallets/flask ships, scored 0 of 11.
|
|
40
|
+
|
|
41
|
+
**Measured effect (`bench/sca-replay`, 13 real repos, 7 ecosystems, labels from
|
|
42
|
+
the advisory database via readers that share no code with the engine): version
|
|
43
|
+
recall 10.89% → 77.92% at 100% precision, held-out 84.29%.**
|
|
44
|
+
|
|
45
|
+
### Go dependency versions were rewritten into versions that do not exist
|
|
46
|
+
|
|
47
|
+
`v0.0.0-20210903162142-ad29c8ab022f` became `0.0.0` in three separate places —
|
|
48
|
+
not a shorter version but a different one, collapsing every pseudo-versioned
|
|
49
|
+
module in a tree onto a single key. **Go went from 5.28% to 100%** once versions
|
|
50
|
+
survived intact.
|
|
51
|
+
|
|
52
|
+
This also corrupted the emitted SBOM, which is the worse half: an SBOM saying
|
|
53
|
+
`golang.org/x/net@0.0.0` is wrong in a document other people are supposed to
|
|
54
|
+
rely on.
|
|
55
|
+
|
|
56
|
+
### The typosquat detector reported 166 findings, none of them typosquats
|
|
57
|
+
|
|
58
|
+
Across 13 real repositories, at critical and high severity: `ms ~ ws`,
|
|
59
|
+
`acorn ~ cors`, `ajv ~ ava`, `six ~ tox`, `arg ~ yargs`, `bail ~ babel`. Every
|
|
60
|
+
one is a legitimate, popular package; `ms` is a top-50 npm package.
|
|
61
|
+
|
|
62
|
+
Absolute edit distance is meaningless on short names — two edits on a
|
|
63
|
+
four-character name changes half of it, and every two-character package is one
|
|
64
|
+
edit from every other. Now Damerau-Levenshtein, so a transposition (`lodahs` for
|
|
65
|
+
`lodash`, the commonest real typo) costs 1 rather than 2, gated on
|
|
66
|
+
`distance / min(len) ≤ 0.25`.
|
|
67
|
+
|
|
68
|
+
### The VS Code extension had never been able to find the scanner
|
|
69
|
+
|
|
70
|
+
It looked for the bundle under a hardcoded
|
|
71
|
+
`…/agentic-security/0.1.0/scanner/dist/…`, and Claude Code caches a plugin under
|
|
72
|
+
its **plugin** version — 0.128.2, 0.136.9, 0.139.1, never 0.1.0. `0.1.0` was the
|
|
73
|
+
extension's own version pasted into the wrong path, so the fallback could not
|
|
74
|
+
resolve on any install and every user got *"scanner not found."*
|
|
75
|
+
|
|
76
|
+
Nothing tested it, because the function read `vscode.workspace` and could not be
|
|
77
|
+
imported outside a VS Code host. The resolver is now a pure function that
|
|
78
|
+
discovers the version instead of hardcoding one, prefers `CLAUDE_PLUGIN_ROOT`,
|
|
79
|
+
and is covered by 15 tests plus a CI job that fails on a stale committed bundle.
|
|
80
|
+
|
|
81
|
+
### Three IaC formats had no rules at all
|
|
82
|
+
|
|
83
|
+
CloudFormation, Bicep and Helm chart values scored **0** against every control
|
|
84
|
+
tested — not weak rules, no rules, and for CloudFormation no file admission
|
|
85
|
+
either, since a template is a `.yaml` that no path predicate recognises.
|
|
86
|
+
|
|
87
|
+
`bench/iac-coverage` scores **verdict flip**: a control counts only when the
|
|
88
|
+
misconfigured variant fires *and* the hardened one stays silent. That caught
|
|
89
|
+
something a recall-only bench never would — `FROM ubuntu@sha256:…`, the most
|
|
90
|
+
tightly pinned form a Dockerfile can use, was reported as *"ubuntu:latest
|
|
91
|
+
(floating tag)"* because the digest was matched but not captured. A false
|
|
92
|
+
positive on the hardened configuration tells the people who did the right thing
|
|
93
|
+
that they did the wrong one.
|
|
94
|
+
|
|
95
|
+
**Coverage 8/14 → 23/26**, with the three still open marked as needing semantic
|
|
96
|
+
analysis rather than another pattern.
|
|
97
|
+
|
|
98
|
+
### Credential patterns that could never fire
|
|
99
|
+
|
|
100
|
+
`CRED_PREFILTER` is a whole-file gate: `scanCredentials` returns early unless
|
|
101
|
+
that one regex matches, so a pattern whose trigger token is missing is dead code
|
|
102
|
+
however correct it is. The generic "Password in URL" rule was in exactly that
|
|
103
|
+
state.
|
|
104
|
+
|
|
105
|
+
Also absent entirely: `postgres://user:pass@host/db` and `mongodb+srv://…` —
|
|
106
|
+
among the commonest real leaks there are — plus GitLab, DigitalOcean, Azure
|
|
107
|
+
Storage, Supabase and HubSpot tokens. Only `jdbc:` was covered.
|
|
108
|
+
|
|
109
|
+
**Format coverage 60% → 92.11%, with correct silence at 28/28** on a hard
|
|
110
|
+
negative set of lockfile integrity fields, git SHAs, content digests, Terraform
|
|
111
|
+
state ids and a security rule file that defines key formats.
|
|
112
|
+
|
|
113
|
+
### Prompt-injection patterns that were correct and far too literal
|
|
114
|
+
|
|
115
|
+
The override rule required the object noun to be one of seven words, so *"Forget
|
|
116
|
+
all previous **tasks**"* and *"Ignore all preceding **orders**"* missed.
|
|
117
|
+
Exfiltration could not match *"show me all your prompt texts"*.
|
|
118
|
+
|
|
119
|
+
**Recall 6.08% → 18.25% against `deepset/prompt-injections` (Apache-2.0, 263
|
|
120
|
+
injections and 399 legitimate prompts), precision and correct-silence at 100%
|
|
121
|
+
throughout.** Reported per technique, because an aggregate hides which one is
|
|
122
|
+
weak: role-play 100%, exfiltration 60%, override 41.86%.
|
|
123
|
+
|
|
124
|
+
And the engine caught its own author — the widened pattern was flagged by this
|
|
125
|
+
project's own ReDoS detector on the self-scan gate, twice, before it was
|
|
126
|
+
rewritten as a flat alternation that scores identically.
|
|
127
|
+
|
|
128
|
+
## Measured, published, and deliberately not "fixed"
|
|
129
|
+
|
|
130
|
+
- **Fix synthesis produces a patch for 0 of 6** real true positives on
|
|
131
|
+
third-party code (`bench/fix-correctness`, scored against the upstream fix
|
|
132
|
+
commit). The deterministic synthesizer has two rules, both JS/Python; the real
|
|
133
|
+
population is injection and authorization across seven languages. Widening it
|
|
134
|
+
to guess at an authorization check would produce patches that pass
|
|
135
|
+
verification while changing behaviour.
|
|
136
|
+
- **German prompt injection scores 2.30% against English's 28.57%.** Patching it
|
|
137
|
+
because *this corpus* is German would be fitting to the benchmark.
|
|
138
|
+
- **Datadog, Vercel and Algolia keys stay undetected.** They are a bare run of
|
|
139
|
+
hex; a pattern for "32 hex characters" would fire on every content digest and
|
|
140
|
+
checksum in existence.
|
|
141
|
+
- **`CODEGEN` produces nothing on any of the five advisories in its own header.**
|
|
142
|
+
Measured dead, and left for a deliberate retire-or-fix decision rather than
|
|
143
|
+
removed silently.
|
|
144
|
+
|
|
145
|
+
## Also
|
|
146
|
+
|
|
147
|
+
- The advisory miner **was not paginating** — the API ignores `&page=N` — so the
|
|
148
|
+
evaluation population was capped at ~100 advisories per ecosystem by
|
|
149
|
+
construction. Fixed via the `Link` cursor: **315 → 1004 entries**, Ruby 32 →
|
|
150
|
+
250, Kotlin 0 → 2.
|
|
151
|
+
- `File.join(<root>, …, <variable>)` reaching a filesystem call with no traversal
|
|
152
|
+
guard is now detected for Ruby (CWE-22), the dominant real-world shape the
|
|
153
|
+
existing rule could not reach.
|
|
154
|
+
- A guard-shaped method **name** on a declaration line (`def check_static_cache(`)
|
|
155
|
+
no longer counts as a containment guard, which was silently dropping path
|
|
156
|
+
findings inside any method called `check*`/`validate*`/`ensure*`.
|
|
157
|
+
- MCP is now smoke-tested end to end over stdio through the shipped binary, with
|
|
158
|
+
both write tools asserted to refuse out-of-tree paths in both directions.
|
|
159
|
+
|
|
160
|
+
## 0.140.0 — Five shipped bugs, found by measuring instead of reading
|
|
161
|
+
|
|
162
|
+
Every fix here is a defect that was live in 0.139.1. None came from the feature
|
|
163
|
+
backlog; all five came from measuring the engine against real code and taking
|
|
164
|
+
failing signals seriously instead of explaining them away.
|
|
165
|
+
|
|
166
|
+
### `scan --format sarif` produced INVALID SARIF in CI
|
|
167
|
+
|
|
168
|
+
The CLI dispatches every command as `process.exit(await cmdX(args))`, and
|
|
169
|
+
`process.exit()` does not flush an asynchronous stdout. stdout is asynchronous
|
|
170
|
+
exactly when it is a pipe — every `> results.sarif`, `| jq`, and CI capture — so
|
|
171
|
+
output was discarded at the 64 KiB pipe boundary, mid-token, with a normal exit
|
|
172
|
+
status. On one directory that was 65,536 bytes emitted of 390,177: roughly 83%
|
|
173
|
+
of the document silently dropped.
|
|
174
|
+
|
|
175
|
+
**If you upload SARIF to code scanning, this affected you on any project large
|
|
176
|
+
enough to matter.** A TTY and a file both flush synchronously, which is why it
|
|
177
|
+
looked fine by hand and broke in automation.
|
|
178
|
+
|
|
179
|
+
### LLM01 — Prompt Injection — could never fail
|
|
180
|
+
|
|
181
|
+
Detectors emit the finding family as `<family>-<rule-slug>`
|
|
182
|
+
(`prompt-injection-http-user-input-in-llm-`), while the compliance evaluator
|
|
183
|
+
resolved `family:prompt-injection` as an exact key. It matched nothing, so the
|
|
184
|
+
control reported as evidenced no matter what the scan found. The first control
|
|
185
|
+
of the OWASP LLM Top 10 was structurally incapable of failing, along with ASVS
|
|
186
|
+
V5.1 and NIST AI 600-1 MG-3.2-005.
|
|
187
|
+
|
|
188
|
+
Two further compliance defects in the same matching code: an empty family
|
|
189
|
+
bucket rendered as `✓ no open critical/high findings`, so two controls (ASVS
|
|
190
|
+
V7.1, NIST Privacy CT.DP-P1) read `present` on every scan of every project; and
|
|
191
|
+
a first attempt at fixing that wrongly declared four live families
|
|
192
|
+
unevidenceable, degrading 15 working controls. Both directions are now gated —
|
|
193
|
+
a control with no possible evidence cannot read `present`, and a family with a
|
|
194
|
+
producer cannot be declared a gap.
|
|
195
|
+
|
|
196
|
+
### `--deterministic` did not produce deterministic output
|
|
197
|
+
|
|
198
|
+
Four of ten emitted formats differed run to run: CycloneDX/SPDX document ids and
|
|
199
|
+
a CycloneDX bom-ref fallback from `crypto.randomUUID()`, a PoC marker from
|
|
200
|
+
`Math.random()`, and per-file wall-clock timings (which also determined the
|
|
201
|
+
sort ORDER, so blanking the values alone would not have been enough).
|
|
202
|
+
|
|
203
|
+
**An attestation over an SBOM was therefore unverifiable** — the point of
|
|
204
|
+
signing an artifact is that someone can regenerate and compare it.
|
|
205
|
+
|
|
206
|
+
### 62% of concurrency findings on Go were false positives
|
|
207
|
+
|
|
208
|
+
The lock guard matched a bare receiver (`defer mu.Unlock()`) but not a qualified
|
|
209
|
+
one (`defer s.mu.Unlock()`), which is how a mutex held as a struct field is
|
|
210
|
+
always written. The acquire pattern always matched the qualified form, so the
|
|
211
|
+
two halves of the rule had disagreed since it was written — and the most
|
|
212
|
+
idiomatic CORRECT code was the most likely to be reported. Measured: 170 of 273
|
|
213
|
+
findings on a Go sample were false positives. Those findings also carried no
|
|
214
|
+
CWE, so they were invisible to every CWE-keyed report.
|
|
215
|
+
|
|
216
|
+
### New gates
|
|
217
|
+
|
|
218
|
+
- `test/stdout-flush.test.js` — spawns the real CLI through a real pipe
|
|
219
|
+
- `test/format-determinism.test.js` — every emitted format, byte-compared
|
|
220
|
+
- `test/compliance-mapping-liveness.test.js` — both vacuous-pass directions
|
|
221
|
+
- `test/concurrency-cwe.test.js` — per-lock guard discrimination
|
|
222
|
+
- `bench/family-producers/OBSERVED.json` — 213 families observed across 331
|
|
223
|
+
real scan roots, recorded explicitly as a LOWER BOUND
|
|
224
|
+
|
|
225
|
+
### Scope
|
|
226
|
+
|
|
227
|
+
The world-class-harness PRD is **partially delivered**. F10.5 (determinism as a
|
|
228
|
+
published property) is complete. F10.2 is half done — the enforcement half
|
|
229
|
+
landed; the measurement half needs detectors to declare their families, because
|
|
230
|
+
this release proved no textual search can enumerate them. Roughly 35 PRD items
|
|
231
|
+
remain, several blocked on design decisions rather than implementation.
|
|
232
|
+
|
|
12
233
|
## 0.139.1 — The same scanner, published with provenance
|
|
13
234
|
|
|
14
235
|
**The shipped artifact is functionally identical to 0.139.0.** Only two commits
|
package/bin/agentic-security.js
CHANGED
|
@@ -9,6 +9,35 @@ const __require = createRequire(import.meta.url);
|
|
|
9
9
|
const PKG_VERSION = __require('../package.json').version;
|
|
10
10
|
import { signLastScan as _signLastScan, verifyLastScan as _verifyLastScanShared } from '../src/posture/integrity.js';
|
|
11
11
|
import { runScan } from '../src/runScan.js';
|
|
12
|
+
|
|
13
|
+
// Every command is dispatched as `process.exit(await cmdX(args))`, and
|
|
14
|
+
// process.exit() does NOT flush an asynchronous stdout. stdout is asynchronous
|
|
15
|
+
// whenever it is a PIPE — which is every `> file`, `| jq`, and CI capture — so
|
|
16
|
+
// anything still buffered when the process exits is discarded at the pipe
|
|
17
|
+
// boundary: 64 KiB on macOS and Linux.
|
|
18
|
+
//
|
|
19
|
+
// That silently truncated `scan --format sarif` mid-token for any project
|
|
20
|
+
// large enough to matter, i.e. the primary CI integration path, while still
|
|
21
|
+
// exiting with a normal status. The consumer sees a JSON parse error with no
|
|
22
|
+
// connection to its cause, or ingests a partial finding set.
|
|
23
|
+
//
|
|
24
|
+
// fs.writeSync(1, …) hands the bytes to the OS before returning, so a later
|
|
25
|
+
// exit cannot lose them. A non-blocking pipe can still short-write or raise
|
|
26
|
+
// EAGAIN, hence the loop — a partial write that is not retried is the same
|
|
27
|
+
// truncation bug wearing a different hat.
|
|
28
|
+
function writeStdout(s) {
|
|
29
|
+
const buf = Buffer.from(String(s), 'utf8');
|
|
30
|
+
let off = 0;
|
|
31
|
+
while (off < buf.length) {
|
|
32
|
+
try {
|
|
33
|
+
off += fs.writeSync(1, buf, off, buf.length - off);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
if (e.code === 'EAGAIN') continue; // pipe full; the reader will drain it
|
|
36
|
+
if (e.code === 'EPIPE') return; // reader closed (`| head`) — not our error
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
12
41
|
import { toJSON, toMarkdown, toSARIF, toSTIX, toCSV, toJUnit, toCLI, toCLIByProfile, toShipVerdict, toProTable, toHTML, toSummary, toVex, exitCodeFor, normalizeFindings } from '../src/report/index.js';
|
|
13
42
|
import { toCycloneDX, toSPDX } from '../src/posture/sbom.js';
|
|
14
43
|
import { toPBOM } from '../src/sast/pipeline.js';
|
|
@@ -679,7 +708,7 @@ async function cmdScan(args) {
|
|
|
679
708
|
}
|
|
680
709
|
|
|
681
710
|
if (output) await fsp.writeFile(output, body);
|
|
682
|
-
else
|
|
711
|
+
else writeStdout(body + '\n');
|
|
683
712
|
|
|
684
713
|
// Persist last scan for /security-fix and /security-report
|
|
685
714
|
const { isSafeStateDir: _isSafeStateDir, stateWritesEnabled: _writesOnScan } = await import('../src/posture/state-dir.js');
|
|
@@ -1251,7 +1280,7 @@ async function cmdHarness(args) {
|
|
|
1251
1280
|
body += `\n\nHarnesses discovered: ${present.join(', ')}${includeHome ? ' (project + ~/)' : ' (project only)'}\n`;
|
|
1252
1281
|
}
|
|
1253
1282
|
if (args.flags.output) await fsp.writeFile(args.flags.output, body);
|
|
1254
|
-
else
|
|
1283
|
+
else writeStdout(body + '\n');
|
|
1255
1284
|
return exitCodeFor(scan);
|
|
1256
1285
|
}
|
|
1257
1286
|
|
|
@@ -1746,7 +1775,7 @@ async function cmdCompliance(args) {
|
|
|
1746
1775
|
|
|
1747
1776
|
if (args.flags.list) {
|
|
1748
1777
|
const fws = listFrameworks(scanRoot);
|
|
1749
|
-
if (fmt === 'json') {
|
|
1778
|
+
if (fmt === 'json') { writeStdout(JSON.stringify(fws, null, 2) + '\n'); return 0; }
|
|
1750
1779
|
for (const f of fws) console.log(` ${f.id.padEnd(20)} ${f.name} [${f.source}]`);
|
|
1751
1780
|
return 0;
|
|
1752
1781
|
}
|
|
@@ -1780,7 +1809,7 @@ async function cmdCompliance(args) {
|
|
|
1780
1809
|
|
|
1781
1810
|
const gapsOnly = !!args.flags.gap;
|
|
1782
1811
|
if (fmt === 'json') {
|
|
1783
|
-
|
|
1812
|
+
writeStdout(JSON.stringify(gapsOnly ? { ...r, controls: r.controls.filter(c => c.bucket === 'gap') } : r, null, 2) + '\n');
|
|
1784
1813
|
} else if (fmt === 'md') {
|
|
1785
1814
|
console.log(fs.readFileSync(statePath(scanRoot, 'privacy-framework.md'), 'utf8'));
|
|
1786
1815
|
} else {
|
|
@@ -2018,7 +2047,7 @@ async function cmdFix(args) {
|
|
|
2018
2047
|
|
|
2019
2048
|
// Default mode: print the canonical template (back-compat — security-fixer subagent applies it).
|
|
2020
2049
|
if (!isPreview && !isApply) {
|
|
2021
|
-
|
|
2050
|
+
writeStdout(JSON.stringify(f, null, 2) + '\n');
|
|
2022
2051
|
if (f.fix?.code) { console.log('\n--- suggested patch ---\n'); console.log(f.fix.code); }
|
|
2023
2052
|
console.log('\nUse --preview to see a diff, or --apply to apply directly.');
|
|
2024
2053
|
return 0;
|
|
@@ -2247,7 +2276,7 @@ async function main() {
|
|
|
2247
2276
|
const { analyzeTranscript, formatCacheReport } = await import('../src/posture/cache-economics.js');
|
|
2248
2277
|
const projectDir = path.resolve(args.flags.root || process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
2249
2278
|
const result = analyzeTranscript({ transcriptPath: args.flags.transcript, projectDir });
|
|
2250
|
-
if (args.flags.json)
|
|
2279
|
+
if (args.flags.json) writeStdout(JSON.stringify(result, null, 2) + '\n');
|
|
2251
2280
|
else console.log(formatCacheReport(result));
|
|
2252
2281
|
process.exit(0);
|
|
2253
2282
|
}
|
|
@@ -2291,7 +2320,7 @@ async function main() {
|
|
|
2291
2320
|
});
|
|
2292
2321
|
if (args.flags.json) {
|
|
2293
2322
|
// Stringify Set/etc. safely.
|
|
2294
|
-
|
|
2323
|
+
writeStdout(JSON.stringify(r, null, 2) + '\n');
|
|
2295
2324
|
} else if (!r.ok) {
|
|
2296
2325
|
console.error(`cve-watch: ${r.reason || 'failed'}`);
|
|
2297
2326
|
}
|
|
@@ -2307,7 +2336,7 @@ async function main() {
|
|
|
2307
2336
|
const headRef = args.flags.head || args.flags.h || 'HEAD';
|
|
2308
2337
|
if (!baseRef) { console.error('pr-delta: --base <ref> is required'); process.exit(2); }
|
|
2309
2338
|
const delta = await computePrDelta(path.resolve(root), { baseRef, headRef });
|
|
2310
|
-
if (args.flags.json)
|
|
2339
|
+
if (args.flags.json) writeStdout(JSON.stringify(delta, null, 2) + '\n');
|
|
2311
2340
|
else console.log(renderPrDeltaText(delta));
|
|
2312
2341
|
// Exit non-zero if any critical/high introduced (useful as CI gate).
|
|
2313
2342
|
const i = delta.summary?.introduced || {};
|
|
@@ -2359,7 +2388,7 @@ async function main() {
|
|
|
2359
2388
|
const repo = args.flags.repo;
|
|
2360
2389
|
if (!repo) { console.error('leaderboard-row: --repo <owner/name> is required'); process.exit(2); }
|
|
2361
2390
|
const row = leaderboardRowFor({ scanRoot: path.resolve(root), repo });
|
|
2362
|
-
|
|
2391
|
+
writeStdout(JSON.stringify(row, null, 2) + '\n');
|
|
2363
2392
|
process.exit(0);
|
|
2364
2393
|
}
|
|
2365
2394
|
case 'history': {
|
|
@@ -2372,7 +2401,7 @@ async function main() {
|
|
|
2372
2401
|
since: args.flags.since || '6.months',
|
|
2373
2402
|
interval: args.flags.interval || '1.month',
|
|
2374
2403
|
});
|
|
2375
|
-
if (args.flags.json)
|
|
2404
|
+
if (args.flags.json) writeStdout(JSON.stringify(r, null, 2) + '\n');
|
|
2376
2405
|
else if (r.error) console.error(`history: ${r.error}`);
|
|
2377
2406
|
else {
|
|
2378
2407
|
console.log(`Scanned ${r.refs.length} refs.`);
|
|
@@ -2408,7 +2437,7 @@ async function main() {
|
|
|
2408
2437
|
? (Array.isArray(args.flags.remove) ? args.flags.remove : [args.flags.remove])
|
|
2409
2438
|
: [];
|
|
2410
2439
|
const r = await runWhatIf(path.resolve(root), { overlays, remove });
|
|
2411
|
-
if (args.flags.json)
|
|
2440
|
+
if (args.flags.json) writeStdout(JSON.stringify(r, null, 2) + '\n');
|
|
2412
2441
|
else {
|
|
2413
2442
|
console.log(`baseline: ${r.baselineFindings} findings`);
|
|
2414
2443
|
console.log(`what-if: ${r.whatIfFindings} findings (delta ${r.delta >= 0 ? '+' : ''}${r.delta})`);
|
package/dist/113.index.js
CHANGED
|
@@ -10,7 +10,7 @@ export const modules = {
|
|
|
10
10
|
/* harmony export */ fixDurationReport: () => (/* binding */ fixDurationReport),
|
|
11
11
|
/* harmony export */ renderFixDurationSummary: () => (/* binding */ renderFixDurationSummary)
|
|
12
12
|
/* harmony export */ });
|
|
13
|
-
/* unused harmony exports FIX_STAGES, loadFixAttempts, bucketOf, summarizeFixDurations, _internals */
|
|
13
|
+
/* unused harmony exports FIX_STAGES, loadFixAttempts, bucketOf, summarizeFixDurations, _internals, summarizeFixAxes, renderFixAxes */
|
|
14
14
|
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
|
|
15
15
|
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
|
|
16
16
|
/* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1174);
|
|
@@ -213,6 +213,82 @@ function renderFixDurationSummary(sum) {
|
|
|
213
213
|
const _internals = { _dist, _pct, RELIABLE_N };
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
// ── PRD F6.1 — score fixes on THREE AXES, not one ──────────────────────────
|
|
217
|
+
//
|
|
218
|
+
// The three axes the PRD names:
|
|
219
|
+
// (a) does the finding disappear — the rescan leg
|
|
220
|
+
// (b) does the project's own suite pass — the tests leg
|
|
221
|
+
// (c) does an independent verifier agree — the PoC re-check leg
|
|
222
|
+
//
|
|
223
|
+
// All three were already computed by verifyFixCore and then collapsed into one
|
|
224
|
+
// boolean, which is the problem: **(a) alone is satisfiable by deleting code.**
|
|
225
|
+
// A patch that removes the vulnerable function passes the rescan, has nothing
|
|
226
|
+
// left to fail, and — on a project with no detectable test suite — reaches
|
|
227
|
+
// ok:true having proven only that the detector went quiet.
|
|
228
|
+
//
|
|
229
|
+
// Reporting the axes separately makes that visible. `aOnly` is the number that
|
|
230
|
+
// matters most and the one nobody was publishing: attempts that satisfied ONLY
|
|
231
|
+
// the disappearance axis. A high aOnly with a high headline is the shape of a
|
|
232
|
+
// remediation feature that is deleting code and calling it a fix.
|
|
233
|
+
function summarizeFixAxes(attempts) {
|
|
234
|
+
const list = Array.isArray(attempts) ? attempts.filter(Boolean) : [];
|
|
235
|
+
const d = list.length;
|
|
236
|
+
|
|
237
|
+
const rate = (pred) => ({ n: list.filter(pred).length, d });
|
|
238
|
+
|
|
239
|
+
// Each axis is judged INDEPENDENTLY of the overall verdict, so a leg that
|
|
240
|
+
// passed inside a failed attempt still counts for its own axis. Reading them
|
|
241
|
+
// off `ok` would make the three axes three copies of the same number.
|
|
242
|
+
const findingDisappeared = rate((a) => a.rescanOk === true || (a.ok === true && a.rescanOk !== false));
|
|
243
|
+
const testsStillPass = rate((a) => a.testsRan === true && a.testsOk !== false);
|
|
244
|
+
const verifierAgrees = rate((a) => a.pocOk === true);
|
|
245
|
+
|
|
246
|
+
const satisfiesAll = rate((a) =>
|
|
247
|
+
(a.rescanOk === true || (a.ok === true && a.rescanOk !== false))
|
|
248
|
+
&& a.testsRan === true && a.testsOk !== false
|
|
249
|
+
&& a.pocOk === true);
|
|
250
|
+
|
|
251
|
+
// The honesty number: disappearance WITHOUT either corroborating axis.
|
|
252
|
+
const aOnly = rate((a) => {
|
|
253
|
+
const disappeared = a.rescanOk === true || (a.ok === true && a.rescanOk !== false);
|
|
254
|
+
const corroborated = (a.testsRan === true && a.testsOk !== false) || a.pocOk === true;
|
|
255
|
+
return disappeared && !corroborated;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
total: d,
|
|
260
|
+
findingDisappeared,
|
|
261
|
+
testsStillPass,
|
|
262
|
+
verifierAgrees,
|
|
263
|
+
satisfiesAll,
|
|
264
|
+
aOnly,
|
|
265
|
+
meaning:
|
|
266
|
+
'findingDisappeared = the detector went quiet; testsStillPass = the project suite ran AND passed; '
|
|
267
|
+
+ 'verifierAgrees = an independent PoC re-check confirmed the hole is shut. '
|
|
268
|
+
+ 'aOnly counts attempts that satisfied ONLY disappearance — the shape a code-deleting "fix" produces.',
|
|
269
|
+
caveat: d === 0
|
|
270
|
+
? 'no attempts recorded; every rate is 0/0 and means nothing'
|
|
271
|
+
: 'rates carry {n,d}; a small d is indicative, not settled',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Markdown for a report. Denominators always attached. */
|
|
276
|
+
function renderFixAxes(sum) {
|
|
277
|
+
if (!sum || !sum.total) return '_No fix attempts recorded._\n';
|
|
278
|
+
const row = (label, r, note) => `| ${label} | ${r.n}/${r.d} | ${note} |`;
|
|
279
|
+
return [
|
|
280
|
+
'| Axis | Rate | Meaning |',
|
|
281
|
+
'|---|---|---|',
|
|
282
|
+
row('(a) finding disappeared', sum.findingDisappeared, 'the detector went quiet'),
|
|
283
|
+
row('(b) project tests pass', sum.testsStillPass, 'the suite RAN and passed'),
|
|
284
|
+
row('(c) verifier agrees', sum.verifierAgrees, 'an independent PoC re-check confirmed it'),
|
|
285
|
+
row('all three', sum.satisfiesAll, 'the only row that means "fixed"'),
|
|
286
|
+
row('(a) ALONE', sum.aOnly, 'satisfiable by deleting code — watch this number'),
|
|
287
|
+
'',
|
|
288
|
+
].join('\n');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
216
292
|
/***/ }),
|
|
217
293
|
|
|
218
294
|
/***/ 4113:
|
|
@@ -415,8 +491,8 @@ var external_node_child_process_ = __webpack_require__(1421);
|
|
|
415
491
|
var external_node_fs_ = __webpack_require__(3024);
|
|
416
492
|
// EXTERNAL MODULE: external "node:path"
|
|
417
493
|
var external_node_path_ = __webpack_require__(6760);
|
|
418
|
-
// EXTERNAL MODULE: ./src/engine.js +
|
|
419
|
-
var engine = __webpack_require__(
|
|
494
|
+
// EXTERNAL MODULE: ./src/engine.js + 200 modules
|
|
495
|
+
var engine = __webpack_require__(3474);
|
|
420
496
|
;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
|
|
421
497
|
// Deterministic honesty gates on fix / finding output (#7).
|
|
422
498
|
//
|
package/dist/178.index.js
CHANGED
|
@@ -13,7 +13,7 @@ export const modules = {
|
|
|
13
13
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
14
14
|
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
|
|
15
15
|
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
16
|
-
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(
|
|
16
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3474);
|
|
17
17
|
// Time-travel + counterfactual scanning (v0.68).
|
|
18
18
|
//
|
|
19
19
|
// Two new modes that exploit the pure-input shape of runFullScan:
|
package/dist/238.index.js
CHANGED
|
@@ -10,7 +10,7 @@ export const modules = {
|
|
|
10
10
|
/* harmony export */ fixDurationReport: () => (/* binding */ fixDurationReport),
|
|
11
11
|
/* harmony export */ renderFixDurationSummary: () => (/* binding */ renderFixDurationSummary)
|
|
12
12
|
/* harmony export */ });
|
|
13
|
-
/* unused harmony exports FIX_STAGES, loadFixAttempts, bucketOf, summarizeFixDurations, _internals */
|
|
13
|
+
/* unused harmony exports FIX_STAGES, loadFixAttempts, bucketOf, summarizeFixDurations, _internals, summarizeFixAxes, renderFixAxes */
|
|
14
14
|
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
|
|
15
15
|
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
|
|
16
16
|
/* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1174);
|
|
@@ -213,6 +213,82 @@ function renderFixDurationSummary(sum) {
|
|
|
213
213
|
const _internals = { _dist, _pct, RELIABLE_N };
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
// ── PRD F6.1 — score fixes on THREE AXES, not one ──────────────────────────
|
|
217
|
+
//
|
|
218
|
+
// The three axes the PRD names:
|
|
219
|
+
// (a) does the finding disappear — the rescan leg
|
|
220
|
+
// (b) does the project's own suite pass — the tests leg
|
|
221
|
+
// (c) does an independent verifier agree — the PoC re-check leg
|
|
222
|
+
//
|
|
223
|
+
// All three were already computed by verifyFixCore and then collapsed into one
|
|
224
|
+
// boolean, which is the problem: **(a) alone is satisfiable by deleting code.**
|
|
225
|
+
// A patch that removes the vulnerable function passes the rescan, has nothing
|
|
226
|
+
// left to fail, and — on a project with no detectable test suite — reaches
|
|
227
|
+
// ok:true having proven only that the detector went quiet.
|
|
228
|
+
//
|
|
229
|
+
// Reporting the axes separately makes that visible. `aOnly` is the number that
|
|
230
|
+
// matters most and the one nobody was publishing: attempts that satisfied ONLY
|
|
231
|
+
// the disappearance axis. A high aOnly with a high headline is the shape of a
|
|
232
|
+
// remediation feature that is deleting code and calling it a fix.
|
|
233
|
+
function summarizeFixAxes(attempts) {
|
|
234
|
+
const list = Array.isArray(attempts) ? attempts.filter(Boolean) : [];
|
|
235
|
+
const d = list.length;
|
|
236
|
+
|
|
237
|
+
const rate = (pred) => ({ n: list.filter(pred).length, d });
|
|
238
|
+
|
|
239
|
+
// Each axis is judged INDEPENDENTLY of the overall verdict, so a leg that
|
|
240
|
+
// passed inside a failed attempt still counts for its own axis. Reading them
|
|
241
|
+
// off `ok` would make the three axes three copies of the same number.
|
|
242
|
+
const findingDisappeared = rate((a) => a.rescanOk === true || (a.ok === true && a.rescanOk !== false));
|
|
243
|
+
const testsStillPass = rate((a) => a.testsRan === true && a.testsOk !== false);
|
|
244
|
+
const verifierAgrees = rate((a) => a.pocOk === true);
|
|
245
|
+
|
|
246
|
+
const satisfiesAll = rate((a) =>
|
|
247
|
+
(a.rescanOk === true || (a.ok === true && a.rescanOk !== false))
|
|
248
|
+
&& a.testsRan === true && a.testsOk !== false
|
|
249
|
+
&& a.pocOk === true);
|
|
250
|
+
|
|
251
|
+
// The honesty number: disappearance WITHOUT either corroborating axis.
|
|
252
|
+
const aOnly = rate((a) => {
|
|
253
|
+
const disappeared = a.rescanOk === true || (a.ok === true && a.rescanOk !== false);
|
|
254
|
+
const corroborated = (a.testsRan === true && a.testsOk !== false) || a.pocOk === true;
|
|
255
|
+
return disappeared && !corroborated;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
total: d,
|
|
260
|
+
findingDisappeared,
|
|
261
|
+
testsStillPass,
|
|
262
|
+
verifierAgrees,
|
|
263
|
+
satisfiesAll,
|
|
264
|
+
aOnly,
|
|
265
|
+
meaning:
|
|
266
|
+
'findingDisappeared = the detector went quiet; testsStillPass = the project suite ran AND passed; '
|
|
267
|
+
+ 'verifierAgrees = an independent PoC re-check confirmed the hole is shut. '
|
|
268
|
+
+ 'aOnly counts attempts that satisfied ONLY disappearance — the shape a code-deleting "fix" produces.',
|
|
269
|
+
caveat: d === 0
|
|
270
|
+
? 'no attempts recorded; every rate is 0/0 and means nothing'
|
|
271
|
+
: 'rates carry {n,d}; a small d is indicative, not settled',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Markdown for a report. Denominators always attached. */
|
|
276
|
+
function renderFixAxes(sum) {
|
|
277
|
+
if (!sum || !sum.total) return '_No fix attempts recorded._\n';
|
|
278
|
+
const row = (label, r, note) => `| ${label} | ${r.n}/${r.d} | ${note} |`;
|
|
279
|
+
return [
|
|
280
|
+
'| Axis | Rate | Meaning |',
|
|
281
|
+
'|---|---|---|',
|
|
282
|
+
row('(a) finding disappeared', sum.findingDisappeared, 'the detector went quiet'),
|
|
283
|
+
row('(b) project tests pass', sum.testsStillPass, 'the suite RAN and passed'),
|
|
284
|
+
row('(c) verifier agrees', sum.verifierAgrees, 'an independent PoC re-check confirmed it'),
|
|
285
|
+
row('all three', sum.satisfiesAll, 'the only row that means "fixed"'),
|
|
286
|
+
row('(a) ALONE', sum.aOnly, 'satisfiable by deleting code — watch this number'),
|
|
287
|
+
'',
|
|
288
|
+
].join('\n');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
216
292
|
/***/ })
|
|
217
293
|
|
|
218
294
|
};
|
package/dist/384.index.js
CHANGED
|
@@ -8,7 +8,7 @@ export const modules = {
|
|
|
8
8
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
9
|
/* harmony export */ scanCredentials: () => (/* reexport safe */ _engine_js__WEBPACK_IMPORTED_MODULE_0__.Sv)
|
|
10
10
|
/* harmony export */ });
|
|
11
|
-
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
|
|
11
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3474);
|
|
12
12
|
// Secrets submodule view of the engine — credential + entropy + TODO scanning.
|
|
13
13
|
|
|
14
14
|
|
package/dist/435.index.js
CHANGED
|
@@ -806,6 +806,18 @@ function _maybeOffload(sessionRoot, toolName, items) {
|
|
|
806
806
|
}
|
|
807
807
|
|
|
808
808
|
// ─── scan_diff ───────────────────────────────────────────────────────────────
|
|
809
|
+
// Test seam for the write boundary (PRD F6.4).
|
|
810
|
+
//
|
|
811
|
+
// `_confine` and `isReservedWrite` ARE the confinement contract in
|
|
812
|
+
// agents/_CONFINEMENT.md. A boundary is only worth what its refusals are worth,
|
|
813
|
+
// and refusals cannot be adversarially tested through the public tools without
|
|
814
|
+
// also exercising a real scan, a real patch and a real filesystem write — so
|
|
815
|
+
// the check would be measuring four things and attributing failure to one.
|
|
816
|
+
//
|
|
817
|
+
// Exported under the `_internals` convention this codebase already uses
|
|
818
|
+
// (see posture/poc-inprocess.js). Not part of the MCP tool surface.
|
|
819
|
+
const tools_internals = { _confine, isReservedWrite: _isReservedWritePath };
|
|
820
|
+
|
|
809
821
|
const scan_diff = {
|
|
810
822
|
name: 'scan_diff',
|
|
811
823
|
description: 'Scan a list of files for security findings. Use BEFORE writing a Write/Edit to disk so the agent can self-correct. Returns findings with severity, file:line, title, remediation. Snippets are redacted of obvious secret patterns. Paths confined to the session root; symlinks are refused.',
|