@clear-capabilities/agentic-security-scanner 0.136.2 → 0.136.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +236 -0
  2. package/bin/agentic-security.js +186 -37
  3. package/dist/113.index.js +13 -4
  4. package/dist/178.index.js +1 -1
  5. package/dist/207.index.js +5 -4
  6. package/dist/238.index.js +1 -1
  7. package/dist/317.index.js +36 -6
  8. package/dist/384.index.js +1 -1
  9. package/dist/435.index.js +183 -14
  10. package/dist/444.index.js +20 -11
  11. package/dist/449.index.js +8 -1
  12. package/dist/526.index.js +3 -3
  13. package/dist/637.index.js +1 -1
  14. package/dist/agentic-security.mjs +14 -14
  15. package/dist/agentic-security.mjs.sha256 +1 -1
  16. package/dist/compliance-frameworks/nist-privacy-1-1.json +2 -2
  17. package/dist/compliance-frameworks/owasp-asvs-5.json +1 -1
  18. package/package.json +18 -10
  19. package/src/dataflow/CLAUDE.md +10 -4
  20. package/src/dataflow/builtin-summaries.js +1 -1
  21. package/src/dataflow/engine.js +324 -60
  22. package/src/dataflow/implicit-flow.js +68 -36
  23. package/src/dataflow/incremental.js +18 -3
  24. package/src/dataflow/index.js +2 -1
  25. package/src/dataflow/proven-clean.js +41 -0
  26. package/src/dataflow/sanitizer-gate.js +35 -9
  27. package/src/dataflow/sanitizer-proof.js +21 -3
  28. package/src/dataflow/stub-aware-filter.js +36 -13
  29. package/src/dataflow/summaries.js +21 -2
  30. package/src/engine.js +202 -42
  31. package/src/ir/CLAUDE.md +4 -1
  32. package/src/ir/balanced-call.js +55 -0
  33. package/src/ir/parser-cs.js +62 -9
  34. package/src/ir/parser-go.js +29 -11
  35. package/src/ir/parser-java.js +96 -19
  36. package/src/ir/parser-js.js +151 -20
  37. package/src/ir/parser-php.js +44 -9
  38. package/src/ir/parser-rb.js +37 -7
  39. package/src/ir/ssa.js +6 -1
  40. package/src/lsp/server.js +28 -2
  41. package/src/mcp/CLAUDE.md +9 -2
  42. package/src/mcp/redact.js +26 -0
  43. package/src/mcp/tools.js +155 -14
  44. package/src/posture/CLAUDE.md +19 -7
  45. package/src/posture/accuracy-scorecard.js +9 -1
  46. package/src/posture/aibom.js +12 -8
  47. package/src/posture/auditor-walkthrough.js +102 -3
  48. package/src/posture/autopilot.js +8 -1
  49. package/src/posture/calibration-drift.js +11 -5
  50. package/src/posture/calibration.js +24 -2
  51. package/src/posture/compliance-frameworks/nist-privacy-1-1.json +2 -2
  52. package/src/posture/compliance-frameworks/owasp-asvs-5.json +1 -1
  53. package/src/posture/compliance-policy.js +33 -1
  54. package/src/posture/confidence.js +44 -10
  55. package/src/posture/corpus-enroll.js +9 -5
  56. package/src/posture/corpus-match.js +19 -0
  57. package/src/posture/csharp-analysis.js +62 -3
  58. package/src/posture/deploy-platform.js +4 -1
  59. package/src/posture/drift.js +7 -1
  60. package/src/posture/epss.js +13 -1
  61. package/src/posture/evidence-bundle.js +36 -6
  62. package/src/posture/exploitability-probability.js +13 -1
  63. package/src/posture/falsification.js +23 -2
  64. package/src/posture/fix-metrics.js +1 -1
  65. package/src/posture/fix-verify-loop.js +10 -1
  66. package/src/posture/iac-reachability.js +14 -8
  67. package/src/posture/integrity.js +25 -7
  68. package/src/posture/model-rescan.js +65 -0
  69. package/src/posture/mttr.js +5 -0
  70. package/src/posture/poc-inprocess.js +27 -8
  71. package/src/posture/regression-test-gen.js +23 -8
  72. package/src/posture/reverse-blast-radius.js +5 -1
  73. package/src/posture/risk-dollars.js +18 -1
  74. package/src/posture/secret-history.js +20 -11
  75. package/src/posture/security-trend.js +7 -1
  76. package/src/posture/stack-playbook.js +22 -1
  77. package/src/posture/threat-model-grounding.js +2 -2
  78. package/src/posture/validator-metrics.js +10 -3
  79. package/src/posture/verifier.js +32 -57
  80. package/src/report/index.js +183 -14
  81. package/src/runScan.js +1 -1
  82. package/src/sast/_comment-strip.js +15 -4
  83. package/src/sast/_secret-entropy.js +1 -1
  84. package/src/sast/authz.js +6 -4
  85. package/src/sast/bench-shape/index.js +2 -7
  86. package/src/sast/claude-md-prompt-injection.js +14 -3
  87. package/src/sast/cloud-iam.js +60 -7
  88. package/src/sast/cpp-bench-extras.js +1 -1
  89. package/src/sast/csrf.js +7 -5
  90. package/src/sast/env-hygiene.js +5 -2
  91. package/src/sast/iac-terraform.js +25 -0
  92. package/src/sast/java-bench-extras.js +1 -1
  93. package/src/sast/java-constant-fold.js +5 -5
  94. package/src/sast/llm-owasp.js +4 -2
  95. package/src/sast/mcp-audit.js +7 -0
  96. package/src/sast/pipeline.js +8 -0
  97. package/src/sast/prompt-template.js +8 -6
  98. package/src/sast/prototype-pollution.js +6 -2
  99. package/src/sast/redos-nfa.js +6 -6
  100. package/src/sast/secret-concat.js +13 -2
  101. package/src/sast/ssrf-cloud-metadata.js +6 -3
  102. package/src/sast/xss-reflected-multilang.js +1 -1
  103. package/src/sast/xxe.js +1 -1
  104. package/src/sca/CLAUDE.md +3 -4
  105. package/src/sca/container.js +35 -3
  106. package/src/sca/dep-confusion.js +7 -0
  107. package/src/sca/sarif-ingest.js +0 -187
