@clear-capabilities/agentic-security-scanner 0.132.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 CHANGED
@@ -1,5 +1,130 @@
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
+
3
128
  ## 0.132.0 — a proven exploit becomes a permanent regression test
4
129
 
5
130
  The corpus stops being only a regression net and starts being fed by the engine
@@ -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';
@@ -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/.
package/dist/113.index.js CHANGED
@@ -406,8 +406,8 @@ var external_node_child_process_ = __webpack_require__(1421);
406
406
  var external_node_fs_ = __webpack_require__(3024);
407
407
  // EXTERNAL MODULE: external "node:path"
408
408
  var external_node_path_ = __webpack_require__(6760);
409
- // EXTERNAL MODULE: ./src/engine.js + 595 modules
410
- var engine = __webpack_require__(4660);
409
+ // EXTERNAL MODULE: ./src/engine.js + 600 modules
410
+ var engine = __webpack_require__(5865);
411
411
  ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
412
412
  // Deterministic honesty gates on fix / finding output (#7).
413
413
  //
@@ -946,7 +946,7 @@ async function verifyFix({
946
946
  let pocLeg = { status: 'not-requested', reason: null, tier: null };
947
947
  if (poc?.code) {
948
948
  try {
949
- const { proveFinding } = await __webpack_require__.e(/* import() */ 259).then(__webpack_require__.bind(__webpack_require__, 8259));
949
+ const { proveFinding } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 1291));
950
950
  const proved = await proveFinding({ ...(poc.finding || {}), poc }, { files });
951
951
  const tier = proved.proofTier;
952
952
  pocLeg = tier === 'execution-proven'
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__(4660);
16
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5865);
17
17
  // Time-travel + counterfactual scanning (v0.68).
18
18
  //
19
19
  // Two new modes that exploit the pure-input shape of runFullScan:
package/dist/384.index.js CHANGED
@@ -8,7 +8,7 @@ export const modules = {
8
8
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
9
  /* harmony export */ scanCredentials: () => (/* reexport safe */ _engine_js__WEBPACK_IMPORTED_MODULE_0__.Sv)
10
10
  /* harmony export */ });
11
- /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4660);
11
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(5865);
12
12
  // Secrets submodule view of the engine — credential + entropy + TODO scanning.
13
13
 
14
14
 
package/dist/526.index.js CHANGED
@@ -234,8 +234,8 @@ var external_node_child_process_ = __webpack_require__(1421);
234
234
  var external_node_fs_ = __webpack_require__(3024);
235
235
  // EXTERNAL MODULE: external "node:path"
236
236
  var external_node_path_ = __webpack_require__(6760);
237
- // EXTERNAL MODULE: ./src/engine.js + 595 modules
238
- var engine = __webpack_require__(4660);
237
+ // EXTERNAL MODULE: ./src/engine.js + 600 modules
238
+ var engine = __webpack_require__(5865);
239
239
  ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
240
240
  // Deterministic honesty gates on fix / finding output (#7).
241
241
  //
@@ -774,7 +774,7 @@ async function verifyFix({
774
774
  let pocLeg = { status: 'not-requested', reason: null, tier: null };
775
775
  if (poc?.code) {
776
776
  try {
777
- const { proveFinding } = await __webpack_require__.e(/* import() */ 259).then(__webpack_require__.bind(__webpack_require__, 8259));
777
+ const { proveFinding } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 1291));
778
778
  const proved = await proveFinding({ ...(poc.finding || {}), poc }, { files });
779
779
  const tier = proved.proofTier;
780
780
  pocLeg = tier === 'execution-proven'
package/dist/637.index.js CHANGED
@@ -11,7 +11,7 @@ export const modules = {
11
11
  /* harmony export */ renderPrDeltaText: () => (/* binding */ renderPrDeltaText)
12
12
  /* harmony export */ });
13
13
  /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
14
- /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4660);
14
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5865);
15
15
  // Shadowscan / security-DELTA on PR (v0.72).
16
16
  //
17
17
  // Most SAST PR-comment integrations show absolute counts — "12 findings