@clear-capabilities/agentic-security-scanner 0.136.2 → 0.137.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +880 -0
  2. package/bin/agentic-security.js +189 -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 +192 -15
  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 +15 -15
  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 +21 -13
  19. package/src/dataflow/CLAUDE.md +12 -4
  20. package/src/dataflow/builtin-summaries.js +1 -1
  21. package/src/dataflow/catalog-expanded.js +1 -0
  22. package/src/dataflow/catalog.js +157 -31
  23. package/src/dataflow/engine.js +639 -112
  24. package/src/dataflow/implicit-flow.js +68 -36
  25. package/src/dataflow/incremental.js +18 -3
  26. package/src/dataflow/index.js +17 -1
  27. package/src/dataflow/points-to.js +19 -6
  28. package/src/dataflow/proven-clean.js +41 -0
  29. package/src/dataflow/sanitizer-gate.js +35 -9
  30. package/src/dataflow/sanitizer-proof.js +21 -3
  31. package/src/dataflow/stub-aware-filter.js +36 -13
  32. package/src/dataflow/summaries.js +21 -2
  33. package/src/engine.js +430 -196
  34. package/src/ir/CLAUDE.md +16 -2
  35. package/src/ir/balanced-call.js +55 -0
  36. package/src/ir/class-hierarchy.js +57 -11
  37. package/src/ir/index.js +14 -2
  38. package/src/ir/parser-cs.js +513 -40
  39. package/src/ir/parser-go.js +29 -11
  40. package/src/ir/parser-java.js +300 -20
  41. package/src/ir/parser-js.js +300 -22
  42. package/src/ir/parser-kt.js +436 -18
  43. package/src/ir/parser-php.js +631 -38
  44. package/src/ir/parser-py.helper.py +32 -2
  45. package/src/ir/parser-py.js +31 -4
  46. package/src/ir/parser-rb.js +161 -26
  47. package/src/ir/ssa.js +6 -1
  48. package/src/lsp/server.js +35 -3
  49. package/src/mcp/CLAUDE.md +9 -2
  50. package/src/mcp/redact.js +26 -0
  51. package/src/mcp/tools.js +164 -15
  52. package/src/posture/CLAUDE.md +19 -7
  53. package/src/posture/accuracy-scorecard.js +9 -1
  54. package/src/posture/aibom.js +12 -8
  55. package/src/posture/auditor-walkthrough.js +102 -3
  56. package/src/posture/autopilot.js +8 -1
  57. package/src/posture/calibration-drift.js +11 -5
  58. package/src/posture/calibration.js +24 -2
  59. package/src/posture/clustering.js +12 -1
  60. package/src/posture/compliance-frameworks/nist-privacy-1-1.json +2 -2
  61. package/src/posture/compliance-frameworks/owasp-asvs-5.json +1 -1
  62. package/src/posture/compliance-policy.js +33 -1
  63. package/src/posture/confidence.js +44 -10
  64. package/src/posture/corpus-enroll.js +9 -5
  65. package/src/posture/corpus-match.js +19 -0
  66. package/src/posture/csharp-analysis.js +62 -3
  67. package/src/posture/deploy-platform.js +4 -1
  68. package/src/posture/drift.js +7 -1
  69. package/src/posture/epss.js +13 -1
  70. package/src/posture/evidence-bundle.js +36 -6
  71. package/src/posture/exploitability-probability.js +13 -1
  72. package/src/posture/falsification.js +23 -2
  73. package/src/posture/fix-metrics.js +1 -1
  74. package/src/posture/fix-verify-loop.js +10 -1
  75. package/src/posture/iac-reachability.js +14 -8
  76. package/src/posture/integrity.js +25 -7
  77. package/src/posture/model-rescan.js +65 -0
  78. package/src/posture/mttr.js +5 -0
  79. package/src/posture/poc-inprocess.js +27 -8
  80. package/src/posture/regression-test-gen.js +23 -8
  81. package/src/posture/reverse-blast-radius.js +5 -1
  82. package/src/posture/risk-dollars.js +18 -1
  83. package/src/posture/sbom.js +2 -2
  84. package/src/posture/secret-history.js +20 -11
  85. package/src/posture/security-trend.js +7 -1
  86. package/src/posture/stack-playbook.js +22 -1
  87. package/src/posture/threat-model-grounding.js +2 -2
  88. package/src/posture/validator-metrics.js +10 -3
  89. package/src/posture/verifier.js +32 -57
  90. package/src/report/index.js +183 -14
  91. package/src/runScan.js +1 -1
  92. package/src/sast/_comment-strip.js +15 -4
  93. package/src/sast/_secret-entropy.js +1 -1
  94. package/src/sast/authz.js +6 -4
  95. package/src/sast/bench-shape/index.js +2 -7
  96. package/src/sast/claude-md-prompt-injection.js +14 -3
  97. package/src/sast/cloud-iam.js +60 -7
  98. package/src/sast/cpp-bench-extras.js +1 -1
  99. package/src/sast/csrf.js +7 -5
  100. package/src/sast/env-hygiene.js +5 -2
  101. package/src/sast/iac-terraform.js +25 -0
  102. package/src/sast/java-bench-extras.js +1 -1
  103. package/src/sast/java-constant-fold.js +5 -5
  104. package/src/sast/llm-owasp.js +4 -2
  105. package/src/sast/mcp-audit.js +7 -0
  106. package/src/sast/pipeline.js +8 -0
  107. package/src/sast/prompt-template.js +8 -6
  108. package/src/sast/prototype-pollution.js +6 -2
  109. package/src/sast/redos-nfa.js +6 -6
  110. package/src/sast/secret-concat.js +13 -2
  111. package/src/sast/ssrf-cloud-metadata.js +6 -3
  112. package/src/sast/xss-reflected-multilang.js +1 -1
  113. package/src/sast/xxe.js +1 -1
  114. package/src/sca/CLAUDE.md +3 -4
  115. package/src/sca/container.js +35 -3
  116. package/src/sca/dep-confusion.js +7 -0
  117. package/src/sca/sarif-ingest.js +0 -187
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) {
@@ -780,17 +814,35 @@ const scan_diff = {
780
814
  fileContents[rel] = content;
781
815
  }
782
816
 
817
+ // PRD R1 (docs/DETECTION_GAP_REMEDIATION_PRD.md): deep mode is default-on
818
+ // for the interactive CLI scan but was never requested here, so an
819
+ // agent's pre-write self-correction scan was regex/AST-only — blind to
820
+ // any bug whose source and sink are connected only through a call
821
+ // (`fileContents` scopes the deep engine's IR to exactly the files
822
+ // passed in, same bound this tool already enforces via MAX_FILES_PER_SCAN
823
+ // / MAX_TOTAL_SCAN_BYTES, so this does not turn scan_diff into a
824
+ // full-project deep scan).
783
825
  const runScan = await getRunScan();
784
- const result = await runScan(sessionRoot, { network: false, fileContents });
826
+ const result = await runScan(sessionRoot, { network: false, fileContents, deep: true, deepInCi: true });
785
827
  const wantSet = new Set(Object.keys(fileContents));
786
828
  const sevRank = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
787
829
  const min = sevRank[severity] ?? 0;
788
- const findings = (result.scan.findings || [])
830
+ // Stage 6 correctness audit: this only ever read result.scan.findings
831
+ // (the SAST channel) — scan.secrets and scan.logicVulns are separate
832
+ // arrays on the raw runScan() result (report/index.js's normalizeFindings
833
+ // is what merges all four channels, and that merge hasn't run yet here).
834
+ // A file containing a bare hardcoded credential reported findingCount: 0
835
+ // through a tool whose own description promises "Use BEFORE writing a
836
+ // Write/Edit to disk so the agent can self-correct". Also reused
837
+ // _remediationOf so a fix-string detector (the majority of engine.js's
838
+ // own, ~127 call sites) doesn't silently report an empty `description`
839
+ // the way reading only `.remediation` did.
840
+ const findings = [...(result.scan.findings || []), ...(result.scan.secrets || []), ...(result.scan.logicVulns || [])]
789
841
  .filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
790
842
  .map(f => redactFinding({
791
843
  id: f.id, severity: f.severity, file: f.file, line: f.line,
792
844
  title: f.title || f.vuln, cwe: f.cwe,
793
- description: f.description, remediation: f.remediation,
845
+ description: f.description, remediation: (0,report/* _remediationOf */.uV)(f),
794
846
  }));
795
847
  // Harness-anatomy #1: offload when the result exceeds OFFLOAD_THRESHOLD.
796
848
  // The agent gets a head+tail preview plus a path it can page through;
@@ -953,10 +1005,40 @@ const apply_fix = {
953
1005
  additionalProperties: { type: 'string', maxLength: 500_000 },
954
1006
  minProperties: 1, maxProperties: 8,
955
1007
  },
1008
+ // Stage 6 correctness audit: same gap and same fix as verify_fix — the
1009
+ // honesty gate is reachable but was never wired to any real caller.
1010
+ // Here it's stronger than advisory: the inline re-verify below already
1011
+ // gates the WRITE on `verdict.ok`, and verifyFixCore's own `ok`
1012
+ // formula already folds in `honesty.ok` when fixMeta is supplied — so
1013
+ // passing it through here makes a dishonest fixMeta (hand-wave
1014
+ // residual, uncited false-positive verdict) block the write itself,
1015
+ // not just report a verdict.
1016
+ fixMeta: {
1017
+ type: 'object',
1018
+ additionalProperties: false,
1019
+ properties: {
1020
+ residual: { type: 'string', maxLength: 2000 },
1021
+ verdict: { type: 'string', maxLength: 64 },
1022
+ evidence: { type: 'array', items: { type: 'string', maxLength: 500 }, maxItems: 20 },
1023
+ signals: {
1024
+ type: 'object',
1025
+ additionalProperties: false,
1026
+ properties: {
1027
+ sinkSignatureChanged: { type: 'boolean' },
1028
+ allCallersRouted: { type: 'boolean' },
1029
+ testDiscriminates: { type: 'boolean' },
1030
+ rateLimitOnly: { type: 'boolean' },
1031
+ docsOnly: { type: 'boolean' },
1032
+ logOnlyNoReject: { type: 'boolean' },
1033
+ partialSanitization: { type: 'boolean' },
1034
+ },
1035
+ },
1036
+ },
1037
+ },
956
1038
  },