package/dist/207.index.js CHANGED
@@ -6,9 +6,10 @@ export const modules = {
6
6
  /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
7
 
8
8
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
- /* harmony export */ computeRunAttestation: () => (/* binding */ computeRunAttestation)
9
+ /* harmony export */ computeRunAttestation: () => (/* binding */ computeRunAttestation),
10
+ /* harmony export */ verifyRunAttestation: () => (/* binding */ verifyRunAttestation)
10
11
  /* harmony export */ });
11
- /* unused harmony exports ATTESTATION_CANONICALISATION, verifyRunAttestation */
12
+ /* unused harmony export ATTESTATION_CANONICALISATION */
12
13
  /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
13
14
  /* harmony import */ var _integrity_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1130);
14
15
  // R4 — determinism as a contract.
@@ -202,11 +203,11 @@ function verifyRunAttestation(attestation, inputs = {}) {
202
203
  }
203
204
  if (attestation.signature) {
204
205
  let expected;
205
- try { expected = signLastScan(attestation.digest); }
206
+ try { expected = (0,_integrity_js__WEBPACK_IMPORTED_MODULE_1__/* .signLastScan */ .lU)(attestation.digest); }
206
207
  catch { return { ok: false, reason: 'signature present but no key material available to check it' }; }
207
208
  const a = Buffer.from(String(attestation.signature), 'hex');
208
209
  const b = Buffer.from(expected, 'hex');
209
- if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
210
+ if (a.length !== b.length || !node_crypto__WEBPACK_IMPORTED_MODULE_0__.timingSafeEqual(a, b)) {
210
211
  return { ok: false, reason: 'signature does not verify under this install key' };
211
212
  }
212
213
  return { ok: true, reason: 'digest and per-install signature verify' };
package/dist/238.index.js CHANGED
@@ -78,7 +78,7 @@ function recordFixAttempt(scanRoot, record) {
78
78
  try {
79
79
  const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .stateDir */ .Pn)(scanRoot);
80
80
  if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.isSafeStateDir)(dir)) return false;
81
- if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return;
81
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return false;
82
82
  node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(dir, { recursive: true });
83
83
  // One writeSync of one newline-terminated line: a concurrent reader sees
84
84
  // whole records or nothing, and a torn tail is dropped on read.
package/dist/317.index.js CHANGED
@@ -123,12 +123,25 @@ function ensureKeyPair(dir = keyDir()) {
123
123
  return { privateKeyPem, publicKeyPem, created: true, ...p };
124
124
  } catch (e) {
125
125
  if (e.code !== 'EEXIST') throw e;
126
- return {
127
- privateKeyPem: node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(p.privateKey, 'utf8'),
128
- publicKeyPem: node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(p.publicKey, 'utf8'),
129
- created: false,
130
- ...p,
131
- };
126
+ // We lost the race on the private key — someone else's 'wx' won.
127
+ // Their public-key write follows immediately after their private-key
128
+ // write but is not itself atomic with it, so it may not have landed
129
+ // yet: a bare read here would throw an uncaught ENOENT on a genuinely
130
+ // transient state, not a real error. Retry briefly rather than crash.
131
+ const sleepBuf = new Int32Array(new SharedArrayBuffer(4));
132
+ for (let attempt = 0; ; attempt++) {
133
+ try {
134
+ return {
135
+ privateKeyPem: node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(p.privateKey, 'utf8'),
136
+ publicKeyPem: node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(p.publicKey, 'utf8'),
137
+ created: false,
138
+ ...p,
139
+ };
140
+ } catch (readErr) {
141
+ if (readErr.code !== 'ENOENT' || attempt >= 50) throw readErr;
142
+ Atomics.wait(sleepBuf, 0, 0, 10); // 10ms; ~500ms total budget
143
+ }
144
+ }
132
145
  }
