@clear-capabilities/agentic-security-scanner 0.130.0 → 0.133.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 +247 -0
- package/bin/agentic-security.js +39 -3
- package/dist/113.index.js +294 -5
- package/dist/178.index.js +1 -1
- package/dist/207.index.js +7 -4
- package/dist/238.index.js +218 -0
- package/dist/259.index.js +975 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +2 -2
- package/dist/526.index.js +294 -5
- package/dist/637.index.js +1 -1
- package/dist/agentic-security.mjs +18 -57
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +19 -10
- package/src/engine.js +48 -1
- package/src/ir/parser-js.js +8 -0
- package/src/llm-validator/cost-ceiling.js +199 -0
- package/src/llm-validator/index.js +241 -12
- package/src/llm-validator/local-endpoint.js +90 -0
- package/src/mcp/tools.js +2 -2
- package/src/posture/CLAUDE.md +83 -6
- package/src/posture/accuracy-scorecard.js +37 -6
- package/src/posture/attestation.js +7 -4
- package/src/posture/corpus-enroll.js +303 -0
- package/src/posture/corpus-match.js +67 -0
- package/src/posture/custom-rules.js +2 -2
- package/src/posture/execution-proof.js +44 -4
- package/src/posture/fix-metrics.js +197 -0
- package/src/posture/fix-verify.js +76 -2
- package/src/posture/integrity.js +42 -9
- package/src/posture/learning.js +8 -1
- package/src/posture/model-routing.js +26 -0
- package/src/posture/model-trust.js +174 -0
- package/src/posture/poc-inprocess.js +165 -0
- package/src/posture/prove-findings.js +148 -0
- package/src/posture/root-cause-sweep.js +0 -0
- package/src/posture/rule-overrides.js +64 -3
- package/src/posture/state-dir.js +25 -0
- package/src/posture/vuln-archaeology.js +231 -0
- package/src/report/index.js +7 -0
- package/src/runScan.js +2 -6
- package/src/sandbox/CLAUDE.md +190 -46
- package/src/sandbox/backend-namespace.js +328 -48
- package/src/sandbox/backend-userspace.js +6 -19
- package/src/sandbox/capabilities.js +132 -4
- package/src/sandbox/limits.js +21 -0
- package/src/sandbox/result.js +1 -1
- package/src/sast/CLAUDE.md +4 -0
- package/src/sast/crypto-specialist.js +247 -0
- package/src/util/glob.js +173 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,252 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.133.0 — two ways findings could be silently deleted, both closed
|
|
4
|
+
|
|
5
|
+
Four rounds of adversarial premortem against this repository's own artifacts.
|
|
6
|
+
Two rounds found working exploits in shipped code; both are fixed and both are
|
|
7
|
+
pinned by tests that run the original attack. The rest is measurement honesty —
|
|
8
|
+
several gates turned out to prove less than they claimed, including one defect
|
|
9
|
+
this effort introduced and then caught.
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
|
|
13
|
+
- **A signature anyone could forge could disable any detector.**
|
|
14
|
+
`verifyLastScan` accepted a second key derived as
|
|
15
|
+
`sha256(<constant salt> + ':' + hostname)`. The salt is a constant in
|
|
16
|
+
published, npm-shipped source and a hostname is not a secret. Because the
|
|
17
|
+
`disable:` list in `rules.yml` is gated on that verification, a signature
|
|
18
|
+
forged from public information alone switched off arbitrary detectors and the
|
|
19
|
+
scan reported clean. Demonstrated end to end: a command-injection finding went
|
|
20
|
+
from 1 reported to 0, and back to 1 after the fix.
|
|
21
|
+
|
|
22
|
+
This had been known and fixed once already. The 0.62.0 entry below introduced
|
|
23
|
+
the per-install key precisely because the old one was "hostname-derived and
|
|
24
|
+
publicly forgeable in CI / containers", and kept legacy verification "for one
|
|
25
|
+
release to migrate existing signed scans". It was still accepted **seventy
|
|
26
|
+
minor releases later**. A migration window nobody closes is not a migration
|
|
27
|
+
window; it is the vulnerability, kept. Verification now accepts exactly one
|
|
28
|
+
key. Signatures made under the legacy key stop verifying — intended, and it
|
|
29
|
+
fails closed.
|
|
30
|
+
|
|
31
|
+
- **The LLM validator cache was a finding-deletion primitive.** A cache hit
|
|
32
|
+
assigned a verdict directly, and a `reject` verdict drops a finding. The cache
|
|
33
|
+
was read with a bare `JSON.parse(readFileSync(...))`, so planting one file
|
|
34
|
+
under `.agentic-security/llm-cache/` deleted a critical finding with no model
|
|
35
|
+
call and no network. The key is derivable by anyone with repo access, and CI
|
|
36
|
+
restoring a cache directory between runs delivers it without a repo write at
|
|
37
|
+
all. Cache entries are now HMAC-signed with the same mechanism `last-scan.json`
|
|
38
|
+
already used; an unsigned, tampered or foreign-keyed entry is a MISS, never a
|
|
39
|
+
verdict.
|
|
40
|
+
|
|
41
|
+
- **A `reject` can no longer delete a strongly-provenanced finding.** The code
|
|
42
|
+
asserted that prompt-injecting the validator was harmless because "the worst
|
|
43
|
+
an attacker can produce is escalate". That was false: the challenge/nonce
|
|
44
|
+
cross-check defends against forged and replayed responses, not against a model
|
|
45
|
+
persuaded by source it legitimately read. Findings from real analysis
|
|
46
|
+
(taint-proven, multi-sink, execution-proven) are now demoted to `escalate`
|
|
47
|
+
rather than dropped, so the guarantee is structural instead of asserted.
|
|
48
|
+
|
|
49
|
+
- **Coverage reduction is now visible in the artifact.** A `disable:` that took
|
|
50
|
+
effect produced findings that were simply absent — indistinguishable from
|
|
51
|
+
clean code to whoever reads the report. `suppressedRules` now carries the
|
|
52
|
+
count, per-rule severity breakdown, example locations and the AUTHORITY the
|
|
53
|
+
suppression ran under, so a signed suppression reads differently from an
|
|
54
|
+
env-var opt-out. Authorised suppressions are reported too: a signature proves
|
|
55
|
+
who asked, not that the hidden findings stopped existing.
|
|
56
|
+
|
|
57
|
+
- **Signatures carry key provenance** (`env` / `per-install` / `ephemeral`).
|
|
58
|
+
`env` means whoever set the environment could have signed the run; `ephemeral`
|
|
59
|
+
means the key could not be persisted and the signature will never verify
|
|
60
|
+
again. Neither was inferable from the digest.
|
|
61
|
+
|
|
62
|
+
- **The suppression quorum has a floor of 2.** `AGENTIC_SECURITY_LEARN_QUORUM=1`
|
|
63
|
+
was honoured, so a single triage verdict could suppress a finding — and with
|
|
64
|
+
family+path matching, a whole family across a path. The root guidance warned
|
|
65
|
+
about exactly this; the code did not enforce it.
|
|
66
|
+
|
|
67
|
+
### Measurement honesty
|
|
68
|
+
|
|
69
|
+
- **The corpus is fitted to the detectors it measures, and now says so.** 98% of
|
|
70
|
+
entries are self-authored synthetic fixtures and none come from the
|
|
71
|
+
disclosed-PoC tier. `npm run corpus:provenance` prints the composition on every
|
|
72
|
+
run and fails a commit that lands a detector together with the corpus entries
|
|
73
|
+
exercising it — caught against real history, including one such commit in this
|
|
74
|
+
very effort. This stops the loop tightening; it does not make the corpus
|
|
75
|
+
independent, and the docs no longer imply otherwise.
|
|
76
|
+
|
|
77
|
+
- **The precision gate covered 6% of the source.** It ran over `hooks/` and
|
|
78
|
+
`scripts/` — 22 files — while `scanner/src` (383 files, the entire product) sat
|
|
79
|
+
outside it. Now 240 files. The `scanner/src` count is published as a DRIFT
|
|
80
|
+
TRIPWIRE, explicitly not hand-reviewed and explicitly not a precision figure,
|
|
81
|
+
because a scanner's own source contains sink patterns as data.
|
|
82
|
+
|
|
83
|
+
- **The determinism gate exercised only the layer that cannot vary.** The
|
|
84
|
+
original fixture produced findings from regex and structural detectors alone. A
|
|
85
|
+
second fixture now drives the interprocedural taint engine and the Python
|
|
86
|
+
parser, digests are compared per fixture so a divergence names the layer, and
|
|
87
|
+
the comparator fails if the deep fixture degraded to the syntactic layer on any
|
|
88
|
+
machine.
|
|
89
|
+
|
|
90
|
+
- **The scorecard gate could not detect a stale scorecard.** It compared only the
|
|
91
|
+
engine version, so a document measured over 200 corpus entries passed while the
|
|
92
|
+
corpus held 210 — every published rate computed over a population that no
|
|
93
|
+
longer existed. It now compares the population too.
|
|
94
|
+
|
|
95
|
+
- **The independent-evaluation gate passed on a 4-sample smoke fixture** its own
|
|
96
|
+
README says must never be cited, because its thresholds had been calibrated to
|
|
97
|
+
pass. It now uses the README's own figures, treats exceeded calibration targets
|
|
98
|
+
as violations rather than notes, and refuses to emit a pass over the built-in
|
|
99
|
+
fixture however the thresholds are set. It fails today, correctly.
|
|
100
|
+
|
|
101
|
+
- **Claims re-scoped to what is measured.** The roadmap carries a "What the gates
|
|
102
|
+
do not prove" section, states outright that the false-positive-rate goal is not
|
|
103
|
+
met, and points at the two harnesses built for that gap — both of which need
|
|
104
|
+
data, not code. R13 is downgraded from "landed" to "mechanism landed, no
|
|
105
|
+
observation pipeline": nothing constructs its ledger, so it can never downgrade
|
|
106
|
+
anything.
|
|
107
|
+
|
|
108
|
+
### Fixed along the way
|
|
109
|
+
|
|
110
|
+
- **The validator cache had never persisted a single entry.** `safeWriteState`
|
|
111
|
+
refused every directory nested under `.agentic-security/`, so `llm-cache/`,
|
|
112
|
+
`fix-history/` and `sbom-history/` were all unwritable while a
|
|
113
|
+
`validator-cache stats|gc` subcommand managed a cache that was always empty.
|
|
114
|
+
Found by a positive-control test asserting a legitimately written entry
|
|
115
|
+
round-trips.
|
|
116
|
+
|
|
117
|
+
- **Sandbox timeouts now use SIGKILL.** The kernel does not deliver
|
|
118
|
+
default-action signals to a PID namespace's pid 1 from outside it, so SIGTERM
|
|
119
|
+
was dropped and a payload ran to completion against a 1200 ms budget — measured
|
|
120
|
+
in CI at 30057 ms. Proof execution also gained its own aggregate wall-clock
|
|
121
|
+
budget, because a count cap bounds nothing in time.
|
|
122
|
+
|
|
123
|
+
- **Cost reporting stopped presenting an estimate as spend.** The endpoint's
|
|
124
|
+
usage report was discarded, so the ledger always booked the pre-call worst
|
|
125
|
+
case. Usage is now plumbed through, and any estimated component renders as
|
|
126
|
+
"at most $X" with the reason.
|
|
127
|
+
|
|
128
|
+
## 0.132.0 — a proven exploit becomes a permanent regression test
|
|
129
|
+
|
|
130
|
+
The corpus stops being only a regression net and starts being fed by the engine
|
|
131
|
+
itself. Every claim below was verified by a command in the session that made it.
|
|
132
|
+
|
|
133
|
+
- **Execution-proven findings auto-enrol as corpus entries.** This was R2's
|
|
134
|
+
differentiator and the last missing piece of it. A finding whose
|
|
135
|
+
proof-of-concept RAN in the sandbox and produced the predicted effect can now
|
|
136
|
+
be turned into a permanent `pre:TP post:TN` corpus entry, so every exploit
|
|
137
|
+
proved once is defended against its own regression forever. Demonstrated end
|
|
138
|
+
to end rather than in a unit test: a real command-injection finding was proved
|
|
139
|
+
in the sandbox, its fix supplied the `post/` tree, and the entry was enrolled
|
|
140
|
+
and scored by the real gate — **the baseline moved 199 → 200**. Proven in the
|
|
141
|
+
other direction too: with the entry's `pre/` neutered the gate reports
|
|
142
|
+
`REGRESSED (1)` and exits non-zero.
|
|
143
|
+
- **Nothing reaches the corpus unscored.** The entry is built in a temporary
|
|
144
|
+
directory, `pre/` and `post/` are scanned, and it is moved into the corpus
|
|
145
|
+
only on `pre:TP post:TN`. There is no force flag, and the scoring function is
|
|
146
|
+
unexported so no caller can score by one route and write by another — the
|
|
147
|
+
v0.106.0 mistake (fixtures committed without verifying they score) is the one
|
|
148
|
+
thing an automated writer must never industrialise. Scoring itself now lives
|
|
149
|
+
in one module shared by the enroller and the corpus runner, so the two cannot
|
|
150
|
+
drift apart; the refactor was proven behaviour-preserving at 199/199, no drift.
|
|
151
|
+
New entries land in `capability/`, never the CI-gated `regression/` tier — a
|
|
152
|
+
machine must not decide what blocks everyone's build.
|
|
153
|
+
- **`verify_fix` now runs the proof-of-concept against the candidate patch.** A
|
|
154
|
+
re-scan only proves the DETECTOR stopped firing, which a cosmetic edit
|
|
155
|
+
achieves; re-running the exploit proves the hole is shut. Still-exploitable
|
|
156
|
+
after the patch is a hard failure. Deliberately asymmetric: a PoC that could
|
|
157
|
+
not run is recorded `inconclusive` and excluded from the verdict, because
|
|
158
|
+
reading "could not prove it" as "fixed" is exactly the false confidence this
|
|
159
|
+
leg exists to prevent. Both directions were executed against the real sandbox.
|
|
160
|
+
- **Time-to-validated-fix is now measured and reported**, closing R5. Every
|
|
161
|
+
verification stage is timed and appended to a per-project log, and the
|
|
162
|
+
reported distribution is deliberately hard to flatter: failed attempts never
|
|
163
|
+
enter the validated median (they short-circuit, so blending them makes a worse
|
|
164
|
+
pipeline look faster), "no test suite to run" is bucketed apart from "tests
|
|
165
|
+
passed", and per-stage timings come from validated runs only. Percentiles are
|
|
166
|
+
nearest-rank — every figure shown is a duration some run actually took — and
|
|
167
|
+
are flagged unreliable below n=10 rather than quoted as settled.
|
|
168
|
+
- **Cross-machine determinism now has a gate behind it.** A dependency-free
|
|
169
|
+
fixture is scanned on two operating systems and the run-attestation digests
|
|
170
|
+
must match. The comparator refuses every route to a meaningless pass: fewer
|
|
171
|
+
than two attestations, two runs from the same platform, a zero-finding digest,
|
|
172
|
+
mismatched canonicalisations, unparseable input. Each refusal was fired
|
|
173
|
+
deliberately and confirmed to exit non-zero.
|
|
174
|
+
|
|
175
|
+
Honest limits, stated rather than implied:
|
|
176
|
+
|
|
177
|
+
- The enrolment loop is **not yet automatic end to end**. Nothing in the scan
|
|
178
|
+
pipeline attaches a proof-of-concept to a finding or promotes proof tiers, so
|
|
179
|
+
a scan never produces an `execution-proven` finding on its own; PoCs come from
|
|
180
|
+
the generator and are proved at enrol time. Automatic attachment during a scan
|
|
181
|
+
is the remaining work.
|
|
182
|
+
- The cross-machine determinism jobs have **not yet run in CI**, so no second
|
|
183
|
+
machine has actually been compared. Same-machine repeatability is verified.
|
|
184
|
+
The attestation's own "does not prove" statement is unchanged and stays
|
|
185
|
+
correct either way: one attestation is one run on one machine.
|
|
186
|
+
- The sandbox wall-clock timeout stops the **direct child, not the process
|
|
187
|
+
tree**, on both backends. No test asserts a backgrounded grandchild dies.
|
|
188
|
+
|
|
189
|
+
Also in this release:
|
|
190
|
+
|
|
191
|
+
- **Documentation corrected against evidence.** The roadmap and the sandbox and
|
|
192
|
+
posture guides still described the kernel-namespace backend as "implemented,
|
|
193
|
+
unverified" after CI had already proved otherwise; they now cite the run
|
|
194
|
+
(Ubuntu 24.04, kernel 6.17.0-1020-azure, 41 assertions, 0 failures, all eight
|
|
195
|
+
escape cases). The corpus entry count was three tiers and 14 entries out of
|
|
196
|
+
date.
|
|
197
|
+
- **Dependency advisories cleared** in the extension tree and `@types/node`
|
|
198
|
+
brought current in both trees.
|
|
199
|
+
|
|
200
|
+
## 0.131.0 — decorated files stop vanishing; the Linux sandbox is verified
|
|
201
|
+
|
|
202
|
+
Two correctness fixes, both found by pushing measurement further than the last
|
|
203
|
+
release did.
|
|
204
|
+
|
|
205
|
+
- **Decorator syntax is now accepted by the IR frontend.** The parser was told
|
|
206
|
+
about TypeScript and JSX but not decorators, so it rejected the ENTIRE file on
|
|
207
|
+
the first `@tracked` it met — no error, no warning, just findings that never
|
|
208
|
+
existed. Measured on a live third-party target: **4,023 of 4,271 JavaScript
|
|
209
|
+
files parsed before, 4,262 after** — 239 files, roughly 6% of that project,
|
|
210
|
+
were invisible to the scanner and are now analysed. `decorators-legacy` plus
|
|
211
|
+
`decoratorAutoAccessors` was chosen over the modern `decorators` variant
|
|
212
|
+
because the modern one cannot parse TypeScript parameter decorators; it would
|
|
213
|
+
have swapped one blind spot for another. No new dependency. Guarded by a
|
|
214
|
+
regression test proven in both directions — 5/5 pass with the fix, 4 fail
|
|
215
|
+
without it.
|
|
216
|
+
- **The kernel-namespace sandbox now confines writes, and is verified.** It
|
|
217
|
+
previously confined network only and had never been executed. It now enters a
|
|
218
|
+
private mount namespace, rebinds every mount read-only except the sandbox
|
|
219
|
+
root, and drops the entire capability set before exec so a payload cannot
|
|
220
|
+
rebind the tree writable. A CI job relaxes the host restriction on
|
|
221
|
+
unprivileged user namespaces and runs the real escape suite: a write outside
|
|
222
|
+
the root is blocked with no file created, and outbound egress is blocked. The
|
|
223
|
+
verifier exits non-zero unless that suite actually RAN, so a skip can never be
|
|
224
|
+
read as a pass. Execution-proof on Linux no longer rests on an unexercised
|
|
225
|
+
backend.
|
|
226
|
+
|
|
227
|
+
Also in this release:
|
|
228
|
+
|
|
229
|
+
- **A quadratic blowup in the root-cause sweep**, which ran on every full scan:
|
|
230
|
+
it re-split the whole corpus into lines once per finding and kept a record per
|
|
231
|
+
finding x matching line. The largest benchmark corpus went from dying after
|
|
232
|
+
55+ minutes to **exit 0, 933 MB peak, 330s** — smaller and roughly ten times
|
|
233
|
+
faster, with metrics identical before and after.
|
|
234
|
+
- **Benchmark corpora are fetched by pinned commit** instead of guessing a clone
|
|
235
|
+
depth; six corpora had never been scoreable because their pin sat more than
|
|
236
|
+
100 commits back.
|
|
237
|
+
- **A pre-push gate** runs bundle integrity, the full suite, the corpus baseline
|
|
238
|
+
and the precision baseline before anything leaves the machine, plus branch
|
|
239
|
+
protection requiring green checks to merge.
|
|
240
|
+
- **A dependency-currency release gate**: any advisory at moderate or above
|
|
241
|
+
fails with no opt-out; anything behind latest fails unless explicitly held
|
|
242
|
+
with a stated reason and a review date that expires.
|
|
243
|
+
- **The glob dependency was replaced by the platform built-in** — 92 to 73
|
|
244
|
+
production packages — after a differential over 320 trees and 1,419,229 paths
|
|
245
|
+
showed zero differences.
|
|
246
|
+
|
|
247
|
+
`npm test` 2072/0; cve-replay 199/199; self-scan no drift; proof corpus
|
|
248
|
+
ghost/superset/godot all 100% parse coverage.
|
|
249
|
+
|
|
3
250
|
## 0.130.0 — the roadmap's first ten: provable security over orchestration parity
|
|
4
251
|
|
|
5
252
|
A capability roadmap (`docs/ROADMAP.md`) plus its first ten items, derived from a
|
package/bin/agentic-security.js
CHANGED
|
@@ -17,7 +17,7 @@ import { recordScan, formatStreakLine, formatGradeDelta } from '../src/posture/s
|
|
|
17
17
|
import { ingestAndMerge } from '../src/sca/sarif-ingest.js';
|
|
18
18
|
import { loadProfile, saveProfile, detectProfile, renderAttributionLine, ATTRIBUTION, ATTRIBUTION_URL } from '../src/posture/profile.js';
|
|
19
19
|
import { applySuppressions, addSoftAcceptance, expiredSoftAcceptances } from '../src/posture/suppressions.js';
|
|
20
|
-
import { applyOverrides, validateOverrides } from '../src/posture/rule-overrides.js';
|
|
20
|
+
import { applyOverrides, validateOverrides, suppressionReport, renderSuppressionSummary } from '../src/posture/rule-overrides.js';
|
|
21
21
|
import { listPacks, loadPack, applyPacks } from '../src/posture/rule-packs.js';
|
|
22
22
|
import { writeLockfile, verifyLockfile, makeDeterministic, isDeterministic } from '../src/posture/deterministic.js';
|
|
23
23
|
import { enrichWithEPSS } from '../src/posture/epss.js';
|
|
@@ -28,7 +28,7 @@ import { syncTickets } from '../src/integrations/tickets.js';
|
|
|
28
28
|
import { decide as decideNextAction, explain as explainDecision } from '../src/posture/router.js';
|
|
29
29
|
import * as triage from '../src/posture/triage.js';
|
|
30
30
|
import { buildSlackDigest, buildDiscordDigest, postWebhook, buildJiraIssue, buildPrComment, buildSiemEvent, loadIntegrationConfig } from '../src/integrations/index.js';
|
|
31
|
-
import
|
|
31
|
+
import { globFiles } from '../src/util/glob.js';
|
|
32
32
|
|
|
33
33
|
// last-scan.json integrity helpers — implementation in posture/integrity.js
|
|
34
34
|
// so the MCP server tools can share verification.
|
|
@@ -483,7 +483,7 @@ async function cmdScan(args) {
|
|
|
483
483
|
// fingerprint and tracking provenance via sources[].
|
|
484
484
|
if (args.flags['ingest-sarif']) {
|
|
485
485
|
const glob = args.flags['ingest-sarif'];
|
|
486
|
-
const paths = await
|
|
486
|
+
const paths = await globFiles(glob);
|
|
487
487
|
if (paths.length) {
|
|
488
488
|
const r = ingestAndMerge(scan, paths);
|
|
489
489
|
if (process.stderr.isTTY) process.stderr.write(`[ingest] merged ${r.merged} / added ${r.added} findings from ${paths.length} SARIF file(s)\n`);
|
|
@@ -512,6 +512,19 @@ async function cmdScan(args) {
|
|
|
512
512
|
scan.findings = applyOverrides(scan.findings || [], targetAbs);
|
|
513
513
|
scan.secrets = applyOverrides(scan.secrets || [], targetAbs);
|
|
514
514
|
scan.logicVulns = applyOverrides(scan.logicVulns || [], targetAbs);
|
|
515
|
+
// Coverage reduction belongs in the ARTIFACT, not only in a log line. A
|
|
516
|
+
// `disable:` that takes effect otherwise produces findings that are simply
|
|
517
|
+
// absent, which is indistinguishable from clean code to whoever reads the
|
|
518
|
+
// report. Recorded whether the suppression was authorised or not — an
|
|
519
|
+
// authorised one still hides results.
|
|
520
|
+
try {
|
|
521
|
+
const _sup = suppressionReport(targetAbs);
|
|
522
|
+
if (_sup) {
|
|
523
|
+
scan.suppressedRules = _sup;
|
|
524
|
+
const _line = renderSuppressionSummary(_sup);
|
|
525
|
+
if (_line) process.stderr.write(`⚠️ agentic-security: ${_line}\n`);
|
|
526
|
+
}
|
|
527
|
+
} catch { /* reporting must never fail a scan */ }
|
|
515
528
|
|
|
516
529
|
// Curated rule packs: --pack <name> (repeatable). Narrows findings to the
|
|
517
530
|
// CWEs covered by the requested pack(s).
|
|
@@ -575,6 +588,7 @@ async function cmdScan(args) {
|
|
|
575
588
|
// only — a failure here must never fail a scan.
|
|
576
589
|
try {
|
|
577
590
|
const { computeRunAttestation } = await import('../src/posture/attestation.js');
|
|
591
|
+
const { keyProvenance } = await import('../src/posture/integrity.js');
|
|
578
592
|
const { effectiveVersion } = await import('../src/posture/ruleset-version.js');
|
|
579
593
|
scan.attestation = computeRunAttestation({
|
|
580
594
|
findings: normalizeFindings(scan),
|
|
@@ -584,6 +598,11 @@ async function cmdScan(args) {
|
|
|
584
598
|
root: targetAbs,
|
|
585
599
|
sign: true,
|
|
586
600
|
});
|
|
601
|
+
// P1-3 — a signature is only as meaningful as the key behind it.
|
|
602
|
+
// `env` means whoever set the environment could have signed this;
|
|
603
|
+
// `ephemeral` means the key could not be persisted, so this signature will
|
|
604
|
+
// never verify on any later run. Neither is inferable from the digest.
|
|
605
|
+
if (scan.attestation) scan.attestation.keyProvenance = keyProvenance();
|
|
587
606
|
} catch { /* attestation is metadata; never fail a scan over it */ }
|
|
588
607
|
|
|
589
608
|
// R2: Always emit machine-readable artifacts to .agentic-security/.
|
|
@@ -655,6 +674,23 @@ async function cmdScan(args) {
|
|
|
655
674
|
if (sla) process.stderr.write(`⏰ agentic-security: ${sla}\n`);
|
|
656
675
|
}
|
|
657
676
|
} catch { /* MTTR is best-effort — never block a scan write */ }
|
|
677
|
+
|
|
678
|
+
// R5 — report the observed time-to-validated-fix distribution from the
|
|
679
|
+
// fix attempts recorded by `verifyFix`. This is measurement, not
|
|
680
|
+
// estimation: it says nothing until fixes have actually been verified in
|
|
681
|
+
// this project, and it prints nothing when there is nothing measured.
|
|
682
|
+
// Skipped under --deterministic for the same reason MTTR is: the
|
|
683
|
+
// durations are wall-clock and would break byte-identical state.
|
|
684
|
+
try {
|
|
685
|
+
const { fixDurationReport, renderFixDurationSummary } = await import('../src/posture/fix-metrics.js');
|
|
686
|
+
const fixMetrics = fixDurationReport(path.resolve(target));
|
|
687
|
+
if (fixMetrics.attempts > 0) {
|
|
688
|
+
persistedScan.fixMetrics = fixMetrics;
|
|
689
|
+
const isJsonFmt = format === 'json' || format === 'sarif' || format === 'cyclonedx' || format === 'sbom' || format === 'spdx' || format === 'vex' || format === 'openvex' || format === 'pbom' || format === 'aibom';
|
|
690
|
+
const line = renderFixDurationSummary(fixMetrics);
|
|
691
|
+
if (!isJsonFmt && line) process.stderr.write(`🔧 agentic-security: ${line}\n`);
|
|
692
|
+
}
|
|
693
|
+
} catch { /* fix metrics are best-effort — never block a scan write */ }
|
|
658
694
|
}
|
|
659
695
|
// #22 — live-secret validation (opt-in, offline-degrading). Label each
|
|
660
696
|
// detected secret live | dead | unknown via a read-only provider "whoami".
|