circle-ir 4.17.0 → 4.18.1

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 (280) hide show
  1. package/dist/analysis/advisories.d.ts +186 -0
  2. package/dist/analysis/advisories.d.ts.map +1 -0
  3. package/dist/analysis/advisories.js +391 -0
  4. package/dist/analysis/advisories.js.map +1 -0
  5. package/dist/analysis/comment-lines.d.ts +14 -0
  6. package/dist/analysis/comment-lines.d.ts.map +1 -0
  7. package/dist/analysis/comment-lines.js +34 -0
  8. package/dist/analysis/comment-lines.js.map +1 -0
  9. package/dist/analysis/config-loader.d.ts.map +1 -1
  10. package/dist/analysis/config-loader.js +59 -5
  11. package/dist/analysis/config-loader.js.map +1 -1
  12. package/dist/analysis/constant-caller-gate.d.ts +2 -2
  13. package/dist/analysis/constant-caller-gate.d.ts.map +1 -1
  14. package/dist/analysis/constant-caller-gate.js +205 -29
  15. package/dist/analysis/constant-caller-gate.js.map +1 -1
  16. package/dist/analysis/constant-propagation/propagator.d.ts +4 -0
  17. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  18. package/dist/analysis/constant-propagation/propagator.js +99 -0
  19. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  20. package/dist/analysis/entry-point-detection.d.ts.map +1 -1
  21. package/dist/analysis/entry-point-detection.js +72 -0
  22. package/dist/analysis/entry-point-detection.js.map +1 -1
  23. package/dist/analysis/interprocedural.d.ts.map +1 -1
  24. package/dist/analysis/interprocedural.js +75 -8
  25. package/dist/analysis/interprocedural.js.map +1 -1
  26. package/dist/analysis/maven-declared.d.ts +72 -0
  27. package/dist/analysis/maven-declared.d.ts.map +1 -0
  28. package/dist/analysis/maven-declared.js +216 -0
  29. package/dist/analysis/maven-declared.js.map +1 -0
  30. package/dist/analysis/maven-version.d.ts +28 -0
  31. package/dist/analysis/maven-version.d.ts.map +1 -0
  32. package/dist/analysis/maven-version.js +172 -0
  33. package/dist/analysis/maven-version.js.map +1 -0
  34. package/dist/analysis/passes/constant-propagation-pass.d.ts.map +1 -1
  35. package/dist/analysis/passes/constant-propagation-pass.js +29 -1
  36. package/dist/analysis/passes/constant-propagation-pass.js.map +1 -1
  37. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  38. package/dist/analysis/passes/cross-file-pass.js +81 -1
  39. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  40. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +7 -0
  41. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  42. package/dist/analysis/passes/csharp-sanitizer-credit.js +44 -1
  43. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  44. package/dist/analysis/passes/insecure-cookie-pass.d.ts +2 -0
  45. package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
  46. package/dist/analysis/passes/insecure-cookie-pass.js +25 -3
  47. package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
  48. package/dist/analysis/passes/interprocedural-pass.d.ts +11 -14
  49. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  50. package/dist/analysis/passes/interprocedural-pass.js +562 -0
  51. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  52. package/dist/analysis/passes/language-sources-pass.d.ts +18 -1
  53. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  54. package/dist/analysis/passes/language-sources-pass.js +1057 -14
  55. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  56. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  57. package/dist/analysis/passes/param-source-flow-gate-pass.js +66 -3
  58. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  59. package/dist/analysis/passes/sink-filter-pass.d.ts +44 -2
  60. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  61. package/dist/analysis/passes/sink-filter-pass.js +690 -12
  62. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  63. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  64. package/dist/analysis/passes/taint-propagation-pass.js +133 -10
  65. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  66. package/dist/analysis/passes/tls-verify-disabled-pass.d.ts +18 -0
  67. package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
  68. package/dist/analysis/passes/tls-verify-disabled-pass.js +161 -0
  69. package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
  70. package/dist/analysis/passes/weak-random-pass.d.ts +6 -0
  71. package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
  72. package/dist/analysis/passes/weak-random-pass.js +29 -0
  73. package/dist/analysis/passes/weak-random-pass.js.map +1 -1
  74. package/dist/analysis/promote-cross-file-flows.d.ts +17 -0
  75. package/dist/analysis/promote-cross-file-flows.d.ts.map +1 -0
  76. package/dist/analysis/promote-cross-file-flows.js +32 -0
  77. package/dist/analysis/promote-cross-file-flows.js.map +1 -0
  78. package/dist/analysis/sbom.d.ts +100 -0
  79. package/dist/analysis/sbom.d.ts.map +1 -1
  80. package/dist/analysis/sbom.js +514 -3
  81. package/dist/analysis/sbom.js.map +1 -1
  82. package/dist/analysis/semver-version.d.ts +30 -0
  83. package/dist/analysis/semver-version.d.ts.map +1 -0
  84. package/dist/analysis/semver-version.js +76 -0
  85. package/dist/analysis/semver-version.js.map +1 -0
  86. package/dist/analysis/taint-matcher.d.ts +5 -0
  87. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  88. package/dist/analysis/taint-matcher.js +225 -4
  89. package/dist/analysis/taint-matcher.js.map +1 -1
  90. package/dist/analyzer.d.ts +57 -0
  91. package/dist/analyzer.d.ts.map +1 -1
  92. package/dist/analyzer.js +69 -6
  93. package/dist/analyzer.js.map +1 -1
  94. package/dist/browser/circle-ir.js +2936 -291
  95. package/dist/core/circle-ir-core.cjs +634 -203
  96. package/dist/core/circle-ir-core.js +634 -203
  97. package/dist/core/extractors/dfg.js +205 -2
  98. package/dist/core/extractors/dfg.js.map +1 -1
  99. package/dist/core/extractors/ts-declaration-detail.d.ts +39 -0
  100. package/dist/core/extractors/ts-declaration-detail.d.ts.map +1 -0
  101. package/dist/core/extractors/ts-declaration-detail.js +266 -0
  102. package/dist/core/extractors/ts-declaration-detail.js.map +1 -0
  103. package/dist/graph/analysis-pass.d.ts +4 -1
  104. package/dist/graph/analysis-pass.d.ts.map +1 -1
  105. package/dist/graph/analysis-pass.js +4 -1
  106. package/dist/graph/analysis-pass.js.map +1 -1
  107. package/dist/graph-facts/dead-code.d.ts +62 -0
  108. package/dist/graph-facts/dead-code.d.ts.map +1 -0
  109. package/dist/graph-facts/dead-code.js +90 -0
  110. package/dist/graph-facts/dead-code.js.map +1 -0
  111. package/dist/graph-facts/dependency-calls.d.ts +86 -0
  112. package/dist/graph-facts/dependency-calls.d.ts.map +1 -0
  113. package/dist/graph-facts/dependency-calls.js +93 -0
  114. package/dist/graph-facts/dependency-calls.js.map +1 -0
  115. package/dist/graph-facts/index.d.ts +10 -0
  116. package/dist/graph-facts/index.d.ts.map +1 -0
  117. package/dist/graph-facts/index.js +10 -0
  118. package/dist/graph-facts/index.js.map +1 -0
  119. package/dist/graph-facts/maven-packages.d.ts +42 -0
  120. package/dist/graph-facts/maven-packages.d.ts.map +1 -0
  121. package/dist/graph-facts/maven-packages.js +133 -0
  122. package/dist/graph-facts/maven-packages.js.map +1 -0
  123. package/dist/graph-facts/reach.d.ts +203 -0
  124. package/dist/graph-facts/reach.d.ts.map +1 -0
  125. package/dist/graph-facts/reach.js +219 -0
  126. package/dist/graph-facts/reach.js.map +1 -0
  127. package/dist/graph-facts/removal-reach.d.ts +73 -0
  128. package/dist/graph-facts/removal-reach.d.ts.map +1 -0
  129. package/dist/graph-facts/removal-reach.js +64 -0
  130. package/dist/graph-facts/removal-reach.js.map +1 -0
  131. package/dist/index.d.ts +21 -6
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +20 -3
  134. package/dist/index.js.map +1 -1
  135. package/dist/navigation/go-graph.d.ts +68 -0
  136. package/dist/navigation/go-graph.d.ts.map +1 -0
  137. package/dist/navigation/go-graph.js +753 -0
  138. package/dist/navigation/go-graph.js.map +1 -0
  139. package/dist/navigation/index.d.ts +7 -1
  140. package/dist/navigation/index.d.ts.map +1 -1
  141. package/dist/navigation/index.js +6 -0
  142. package/dist/navigation/index.js.map +1 -1
  143. package/dist/navigation/java-entry-tables.d.ts +43 -0
  144. package/dist/navigation/java-entry-tables.d.ts.map +1 -0
  145. package/dist/navigation/java-entry-tables.js +56 -0
  146. package/dist/navigation/java-entry-tables.js.map +1 -0
  147. package/dist/navigation/java-graph.d.ts +64 -0
  148. package/dist/navigation/java-graph.d.ts.map +1 -0
  149. package/dist/navigation/java-graph.js +466 -0
  150. package/dist/navigation/java-graph.js.map +1 -0
  151. package/dist/navigation/navigation-index.d.ts +51 -1
  152. package/dist/navigation/navigation-index.d.ts.map +1 -1
  153. package/dist/navigation/navigation-index.js +72 -0
  154. package/dist/navigation/navigation-index.js.map +1 -1
  155. package/dist/navigation/python-graph.d.ts +73 -0
  156. package/dist/navigation/python-graph.d.ts.map +1 -0
  157. package/dist/navigation/python-graph.js +867 -0
  158. package/dist/navigation/python-graph.js.map +1 -0
  159. package/dist/navigation/ts-call-edges.d.ts +193 -0
  160. package/dist/navigation/ts-call-edges.d.ts.map +1 -0
  161. package/dist/navigation/ts-call-edges.js +2089 -0
  162. package/dist/navigation/ts-call-edges.js.map +1 -0
  163. package/dist/navigation/ts-entry-tables.d.ts +88 -0
  164. package/dist/navigation/ts-entry-tables.d.ts.map +1 -0
  165. package/dist/navigation/ts-entry-tables.js +50 -0
  166. package/dist/navigation/ts-entry-tables.js.map +1 -0
  167. package/dist/path-set/branches.d.ts +52 -0
  168. package/dist/path-set/branches.d.ts.map +1 -0
  169. package/dist/path-set/branches.js +124 -0
  170. package/dist/path-set/branches.js.map +1 -0
  171. package/dist/path-set/finding-diff.d.ts +63 -0
  172. package/dist/path-set/finding-diff.d.ts.map +1 -0
  173. package/dist/path-set/finding-diff.js +150 -0
  174. package/dist/path-set/finding-diff.js.map +1 -0
  175. package/dist/path-set/index.d.ts +18 -0
  176. package/dist/path-set/index.d.ts.map +1 -0
  177. package/dist/path-set/index.js +12 -0
  178. package/dist/path-set/index.js.map +1 -0
  179. package/dist/path-set/line-diff.d.ts +19 -0
  180. package/dist/path-set/line-diff.d.ts.map +1 -0
  181. package/dist/path-set/line-diff.js +98 -0
  182. package/dist/path-set/line-diff.js.map +1 -0
  183. package/dist/path-set/path-set.d.ts +88 -0
  184. package/dist/path-set/path-set.d.ts.map +1 -0
  185. package/dist/path-set/path-set.js +716 -0
  186. package/dist/path-set/path-set.js.map +1 -0
  187. package/dist/path-set/templates.d.ts +42 -0
  188. package/dist/path-set/templates.d.ts.map +1 -0
  189. package/dist/path-set/templates.js +119 -0
  190. package/dist/path-set/templates.js.map +1 -0
  191. package/dist/path-set/types.d.ts +195 -0
  192. package/dist/path-set/types.d.ts.map +1 -0
  193. package/dist/path-set/types.js +8 -0
  194. package/dist/path-set/types.js.map +1 -0
  195. package/dist/remedy-class/index.d.ts +12 -0
  196. package/dist/remedy-class/index.d.ts.map +1 -0
  197. package/dist/remedy-class/index.js +9 -0
  198. package/dist/remedy-class/index.js.map +1 -0
  199. package/dist/remedy-class/match.d.ts +165 -0
  200. package/dist/remedy-class/match.d.ts.map +1 -0
  201. package/dist/remedy-class/match.js +705 -0
  202. package/dist/remedy-class/match.js.map +1 -0
  203. package/dist/remedy-class/output-context.d.ts +25 -0
  204. package/dist/remedy-class/output-context.d.ts.map +1 -0
  205. package/dist/remedy-class/output-context.js +150 -0
  206. package/dist/remedy-class/output-context.js.map +1 -0
  207. package/dist/remedy-class/sha256.d.ts +18 -0
  208. package/dist/remedy-class/sha256.d.ts.map +1 -0
  209. package/dist/remedy-class/sha256.js +89 -0
  210. package/dist/remedy-class/sha256.js.map +1 -0
  211. package/dist/remedy-class/table.d.ts +130 -0
  212. package/dist/remedy-class/table.d.ts.map +1 -0
  213. package/dist/remedy-class/table.js +457 -0
  214. package/dist/remedy-class/table.js.map +1 -0
  215. package/dist/resolution/cross-file.d.ts +9 -0
  216. package/dist/resolution/cross-file.d.ts.map +1 -1
  217. package/dist/resolution/cross-file.js +60 -5
  218. package/dist/resolution/cross-file.js.map +1 -1
  219. package/dist/stable-ids/builder.d.ts +20 -0
  220. package/dist/stable-ids/builder.d.ts.map +1 -0
  221. package/dist/stable-ids/builder.js +65 -0
  222. package/dist/stable-ids/builder.js.map +1 -0
  223. package/dist/stable-ids/diff.d.ts +18 -0
  224. package/dist/stable-ids/diff.d.ts.map +1 -0
  225. package/dist/stable-ids/diff.js +37 -0
  226. package/dist/stable-ids/diff.js.map +1 -0
  227. package/dist/stable-ids/go.d.ts +28 -0
  228. package/dist/stable-ids/go.d.ts.map +1 -0
  229. package/dist/stable-ids/go.js +160 -0
  230. package/dist/stable-ids/go.js.map +1 -0
  231. package/dist/stable-ids/hash.d.ts +22 -0
  232. package/dist/stable-ids/hash.d.ts.map +1 -0
  233. package/dist/stable-ids/hash.js +167 -0
  234. package/dist/stable-ids/hash.js.map +1 -0
  235. package/dist/stable-ids/index.d.ts +30 -0
  236. package/dist/stable-ids/index.d.ts.map +1 -0
  237. package/dist/stable-ids/index.js +59 -0
  238. package/dist/stable-ids/index.js.map +1 -0
  239. package/dist/stable-ids/java.d.ts +28 -0
  240. package/dist/stable-ids/java.d.ts.map +1 -0
  241. package/dist/stable-ids/java.js +224 -0
  242. package/dist/stable-ids/java.js.map +1 -0
  243. package/dist/stable-ids/moves.d.ts +87 -0
  244. package/dist/stable-ids/moves.d.ts.map +1 -0
  245. package/dist/stable-ids/moves.js +222 -0
  246. package/dist/stable-ids/moves.js.map +1 -0
  247. package/dist/stable-ids/nodes.d.ts +32 -0
  248. package/dist/stable-ids/nodes.d.ts.map +1 -0
  249. package/dist/stable-ids/nodes.js +92 -0
  250. package/dist/stable-ids/nodes.js.map +1 -0
  251. package/dist/stable-ids/normalize.d.ts +35 -0
  252. package/dist/stable-ids/normalize.d.ts.map +1 -0
  253. package/dist/stable-ids/normalize.js +174 -0
  254. package/dist/stable-ids/normalize.js.map +1 -0
  255. package/dist/stable-ids/parse-errors.d.ts +24 -0
  256. package/dist/stable-ids/parse-errors.d.ts.map +1 -0
  257. package/dist/stable-ids/parse-errors.js +46 -0
  258. package/dist/stable-ids/parse-errors.js.map +1 -0
  259. package/dist/stable-ids/python.d.ts +27 -0
  260. package/dist/stable-ids/python.d.ts.map +1 -0
  261. package/dist/stable-ids/python.js +212 -0
  262. package/dist/stable-ids/python.js.map +1 -0
  263. package/dist/stable-ids/ts-parse-errors.d.ts +27 -0
  264. package/dist/stable-ids/ts-parse-errors.d.ts.map +1 -0
  265. package/dist/stable-ids/ts-parse-errors.js +75 -0
  266. package/dist/stable-ids/ts-parse-errors.js.map +1 -0
  267. package/dist/stable-ids/types.d.ts +106 -0
  268. package/dist/stable-ids/types.d.ts.map +1 -0
  269. package/dist/stable-ids/types.js +16 -0
  270. package/dist/stable-ids/types.js.map +1 -0
  271. package/dist/stable-ids/typescript.d.ts +30 -0
  272. package/dist/stable-ids/typescript.d.ts.map +1 -0
  273. package/dist/stable-ids/typescript.js +362 -0
  274. package/dist/stable-ids/typescript.js.map +1 -0
  275. package/dist/types/config.d.ts +9 -0
  276. package/dist/types/config.d.ts.map +1 -1
  277. package/dist/types/index.d.ts +35 -0
  278. package/dist/types/index.d.ts.map +1 -1
  279. package/dist/types/index.js.map +1 -1
  280. package/package.json +3 -3