133
146
  }
134
147
 
@@ -246,9 +259,26 @@ function signEvidenceBundle(bundle, privateKeyPem) {
246
259
  * Returns {ok, reason}. Never throws — a malformed bundle from an untrusted
247
260
  * source is an expected input, not an exceptional one.
248
261
  */
262
+ // The complete set of top-level keys a legitimately-built, signed bundle can
263
+ // carry — buildEvidenceBundle's six plus signEvidenceBundle's `signature`.
264
+ // EA-03 (Stage-0 audit, 2026): canonicalBytes SIGNS an allowlist of fields;
265
+ // verifyEvidenceBundle never checked for keys OUTSIDE that allowlist, so a
266
+ // bundle with a fabricated `verdict`/`proofLevel`/anything-else stapled on
267
+ // after signing verified as authentic — the signature simply never covered
268
+ // those bytes. Rejecting unknown keys here closes that; it must exactly match
269
+ // what buildEvidenceBundle+signEvidenceBundle actually produce, or a
270
+ // legitimate bundle would start failing verification.
271
+ const BUNDLE_TOP_LEVEL_KEYS = new Set([
272
+ 'schema', 'finding', 'evidence', 'engine', 'proves', 'doesNotProve', 'signature',
273
+ ]);
274
+
249
275
  function verifyEvidenceBundle(bundle, publicKeyPem) {
250
276
  if (!bundle || typeof bundle !== 'object') return { ok: false, reason: 'bundle is not an object' };
251
277
  if (bundle.schema !== BUNDLE_SCHEMA) return { ok: false, reason: `unrecognised schema: ${bundle.schema}` };
278
+ const unknownKeys = Object.keys(bundle).filter(k => !BUNDLE_TOP_LEVEL_KEYS.has(k));
279
+ if (unknownKeys.length) {
280
+ return { ok: false, reason: `unrecognised top-level key(s) not covered by the signature: ${unknownKeys.join(', ')}` };
281
+ }
252
282
  const sig = bundle.signature;
253
283
  if (!sig?.value) return { ok: false, reason: 'bundle is unsigned' };
254
284
  if (sig.algorithm !== 'ed25519') return { ok: false, reason: `unsupported algorithm: ${sig.algorithm}` };
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__(2912);
11
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2739);
12
12
  // Secrets submodule view of the engine — credential + entropy + TODO scanning.
13
13
 
14
14
 
package/dist/435.index.js CHANGED
@@ -124,6 +124,32 @@ const PATTERNS = [
124
124
  [/rk_(?:live|test)_[A-Za-z0-9]{20,}/g, 'stripe-restricted-key'],
125
125
  [/SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g, 'sendgrid-key'],
126
126
  [/AIza[0-9A-Za-z_-]{35}/g, 'google-api-key'],
127
+ // Stage 4 correctness audit (coverage breadth, AI security): this list
128
+ // only covered a small subset of what the scanner's OWN credential
129
+ // detector (engine.js's CREDENTIAL_PATTERNS, 40+ provider shapes) finds
130
+ // — a Shopify/Telegram/Twilio/Discord-webhook/Square/Google-OAuth/JDBC
131
+ // secret detected and reported by a scan reached explain_finding's
132
+ // output completely unredacted, because none of those shapes were in
133
+ // THIS separate, narrower list. Reusing the same regex bodies as
134
+ // engine.js's CREDENTIAL_PATTERNS for the shapes verified to leak
135
+ // (rather than importing engine.js itself, which would pull its entire
136
+ // multi-thousand-line module graph into the MCP server's dependency
137
+ // surface for a handful of consts).
138
+ [/ya29\.[0-9A-Za-z_-]{20,}/g, 'google-oauth-token'],
139
+ [/shp(?:at|ss|ca|pa)_[a-fA-F0-9]{32}/g, 'shopify-token'],
140
+ [/(?<![0-9])[0-9]{8,10}:AA[0-9A-Za-z_-]{33}(?![A-Za-z0-9_])/g, 'telegram-bot-token'],
141
+ [/twilio.{0,20}SK[0-9a-fA-F]{32}/gi, 'twilio-api-key'],
142
+ [/sq0atp-[0-9A-Za-z_-]{22}/g, 'square-access-token'],
143
+ [/sq0csp-[0-9A-Za-z_-]{43}/g, 'square-oauth-secret'],
144
+ [/access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}/g, 'paypal-braintree-token'],
145
+ [/https:\/\/(?:discordapp|discord)\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_-]+/g, 'discord-webhook'],
146
+ [/https:\/\/hooks\.slack\.com\/services\/T[a-zA-Z0-9_]{8}\/B[a-zA-Z0-9_]{8,12}\/[a-zA-Z0-9_]{24}/g, 'slack-webhook'],
147
+ [/https:\/\/outlook\.office\.com\/webhook\/[A-Za-z0-9\-@]+\/IncomingWebhook\/[A-Za-z0-9-]+\/[A-Za-z0-9-]+/g, 'teams-webhook'],
148
+ [/https:\/\/(?:www\.)?hooks\.zapier\.com\/hooks\/catch\/[A-Za-z0-9]+\/[A-Za-z0-9]+\//g, 'zapier-webhook'],
149
+ // JDBC connection string carrying a password: only redact when password
150
+ // evidence is actually on the line (matches engine.js's own ctx gate),
151
+ // so a credential-free JDBC URL in docs isn't needlessly mangled.
152
+ [/jdbc:[a-z:]+:\/\/[A-Za-z0-9.\-_:;=/@?,&]*(?:@|password=|passwd=|pwd=)[A-Za-z0-9.\-_:;=/@?,&]*/gi, 'jdbc-connection-string'],
127
153
  // JWT — three dot-separated b64url segments starting with eyJ
128
154
  [/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, 'jwt'],
129
155
  // PEM-encoded private keys
@@ -171,6 +197,8 @@ function redactArgsBlob(s) {
171
197
  return redactString(s);
172
198
  }
173
199
 
200
+ // EXTERNAL MODULE: ./src/report/index.js + 2 modules
201
+ var report = __webpack_require__(3550);
174
202
  // EXTERNAL MODULE: ./src/posture/state-dir.js
175
203
  var state_dir = __webpack_require__(1174);
176
204
  ;// CONCATENATED MODULE: ./src/posture/agents-memory.js
@@ -470,6 +498,7 @@ const cve_lookup_internals = { CACHE_DIR, CVE_RE, _stalenessTier };
470
498
 
471
499
 
472
500
 
501
+
473
502
  // Lazy-loaded: these transitively pull in npm packages (@babel/core and
474
503
  // friends) that aren't available in the plugin-cache install path
475
504
  // (no node_modules). Deferring keeps the MCP server bootable everywhere;
@@ -600,8 +629,13 @@ function _validateScratchpadPath(relPath) {
600
629
  return { ok: true, agent, session, fileParts };
601
630
  }
602
631
 
632
+ // Routes through the same lstat+realpath confinement every other write/
633
+ // path-taking tool uses (OWASP MCP05) — a lexical prefix/charset check
634
+ // alone doesn't stop a pre-planted symlink at any path component from
635
+ // relocating the write/read outside the session root. Throws on escape;
636
+ // callers must catch (see append_scratchpad / read_scratchpad).
603
637
  function _scratchpadAbs(sessionRoot, relPath) {
604
- return external_node_path_.resolve(sessionRoot, relPath.replace(/\\/g, '/'));
638
+ return _confine(sessionRoot, relPath.replace(/\\/g, '/'), 'scratchpad path');
605
639
  }
606
640
 
607
641
  function _scratchpadTotalBytes(sessionRoot) {
@@ -785,12 +819,22 @@ const scan_diff = {
785
819
  const wantSet = new Set(Object.keys(fileContents));
786
820
  const sevRank = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
787
821
  const min = sevRank[severity] ?? 0;
788
- const findings = (result.scan.findings || [])
822
+ // Stage 6 correctness audit: this only ever read result.scan.findings
823
+ // (the SAST channel) — scan.secrets and scan.logicVulns are separate
824
+ // arrays on the raw runScan() result (report/index.js's normalizeFindings
825
+ // is what merges all four channels, and that merge hasn't run yet here).
826
+ // A file containing a bare hardcoded credential reported findingCount: 0
827
+ // through a tool whose own description promises "Use BEFORE writing a
828
+ // Write/Edit to disk so the agent can self-correct". Also reused
829
+ // _remediationOf so a fix-string detector (the majority of engine.js's
830
+ // own, ~127 call sites) doesn't silently report an empty `description`
831
+ // the way reading only `.remediation` did.
832
+ const findings = [...(result.scan.findings || []), ...(result.scan.secrets || []), ...(result.scan.logicVulns || [])]
789
833
  .filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
790
834
  .map(f => redactFinding({
791
835
  id: f.id, severity: f.severity, file: f.file, line: f.line,
792
836
  title: f.title || f.vuln, cwe: f.cwe,
793
- description: f.description, remediation: f.remediation,
837
+ description: f.description, remediation: (0,report/* _remediationOf */.uV)(f),
794
838
  }));
795
839
  // Harness-anatomy #1: offload when the result exceeds OFFLOAD_THRESHOLD.
796
840
  // The agent gets a head+tail preview plus a path it can page through;
@@ -953,10 +997,40 @@ const apply_fix = {
953
997
  additionalProperties: { type: 'string', maxLength: 500_000 },
954
998
  minProperties: 1, maxProperties: 8,
955
999
  },
1000
+ // Stage 6 correctness audit: same gap and same fix as verify_fix — the
1001
+ // honesty gate is reachable but was never wired to any real caller.
1002
+ // Here it's stronger than advisory: the inline re-verify below already
1003
+ // gates the WRITE on `verdict.ok`, and verifyFixCore's own `ok`
1004
+ // formula already folds in `honesty.ok` when fixMeta is supplied — so
1005
+ // passing it through here makes a dishonest fixMeta (hand-wave
1006
+ // residual, uncited false-positive verdict) block the write itself,
1007
+ // not just report a verdict.
1008
+ fixMeta: {
1009
+ type: 'object',
1010
+ additionalProperties: false,
1011
+ properties: {
1012
+ residual: { type: 'string', maxLength: 2000 },
1013
+ verdict: { type: 'string', maxLength: 64 },
1014
+ evidence: { type: 'array', items: { type: 'string', maxLength: 500 }, maxItems: 20 },
1015
+ signals: {
1016
+ type: 'object',
1017
+ additionalProperties: false,
1018
+ properties: {
1019
+ sinkSignatureChanged: { type: 'boolean' },
1020
+ allCallersRouted: { type: 'boolean' },
1021
+ testDiscriminates: { type: 'boolean' },
1022
+ rateLimitOnly: { type: 'boolean' },
1023
+ docsOnly: { type: 'boolean' },
1024
+ logOnlyNoReject: { type: 'boolean' },
1025
+ partialSanitization: { type: 'boolean' },
1026
+ },
1027
+ },
1028
+ },
1029
+ },
956
1030
  },
957
1031
  required: ['finding_id', 'confirm'],
958
1032
  },
959
- async handler({ finding_id, confirm, dry_run = false, patch = null }, ctx) {
1033
+ async handler({ finding_id, confirm, dry_run = false, patch = null, fixMeta = null }, ctx) {
960
1034
  if (confirm !== true) {
961
1035
  return { _meta: META, applied: false, reason: 'apply_fix requires confirm: true.' };
962
1036
  }
@@ -1012,6 +1086,7 @@ const apply_fix = {
1012
1086
  scanRoot: ctx.sessionRoot,
1013
1087
  originalFindingStableId: f.stableId,
1014
1088
  files: _files,
1089
+ fixMeta,
1015
1090
  });
1016
1091
  }
1017
1092
  } catch (e) {
@@ -1021,7 +1096,7 @@ const apply_fix = {
1021
1096
  return {
1022
1097
  _meta: META, applied: false,
1023
1098
  reason: `patch rejected by verifier: ${verdict.summary || verdict.rescan?.reason || 'did not verify'}`,
1024
- verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok } },
1099
+ verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
1025
1100
  };
