@clear-capabilities/agentic-security-scanner 0.139.0 → 0.140.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 +113 -0
- package/bin/agentic-security.js +40 -11
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +12 -3
- package/src/engine.js +19 -1
- package/src/posture/auditor-walkthrough.js +91 -14
- package/src/posture/concurrency-checker.js +39 -2
- package/src/posture/integrity.js +59 -8
- package/src/posture/poc-generator.js +17 -1
- package/src/posture/sbom.js +38 -4
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,119 @@
|
|
|
9
9
|
> make the history less accurate, not more.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.140.0 — Five shipped bugs, found by measuring instead of reading
|
|
13
|
+
|
|
14
|
+
Every fix here is a defect that was live in 0.139.1. None came from the feature
|
|
15
|
+
backlog; all five came from measuring the engine against real code and taking
|
|
16
|
+
failing signals seriously instead of explaining them away.
|
|
17
|
+
|
|
18
|
+
### `scan --format sarif` produced INVALID SARIF in CI
|
|
19
|
+
|
|
20
|
+
The CLI dispatches every command as `process.exit(await cmdX(args))`, and
|
|
21
|
+
`process.exit()` does not flush an asynchronous stdout. stdout is asynchronous
|
|
22
|
+
exactly when it is a pipe — every `> results.sarif`, `| jq`, and CI capture — so
|
|
23
|
+
output was discarded at the 64 KiB pipe boundary, mid-token, with a normal exit
|
|
24
|
+
status. On one directory that was 65,536 bytes emitted of 390,177: roughly 83%
|
|
25
|
+
of the document silently dropped.
|
|
26
|
+
|
|
27
|
+
**If you upload SARIF to code scanning, this affected you on any project large
|
|
28
|
+
enough to matter.** A TTY and a file both flush synchronously, which is why it
|
|
29
|
+
looked fine by hand and broke in automation.
|
|
30
|
+
|
|
31
|
+
### LLM01 — Prompt Injection — could never fail
|
|
32
|
+
|
|
33
|
+
Detectors emit the finding family as `<family>-<rule-slug>`
|
|
34
|
+
(`prompt-injection-http-user-input-in-llm-`), while the compliance evaluator
|
|
35
|
+
resolved `family:prompt-injection` as an exact key. It matched nothing, so the
|
|
36
|
+
control reported as evidenced no matter what the scan found. The first control
|
|
37
|
+
of the OWASP LLM Top 10 was structurally incapable of failing, along with ASVS
|
|
38
|
+
V5.1 and NIST AI 600-1 MG-3.2-005.
|
|
39
|
+
|
|
40
|
+
Two further compliance defects in the same matching code: an empty family
|
|
41
|
+
bucket rendered as `✓ no open critical/high findings`, so two controls (ASVS
|
|
42
|
+
V7.1, NIST Privacy CT.DP-P1) read `present` on every scan of every project; and
|
|
43
|
+
a first attempt at fixing that wrongly declared four live families
|
|
44
|
+
unevidenceable, degrading 15 working controls. Both directions are now gated —
|
|
45
|
+
a control with no possible evidence cannot read `present`, and a family with a
|
|
46
|
+
producer cannot be declared a gap.
|
|
47
|
+
|
|
48
|
+
### `--deterministic` did not produce deterministic output
|
|
49
|
+
|
|
50
|
+
Four of ten emitted formats differed run to run: CycloneDX/SPDX document ids and
|
|
51
|
+
a CycloneDX bom-ref fallback from `crypto.randomUUID()`, a PoC marker from
|
|
52
|
+
`Math.random()`, and per-file wall-clock timings (which also determined the
|
|
53
|
+
sort ORDER, so blanking the values alone would not have been enough).
|
|
54
|
+
|
|
55
|
+
**An attestation over an SBOM was therefore unverifiable** — the point of
|
|
56
|
+
signing an artifact is that someone can regenerate and compare it.
|
|
57
|
+
|
|
58
|
+
### 62% of concurrency findings on Go were false positives
|
|
59
|
+
|
|
60
|
+
The lock guard matched a bare receiver (`defer mu.Unlock()`) but not a qualified
|
|
61
|
+
one (`defer s.mu.Unlock()`), which is how a mutex held as a struct field is
|
|
62
|
+
always written. The acquire pattern always matched the qualified form, so the
|
|
63
|
+
two halves of the rule had disagreed since it was written — and the most
|
|
64
|
+
idiomatic CORRECT code was the most likely to be reported. Measured: 170 of 273
|
|
65
|
+
findings on a Go sample were false positives. Those findings also carried no
|
|
66
|
+
CWE, so they were invisible to every CWE-keyed report.
|
|
67
|
+
|
|
68
|
+
### New gates
|
|
69
|
+
|
|
70
|
+
- `test/stdout-flush.test.js` — spawns the real CLI through a real pipe
|
|
71
|
+
- `test/format-determinism.test.js` — every emitted format, byte-compared
|
|
72
|
+
- `test/compliance-mapping-liveness.test.js` — both vacuous-pass directions
|
|
73
|
+
- `test/concurrency-cwe.test.js` — per-lock guard discrimination
|
|
74
|
+
- `bench/family-producers/OBSERVED.json` — 213 families observed across 331
|
|
75
|
+
real scan roots, recorded explicitly as a LOWER BOUND
|
|
76
|
+
|
|
77
|
+
### Scope
|
|
78
|
+
|
|
79
|
+
The world-class-harness PRD is **partially delivered**. F10.5 (determinism as a
|
|
80
|
+
published property) is complete. F10.2 is half done — the enforcement half
|
|
81
|
+
landed; the measurement half needs detectors to declare their families, because
|
|
82
|
+
this release proved no textual search can enumerate them. Roughly 35 PRD items
|
|
83
|
+
remain, several blocked on design decisions rather than implementation.
|
|
84
|
+
|
|
85
|
+
## 0.139.1 — The same scanner, published with provenance
|
|
86
|
+
|
|
87
|
+
**The shipped artifact is functionally identical to 0.139.0.** Only two commits
|
|
88
|
+
separate them, and neither is in the published package: a test-harness timeout
|
|
89
|
+
(`test/` does not ship) and a dependency hold (`.dependency-holds.json` does not
|
|
90
|
+
ship). Nothing about how the scanner behaves has changed.
|
|
91
|
+
|
|
92
|
+
This release exists for one reason: **0.139.0 was published without a provenance
|
|
93
|
+
attestation.** It went out from a maintainer's laptop because the CI token
|
|
94
|
+
lacked write access on the scoped package, so every tag-triggered publish failed
|
|
95
|
+
at `PUT` with `E404` — after having already signed provenance to the Sigstore
|
|
96
|
+
transparency log. The token has been replaced and verified (a re-run now returns
|
|
97
|
+
`E403 "cannot publish over the previously published versions"`, which is the
|
|
98
|
+
registry authenticating the write and rejecting only the duplicate version), so
|
|
99
|
+
this is the first release published by CI with `--provenance`.
|
|
100
|
+
|
|
101
|
+
For a security scanner that argues its case on *provable, measurable,
|
|
102
|
+
reproducible*, "you can cryptographically verify this artifact was built from
|
|
103
|
+
this commit in this repository" is a product property rather than a formality.
|
|
104
|
+
Provenance is signed at publish time and cannot be retrofitted, which is why it
|
|
105
|
+
takes a version number rather than an amendment to the last one.
|
|
106
|
+
|
|
107
|
+
Fixes carried along, neither user-visible:
|
|
108
|
+
|
|
109
|
+
- **`test/audit-cli.test.js` budgeted 4 s for a CLI that takes ~2.7 s idle** — a
|
|
110
|
+
1.5× margin in a suite that runs files concurrently. It failed as `status:
|
|
111
|
+
null` on a *different* test each run, was unreproducible in isolation, and
|
|
112
|
+
blocked a release push on a commit whose entire diff was 8 lines of JSON.
|
|
113
|
+
Now 30 s (~11× idle), still bounded so a genuinely hung CLI fails rather than
|
|
114
|
+
stalls.
|
|
115
|
+
- **`@types/vscode` is held rather than upgraded.** It declares the VS Code API
|
|
116
|
+
surface the extension compiles against and should track `engines.vscode` — the
|
|
117
|
+
*oldest* supported host — not the newest published types. Chasing latest lets
|
|
118
|
+
the extension compile against APIs absent from hosts it claims to support, and
|
|
119
|
+
that failure lands as a runtime `TypeError` in a user's editor. The hold
|
|
120
|
+
records a pre-existing mismatch (`engines ^1.95.0` vs types `^1.125.0`) for a
|
|
121
|
+
deliberate decision at review time, since narrowing the supported host range
|
|
122
|
+
is a product call, not a dependency bump.
|
|
123
|
+
|
|
124
|
+
|
|
12
125
|
## 0.139.0 — Two detectors that were dead, a confinement rule that was documented but unenforced, and a new Go rule
|
|
13
126
|
|
|
14
127
|
Every defect in this release is the same shape: **a control that exists, is
|
package/bin/agentic-security.js
CHANGED
|
@@ -9,6 +9,35 @@ const __require = createRequire(import.meta.url);
|
|
|
9
9
|
const PKG_VERSION = __require('../package.json').version;
|
|
10
10
|
import { signLastScan as _signLastScan, verifyLastScan as _verifyLastScanShared } from '../src/posture/integrity.js';
|
|
11
11
|
import { runScan } from '../src/runScan.js';
|
|
12
|
+
|
|
13
|
+
// Every command is dispatched as `process.exit(await cmdX(args))`, and
|
|
14
|
+
// process.exit() does NOT flush an asynchronous stdout. stdout is asynchronous
|
|
15
|
+
// whenever it is a PIPE — which is every `> file`, `| jq`, and CI capture — so
|
|
16
|
+
// anything still buffered when the process exits is discarded at the pipe
|
|
17
|
+
// boundary: 64 KiB on macOS and Linux.
|
|
18
|
+
//
|
|
19
|
+
// That silently truncated `scan --format sarif` mid-token for any project
|
|
20
|
+
// large enough to matter, i.e. the primary CI integration path, while still
|
|
21
|
+
// exiting with a normal status. The consumer sees a JSON parse error with no
|
|
22
|
+
// connection to its cause, or ingests a partial finding set.
|
|
23
|
+
//
|
|
24
|
+
// fs.writeSync(1, …) hands the bytes to the OS before returning, so a later
|
|
25
|
+
// exit cannot lose them. A non-blocking pipe can still short-write or raise
|
|
26
|
+
// EAGAIN, hence the loop — a partial write that is not retried is the same
|
|
27
|
+
// truncation bug wearing a different hat.
|
|
28
|
+
function writeStdout(s) {
|
|
29
|
+
const buf = Buffer.from(String(s), 'utf8');
|
|
30
|
+
let off = 0;
|
|
31
|
+
while (off < buf.length) {
|
|
32
|
+
try {
|
|
33
|
+
off += fs.writeSync(1, buf, off, buf.length - off);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
if (e.code === 'EAGAIN') continue; // pipe full; the reader will drain it
|
|
36
|
+
if (e.code === 'EPIPE') return; // reader closed (`| head`) — not our error
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
12
41
|
import { toJSON, toMarkdown, toSARIF, toSTIX, toCSV, toJUnit, toCLI, toCLIByProfile, toShipVerdict, toProTable, toHTML, toSummary, toVex, exitCodeFor, normalizeFindings } from '../src/report/index.js';
|
|
13
42
|
import { toCycloneDX, toSPDX } from '../src/posture/sbom.js';
|
|
14
43
|
import { toPBOM } from '../src/sast/pipeline.js';
|
|
@@ -679,7 +708,7 @@ async function cmdScan(args) {
|
|
|
679
708
|
}
|
|
680
709
|
|
|
681
710
|
if (output) await fsp.writeFile(output, body);
|
|
682
|
-
else
|
|
711
|
+
else writeStdout(body + '\n');
|
|
683
712
|
|
|
684
713
|
// Persist last scan for /security-fix and /security-report
|
|
685
714
|
const { isSafeStateDir: _isSafeStateDir, stateWritesEnabled: _writesOnScan } = await import('../src/posture/state-dir.js');
|
|
@@ -1251,7 +1280,7 @@ async function cmdHarness(args) {
|
|
|
1251
1280
|
body += `\n\nHarnesses discovered: ${present.join(', ')}${includeHome ? ' (project + ~/)' : ' (project only)'}\n`;
|
|
1252
1281
|
}
|
|
1253
1282
|
if (args.flags.output) await fsp.writeFile(args.flags.output, body);
|
|
1254
|
-
else
|
|
1283
|
+
else writeStdout(body + '\n');
|
|
1255
1284
|
return exitCodeFor(scan);
|
|
1256
1285
|
}
|
|
1257
1286
|
|
|
@@ -1746,7 +1775,7 @@ async function cmdCompliance(args) {
|
|
|
1746
1775
|
|
|
1747
1776
|
if (args.flags.list) {
|
|
1748
1777
|
const fws = listFrameworks(scanRoot);
|
|
1749
|
-
if (fmt === 'json') {
|
|
1778
|
+
if (fmt === 'json') { writeStdout(JSON.stringify(fws, null, 2) + '\n'); return 0; }
|
|
1750
1779
|
for (const f of fws) console.log(` ${f.id.padEnd(20)} ${f.name} [${f.source}]`);
|
|
1751
1780
|
return 0;
|
|
1752
1781
|
}
|
|
@@ -1780,7 +1809,7 @@ async function cmdCompliance(args) {
|
|
|
1780
1809
|
|
|
1781
1810
|
const gapsOnly = !!args.flags.gap;
|
|
1782
1811
|
if (fmt === 'json') {
|
|
1783
|
-
|
|
1812
|
+
writeStdout(JSON.stringify(gapsOnly ? { ...r, controls: r.controls.filter(c => c.bucket === 'gap') } : r, null, 2) + '\n');
|
|
1784
1813
|
} else if (fmt === 'md') {
|
|
1785
1814
|
console.log(fs.readFileSync(statePath(scanRoot, 'privacy-framework.md'), 'utf8'));
|
|
1786
1815
|
} else {
|
|
@@ -2018,7 +2047,7 @@ async function cmdFix(args) {
|
|
|
2018
2047
|
|
|
2019
2048
|
// Default mode: print the canonical template (back-compat — security-fixer subagent applies it).
|
|
2020
2049
|
if (!isPreview && !isApply) {
|
|
2021
|
-
|
|
2050
|
+
writeStdout(JSON.stringify(f, null, 2) + '\n');
|
|
2022
2051
|
if (f.fix?.code) { console.log('\n--- suggested patch ---\n'); console.log(f.fix.code); }
|
|
2023
2052
|
console.log('\nUse --preview to see a diff, or --apply to apply directly.');
|
|
2024
2053
|
return 0;
|
|
@@ -2247,7 +2276,7 @@ async function main() {
|
|
|
2247
2276
|
const { analyzeTranscript, formatCacheReport } = await import('../src/posture/cache-economics.js');
|
|
2248
2277
|
const projectDir = path.resolve(args.flags.root || process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
2249
2278
|
const result = analyzeTranscript({ transcriptPath: args.flags.transcript, projectDir });
|
|
2250
|
-
if (args.flags.json)
|
|
2279
|
+
if (args.flags.json) writeStdout(JSON.stringify(result, null, 2) + '\n');
|
|
2251
2280
|
else console.log(formatCacheReport(result));
|
|
2252
2281
|
process.exit(0);
|
|
2253
2282
|
}
|
|
@@ -2291,7 +2320,7 @@ async function main() {
|
|
|
2291
2320
|
});
|
|
2292
2321
|
if (args.flags.json) {
|
|
2293
2322
|
// Stringify Set/etc. safely.
|
|
2294
|
-
|
|
2323
|
+
writeStdout(JSON.stringify(r, null, 2) + '\n');
|
|
2295
2324
|
} else if (!r.ok) {
|
|
2296
2325
|
console.error(`cve-watch: ${r.reason || 'failed'}`);
|
|
2297
2326
|
}
|
|
@@ -2307,7 +2336,7 @@ async function main() {
|
|
|
2307
2336
|
const headRef = args.flags.head || args.flags.h || 'HEAD';
|
|
2308
2337
|
if (!baseRef) { console.error('pr-delta: --base <ref> is required'); process.exit(2); }
|
|
2309
2338
|
const delta = await computePrDelta(path.resolve(root), { baseRef, headRef });
|
|
2310
|
-
if (args.flags.json)
|
|
2339
|
+
if (args.flags.json) writeStdout(JSON.stringify(delta, null, 2) + '\n');
|
|
2311
2340
|
else console.log(renderPrDeltaText(delta));
|
|
2312
2341
|
// Exit non-zero if any critical/high introduced (useful as CI gate).
|
|
2313
2342
|
const i = delta.summary?.introduced || {};
|
|
@@ -2359,7 +2388,7 @@ async function main() {
|
|
|
2359
2388
|
const repo = args.flags.repo;
|
|
2360
2389
|
if (!repo) { console.error('leaderboard-row: --repo <owner/name> is required'); process.exit(2); }
|
|
2361
2390
|
const row = leaderboardRowFor({ scanRoot: path.resolve(root), repo });
|
|
2362
|
-
|
|
2391
|
+
writeStdout(JSON.stringify(row, null, 2) + '\n');
|
|
2363
2392
|
process.exit(0);
|
|
2364
2393
|
}
|
|
2365
2394
|
case 'history': {
|
|
@@ -2372,7 +2401,7 @@ async function main() {
|
|
|
2372
2401
|
since: args.flags.since || '6.months',
|
|
2373
2402
|
interval: args.flags.interval || '1.month',
|
|
2374
2403
|
});
|
|
2375
|
-
if (args.flags.json)
|
|
2404
|
+
if (args.flags.json) writeStdout(JSON.stringify(r, null, 2) + '\n');
|
|
2376
2405
|
else if (r.error) console.error(`history: ${r.error}`);
|
|
2377
2406
|
else {
|
|
2378
2407
|
console.log(`Scanned ${r.refs.length} refs.`);
|
|
@@ -2408,7 +2437,7 @@ async function main() {
|
|
|
2408
2437
|
? (Array.isArray(args.flags.remove) ? args.flags.remove : [args.flags.remove])
|
|
2409
2438
|
: [];
|
|
2410
2439
|
const r = await runWhatIf(path.resolve(root), { overlays, remove });
|
|
2411
|
-
if (args.flags.json)
|
|
2440
|
+
if (args.flags.json) writeStdout(JSON.stringify(r, null, 2) + '\n');
|
|
2412
2441
|
else {
|
|
2413
2442
|
console.log(`baseline: ${r.baselineFindings} findings`);
|
|
2414
2443
|
console.log(`what-if: ${r.whatIfFindings} findings (delta ${r.delta >= 0 ? '+' : ''}${r.delta})`);
|