@@ -20,6 +20,7 @@ import { javaCallArgument, javaCanonicalBaseLacksSeparator } from '../java-canon
20
20
  import { findGoStringsReplacerSanitizers } from '../go-strings-replacer.js';
21
21
  import { findRustLiteralEqualityGuardSanitizers } from '../rust-literal-guard.js';
22
22
  import { constLookupTables, isConstTableLookup } from '../const-table-lookup.js';
23
+ import { jsReplaceChainVerdict } from '../js-crlf-strip.js';
23
24
  // ---------------------------------------------------------------------------
24
25
  // Pattern tables (moved from analyzer.ts)
25
26
  // ---------------------------------------------------------------------------
@@ -298,6 +299,8 @@ export class LanguageSourcesPass {
298
299
  }
299
300
  additionalSanitizers.push(...findBashRegexAllowlistSanitizers(code));
300
301
  additionalSanitizers.push(...findBashRealpathPrefixGuardSanitizers(code));
302
+ // #816 CVE-2026-8398: gpg/sha verify-then-install download scripts.
303
+ additionalSanitizers.push(...findBashVerifiedPackageDownloadSanitizers(code));
301
304
  // #213 twelfth slice: printf '%q' + ${var@Q} shell-quote helpers.
302
305
  additionalSanitizers.push(...findBashShellQuoteSanitizers(code));
303
306
  }
@@ -417,8 +420,14 @@ export class LanguageSourcesPass {
417
420
  }
418
421
  // -- Rust: safe-handler sanitizer detectors (cognium-dev #115 Sprint 31) --
