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
@@ -18,6 +18,7 @@ import { attachSourceLineCode } from '../taint-matcher.js';
18
18
  import { shouldGateInterproceduralParam } from '../entry-point-detection.js';
19
19
  import { sanitizerCoversSink } from '../sanitizer-index.js';
20
20
  import { identifierOutsideSanitizerCalls } from '../taint-propagation.js';
21
+ import { buildJavaTaintedVars, computeDeadJavaBranchLines } from './language-sources-pass.js';
21
22
  export class InterproceduralPass {
22
23
  name = 'interprocedural';
23
24
  category = 'security';
@@ -150,6 +151,7 @@ export class InterproceduralPass {
150
151
  }
151
152
  }
152
153
  }
154
+ appendCalleeSourceCallerSinkFlows(graph, sources, sinks, interProc, additionalFlows, language === 'java' ? ctx.code : undefined, language === 'java' ? constProp : undefined);
153
155
  const taintBridges = findTaintBridges(interProc);
154
156
  interprocedural = {
155
157
  tainted_methods: Array.from(interProc.taintedMethods),
@@ -348,6 +350,566 @@ export class InterproceduralPass {
348
350
  * The sink is reached when its statement mentions a derived name that is not
349
351
  * sanitized or guarded there.
350
352
  */
353
+ /**
354
+ * True when constant propagation tracked `variable` in `method` and did not
355
+ * mark it tainted. Only a tracked symbol counts: "not in the tainted set" alone
356
+ * also means "never looked at" (see `isFalsePositive` reason 3).
357
+ */
358
+ function assigneeIsConstPropClean(constProp, method, variable) {
359
+ if (!method)
360
+ return false;
361
+ const scoped = `${method}:${variable}`;
362
+ if (!constProp.symbols.has(scoped))
363
+ return false;
364
+ if (constProp.tainted.has(scoped))
365
+ return false;
366
+ for (const t of constProp.tainted) {
367
+ if (t.startsWith(`${method}:`) && t.endsWith(`:${variable}`))
368
+ return false;
369
+ }
370
+ if (constProp.sanitizedVarUncovered?.has(scoped))
371
+ return false;
372
+ return true;
373
+ }
374
+ function constPropTaints(constProp, method, variable) {
375
+ if (constProp.tainted.has(`${method}:${variable}`))
376
+ return true;
377
+ for (const t of constProp.tainted) {
378
+ if (t.startsWith(`${method}:`) && t.endsWith(`:${variable}`))
379
+ return true;
380
+ }
381
+ return false;
382
+ }
383
+ /** Does a call on `line` in `method` take an argument const-prop holds tainted? */
384
+ function callArgumentIsConstPropTainted(graph, constProp, method, line) {
385
+ if (!method)
386
+ return false;
387
+ for (const call of graph.ir.calls) {
388
+ if (call.location.line !== line)
389
+ continue;
390
+ for (const arg of call.arguments) {
391
+ const names = arg.variable
392
+ ? [arg.variable]
393
+ : (arg.expression ?? '').match(/[A-Za-z_]\w*/g) ?? [];
394
+ if (names.some(n => constPropTaints(constProp, method, n)))
395
+ return true;
396
+ }
397
+ }
398
+ return false;
399
+ }
400
+ /**
401
+ * Does the callee `return` a variable const-prop holds tainted elements or keys
402
+ * for (`vars[i] = "X=" + param; return vars;`)? Const-prop does not carry that
403
+ * into the call's result, so its "clean" assignee is not proof here.
404
+ */
405
+ function calleeReturnsTaintedContainer(constProp, code, startLine, endLine) {
406
+ const lines = code.split('\n');
407
+ for (let l = startLine; l <= endLine; l++) {
408
+ const m = /^\s*return\s+([A-Za-z_]\w*)\s*;/.exec(lines[l - 1] ?? '');
409
+ if (!m)
410
+ continue;
411
+ if ((constProp.taintedArrayElements.get(m[1])?.size ?? 0) > 0)
412
+ return true;
413
+ if ((constProp.taintedCollections.get(m[1])?.size ?? 0) > 0)
414
+ return true;
415
+ }
416
+ return false;
417
+ }
418
+ /**
419
+ * Sink types whose parameter-sourced helper flows are dropped when the
420
+ * parameter reaches the helper's return only through a constant-dead branch
421
+ * (`if ((7 * 42) - num > 200) bar = "safe"; else bar = param;`).
422
+ */
423
+ const DEAD_BRANCH_CHECKED_SINKS = new Set([
424
+ 'sql_injection', 'trust_boundary', 'path_traversal', 'ldap_injection', 'xpath_injection',
425
+ ]);
426
+ /**
427
+ * Java: do the defs a callee source creates reach a `return` of the callee?
428
+ * Follows def-use chains forward (a use of a reached def reaches the locals
429
+ * defined on its line), so an overwrite before the return ends the chain.
430
+ * A source on the return line itself reaches it.
431
+ */
432
+ function calleeSourceReachesReturn(graph, code, startLine, endLine, sourceLine) {
433
+ const lines = code.split('\n');
434
+ const returnLines = new Set();
435
+ for (let l = startLine; l <= endLine; l++) {
436
+ if (/^\s*return\b/.test(lines[l - 1] ?? ''))
437
+ returnLines.add(l);
438
+ }
439
+ if (returnLines.size === 0)
440
+ return true; // nothing to prove against: keep the source
441
+ if (returnLines.has(sourceLine))
442
+ return true;
443
+ const inRange = (l) => l >= startLine && l <= endLine;
444
+ // A statement can span lines (`String e =\n new String(\n … d.getBytes()));`):
445
+ // the def sits on its first line, the uses on later ones.
446
+ const ends = (l) => /[;{}]\s*$/.test((lines[l - 1] ?? '').replace(/\/\/.*$/, '').trimEnd());
447
+ const statementStart = (l) => {
448
+ let s = l;
449
+ while (s > startLine && !ends(s - 1) && (lines[s - 2] ?? '').trim() !== '')
450
+ s--;
451
+ return s;
452
+ };
453
+ const reached = new Set(graph.ir.dfg.defs.filter(d => d.line === sourceLine && d.kind === 'local').map(d => d.id));
454
+ if (reached.size === 0)
455
+ return true; // the source defines nothing we can follow
456
+ let changed = true;
457
+ while (changed) {
458
+ changed = false;
459
+ for (const u of graph.ir.dfg.uses) {
460
+ if (u.def_id === null || !reached.has(u.def_id) || !inRange(u.line))
461
+ continue;
462
+ const stmt = statementStart(u.line);
463
+ if (returnLines.has(u.line) || returnLines.has(stmt))
464
+ return true;
465
+ for (const d of graph.ir.dfg.defs) {
466
+ if ((d.line === u.line || d.line === stmt) && d.kind === 'local' && !reached.has(d.id)) {
467
+ reached.add(d.id);
468
+ changed = true;
469
+ }
470
+ }
471
+ // An element or field store (`arr[i] = name + "=" + value;`, `o.f = value;`)
472
+ // defines no local, but the value now lives in the base: follow the base's
473
+ // def (plexus-utils `getEnvironmentVariables()` fills an array it returns).
474
+ // Container calls (`map.put`, `list.add`) are not followed: the keyed
475
+ // overwrite cases above depend on them ending the chain.
476
+ const base = storeBase(lines[stmt - 1] ?? '');
477
+ if (base && base !== u.variable) {
478
+ for (const b of graph.ir.dfg.uses) {
479
+ if ((b.line === u.line || b.line === stmt) && b.variable === base &&
480
+ b.def_id !== null && !reached.has(b.def_id)) {
481
+ reached.add(b.def_id);
482
+ changed = true;
483
+ }
484
+ }
485
+ }
486
+ }
487
+ }
488
+ return false;
489
+ }
490
+ /** `arr[i] = …` / `arr[i][j] = …` / `obj.field = …` → the base local's name; else null. */
491
+ function storeBase(line) {
492
+ const m = /^\s*([A-Za-z_]\w*)\s*(?:\[[^\]=]*\]\s*)+=(?!=)/.exec(line) ??
493
+ /^\s*([A-Za-z_]\w*)\s*\.\s*[A-Za-z_]\w*\s*=(?!=)/.exec(line);
494
+ if (!m || m[1] === 'this')
495
+ return null;
496
+ return m[1];
497
+ }
498
+ /** cognium-dev#590 — connect a source inside a callee to a sink in the caller. */
499
+ function appendCalleeSourceCallerSinkFlows(graph, sources, sinks, interProc, additionalFlows, fileCode, constProp) {
500
+ const types = graph.ir.types ?? [];
501
+ for (const edge of interProc.callEdges) {
502
+ const callee = interProc.methodNodes.get(edge.calleeMethod);
503
+ if (!callee)
504
+ continue;
505
+ // Java: a source the callee overwrites before it returns
506
+ // (`bar = map.get("keyB"); bar = map.get("keyA"); return bar;`) does not
507
+ // reach the caller. Parameters are kept here; their live reach is the
508
+ // dead-branch check below.
509
+ const calleeSources = sources.filter(s => s.line >= callee.startLine && s.line <= callee.endLine &&
510
+ (!fileCode || s.type === 'interprocedural_param' ||
511
+ calleeSourceReachesReturn(graph, fileCode, callee.startLine, callee.endLine, s.line)));
512
+ if (calleeSources.length === 0)
513
+ continue;
514
+ const assignee = graph.ir.dfg.defs.find(d => d.line === edge.callLine && d.kind === 'local');
515
+ if (!assignee?.variable)
516
+ continue;
517
+ // A source inside the callee taints the caller only through the value the
518
+ // callee returns. Constant propagation evaluates that return with the
519
+ // callee's arguments (map keys, list indexes, constant branches, switch
520
+ // arms), and the DFG flows it vetoes are not reported either. When it has
521
+ // the assignee as a tracked, untainted value, the callee's source does not
522
+ // reach the return (OWASP `doSomething` helpers: `map.get("keyA")` after a
523
+ // `map.get("keyB")`, `if ((7 * 42) - num > 200)`, `valuesList.get(1)`).
524
+ // Its verdict is informed only when it saw tainted data go into the call:
525
+ // a value from another file (`scr.getTheParameter(…)`) is not a source to
526
+ // it, and then "clean" means "never tainted here", not "the callee drops it".
527
+ if (constProp &&
528
+ callArgumentIsConstPropTainted(graph, constProp, edge.callerMethod, edge.callLine) &&
529
+ assigneeIsConstPropClean(constProp, edge.callerMethod, assignee.variable) &&
530
+ !(fileCode && calleeReturnsTaintedContainer(constProp, fileCode, callee.startLine, callee.endLine))) {
531
+ continue;
532
+ }
533
+ const derivedLocals = localsDerivedFromAssignee(graph, assignee.variable, assignee.line);
534
+ for (const sink of sinks) {
535
+ if (sink.type === 'external_taint_escape')
536
+ continue;
537
+ if (sink.line <= edge.callLine)
538
+ continue;
539
+ if (enclosingMethodName(types, sink.line) !== edge.callerMethod)
540
+ continue;
541
+ const sinkVar = sinkVariableConsumingDerived(graph, sink, derivedLocals);
542
+ if (!sinkVar)
543
+ continue;
544
+ if (additionalFlows.some(f => f.source_line === calleeSources[0].line && f.sink_line === sink.line)) {
545
+ continue;
546
+ }
547
+ const source = calleeSources[0];
548
+ if (fileCode &&
549
+ source.type === 'interprocedural_param' &&
550
+ source.variable &&
551
+ DEAD_BRANCH_CHECKED_SINKS.has(sink.type) &&
552
+ interproceduralParamReachIsDeadBranchOnly(fileCode, callee.startLine, callee.endLine, source.variable, assignee.variable)) {
553
+ continue;
554
+ }
555
+ // cognium-dev#913 / #914 / #916 / #917 — the helper's return value cannot
556
+ // carry any of its parameters on a live path (constant / literal-slot /
557
+ // folded-ternary returns). Every source inside the callee must be a
558
+ // parameter source, and none may reach the return: a helper taking
559
+ // `(request, param)` is judged on both, and a real source inside the
560
+ // callee is never gated here.
561
+ if (fileCode &&
562
+ HELPER_RETURN_GATED_SINKS.has(sink.type) &&
563
+ calleeSources.every(s => (s.type === 'interprocedural_param' || s.type === 'plugin_param') && !!s.variable) &&
564
+ !calleeReturnMayCarryParam(fileCode, callee.startLine, callee.endLine, calleeSources.map(s => s.variable))) {
565
+ continue;
566
+ }
567
+ additionalFlows.push({
568
+ source_line: source.line,
569
+ sink_line: sink.line,
570
+ source_type: source.type,
571
+ sink_type: sink.type,
572
+ path: [
573
+ { variable: source.location, line: source.line, type: 'source' },
574
+ { variable: assignee.variable, line: edge.callLine, type: 'use' },
575
+ { variable: sinkVar, line: sink.line, type: 'sink' },
576
+ ],
577
+ confidence: sink.confidence * source.confidence * 0.85,
578
+ sanitized: false,
579
+ });
580
+ }
581
+ }
582
+ }
583
+ /**
584
+ * True when `param` would taint `local` only if dead-branch lines are counted
585
+ * (e.g. OWASP #913 `else bar = param`), not when live propagation also reaches
586
+ * `local` (list/Base64 helpers — cognium-dev#913 round 2).
587
+ */
588
+ function interproceduralParamReachIsDeadBranchOnly(code, startLine, endLine, paramName, localName) {
589
+ if (paramName === localName)
590
+ return false;
591
+ const lines = code.split('\n');
592
+ const slice = lines.slice(startLine - 1, endLine);
593
+ const sliceText = slice.join('\n');
594
+ const dead = computeDeadJavaBranchLines(slice);
595
+ const onLivePaths = buildJavaTaintedVars(sliceText, new Set([paramName]), dead);
596
+ const ifDeadLinesCount = buildJavaTaintedVars(sliceText, new Set([paramName]), new Set());
597
+ return ifDeadLinesCount.has(localName) && !onLivePaths.has(localName);
598
+ }
599
+ /** Sink types whose caller-side flow is dropped when the helper cannot return the parameter. */
600
+ const HELPER_RETURN_GATED_SINKS = new Set([
601
+ 'sql_injection',
602
+ 'trust_boundary',
603
+ 'command_injection',
604
+ 'path_traversal',
605
+ // #976: the same helper shapes feed LDAP and XPath sinks (OWASP ldapi/xpathi).
606
+ 'ldap_injection',
607
+ 'xpath_injection',
608
+ ]);
609
+ /**
610
+ * Can the callee's return value carry any of `sourceVars` (or a declared
611
+ * parameter) on a live path?
612
+ * cognium-dev#913 / #914 / #916 / #917.
613
+ *
614
+ * Conservative: answers false (gate the flow) only when every `return` on a live
615
+ * path is understood and none can carry the parameter. A return expression that
616
+ * mentions the parameter or anything derived from it, or a callee with no
617
+ * readable `return`, answers true.
618
+ * Derivation uses `buildJavaTaintedVars` with slot tracking, so `list.get(1)`
619
+ * after `list.remove(0)`, `map.get("<literal key>")` and a ternary whose
620
+ * condition folds to a constant pick the value actually returned.
621
+ */
622
+ export function calleeReturnMayCarryParam(code, startLine, endLine, sourceVars) {
623
+ const raw = code.split('\n').slice(startLine - 1, endLine);
624
+ // Seed with the helper's declared parameters as well as the sources found in
625
+ // it: a `plugin_param` on `bar = (String) map.get("keyB")` stands for the
626
+ // parameter stored under that key, which the slot tables resolve.
627
+ const paramNames = [...sourceVars, ...javaMethodParamNames(raw)];
628
+ const dead = computeDeadJavaBranchLines(raw);
629
+ const inlined = new Set();
630
+ const slice = joinWrappedStatements(raw, inlined);
631
+ // The tracker follows assignments. A receiver mutated by a call with a carried
632
+ // argument (`sb.append(param)`, `parts.addAll(list)`) carries it too, so it is
633
+ // added as a seed and the tracker re-run until nothing new is reached.
634
+ const text = slice.join('\n');
635
+ const seeds = new Set(paramNames);
636
+ // Receivers that carry a value only because they were mutated with it: their
637
+ // declaration (`new String[n]`, `new StringBuilder()`) never does.
638
+ const mutated = new Set();
639
+ let live = new Set(seeds);
640
+ for (let round = 0; round < 8; round++) {
641
+ live = new Set([...seeds, ...buildJavaTaintedVars(text, seeds, dead, { trackSlots: true }).keys()]);
642
+ let grew = false;
643
+ for (let i = 0; i < slice.length; i++) {
644
+ if (dead.has(i))
645
+ continue;
646
+ for (const call of slice[i].matchAll(/([A-Za-z_]\w*)\s*\.\s*[A-Za-z_]\w*\s*\(([^;]*)\)/g)) {
647
+ if (live.has(call[1]))
648
+ continue;
649
+ const args = call[2].replace(/"(?:[^"\\]|\\.)*"/g, ' ').match(/[\p{L}\p{N}_]+/gu) ?? [];
650
+ if (args.some(t => live.has(t))) {
651
+ seeds.add(call[1]);
652
+ mutated.add(call[1]);
653
+ grew = true;
654
+ }
655
+ }
656
+ // An element or field store (`vars[i] = name + "=" + value;`, `o.f = value;`)
657
+ // carries the value into its base (plexus-utils getEnvironmentVariables()).
658
+ const store = /^\s*([A-Za-z_]\w*)\s*(?:(?:\[[^\]=]*\]\s*)+|\.\s*[A-Za-z_]\w*\s*)=(?!=)\s*(.*?);?\s*$/.exec(slice[i]);
659
+ if (store && store[1] !== 'this' && !live.has(store[1])) {
660
+ const rhs = store[2].replace(/"(?:[^"\\]|\\.)*"/g, ' ').match(/[\p{L}\p{N}_]+/gu) ?? [];
661
+ if (rhs.some(t => live.has(t))) {
662
+ seeds.add(store[1]);
663
+ mutated.add(store[1]);
664
+ grew = true;
665
+ }
666
+ }
667
+ }
668
+ if (!grew)
669
+ break;
670
+ }
671
+ let sawReturn = false;
672
+ for (let i = 0; i < slice.length; i++) {
673
+ if (dead.has(i) || !/^\s*return\b/.test(slice[i]))
674
+ continue;
675
+ const m = /^\s*return\b\s*(.*?)\s*;\s*(?:\/\/.*)?$/.exec(slice[i]);
676
+ if (!m)
677
+ return true; // could not read this return
678
+ sawReturn = true;
679
+ const expr = m[1];
680
+ const bare = /^[A-Za-z_]\w*$/.test(expr) ? expr : null;
681
+ if (bare && mutated.has(bare))
682
+ return true;
683
+ if (bare && live.has(bare)) {
684
+ // The tracker is flow-insensitive: `bar = map.get("tainted-key"); bar =
685
+ // map.get("safe-key"); return bar;` reaches it either way. Judge the
686
+ // assignment that actually reaches this return instead, when there is one.
687
+ const def = mustReachingAssignment(slice, dead, i, bare, inlined);
688
+ if (!def)
689
+ return true;
690
+ const upTo = slice.slice(0, def.line);
691
+ upTo.push(`__helperReturn = ${def.rhs};`);
692
+ const atDef = buildJavaTaintedVars(upTo.join('\n'), seeds, dead, { trackSlots: true });
693
+ if (atDef.has('__helperReturn'))
694
+ return true;
695
+ continue;
696
+ }
697
+ const idents = (expr.replace(/"(?:[^"\\]|\\.)*"/g, ' ').replace(/'(?:[^'\\]|\\.)'/g, ' ').match(/[\p{L}\p{N}_]+/gu) ?? []);
698
+ if (idents.some(t => live.has(t)))
699
+ return true;
700
+ }
701
+ return !sawReturn;
702
+ }
703
+ /** Parameter names from a Java method header (the lines before its body's `{`). */
704
+ function javaMethodParamNames(lines) {
705
+ const header = lines.join('\n').split('{')[0];
706
+ const open = header.indexOf('(');
707
+ if (open < 0)
708
+ return [];
709
+ let depth = 0;
710
+ let end = -1;
711
+ for (let k = open; k < header.length; k++) {
712
+ if (header[k] === '(')
713
+ depth++;
714
+ else if (header[k] === ')' && --depth === 0) {
715
+ end = k;
716
+ break;
717
+ }
718
+ }
719
+ if (end < 0)
720
+ return [];
721
+ const names = [];
722
+ let angle = 0;
723
+ let piece = '';
724
+ for (const c of header.slice(open + 1, end) + ',') {
725
+ if (c === '<')
726
+ angle++;
727
+ else if (c === '>')
728
+ angle--;
729
+ if (c === ',' && angle === 0) {
730
+ const m = /([A-Za-z_]\w*)\s*(?:\[\s*\])*\s*$/.exec(piece.trim());
731
+ if (m)
732
+ names.push(m[1]);
733
+ piece = '';
734
+ }
735
+ else
736
+ piece += c;
737
+ }
738
+ return names;
739
+ }
740
+ /**
741
+ * Join each statement that wraps over several lines onto its first line and
742
+ * leave the continuation lines blank, so line indices (and dead-branch lines)
743
+ * still line up: `bar =\n new String(\n param.getBytes());` reads as one
744
+ * assignment. Control-flow headers (`if (x)` with the statement on the next
745
+ * line) and annotations are left as they are, as are comment lines.
746
+ */
747
+ function joinWrappedStatements(lines, inlined) {
748
+ const out = [...lines];
749
+ const ends = /[;{}:]\s*(?:\/\/.*)?$/;
750
+ const header = /^\s*(?:if|else|for|while|do|switch|case|default|try|catch|finally|synchronized)\b|^\s*@|^\s*(?:\/\/|\/?\*)/;
751
+ for (let i = 0; i < out.length; i++) {
752
+ const line = out[i];
753
+ if (line.trim() === '' || ends.test(line) || header.test(line))
754
+ continue;
755
+ let joined = line;
756
+ let j = i + 1;
757
+ for (; j < out.length && j < i + 15; j++) {
758
+ joined += ' ' + out[j].trim();
759
+ if (ends.test(out[j]))
760
+ break;
761
+ }
762
+ if (j >= out.length || j >= i + 15)
763
+ continue; // never terminated: leave it alone
764
+ out[i] = joined;
765
+ for (let k = i + 1; k <= j; k++)
766
+ out[k] = '';
767
+ i = j;
768
+ }
769
+ return out.map((line, i) => {
770
+ const body = inlineControlBody(line);
771
+ if (body !== line)
772
+ inlined.add(i);
773
+ return body;
774
+ });
775
+ }
776
+ /**
777
+ * `if (c) bar = param;` → `bar = param;` (also `else …`, `else if (c) …`,
778
+ * `for (…) …`, `while (…) …`). The tracker anchors assignments at the start of
779
+ * the line, so a one-line branch body was invisible to it. Dead branches were
780
+ * already computed on the original lines, so only the reading changes.
781
+ */
782
+ function inlineControlBody(line) {
783
+ let rest = line;
784
+ const indent = /^\s*/.exec(line)[0];
785
+ for (let guard = 0; guard < 4; guard++) {
786
+ const t = rest.trimStart();
787
+ const elseM = /^(?:\}\s*)?else\b\s*/.exec(t);
788
+ if (elseM && !/^(?:\}\s*)?else\s+if\b/.test(t)) {
789
+ rest = t.slice(elseM[0].length);
790
+ continue;
791
+ }
792
+ const head = /^(?:\}\s*)?(?:else\s+)?(?:if|for|while)\s*\(/.exec(t);
793
+ if (!head)
794
+ break;
795
+ // Skip the balanced condition.
796
+ let depth = 1;
797
+ let k = head[0].length;
798
+ for (; k < t.length && depth > 0; k++) {
799
+ if (t[k] === '(')
800
+ depth++;
801
+ else if (t[k] === ')')
802
+ depth--;
803
+ }
804
+ if (depth !== 0)
805
+ return line;
806
+ rest = t.slice(k);
807
+ }
808
+ const body = rest.trim();
809
+ if (body === '' || body === rest.trim() && body === line.trim() || body.startsWith('{'))
810
+ return line;
811
+ return indent + body;
812
+ }
813
+ /**
814
+ * The assignment to `name` that must reach the `return` on slice line `retLine`:
815
+ * the closest preceding single-line `name = …;` in the same block, with no
816
+ * assignment to `name` in a nested block after it and no exit from the block in
817
+ * between. `null` when there is no such assignment (the caller then stays
818
+ * conservative).
819
+ */
820
+ function mustReachingAssignment(slice, dead, retLine, name,
821
+ /** lines that were a one-line branch body: an assignment there is conditional */
822
+ inlined) {
823
+ // Brace depth at the start of each line, string and char literals skipped.
824
+ const depth = [];
825
+ let d = 0;
826
+ for (const line of slice) {
827
+ depth.push(d);
828
+ const code = line.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)'/g, "''").replace(/\/\/.*$/, '');
829
+ for (const c of code) {
830
+ if (c === '{')
831
+ d++;
832
+ else if (c === '}')
833
+ d--;
834
+ }
835
+ }
836
+ const target = depth[retLine];
837
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
838
+ const assign = new RegExp(`^\\s*(?:(?:final\\s+)?[A-Za-z_][\\w.]*(?:\\s*<[^>]*>)?(?:\\s*\\[\\s*\\])*\\s+)?${esc}\\s*=\\s*(.+?);\\s*(?://.*)?$`);
839
+ const mentionsAssign = new RegExp(`(?<![\\w.])${esc}\\s*=(?!=)`);
840
+ for (let j = retLine - 1; j >= 0; j--) {
841
+ if (depth[j] < target)
842
+ return null; // left the return's block
843
+ if (dead.has(j))
844
+ continue;
845
+ if (!mentionsAssign.test(slice[j]))
846
+ continue;
847
+ if (depth[j] > target || inlined.has(j))
848
+ return null; // a conditional reassignment
849
+ const m = assign.exec(slice[j]);
850
+ if (!m)
851
+ return null; // wrapped or compound assignment
852
+ return { line: j, rhs: m[1] };
853
+ }
854
+ return null;
855
+ }
856
+ /** DFG-forward names reachable from a helper-return assignment LHS (same file). */
857
+ function localsDerivedFromAssignee(graph, seedVar, seedLine) {
858
+ const defs = graph.ir.dfg.defs;
859
+ const chains = graph.ir.dfg.chains ?? [];
860
+ const seedDefs = defs.filter(d => d.variable === seedVar && d.line === seedLine && d.kind === 'local');
861
+ if (seedDefs.length === 0)
862
+ return new Set([seedVar]);
863
+ const defIds = new Set(seedDefs.map(d => d.id));
864
+ const frontier = seedDefs.map(d => d.id);
865
+ const visited = new Set(frontier);
866
+ while (frontier.length > 0) {
867
+ const cur = frontier.shift();
868
+ for (const ch of chains) {
869
+ if (ch.from_def !== cur || visited.has(ch.to_def))
870
+ continue;
871
+ visited.add(ch.to_def);
872
+ defIds.add(ch.to_def);
873
+ frontier.push(ch.to_def);
874
+ }
875
+ }
876
+ const names = new Set([seedVar]);
877
+ for (const d of defs) {
878
+ if (d.variable && defIds.has(d.id))
879
+ names.add(d.variable);
880
+ }
881
+ return names;
882
+ }
883
+ function sinkVariableConsumingDerived(graph, sink, derived) {
884
+ for (const u of graph.usesAtLine(sink.line)) {
885
+ if (u.variable && derived.has(u.variable))
886
+ return u.variable;
887
+ }
888
+ for (const call of graph.callsByLine.get(sink.line) ?? []) {
889
+ for (const arg of call.arguments ?? []) {
890
+ if (arg.variable && derived.has(arg.variable))
891
+ return arg.variable;
892
+ if (arg.expression) {
893
+ for (const v of derived) {
894
+ const re = new RegExp(`\\b${v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
895
+ if (re.test(arg.expression))
896
+ return v;
897
+ }
898
+ }
899
+ }
900
+ }
901
+ return undefined;
902
+ }
903
+ function enclosingMethodName(types, line) {
904
+ for (const type of types) {
905
+ for (const method of type.methods) {
906
+ if (line >= method.start_line && line <= method.end_line) {
907
+ return method.name;
908
+ }
909
+ }
910
+ }
911
+ return null;
912
+ }
351
913
  export function csharpParamReachesSink(code, method, taintedArgs, sink, credit) {
352
914
  // CRLF files leave `\r` on every line, which `.` and `$` will not cross.
353
915
  const lines = code.split(/\r?\n/);