1026
1101
  }
1027
1102
  if (dry_run) {
@@ -1033,7 +1108,7 @@ const apply_fix = {
1033
1108
  const originalContent = external_node_fs_.existsSync(v.abs) ? await promises_.readFile(v.abs, 'utf8') : '';
1034
1109
  const entry = await (0,fix_history/* applyFix */.oM)({
1035
1110
  scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content,
1036
- findingId: f.id, stableId: f.stableId, ruleId: f.rule || null, vuln: f.vuln || f.title || null,
1111
+ findingId: f.id, stableId: f.stableId, ruleId: f.ruleId || f.cwe || f.family || null, vuln: f.vuln || f.title || null,
1037
1112
  });
1038
1113
  written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
1039
1114
  }
@@ -1091,7 +1166,7 @@ const apply_fix = {
1091
1166
  newContent: f.fix.replacement,
1092
1167
  findingId: f.id,
1093
1168
  stableId: f.stableId || null, // premortem 4R-8
1094
- ruleId: f.rule || null,
1169
+ ruleId: f.ruleId || f.cwe || f.family || null,
1095
1170
  vuln: f.vuln || f.title || null,
1096
1171
  });
1097
1172
  } catch (e) {
@@ -1128,7 +1203,7 @@ const apply_fix = {
1128
1203
  // proceed with apply_fix.
1129
1204
  const verify_fix = {
1130
1205
  name: 'verify_fix',
1131
- description: 'Verify a proposed patch before applying. Re-scans the patched files in memory and runs the project linter. Returns { ok, rescan, lint, summary }. No filesystem writes.',
1206
+ description: 'Verify a proposed patch before applying. Re-scans the patched files in memory, runs the project linter, runs the project test suite, checks fix honesty (FULL/MITIGATION/WORKAROUND) when fixMeta is supplied, and re-runs the PoC when one exists. Returns { ok, rescan, lint, tests, honesty, poc, summary }. Does not write to the target project’s own files, but DOES append one record per attempt to .agentic-security/fix-metrics.jsonl for the measured fix-loop.',
1132
1207
  inputSchema: {
1133
1208
  type: 'object',
1134
1209
  additionalProperties: false,
@@ -1140,10 +1215,41 @@ const verify_fix = {
1140
1215
  minProperties: 1,
1141
1216
  maxProperties: 8,
1142
1217
  },
1218
+ // Stage 6 correctness audit: posture/fix-honesty-gate.js's deterministic
1219
+ // honesty checks (vague-assurance residual prose, unbacked false-
1220
+ // positive verdicts, tier/residual consistency) were fully built and
1221
+ // fix-verify.js already consulted them when given a `fixMeta` — but
1222
+ // this schema never had a `fixMeta` property, so no call through the
1223
+ // MCP surface could ever supply one. The gate can only run against
1224
+ // claims the AGENT self-reports (residual risk, verdict, evidence,
1225
+ // completeness signals) — nothing here is server-computable — so
1226
+ // fixing this meant exposing the property, not inventing a lookup.
1227
+ fixMeta: {
1228
+ type: 'object',
1229
+ additionalProperties: false,
1230
+ properties: {
1231
+ residual: { type: 'string', maxLength: 2000 },
1232
+ verdict: { type: 'string', maxLength: 64 },
1233
+ evidence: { type: 'array', items: { type: 'string', maxLength: 500 }, maxItems: 20 },
1234
+ signals: {
1235
+ type: 'object',
1236
+ additionalProperties: false,
1237
+ properties: {
1238
+ sinkSignatureChanged: { type: 'boolean' },
1239
+ allCallersRouted: { type: 'boolean' },
1240
+ testDiscriminates: { type: 'boolean' },
1241
+ rateLimitOnly: { type: 'boolean' },
1242
+ docsOnly: { type: 'boolean' },
1243
+ logOnlyNoReject: { type: 'boolean' },
1244
+ partialSanitization: { type: 'boolean' },
1245
+ },
1246
+ },
1247
+ },
1248
+ },
1143
1249
  },
1144
1250
  required: ['stable_id', 'files'],
1145
1251
  },
1146
- async handler({ stable_id, files }, ctx) {
1252
+ async handler({ stable_id, files, fixMeta }, ctx) {
1147
1253
  // Confine every file path before passing to the verifier.
1148
1254
  const confined = {};
1149
1255
  for (const [relPath, content] of Object.entries(files || {})) {
@@ -1155,17 +1261,48 @@ const verify_fix = {
1155
1261
  confined[relPath] = String(content);
1156
1262
  }
1157
1263
  try {
1264
+ // The PoC-re-check leg (verifyFixCore's `pocLeg`) needs a `poc` param
1265
+ // to do anything — until now nothing supplied one, so it always
1266
+ // reported {status:'not-requested'} through this surface (see
1267
+ // posture/CLAUDE.md's disclosure). Rather than widening inputSchema
1268
+ // to make the CALLER pass PoC data back, look it up server-side: the
1269
+ // scan pipeline already attaches an HTTP-shaped f.poc to matching
1270
+ // findings by default (engine.js's annotatePocs), and last-scan.json
1271
+ // already carries it under the same stableId this handler receives.
1272
+ // Best-effort: a missing/unsigned/tampered scan just means no PoC is
1273
+ // available to re-check, not a verify_fix failure — the rescan/lint/
1274
+ // tests legs below are independent of this and still apply.
1275
+ let poc = null;
1276
+ try {
1277
+ const { scan: lastScan } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: true });
1278
+ const orig = lastScan && (lastScan.findings || []).find(f => f.stableId === stable_id);
1279
+ if (orig && orig.poc && orig.poc.code) poc = { ...orig.poc, finding: orig };
1280
+ } catch { /* best-effort lookup; poc stays null */ }
1281
+
1158
1282
  const verifyFixCore = await getVerifyFixCore();
1159
1283
  const r = await verifyFixCore({
1160
1284
  scanRoot: ctx.sessionRoot,
1161
1285
  originalFindingStableId: stable_id,
1162
1286
  files: confined,
1287
+ poc,
1288
+ fixMeta,
1163
1289
  });
1164
1290
  return {
1165
1291
  _meta: META,
1166
1292
  ok: r.ok,
1167
1293
  rescan: { ok: r.rescan.ok, reason: r.rescan.reason, introduced: r.rescan.introduced || [] },
1168
1294
  lint: { runner: r.lint.runner, ok: r.lint.ok, skipped: r.lint.skipped || false, output: redactString(r.lint.output || '').slice(0, 1500) },
1295
+ // verifyFix computes five legs, not two — tests/honesty/poc were
1296
+ // being silently dropped here, leaving an agent with no structured
1297
+ // way to see WHY verification failed when the failure was in one
1298
+ // of those three (only the free-text summary carried it).
1299
+ // test-runner.js's runProjectTests never returns raw stdout/stderr,
1300
+ // so no redaction is needed there; honesty.violations are static,
1301
+ // code-generated strings; poc.reason is redacted defensively since
1302
+ // it can echo proof-harness detail derived from scanned source.
1303
+ tests: r.tests,
1304
+ honesty: r.honesty,
1305
+ poc: r.poc ? { ...r.poc, reason: r.poc.reason ? redactString(r.poc.reason) : r.poc.reason } : r.poc,
1169
1306
  summary: r.summary,
1170
1307
  };
1171
1308
  } catch (e) {
@@ -1249,7 +1386,13 @@ const synthesize_fix = {
1249
1386
  regression_test: f.regression_test || null,
1250
1387
  remediation: typeof fix.description === 'string' ? fix.description : (typeof fix === 'string' ? fix : null),
1251
1388
  patchBounds: { touchedFiles, locDelta, oversized },
1252
- recommendsFixPlan: oversized && !hasReplacement && !autofix,
1389
+ // oversized can only be true when hasReplacement is true (locDelta is
1390
+ // only computed in that branch, and touchedFiles never varies) — a
1391
+ // `!hasReplacement` conjunct here was a structural contradiction that
1392
+ // made this permanently false. The correct signal: the stored
1393
+ // replacement itself is too big to trust auto-applying, and there's
1394
+ // no safer deterministic alternative.
1395
+ recommendsFixPlan: oversized && !autofix,
1253
1396
  };
1254
1397
  },
1255
1398
  };
@@ -1381,7 +1524,9 @@ const append_scratchpad = {
1381
1524
  async handler({ path: relPath, content }, ctx) {
1382
1525
  const v = _validateScratchpadPath(relPath);
1383
1526
  if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
1384
- const abs = _scratchpadAbs(ctx.sessionRoot, relPath);
1527
+ let abs;
1528
+ try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
1529
+ catch (e) { return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` }; }
1385
1530
  const total = _scratchpadTotalBytes(ctx.sessionRoot);
1386
1531
  if (total + content.length > SCRATCHPAD_MAX_TOTAL_BYTES) {
1387
1532
  return {
@@ -1427,7 +1572,9 @@ const read_scratchpad = {
1427
1572
  async handler({ path: relPath, offset, limit }, ctx) {
1428
1573
  const v = _validateScratchpadPath(relPath);
1429
1574
  if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
1430
- const abs = _scratchpadAbs(ctx.sessionRoot, relPath);
1575
+ let abs;
1576
+ try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
1577
+ catch (e) { return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` }; }
1431
1578
  if (!external_node_fs_.existsSync(abs)) return { _meta: META, ok: false, reason: 'not-found' };
1432
1579
  let stat;
1433
1580
  try { stat = external_node_fs_.statSync(abs); } catch (e) { return { _meta: META, ok: false, reason: `stat-failed: ${e.message}` }; }
@@ -1523,7 +1670,20 @@ const query_triage_memory = {
1523
1670
  },
1524
1671
  async handler({ query }, ctx) {
1525
1672
  const { queryMemory } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 1905));
1526
- const results = queryMemory(ctx.sessionRoot, query || '');
1673
+ const raw = queryMemory(ctx.sessionRoot, query || '');
1674
+ // Stage 6 correctness audit: this returned queryMemory's output
1675
+ // verbatim, with no redaction pass — every other tool that echoes
1676
+ // scanned-source-derived text redacts it (mcp/CLAUDE.md's "Adding a new
1677
+ // tool" step 3). Round-trip through redactString the same way
1678
+ // redactFinding already does for its own opaque `.trace` field: results
1679
+ // here mix shapes (a triage decision's free-text `reason`, a finding's
1680
+ // `vuln`/`family`/file path), so scrubbing the whole serialized
1681
+ // structure catches secret-shaped substrings regardless of which field
1682
+ // they landed in, rather than hardcoding a field allowlist that could
1683
+ // miss one.
1684
+ let results;
1685
+ try { results = JSON.parse(redactString(JSON.stringify(raw))); }
1686
+ catch { results = raw; }
1527
1687
  return {
1528
1688
  _meta: META,
1529
1689
  count: results.length,
@@ -1551,7 +1711,16 @@ const query_findings_memory = {
1551
1711
  },
1552
1712
  async handler({ query }, ctx) {
1553
1713
  const { queryFindingsMemory } = await __webpack_require__.e(/* import() */ 839).then(__webpack_require__.bind(__webpack_require__, 3839));
1554
- return { _meta: META, ...queryFindingsMemory(ctx.sessionRoot, query || '') };
1714
+ const raw = queryFindingsMemory(ctx.sessionRoot, query || '');
1715
+ // Stage 6 correctness audit — same redaction gap and same fix as
1716
+ // query_triage_memory just above: this mixes four differently-shaped
1717
+ // result kinds (finding / triage / history / AGENTS.md text), so a
1718
+ // whole-structure redactString round-trip is applied rather than a
1719
+ // per-field allowlist that could miss one of the four shapes.
1720
+ let body;
1721
+ try { body = JSON.parse(redactString(JSON.stringify(raw))); }
1722
+ catch { body = raw; }
1723
+ return { _meta: META, ...body };
1555
1724
  },
1556
1725
  };
1557
1726
 
package/dist/444.index.js CHANGED
@@ -43,17 +43,26 @@ function scanHistoryDiff(diffText, commit, detectFn) {
43
43
  if (!added.trim()) return [];
44
44
  let findings = [];
45
45
  try { findings = detectFn(`git-history@${commit}`, added) || []; } catch { return []; }
46
- return findings.map((f) => ({
47
- ...f,
48
- id: `secret-history:${commit}:${f.id || f.vuln || 'secret'}`,
49
- file: `git-history@${commit}`,
50
- line: 0,
51
- commit,
52
- _historical: true,
53
- vuln: `${f.vuln || 'Hardcoded Secret'} (in git history)`,
54
- description: `${f.description || 'A credential was committed.'} Found in commit ${commit}; even if removed from HEAD it remains recoverable from git and must be rotated.`,
55
- remediation: 'Rotate the credential now, then purge it from history (git filter-repo / BFG) and move it to a secrets manager. Removing it from HEAD alone is insufficient.',
56
- }));
46
+ return findings.map((f) => {
47
+ const remediation = 'Rotate the credential now, then purge it from history (git filter-repo / BFG) and move it to a secrets manager. Removing it from HEAD alone is insufficient.';
48
+ return {
49
+ ...f,
50
+ id: `secret-history:${commit}:${f.id || f.vuln || 'secret'}`,
51
+ file: `git-history@${commit}`,
52
+ line: 0,
53
+ commit,
54
+ _historical: true,
55
+ vuln: `${f.vuln || 'Hardcoded Secret'} (in git history)`,
56
+ description: `${f.description || 'A credential was committed.'} Found in commit ${commit}; even if removed from HEAD it remains recoverable from git and must be rotated.`,
57
+ remediation,
58
+ // report/index.js's _remediationOf checks `.fix` before `.remediation`
59
+ // — the underlying detector already set `.fix` to a generic "remove
60
+ // the line" string, which would otherwise silently shadow this
61
+ // history-specific instruction ("removing it from HEAD alone is
62
+ // insufficient") in every report format.
63
+ fix: remediation,
64
+ };
65
+ });
57
66
  }
58
67
 
59
68
  /**
package/dist/449.index.js CHANGED
@@ -7,10 +7,12 @@ export const modules = {
7
7
 
8
8
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
9
  /* harmony export */ buildBaselineMap: () => (/* binding */ buildBaselineMap),
10
+ /* harmony export */ computeMTTR: () => (/* binding */ computeMTTR),
11
+ /* harmony export */ fingerprintFinding: () => (/* binding */ fingerprintFinding),
10
12
  /* harmony export */ renderSlaSummary: () => (/* binding */ renderSlaSummary),
11
13
  /* harmony export */ stampFindingTimestamps: () => (/* binding */ stampFindingTimestamps)
12
14
  /* harmony export */ });