957
1039
  required: ['finding_id', 'confirm'],
958
1040
  },
959
- async handler({ finding_id, confirm, dry_run = false, patch = null }, ctx) {
1041
+ async handler({ finding_id, confirm, dry_run = false, patch = null, fixMeta = null }, ctx) {
960
1042
  if (confirm !== true) {
961
1043
  return { _meta: META, applied: false, reason: 'apply_fix requires confirm: true.' };
962
1044
  }
@@ -1012,6 +1094,7 @@ const apply_fix = {
1012
1094
  scanRoot: ctx.sessionRoot,
1013
1095
  originalFindingStableId: f.stableId,
1014
1096
  files: _files,
1097
+ fixMeta,
1015
1098
  });
1016
1099
  }
1017
1100
  } catch (e) {
@@ -1021,7 +1104,7 @@ const apply_fix = {
1021
1104
  return {
1022
1105
  _meta: META, applied: false,
1023
1106
  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 } },
1107
+ verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
1025
1108
  };
1026
1109
  }
1027
1110
  if (dry_run) {
@@ -1033,7 +1116,7 @@ const apply_fix = {
1033
1116
  const originalContent = external_node_fs_.existsSync(v.abs) ? await promises_.readFile(v.abs, 'utf8') : '';
1034
1117
  const entry = await (0,fix_history/* applyFix */.oM)({
1035
1118
  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,
1119
+ findingId: f.id, stableId: f.stableId, ruleId: f.ruleId || f.cwe || f.family || null, vuln: f.vuln || f.title || null,
1037
1120
  });
1038
1121
  written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
1039
1122
  }
@@ -1091,7 +1174,7 @@ const apply_fix = {
1091
1174
  newContent: f.fix.replacement,
1092
1175
  findingId: f.id,
1093
1176
  stableId: f.stableId || null, // premortem 4R-8
1094
- ruleId: f.rule || null,
1177
+ ruleId: f.ruleId || f.cwe || f.family || null,
1095
1178
  vuln: f.vuln || f.title || null,
1096
1179
  });
