@clear-capabilities/agentic-security-scanner 0.136.2 → 0.136.9
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 +236 -0
- package/bin/agentic-security.js +186 -37
- package/dist/113.index.js +13 -4
- package/dist/178.index.js +1 -1
- package/dist/207.index.js +5 -4
- package/dist/238.index.js +1 -1
- package/dist/317.index.js +36 -6
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +183 -14
- package/dist/444.index.js +20 -11
- package/dist/449.index.js +8 -1
- package/dist/526.index.js +3 -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/nist-privacy-1-1.json +2 -2
- package/dist/compliance-frameworks/owasp-asvs-5.json +1 -1
- package/package.json +18 -10
- package/src/dataflow/CLAUDE.md +10 -4
- package/src/dataflow/builtin-summaries.js +1 -1
- package/src/dataflow/engine.js +324 -60
- package/src/dataflow/implicit-flow.js +68 -36
- package/src/dataflow/incremental.js +18 -3
- package/src/dataflow/index.js +2 -1
- package/src/dataflow/proven-clean.js +41 -0
- package/src/dataflow/sanitizer-gate.js +35 -9
- package/src/dataflow/sanitizer-proof.js +21 -3
- package/src/dataflow/stub-aware-filter.js +36 -13
- package/src/dataflow/summaries.js +21 -2
- package/src/engine.js +202 -42
- package/src/ir/CLAUDE.md +4 -1
- package/src/ir/balanced-call.js +55 -0
- package/src/ir/parser-cs.js +62 -9
- package/src/ir/parser-go.js +29 -11
- package/src/ir/parser-java.js +96 -19
- package/src/ir/parser-js.js +151 -20
- package/src/ir/parser-php.js +44 -9
- package/src/ir/parser-rb.js +37 -7
- package/src/ir/ssa.js +6 -1
- package/src/lsp/server.js +28 -2
- package/src/mcp/CLAUDE.md +9 -2
- package/src/mcp/redact.js +26 -0
- package/src/mcp/tools.js +155 -14
- package/src/posture/CLAUDE.md +19 -7
- package/src/posture/accuracy-scorecard.js +9 -1
- package/src/posture/aibom.js +12 -8
- package/src/posture/auditor-walkthrough.js +102 -3
- package/src/posture/autopilot.js +8 -1
- package/src/posture/calibration-drift.js +11 -5
- package/src/posture/calibration.js +24 -2
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +2 -2
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +1 -1
- package/src/posture/compliance-policy.js +33 -1
- package/src/posture/confidence.js +44 -10
- package/src/posture/corpus-enroll.js +9 -5
- package/src/posture/corpus-match.js +19 -0
- package/src/posture/csharp-analysis.js +62 -3
- package/src/posture/deploy-platform.js +4 -1
- package/src/posture/drift.js +7 -1
- package/src/posture/epss.js +13 -1
- package/src/posture/evidence-bundle.js +36 -6
- package/src/posture/exploitability-probability.js +13 -1
- package/src/posture/falsification.js +23 -2
- package/src/posture/fix-metrics.js +1 -1
- package/src/posture/fix-verify-loop.js +10 -1
- package/src/posture/iac-reachability.js +14 -8
- package/src/posture/integrity.js +25 -7
- package/src/posture/model-rescan.js +65 -0
- package/src/posture/mttr.js +5 -0
- package/src/posture/poc-inprocess.js +27 -8
- package/src/posture/regression-test-gen.js +23 -8
- package/src/posture/reverse-blast-radius.js +5 -1
- package/src/posture/risk-dollars.js +18 -1
- package/src/posture/secret-history.js +20 -11
- package/src/posture/security-trend.js +7 -1
- package/src/posture/stack-playbook.js +22 -1
- package/src/posture/threat-model-grounding.js +2 -2
- package/src/posture/validator-metrics.js +10 -3
- package/src/posture/verifier.js +32 -57
- package/src/report/index.js +183 -14
- package/src/runScan.js +1 -1
- package/src/sast/_comment-strip.js +15 -4
- package/src/sast/_secret-entropy.js +1 -1
- package/src/sast/authz.js +6 -4
- package/src/sast/bench-shape/index.js +2 -7
- package/src/sast/claude-md-prompt-injection.js +14 -3
- package/src/sast/cloud-iam.js +60 -7
- package/src/sast/cpp-bench-extras.js +1 -1
- package/src/sast/csrf.js +7 -5
- package/src/sast/env-hygiene.js +5 -2
- package/src/sast/iac-terraform.js +25 -0
- package/src/sast/java-bench-extras.js +1 -1
- package/src/sast/java-constant-fold.js +5 -5
- package/src/sast/llm-owasp.js +4 -2
- package/src/sast/mcp-audit.js +7 -0
- package/src/sast/pipeline.js +8 -0
- package/src/sast/prompt-template.js +8 -6
- package/src/sast/prototype-pollution.js +6 -2
- package/src/sast/redos-nfa.js +6 -6
- package/src/sast/secret-concat.js +13 -2
- package/src/sast/ssrf-cloud-metadata.js +6 -3
- package/src/sast/xss-reflected-multilang.js +1 -1
- package/src/sast/xxe.js +1 -1
- package/src/sca/CLAUDE.md +3 -4
- package/src/sca/container.js +35 -3
- package/src/sca/dep-confusion.js +7 -0
- package/src/sca/sarif-ingest.js +0 -187
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,242 @@
|
|
|
9
9
|
> make the history less accurate, not more.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.136.9 — the real bug: the single-file bundle was never actually self-contained
|
|
13
|
+
|
|
14
|
+
0.136.8's diagnostic logging answered the question immediately:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Error [ERR_MODULE_NOT_FOUND]: Cannot find module
|
|
18
|
+
'.../scanner/dist/449.index.js' imported from
|
|
19
|
+
'.../scanner/dist/agentic-security.mjs'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`dist/agentic-security.mjs` is documented and committed as a self-contained
|
|
23
|
+
single-file bundle — `.gitignore`'s own comment says the reusable
|
|
24
|
+
`scan.yml` workflow fetches *only that one file* from
|
|
25
|
+
raw.githubusercontent.com so downstream users need no install step. That
|
|
26
|
+
was never quite true. `bin/agentic-security.js` lazily loads each
|
|
27
|
+
subcommand's implementation with `await import('../src/...')` — about 58
|
|
28
|
+
call sites, deliberately, so running `agentic-security scan` doesn't pay
|
|
29
|
+
the cost of loading every posture/discovery/compliance module `agentic-security
|
|
30
|
+
compliance` or `agentic-security hunt` would need. `ncc` code-splits every
|
|
31
|
+
one of those into its own `dist/NNN.index.js` chunk rather than inlining
|
|
32
|
+
it, and the chunk is loaded at runtime via a path relative to the bundle's
|
|
33
|
+
own location on disk — but `.gitignore` only ever allowlisted the main
|
|
34
|
+
`.mjs`, its sha256 sidecar, and the compliance-frameworks data. All 38
|
|
35
|
+
chunk files were silently gitignored. `.claude/settings.local.json`-style
|
|
36
|
+
invisible: they exist on any machine that has ever run `npm run build`
|
|
37
|
+
locally (which is every contributor's, permanently, from the first
|
|
38
|
+
`npm install`), so nobody — human or gate — had a checkout that lacked
|
|
39
|
+
them until this release's hosted CI runs did.
|
|
40
|
+
|
|
41
|
+
This is the third occurrence of the identical bug class in this file's own
|
|
42
|
+
history — `.gitignore` already carries a comment about the same thing
|
|
43
|
+
happening to `compliance-frameworks/` data before ("the shipped CLI
|
|
44
|
+
silently listed ZERO frameworks and exited 0"). Same shape both times: a
|
|
45
|
+
single tracked entry point quietly depends on sibling files nobody
|
|
46
|
+
allowlisted.
|
|
47
|
+
|
|
48
|
+
Fixed by allowlisting `scanner/dist/*.index.js` and committing all 38
|
|
49
|
+
current chunks (744 KB), and by adding a permanent regression guard
|
|
50
|
+
(`test/dist-chunks-tracked.test.js`, wired into `test:lifecycle`) that
|
|
51
|
+
diffs `dist/*.index.js` on disk against `git ls-files dist` and fails
|
|
52
|
+
loud, by name, the moment a future build produces a chunk nobody
|
|
53
|
+
allowlisted — rather than waiting for a lazy code path to hit it in
|
|
54
|
+
production. Verified RED against the pre-fix git state (all 38 chunks
|
|
55
|
+
correctly reported untracked) and GREEN after staging them.
|
|
56
|
+
|
|
57
|
+
## 0.136.8 — instrumenting a real, still-unexplained hosted-CI-only failure
|
|
58
|
+
|
|
59
|
+
0.136.7's hosted release run got past every other check and failed on
|
|
60
|
+
exactly one: `test/mttr.test.js`'s CLI-wiring test, which asserts a real
|
|
61
|
+
`agentic-security scan` reports an `mttr` field in `last-scan.json`. That
|
|
62
|
+
field comes from a deliberately best-effort code path in `cmdScan`
|
|
63
|
+
(`bin/agentic-security.js`) wrapped in a bare `catch { /* MTTR is
|
|
64
|
+
best-effort — never block a scan write */ }` — correct as a product
|
|
65
|
+
decision (a scan must never fail because a secondary metric couldn't be
|
|
66
|
+
computed), but it means whatever throws inside that block has never been
|
|
67
|
+
visible to anyone, including this investigation.
|
|
68
|
+
|
|
69
|
+
This is now confirmed NOT the shared-runner flakiness 0.136.5/0.136.6
|
|
70
|
+
blamed it as: reran the identical failed job against the identical commit
|
|
71
|
+
(`gh run rerun --failed`, no code change) and got the exact same failure,
|
|
72
|
+
same file, same line, same message, a fourth consecutive time. Extensive
|
|
73
|
+
local reproduction attempts — simulating `CI=true`/`GITHUB_ACTIONS=true`,
|
|
74
|
+
running the full 1328-test `test:posture` scope concurrently to match CI's
|
|
75
|
+
exact invocation shape, invoking the real bundled CLI directly under the
|
|
76
|
+
same env vars — all passed cleanly. Whatever this is, it is deterministic
|
|
77
|
+
on GitHub's hosted runner and has not reproduced anywhere else tried so far.
|
|
78
|
+
|
|
79
|
+
Rather than keep guessing, the silent catch now logs the actual error to
|
|
80
|
+
stderr whenever `CI`/`GITHUB_ACTIONS` is set (or `AGENTIC_SECURITY_MTTR_DEBUG=1`
|
|
81
|
+
locally), and the test's own assertion message now includes the scan
|
|
82
|
+
subprocess's stderr. Scan behavior is unchanged — this is instrumentation
|
|
83
|
+
only, shipped specifically to get a real answer out of the next hosted run
|
|
84
|
+
instead of another guess.
|
|
85
|
+
|
|
86
|
+
## 0.136.7 — the doc-drift checker was resolving paths outside the repo
|
|
87
|
+
|
|
88
|
+
0.136.6's hosted release run failed on a check this same audit added a few
|
|
89
|
+
releases ago: `check-doc-drift.mjs`'s own regression test, "the real
|
|
90
|
+
repository currently has zero mechanically-checkable drift," flagged root
|
|
91
|
+
CLAUDE.md's documented (and correctly gitignored) `.claude/settings.local.json`
|
|
92
|
+
reference as a dangling path.
|
|
93
|
+
|
|
94
|
+
The actual defect was in `resolveCandidate`'s search bases, not in the
|
|
95
|
+
reference: for a CLAUDE.md near the repo root, one of the fallback bases is
|
|
96
|
+
"two directories up from the CLAUDE.md's own directory" — meant for nested
|
|
97
|
+
CLAUDE.md files reaching back toward the repo root, but for the ROOT
|
|
98
|
+
CLAUDE.md itself that lands OUTSIDE the checkout entirely, in whatever
|
|
99
|
+
happens to be the parent of wherever the repo was cloned. On the
|
|
100
|
+
maintainer's laptop that's their home directory, which happens to contain
|
|
101
|
+
an unrelated, machine-global `~/.claude/settings.local.json` from ordinary
|
|
102
|
+
Claude Code usage — so the checker "resolved" the reference against a file
|
|
103
|
+
that has nothing to do with this project, passed locally, and failed on a
|
|
104
|
+
clean CI checkout where no such coincidence exists. `resolveCandidate` now
|
|
105
|
+
rejects any candidate base or resolved path outside the repo root, and
|
|
106
|
+
`settings.local.json` — genuinely optional, gitignored, user-created — joins
|
|
107
|
+
the checker's existing known-example-basename allowlist as a second,
|
|
108
|
+
independent fix.
|
|
109
|
+
|
|
110
|
+
Reproduced without touching CI: moved the local override file aside,
|
|
111
|
+
confirmed the existing regression test still passed (proving the checker was
|
|
112
|
+
resolving against something else entirely), traced it to the home-directory
|
|
113
|
+
escape, fixed both the escape and the allowlist gap, verified clean with the
|
|
114
|
+
file present and absent, then restored it.
|
|
115
|
+
|
|
116
|
+
## 0.136.6 — correction: the "flaky" dataflow tests were a real, deterministic gap
|
|
117
|
+
|
|
118
|
+
0.136.5's entry below called a cluster of `test:dataflow` failures on the
|
|
119
|
+
hosted runner resource-contention flakiness, because a second, simultaneous
|
|
120
|
+
workflow run of the identical commit showed a completely different failure
|
|
121
|
+
and neither reproduced locally. That diagnosis was wrong, and the actual
|
|
122
|
+
cause is more interesting: 13 test files pass `runScan(dir, { deep: true })`
|
|
123
|
+
to exercise the interprocedural taint engine directly, but never opt into
|
|
124
|
+
`AGENTIC_SECURITY_DEEP_IN_CI` — and `engine.js` deliberately auto-disables
|
|
125
|
+
deep mode under any detected CI environment unless that second flag is also
|
|
126
|
+
set, precisely so a pathological file can't hang a CI pipeline. Every one of
|
|
127
|
+
those 13 files has therefore been silently CI-broken (never actually
|
|
128
|
+
exercising the deep engine there, only recording the "skipped in CI"
|
|
129
|
+
placeholder finding) since the day it was written — invisible until this
|
|
130
|
+
release's tag push put 62 accumulated commits through hosted CI for the
|
|
131
|
+
first time. Reproduced deterministically with `CI=true node --test
|
|
132
|
+
<file>.test.js` locally (no CI service required), fixed by threading
|
|
133
|
+
`deepInCi: true` alongside `deep: true` at all 21 call sites across those 13
|
|
134
|
+
files, and confirmed both directions: `npm test` is clean with `CI` unset
|
|
135
|
+
and with `CI=true` set.
|
|
136
|
+
|
|
137
|
+
The earlier flakiness diagnosis wasn't entirely wrong — the *other* workflow
|
|
138
|
+
run's single MTTR-wiring failure genuinely didn't reproduce on a third run
|
|
139
|
+
and remains unexplained — but it was wrong about *this* failure cluster,
|
|
140
|
+
which was 100% reproducible once isolated with the right environment
|
|
141
|
+
variable rather than blamed on shared-runner load.
|
|
142
|
+
|
|
143
|
+
## 0.136.5 — the release workflow gets its own missing dependency
|
|
144
|
+
|
|
145
|
+
0.136.4 was tagged and its release workflow ran — for the first time ever,
|
|
146
|
+
since the 60-odd commits it carried had accumulated across many local
|
|
147
|
+
sessions without a single hosted-CI push. The gate caught a real gap in
|
|
148
|
+
itself: `nist-catalog-freshness` (a check this same release adds, see below)
|
|
149
|
+
shells out to `scripts/nist-compliance/build-catalog.py`, which needs
|
|
150
|
+
`openpyxl` to read the source workbook. That's present on the maintainer's
|
|
151
|
+
machine via a system Python install, but nothing installs it on the hosted
|
|
152
|
+
runner — so the check could never have passed there. `release.yml` now
|
|
153
|
+
installs it explicitly before the gate runs.
|
|
154
|
+
|
|
155
|
+
The same CI run also showed roughly a dozen dataflow tests fail — a
|
|
156
|
+
completely different set than the *other* workflow (`ci.yml`) failed on the
|
|
157
|
+
same commit at the same time (one flaky MTTR-wiring test, no overlap with the
|
|
158
|
+
dataflow set). Neither set reproduces locally, isolated or otherwise. That
|
|
159
|
+
non-overlap is the signature of resource-contention flakiness on a shared
|
|
160
|
+
runner, not a deterministic regression, so it isn't chased further here — but
|
|
161
|
+
it's worth knowing about if a future release gate flakes on `npm test` again.
|
|
162
|
+
|
|
163
|
+
Per the project's own precedent (see 0.136.1 below): a tag that failed to
|
|
164
|
+
publish stays where it is rather than being moved. 0.136.4 is that tag. This
|
|
165
|
+
ships as 0.136.5 instead, otherwise identical.
|
|
166
|
+
|
|
167
|
+
## 0.136.4 — dominance-correct implicit-flow taint, and wiring three dead-reachable tools
|
|
168
|
+
|
|
169
|
+
A capability-PRD follow-up audit found several places where a real, tested
|
|
170
|
+
mechanism existed in the code but nothing in the product could actually reach
|
|
171
|
+
it. This release closes those gaps rather than documenting them further.
|
|
172
|
+
|
|
173
|
+
**`implicit-flow.js` now uses real dominance, not a depth counter.** The
|
|
174
|
+
branch-scoping check for implicit taint (does a constant-argument sink sit
|
|
175
|
+
*inside* a tainted branch, or after it closes?) was a path-dependent DFS
|
|
176
|
+
depth-counter, which cannot distinguish a join point from a nested branch and
|
|
177
|
+
both over- and under-attributes depending on CFG shape. It's replaced with a
|
|
178
|
+
proper dominance check (`ssa.js`'s `computeDominators`, already used for φ-node
|
|
179
|
+
placement, now exported and reused here) plus a predecessor-count "sole
|
|
180
|
+
parent" test — needed because an else-less `if`'s CFG lowering links the
|
|
181
|
+
condition directly to the join node, which a naive dominance check alone would
|
|
182
|
+
still misread as a branch root. `dataflow/engine.js`'s implicit-flow post-pass
|
|
183
|
+
is split into two correctly-scoped passes: a sink-call-must-be-inside-the-
|
|
184
|
+
branch check for constant-argument leaks, and an ungated check for
|
|
185
|
+
already-tainted-variable-as-argument leaks, which don't need the sink itself
|
|
186
|
+
to be branch-local.
|
|
187
|
+
|
|
188
|
+
**Three previously-dead mechanisms are now reachable.** `verify_fix`'s PoC-
|
|
189
|
+
recheck leg always reported `not-requested` — the caller never had the PoC to
|
|
190
|
+
pass it, so it's now looked up server-side from the finding's own
|
|
191
|
+
`last-scan.json` entry. `fix-honesty-gate.js`'s deterministic honesty checks
|
|
192
|
+
(vague-assurance residual prose, unbacked false-positive verdicts) were fully
|
|
193
|
+
built and consulted by the verifier, but `fixMeta` was never in `apply_fix` or
|
|
194
|
+
`verify_fix`'s MCP schema, so no caller could ever supply one; a dishonest
|
|
195
|
+
`fixMeta` now blocks the write itself, not just the report. `verifyRunAttestation`
|
|
196
|
+
had no CLI caller at all — `verify-attestation` now auto-detects a
|
|
197
|
+
run-attestation shape and re-scans the target to check it reproduces the
|
|
198
|
+
attested digest, backed by two new release-gate checks
|
|
199
|
+
(`attestation-self-check`, `nist-catalog-freshness`).
|
|
200
|
+
|
|
201
|
+
**Stale docs, fixed instead of flagged.** `docs/compliance/{nist-ai-600-1,
|
|
202
|
+
owasp-asvs}-coverage.md` carried static control tables that drifted from the
|
|
203
|
+
live evaluator; both now point at the `/compliance` walkthrough/report instead
|
|
204
|
+
of duplicating data that can go stale. The ASVS doc also had a genuine
|
|
205
|
+
version mismatch (4.0.3 vs. the 5.0 catalog actually in use). A new
|
|
206
|
+
`scripts/check-doc-drift.mjs` catches this class of staleness mechanically —
|
|
207
|
+
it resolves every backtick-quoted path/export reference in a CLAUDE.md file
|
|
208
|
+
against the real filesystem — after this audit found several by hand.
|
|
209
|
+
|
|
210
|
+
**CVE-replay corpus:** four new capability entries (IaC open-ingress, LLM
|
|
211
|
+
system-prompt injection, MCP untrusted-install, API missing-auth/BFLA),
|
|
212
|
+
closing four of six previously-flagged zero-coverage categories; each verified
|
|
213
|
+
`pre:TP post:TN` against the real runner before joining the baseline. SBOM and
|
|
214
|
+
SCA-reachability are documented in `bench/cve-replay/CONTRIBUTING.md` as
|
|
215
|
+
structurally unable to fit this corpus's binary presence/absence schema —
|
|
216
|
+
they're covered by their own test suites instead.
|
|
217
|
+
|
|
218
|
+
CMP-1's family-alias table also closed three more gaps
|
|
219
|
+
(`k8s-pod-security-privileged`, `mcp-audit.js`'s `agent-tool-exec` backfill,
|
|
220
|
+
dependency-confusion family tagging).
|
|
221
|
+
|
|
222
|
+
**No functional change from 0.136.2.** This version exists for one reason, and
|
|
223
|
+
it is worth stating plainly rather than dressing up: 0.136.2 reached npm from a
|
|
224
|
+
maintainer's laptop, not from CI, so it carries **no provenance attestation** —
|
|
225
|
+
nothing ties that tarball to this repository or this commit beyond trust in the
|
|
226
|
+
publisher.
|
|
227
|
+
|
|
228
|
+
npm will not accept a re-publish of an existing version, so obtaining provenance
|
|
229
|
+
requires a new one. 0.136.3 is that, and nothing else.
|
|
230
|
+
|
|
231
|
+
The release path itself was already proven end to end on 0.136.2: the gate passed
|
|
232
|
+
on a clean runner, npm signed a provenance statement and recorded it in the
|
|
233
|
+
Sigstore transparency log, and only the final registry upload was rejected —
|
|
234
|
+
because the token in CI was not authorized to publish. With a valid automation
|
|
235
|
+
token that last step completes, and the attestation that was already being
|
|
236
|
+
generated actually lands.
|
|
237
|
+
|
|
238
|
+
Verify it yourself once published:
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
npm view @clear-capabilities/agentic-security-scanner@0.136.3 --json | jq .dist.attestations
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`null` means it went out unattested again. `dist.signatures` is NOT the same
|
|
245
|
+
thing — the registry signs every package it serves; provenance is the separate
|
|
246
|
+
Sigstore statement binding the artifact to its source.
|
|
247
|
+
|
|
12
248
|
## 0.136.2 — authenticate the gate on the path that actually runs it
|
|
13
249
|
|
|
14
250
|
0.136.1 removed the self-deadlock and the release workflow got further: every
|
package/bin/agentic-security.js
CHANGED
|
@@ -14,7 +14,6 @@ import { toCycloneDX, toSPDX } from '../src/posture/sbom.js';
|
|
|
14
14
|
import { toPBOM } from '../src/sast/pipeline.js';
|
|
15
15
|
import { buildAIBOM, aibomToMarkdown } from '../src/posture/aibom.js';
|
|
16
16
|
import { recordScan, formatStreakLine, formatGradeDelta } from '../src/posture/streak.js';
|
|
17
|
-
import { ingestAndMerge } from '../src/sca/sarif-ingest.js';
|
|
18
17
|
import { loadProfile, saveProfile, detectProfile, renderAttributionLine, ATTRIBUTION, ATTRIBUTION_URL } from '../src/posture/profile.js';
|
|
19
18
|
import { applySuppressions, addSoftAcceptance, expiredSoftAcceptances } from '../src/posture/suppressions.js';
|
|
20
19
|
import { applyOverrides, validateOverrides, suppressionReport, renderSuppressionSummary } from '../src/posture/rule-overrides.js';
|
|
@@ -28,7 +27,6 @@ import { syncTickets } from '../src/integrations/tickets.js';
|
|
|
28
27
|
import { decide as decideNextAction, explain as explainDecision } from '../src/posture/router.js';
|
|
29
28
|
import * as triage from '../src/posture/triage.js';
|
|
30
29
|
import { buildSlackDigest, buildDiscordDigest, postWebhook, buildJiraIssue, buildPrComment, buildSiemEvent, loadIntegrationConfig } from '../src/integrations/index.js';
|
|
31
|
-
import { globFiles } from '../src/util/glob.js';
|
|
32
30
|
|
|
33
31
|
import { stateDir, statePath } from '../src/posture/state-dir.js';
|
|
34
32
|
// last-scan.json integrity helpers — implementation in posture/integrity.js
|
|
@@ -111,7 +109,6 @@ Options:
|
|
|
111
109
|
--show-threat-model Append the auto-derived STRIDE threat model summary
|
|
112
110
|
--show-drift Append calibration-drift alarms (overconfidence detection)
|
|
113
111
|
--sca-reachable-only Only SCA findings where the vulnerable function is reachable
|
|
114
|
-
--ingest-sarif <glob> Merge external SARIF into this scan
|
|
115
112
|
--scorecard Enrich components with OSSF Scorecard scores
|
|
116
113
|
--no-network Skip OSV/registry queries (offline mode)
|
|
117
114
|
--pr [ref] Diff-aware: scan only files changed since ref (auto-detects PR base)
|
|
@@ -297,7 +294,7 @@ function _bundleSha() {
|
|
|
297
294
|
return 'unavailable';
|
|
298
295
|
}
|
|
299
296
|
|
|
300
|
-
async function writeMachineOutput(targetAbs, scan, meta, profile) {
|
|
297
|
+
async function writeMachineOutput(targetAbs, scan, meta, profile, args) {
|
|
301
298
|
const stateDirPath = stateDir(targetAbs);
|
|
302
299
|
const { isSafeStateDir: _isSafe, stateWritesEnabled: _writesOn } = await import('../src/posture/state-dir.js');
|
|
303
300
|
// Read-only scan (NON_MUTATING_SCAN_PRD S1). These writes bypass
|
|
@@ -311,7 +308,7 @@ async function writeMachineOutput(targetAbs, scan, meta, profile) {
|
|
|
311
308
|
// Always JSON (used by /security-fix and /security-report).
|
|
312
309
|
await fsp.writeFile(path.join(stateDirPath, 'findings.json'),
|
|
313
310
|
JSON.stringify(toJSON(scan, meta), null, 2));
|
|
314
|
-
if (profile.profile === 'pro' || profile.machineOutput) {
|
|
311
|
+
if (profile.profile === 'pro' || profile.machineOutput || (args && args.flags['machine-output'])) {
|
|
315
312
|
await fsp.writeFile(path.join(stateDirPath, 'findings.sarif'),
|
|
316
313
|
JSON.stringify(toSARIF(scan, meta), null, 2));
|
|
317
314
|
await fsp.writeFile(path.join(stateDirPath, 'findings.csv'), toCSV(scan));
|
|
@@ -440,9 +437,16 @@ async function cmdScan(args) {
|
|
|
440
437
|
|
|
441
438
|
const only = args.flags.only;
|
|
442
439
|
if (only) {
|
|
440
|
+
// Four channels exist (scanner/CLAUDE.md): findings (SAST), secrets,
|
|
441
|
+
// supplyChain (SCA), logicVulns (business-logic). --only sast keeps
|
|
442
|
+
// logicVulns (business-logic is source analysis, part of the SAST
|
|
443
|
+
// pillar); --only sca/secrets must also clear it — it's neither SCA
|
|
444
|
+
// nor secrets, and normalizeFindings()/exitCodeFor() both fold it in,
|
|
445
|
+
// so leaving it meant a business-logic finding leaked into a
|
|
446
|
+
// single-pillar scan's output AND its exit code.
|
|
443
447
|
if (only === 'sast') { scan.secrets = []; scan.supplyChain = []; }
|
|
444
|
-
if (only === 'sca') { scan.findings = []; scan.secrets = []; }
|
|
445
|
-
if (only === 'secrets') { scan.findings = []; scan.supplyChain = []; }
|
|
448
|
+
if (only === 'sca') { scan.findings = []; scan.secrets = []; scan.logicVulns = []; }
|
|
449
|
+
if (only === 'secrets') { scan.findings = []; scan.supplyChain = []; scan.logicVulns = []; }
|
|
446
450
|
}
|
|
447
451
|
|
|
448
452
|
// --set-baseline: save current findings as baseline for future --since-baseline filtering
|
|
@@ -494,18 +498,6 @@ async function cmdScan(args) {
|
|
|
494
498
|
// 0.9.0 Feat-18: --scorecard flag enables OSSF Scorecard enrichment
|
|
495
499
|
if (args.flags['scorecard']) process.env.AGENTIC_SECURITY_SCORECARD = '1';
|
|
496
500
|
|
|
497
|
-
// 0.7.0 Feat-7: --ingest-sarif <path-or-glob> merges SARIF from external tools (Semgrep,
|
|
498
|
-
// gitleaks, Bandit, Trivy, Checkov, etc.) into this scan's findings, deduping by
|
|
499
|
-
// fingerprint and tracking provenance via sources[].
|
|
500
|
-
if (args.flags['ingest-sarif']) {
|
|
501
|
-
const glob = args.flags['ingest-sarif'];
|
|
502
|
-
const paths = await globFiles(glob);
|
|
503
|
-
if (paths.length) {
|
|
504
|
-
const r = ingestAndMerge(scan, paths);
|
|
505
|
-
if (process.stderr.isTTY) process.stderr.write(`[ingest] merged ${r.merged} / added ${r.added} findings from ${paths.length} SARIF file(s)\n`);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
501
|
// 0.6.0 Feat-1: --sca-reachable-only filters to only SCA findings where the vulnerable
|
|
510
502
|
// function was confirmed reachable from a route handler.
|
|
511
503
|
if (args.flags['sca-reachable-only']) {
|
|
@@ -566,6 +558,31 @@ async function cmdScan(args) {
|
|
|
566
558
|
try { await enrichWithEPSS(scan); } catch {}
|
|
567
559
|
}
|
|
568
560
|
|
|
561
|
+
// Re-run risk-dollars now that both signals it needs are actually
|
|
562
|
+
// available: EPSS (just enriched above — engine.js's own call ran before
|
|
563
|
+
// this, so every finding's epssScore was still unset) and relevanceTier
|
|
564
|
+
// (posture/relevance.js runs last inside runScan/engine.js, so it's
|
|
565
|
+
// populated on `scan.findings` by the time we get here, but wasn't yet
|
|
566
|
+
// when engine.js's own annotateRiskDollars call ran mid-pipeline).
|
|
567
|
+
try {
|
|
568
|
+
const { annotateRiskDollars } = await import('../src/posture/risk-dollars.js');
|
|
569
|
+
annotateRiskDollars(targetAbs, scan.findings || []);
|
|
570
|
+
} catch {}
|
|
571
|
+
|
|
572
|
+
// Re-run composite risk for the same reason as annotateRiskDollars above:
|
|
573
|
+
// engine.js's mid-pipeline annotateCompositeRisk calls ran before
|
|
574
|
+
// enrichWithEPSS (just above) had a chance to set f.exploitedNow, so
|
|
575
|
+
// every finding's 'exploited-now-floor:75' boost was computed against a
|
|
576
|
+
// field that was always undefined at the time — an actively-exploited
|
|
577
|
+
// CVE (EPSS percentile >= 0.95) never got its compositeRisk score raised.
|
|
578
|
+
// annotateCompositeRisk is pure/idempotent (only sets compositeRisk*
|
|
579
|
+
// fields, reads everything else), so re-running it here is safe.
|
|
580
|
+
try {
|
|
581
|
+
const { annotateCompositeRisk } = await import('../src/posture/composite-risk.js');
|
|
582
|
+
annotateCompositeRisk(scan.findings || []);
|
|
583
|
+
annotateCompositeRisk(scan.supplyChain || []);
|
|
584
|
+
} catch {}
|
|
585
|
+
|
|
569
586
|
// Blast-radius narrative — purely local, always safe to run.
|
|
570
587
|
if (!args.flags['no-blast-radius']) {
|
|
571
588
|
try { enrichWithBlastRadius(scan, targetAbs); } catch {}
|
|
@@ -622,7 +639,7 @@ async function cmdScan(args) {
|
|
|
622
639
|
} catch { /* attestation is metadata; never fail a scan over it */ }
|
|
623
640
|
|
|
624
641
|
// R2: Always emit machine-readable artifacts to .agentic-security/.
|
|
625
|
-
await writeMachineOutput(targetAbs, scan, meta, profile);
|
|
642
|
+
await writeMachineOutput(targetAbs, scan, meta, profile, args);
|
|
626
643
|
|
|
627
644
|
const includeSuppressed = !!args.flags['include-suppressed'];
|
|
628
645
|
let body;
|
|
@@ -664,32 +681,70 @@ async function cmdScan(args) {
|
|
|
664
681
|
// Persist last scan for /security-fix and /security-report
|
|
665
682
|
const { isSafeStateDir: _isSafeStateDir, stateWritesEnabled: _writesOnScan } = await import('../src/posture/state-dir.js');
|
|
666
683
|
const stateDirPath = stateDir(path.resolve(target));
|
|
684
|
+
// S7 (Stage-0 audit): declared here, not with `const` inside the `if`
|
|
685
|
+
// below — it used to be block-scoped there while recordScan() further
|
|
686
|
+
// down referenced it unconditionally, throwing a ReferenceError on every
|
|
687
|
+
// single scan (silently swallowed by that call's empty catch{}). streak.json
|
|
688
|
+
// — grades, streak days, achievements — never actually persisted through
|
|
689
|
+
// the real CLI as a result, reproduced live before this fix.
|
|
690
|
+
let persistedScan = null;
|
|
667
691
|
if (_writesOnScan() && _isSafeStateDir(stateDirPath)) {
|
|
668
692
|
await fsp.mkdir(stateDirPath, { recursive: true });
|
|
669
|
-
|
|
693
|
+
persistedScan = toJSON(scan, meta);
|
|
670
694
|
// #10 — MTTR: stamp firstSeenAt/lastSeenAt/ageDays from the PREVIOUS scan so
|
|
671
695
|
// every finding carries an age, SLA breaches can be surfaced, and the fix
|
|
672
696
|
// loop can report time-to-clean. Best-effort; skipped under --deterministic
|
|
673
697
|
// so deterministic state stays byte-identical run-to-run.
|
|
674
698
|
if (!args.flags.deterministic) {
|
|
675
699
|
try {
|
|
676
|
-
const { stampFindingTimestamps, buildBaselineMap, renderSlaSummary } = await import('../src/posture/mttr.js');
|
|
700
|
+
const { stampFindingTimestamps, buildBaselineMap, renderSlaSummary, fingerprintFinding, computeMTTR } = await import('../src/posture/mttr.js');
|
|
677
701
|
let baselineMap = new Map();
|
|
702
|
+
let prevAll = [];
|
|
678
703
|
try {
|
|
679
704
|
const prev = JSON.parse(await fsp.readFile(path.join(stateDirPath, 'last-scan.json'), 'utf8'));
|
|
680
705
|
baselineMap = buildBaselineMap(prev);
|
|
706
|
+
// The previous scan's own findings already carry firstSeenAt/lastSeenAt
|
|
707
|
+
// from ITS baseline lookup — exactly the shape computeMTTR needs for
|
|
708
|
+
// whichever of them are no longer present now (i.e. were fixed).
|
|
709
|
+
prevAll = [
|
|
710
|
+
...(prev?.findings || []), ...(prev?.secrets || []),
|
|
711
|
+
...(prev?.supplyChain || []).filter(s => s.type === 'vulnerable_dep'),
|
|
712
|
+
];
|
|
681
713
|
} catch { /* first run — empty baseline, everything is firstSeen now */ }
|
|
682
714
|
const now = Date.now();
|
|
683
715
|
stampFindingTimestamps(persistedScan.findings || [], baselineMap, now);
|
|
684
716
|
stampFindingTimestamps(persistedScan.secrets || [], baselineMap, now);
|
|
685
717
|
stampFindingTimestamps((persistedScan.supplyChain || []).filter(s => s.type === 'vulnerable_dep'), baselineMap, now);
|
|
718
|
+
// #10 — MTTR: which of the previous scan's findings are no longer
|
|
719
|
+
// present now (fixed), and how long each took. computeMTTR itself was
|
|
720
|
+
// fully built and tested but had never had a real caller — the module's
|
|
721
|
+
// own comment calls it "true MTTR," distinct from the open-backlog
|
|
722
|
+
// median-age proxy renderSlaSummary already surfaces below.
|
|
723
|
+
const currentFps = new Set([
|
|
724
|
+
...(persistedScan.findings || []), ...(persistedScan.secrets || []),
|
|
725
|
+
...(persistedScan.supplyChain || []).filter(s => s.type === 'vulnerable_dep'),
|
|
726
|
+
].map(fingerprintFinding));
|
|
727
|
+
const removed = prevAll.filter(f => !currentFps.has(fingerprintFinding(f)));
|
|
728
|
+
persistedScan.mttr = computeMTTR(removed);
|
|
686
729
|
// Surface the SLA-breach line on human-readable formats (not JSON/CI pipes).
|
|
687
730
|
const isJson = format === 'json' || format === 'sarif' || format === 'cyclonedx' || format === 'sbom' || format === 'spdx' || format === 'vex' || format === 'openvex' || format === 'pbom' || format === 'aibom';
|
|
688
731
|
if (!isJson) {
|
|
689
732
|
const sla = renderSlaSummary(persistedScan.findings || []);
|
|
690
733
|
if (sla) process.stderr.write(`⏰ agentic-security: ${sla}\n`);
|
|
734
|
+
if (persistedScan.mttr.count > 0) {
|
|
735
|
+
process.stderr.write(`✅ agentic-security: ${persistedScan.mttr.count} finding(s) fixed since last scan, median ${Math.round(persistedScan.mttr.medianDays)}d to remediate\n`);
|
|
736
|
+
}
|
|
691
737
|
}
|
|
692
|
-
} catch {
|
|
738
|
+
} catch (e) {
|
|
739
|
+
// MTTR is best-effort — never block a scan write. But a silent catch
|
|
740
|
+
// here previously hid a real, deterministic CI-only failure (the
|
|
741
|
+
// mttr field went missing on every hosted-CI run, never locally, and
|
|
742
|
+
// nothing explained why) for long enough that it shipped several
|
|
743
|
+
// releases undiagnosed. Surface it — never fail the scan on it.
|
|
744
|
+
if (process.env.AGENTIC_SECURITY_MTTR_DEBUG === '1' || process.env.CI || process.env.GITHUB_ACTIONS) {
|
|
745
|
+
process.stderr.write(`agentic-security: MTTR computation failed (best-effort, scan unaffected): ${(e && e.stack) || e}\n`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
693
748
|
|
|
694
749
|
// R5 — report the observed time-to-validated-fix distribution from the
|
|
695
750
|
// fix attempts recorded by `verifyFix`. This is measurement, not
|
|
@@ -735,10 +790,17 @@ async function cmdScan(args) {
|
|
|
735
790
|
|
|
736
791
|
// 0.14.0 — update streak / achievements after every full scan. Suppress
|
|
737
792
|
// streak side effects when the user only wants raw JSON output (CI piping).
|
|
793
|
+
// Gated on `persistedScan !== null` (i.e. the same _writesOnScan() &&
|
|
794
|
+
// _isSafeStateDir() condition above) — recordScan()'s own isSafeStateDir
|
|
795
|
+
// check alone does not know about --no-state / AGENTIC_SECURITY_NO_STATE,
|
|
796
|
+
// so without this a "read-only, mutates nothing" scan would still write
|
|
797
|
+
// streak.json. This was accidentally masked before the persistedScan
|
|
798
|
+
// scoping fix (recordScan was never reached at all, for the wrong reason);
|
|
799
|
+
// fixing that reachability bug required adding this guard explicitly.
|
|
738
800
|
try {
|
|
739
|
-
const streak = recordScan(stateDirPath, persistedScan);
|
|
801
|
+
const streak = persistedScan !== null ? recordScan(stateDirPath, persistedScan) : null;
|
|
740
802
|
// Print celebration / streak line to stderr so it doesn't pollute --format json
|
|
741
|
-
if (process.stderr.isTTY && format !== 'json' && format !== 'sarif') {
|
|
803
|
+
if (streak && process.stderr.isTTY && format !== 'json' && format !== 'sarif') {
|
|
742
804
|
const delta = formatGradeDelta(streak);
|
|
743
805
|
const line = formatStreakLine(streak);
|
|
744
806
|
if (delta) process.stderr.write('\n' + delta + '\n');
|
|
@@ -814,20 +876,48 @@ async function cmdCi(args) {
|
|
|
814
876
|
const packNames = packArg ? (Array.isArray(packArg) ? packArg : String(packArg).split(',')) : [];
|
|
815
877
|
if (packNames.length) Object.assign(scan, applyPacks(scan, packNames));
|
|
816
878
|
|
|
817
|
-
//
|
|
879
|
+
// R4 — same run attestation cmdScan attaches, computed here too so
|
|
880
|
+
// findings.json (the actual CI evidence artifact) carries the same
|
|
881
|
+
// tamper-evidence digest 'agentic-security scan' does, rather than
|
|
882
|
+
// always shipping attestation: null. Runs after every filter above, so
|
|
883
|
+
// it attests what actually ships. Metadata only — never fails the scan.
|
|
884
|
+
try {
|
|
885
|
+
const { computeRunAttestation } = await import('../src/posture/attestation.js');
|
|
886
|
+
const { keyProvenance } = await import('../src/posture/integrity.js');
|
|
887
|
+
const { effectiveVersion } = await import('../src/posture/ruleset-version.js');
|
|
888
|
+
scan.attestation = computeRunAttestation({
|
|
889
|
+
findings: normalizeFindings(scan),
|
|
890
|
+
engineVersion: PKG_VERSION,
|
|
891
|
+
rulesetVersion: effectiveVersion(targetAbs).version,
|
|
892
|
+
bundleSha: _bundleSha(),
|
|
893
|
+
root: targetAbs,
|
|
894
|
+
sign: true,
|
|
895
|
+
});
|
|
896
|
+
if (scan.attestation) scan.attestation.keyProvenance = keyProvenance();
|
|
897
|
+
} catch { /* attestation is metadata; never fail a scan over it */ }
|
|
898
|
+
|
|
899
|
+
// Persist the three CI artifacts — but a refusal to write must NEVER skip
|
|
900
|
+
// the --fail-on evaluation below. S1 (Stage-0 audit, 2026): this used to be
|
|
901
|
+
// a bare `return;` inside the write-guard, so `cmdCi` returned `undefined`
|
|
902
|
+
// and `process.exit(await cmdCi(args))` became `process.exit(undefined)`,
|
|
903
|
+
// which Node treats as exit 0 — a CI pipeline reading only $? would see a
|
|
904
|
+
// passing build whose findings were never evaluated against --fail-on at
|
|
905
|
+
// all. The write and the gate are orthogonal: "we couldn't persist
|
|
906
|
+
// artifacts" is not evidence about "the scan is clean."
|
|
818
907
|
const stateDirPath = stateDir(targetAbs);
|
|
819
908
|
const { isSafeStateDir: _isSafeCi, stateWritesEnabled: _writesOnCi } = await import('../src/posture/state-dir.js');
|
|
820
|
-
|
|
909
|
+
const _canWriteCi = _writesOnCi() && _isSafeCi(stateDirPath);
|
|
910
|
+
if (!_canWriteCi) {
|
|
821
911
|
if (process.env.AGENTIC_SECURITY_DEBUG === '1') process.stderr.write(`[agentic-security] refusing to write CI artifacts at ${stateDirPath} — no project marker\n`);
|
|
822
|
-
|
|
912
|
+
} else {
|
|
913
|
+
await fsp.mkdir(stateDirPath, { recursive: true });
|
|
914
|
+
await fsp.writeFile(path.join(stateDirPath, 'findings.json'),
|
|
915
|
+
JSON.stringify(toJSON(scan, meta), null, 2));
|
|
916
|
+
await fsp.writeFile(path.join(stateDirPath, 'findings.sarif'),
|
|
917
|
+
JSON.stringify(toSARIF(scan, meta), null, 2));
|
|
918
|
+
await fsp.writeFile(path.join(stateDirPath, 'findings.junit.xml'),
|
|
919
|
+
toJUnit(scan, meta));
|
|
823
920
|
}
|
|
824
|
-
await fsp.mkdir(stateDirPath, { recursive: true });
|
|
825
|
-
await fsp.writeFile(path.join(stateDirPath, 'findings.json'),
|
|
826
|
-
JSON.stringify(toJSON(scan, meta), null, 2));
|
|
827
|
-
await fsp.writeFile(path.join(stateDirPath, 'findings.sarif'),
|
|
828
|
-
JSON.stringify(toSARIF(scan, meta), null, 2));
|
|
829
|
-
await fsp.writeFile(path.join(stateDirPath, 'findings.junit.xml'),
|
|
830
|
-
toJUnit(scan, meta));
|
|
831
921
|
|
|
832
922
|
const scanCode = exitCodeFor(scan);
|
|
833
923
|
const findings = normalizeFindings(scan);
|
|
@@ -835,7 +925,9 @@ async function cmdCi(args) {
|
|
|
835
925
|
for (const f of findings) sev[f.severity] = (sev[f.severity] || 0) + 1;
|
|
836
926
|
process.stderr.write(
|
|
837
927
|
`[ci] ${findings.length} findings — ${sev.critical} critical · ${sev.high} high · ${sev.medium} medium · ${sev.low} low\n` +
|
|
838
|
-
|
|
928
|
+
(_canWriteCi
|
|
929
|
+
? `[ci] artifacts: .agentic-security/findings.{json,sarif,junit.xml}\n`
|
|
930
|
+
: `[ci] artifacts: NOT written (state writes refused — set AGENTIC_SECURITY_DEBUG=1 for the reason)\n`) +
|
|
839
931
|
`[ci] fail-on=${failOn} scan-exit=${scanCode}\n`
|
|
840
932
|
);
|
|
841
933
|
// FR-SDLC-9: when --policy <file.rego> is supplied, evaluate against the
|
|
@@ -1758,16 +1850,73 @@ async function cmdAttest(args) {
|
|
|
1758
1850
|
return 0;
|
|
1759
1851
|
}
|
|
1760
1852
|
|
|
1853
|
+
// R4/D2 correctness follow-up: attestation.js ships TWO independent
|
|
1854
|
+
// verifiers — verifyEvidenceBundle (per-finding, Ed25519, self-contained —
|
|
1855
|
+
// only needs a public key) and verifyRunAttestation (whole-run, per-install
|
|
1856
|
+
// HMAC, NOT self-contained — needs the caller to also supply the finding
|
|
1857
|
+
// set to re-derive against). This CLI command only ever called the first;
|
|
1858
|
+
// the second had zero production callers anywhere in the repo (confirmed by
|
|
1859
|
+
// grep — only test/attestation.test.js called it). Auto-detect which
|
|
1860
|
+
// artifact was handed in and dispatch to the matching verifier instead of
|
|
1861
|
+
// silently misinterpreting a run-attestation as a (structurally different)
|
|
1862
|
+
// evidence bundle.
|
|
1863
|
+
function _asRunAttestation(obj) {
|
|
1864
|
+
if (obj && typeof obj.digest === 'string' && obj.canonicalisation) return obj;
|
|
1865
|
+
if (obj && obj.attestation && typeof obj.attestation.digest === 'string') return obj.attestation;
|
|
1866
|
+
return null;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// Verifies a run attestation the ONLY way it can be verified: by re-deriving
|
|
1870
|
+
// the digest from a fresh scan and comparing. Unlike an evidence bundle
|
|
1871
|
+
// (self-contained, just needs a public key), a run attestation is a claim
|
|
1872
|
+
// about a FINDING SET, not a standalone signed blob — "does scanning this
|
|
1873
|
+
// codebase right now reproduce the exact digest an earlier run attested"
|
|
1874
|
+
// is the actual, meaningful question this command answers.
|
|
1875
|
+
async function cmdVerifyRunAttestation(attestation, args) {
|
|
1876
|
+
const projectPath = path.resolve(args.flags.against || '.');
|
|
1877
|
+
if (!fs.existsSync(projectPath)) {
|
|
1878
|
+
console.error(`--against path does not exist: ${projectPath}`);
|
|
1879
|
+
return 2;
|
|
1880
|
+
}
|
|
1881
|
+
console.log(`Re-scanning ${projectPath} to check it reproduces the attested digest...`);
|
|
1882
|
+
const { runScan } = await import('../src/runScan.js');
|
|
1883
|
+
const { normalizeFindings } = await import('../src/report/index.js');
|
|
1884
|
+
const { effectiveVersion } = await import('../src/posture/ruleset-version.js');
|
|
1885
|
+
const { verifyRunAttestation } = await import('../src/posture/attestation.js');
|
|
1886
|
+
const { scan } = await runScan(projectPath);
|
|
1887
|
+
const r = verifyRunAttestation(attestation, {
|
|
1888
|
+
findings: normalizeFindings(scan),
|
|
1889
|
+
engineVersion: PKG_VERSION,
|
|
1890
|
+
rulesetVersion: effectiveVersion(projectPath).version,
|
|
1891
|
+
bundleSha: _bundleSha(),
|
|
1892
|
+
root: projectPath,
|
|
1893
|
+
});
|
|
1894
|
+
if (!r.ok) {
|
|
1895
|
+
console.error(`✗ INVALID — ${r.reason}`);
|
|
1896
|
+
return 1;
|
|
1897
|
+
}
|
|
1898
|
+
console.log('✓ VALID — a fresh scan of this project reproduces the attested digest exactly.');
|
|
1899
|
+
console.log('');
|
|
1900
|
+
console.log(` digest: ${attestation.digest}`);
|
|
1901
|
+
console.log(` findingCount: ${attestation.findingCount}`);
|
|
1902
|
+
console.log(` proves: ${attestation.proves}`);
|
|
1903
|
+
console.log(` does NOT prove: ${attestation.doesNotProve}`);
|
|
1904
|
+
return 0;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1761
1907
|
async function cmdVerifyAttestation(args) {
|
|
1762
1908
|
const { verifyEvidenceBundle, keyPaths } = await import('../src/posture/evidence-bundle.js');
|
|
1763
1909
|
// `args._[0]` is the command name itself — the same convention cmdScan uses.
|
|
1764
1910
|
const file = args.flags.bundle || args._[1];
|
|
1765
|
-
if (!file) { console.error('Usage: agentic-security verify-attestation <bundle.json> [--public-key <path>]'); return 2; }
|
|
1911
|
+
if (!file) { console.error('Usage: agentic-security verify-attestation <bundle.json|last-scan.json> [--public-key <path>] [--against <project-path>]'); return 2; }
|
|
1766
1912
|
|
|
1767
1913
|
let bundle;
|
|
1768
1914
|
try { bundle = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8')); }
|
|
1769
1915
|
catch (e) { console.error(`Could not read bundle: ${e.message}`); return 2; }
|
|
1770
1916
|
|
|
1917
|
+
const runAttestation = _asRunAttestation(bundle);
|
|
1918
|
+
if (runAttestation) return cmdVerifyRunAttestation(runAttestation, args);
|
|
1919
|
+
|
|
1771
1920
|
const keyFile = args.flags['public-key'] || keyPaths().publicKey;
|
|
1772
1921
|
let publicKeyPem = null;
|
|
1773
1922
|
try { publicKeyPem = fs.readFileSync(path.resolve(keyFile), 'utf8'); }
|
package/dist/113.index.js
CHANGED
|
@@ -78,7 +78,7 @@ function recordFixAttempt(scanRoot, record) {
|
|
|
78
78
|
try {
|
|
79
79
|
const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .stateDir */ .Pn)(scanRoot);
|
|
80
80
|
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.isSafeStateDir)(dir)) return false;
|
|
81
|
-
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return;
|
|
81
|
+
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return false;
|
|
82
82
|
node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(dir, { recursive: true });
|
|
83
83
|
// One writeSync of one newline-terminated line: a concurrent reader sees
|
|
84
84
|
// whole records or nothing, and a torn tail is dropped on read.
|
|
@@ -266,7 +266,16 @@ function _detectRunner(scanRoot) {
|
|
|
266
266
|
} catch { return null; }
|
|
267
267
|
})();
|
|
268
268
|
if (pkg && pkg.scripts && pkg.scripts.test && !/no test specified/.test(String(pkg.scripts.test))) {
|
|
269
|
-
|
|
269
|
+
// --passWithNoTests is Jest-specific CLI syntax — appending it
|
|
270
|
+
// unconditionally broke every non-Jest npm test script (mocha, vitest,
|
|
271
|
+
// ava, tap, or a plain node script) with an "unrecognized option" exit,
|
|
272
|
+
// failing verification for a reason that has nothing to do with
|
|
273
|
+
// whether the patch actually broke anything. Only add it when Jest is
|
|
274
|
+
// actually the configured runner.
|
|
275
|
+
const usesJest = /\bjest\b/.test(String(pkg.scripts.test))
|
|
276
|
+
|| Boolean(pkg.devDependencies?.jest) || Boolean(pkg.dependencies?.jest);
|
|
277
|
+
const args = usesJest ? ['test', '--silent', '--', '--passWithNoTests'] : ['test', '--silent'];
|
|
278
|
+
return { runner: 'npm', cmd: 'npm', args };
|
|
270
279
|
}
|
|
271
280
|
if (has('pytest.ini') || has('pyproject.toml') || has('setup.cfg')) {
|
|
272
281
|
return { runner: 'pytest', cmd: 'pytest', args: ['-q', '--no-header', '-x'] };
|
|
@@ -406,8 +415,8 @@ var external_node_child_process_ = __webpack_require__(1421);
|
|
|
406
415
|
var external_node_fs_ = __webpack_require__(3024);
|
|
407
416
|
// EXTERNAL MODULE: external "node:path"
|
|
408
417
|
var external_node_path_ = __webpack_require__(6760);
|
|
409
|
-
// EXTERNAL MODULE: ./src/engine.js +
|
|
410
|
-
var engine = __webpack_require__(
|
|
418
|
+
// EXTERNAL MODULE: ./src/engine.js + 196 modules
|
|
419
|
+
var engine = __webpack_require__(2739);
|
|
411
420
|
;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
|
|
412
421
|
// Deterministic honesty gates on fix / finding output (#7).
|
|
413
422
|
//
|
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__(2739);
|
|
17
17
|
// Time-travel + counterfactual scanning (v0.68).
|
|
18
18
|
//
|
|
19
19
|
// Two new modes that exploit the pure-input shape of runFullScan:
|