13
- /* unused harmony exports findingsExceedingSLA, computeMTTR */
15
+ /* unused harmony export findingsExceedingSLA */
14
16
  /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
15
17
  // 0.8.0 Feat-11: MTTR / finding-age tracking — per-finding firstSeenAt/lastSeenAt with SLA breach detection.
16
18
  //
@@ -24,6 +26,11 @@ export const modules = {
24
26
 
25
27
 
26
28
  // Stable fingerprint for cross-scan finding identity. Mirrors the dedupe key.
29
+ // Exported so a caller can compute the "removed since baseline" (i.e. fixed)
30
+ // set that computeMTTR needs, using the exact same identity function
31
+ // buildBaselineMap uses internally — a caller-side reimplementation would
32
+ // risk drifting from this one and silently under/over-counting fixes.
33
+ function fingerprintFinding(f) { return _fingerprint(f); }
27
34
  function _fingerprint(f) {
28
35
  const file = (f.file || '').split(' -> ').pop();
29
36
  const line = f.line || f.source?.line || f.sink?.line || 0;
package/dist/526.index.js CHANGED
@@ -78,7 +78,7 @@ function recordFixAttempt(scanRoot, record) {
78
78
  try {
79
79
  const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__/* .stateDir */ .Pn)(scanRoot);
80
80
  if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.isSafeStateDir)(dir)) return false;
81
- if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return;
81
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return false;
82
82
  node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(dir, { recursive: true });
83
83
  // One writeSync of one newline-terminated line: a concurrent reader sees
84
84
  // whole records or nothing, and a torn tail is dropped on read.
@@ -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 + 499 modules
238
- var engine = __webpack_require__(2912);
237
+ // EXTERNAL MODULE: ./src/engine.js + 196 modules
238
+ var engine = __webpack_require__(2739);
239
239
  ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
240
240
  // Deterministic honesty gates on fix / finding output (#7).
241
241
  //
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__(2912);
14
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2739);
15
15
  // Shadowscan / security-DELTA on PR (v0.72).
16
16
  //
17
17
  // Most SAST PR-comment integrations show absolute counts — "12 findings