1097
1180
  } catch (e) {
@@ -1128,7 +1211,7 @@ const apply_fix = {
1128
1211
  // proceed with apply_fix.
1129
1212
  const verify_fix = {
1130
1213
  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.',
1214
+ 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
1215
  inputSchema: {
1133
1216
  type: 'object',
1134
1217
  additionalProperties: false,
@@ -1140,10 +1223,41 @@ const verify_fix = {
1140
1223
  minProperties: 1,
1141
1224
  maxProperties: 8,
1142
1225
  },
1226
+ // Stage 6 correctness audit: posture/fix-honesty-gate.js's deterministic
1227
+ // honesty checks (vague-assurance residual prose, unbacked false-
1228
+ // positive verdicts, tier/residual consistency) were fully built and
1229
+ // fix-verify.js already consulted them when given a `fixMeta` — but
1230
+ // this schema never had a `fixMeta` property, so no call through the
1231
+ // MCP surface could ever supply one. The gate can only run against
1232
+ // claims the AGENT self-reports (residual risk, verdict, evidence,
1233
+ // completeness signals) — nothing here is server-computable — so
1234
+ // fixing this meant exposing the property, not inventing a lookup.
1235
+ fixMeta: {
1236
+ type: 'object',
1237
+ additionalProperties: false,
1238
+ properties: {
1239
+ residual: { type: 'string', maxLength: 2000 },
1240
+ verdict: { type: 'string', maxLength: 64 },
1241
+ evidence: { type: 'array', items: { type: 'string', maxLength: 500 }, maxItems: 20 },
1242
+ signals: {
1243
+ type: 'object',
1244
+ additionalProperties: false,
1245
+ properties: {
1246
+ sinkSignatureChanged: { type: 'boolean' },
1247
+ allCallersRouted: { type: 'boolean' },
1248
+ testDiscriminates: { type: 'boolean' },
1249
+ rateLimitOnly: { type: 'boolean' },
1250
+ docsOnly: { type: 'boolean' },
1251
+ logOnlyNoReject: { type: 'boolean' },
1252
+ partialSanitization: { type: 'boolean' },
1253
+ },
1254
+ },
1255
+ },
1256
+ },
1143
1257
  },
1144
1258
  required: ['stable_id', 'files'],
1145
1259
  },
1146
- async handler({ stable_id, files }, ctx) {
1260
+ async handler({ stable_id, files, fixMeta }, ctx) {
1147
1261
  // Confine every file path before passing to the verifier.
1148
1262
  const confined = {};
1149
1263
  for (const [relPath, content] of Object.entries(files || {})) {
@@ -1155,17 +1269,48 @@ const verify_fix = {
1155
1269
  confined[relPath] = String(content);
1156
1270
  }
1157
1271
  try {
1272
+ // The PoC-re-check leg (verifyFixCore's `pocLeg`) needs a `poc` param
1273
+ // to do anything — until now nothing supplied one, so it always
1274
+ // reported {status:'not-requested'} through this surface (see
1275
+ // posture/CLAUDE.md's disclosure). Rather than widening inputSchema
1276
+ // to make the CALLER pass PoC data back, look it up server-side: the
1277
+ // scan pipeline already attaches an HTTP-shaped f.poc to matching
1278
+ // findings by default (engine.js's annotatePocs), and last-scan.json
1279
+ // already carries it under the same stableId this handler receives.
1280
+ // Best-effort: a missing/unsigned/tampered scan just means no PoC is
1281
+ // available to re-check, not a verify_fix failure — the rescan/lint/
1282
+ // tests legs below are independent of this and still apply.
1283
+ let poc = null;
1284
+ try {
1285
+ const { scan: lastScan } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: true });
1286
+ const orig = lastScan && (lastScan.findings || []).find(f => f.stableId === stable_id);
1287
+ if (orig && orig.poc && orig.poc.code) poc = { ...orig.poc, finding: orig };
1288
+ } catch { /* best-effort lookup; poc stays null */ }
1289
+
1158
1290
  const verifyFixCore = await getVerifyFixCore();
1159
1291
  const r = await verifyFixCore({
1160
1292
  scanRoot: ctx.sessionRoot,
1161
1293
  originalFindingStableId: stable_id,
1162
1294
  files: confined,
1295
+ poc,
1296
+ fixMeta,
1163
1297
  });
1164
1298
  return {
1165
1299
  _meta: META,
1166
1300
  ok: r.ok,
1167
1301
  rescan: { ok: r.rescan.ok, reason: r.rescan.reason, introduced: r.rescan.introduced || [] },
1168
1302
  lint: { runner: r.lint.runner, ok: r.lint.ok, skipped: r.lint.skipped || false, output: redactString(r.lint.output || '').slice(0, 1500) },
1303
+ // verifyFix computes five legs, not two — tests/honesty/poc were
1304
+ // being silently dropped here, leaving an agent with no structured
1305
+ // way to see WHY verification failed when the failure was in one
1306
+ // of those three (only the free-text summary carried it).
1307
+ // test-runner.js's runProjectTests never returns raw stdout/stderr,
1308
+ // so no redaction is needed there; honesty.violations are static,
1309
+ // code-generated strings; poc.reason is redacted defensively since
1310
+ // it can echo proof-harness detail derived from scanned source.
1311
+ tests: r.tests,
1312
+ honesty: r.honesty,
1313
+ poc: r.poc ? { ...r.poc, reason: r.poc.reason ? redactString(r.poc.reason) : r.poc.reason } : r.poc,
1169
1314
  summary: r.summary,
1170
1315
  };
1171
1316
  } catch (e) {
@@ -1249,7 +1394,13 @@ const synthesize_fix = {
1249
1394
  regression_test: f.regression_test || null,
1250
1395
  remediation: typeof fix.description === 'string' ? fix.description : (typeof fix === 'string' ? fix : null),
1251
1396
  patchBounds: { touchedFiles, locDelta, oversized },
1252
- recommendsFixPlan: oversized && !hasReplacement && !autofix,
1397
+ // oversized can only be true when hasReplacement is true (locDelta is
1398
+ // only computed in that branch, and touchedFiles never varies) — a
1399
+ // `!hasReplacement` conjunct here was a structural contradiction that
1400
+ // made this permanently false. The correct signal: the stored
1401
+ // replacement itself is too big to trust auto-applying, and there's
1402
+ // no safer deterministic alternative.
1403
+ recommendsFixPlan: oversized && !autofix,
1253
1404
  };
1254
1405
  },
1255
1406
  };
@@ -1381,7 +1532,9 @@ const append_scratchpad = {
1381
1532
  async handler({ path: relPath, content }, ctx) {
1382
1533
  const v = _validateScratchpadPath(relPath);
1383
1534
  if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
1384
- const abs = _scratchpadAbs(ctx.sessionRoot, relPath);
1535
+ let abs;
1536
+ try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
1537
+ catch (e) { return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` }; }
1385
1538
  const total = _scratchpadTotalBytes(ctx.sessionRoot);
1386
1539
  if (total + content.length > SCRATCHPAD_MAX_TOTAL_BYTES) {
1387
1540
  return {
@@ -1427,7 +1580,9 @@ const read_scratchpad = {
1427
1580
  async handler({ path: relPath, offset, limit }, ctx) {
1428
1581
  const v = _validateScratchpadPath(relPath);
1429
1582
  if (!v.ok) return { _meta: META, ok: false, reason: v.reason };
1430
- const abs = _scratchpadAbs(ctx.sessionRoot, relPath);
1583
+ let abs;
1584
+ try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
1585
+ catch (e) { return { _meta: META, ok: false, reason: `path-escape refused: ${e.message}` }; }
1431
1586
  if (!external_node_fs_.existsSync(abs)) return { _meta: META, ok: false, reason: 'not-found' };
1432
1587
  let stat;
1433
1588
  try { stat = external_node_fs_.statSync(abs); } catch (e) { return { _meta: META, ok: false, reason: `stat-failed: ${e.message}` }; }
@@ -1523,7 +1678,20 @@ const query_triage_memory = {
1523
1678
  },
1524
1679
  async handler({ query }, ctx) {
1525
1680
  const { queryMemory } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 1905));
1526
- const results = queryMemory(ctx.sessionRoot, query || '');
1681
+ const raw = queryMemory(ctx.sessionRoot, query || '');
1682
+ // Stage 6 correctness audit: this returned queryMemory's output
1683
+ // verbatim, with no redaction pass — every other tool that echoes
1684
+ // scanned-source-derived text redacts it (mcp/CLAUDE.md's "Adding a new
1685
+ // tool" step 3). Round-trip through redactString the same way
1686
+ // redactFinding already does for its own opaque `.trace` field: results
1687
+ // here mix shapes (a triage decision's free-text `reason`, a finding's
1688
+ // `vuln`/`family`/file path), so scrubbing the whole serialized
1689
+ // structure catches secret-shaped substrings regardless of which field
1690
+ // they landed in, rather than hardcoding a field allowlist that could
1691
+ // miss one.
1692
+ let results;
1693
+ try { results = JSON.parse(redactString(JSON.stringify(raw))); }
1694
+ catch { results = raw; }
1527
1695
  return {
1528
1696
  _meta: META,
1529
1697
  count: results.length,
@@ -1551,7 +1719,16 @@ const query_findings_memory = {
1551
1719
  },
1552
1720
  async handler({ query }, ctx) {
1553
1721
  const { queryFindingsMemory } = await __webpack_require__.e(/* import() */ 839).then(__webpack_require__.bind(__webpack_require__, 3839));
1554
- return { _meta: META, ...queryFindingsMemory(ctx.sessionRoot, query || '') };
1722
+ const raw = queryFindingsMemory(ctx.sessionRoot, query || '');
1723
+ // Stage 6 correctness audit — same redaction gap and same fix as
1724
+ // query_triage_memory just above: this mixes four differently-shaped
1725
+ // result kinds (finding / triage / history / AGENTS.md text), so a
1726
+ // whole-structure redactString round-trip is applied rather than a
1727
+ // per-field allowlist that could miss one of the four shapes.
1728
+ let body;
1729
+ try { body = JSON.parse(redactString(JSON.stringify(raw))); }
1730
+ catch { body = raw; }
1731
+ return { _meta: META, ...body };
1555
1732
  },
1556
1733
  };
1557
1734
 
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