@clear-capabilities/agentic-security-scanner 0.144.0 → 0.145.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 +251 -0
- package/bin/agentic-security.js +294 -3
- package/dist/113.index.js +11 -3
- package/dist/178.index.js +24 -6
- package/dist/271.index.js +165 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +22 -0
- package/dist/444.index.js +11 -2
- package/dist/449.index.js +76 -12
- package/dist/526.index.js +11 -3
- package/dist/637.index.js +27 -5
- package/dist/970.index.js +65 -1
- package/dist/agentic-security.mjs +9 -9
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +14 -8
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/engine.js +488 -29
- package/src/fix/apply-fix-service.js +1 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +1 -1
- package/src/lsp/server.js +49 -2
- package/src/mcp/tools.js +20 -0
- package/src/pipeline/assurance-mode.js +64 -1
- package/src/pipeline/finding-schema.js +8 -1
- package/src/posture/CLAUDE.md +121 -0
- package/src/posture/accuracy-scorecard.js +60 -0
- package/src/posture/artifact-registry.js +24 -0
- package/src/posture/auditor-walkthrough.js +116 -13
- package/src/posture/compliance-policy.js +12 -2
- package/src/posture/cross-repo-memory.js +7 -2
- package/src/posture/fix-history.js +25 -2
- package/src/posture/fix-verify.js +9 -1
- package/src/posture/fleet.js +0 -0
- package/src/posture/git-history.js +13 -5
- package/src/posture/material-change.js +21 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +14 -0
- package/src/posture/provenance/ai-authorship.js +68 -0
- package/src/posture/provenance/branch-entry.js +80 -0
- package/src/posture/provenance/cache.js +143 -0
- package/src/posture/provenance/confidence.js +36 -0
- package/src/posture/provenance/coordinator.js +786 -0
- package/src/posture/provenance/dag-walk.js +249 -0
- package/src/posture/provenance/evidence-attribution.js +59 -0
- package/src/posture/provenance/git-evidence.js +310 -0
- package/src/posture/provenance/lifecycle.js +208 -0
- package/src/posture/provenance/missing-control-resolver.js +137 -0
- package/src/posture/provenance/origin-resolver.js +342 -0
- package/src/posture/provenance/predicate-replay.js +133 -0
- package/src/posture/provenance/providers/config.js +39 -0
- package/src/posture/provenance/providers/github.js +62 -0
- package/src/posture/provenance/providers/gitlab.js +58 -0
- package/src/posture/provenance/repo-lineage.js +74 -0
- package/src/posture/provenance/sca-origin.js +139 -0
- package/src/posture/provenance/schema.js +255 -0
- package/src/posture/provenance/transitive-sca.js +147 -0
- package/src/posture/provenance/validate.js +30 -0
- package/src/posture/provenance-evidence-bundle.js +144 -0
- package/src/posture/sbom-diff.js +15 -2
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +38 -14
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/report/index.js +197 -3
- package/src/runScan.js +34 -5
- package/src/sast/rate-limit.js +33 -3
- package/src/util/git-hardening.js +128 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,257 @@
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
## 0.145.0 — Finding Provenance ships, and two audits find what the first one missed
|
|
15
|
+
|
|
16
|
+
Every finding now carries a `findingProvenance` record answering "when did this
|
|
17
|
+
enter the codebase, and how sure are we?" — resolved from Git history, not
|
|
18
|
+
guessed. It is **opt-in**: pass `--provenance` (or any provenance flag, e.g.
|
|
19
|
+
`--provenance-since`, `--require-provenance`) on a scan of a Git repository.
|
|
20
|
+
|
|
21
|
+
It is opt-in rather than on-by-default because the release gate measured what
|
|
22
|
+
on-by-default costs: time-to-first-finding over a 207-file tree went 4.5s to
|
|
23
|
+
45s. That is a 7.6x regression on the one metric this product's own benchmark
|
|
24
|
+
calls the binding constraint for its ICP — "how long until the FIRST useful
|
|
25
|
+
result, not aggregate F1" — and resolving commit history for every finding is
|
|
26
|
+
not what a first-time user is waiting on. CI, compliance and triage callers
|
|
27
|
+
that want provenance ask for it explicitly and pay the cost knowingly.
|
|
28
|
+
|
|
29
|
+
**What it resolves.** For a SAST finding: the commit that introduced it, found
|
|
30
|
+
by replaying the finding's own predicate against historical blobs (`git blame`
|
|
31
|
+
answers "who last touched this line", which is a different and usually wrong
|
|
32
|
+
question) and then confirming the predicate was FALSE in that commit's parent.
|
|
33
|
+
For a direct dependency: the commit that moved the declared version in
|
|
34
|
+
`package.json`/`requirements.txt` into an advisory's vulnerable range. Alongside
|
|
35
|
+
the origin commit: the branch/PR the change entered through, the evidence nodes
|
|
36
|
+
(source/sink/manifest, each as a path:line:commit triple), a confidence level
|
|
37
|
+
with its reasons, and a lifecycle ledger of introduce/remediate/reintroduce
|
|
38
|
+
events at `.agentic-security/provenance/lifecycle.json`.
|
|
39
|
+
|
|
40
|
+
**Only a complete scan can close a finding.** The ledger's remediation pass turns
|
|
41
|
+
a finding's *absence* into the claim "this was fixed," which is sound only when
|
|
42
|
+
the scan actually looked everywhere it could have found it. A `--changed-since` /
|
|
43
|
+
`--pr` scan, or any caller-supplied file list (the MCP `scan_diff` tool, the LSP's
|
|
44
|
+
on-save scan), therefore records new and reintroduced findings normally but closes
|
|
45
|
+
nothing — the entries it did not look at stay open until a full scan says
|
|
46
|
+
otherwise. For the same reason the ledger is written only when the scan target is
|
|
47
|
+
a real directory: a scan of a path that does not exist resolves its state
|
|
48
|
+
directory by walking up from the current working directory, and would otherwise
|
|
49
|
+
write a verdict about a project it never read.
|
|
50
|
+
|
|
51
|
+
**It refuses to guess.** Every record carries a terminal `status`, and there is
|
|
52
|
+
no path that leaves the field absent: `complete`, `partial` (history could not
|
|
53
|
+
confirm a parent boundary — a shallow clone, or an advisory with no `introduced`
|
|
54
|
+
bound), `uncommitted` (the finding exists only in the working tree),
|
|
55
|
+
`not_available`, `budget_exhausted`, or `error`. A shallow clone can never reach
|
|
56
|
+
`complete`. Author emails are redacted from every output format unless
|
|
57
|
+
`--include-author-email` is passed.
|
|
58
|
+
|
|
59
|
+
**New flags** (`agentic-security scan --help` documents all seven):
|
|
60
|
+
`--provenance <standard|deep>`, `--no-provenance`, `--provenance-since <ref>`,
|
|
61
|
+
`--provenance-timeout <ms>`, `--include-author-email`, `--pseudonymize-authors`,
|
|
62
|
+
`--require-provenance`. `deep` mode performs real non-linear DAG analysis (see
|
|
63
|
+
the M3 subsection below) — it is no longer a stub that silently runs `standard`.
|
|
64
|
+
`--require-provenance` reports unresolved provenance as a scan-health condition
|
|
65
|
+
and downgrades `scanHealth.status` to `partial`; it never changes the exit code
|
|
66
|
+
by itself — see the M2 subsection below for the mechanism that does.
|
|
67
|
+
`--verbose --firehose` prints the provenance block per finding in text output.
|
|
68
|
+
See [`docs/guides/finding-provenance.md`](docs/guides/finding-provenance.md)
|
|
69
|
+
for the full user-facing writeup.
|
|
70
|
+
|
|
71
|
+
### M2: format parity, compliance/MTTR/fix-lifecycle surfacing, and `--assurance strict` can now fail the build
|
|
72
|
+
|
|
73
|
+
M0+M1 landed provenance resolution and the JSON/text surfaces above. M2 threads
|
|
74
|
+
that data through every other output and, for the first time, gives provenance
|
|
75
|
+
completeness the power to fail a CI build.
|
|
76
|
+
|
|
77
|
+
- **Format parity.** SARIF, CSV, Markdown, and HTML output now all carry each
|
|
78
|
+
finding's `findingProvenance`, matching what JSON already had — a consumer no
|
|
79
|
+
longer has to switch formats to see it.
|
|
80
|
+
- **Compliance evidence.** Auditor-walkthrough and NIST Privacy Framework gap
|
|
81
|
+
findings now carry `controlRefs` (the finding ids backing a control's gap
|
|
82
|
+
determination) and `derivedProvenance` (the earliest proven open condition
|
|
83
|
+
among those findings, with a `confidence` level and stated `limitations`).
|
|
84
|
+
- **MTTR.** `mttr.js` reports `ageBasis` (`finding_origin` | `earliest_observable`
|
|
85
|
+
| `uncommitted` | `first_observed`) and `provenAgeDays` alongside the existing
|
|
86
|
+
wall-clock `ageDays`, so age-to-remediate can be read against the commit that
|
|
87
|
+
actually introduced the finding, not just when the scanner first saw it.
|
|
88
|
+
- **Fix records.** Every fix-history entry now carries a `provenanceAtFix`
|
|
89
|
+
snapshot — the finding's `findingProvenance` as it stood at the moment the fix
|
|
90
|
+
was applied — so a later audit can see what was known at fix time, not just
|
|
91
|
+
what is knowable now.
|
|
92
|
+
- **`--assurance strict` can now fail the build over incomplete provenance.**
|
|
93
|
+
This is the behavior change that matters most, and it is a genuinely new
|
|
94
|
+
failure mode, not a rewording of `--require-provenance` above: strict mode
|
|
95
|
+
(`--assurance strict`) now treats any finding whose `findingProvenance.status`
|
|
96
|
+
is outside `[complete, uncommitted]` as making the scan incomplete, and fails
|
|
97
|
+
the same way it already fails on a failed or skipped analyzer. `--require-provenance`
|
|
98
|
+
is unchanged by this — it still only flags and never fails the exit code.
|
|
99
|
+
`--assurance strict` is the new, separate mechanism that does fail it. See the
|
|
100
|
+
`KNOWN INTERACTION` comment in `scanner/src/pipeline/assurance-mode.js` for a
|
|
101
|
+
known, disclosed consequence: transitive `vulnerable_dep`, `unpinned_dep`, and
|
|
102
|
+
`no_lockfile` supply-chain findings are stamped `not_available` today (the
|
|
103
|
+
latter two are a category error — there is no commit that introduced a
|
|
104
|
+
*missing* lockfile — not merely a deferral), so `--assurance strict` will fail
|
|
105
|
+
on nearly any real project with a dependency manifest until that gap closes.
|
|
106
|
+
|
|
107
|
+
### M3: real non-linear history, transitive-dependency provenance, missing-control regressions, PR/CODEOWNERS enrichment
|
|
108
|
+
|
|
109
|
+
- **`--provenance deep` now does real work.** Instead of the M0–M2 stub that
|
|
110
|
+
accepted the flag and silently ran `standard`, deep mode walks every parent
|
|
111
|
+
of a merge commit (not just the first), which resolves origins standard
|
|
112
|
+
mode's first-parent-only walk cannot see, and detects reverts and
|
|
113
|
+
cherry-picks — surfaced as `findingOrigin.revertOf` / `.cherryPickOf` — via
|
|
114
|
+
a real unified-diff inversion.
|
|
115
|
+
- **Transitive-dependency provenance is now live-wired**, not merely
|
|
116
|
+
modeled: `transitive-sca.js` re-derives lockfile ancestry per historical
|
|
117
|
+
commit to find the commit that moved a *lockfile-resolved* (not
|
|
118
|
+
manifest-declared) dependency into an advisory's vulnerable range.
|
|
119
|
+
- **Missing-control regressions** — a previously-observed safeguard (today:
|
|
120
|
+
`sast/rate-limit.js`'s findings) disappearing — can now resolve a real
|
|
121
|
+
origin via `missing-control-resolver.js`.
|
|
122
|
+
- **Optional GitHub/GitLab PR-metadata + CODEOWNERS enrichment**
|
|
123
|
+
(`findingProvenance.providerEnrichment`) is live for `complete`-status
|
|
124
|
+
findings, configured via `.agentic-security/provenance-providers.yml` /
|
|
125
|
+
`AGENTIC_SECURITY_GITHUB_TOKEN` / `AGENTIC_SECURITY_GITLAB_TOKEN`, capped
|
|
126
|
+
per scan.
|
|
127
|
+
|
|
128
|
+
### M4: signed provenance evidence bundles, cross-repository lineage, an AI-authorship hook
|
|
129
|
+
|
|
130
|
+
- **`agentic-security attest --provenance`** signs a per-finding provenance
|
|
131
|
+
record (origin commit, confidence, evidence attribution) with the same
|
|
132
|
+
Ed25519 key material as the existing finding-evidence bundle mechanism;
|
|
133
|
+
`verify-attestation` auto-detects and verifies it against a public key
|
|
134
|
+
alone. Note the flag-shape collision with `scan`: on `attest`, `--provenance`
|
|
135
|
+
takes an optional *finding id*, not a mode — `attest --provenance deep`
|
|
136
|
+
looks for a finding literally named `deep`.
|
|
137
|
+
- **Cross-repository lineage.** An operator-declared
|
|
138
|
+
`.agentic-security/repo-lineage.json` can link a root-commit origin (a
|
|
139
|
+
finding whose earliest commit has no parent in the current repo) across a
|
|
140
|
+
prior, local-clone-only fork/split history. Resolution is conservative:
|
|
141
|
+
content at the linked line must actually match, not merely exist, before
|
|
142
|
+
an origin is reported, and the record discloses the boundary crossing
|
|
143
|
+
explicitly.
|
|
144
|
+
- **An extensible AI-authorship verifier registry**
|
|
145
|
+
(`registerAIAuthorshipVerifier` / `resolveAIAuthorship`) now stamps every
|
|
146
|
+
SAST `findingOrigin` with an `aiAuthorship` field; no verifier is
|
|
147
|
+
registered today, so it defaults honestly to `{status:'unknown', verifier:null}`.
|
|
148
|
+
- **Fleet-wide rollups** (`fleet.js`) now surface provenance-proven
|
|
149
|
+
remediation debt. Fleet MTTR is honestly disclosed rather than fabricated:
|
|
150
|
+
real remediation-timing data isn't reachable from the production fleet
|
|
151
|
+
driver without new state, so `rollupFleet` distinguishes "never tracked"
|
|
152
|
+
from "tracked, zero remediations" instead of reporting a misleading number.
|
|
153
|
+
|
|
154
|
+
### PRD completion: injection hardening, evidence-digest binding, retention split, and the first real coverage/accuracy measurements
|
|
155
|
+
|
|
156
|
+
- Author names and commit summaries are now sanitized against terminal
|
|
157
|
+
control-character and Markdown/HTML injection everywhere they reach a
|
|
158
|
+
human (FR-PROV-026).
|
|
159
|
+
- The run-attestation digest and provenance cache key are now genuinely
|
|
160
|
+
bound to the PRD-named inputs they claim to cover, including
|
|
161
|
+
detector/ruleset version.
|
|
162
|
+
- Symlink-escape protection added to the git evidence layer.
|
|
163
|
+
- **`--pseudonymize-authors`** (new flag, listed above) replaces raw
|
|
164
|
+
commit-author names with a stable `Contributor-XXXXXXXX` id — for when you
|
|
165
|
+
need to compare "who introduced what" without a raw name in the output.
|
|
166
|
+
Honored everywhere `--include-author-email`'s redaction already was,
|
|
167
|
+
including PR-reviewer logins and CODEOWNERS entries from provider
|
|
168
|
+
enrichment.
|
|
169
|
+
- The provenance cache now lives in its own top-level, independently-retained
|
|
170
|
+
state directory (`.agentic-security/provenance-cache/`), split from the
|
|
171
|
+
permanent lifecycle ledger (`.agentic-security/provenance/lifecycle.json`),
|
|
172
|
+
so cache eviction can never touch permanent history.
|
|
173
|
+
- **Two PRD success metrics are now genuinely measured and published, not
|
|
174
|
+
just designed:** known-origin accuracy (12/13 = 92.3% on the labeled
|
|
175
|
+
corpus, against a ≥98% target) and provenance coverage (311/341 = 91.2% on
|
|
176
|
+
this repository's own tree, against a ≥95% target — all 30 shortfall
|
|
177
|
+
findings resolve to `partial`, not `error`/`not_available`, so the gap is
|
|
178
|
+
reduced confidence rather than pipeline failure).
|
|
179
|
+
- `scan.secrets` and blameable `scan.logicVulns` entries now go through real
|
|
180
|
+
origin resolution (real stableIds, real git history) instead of a
|
|
181
|
+
permanent `not_available` placeholder.
|
|
182
|
+
|
|
183
|
+
### Second-audit remediation: a hostile-repository RCE closed, and further honesty fixes
|
|
184
|
+
|
|
185
|
+
An independent second audit of the completed Finding Provenance PRD found
|
|
186
|
+
one security-critical gap and several places where a metric or a claim
|
|
187
|
+
wasn't as real as it read. All fixed this release:
|
|
188
|
+
|
|
189
|
+
- **Security fix (1 of 2).** Git subprocess calls in the provenance pipeline
|
|
190
|
+
are now hardened against a hostile repository's own `.git/config` (e.g. a
|
|
191
|
+
malicious `core.fsmonitor`), `.gitattributes` `textconv` drivers, and
|
|
192
|
+
external `diff` drivers — a repository could previously trigger arbitrary
|
|
193
|
+
code execution merely by being scanned, provenance on or off. The sweep
|
|
194
|
+
covers every git invocation in the scanner, not just the provenance
|
|
195
|
+
module, and a source-level guard now fails the build if a future `git
|
|
196
|
+
diff` call site omits `--no-ext-diff`. Removing the shell from two
|
|
197
|
+
remaining `execSync` call sites also closed a command-injection path via
|
|
198
|
+
attacker-controlled filenames.
|
|
199
|
+
- **Security fix (2 of 2), found while reviewing a performance optimization
|
|
200
|
+
in this same release.** A change that fused the parent-commit lookup into
|
|
201
|
+
an existing `git show` placed that field after the author name in a
|
|
202
|
+
delimiter-separated record. Git preserves a literal `0x1f` inside an
|
|
203
|
+
author name, so an outside contributor choosing their own author name
|
|
204
|
+
could shift the parse and select the bytes read as the parent commit.
|
|
205
|
+
Because a parent whose blobs cannot be fetched is indistinguishable from
|
|
206
|
+
one that genuinely lacks the finding, this could manufacture a
|
|
207
|
+
`status: 'complete'` origin with **HIGH confidence** for a boundary that
|
|
208
|
+
was never verified — a fabricated certainty claim that would then flow
|
|
209
|
+
into signed evidence bundles. The field now sits behind the commit hash
|
|
210
|
+
only, with hex validation as defense in depth. Both directions are pinned
|
|
211
|
+
by a regression test.
|
|
212
|
+
- The provenance cache key and evidence digest are now genuinely bound to
|
|
213
|
+
the running detector/ruleset version (previously always `null` in
|
|
214
|
+
practice), so upgrading the scanner correctly invalidates stale cached
|
|
215
|
+
provenance.
|
|
216
|
+
- `ageBasis` and `provenAgeDays` are now rendered wherever a finding's age is
|
|
217
|
+
shown, not only written to `last-scan.json`.
|
|
218
|
+
- Provenance coverage is now wired into the real, running scorecard-generation
|
|
219
|
+
path (see "PRD completion" above for the number) instead of always
|
|
220
|
+
reporting "unmeasured."
|
|
221
|
+
- **Performance measurement is now honest end-to-end**, and the honest
|
|
222
|
+
numbers are a real miss against target: real p95 (n=20) over both cold and
|
|
223
|
+
warm cache arms, and a genuine two-sided memory comparison (not a one-arm
|
|
224
|
+
heap delta). Measured this release — cold-cache time ~27x wall-clock p95
|
|
225
|
+
against a ≤1.3x (≤30% overhead) target, warm-cache ~2x; cold-cache memory
|
|
226
|
+
~13x against a ≤1.2x target. `bench:provenance-accuracy:check` is now
|
|
227
|
+
wired into the pre-push gate so the known-origin-accuracy number can no
|
|
228
|
+
longer silently rot.
|
|
229
|
+
- The PRD's **required compliance-evidence disclaimer** ("Provenance
|
|
230
|
+
establishes repository history for technical evidence. It does not prove
|
|
231
|
+
developer intent, control operation outside code, organizational
|
|
232
|
+
compliance, or certification.") now appears next to every
|
|
233
|
+
provenance-derived origin the auditor walkthrough renders
|
|
234
|
+
(`compliance --walkthrough`), and user-facing documentation for the whole
|
|
235
|
+
feature now exists at
|
|
236
|
+
[`docs/guides/finding-provenance.md`](docs/guides/finding-provenance.md).
|
|
237
|
+
- **Not fixed, disclosed honestly:** a finding in a file renamed after
|
|
238
|
+
introduction still degrades to `status: 'partial'` rather than resolving
|
|
239
|
+
its true pre-rename origin commit. Known and traced, not fixed this round.
|
|
240
|
+
|
|
241
|
+
### Breaking: SCA finding ids change for manifest-declared dependencies
|
|
242
|
+
|
|
243
|
+
Direct dependencies declared in `package.json` / `requirements.txt` now carry the
|
|
244
|
+
manifest **line number** where they are declared, and `report/index.js`'s
|
|
245
|
+
`fingerprint()` folds that line into the finding id. This was necessary for
|
|
246
|
+
provenance — an SCA finding had no line, so every `vulnerable_dep` from one
|
|
247
|
+
manifest previously hashed to the SAME id and could not be told apart — but it
|
|
248
|
+
means those ids are **not stable across this upgrade**.
|
|
249
|
+
|
|
250
|
+
Concretely: any triage verdict, baseline entry, or suppression keyed on the OLD
|
|
251
|
+
id of a `package.json`- or `requirements.txt`-declared dependency finding will no
|
|
252
|
+
longer match and is effectively orphaned. Affected state:
|
|
253
|
+
`.agentic-security/baseline.json`, triage memory, and `disable:`/suppression
|
|
254
|
+
entries naming an SCA finding id. Transitive dependencies (resolved from a
|
|
255
|
+
lockfile, not declared in a manifest) are unaffected, as are all SAST, secrets,
|
|
256
|
+
and business-logic findings.
|
|
257
|
+
|
|
258
|
+
**There is no automatic migration**, and that is deliberate rather than an
|
|
259
|
+
oversight — id aliasing would have to be carried indefinitely to be safe, and
|
|
260
|
+
the orphaned entries fail open (a finding reappears) rather than closed (a real
|
|
261
|
+
finding stays hidden). Re-triage or re-baseline the affected SCA findings after
|
|
262
|
+
upgrading; `agentic-security scan --set-baseline` regenerates the baseline in one
|
|
263
|
+
step.
|
|
264
|
+
|
|
14
265
|
## 0.144.0 — Assurance hardening closes Epic E2, and an independent audit finds what "verified" missed
|
|
15
266
|
|
|
16
267
|
The assurance-hardening PRD (`docs/implementation/assurance-hardening-*`)
|
package/bin/agentic-security.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createRequire } from 'node:module';
|
|
|
8
8
|
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
|
+
import { isProvenanceHealthy, sanitizeForTerminal } from '../src/posture/provenance/schema.js';
|
|
11
12
|
import { runScan } from '../src/runScan.js';
|
|
12
13
|
|
|
13
14
|
// Every command is dispatched as `process.exit(await cmdX(args))`, and
|
|
@@ -185,9 +186,20 @@ Options:
|
|
|
185
186
|
--no-epss Skip EPSS exploit-prediction enrichment (default: enabled)
|
|
186
187
|
--no-blast-radius Skip blast-radius / cost framing (default: enabled)
|
|
187
188
|
--verbose Include fix bodies + taxonomy in CLI output
|
|
189
|
+
(with --firehose, also prints each finding's git-origin provenance)
|
|
188
190
|
--output <file> Write report to file instead of stdout
|
|
189
191
|
--machine-output Always write .agentic-security/findings.{sarif,json,csv}
|
|
190
192
|
|
|
193
|
+
Finding provenance (which commit introduced each finding):
|
|
194
|
+
--provenance <standard|deep> Resolution depth (default: standard; deep explores non-linear ancestry — merges, reverts, cherry-picks)
|
|
195
|
+
--no-provenance Skip git-history provenance entirely (findings report not_available)
|
|
196
|
+
--provenance-since <ref> Do not walk history earlier than this git ref/commit
|
|
197
|
+
--provenance-timeout <ms> Whole-scan provenance budget in MILLISECONDS (default 60000)
|
|
198
|
+
--include-author-email Keep commit author emails in output (redacted by default)
|
|
199
|
+
--pseudonymize-authors Replace commit author names with a stable Contributor-XXXXXXXX id
|
|
200
|
+
--require-provenance Report unresolved provenance as a scan-health condition
|
|
201
|
+
(downgrades scanHealth.status to 'partial'; never changes the exit code)
|
|
202
|
+
|
|
191
203
|
Exit codes:
|
|
192
204
|
0 = clean 1 = low/medium 2 = high 3 = critical 4 = error`;
|
|
193
205
|
|
|
@@ -393,6 +405,112 @@ function parseArgs(argv) {
|
|
|
393
405
|
return args;
|
|
394
406
|
}
|
|
395
407
|
|
|
408
|
+
// The only values `--provenance` accepts. Used both to validate an inline
|
|
409
|
+
// `--provenance=<mode>` and to decide whether a following bare token is this
|
|
410
|
+
// flag's value at all — see the comment inside the parser.
|
|
411
|
+
const PROVENANCE_MODES = new Set(['standard', 'deep']);
|
|
412
|
+
|
|
413
|
+
// Finding Provenance (M0/M1) — CLI flags that set the env vars engine.js
|
|
414
|
+
// (Task 15) and report/index.js (Task 16) already read:
|
|
415
|
+
// AGENTIC_SECURITY_NO_PROVENANCE, AGENTIC_SECURITY_PROVENANCE_MODE,
|
|
416
|
+
// AGENTIC_SECURITY_PROVENANCE_SINCE, AGENTIC_SECURITY_PROVENANCE_TIMEOUT_MS,
|
|
417
|
+
// AGENTIC_SECURITY_INCLUDE_AUTHOR_EMAIL,
|
|
418
|
+
// AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS. `requireProvenance` is consumed
|
|
419
|
+
// directly by cmdScan (post-scan scanHealth augmentation), not via an env var.
|
|
420
|
+
// Kept as a pure function (argv in, plain object out) so it's unit-testable
|
|
421
|
+
// without invoking the CLI dispatch or touching process.env.
|
|
422
|
+
export function parseProvenanceFlags(argv) {
|
|
423
|
+
// OPT-IN, not opt-out (0.145.0). Provenance was on by default through M0-M4,
|
|
424
|
+
// and the release gate caught what that costs: time-to-first-finding on a
|
|
425
|
+
// 207-file tree went 4.5s -> 45s, a 7.6x regression on the ONE metric
|
|
426
|
+
// bench/ttff/runner.mjs's own header calls the binding constraint for this
|
|
427
|
+
// product's vibecoder ICP ("how long until the FIRST useful result, not
|
|
428
|
+
// aggregate F1"). Resolving history for every finding is simply not what a
|
|
429
|
+
// first-time user is waiting for. It stays one flag away, and CI/compliance
|
|
430
|
+
// callers that DO want it pass `--provenance` explicitly.
|
|
431
|
+
//
|
|
432
|
+
// `disabled` therefore starts true, and any provenance-shaped flag flips it
|
|
433
|
+
// off — asking for `--provenance-since` or `--require-provenance` is asking
|
|
434
|
+
// for provenance, and making the user also pass a bare `--provenance`
|
|
435
|
+
// alongside it would be a papercut with no upside.
|
|
436
|
+
const result = { mode: 'standard', since: null, timeoutMs: undefined, includeEmail: false, pseudonymize: false, requireProvenance: false, disabled: true, warning: null };
|
|
437
|
+
const warnings = [];
|
|
438
|
+
for (let i = 0; i < argv.length; i++) {
|
|
439
|
+
const a = argv[i];
|
|
440
|
+
if (typeof a !== 'string' || !a.startsWith('--')) continue;
|
|
441
|
+
// BOTH `--flag value` and `--flag=value`, because parseArgs() above accepts
|
|
442
|
+
// both for every other flag in this CLI and an operator has no way to know
|
|
443
|
+
// this one parser is different. Exact-string matching silently ignored
|
|
444
|
+
// `--provenance=deep` / `--provenance-since=v1.0.0` /
|
|
445
|
+
// `--provenance-timeout=30000` — no warning, defaults quietly used.
|
|
446
|
+
//
|
|
447
|
+
// Split on the FIRST `=` and keep the whole remainder, rather than
|
|
448
|
+
// parseArgs's `split('=', 2)` which drops everything after a second `=`.
|
|
449
|
+
// A git ref (`--provenance-since=refs/tags/v1=rc1`) is a legal value and
|
|
450
|
+
// truncating it would be a worse failure than the one being fixed.
|
|
451
|
+
const eq = a.indexOf('=');
|
|
452
|
+
const key = eq === -1 ? a : a.slice(0, eq);
|
|
453
|
+
const inline = eq === -1 ? undefined : a.slice(eq + 1);
|
|
454
|
+
// `--flag value` consumes the NEXT argv entry only when there is no inline
|
|
455
|
+
// value; a value that itself starts with `--` is another flag, not this
|
|
456
|
+
// one's argument.
|
|
457
|
+
const takeValue = () => {
|
|
458
|
+
if (inline !== undefined) return inline;
|
|
459
|
+
const next = argv[i + 1];
|
|
460
|
+
if (next !== undefined && !String(next).startsWith('--')) { i++; return next; }
|
|
461
|
+
return undefined;
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
if (key === '--no-provenance') result.disabled = true;
|
|
465
|
+
else if (key === '--provenance') {
|
|
466
|
+
result.disabled = false;
|
|
467
|
+
// The SPACE form only claims the next token when it actually names a
|
|
468
|
+
// mode. `--provenance` is legal on its own (provenance is on by default;
|
|
469
|
+
// the flag is how you say "standard, explicitly"), and this CLI's target
|
|
470
|
+
// is nearly always a positional — `scan --provenance ./src` must scan
|
|
471
|
+
// `./src`, not report `unrecognised mode './src'`. parseArgs() does its
|
|
472
|
+
// own positional collection over the same argv, so consuming here never
|
|
473
|
+
// steals the path from it, but the warning would still be a lie.
|
|
474
|
+
//
|
|
475
|
+
// The INLINE form has no such ambiguity: `--provenance=x` can only ever
|
|
476
|
+
// be a mode, so an unknown one is a typo worth naming.
|
|
477
|
+
const v = inline !== undefined
|
|
478
|
+
? inline
|
|
479
|
+
: (PROVENANCE_MODES.has(String(argv[i + 1])) ? argv[++i] : undefined);
|
|
480
|
+
if (v === 'deep' || v === 'standard' || v === undefined) {
|
|
481
|
+
result.mode = v || 'standard';
|
|
482
|
+
} else {
|
|
483
|
+
warnings.push(`unrecognised --provenance mode '${v}' (expected standard|deep), running standard`);
|
|
484
|
+
}
|
|
485
|
+
} else if (key === '--provenance-since') { result.since = takeValue() ?? null; result.disabled = false; }
|
|
486
|
+
else if (key === '--provenance-timeout') {
|
|
487
|
+
// MILLISECONDS, and validated as such. `parseInt` alone turned
|
|
488
|
+
// `--provenance-timeout 30s` into 30 — a 30-MILLISECOND budget that
|
|
489
|
+
// expires before the first `git blame` returns, so every finding came
|
|
490
|
+
// back `budget_exhausted` and nothing said why. A missing value produced
|
|
491
|
+
// NaN, which `if (_provFlags.timeoutMs)` then discarded silently. Both
|
|
492
|
+
// now warn and fall back to the engine default rather than inventing a
|
|
493
|
+
// budget the operator did not ask for. The unit is fixed by the env var
|
|
494
|
+
// this flag feeds, AGENTIC_SECURITY_PROVENANCE_TIMEOUT_MS.
|
|
495
|
+
const raw = takeValue();
|
|
496
|
+
if (raw === undefined) {
|
|
497
|
+
warnings.push('--provenance-timeout requires a value in milliseconds; using the default budget');
|
|
498
|
+
} else if (!/^\d+$/.test(String(raw).trim()) || parseInt(raw, 10) <= 0) {
|
|
499
|
+
warnings.push(`--provenance-timeout expects a positive integer number of MILLISECONDS, got '${raw}'; using the default budget`);
|
|
500
|
+
} else {
|
|
501
|
+
result.timeoutMs = parseInt(raw, 10);
|
|
502
|
+
result.disabled = false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else if (key === '--include-author-email') { result.includeEmail = true; result.disabled = false; }
|
|
506
|
+
else if (key === '--pseudonymize-authors') { result.pseudonymize = true; result.disabled = false; }
|
|
507
|
+
else if (key === '--require-provenance') { result.requireProvenance = true; result.disabled = false; }
|
|
508
|
+
}
|
|
509
|
+
// One field, so a caller that prints `warning` cannot drop the second one.
|
|
510
|
+
result.warning = warnings.length ? warnings.join('; ') : null;
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
|
|
396
514
|
async function cmdScan(args) {
|
|
397
515
|
// NON_MUTATING_SCAN_PRD S1 — a scan is an observation; --no-state makes it one.
|
|
398
516
|
if (args.flags['no-state']) {
|
|
@@ -488,12 +606,67 @@ async function cmdScan(args) {
|
|
|
488
606
|
process.stderr.write(`[pr-mode] scanning files changed since: ${changedSince}\n`);
|
|
489
607
|
}
|
|
490
608
|
|
|
609
|
+
// Finding Provenance (M0/M1) — translate --provenance/--no-provenance/etc.
|
|
610
|
+
// into the env vars engine.js already reads. Must run before runScan()
|
|
611
|
+
// below, same as the --deep/--no-deep wiring above.
|
|
612
|
+
const _provFlags = parseProvenanceFlags(process.argv.slice(2));
|
|
613
|
+
if (_provFlags.warning) console.error(`Warning: ${_provFlags.warning}`);
|
|
614
|
+
if (_provFlags.disabled) process.env.AGENTIC_SECURITY_NO_PROVENANCE = '1';
|
|
615
|
+
process.env.AGENTIC_SECURITY_PROVENANCE_MODE = _provFlags.mode;
|
|
616
|
+
if (_provFlags.since) process.env.AGENTIC_SECURITY_PROVENANCE_SINCE = _provFlags.since;
|
|
617
|
+
if (_provFlags.timeoutMs) process.env.AGENTIC_SECURITY_PROVENANCE_TIMEOUT_MS = String(_provFlags.timeoutMs);
|
|
618
|
+
if (_provFlags.includeEmail) process.env.AGENTIC_SECURITY_INCLUDE_AUTHOR_EMAIL = '1';
|
|
619
|
+
if (_provFlags.pseudonymize) process.env.AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS = '1';
|
|
620
|
+
|
|
491
621
|
const { scan, meta } = await runScan(target, {
|
|
492
622
|
changedSince,
|
|
493
623
|
onProgress: (p) => {
|
|
494
624
|
if (process.stderr.isTTY) process.stderr.write(`\r[${p.phase}] ${p.current}/${p.total} ${p.file} `);
|
|
495
625
|
},
|
|
496
626
|
});
|
|
627
|
+
// --require-provenance: flag (never fail) any finding whose provenance
|
|
628
|
+
// isn't resolved, via scanHealth — deliberately independent of the
|
|
629
|
+
// severity-based exit code computed by exitCodeFor() at the end of this
|
|
630
|
+
// function. 'uncommitted' is a legitimate terminal status (the finding is
|
|
631
|
+
// in a file with no git history yet), not an incomplete one.
|
|
632
|
+
if (_provFlags.requireProvenance) {
|
|
633
|
+
// ALL FOUR channels (scanner/CLAUDE.md: findings / secrets / supplyChain /
|
|
634
|
+
// logicVulns). Checking only `scan.findings` made the flag report a clean
|
|
635
|
+
// bill of provenance health for a scan whose SCA and secrets findings had
|
|
636
|
+
// none — and report/index.js's normalizeFindings ships all four to the
|
|
637
|
+
// user as findings, so "every finding" has to mean all four here too.
|
|
638
|
+
const incomplete = [];
|
|
639
|
+
for (const [channel, bucket] of [
|
|
640
|
+
['findings', scan.findings], ['secrets', scan.secrets],
|
|
641
|
+
['supplyChain', scan.supplyChain], ['logicVulns', scan.logicVulns],
|
|
642
|
+
]) {
|
|
643
|
+
for (const f of (bucket || [])) {
|
|
644
|
+
if (!f || typeof f !== 'object') continue;
|
|
645
|
+
if (isProvenanceHealthy(f.findingProvenance)) continue;
|
|
646
|
+
incomplete.push(f.id || f.stableId || `${channel}:${f.name || f.file || f.type || 'entry'}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (incomplete.length > 0) {
|
|
650
|
+
// Written the way EVERY other scan-health signal is written — a sentence
|
|
651
|
+
// in `conditions[]` plus a `complete` -> `partial` status demotion (see
|
|
652
|
+
// pipeline/scan-health.js's applyFreshness, the same "patch it on from
|
|
653
|
+
// bin/ after the engine already built scanHealth" pattern). The previous
|
|
654
|
+
// version set a bespoke `scanHealth.provenanceIncomplete` key that NO
|
|
655
|
+
// consumer reads: pipeline/assurance-mode.js and
|
|
656
|
+
// posture/compliance-policy.js both read `status`/`conditions[]` only, so
|
|
657
|
+
// --require-provenance changed no behaviour anywhere. The array is still
|
|
658
|
+
// carried, for a consumer that wants the ids, but it is no longer the
|
|
659
|
+
// only trace.
|
|
660
|
+
const condition = `--require-provenance: ${incomplete.length} finding(s) have unresolved provenance`;
|
|
661
|
+
scan.scanHealth = {
|
|
662
|
+
...(scan.scanHealth || {}),
|
|
663
|
+
conditions: [...(Array.isArray(scan.scanHealth?.conditions) ? scan.scanHealth.conditions : []), condition],
|
|
664
|
+
status: (scan.scanHealth?.status ?? 'complete') === 'complete' ? 'partial' : scan.scanHealth.status,
|
|
665
|
+
provenanceIncomplete: incomplete,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
497
670
|
// The BOM/attestation emitters stamp the producing engine's version into
|
|
498
671
|
// their metadata; carry the real package version so it can never drift.
|
|
499
672
|
if (meta && meta.engineVersion == null) meta.engineVersion = PKG_VERSION;
|
|
@@ -745,7 +918,20 @@ async function cmdScan(args) {
|
|
|
745
918
|
// per-finding list (with inline why-it-matters / how-it-fires / fix depth) so
|
|
746
919
|
// "Show ALL findings" actually shows them. Add --verbose for full narration + code.
|
|
747
920
|
if (args.flags.firehose && (!format || format === 'ship' || format === 'summary')) {
|
|
748
|
-
|
|
921
|
+
// `provenance` reuses --verbose rather than adding a seventh provenance
|
|
922
|
+
// flag: --verbose already means "print the extra per-finding narration"
|
|
923
|
+
// (explainParts's why/how/fix bodies), and the provenance block is exactly
|
|
924
|
+
// that kind of detail. Before this, explainProvenance()/toCLI's
|
|
925
|
+
// `{provenance}` option had NO production caller at all — it was reachable
|
|
926
|
+
// only from its own unit test, so a feature that was built, reviewed and
|
|
927
|
+
// shipped could never be seen by a user.
|
|
928
|
+
//
|
|
929
|
+
// Gated on provenance actually having run, too: with --no-provenance (or
|
|
930
|
+
// AGENTIC_SECURITY_NO_PROVENANCE=1) every finding carries a `not_available`
|
|
931
|
+
// record, and printing five lines of "we did not look" per finding is noise
|
|
932
|
+
// the operator explicitly opted out of.
|
|
933
|
+
const provenanceOn = !_provFlags.disabled && process.env.AGENTIC_SECURITY_NO_PROVENANCE !== '1';
|
|
934
|
+
body += '\n\n' + toCLI(scan, { verbose, provenance: verbose && provenanceOn });
|
|
749
935
|
}
|
|
750
936
|
|
|
751
937
|
// v3 next-gen — supplementary blocks for human-readable formats. These
|
|
@@ -1054,7 +1240,7 @@ async function cmdCi(args) {
|
|
|
1054
1240
|
console.error(`[ci] --assurance must be one of: ${ASSURANCE_MODES.join('|')} (got '${assuranceMode}')`);
|
|
1055
1241
|
return 1;
|
|
1056
1242
|
}
|
|
1057
|
-
const assuranceVerdict = evaluateAssuranceMode(assuranceMode, scan.scanHealth);
|
|
1243
|
+
const assuranceVerdict = evaluateAssuranceMode(assuranceMode, scan.scanHealth, findings);
|
|
1058
1244
|
if (!assuranceVerdict.ok) {
|
|
1059
1245
|
console.error(`[ci] assurance gate FAILED (mode=${assuranceMode}): ${assuranceVerdict.reason}`);
|
|
1060
1246
|
return 1;
|
|
@@ -2162,6 +2348,53 @@ async function cmdCompliance(args) {
|
|
|
2162
2348
|
|
|
2163
2349
|
async function cmdAttest(args) {
|
|
2164
2350
|
const scanRoot = path.resolve(args.flags.root || '.');
|
|
2351
|
+
|
|
2352
|
+
if (args.flags.provenance) {
|
|
2353
|
+
const {
|
|
2354
|
+
buildProvenanceEvidenceBundle, signProvenanceEvidenceBundle, ensureKeyPair,
|
|
2355
|
+
} = await import('../src/posture/provenance-evidence-bundle.js');
|
|
2356
|
+
|
|
2357
|
+
let scan;
|
|
2358
|
+
try { scan = JSON.parse(fs.readFileSync(statePath(scanRoot, 'last-scan.json'), 'utf8')); }
|
|
2359
|
+
catch { console.error('No .agentic-security/last-scan.json — run a scan first.'); return 2; }
|
|
2360
|
+
|
|
2361
|
+
const findings = scan.findings || [];
|
|
2362
|
+
const wanted = args.flags.provenance === true ? undefined : args.flags.provenance;
|
|
2363
|
+
// `--provenance` alone (boolean flag) attests every finding WITH
|
|
2364
|
+
// findingProvenance present; `--provenance <id>` scopes to one.
|
|
2365
|
+
const subset = (wanted ? findings.filter((f) => f.id === wanted || f.stableId === wanted) : findings)
|
|
2366
|
+
.filter((f) => f.findingProvenance);
|
|
2367
|
+
if (!subset.length) {
|
|
2368
|
+
console.error(wanted ? `No finding matching "${wanted}" with findingProvenance.` : 'No findings with findingProvenance to attest.');
|
|
2369
|
+
return 2;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
const kp = ensureKeyPair();
|
|
2373
|
+
if (kp.created) console.error(`Generated a new signing key at ${kp.privateKey} (public: ${kp.publicKey}).`);
|
|
2374
|
+
|
|
2375
|
+
const outDir = statePath(scanRoot, 'attestations');
|
|
2376
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
2377
|
+
// repoIdentity: best-effort, from the same `git remote` lookup other
|
|
2378
|
+
// provenance modules avoid (no such lookup exists yet) — keep it simple,
|
|
2379
|
+
// pass null when unavailable rather than inventing a git-remote reader
|
|
2380
|
+
// here. A future task can enrich this; the field degrades honestly.
|
|
2381
|
+
const meta = { engineVersion: scan.engineVersion || null, repoIdentity: null, head: scan.commit || null };
|
|
2382
|
+
|
|
2383
|
+
let n = 0;
|
|
2384
|
+
for (const f of subset) {
|
|
2385
|
+
const bundle = signProvenanceEvidenceBundle(buildProvenanceEvidenceBundle(f, meta), kp.privateKeyPem);
|
|
2386
|
+
const name = `provenance-${(f.stableId || f.id || `finding-${n}`)}.json`.replace(/[^\w.-]/g, '_');
|
|
2387
|
+
fs.writeFileSync(path.join(outDir, name), JSON.stringify(bundle, null, 2) + '\n');
|
|
2388
|
+
n++;
|
|
2389
|
+
}
|
|
2390
|
+
console.log(`Signed ${n} provenance evidence bundle(s) → ${path.relative(scanRoot, outDir)}/`);
|
|
2391
|
+
console.log(`Public key (share this with whoever verifies): ${kp.publicKey}`);
|
|
2392
|
+
console.log('');
|
|
2393
|
+
console.log('A bundle proves its contents are unmodified since signing. It does NOT');
|
|
2394
|
+
console.log('prove the origin commit is correctly identified — read confidence.level.');
|
|
2395
|
+
return 0;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2165
2398
|
const {
|
|
2166
2399
|
ensureKeyPair, buildEvidenceBundle, signEvidenceBundle,
|
|
2167
2400
|
} = await import('../src/posture/evidence-bundle.js');
|
|
@@ -2302,6 +2535,37 @@ async function cmdVerifyAttestation(args) {
|
|
|
2302
2535
|
return 0;
|
|
2303
2536
|
}
|
|
2304
2537
|
|
|
2538
|
+
// Finding Provenance PRD M4 §4.1: a provenance evidence bundle
|
|
2539
|
+
// (schema: agentic-security/provenance-evidence@1) is a fourth distinct
|
|
2540
|
+
// shape this same command can be handed — same auto-detection chain as
|
|
2541
|
+
// the ComplianceEvidence branch above, dispatched by schema marker
|
|
2542
|
+
// rather than a new CLI verb. Must be checked BEFORE the fallback
|
|
2543
|
+
// verifyEvidenceBundle() call below, which assumes evidence-bundle.js's
|
|
2544
|
+
// own shape (`.evidence`, `.finding` with severity/vuln/file/line) and
|
|
2545
|
+
// would misinterpret a provenance bundle.
|
|
2546
|
+
const { verifyProvenanceEvidenceBundle, PROVENANCE_BUNDLE_SCHEMA } = await import('../src/posture/provenance-evidence-bundle.js');
|
|
2547
|
+
if (bundle.schema === PROVENANCE_BUNDLE_SCHEMA) {
|
|
2548
|
+
const pr = verifyProvenanceEvidenceBundle(bundle, publicKeyPem);
|
|
2549
|
+
if (!pr.ok) { console.error(`✗ INVALID — ${pr.reason}`); return 1; }
|
|
2550
|
+
const p = bundle.provenance || {};
|
|
2551
|
+
console.log('✓ VALID — the provenance record is exactly what the signer attested.');
|
|
2552
|
+
console.log('');
|
|
2553
|
+
console.log(` finding: ${bundle.finding?.stableId || bundle.finding?.id || '?'}`);
|
|
2554
|
+
console.log(` status: ${p.status || 'n/a'} method: ${p.method || 'n/a'}`);
|
|
2555
|
+
// FR-PROV-026: findingOrigin.authorName is untrusted git commit metadata
|
|
2556
|
+
// (this bundle's signature only proves the BUNDLE wasn't tampered with —
|
|
2557
|
+
// it says nothing about what the original commit author put in their
|
|
2558
|
+
// name — see the "Signed, portable evidence" section of the root
|
|
2559
|
+
// CLAUDE.md), printed straight to the terminal by `verify-attestation`.
|
|
2560
|
+
if (p.findingOrigin) console.log(` origin: ${p.findingOrigin.commit || '?'} by ${sanitizeForTerminal(p.findingOrigin.authorName) || '?'} on ${p.findingOrigin.authorDate || '?'}`);
|
|
2561
|
+
console.log(` confidence: ${p.confidence?.level || 'n/a'} (${p.confidence?.score ?? 'n/a'})`);
|
|
2562
|
+
if ((p.limitations || []).length) console.log(` limitations: ${p.limitations.join('; ')}`);
|
|
2563
|
+
console.log('');
|
|
2564
|
+
console.log(` proves: ${bundle.proves}`);
|
|
2565
|
+
console.log(` does NOT prove: ${bundle.doesNotProve}`);
|
|
2566
|
+
return 0;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2305
2569
|
const r = verifyEvidenceBundle(bundle, publicKeyPem);
|
|
2306
2570
|
if (!r.ok) {
|
|
2307
2571
|
console.error(`✗ INVALID — ${r.reason}`);
|
|
@@ -2947,4 +3211,31 @@ async function main() {
|
|
|
2947
3211
|
}
|
|
2948
3212
|
}
|
|
2949
3213
|
|
|
2950
|
-
|
|
3214
|
+
// Guard against running as the CLI entry point vs. being `import`ed (e.g. by
|
|
3215
|
+
// scanner/test/cli/provenance-flags.test.js, which imports parseProvenanceFlags
|
|
3216
|
+
// for a unit test). Without this, any import of this module — for a single
|
|
3217
|
+
// named export — re-runs the entire CLI dispatch and calls process.exit(),
|
|
3218
|
+
// killing whatever process did the importing.
|
|
3219
|
+
//
|
|
3220
|
+
// `import.meta.url === file://${process.argv[1]}` looks equivalent but is
|
|
3221
|
+
// NOT: when this script is invoked through a symlink (exactly what
|
|
3222
|
+
// `npm install -g`, `npx`, and `node_modules/.bin/<name>` all do for a
|
|
3223
|
+
// package's `bin` entries — which is the documented `npx
|
|
3224
|
+
// @clear-capabilities/agentic-security-scanner` install path), Node
|
|
3225
|
+
// resolves `import.meta.url` to the symlink's realpath while
|
|
3226
|
+
// `process.argv[1]` stays the symlink path as invoked, so the two never
|
|
3227
|
+
// match, the guard is always false, and the CLI silently exits with no
|
|
3228
|
+
// output. `import.meta.main` is resolved correctly through a symlink —
|
|
3229
|
+
// verified live through an actual symlink, not just read about — see the
|
|
3230
|
+
// Task 17 fix report. It was added in Node v24.2.0 (backported to
|
|
3231
|
+
// v22.18.0) and is currently Stability 1.0 (early development) per Node's
|
|
3232
|
+
// own docs — NOT stable, and NOT available on v20.11 as an earlier
|
|
3233
|
+
// version of this comment incorrectly claimed. Concretely: it is
|
|
3234
|
+
// `undefined` on Node 24.0.0/24.1.x, which satisfy this repo's declared
|
|
3235
|
+
// `engines.node: ">=24.0.0"` floor, so `import.meta.main` alone would
|
|
3236
|
+
// reproduce this exact bug (main() silently never runs) on a plain
|
|
3237
|
+
// non-symlinked invocation under those two point releases. The `??`
|
|
3238
|
+
// fallback below covers that gap without bumping the engines floor.
|
|
3239
|
+
if (import.meta.main ?? (import.meta.url === `file://${process.argv[1]}`)) {
|
|
3240
|
+
main();
|
|
3241
|
+
}
|
package/dist/113.index.js
CHANGED
|
@@ -492,8 +492,8 @@ var external_node_child_process_ = __webpack_require__(1421);
|
|
|
492
492
|
var external_node_fs_ = __webpack_require__(3024);
|
|
493
493
|
// EXTERNAL MODULE: external "node:path"
|
|
494
494
|
var external_node_path_ = __webpack_require__(6760);
|
|
495
|
-
// EXTERNAL MODULE: ./src/engine.js +
|
|
496
|
-
var engine = __webpack_require__(
|
|
495
|
+
// EXTERNAL MODULE: ./src/engine.js + 229 modules
|
|
496
|
+
var engine = __webpack_require__(7691);
|
|
497
497
|
;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
|
|
498
498
|
// Deterministic honesty gates on fix / finding output (#7).
|
|
499
499
|
//
|
|
@@ -904,7 +904,15 @@ async function verifyPatch({
|
|
|
904
904
|
const fileContents = { ...files };
|
|
905
905
|
let scan;
|
|
906
906
|
try {
|
|
907
|
-
|
|
907
|
+
// `provenance:false` is REQUIRED here, not an optimisation. This scan is
|
|
908
|
+
// deliberately scoped to just the patched file(s), so its finding set is a
|
|
909
|
+
// tiny subset of the project's. updateLifecycle marks every open stableId
|
|
910
|
+
// NOT in the set it is handed as `remediated` — so a single fix
|
|
911
|
+
// verification (every /fix, apply_fix, and autopilot iteration runs one)
|
|
912
|
+
// would mass-mark the rest of the project as remediated, then
|
|
913
|
+
// `reintroduced` on the next real scan. The patched content is also not
|
|
914
|
+
// committed, so there is no history to resolve provenance against anyway.
|
|
915
|
+
scan = await (0,engine/* runFullScan */.wW)({ fileContents, depFileContents, scanRoot, provenance: false }, () => {});
|
|
908
916
|
} catch (e) {
|
|
909
917
|
return { ok: false, reason: 'rescan-failed', error: e.message };
|
|
910
918
|
}
|