419
422
  if (language === 'rust') {
423
+ additionalSanitizers.push(...findRustConstSliceMembershipGuardSanitizers(code));
420
424
  additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
421
425
  additionalSanitizers.push(...findRustCanonicalizeGuardSanitizers(code));
426
+ // cognium-dev #816 — PAT-INPUT-01 rust safe mirror: parse + range guards
427
+ // before actix/axum response body (same credit model as JS twin).
428
+ additionalSanitizers.push(...findRustNumericRangeCheckGuardSanitizers(code));
429
+ // cognium-dev #816 — PAT-INFO-01: response echoes only query params, no env secrets.
430
+ additionalSanitizers.push(...findRustPatInfoHttpOnlyResponseSanitizers(code));
422
431
  // cognium-dev#293: `if x != "a" && x != "b" { return Err(..) }` literal-equality guard.
423
432
  additionalSanitizers.push(...findRustLiteralEqualityGuardSanitizers(code));
424
433
  // Sprint 77a (#216 Pattern X): argv-form Command::new(literal).arg(...)
@@ -484,6 +493,9 @@ export class LanguageSourcesPass {
484
493
  // #223 REG-144-17/18/19: JS/TS command allowlist guard
485
494
  // (Set.has / Array.includes / indexOf) before execFile/spawn.
486
495
  additionalSanitizers.push(...findJsCommandAllowlistGuardSanitizers(code));
496
+ // cognium-dev #816 — PAT-INPUT-01 js safe mirror: Number/parseInt
497
+ // coercion + finite/range guards before template response.
498
+ additionalSanitizers.push(...findJsNumericRangeCheckGuardSanitizers(code));
487
499
  // Sprint 78 (#190): JS misconfig pattern findings — libxmljs noent:true.
488
500
  for (const finding of findJsPatternFindings(code, graph.ir.meta.file)) {
489
501
  ctx.addFinding(finding);
@@ -517,6 +529,9 @@ export class LanguageSourcesPass {
517
529
  for (const finding of findJsLdapInjectionFindings(code, graph.ir.meta.file)) {
518
530
  ctx.addFinding(finding);
519
531
  }
532
+ for (const finding of findJsZipHeaderFailopenFindings(code, graph.ir.meta.file)) {
533
+ ctx.addFinding(finding);
534
+ }
520
535
  // Sprint 89 (#189): JS insecure_deserialization —
521
536
  // JSON.parse(req.body) on raw / text bodies.
522
537
  for (const finding of findJsJsonParseBodyFindings(code, graph.ir.meta.file)) {
@@ -576,6 +591,9 @@ export class LanguageSourcesPass {
576
591
  additionalSanitizers.push(...findJavaInlineCrlfStripLogSanitizers(code));
577
592
  // cognium-dev#293: the same strip, but living in a user-defined helper.
578
593
  additionalSanitizers.push(...findCrlfWrapperFunctionSanitizers(code, 'java'));
594
+ // cognium-dev#947: flow-sanitize ssti custom_wrapper — brace-stripping
595
+ // helper at Velocity.evaluate call site.
596
+ additionalSanitizers.push(...findJavaTemplateDelimiterWrapperSanitizers(code));
579
597
  // Sprint 77a (#216 Pattern X): argv-form exec sanitizer.
580
598
  additionalSanitizers.push(...findJavaArgvFormExecSanitizers(code));
581
599
  // Sprint 78 (#190): Java misconfig pattern findings —
@@ -2851,6 +2869,171 @@ function computeDeadPythonBranchLines(lines) {
2851
2869
  }
2852
2870
  return dead;
2853
2871
  }
2872
+ /**
2873
+ * Line indices (0-based) in unreachable `if`/`else` arms when the condition is
2874
+ * decidable from tracked integer locals (`int num = 86; if ((7*42)-num > 200) …`).
2875
+ *
2876
+ * Targets OWASP Benchmark shapes where the else arm assigns a parameter but
2877
+ * can never run (cognium-dev#913). Braced bodies defer to constant propagation;
2878
+ * this pass handles the common single-statement `if` / `else` lines.
2879
+ */
2880
+ function parseJavaSwitchCaseLabel(label) {
2881
+ const t = label.trim();
2882
+ if (t === 'default')
2883
+ return 'default';
2884
+ const ch = t.match(/^'(\\.|[^'])'\s*$/);
2885
+ if (ch) {
2886
+ const inner = ch[1];
2887
+ if (inner.startsWith('\\')) {
2888
+ if (inner === "\\'")
2889
+ return 39;
2890
+ if (inner === '\\\\')
2891
+ return 92;
2892
+ if (inner === '\\n')
2893
+ return 10;
2894
+ return undefined;
2895
+ }
2896
+ return inner.charCodeAt(0);
2897
+ }
2898
+ const num = t.match(/^(-?\d+)\s*$/);
2899
+ if (num)
2900
+ return Number(num[1]);
2901
+ return undefined;
2902
+ }
2903
+ function parseJavaSwitchLabelLine(line) {
2904
+ const caseLine = line.match(/^\s*case\s+(.+?)\s*:\s*$/);
2905
+ if (caseLine)
2906
+ return parseJavaSwitchCaseLabel(caseLine[1]);
2907
+ if (/^\s*default\s*:\s*$/.test(line))
2908
+ return 'default';
2909
+ return undefined;
2910
+ }
2911
+ function markDeadJavaSwitchBodies(lines, switchLineIdx, switchValue, dead) {
2912
+ let depth = 0;
2913
+ let started = false;
2914
+ let groupLabels = [];
2915
+ let bodyStart = -1;
2916
+ let explicitCaseMatched = false;
2917
+ const groupIsTaken = () => {
2918
+ if (groupLabels.length === 0)
2919
+ return false;
2920
+ const explicit = groupLabels.some(v => v !== 'default' && v === switchValue);
2921
+ if (explicit)
2922
+ return true;
2923
+ return groupLabels.includes('default') && !explicitCaseMatched;
2924
+ };
2925
+ const flushGroup = (bodyEnd) => {
2926
+ if (bodyStart < 0 || groupLabels.length === 0)
2927
+ return;
2928
+ const taken = groupIsTaken();
2929
+ if (groupLabels.some(v => v !== 'default' && v === switchValue)) {
2930
+ explicitCaseMatched = true;
2931
+ }
2932
+ if (!taken) {
2933
+ for (let k = bodyStart; k < bodyEnd; k++)
2934
+ dead.add(k);
2935
+ }
2936
+ groupLabels = [];
2937
+ bodyStart = -1;
2938
+ };
2939
+ for (let j = switchLineIdx; j < lines.length; j++) {
2940
+ const line = lines[j];
2941
+ for (const ch of line) {
2942
+ if (ch === '{') {
2943
+ depth++;
2944
+ started = true;
2945
+ }
2946
+ else if (ch === '}')
2947
+ depth--;
2948
+ }
2949
+ if (!started)
2950
+ continue;
2951
+ const label = parseJavaSwitchLabelLine(line);
2952
+ if (label !== undefined) {
2953
+ if (bodyStart >= 0 && j === bodyStart) {
2954
+ groupLabels.push(label);
2955
+ bodyStart = j + 1;
2956
+ }
2957
+ else {
2958
+ flushGroup(j);
2959
+ groupLabels = [label];
2960
+ bodyStart = j + 1;
2961
+ }
2962
+ }
2963
+ if (started && depth === 0) {
2964
+ flushGroup(j);
2965
+ break;
2966
+ }
2967
+ }
2968
+ }
2969
+ export function computeDeadJavaBranchLines(lines) {
2970
+ const dead = new Set();
2971
+ const consts = new Map();
2972
+ const stringConsts = new Map();
2973
+ const charConsts = new Map();
2974
+ for (let i = 0; i < lines.length; i++) {
2975
+ const line = lines[i];
2976
+ const stringAssign = line.match(/^\s*String\s+([A-Za-z_]\w*)\s*=\s*"([^"]*)"\s*;\s*$/);
2977
+ if (stringAssign) {
2978
+ stringConsts.set(stringAssign[1], stringAssign[2]);
2979
+ continue;
2980
+ }
2981
+ const charAtAssign = line.match(/^\s*char\s+([A-Za-z_]\w*)\s*=\s*([A-Za-z_]\w*)\.charAt\s*\(\s*(\d+)\s*\)\s*;\s*(?:\/\/.*)?$/);
2982
+ if (charAtAssign) {
2983
+ const str = stringConsts.get(charAtAssign[2]);
2984
+ const idx = Number(charAtAssign[3]);
2985
+ if (str !== undefined && idx >= 0 && idx < str.length) {
2986
+ charConsts.set(charAtAssign[1], str.charCodeAt(idx));
2987
+ }
2988
+ else {
2989
+ charConsts.delete(charAtAssign[1]);
2990
+ }
2991
+ continue;
2992
+ }
2993
+ const intAssign = line.match(/^\s*(?:int|long|short|byte)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/);
2994
+ if (intAssign) {
2995
+ const v = evaluatePythonConstExpression(intAssign[2].trim(), consts);
2996
+ if (typeof v === 'number')
2997
+ consts.set(intAssign[1], v);
2998
+ else
2999
+ consts.delete(intAssign[1]);
3000
+ continue;
3001
+ }
3002
+ const switchMatch = line.match(/^\s*switch\s*\(([^)]+)\)\s*\{?\s*$/);
3003
+ if (switchMatch) {
3004
+ const sel = switchMatch[1].trim();
3005
+ let switchVal = charConsts.get(sel);
3006
+ if (switchVal === undefined) {
3007
+ const n = evaluatePythonConstExpression(sel, consts);
3008
+ if (typeof n === 'number')
3009
+ switchVal = n;
3010
+ }
3011
+ if (switchVal !== undefined) {
3012
+ markDeadJavaSwitchBodies(lines, i, switchVal, dead);
3013
+ }
3014
+ continue;
3015
+ }
3016
+ const ifMatch = line.match(/^(\s*)if\s*\((.+)\)\s+(.+)$/);
3017
+ if (!ifMatch)
3018
+ continue;
3019
+ const decided = evaluatePythonConstExpression(ifMatch[2].trim(), consts);
3020
+ if (decided !== true && decided !== false)
3021
+ continue;
3022
+ const rest = ifMatch[3].trim();
3023
+ if (/\belse\b/.test(rest))
3024
+ continue;
3025
+ if (i + 1 < lines.length && /^\s*else\b/.test(lines[i + 1])) {
3026
+ if (decided === true)
3027
+ dead.add(i + 1);
3028
+ else
3029
+ dead.add(i);
3030
+ }
3031
+ else if (decided === false) {
3032
+ dead.add(i);
3033
+ }
3034
+ }
3035
+ return dead;
3036
+ }
2854
3037
  export function buildPythonTaintedVars(sourceCode) {
2855
3038
  const tainted = new Map();
2856
3039
  const containerTainted = new Map();
@@ -3916,6 +4099,113 @@ export function buildRustTaintedVars(sourceCode, seedVars) {
3916
4099
  * on the way (a lambda, anonymous class, switch expression or block is not a
3917
4100
  * plain wrapped value).
3918
4101
  */
4102
+ /** `code(); // note` → `code();` — a `//` inside a string or char literal is kept. */
4103
+ function stripJavaTrailingLineComment(line) {
4104
+ let quote = null;
4105
+ for (let i = 0; i < line.length; i++) {
4106
+ const c = line[i];
4107
+ if (quote) {
4108
+ if (c === '\\')
4109
+ i++;
4110
+ else if (c === quote)
4111
+ quote = null;
4112
+ }
4113
+ else if (c === '"' || c === "'") {
4114
+ quote = c;
4115
+ }
4116
+ else if (c === '/' && line[i + 1] === '/') {
4117
+ return line.slice(0, i).trimEnd();
4118
+ }
4119
+ }
4120
+ return line;
4121
+ }
4122
+ /** Identifiers at code positions in a Java expression (string and char literal bodies dropped). */
4123
+ function javaExpressionIdents(expr) {
4124
+ const code = expr.replace(/"(?:[^"\\]|\\.)*"/g, ' ').replace(/'(?:[^'\\]|\\.)'/g, ' ');
4125
+ return code.match(/[\p{L}\p{N}_]+/gu) ?? [];
4126
+ }
4127
+ /**
4128
+ * `buildJavaTaintedVars` slot tracking (cognium-dev#913/#914/#916/#917). Records
4129
+ * `list.add(x)` / `list.remove(<int>)` / `map.put("<key>", x)` into the slot
4130
+ * tables and returns true for those statements. An `int` constant declaration
4131
+ * is recorded for ternary folding but returns false, so the line is still read
4132
+ * as an assignment.
4133
+ */
4134
+ function recordJavaSlotStatement(line, listElems, mapSlots, intConsts) {
4135
+ const t = line.trimEnd().replace(/;\s*\/\/[^"]*$/, ';');
4136
+ const intDecl = /^\s*(?:final\s+)?(?:int|long|short|byte)\s+([A-Za-z_]\w*)\s*=\s*([^;]+);\s*$/.exec(t);
4137
+ if (intDecl) {
4138
+ const v = evaluatePythonConstExpression(intDecl[2].trim(), intConsts);
4139
+ if (typeof v === 'number')
4140
+ intConsts.set(intDecl[1], v);
4141
+ else
4142
+ intConsts.delete(intDecl[1]);
4143
+ return false;
4144
+ }
4145
+ const add = /^\s*([A-Za-z_]\w*)\.add\s*\(\s*([A-Za-z_]\w*|"(?:[^"\\]|\\.)*")\s*\)\s*;\s*$/.exec(t);
4146
+ if (add) {
4147
+ const elems = listElems.get(add[1]) ?? [];
4148
+ elems.push(add[2].startsWith('"') ? null : add[2]);
4149
+ listElems.set(add[1], elems);
4150
+ return true;
4151
+ }
4152
+ const remove = /^\s*([A-Za-z_]\w*)\.remove\s*\(\s*(\d+)\s*\)\s*;\s*$/.exec(t);
4153
+ if (remove && listElems.has(remove[1])) {
4154
+ const elems = listElems.get(remove[1]);
4155
+ const idx = Number(remove[2]);
4156
+ if (idx < elems.length)
4157
+ elems.splice(idx, 1);
4158
+ else
4159
+ listElems.delete(remove[1]); // an index we did not see added: stop trusting the slots
4160
+ return true;
4161
+ }
4162
+ const put = /^\s*([A-Za-z_]\w*)\.put\s*\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*(.+?)\s*\)\s*;\s*$/.exec(t);
4163
+ if (put) {
4164
+ const keys = mapSlots.get(put[1]) ?? new Map();
4165
+ keys.set(put[2], javaExpressionIdents(put[3]));
4166
+ mapSlots.set(put[1], keys);
4167
+ return true;
4168
+ }
4169
+ // Any other use of a tracked collection that is not a plain read (passed to a
4170
+ // call, `addAll`, `set`, `clear`, a non-literal `put`, re-declared …) may change
4171
+ // its contents in a way the tables do not model: stop trusting its slots.
4172
+ for (const table of [listElems, mapSlots]) {
4173
+ for (const name of [...table.keys()]) {
4174
+ const mention = new RegExp(`(?<![\\w.])${name}(?!\\w)`);
4175
+ if (!mention.test(t))
4176
+ continue;
4177
+ const reads = new RegExp(`(?<![\\w.])${name}\\.(?:get|size|isEmpty|contains\\w*)\\s*\\(`, 'g');
4178
+ if (mention.test(t.replace(reads, ' ')))
4179
+ table.delete(name);
4180
+ }
4181
+ }
4182
+ return false;
4183
+ }
4184
+ /**
4185
+ * The identifiers a `list.get(<int>)` / `map.get("<key>")` read (optionally
4186
+ * cast) can carry, from the slot tables; `undefined` when the RHS is not such a
4187
+ * read or the slots are unknown, so the caller falls back to whole-RHS matching.
4188
+ */
4189
+ function javaSlotRead(rhs, listElems, mapSlots) {
4190
+ const m = /^(?:\(\s*[A-Za-z_][\w.<>]*\s*\)\s*)?([A-Za-z_]\w*)\.get\s*\(\s*(\d+|"(?:[^"\\]|\\.)*")\s*\)$/.exec(rhs.trim());
4191
+ if (!m)
4192
+ return undefined;
4193
+ const [, recv, arg] = m;
4194
+ if (arg.startsWith('"')) {
4195
+ const keys = mapSlots.get(recv);
4196
+ if (!keys)
4197
+ return undefined;
4198
+ return keys.get(arg.slice(1, -1)) ?? [];
4199
+ }
4200
+ const elems = listElems.get(recv);
4201
+ if (!elems)
4202
+ return undefined;
4203
+ const idx = Number(arg);
4204
+ if (idx >= elems.length)
4205
+ return undefined;
4206
+ const slot = elems[idx];
4207
+ return slot === null ? [] : [slot];
4208
+ }
3919
4209
  function joinWrappedJavaStatement(lines, i) {
3920
4210
  const strip = (l) => l.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)'/g, '""').replace(/\/\/.*$/, '');
3921
4211
  const first = strip(lines[i]);
@@ -3962,10 +4252,30 @@ function joinWrappedJavaStatement(lines, i) {
3962
4252
  * consumed by `detectExpressionScanFlows` in `taint-propagation-pass.ts`
3963
4253
  * to synthesize a virtual source for each derived var.
3964
4254
  */
3965
- export function buildJavaTaintedVars(sourceCode, seedVars) {
4255
+ export function buildJavaTaintedVars(sourceCode, seedVars, deadBranchLines,
4256
+ /**
4257
+ * `trackSlots` (cognium-dev#913/#914/#916/#917): follow which element of a list
4258
+ * and which literal key of a map holds taint, and pick the branch of a ternary
4259
+ * whose condition folds to a constant int comparison. Only the helper-return
4260
+ * gate in InterproceduralPass asks for it; every other caller keeps the
4261
+ * whole-collection behaviour.
4262
+ */
4263
+ options) {
3966
4264
  const derived = new Map();
3967
4265
  const knownTainted = new Set(seedVars);
3968
- const lines = sourceCode.split('\n');
4266
+ const trackSlots = options?.trackSlots === true;
4267
+ // Slot mode also reads `x = list.get(0); // note` as the statement it is: the
4268
+ // assignment patterns below are anchored at `;`.
4269
+ const lines = trackSlots
4270
+ ? sourceCode.split('\n').map(stripJavaTrailingLineComment)
4271
+ : sourceCode.split('\n');
4272
+ const dead = deadBranchLines ?? new Set();
4273
+ /** list var → element slots: the added identifier, or `null` for a literal. */
4274
+ const listElems = new Map();
4275
+ /** map var → literal key → the identifiers its stored value mentions. */
4276
+ const mapSlots = new Map();
4277
+ /** int locals with a constant value, for folding a ternary condition. */
4278
+ const intConsts = new Map();
3969
4279
  // Declaration: [modifiers] Type[<...>] [ ] lhs = rhs;
3970
4280
  // Simple assignment: lhs = rhs;
3971
4281
  // The declaration form is tried first because a Type token in front
@@ -3980,6 +4290,8 @@ export function buildJavaTaintedVars(sourceCode, seedVars) {
3980
4290
  // original var directly.
3981
4291
  const declRe = /^\s*(?:public|private|protected|static|final|volatile|transient|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
3982
4292
  const assignRe = /^\s*([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
4293
+ const thisFieldAssignRe = /^\s*this\.([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
4294
+ const beanSetterCallRe = /\bset([A-Z]\w*)\s*\((.*)\)\s*;?\s*$/;
3983
4295
  const JAVA_KEYWORDS = new Set([
3984
4296
  'if', 'else', 'while', 'for', 'do', 'switch', 'case', 'return',
3985
4297
  'throw', 'try', 'catch', 'finally', 'new', 'this', 'super',
@@ -4003,11 +4315,28 @@ export function buildJavaTaintedVars(sourceCode, seedVars) {
4003
4315
  while (changed && guard < lines.length + 2) {
4004
4316
  changed = false;
4005
4317
  guard++;
4318
+ // Slot state is rebuilt on every pass: it describes the code top to bottom,
4319
+ // while `knownTainted` only grows.
4320
+ listElems.clear();
4321
+ mapSlots.clear();
4322
+ intConsts.clear();
4006
4323
  for (let i = 0; i < lines.length; i++) {
4324
+ if (deadBranchLines !== undefined && dead.has(i))
4325
+ continue;
4007
4326
  const line = lines[i];
4008
4327
  const trimmed = line.trimStart();
4009
4328
  if (trimmed.startsWith('//') || trimmed.startsWith('*'))
4010
4329
  continue;
4330
+ if (trackSlots && recordJavaSlotStatement(line, listElems, mapSlots, intConsts)) {
4331
+ // `list.add(x)` still taints the list as a whole, as below.
4332
+ const add = /^\s*([A-Za-z_]\w*)\.add\s*\(\s*([A-Za-z_]\w*)\s*\)\s*;/.exec(line);
4333
+ if (add && knownTainted.has(add[2]) && !knownTainted.has(add[1])) {
4334
+ derived.set(add[1], i + 1);
4335
+ knownTainted.add(add[1]);
4336
+ changed = true;
4337
+ }
4338
+ continue;
4339
+ }
4011
4340
  // cognium-dev#476: non-assignment statements (must run before the #753
4012
4341
  // wrap-join early-continue, which would skip them).
4013
4342
  const arrayCopyRe = /\bSystem\.arraycopy\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^,]+\s*,\s*([A-Za-z_]\w*)\s*,/;
@@ -4042,21 +4371,92 @@ export function buildJavaTaintedVars(sourceCode, seedVars) {
4042
4371
  // statement whose call argument list wraps over several lines reads like
4043
4372
  // the same statement on one line.
4044
4373
  const joined = joinWrappedJavaStatement(lines, i);
4045
- if (joined === null)
4046
- continue;
4047
- statement = joined;
4048
- declMatch = declRe.exec(statement);
4049
- assignMatch = !declMatch ? assignRe.exec(statement) : null;
4374
+ if (joined !== null) {
4375
+ statement = joined;
4376
+ declMatch = declRe.exec(statement);
4377
+ assignMatch = !declMatch ? assignRe.exec(statement) : null;
4378
+ }
4379
+ }
4380
+ const thisFieldMatch = thisFieldAssignRe.exec(statement.trim());
4381
+ if (thisFieldMatch) {
4382
+ const fieldName = thisFieldMatch[1];
4383
+ const rhs = thisFieldMatch[2];
4384
+ const rhsCode = rhs
4385
+ .replace(/\$@?"(?:[^"\\]|\\.)*"/g, lit => ' ' + [...lit.matchAll(/\{([^}]*)\}/g)].map(x => x[1]).join(' ') + ' ')
4386
+ .replace(/@?"(?:[^"\\]|\\.)*"/g, ' ')
4387
+ .replace(/'(?:[^'\\]|\\.)'/g, ' ');
4388
+ const rhsIdents = rhsCode.match(/[\p{L}\p{N}_]+/gu);
4389
+ const ref = (rhsIdents !== null && rhsIdents.some(t => knownTainted.has(t))) ||
4390
+ (complexTaintedRes.length > 0 && complexTaintedRes.some(re => re.test(rhsCode)));
4391
+ if (ref) {
4392
+ const thisField = `this.${fieldName}`;
4393
+ if (!knownTainted.has(thisField)) {
4394
+ derived.set(thisField, i + 1);
4395
+ knownTainted.add(thisField);
4396
+ changed = true;
4397
+ }
4398
+ if (!knownTainted.has(fieldName)) {
4399
+ derived.set(fieldName, i + 1);
4400
+ knownTainted.add(fieldName);
4401
+ changed = true;
4402
+ }
4403
+ }
4404
+ continue;
4405
+ }
4406
+ const setterCall = beanSetterCallRe.exec(statement);
4407
+ if (setterCall) {
4408
+ const fieldName = setterCall[1].charAt(0).toLowerCase() + setterCall[1].slice(1);
4409
+ const arg = setterCall[2];
4410
+ const rhsCode = arg
4411
+ .replace(/\$@?"(?:[^"\\]|\\.)*"/g, lit => ' ' + [...lit.matchAll(/\{([^}]*)\}/g)].map(x => x[1]).join(' ') + ' ')
4412
+ .replace(/@?"(?:[^"\\]|\\.)*"/g, ' ')
4413
+ .replace(/'(?:[^'\\]|\\.)'/g, ' ');
4414
+ const rhsIdents = rhsCode.match(/[\p{L}\p{N}_]+/gu);
4415
+ const ref = (rhsIdents !== null && rhsIdents.some(t => knownTainted.has(t))) ||
4416
+ (complexTaintedRes.length > 0 && complexTaintedRes.some(re => re.test(rhsCode)));
4417
+ if (ref) {
4418
+ const thisField = `this.${fieldName}`;
4419
+ if (!knownTainted.has(thisField)) {
4420
+ derived.set(thisField, i + 1);
4421
+ knownTainted.add(thisField);
4422
+ changed = true;
4423
+ }
4424
+ if (!knownTainted.has(fieldName)) {
4425
+ derived.set(fieldName, i + 1);
4426
+ knownTainted.add(fieldName);
4427
+ changed = true;
4428
+ }
4429
+ }
4430
+ continue;
4050
4431
  }
4051
4432
  const m = declMatch ?? assignMatch;
4052
4433
  if (!m)
4053
4434
  continue;
4054
4435
  const lhs = m[1];
4055
- const rhs = m[2];
4436
+ let rhs = m[2];
4056
4437
  if (JAVA_KEYWORDS.has(lhs))
4057
4438
  continue;
4058
4439
  if (knownTainted.has(lhs))
4059
4440
  continue;
4441
+ if (trackSlots) {
4442
+ const slot = javaSlotRead(rhs, listElems, mapSlots);
4443
+ if (slot !== undefined) {
4444
+ if (slot.some(t => knownTainted.has(t))) {
4445
+ derived.set(lhs, i + 1);
4446
+ knownTainted.add(lhs);
4447
+ changed = true;
4448
+ }
4449
+ continue;
4450
+ }
4451
+ const tern = /^([^?]+?)\s*\?\s*([^:]+?)\s*:\s*(.+)$/.exec(rhs.trim());
4452
+ if (tern) {
4453
+ const decided = evaluatePythonConstExpression(tern[1].trim().replace(/^\((.*)\)$/, '$1'), intConsts);
4454
+ if (decided === true)
4455
+ rhs = tern[2];
4456
+ else if (decided === false)
4457
+ rhs = tern[3];
4458
+ }
4459
+ }
4060
4460
  // Reduce the RHS to code positions before matching a tainted var name:
4061
4461
  // keep `$"…{expr}"` interpolation contents, drop plain / verbatim string
4062
4462
  // literal bodies and char literals. Otherwise a tainted var NAME that
@@ -4152,8 +4552,15 @@ function findBashTaintSources(sourceCode, dfg) {
4152
4552
  // Suppressed inside function bodies (#73 part 1).
4153
4553
  if (!insideFunction) {
4154
4554
  const positionalRe = /\$([1-9@*])|\$\{([1-9@*])\}/g;
4555
+ // A one-line definition `name() { …; }` keeps its body on the header
4556
+ // line: only text before the opening brace is at the top level.
4557
+ const header = /^\s*(?:function\s+)?[A-Za-z_][\w-]*\s*(?:\(\s*\))?\s*\{/.exec(line);
4558
+ const isFnHeader = header !== null && (/\(\s*\)/.test(header[0]) || /^\s*function\s/.test(header[0]));
4559
+ // `$1` inside single quotes is literal text (an awk or sed program), not
4560
+ // a positional parameter.
4561
+ const topLevelText = (isFnHeader ? line.slice(0, header[0].length) : line).replace(/'[^']*'/g, "''");
4155
4562
  let m;
4156
- while ((m = positionalRe.exec(line)) !== null) {
4563
+ while ((m = positionalRe.exec(topLevelText)) !== null) {
4157
4564
  const param = m[1] ?? m[2];
4158
4565
  const alreadyExists = sources.some(s => s.line === lineNumber && s.variable === param);
4159
4566
  if (!alreadyExists) {
@@ -4893,6 +5300,39 @@ function findBashRealpathPrefixGuardSanitizers(code) {
4893
5300
  }
4894
5301
  return sanitizers;
4895
5302
  }
5303
+ /**
5304
+ * cognium-dev #816 (CVE-2026-8398 `daemon_pkg_install` safe mirror): when a
5305
+ * script downloads a package artifact with curl/wget and later verifies
5306
+ * integrity (gpg/sha) before `dpkg`/`rpm` install, credit the download
5307
+ * step's outbound fetch as gated — same whole-script idiom as
5308
+ * `hasIntegrityVerifierAnywhere` for `unverified-package-install`.
5309
+ */
5310
+ function findBashVerifiedPackageDownloadSanitizers(code) {
5311
+ const lines = code.split('\n');
5312
+ if (!hasIntegrityVerifierAnywhere(lines)) {
5313
+ return [];
5314
+ }
5315
+ const hasDownload = lines.some((l) => /\b(curl|wget)\b/.test(l));
5316
+ const hasInstall = lines.some((l) => matchUnverifiedPackageInstall(l.trim()) !== null);
5317
+ if (!hasDownload || !hasInstall) {
5318
+ return [];
5319
+ }
5320
+ const sanitizers = [];
5321
+ for (let i = 0; i < lines.length; i++) {
5322
+ const line = lines[i];
5323
+ if (line.trimStart().startsWith('#'))
5324
+ continue;
5325
+ if (!/\b(curl|wget)\b/.test(line))
5326
+ continue;
5327
+ sanitizers.push({
5328
+ type: 'verified_package_download',
5329
+ method: 'curl',
5330
+ line: i + 1,
5331
+ sanitizes: ['ssrf'],
5332
+ });
5333
+ }
5334
+ return sanitizers;
5335
+ }
4896
5336
  // ---------------------------------------------------------------------------
4897
5337
  // Go safe-handler sanitizer detectors (cognium-dev #102 Sprint 24)
4898
5338
  // ---------------------------------------------------------------------------
@@ -5194,6 +5634,208 @@ function findPythonNetlocAllowlistGuardSanitizers(code) {
5194
5634
  * or UPPER_SNAKE constants (e.g. MAX_QTY). The guard body must contain
5195
5635
  * a terminator.
5196
5636
  */
5637
+ /**
5638
+ * JS/TS numeric range-check guards on Number/parseInt-coerced values
5639
+ * (cognium-dev #816, PAT-INPUT-01 safe mirror; Python twin #114).
5640
+ *
5641
+ * const qty = parseInt(req.query.qty, 10);
5642
+ * if (!Number.isFinite(qty) || qty <= 0) return res.status(400).end();
5643
+ * if (qty > MAX) return res.status(400).end();
5644
+ * res.end(`xfer ${qty}`);
5645
+ *
5646
+ * Coercion strips xss in DEFAULT_SANITIZERS but template interpolation
5647
+ * re-taints; a proven finite + bounded range guard shows the emitted
5648
+ * scalar cannot carry markup.
5649
+ */
5650
+ function findJsNumericRangeCheckGuardSanitizers(code) {
5651
+ const sanitizers = [];
5652
+ const lines = code.split('\n');
5653
+ const coerceDecl = /^\s*(?:const|let|var)\s+([A-Za-z_]\w*)\s*=\s*(?:Number|parseInt)\s*\(/;
5654
+ const lowerGuard = /\bif\s*\(\s*!(?:Number\.)?isFinite\s*\(\s*([A-Za-z_]\w*)\s*\)|\bif\s*\([^)]*\b([A-Za-z_]\w*)\s*<=\s*0/;
5655
+ const upperGuard = /\bif\s*\(\s*([A-Za-z_]\w*)\s*>\s*(?:\d+|[A-Z][A-Z0-9_]+)\s*\)/;
5656
+ const terminator = /\b(?:return|throw|res\s*\.\s*status\s*\([^)]*\)\s*\.\s*(?:send|end|json))/;
5657
+ const coerced = new Map();
5658
+ for (let i = 0; i < lines.length; i++) {
5659
+ const m = coerceDecl.exec(lines[i]);
5660
+ if (m)
5661
+ coerced.set(m[1], i);
5662
+ }
5663
+ if (coerced.size === 0)
5664
+ return sanitizers;
5665
+ const state = new Map();
5666
+ for (let i = 0; i < lines.length; i++) {
5667
+ const line = lines[i];
5668
+ let varName = null;
5669
+ let kind = null;
5670
+ const low = lowerGuard.exec(line);
5671
+ if (low) {
5672
+ varName = low[1] ?? low[2];
5673
+ kind = 'lower';
5674
+ }
5675
+ else {
5676
+ const up = upperGuard.exec(line);
5677
+ if (up) {
5678
+ varName = up[1];
5679
+ kind = 'upper';
5680
+ }
5681
+ }
5682
+ if (!varName || !kind || !coerced.has(varName))
5683
+ continue;
5684
+ if (!terminator.test(line)) {
5685
+ let bodyHasTerminator = false;
5686
+ let blockEnd = i;
5687
+ let braceDepth = 0;
5688
+ let started = false;
5689
+ const maxScan = Math.min(lines.length, i + 26);
5690
+ for (let j = i; j < maxScan; j++) {
5691
+ const ln = lines[j];
5692
+ for (const ch of ln) {
5693
+ if (ch === '{') {
5694
+ braceDepth++;
5695
+ started = true;
5696
+ }
5697
+ else if (ch === '}') {
5698
+ braceDepth--;
5699
+ if (started && braceDepth === 0) {
5700
+ blockEnd = j;
5701
+ break;
5702
+ }
5703
+ }
5704
+ }
5705
+ if (j > i && terminator.test(ln))
5706
+ bodyHasTerminator = true;
5707
+ if (started && braceDepth === 0 && blockEnd !== i)
5708
+ break;
5709
+ }
5710
+ if (!bodyHasTerminator && !started)
5711
+ continue;
5712
+ if (!bodyHasTerminator)
5713
+ continue;
5714
+ }
5715
+ const st = state.get(varName) ?? { lower: false, upper: false, lastGuardEnd: i };
5716
+ if (kind === 'lower')
5717
+ st.lower = true;
5718
+ else
5719
+ st.upper = true;
5720
+ st.lastGuardEnd = i;
5721
+ state.set(varName, st);
5722
+ }
5723
+ for (const [varName, st] of state) {
5724
+ if (!st.lower || !st.upper)
5725
+ continue;
5726
+ const varRefRe = new RegExp(`\\b${varName}\\b`);
5727
+ for (let l = st.lastGuardEnd + 1; l < lines.length; l++) {
5728
+ if (!varRefRe.test(lines[l]))
5729
+ continue;
5730
+ sanitizers.push({
5731
+ type: 'js_numeric_range_check_guard',
5732
+ method: 'if',
5733
+ line: l + 1,
5734
+ sanitizes: ['xss', 'external_taint_escape'],
5735
+ });
5736
+ }
5737
+ }
5738
+ return sanitizers;
5739
+ }
5740
+ /**
5741
+ * Rust numeric range-check guards on `.parse()`-coerced values
5742
+ * (cognium-dev #816, PAT-INPUT-01 safe mirror).
5743
+ *
5744
+ * let amount: i32 = amount_str.parse().unwrap_or(0);
5745
+ * if amount <= 0 { return HttpResponse::BadRequest().finish(); }
5746
+ * if amount > MAX { return HttpResponse::BadRequest().finish(); }
5747
+ * HttpResponse::Ok().body(format!("xfer {}", amount))
5748
+ */
5749
+ function findRustNumericRangeCheckGuardSanitizers(code) {
5750
+ const sanitizers = [];
5751
+ const lines = code.split('\n');
5752
+ const coerceDecl = /^\s*let\s+([A-Za-z_]\w*)\s*(?::\s*(?:i\d+|u\d+|isize|usize))?\s*=\s*[^;]*\.parse\s*(?:\(\s*\)|::<)/;
5753
+ const lowerGuard = /\bif\s+([A-Za-z_]\w*)\s*<=\s*0\b/;
5754
+ const upperGuard = /\bif\s+([A-Za-z_]\w*)\s*>\s*(?:\d+|[A-Z][A-Z0-9_]+)\b/;
5755
+ const terminator = /\b(?:return\s+HttpResponse::|return\s+StatusCode::|return\s+Err\s*\(|panic!\s*\()/;
5756
+ const coerced = new Map();
5757
+ for (let i = 0; i < lines.length; i++) {
5758
+ const m = coerceDecl.exec(lines[i]);
5759
+ if (m)
5760
+ coerced.set(m[1], i);
5761
+ }
5762
+ if (coerced.size === 0)
5763
+ return sanitizers;
5764
+ const state = new Map();
5765
+ for (let i = 0; i < lines.length; i++) {
5766
+ const line = lines[i];
5767
+ let varName = null;
5768
+ let kind = null;
5769
+ const low = lowerGuard.exec(line);
5770
+ if (low) {
5771
+ varName = low[1];
5772
+ kind = 'lower';
5773
+ }
5774
+ else {
5775
+ const up = upperGuard.exec(line);
5776
+ if (up) {
5777
+ varName = up[1];
5778
+ kind = 'upper';
5779
+ }
5780
+ }
5781
+ if (!varName || !kind || !coerced.has(varName))
5782
+ continue;
5783
+ if (!terminator.test(line)) {
5784
+ let bodyHasTerminator = false;
5785
+ let blockEnd = i;
5786
+ let braceDepth = 0;
5787
+ let started = false;
5788
+ const maxScan = Math.min(lines.length, i + 26);
5789
+ for (let j = i; j < maxScan; j++) {
5790
+ const ln = lines[j];
5791
+ for (const ch of ln) {
5792
+ if (ch === '{') {
5793
+ braceDepth++;
5794
+ started = true;
5795
+ }
5796
+ else if (ch === '}') {
5797
+ braceDepth--;
5798
+ if (started && braceDepth === 0) {
5799
+ blockEnd = j;
5800
+ break;
5801
+ }
5802
+ }
5803
+ }
5804
+ if (j > i && terminator.test(ln))
5805
+ bodyHasTerminator = true;
5806
+ if (started && braceDepth === 0 && blockEnd !== i)
5807
+ break;
5808
+ }
5809
+ if (!bodyHasTerminator && !started)
5810
+ continue;
5811
+ if (!bodyHasTerminator)
5812
+ continue;
5813
+ }
5814
+ const st = state.get(varName) ?? { lower: false, upper: false, lastGuardEnd: i };
5815
+ if (kind === 'lower')
5816
+ st.lower = true;
5817
+ else
5818
+ st.upper = true;
5819
+ st.lastGuardEnd = i;
5820
+ state.set(varName, st);
5821
+ }
5822
+ for (const [varName, st] of state) {
5823
+ if (!st.lower || !st.upper)
5824
+ continue;
5825
+ const varRefRe = new RegExp(`\\b${varName}\\b`);
5826
+ for (let l = st.lastGuardEnd + 1; l < lines.length; l++) {
5827
+ if (!varRefRe.test(lines[l]))
5828
+ continue;
5829
+ sanitizers.push({
5830
+ type: 'rust_numeric_range_check_guard',
5831
+ method: 'if',
5832
+ line: l + 1,
5833
+ sanitizes: ['xss', 'external_taint_escape'],
5834
+ });
5835
+ }
5836
+ }
5837
+ return sanitizers;
5838
+ }
5197
5839
  function findPythonRangeCheckGuardSanitizers(code) {
5198
5840
  const sanitizers = [];
5199
5841
  const lines = code.split('\n');
@@ -6062,6 +6704,161 @@ function findPythonDefusedXmlSanitizers(code) {
6062
6704
  }
6063
6705
  return sanitizers;
6064
6706
  }
6707
+ /**
6708
+ * PAT-INFO-01 safe mirror (cognium-dev #816): `HttpResponse` body built with
6709
+ * `format!` interpolates only `web::Query` / `.get(...)` values — no `env::var`.
6710
+ * Credits the sink so CWE-200-style response leaks stay on env-backed TP controls.
6711
+ */
6712
+ function findRustPatInfoHttpOnlyResponseSanitizers(code) {
6713
+ const sanitizers = [];
6714
+ const lines = code.split('\n');
6715
+ const bodyFormatRe = /HttpResponse::\w+\(\)\s*\.\s*body\s*\(\s*format!\s*\(/;
6716
+ const envBindRe = /(?:^|[^:])env::var\s*\(|std::env::/;
6717
+ const httpGetRe = /\.get\s*\(\s*"[^"]+"\s*\)/;
6718
+ const letVarRe = (v) => new RegExp(String.raw `^\s*let\s+(?:mut\s+)?${v}\s*=`);
6719
+ for (let i = 0; i < lines.length; i++) {
6720
+ const sinkLine = lines[i];
6721
+ if (!bodyFormatRe.test(sinkLine))
6722
+ continue;
6723
+ if (envBindRe.test(sinkLine))
6724
+ continue;
6725
+ const varRefs = [
6726
+ ...sinkLine.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g),
6727
+ ].map((m) => m[1]);
6728
+ if (varRefs.length === 0)
6729
+ continue;
6730
+ let allHttpOnly = true;
6731
+ for (const v of varRefs) {
6732
+ let sawBinding = false;
6733
+ let boundEnv = false;
6734
+ let boundHttp = false;
6735
+ for (let j = i - 1; j >= Math.max(0, i - 24); j--) {
6736
+ const line = lines[j];
6737
+ if (!letVarRe(v).test(line))
6738
+ continue;
6739
+ sawBinding = true;
6740
+ if (envBindRe.test(line))
6741
+ boundEnv = true;
6742
+ if (httpGetRe.test(line))
6743
+ boundHttp = true;
6744
+ break;
6745
+ }
6746
+ if (!sawBinding || boundEnv || !boundHttp) {
6747
+ allHttpOnly = false;
6748
+ break;
6749
+ }
6750
+ }
6751
+ if (!allHttpOnly)
6752
+ continue;
6753
+ sanitizers.push({
6754
+ type: 'rust_pat_info_http_only_body',
6755
+ method: 'format',
6756
+ line: i + 1,
6757
+ sanitizes: [
6758
+ 'xss',
6759
+ 'external_taint_escape',
6760
+ 'open_redirect',
6761
+ 'log_injection',
6762
+ ],
6763
+ });
6764
+ }
6765
+ return sanitizers;
6766
+ }
6767
+ /**
6768
+ * Rust `const SLICE: &[&str] = &["a", "b"]` membership reject guard
6769
+ * (cognium-dev #816 PAT-TRUST-01; Python twin #563).
6770
+ *
6771
+ * if !THEMES.contains(&theme.as_str()) {
6772
+ * return HttpResponse::BadRequest().finish();
6773
+ * }
6774
+ * session.insert("theme", theme)
6775
+ */
6776
+ function collectRustConstStrSliceNames(code) {
6777
+ const names = new Set();
6778
+ const re = /^\s*const\s+([A-Z][A-Z0-9_]*)\s*:\s*&\s*\[\s*&\s*str\s*\]\s*=\s*&\s*\[([^\]]*)\]\s*;/;
6779
+ for (const line of code.split('\n')) {
6780
+ const m = re.exec(line);
6781
+ if (!m)
6782
+ continue;
6783
+ const inner = m[2].trim();
6784
+ if (!inner) {
6785
+ names.add(m[1]);
6786
+ continue;
6787
+ }
6788
+ const elems = inner.split(',').map(s => s.trim()).filter(Boolean);
6789
+ if (elems.every(e => /^"(?:[^"\\]|\\.)*"$/.test(e)))
6790
+ names.add(m[1]);
6791
+ }
6792
+ return names;
6793
+ }
6794
+ function findRustConstSliceMembershipGuardSanitizers(code) {
6795
+ const sanitizers = [];
6796
+ const slices = collectRustConstStrSliceNames(code);
6797
+ if (slices.size === 0)
6798
+ return sanitizers;
6799
+ const lines = code.split('\n');
6800
+ const guardOpen = /^\s*if\s+!\s*([A-Z][A-Z0-9_]*)\s*\.\s*contains\s*\(\s*&\s*([A-Za-z_]\w*)(?:\s*\.\s*as_str\s*\(\s*\))?\s*\)\s*\{?\s*$/;
6801
+ const terminator = /\b(?:return\s+HttpResponse::|return\s+StatusCode::|return\s+Err\s*\(|panic!\s*\()/;
6802
+ for (let i = 0; i < lines.length; i++) {
6803
+ const m = guardOpen.exec(lines[i]);
6804
+ if (!m)
6805
+ continue;
6806
+ const sliceName = m[1];
6807
+ const guardedVar = m[2];
6808
+ if (!slices.has(sliceName))
6809
+ continue;
6810
+ let depth = 0;
6811
+ for (const ch of lines[i]) {
6812
+ if (ch === '{')
6813
+ depth++;
6814
+ else if (ch === '}')
6815
+ depth--;
6816
+ }
6817
+ let closeLine = i;
6818
+ let bodyHasTerminator = terminator.test(lines[i]);
6819
+ if (depth > 0) {
6820
+ for (let j = i + 1; j < Math.min(lines.length, i + 26); j++) {
6821
+ if (terminator.test(lines[j]))
6822
+ bodyHasTerminator = true;
6823
+ for (const ch of lines[j]) {
6824
+ if (ch === '{')
6825
+ depth++;
6826
+ else if (ch === '}')
6827
+ depth--;
6828
+ }
6829
+ if (depth === 0) {
6830
+ closeLine = j;
6831
+ break;
6832
+ }
6833
+ }
6834
+ }
6835
+ if (!bodyHasTerminator)
6836
+ continue;
6837
+ const varRe = new RegExp(String.raw `\b${guardedVar}\b`);
6838
+ const reassign = new RegExp(String.raw `^\s*(?:let\s+)?(?:mut\s+)?${guardedVar}\s*=(?!=)`);
6839
+ for (let l = closeLine + 2; l <= lines.length; l++) {
6840
+ const lineText = lines[l - 1];
6841
+ if (!lineText || !varRe.test(lineText))
6842
+ continue;
6843
+ if (reassign.test(lineText))
6844
+ break;
6845
+ sanitizers.push({
6846
+ type: 'rust_const_slice_membership_guard',
6847
+ method: 'if',
6848
+ line: l,
6849
+ sanitizes: [
6850
+ 'trust_boundary',
6851
+ 'external_taint_escape',
6852
+ 'xss',
6853
+ 'open_redirect',
6854
+ 'ssrf',
6855
+ 'command_injection',
6856
+ ],
6857
+ });
6858
+ }
6859
+ }
6860
+ return sanitizers;
6861
+ }
6065
6862
  /**
6066
6863
  * Detect Rust HashSet/HashMap host allow-list guards (cognium-dev #115 FP-23).
6067
6864
  *
@@ -6333,6 +7130,16 @@ function findJsCsvFormulaPrefixSanitizers(code) {
6333
7130
  * threat-set. Custom wrappers using different escape strategies (e.g.
6334
7131
  * URL encode) are not yet covered.
6335
7132
  */
7133
+ /** Return expression of a small JS wrapper (function or arrow) for #562 chain verdict. */
7134
+ function jsWrapperBodyReplaceChainText(body) {
7135
+ const returnM = /\breturn\s+([^;}\n]+)/.exec(body);
7136
+ if (returnM)
7137
+ return returnM[1].trim();
7138
+ const arrowM = /=>\s*([^;}\n]+)/.exec(body);
7139
+ if (arrowM)
7140
+ return arrowM[1].trim();
7141
+ return null;
7142
+ }
6336
7143
  function findJsWrapperFunctionSanitizers(code) {
6337
7144
  const sanitizers = [];
6338
7145
  const lines = code.split('\n');
@@ -6355,8 +7162,13 @@ function findJsWrapperFunctionSanitizers(code) {
6355
7162
  kinds.add('external_taint_escape');
6356
7163
  }
6357
7164
  if (crlfCharClass.test(body)) {
6358
- kinds.add('log_injection');
6359
- kinds.add('external_taint_escape');
7165
+ const chainText = jsWrapperBodyReplaceChainText(body);
7166
+ const verdict = chainText ? jsReplaceChainVerdict(chainText) : 'undecided';
7167
+ // #562 / #605: LF-only or CRLF-pair-only strips must not credit log_injection.
7168
+ if (verdict !== 'leaves-crlf') {
7169
+ kinds.add('log_injection');
7170
+ kinds.add('external_taint_escape');
7171
+ }
6360
7172
  }
6361
7173
  if (kinds.size > 0)
6362
7174
  wrappers.set(name, kinds);
@@ -7765,6 +8577,87 @@ function javaBodyStripsCrAndLf(body) {
7765
8577
  }
7766
8578
  return cr && lf;
7767
8579
  }
8580
+ /** True when the helper removes both `{` and `}` via replace/replaceAll (flow-sanitize ssti). */
8581
+ function javaBodyStripsTemplateDelimiters(body) {
8582
+ let open = false;
8583
+ let close = false;
8584
+ for (const m of body.matchAll(/\.\s*(replace|replaceAll)\s*\(\s*(?:"((?:\\.|[^"\\])*)"|'(\\.|[^'\\])')/g)) {
8585
+ const lit = m[2] ?? m[3] ?? '';
8586
+ if (lit === '{')
8587
+ open = true;
8588
+ if (lit === '}')
8589
+ close = true;
8590
+ }
8591
+ return open && close;
8592
+ }
8593
+ /**
8594
+ * Java user-defined wrapper that strips Velocity/Jinja-style `{` `}` delimiters.
8595
+ * Mirrors flow-sanitize `ssti` / `custom_wrapper` safe mirrors (cognium-dev#947).
8596
+ */
8597
+ function findJavaTemplateDelimiterWrapperSanitizers(code) {
8598
+ const sanitizers = [];
8599
+ const lines = code.split('\n');
8600
+ const declRe = /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:final\s+)?(?:String|CharSequence)\s+([A-Za-z_]\w*)\s*\(/;
8601
+ const bodyWindow = 10;
8602
+ const wrappers = new Set();
8603
+ for (let i = 0; i < lines.length; i++) {
8604
+ const m = declRe.exec(lines[i]);
8605
+ if (!m)
8606
+ continue;
8607
+ let end = Math.min(lines.length, i + bodyWindow);
8608
+ for (let j = i + 1; j < end; j++) {
8609
+ if (declRe.test(lines[j])) {
8610
+ end = j;
8611
+ break;
8612
+ }
8613
+ }
8614
+ const body = lines.slice(i, end).join('\n');
8615
+ if (javaBodyStripsTemplateDelimiters(body))
8616
+ wrappers.add(m[1]);
8617
+ }
8618
+ if (wrappers.size === 0)
8619
+ return sanitizers;
8620
+ const kinds = ['code_injection', 'external_taint_escape'];
8621
+ for (const name of wrappers) {
8622
+ const assignedCallRe = new RegExp(`\\b([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*${name}\\s*\\(`);
8623
+ const bareCallRe = new RegExp(`\\b${name}\\s*\\(`);
8624
+ for (let i = 0; i < lines.length; i++) {
8625
+ if (!bareCallRe.test(lines[i]))
8626
+ continue;
8627
+ if (declRe.test(lines[i]) && bareCallRe.test(lines[i]))
8628
+ continue;
8629
+ sanitizers.push({
8630
+ type: 'java_wrapper_template_delim_strip',
8631
+ method: name,
8632
+ line: i + 1,
8633
+ sanitizes: [...kinds],
8634
+ });
8635
+ const am = assignedCallRe.exec(lines[i]);
8636
+ if (!am)
8637
+ continue;
8638
+ const callIndent = lines[i].length - lines[i].trimStart().length;
8639
+ const validated = am[1];
8640
+ const validatedRe = new RegExp(`\\b${validated}\\b`);
8641
+ for (let j = i + 1; j < lines.length; j++) {
8642
+ const jline = lines[j];
8643
+ if (jline.trim() === '')
8644
+ continue;
8645
+ const jindent = jline.length - jline.trimStart().length;
8646
+ if (jindent < callIndent)
8647
+ break;
8648
+ if (validatedRe.test(jline)) {
8649
+ sanitizers.push({
8650
+ type: 'java_wrapper_template_delim_strip',
8651
+ method: name,
8652
+ line: j + 1,
8653
+ sanitizes: [...kinds],
8654
+ });
8655
+ }
8656
+ }
8657
+ }
8658
+ }
8659
+ return sanitizers;
8660
+ }
7768
8661
  function findCrlfWrapperFunctionSanitizers(code, language) {
7769
8662
  const sanitizers = [];
7770
8663
  const lines = code.split('\n');
@@ -11196,6 +12089,108 @@ export function findJsLdapInjectionFindings(code, file) {
11196
12089
  snippet: line.trim(),
11197
12090
  });
11198
12091
  }
12092
+ // cognium-dev #899: ldapjs also accepts the filter as the 2nd positional
12093
+ // argument (`client.search(base, '(mail=' + email + ')', cb)`), not only
12094
+ // as `opts.filter`. Flag when that argument is built from tainted data.
12095
+ // Base DN is often comma-separated; only match a quoted first argument.
12096
+ const positionalSearchRe = /\.\s*search\s*\(\s*(?:'[^']*'|"[^"]*")\s*,\s*([^,{]+)/;
12097
+ for (let i = 0; i < lines.length; i++) {
12098
+ const line = lines[i];
12099
+ const pm = line.match(positionalSearchRe);
12100
+ if (!pm)
12101
+ continue;
12102
+ const expr = pm[1].trim();
12103
+ if (expr.startsWith('{'))
12104
+ continue;
12105
+ let tainted = false;
12106
+ for (const v of taintedVars) {
12107
+ if (new RegExp(`\\b${v}\\b`).test(expr)) {
12108
+ tainted = true;
12109
+ break;
12110
+ }
12111
+ }
12112
+ if (!tainted)
12113
+ continue;
12114
+ const lineNo = i + 1;
12115
+ if (seen.has(lineNo))
12116
+ continue;
12117
+ seen.add(lineNo);
12118
+ findings.push({
12119
+ id: `ldap_injection-${file}-${lineNo}-js-ldap-positional-filter`,
12120
+ pass: 'language-sources',
12121
+ category: 'security',
12122
+ rule_id: 'ldap_injection',
12123
+ cwe: 'CWE-90',
12124
+ severity: 'critical',
12125
+ level: 'error',
12126
+ message: 'LDAP injection: ldapjs/ldapts `client.search(base, <tainted-filter>, ...)` ' +
12127
+ 'lets the attacker break out of the filter expression. Escape user input ' +
12128
+ 'or use a structured filter builder.',
12129
+ file,
12130
+ line: lineNo,
12131
+ snippet: line.trim(),
12132
+ });
12133
+ }
12134
+ return findings;
12135
+ }
12136
+ /**
12137
+ * cognium-dev #897 — ZIP local-file-header validation that fail-opens: after
12138
+ * rejecting an invalid PK\\x03\\x04 signature, returning a slice of the caller
12139
+ * buffer instead of throwing lets untrusted bytes through (CWE-20).
12140
+ */
12141
+ export function findJsZipHeaderFailopenFindings(code, file) {
12142
+ const findings = [];
12143
+ if (typeof code !== 'string' || code.length === 0)
12144
+ return findings;
12145
+ const lines = code.split('\n');
12146
+ const hasPkLocalSig = lines.some((l) => {
12147
+ const t = l.trim();
12148
+ if (t.startsWith('//'))
12149
+ return false;
12150
+ return (/0x50/.test(t) &&
12151
+ /0x4[bB]/.test(t) &&
12152
+ /0x03/.test(t) &&
12153
+ /0x04/.test(t));
12154
+ });
12155
+ if (!hasPkLocalSig)
12156
+ return findings;
12157
+ const seen = new Set();
12158
+ for (let i = 0; i < lines.length; i++) {
12159
+ const trimmed = lines[i].trim();
12160
+ if (trimmed.startsWith('//'))
12161
+ continue;
12162
+ const failBranch = trimmed.match(/^if\s*\(\s*!(\w+)\s*\)\s*(.+)$/);
12163
+ if (!failBranch)
12164
+ continue;
12165
+ const tail = failBranch[2];
12166
+ if (/\bthrow\b/.test(tail))
12167
+ continue;
12168
+ if (!/\breturn\b/.test(tail))
12169
+ continue;
12170
+ if (!/\.(subarray|slice)\s*\(/.test(tail) &&
12171
+ !/Buffer\.from\s*\(/.test(tail)) {
12172
+ continue;
12173
+ }
12174
+ const lineNo = i + 1;
12175
+ if (seen.has(lineNo))
12176
+ continue;
12177
+ seen.add(lineNo);
12178
+ findings.push({
12179
+ id: `input_validation-${file}-${lineNo}-js-zip-header-failopen`,
12180
+ pass: 'language-sources',
12181
+ category: 'security',
12182
+ rule_id: 'input_validation',
12183
+ cwe: 'CWE-20',
12184
+ severity: 'high',
12185
+ level: 'error',
12186
+ message: 'Input validation fail-open: an invalid ZIP local header check still ' +
12187
+ 'returns a slice of the caller-controlled buffer. On parse failure, ' +
12188
+ 'throw or return empty — do not pass through untrusted bytes.',
12189
+ file,
12190
+ line: lineNo,
12191
+ snippet: trimmed.substring(0, 120),
12192
+ });
12193
+ }
11199
12194
  return findings;
11200
12195
  }
11201
12196
  /**
@@ -11845,6 +12840,53 @@ export function findGoGobDeserializationFindings(code, file) {
11845
12840
  }
11846
12841
  return findings;
11847
12842
  }
12843
+ /**
12844
+ * Blank the contents of JS/TS string and template literals and comments,
12845
+ * keeping newlines and every offset, so a line scanner does not match code
12846
+ * that only appears as text (a message describing `JSON.parse(req.body)`).
12847
+ */
12848
+ function maskJsLiteralsAndComments(code) {
12849
+ const out = code.split('');
12850
+ let i = 0;
12851
+ const blank = (from, to) => {
12852
+ for (let k = from; k < to; k++)
12853
+ if (out[k] !== '\n')
12854
+ out[k] = ' ';
12855
+ };
12856
+ while (i < code.length) {
12857
+ const c = code[i];
12858
+ const n = code[i + 1];
12859
+ if (c === '/' && n === '/') {
12860
+ let j = i;
12861
+ while (j < code.length && code[j] !== '\n')
12862
+ j++;
12863
+ blank(i, j);
12864
+ i = j;
12865
+ }
12866
+ else if (c === '/' && n === '*') {
12867
+ const end = code.indexOf('*/', i + 2);
12868
+ const j = end < 0 ? code.length : end + 2;
12869
+ blank(i, j);
12870
+ i = j;
12871
+ }
12872
+ else if (c === '"' || c === "'" || c === '`') {
12873
+ let j = i + 1;
12874
+ while (j < code.length && code[j] !== c) {
12875
+ if (code[j] === '\\')
12876
+ j++;
12877
+ else if (c !== '`' && code[j] === '\n')
12878
+ break;
12879
+ j++;
12880
+ }
12881
+ blank(i + 1, Math.min(j, code.length));
12882
+ i = j + 1;
12883
+ }
12884
+ else {
12885
+ i++;
12886
+ }
12887
+ }
12888
+ return out.join('');
12889
+ }
11848
12890
  /**
11849
12891
  * Sprint 89 detector B (#189) — JS insecure deserialization via
11850
12892
  * `JSON.parse(req.body)`.
@@ -11900,13 +12942,14 @@ export function findJsJsonParseBodyFindings(code, file) {
11900
12942
  }
11901
12943
  const callRe = /\bJSON\s*\.\s*parse\s*\(\s*([^)]+?)\s*\)/g;
11902
12944
  const seen = new Set();
12945
+ // Calls only: `JSON.parse(req.body)` inside a string or comment is text.
12946
+ const codeLines = maskJsLiteralsAndComments(code).split('\n');
11903
12947
  for (let i = 0; i < lines.length; i++) {
11904
12948
  const line = lines[i];
11905
- if (line.trim().startsWith('//'))
11906
- continue;
12949
+ const codeLine = codeLines[i] ?? '';
11907
12950
  callRe.lastIndex = 0;
11908
12951
  let m;
11909
- while ((m = callRe.exec(line)) !== null) {
12952
+ while ((m = callRe.exec(codeLine)) !== null) {
11910
12953
  const arg = m[1];
11911
12954
  let tainted = reqBodyRe.test(arg);
11912
12955
  if (!tainted) {