circle-ir 4.16.1 → 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 (295) 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/index.d.ts +2 -0
  17. package/dist/analysis/constant-propagation/index.d.ts.map +1 -1
  18. package/dist/analysis/constant-propagation/index.js +10 -0
  19. package/dist/analysis/constant-propagation/index.js.map +1 -1
  20. package/dist/analysis/constant-propagation/propagator.d.ts +15 -0
  21. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  22. package/dist/analysis/constant-propagation/propagator.js +224 -20
  23. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  24. package/dist/analysis/constant-propagation/types.d.ts +2 -0
  25. package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
  26. package/dist/analysis/constant-propagation.d.ts +1 -1
  27. package/dist/analysis/constant-propagation.d.ts.map +1 -1
  28. package/dist/analysis/constant-propagation.js +1 -1
  29. package/dist/analysis/constant-propagation.js.map +1 -1
  30. package/dist/analysis/entry-point-detection.d.ts.map +1 -1
  31. package/dist/analysis/entry-point-detection.js +72 -0
  32. package/dist/analysis/entry-point-detection.js.map +1 -1
  33. package/dist/analysis/interprocedural.d.ts.map +1 -1
  34. package/dist/analysis/interprocedural.js +75 -8
  35. package/dist/analysis/interprocedural.js.map +1 -1
  36. package/dist/analysis/maven-declared.d.ts +72 -0
  37. package/dist/analysis/maven-declared.d.ts.map +1 -0
  38. package/dist/analysis/maven-declared.js +216 -0
  39. package/dist/analysis/maven-declared.js.map +1 -0
  40. package/dist/analysis/maven-version.d.ts +28 -0
  41. package/dist/analysis/maven-version.d.ts.map +1 -0
  42. package/dist/analysis/maven-version.js +172 -0
  43. package/dist/analysis/maven-version.js.map +1 -0
  44. package/dist/analysis/passes/constant-propagation-pass.d.ts.map +1 -1
  45. package/dist/analysis/passes/constant-propagation-pass.js +29 -1
  46. package/dist/analysis/passes/constant-propagation-pass.js.map +1 -1
  47. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  48. package/dist/analysis/passes/cross-file-pass.js +84 -2
  49. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  50. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +7 -0
  51. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  52. package/dist/analysis/passes/csharp-sanitizer-credit.js +44 -1
  53. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  54. package/dist/analysis/passes/insecure-cookie-pass.d.ts +2 -0
  55. package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
  56. package/dist/analysis/passes/insecure-cookie-pass.js +25 -3
  57. package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
  58. package/dist/analysis/passes/interprocedural-pass.d.ts +11 -14
  59. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  60. package/dist/analysis/passes/interprocedural-pass.js +562 -0
  61. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  62. package/dist/analysis/passes/language-sources-pass.d.ts +18 -1
  63. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  64. package/dist/analysis/passes/language-sources-pass.js +1079 -14
  65. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  66. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  67. package/dist/analysis/passes/param-source-flow-gate-pass.js +82 -3
  68. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  69. package/dist/analysis/passes/sink-filter-pass.d.ts +44 -2
  70. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  71. package/dist/analysis/passes/sink-filter-pass.js +690 -12
  72. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  73. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  74. package/dist/analysis/passes/taint-propagation-pass.js +298 -19
  75. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  76. package/dist/analysis/passes/tls-verify-disabled-pass.d.ts +18 -0
  77. package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
  78. package/dist/analysis/passes/tls-verify-disabled-pass.js +161 -0
  79. package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
  80. package/dist/analysis/passes/weak-random-pass.d.ts +6 -0
  81. package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
  82. package/dist/analysis/passes/weak-random-pass.js +29 -0
  83. package/dist/analysis/passes/weak-random-pass.js.map +1 -1
  84. package/dist/analysis/promote-cross-file-flows.d.ts +17 -0
  85. package/dist/analysis/promote-cross-file-flows.d.ts.map +1 -0
  86. package/dist/analysis/promote-cross-file-flows.js +32 -0
  87. package/dist/analysis/promote-cross-file-flows.js.map +1 -0
  88. package/dist/analysis/rust-literal-guard.d.ts.map +1 -1
  89. package/dist/analysis/rust-literal-guard.js +100 -1
  90. package/dist/analysis/rust-literal-guard.js.map +1 -1
  91. package/dist/analysis/sbom.d.ts +100 -0
  92. package/dist/analysis/sbom.d.ts.map +1 -1
  93. package/dist/analysis/sbom.js +514 -3
  94. package/dist/analysis/sbom.js.map +1 -1
  95. package/dist/analysis/semver-version.d.ts +30 -0
  96. package/dist/analysis/semver-version.d.ts.map +1 -0
  97. package/dist/analysis/semver-version.js +76 -0
  98. package/dist/analysis/semver-version.js.map +1 -0
  99. package/dist/analysis/taint-matcher.d.ts +5 -0
  100. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  101. package/dist/analysis/taint-matcher.js +226 -4
  102. package/dist/analysis/taint-matcher.js.map +1 -1
  103. package/dist/analyzer.d.ts +57 -0
  104. package/dist/analyzer.d.ts.map +1 -1
  105. package/dist/analyzer.js +69 -6
  106. package/dist/analyzer.js.map +1 -1
  107. package/dist/browser/circle-ir.js +3360 -314
  108. package/dist/core/circle-ir-core.cjs +783 -220
  109. package/dist/core/circle-ir-core.js +783 -220
  110. package/dist/core/extractors/dfg.js +205 -2
  111. package/dist/core/extractors/dfg.js.map +1 -1
  112. package/dist/core/extractors/ts-declaration-detail.d.ts +39 -0
  113. package/dist/core/extractors/ts-declaration-detail.d.ts.map +1 -0
  114. package/dist/core/extractors/ts-declaration-detail.js +266 -0
  115. package/dist/core/extractors/ts-declaration-detail.js.map +1 -0
  116. package/dist/core/extractors/types.js +53 -9
  117. package/dist/core/extractors/types.js.map +1 -1
  118. package/dist/graph/analysis-pass.d.ts +4 -1
  119. package/dist/graph/analysis-pass.d.ts.map +1 -1
  120. package/dist/graph/analysis-pass.js +4 -1
  121. package/dist/graph/analysis-pass.js.map +1 -1
  122. package/dist/graph-facts/dead-code.d.ts +62 -0
  123. package/dist/graph-facts/dead-code.d.ts.map +1 -0
  124. package/dist/graph-facts/dead-code.js +90 -0
  125. package/dist/graph-facts/dead-code.js.map +1 -0
  126. package/dist/graph-facts/dependency-calls.d.ts +86 -0
  127. package/dist/graph-facts/dependency-calls.d.ts.map +1 -0
  128. package/dist/graph-facts/dependency-calls.js +93 -0
  129. package/dist/graph-facts/dependency-calls.js.map +1 -0
  130. package/dist/graph-facts/index.d.ts +10 -0
  131. package/dist/graph-facts/index.d.ts.map +1 -0
  132. package/dist/graph-facts/index.js +10 -0
  133. package/dist/graph-facts/index.js.map +1 -0
  134. package/dist/graph-facts/maven-packages.d.ts +42 -0
  135. package/dist/graph-facts/maven-packages.d.ts.map +1 -0
  136. package/dist/graph-facts/maven-packages.js +133 -0
  137. package/dist/graph-facts/maven-packages.js.map +1 -0
  138. package/dist/graph-facts/reach.d.ts +203 -0
  139. package/dist/graph-facts/reach.d.ts.map +1 -0
  140. package/dist/graph-facts/reach.js +219 -0
  141. package/dist/graph-facts/reach.js.map +1 -0
  142. package/dist/graph-facts/removal-reach.d.ts +73 -0
  143. package/dist/graph-facts/removal-reach.d.ts.map +1 -0
  144. package/dist/graph-facts/removal-reach.js +64 -0
  145. package/dist/graph-facts/removal-reach.js.map +1 -0
  146. package/dist/index.d.ts +21 -6
  147. package/dist/index.d.ts.map +1 -1
  148. package/dist/index.js +20 -3
  149. package/dist/index.js.map +1 -1
  150. package/dist/navigation/go-graph.d.ts +68 -0
  151. package/dist/navigation/go-graph.d.ts.map +1 -0
  152. package/dist/navigation/go-graph.js +753 -0
  153. package/dist/navigation/go-graph.js.map +1 -0
  154. package/dist/navigation/index.d.ts +7 -1
  155. package/dist/navigation/index.d.ts.map +1 -1
  156. package/dist/navigation/index.js +6 -0
  157. package/dist/navigation/index.js.map +1 -1
  158. package/dist/navigation/java-entry-tables.d.ts +43 -0
  159. package/dist/navigation/java-entry-tables.d.ts.map +1 -0
  160. package/dist/navigation/java-entry-tables.js +56 -0
  161. package/dist/navigation/java-entry-tables.js.map +1 -0
  162. package/dist/navigation/java-graph.d.ts +64 -0
  163. package/dist/navigation/java-graph.d.ts.map +1 -0
  164. package/dist/navigation/java-graph.js +466 -0
  165. package/dist/navigation/java-graph.js.map +1 -0
  166. package/dist/navigation/navigation-index.d.ts +91 -2
  167. package/dist/navigation/navigation-index.d.ts.map +1 -1
  168. package/dist/navigation/navigation-index.js +372 -45
  169. package/dist/navigation/navigation-index.js.map +1 -1
  170. package/dist/navigation/python-graph.d.ts +73 -0
  171. package/dist/navigation/python-graph.d.ts.map +1 -0
  172. package/dist/navigation/python-graph.js +867 -0
  173. package/dist/navigation/python-graph.js.map +1 -0
  174. package/dist/navigation/ts-call-edges.d.ts +193 -0
  175. package/dist/navigation/ts-call-edges.d.ts.map +1 -0
  176. package/dist/navigation/ts-call-edges.js +2089 -0
  177. package/dist/navigation/ts-call-edges.js.map +1 -0
  178. package/dist/navigation/ts-entry-tables.d.ts +88 -0
  179. package/dist/navigation/ts-entry-tables.d.ts.map +1 -0
  180. package/dist/navigation/ts-entry-tables.js +50 -0
  181. package/dist/navigation/ts-entry-tables.js.map +1 -0
  182. package/dist/path-set/branches.d.ts +52 -0
  183. package/dist/path-set/branches.d.ts.map +1 -0
  184. package/dist/path-set/branches.js +124 -0
  185. package/dist/path-set/branches.js.map +1 -0
  186. package/dist/path-set/finding-diff.d.ts +63 -0
  187. package/dist/path-set/finding-diff.d.ts.map +1 -0
  188. package/dist/path-set/finding-diff.js +150 -0
  189. package/dist/path-set/finding-diff.js.map +1 -0
  190. package/dist/path-set/index.d.ts +18 -0
  191. package/dist/path-set/index.d.ts.map +1 -0
  192. package/dist/path-set/index.js +12 -0
  193. package/dist/path-set/index.js.map +1 -0
  194. package/dist/path-set/line-diff.d.ts +19 -0
  195. package/dist/path-set/line-diff.d.ts.map +1 -0
  196. package/dist/path-set/line-diff.js +98 -0
  197. package/dist/path-set/line-diff.js.map +1 -0
  198. package/dist/path-set/path-set.d.ts +88 -0
  199. package/dist/path-set/path-set.d.ts.map +1 -0
  200. package/dist/path-set/path-set.js +716 -0
  201. package/dist/path-set/path-set.js.map +1 -0
  202. package/dist/path-set/templates.d.ts +42 -0
  203. package/dist/path-set/templates.d.ts.map +1 -0
  204. package/dist/path-set/templates.js +119 -0
  205. package/dist/path-set/templates.js.map +1 -0
  206. package/dist/path-set/types.d.ts +195 -0
  207. package/dist/path-set/types.d.ts.map +1 -0
  208. package/dist/path-set/types.js +8 -0
  209. package/dist/path-set/types.js.map +1 -0
  210. package/dist/remedy-class/index.d.ts +12 -0
  211. package/dist/remedy-class/index.d.ts.map +1 -0
  212. package/dist/remedy-class/index.js +9 -0
  213. package/dist/remedy-class/index.js.map +1 -0
  214. package/dist/remedy-class/match.d.ts +165 -0
  215. package/dist/remedy-class/match.d.ts.map +1 -0
  216. package/dist/remedy-class/match.js +705 -0
  217. package/dist/remedy-class/match.js.map +1 -0
  218. package/dist/remedy-class/output-context.d.ts +25 -0
  219. package/dist/remedy-class/output-context.d.ts.map +1 -0
  220. package/dist/remedy-class/output-context.js +150 -0
  221. package/dist/remedy-class/output-context.js.map +1 -0
  222. package/dist/remedy-class/sha256.d.ts +18 -0
  223. package/dist/remedy-class/sha256.d.ts.map +1 -0
  224. package/dist/remedy-class/sha256.js +89 -0
  225. package/dist/remedy-class/sha256.js.map +1 -0
  226. package/dist/remedy-class/table.d.ts +130 -0
  227. package/dist/remedy-class/table.d.ts.map +1 -0
  228. package/dist/remedy-class/table.js +457 -0
  229. package/dist/remedy-class/table.js.map +1 -0
  230. package/dist/resolution/cross-file.d.ts +18 -0
  231. package/dist/resolution/cross-file.d.ts.map +1 -1
  232. package/dist/resolution/cross-file.js +194 -5
  233. package/dist/resolution/cross-file.js.map +1 -1
  234. package/dist/stable-ids/builder.d.ts +20 -0
  235. package/dist/stable-ids/builder.d.ts.map +1 -0
  236. package/dist/stable-ids/builder.js +65 -0
  237. package/dist/stable-ids/builder.js.map +1 -0
  238. package/dist/stable-ids/diff.d.ts +18 -0
  239. package/dist/stable-ids/diff.d.ts.map +1 -0
  240. package/dist/stable-ids/diff.js +37 -0
  241. package/dist/stable-ids/diff.js.map +1 -0
  242. package/dist/stable-ids/go.d.ts +28 -0
  243. package/dist/stable-ids/go.d.ts.map +1 -0
  244. package/dist/stable-ids/go.js +160 -0
  245. package/dist/stable-ids/go.js.map +1 -0
  246. package/dist/stable-ids/hash.d.ts +22 -0
  247. package/dist/stable-ids/hash.d.ts.map +1 -0
  248. package/dist/stable-ids/hash.js +167 -0
  249. package/dist/stable-ids/hash.js.map +1 -0
  250. package/dist/stable-ids/index.d.ts +30 -0
  251. package/dist/stable-ids/index.d.ts.map +1 -0
  252. package/dist/stable-ids/index.js +59 -0
  253. package/dist/stable-ids/index.js.map +1 -0
  254. package/dist/stable-ids/java.d.ts +28 -0
  255. package/dist/stable-ids/java.d.ts.map +1 -0
  256. package/dist/stable-ids/java.js +224 -0
  257. package/dist/stable-ids/java.js.map +1 -0
  258. package/dist/stable-ids/moves.d.ts +87 -0
  259. package/dist/stable-ids/moves.d.ts.map +1 -0
  260. package/dist/stable-ids/moves.js +222 -0
  261. package/dist/stable-ids/moves.js.map +1 -0
  262. package/dist/stable-ids/nodes.d.ts +32 -0
  263. package/dist/stable-ids/nodes.d.ts.map +1 -0
  264. package/dist/stable-ids/nodes.js +92 -0
  265. package/dist/stable-ids/nodes.js.map +1 -0
  266. package/dist/stable-ids/normalize.d.ts +35 -0
  267. package/dist/stable-ids/normalize.d.ts.map +1 -0
  268. package/dist/stable-ids/normalize.js +174 -0
  269. package/dist/stable-ids/normalize.js.map +1 -0
  270. package/dist/stable-ids/parse-errors.d.ts +24 -0
  271. package/dist/stable-ids/parse-errors.d.ts.map +1 -0
  272. package/dist/stable-ids/parse-errors.js +46 -0
  273. package/dist/stable-ids/parse-errors.js.map +1 -0
  274. package/dist/stable-ids/python.d.ts +27 -0
  275. package/dist/stable-ids/python.d.ts.map +1 -0
  276. package/dist/stable-ids/python.js +212 -0
  277. package/dist/stable-ids/python.js.map +1 -0
  278. package/dist/stable-ids/ts-parse-errors.d.ts +27 -0
  279. package/dist/stable-ids/ts-parse-errors.d.ts.map +1 -0
  280. package/dist/stable-ids/ts-parse-errors.js +75 -0
  281. package/dist/stable-ids/ts-parse-errors.js.map +1 -0
  282. package/dist/stable-ids/types.d.ts +106 -0
  283. package/dist/stable-ids/types.d.ts.map +1 -0
  284. package/dist/stable-ids/types.js +16 -0
  285. package/dist/stable-ids/types.js.map +1 -0
  286. package/dist/stable-ids/typescript.d.ts +30 -0
  287. package/dist/stable-ids/typescript.d.ts.map +1 -0
  288. package/dist/stable-ids/typescript.js +362 -0
  289. package/dist/stable-ids/typescript.js.map +1 -0
  290. package/dist/types/config.d.ts +9 -0
  291. package/dist/types/config.d.ts.map +1 -1
  292. package/dist/types/index.d.ts +53 -0
  293. package/dist/types/index.d.ts.map +1 -1
  294. package/dist/types/index.js.map +1 -1
  295. package/package.json +3 -3
@@ -290,6 +290,106 @@ function argvFirstLiteralIsShell(lineText) {
290
290
  .toLowerCase();
291
291
  return SHELL_PROGRAMS.has(base);
292
292
  }
293
+ /** Last RHS assigned to `varName` on a line before `beforeLine` inside the method span. */
294
+ function javaLocalInitRhsBeforeLine(varName, beforeLine, methodStart, methodEnd, sourceLines) {
295
+ const declAssign = new RegExp(`^\\s*(?:final\\s+)?[\\w<>,\\s\\[\\]]+\\s+${escapeReSf(varName)}\\s*=\\s*(.+?)\\s*;\\s*(?://.*)?$`);
296
+ const reassign = new RegExp(`^\\s*${escapeReSf(varName)}\\s*=\\s*(.+?)\\s*;\\s*(?://.*)?$`);
297
+ let lastRhs;
298
+ for (let l = methodStart; l < beforeLine && l <= methodEnd; l++) {
299
+ const line = (sourceLines[l - 1] ?? '').trim();
300
+ const m = declAssign.exec(line) ?? reassign.exec(line);
301
+ if (m)
302
+ lastRhs = m[1].trim();
303
+ }
304
+ return lastRhs;
305
+ }
306
+ function callArgLooksLikeShellArgv(expr, probePrefix) {
307
+ return argvFirstLiteralIsShell(`${probePrefix}${expr}`);
308
+ }
309
+ function parseSimpleJavaCall(expr) {
310
+ const trimmed = expr.trim();
311
+ const m = /^([\w][\w.]*)\s*\(/.exec(trimmed);
312
+ if (!m)
313
+ return null;
314
+ const head = m[1];
315
+ const dot = head.lastIndexOf('.');
316
+ if (dot < 0)
317
+ return { method: head };
318
+ return { qualifier: head.slice(0, dot), method: head.slice(dot + 1) };
319
+ }
320
+ function javaMethodBodyReturnLooksLikeShellArgv(methodStart, methodEnd, sourceLines) {
321
+ for (let l = methodStart; l <= methodEnd; l++) {
322
+ const line = (sourceLines[l - 1] ?? '').trim();
323
+ const ret = /\breturn\s+(.+?)\s*;/.exec(line);
324
+ if (!ret)
325
+ continue;
326
+ if (callArgLooksLikeShellArgv(ret[1].trim(), 'new ProcessBuilder('))
327
+ return true;
328
+ }
329
+ return false;
330
+ }
331
+ function javaResolveCalleeMethodLines(methodName, qualifier, refLine, types, sourceLines, ctx) {
332
+ const enclosing = types.find(t => refLine >= t.start_line && refLine <= t.end_line);
333
+ const typeName = qualifier
334
+ ? (qualifier.includes('.') ? qualifier.split('.').pop() : qualifier)
335
+ : enclosing?.name;
336
+ if (!typeName)
337
+ return undefined;
338
+ const tryType = (t, lines) => {
339
+ const mm = t.methods.find(m => m.name === methodName);
340
+ if (!mm)
341
+ return undefined;
342
+ return { start: mm.start_line, end: mm.end_line, lines };
343
+ };
344
+ if (ctx?.projectGraph && ctx.sourceLinesByFile) {
345
+ for (const fp of ctx.projectGraph.filePaths) {
346
+ const ir = ctx.projectGraph.getIR(fp);
347
+ if (!ir)
348
+ continue;
349
+ const t = ir.types.find(tt => tt.name === typeName);
350
+ if (!t)
351
+ continue;
352
+ const lines = ctx.sourceLinesByFile.get(fp);
353
+ if (!lines)
354
+ continue;
355
+ const hit = tryType(t, lines);
356
+ if (hit)
357
+ return hit;
358
+ }
359
+ }
360
+ const local = types.find(t => t.name === typeName);
361
+ if (local)
362
+ return tryType(local, sourceLines);
363
+ return undefined;
364
+ }
365
+ function javaExpressionLooksLikeShellArgv(expr, refLine, types, sourceLines, ctx) {
366
+ if (callArgLooksLikeShellArgv(expr, 'new ProcessBuilder('))
367
+ return true;
368
+ const parsed = parseSimpleJavaCall(expr);
369
+ if (!parsed)
370
+ return false;
371
+ const body = javaResolveCalleeMethodLines(parsed.method, parsed.qualifier, refLine, types, sourceLines, ctx);
372
+ if (!body)
373
+ return false;
374
+ return javaMethodBodyReturnLooksLikeShellArgv(body.start, body.end, body.lines);
375
+ }
376
+ /** cognium-dev#626 — shell argv written to instance field `fieldName` before `beforeLine`. */
377
+ function javaFieldShellArgvWrittenBeforeLine(fieldName, beforeLine, methodStart, methodEnd, sourceLines, types, ctx) {
378
+ const cap = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
379
+ const beanSetter = new RegExp(`\\bset${escapeReSf(cap)}\\s*\\((.+)\\)\\s*;?\\s*$`);
380
+ const fieldAssign = new RegExp(`^\\s*(?:this\\.)?${escapeReSf(fieldName)}\\s*=\\s*(.+?)\\s*;\\s*(?://.*)?$`);
381
+ for (let l = methodStart; l < beforeLine && l <= methodEnd; l++) {
382
+ const line = (sourceLines[l - 1] ?? '').trim();
383
+ const sm = beanSetter.exec(line);
384
+ if (sm && javaExpressionLooksLikeShellArgv(sm[1].trim(), l, types, sourceLines, ctx)) {
385
+ return true;
386
+ }
387
+ const fm = fieldAssign.exec(line);
388
+ if (fm && callArgLooksLikeShellArgv(fm[1].trim(), 'new ProcessBuilder('))
389
+ return true;
390
+ }
391
+ return false;
392
+ }
293
393
  /**
294
394
  * cognium-dev#626 — does a same-file call of the wrapper method that contains
295
395
  * the sink pass a shell argv (`Arrays.asList("/bin/sh", "-c", ...)`,
@@ -308,7 +408,7 @@ function argvFirstLiteralIsShell(lineText) {
308
408
  * type is found by its line range). A parameter that is not found answers
309
409
  * `false` and today's suppression stands.
310
410
  */
311
- function sameFileCallerPassesShellArgv(paramName, methodName, sinkLine, types, calls) {
411
+ function sameFileCallerPassesShellArgv(paramName, methodName, sinkLine, types, calls, sourceLines) {
312
412
  if (!methodName)
313
413
  return false;
314
414
  for (const t of types) {
@@ -316,8 +416,25 @@ function sameFileCallerPassesShellArgv(paramName, methodName, sinkLine, types, c
316
416
  if (!m)
317
417
  continue;
318
418
  const pos = m.parameters.findIndex(p => p.name === paramName);
319
- if (pos < 0)
320
- return false;
419
+ if (pos < 0) {
420
+ const isField = t.fields?.some(f => f.name === paramName);
421
+ if (!isField)
422
+ return false;
423
+ return calls.some(c => {
424
+ if (c.method_name !== methodName)
425
+ return false;
426
+ if (c.receiver != null && c.receiver !== 'this')
427
+ return false;
428
+ if (c.location.line < t.start_line || c.location.line > t.end_line)
429
+ return false;
430
+ if (c.arguments.length !== m.parameters.length)
431
+ return false;
432
+ const callerMethod = t.methods.find(mm => c.location.line >= mm.start_line && c.location.line <= mm.end_line);
433
+ if (!callerMethod)
434
+ return false;
435
+ return javaFieldShellArgvWrittenBeforeLine(paramName, c.location.line, callerMethod.start_line, callerMethod.end_line, sourceLines, types);
436
+ });
437
+ }
321
438
  return calls.some(c => {
322
439
  if (c.method_name !== methodName)
323
440
  return false;
@@ -338,12 +455,462 @@ function sameFileCallerPassesShellArgv(paramName, methodName, sinkLine, types, c
338
455
  u.methods.some(um => um.name === methodName && um.parameters.length === c.arguments.length));
339
456
  if (shadowed)
340
457
  return false;
341
- const expr = c.arguments[pos]?.expression;
342
- return typeof expr === 'string' && argvFirstLiteralIsShell(`new ProcessBuilder(${expr}`);
458
+ const expr = (c.arguments[pos]?.expression ?? '').trim();
459
+ if (!expr)
460
+ return false;
461
+ if (javaExpressionLooksLikeShellArgv(expr, c.location.line, types, sourceLines)) {
462
+ return true;
463
+ }
464
+ const argVar = c.arguments[pos]?.variable;
465
+ if (argVar && expr === argVar && !/[.(]/.test(expr)) {
466
+ const callerMethod = t.methods.find(mm => c.location.line >= mm.start_line && c.location.line <= mm.end_line);
467
+ if (callerMethod) {
468
+ const rhs = javaLocalInitRhsBeforeLine(argVar, c.location.line, callerMethod.start_line, callerMethod.end_line, sourceLines);
469
+ if (rhs && javaExpressionLooksLikeShellArgv(rhs, c.location.line, types, sourceLines)) {
470
+ return true;
471
+ }
472
+ }
473
+ }
474
+ return false;
343
475
  });
344
476
  }
345
477
  return false;
346
478
  }
479
+ function callArgumentIsShellArgv(call, argPos, types, sourceLines, ctx) {
480
+ const expr = (call.arguments[argPos]?.expression ?? '').trim();
481
+ if (!expr)
482
+ return false;
483
+ if (javaExpressionLooksLikeShellArgv(expr, call.location.line, types, sourceLines, ctx)) {
484
+ return true;
485
+ }
486
+ const argVar = call.arguments[argPos]?.variable;
487
+ if (argVar && expr === argVar && !/[.(]/.test(expr)) {
488
+ const callerType = types.find(t => call.location.line >= t.start_line && call.location.line <= t.end_line);
489
+ const callerMethod = callerType?.methods.find(mm => call.location.line >= mm.start_line && call.location.line <= mm.end_line);
490
+ if (callerMethod) {
491
+ const rhs = javaLocalInitRhsBeforeLine(argVar, call.location.line, callerMethod.start_line, callerMethod.end_line, sourceLines);
492
+ if (rhs && javaExpressionLooksLikeShellArgv(rhs, call.location.line, types, sourceLines, ctx)) {
493
+ return true;
494
+ }
495
+ }
496
+ }
497
+ return false;
498
+ }
499
+ function projectCallerPassesShellArgvForWrapper(paramName, wrapperMethod, sinkLine, calleeTypes, calleeCalls, calleeLines, projectGraph, sourceLinesByFile, getCallerContext, forEachCrossFileCall) {
500
+ const shellArgvCtx = { projectGraph, sourceLinesByFile };
501
+ if (sameFileCallerPassesShellArgv(paramName, wrapperMethod, sinkLine, calleeTypes, calleeCalls, calleeLines)) {
502
+ return true;
503
+ }
504
+ if (!wrapperMethod)
505
+ return false;
506
+ const calleeMethod = calleeTypes
507
+ .flatMap(t => t.methods)
508
+ .find(mm => mm.name === wrapperMethod && sinkLine >= mm.start_line && sinkLine <= mm.end_line);
509
+ const pos = calleeMethod?.parameters.findIndex(p => p.name === paramName) ?? -1;
510
+ if (pos < 0) {
511
+ if (!calleeMethod
512
+ || !javaFieldArgvAtSink(projectGraph, calleeTypes, paramName, wrapperMethod, sinkLine)) {
513
+ return false;
514
+ }
515
+ let found = false;
516
+ const setterName = `set${paramName.charAt(0).toUpperCase()}${paramName.slice(1)}`;
517
+ forEachCrossFileCall((callerFile, call, targetMethod) => {
518
+ const ctx = getCallerContext(callerFile);
519
+ if (!ctx)
520
+ return;
521
+ const callerMethod = ctx.types
522
+ .flatMap(t => t.methods)
523
+ .find(mm => call.location.line >= mm.start_line && call.location.line <= mm.end_line);
524
+ if (!callerMethod)
525
+ return;
526
+ if (targetMethod.endsWith(`.${setterName}`) || targetMethod.endsWith(setterName)) {
527
+ if (call.arguments.length > 0
528
+ && callArgumentIsShellArgv(call, 0, ctx.types, ctx.lines, shellArgvCtx)) {
529
+ found = true;
530
+ }
531
+ return;
532
+ }
533
+ if (!wrapperMethodMatchesTarget(wrapperMethod, targetMethod))
534
+ return;
535
+ if (call.arguments.length !== calleeMethod.parameters.length)
536
+ return;
537
+ if (javaFieldShellArgvWrittenBeforeLine(paramName, call.location.line, callerMethod.start_line, callerMethod.end_line, ctx.lines, ctx.types, shellArgvCtx)) {
538
+ found = true;
539
+ }
540
+ });
541
+ return found;
542
+ }
543
+ let found = false;
544
+ forEachCrossFileCall((callerFile, call, targetMethod) => {
545
+ if (!targetMethod.endsWith(`.${wrapperMethod}`) && !targetMethod.endsWith(wrapperMethod))
546
+ return;
547
+ if (call.arguments.length <= pos)
548
+ return;
549
+ const ctx = getCallerContext(callerFile);
550
+ if (!ctx)
551
+ return;
552
+ if (callArgumentIsShellArgv(call, pos, ctx.types, ctx.lines, shellArgvCtx))
553
+ found = true;
554
+ });
555
+ return found;
556
+ }
557
+ function pickCallerTaintSource(callerIr, beforeLine) {
558
+ const fromFlow = (callerIr.taint.flows ?? [])
559
+ .filter(f => f.sink_line <= beforeLine && f.source_line <= beforeLine)
560
+ .sort((a, b) => b.source_line - a.source_line)[0];
561
+ if (fromFlow) {
562
+ return { line: fromFlow.source_line, type: fromFlow.source_type };
563
+ }
564
+ const fromSource = (callerIr.taint.sources ?? [])
565
+ .filter(s => s.line <= beforeLine)
566
+ .sort((a, b) => b.line - a.line)[0];
567
+ if (fromSource) {
568
+ return { line: fromSource.line, type: fromSource.type };
569
+ }
570
+ return undefined;
571
+ }
572
+ function wrapperMethodMatchesTarget(wrapperMethod, targetMethod) {
573
+ return targetMethod.endsWith(`.${wrapperMethod}`) || targetMethod.endsWith(wrapperMethod);
574
+ }
575
+ /** Type that owns `wrapperMethod` at `sinkLine` in `types`. */
576
+ function javaHostTypeAtSink(types, wrapperMethod, sinkLine) {
577
+ for (const t of types) {
578
+ if (t.methods.some(mm => mm.name === wrapperMethod && sinkLine >= mm.start_line && sinkLine <= mm.end_line)) {
579
+ return t.name;
580
+ }
581
+ }
582
+ return undefined;
583
+ }
584
+ /** Field on `typeName` or a supertype declared anywhere in the project. */
585
+ function javaProjectTypeDeclaresField(projectGraph, typeName, fieldName) {
586
+ const seen = new Set();
587
+ const queue = [typeName];
588
+ while (queue.length > 0) {
589
+ const tn = queue.pop();
590
+ if (seen.has(tn))
591
+ continue;
592
+ seen.add(tn);
593
+ for (const fp of projectGraph.filePaths) {
594
+ const ir = projectGraph.getIR(fp);
595
+ if (!ir)
596
+ continue;
597
+ for (const t of ir.types) {
598
+ if (t.name !== tn)
599
+ continue;
600
+ if (t.fields?.some(f => f.name === fieldName))
601
+ return true;
602
+ if (t.extends)
603
+ queue.push(t.extends);
604
+ }
605
+ }
606
+ }
607
+ return false;
608
+ }
609
+ /** Callee files for cross-file argv recall: sink file plus supertype declaration files. */
610
+ function javaRelatedCalleeFiles(projectGraph, primaryCalleeFile, hostTypeName) {
611
+ const files = new Set([primaryCalleeFile]);
612
+ if (!hostTypeName)
613
+ return [...files];
614
+ const seen = new Set();
615
+ const queue = [hostTypeName];
616
+ while (queue.length > 0) {
617
+ const tn = queue.pop();
618
+ if (seen.has(tn))
619
+ continue;
620
+ seen.add(tn);
621
+ for (const fp of projectGraph.filePaths) {
622
+ const ir = projectGraph.getIR(fp);
623
+ const t = ir?.types.find(tt => tt.name === tn);
624
+ if (!t)
625
+ continue;
626
+ files.add(fp);
627
+ if (t.extends)
628
+ queue.push(t.extends);
629
+ }
630
+ }
631
+ return [...files];
632
+ }
633
+ function javaFieldArgvAtSink(projectGraph, calleeTypes, paramName, wrapperMethod, sinkLine) {
634
+ const hostType = javaHostTypeAtSink(calleeTypes, wrapperMethod, sinkLine);
635
+ if (!hostType)
636
+ return false;
637
+ return javaProjectTypeDeclaresField(projectGraph, hostType, paramName);
638
+ }
639
+ function javaProjectFieldType(projectGraph, typeName, fieldName) {
640
+ const seen = new Set();
641
+ const queue = [typeName];
642
+ while (queue.length > 0) {
643
+ const tn = queue.pop();
644
+ if (seen.has(tn))
645
+ continue;
646
+ seen.add(tn);
647
+ for (const fp of projectGraph.filePaths) {
648
+ const ir = projectGraph.getIR(fp);
649
+ if (!ir)
650
+ continue;
651
+ for (const t of ir.types) {
652
+ if (t.name !== tn)
653
+ continue;
654
+ const f = t.fields?.find(ff => ff.name === fieldName);
655
+ if (f?.type)
656
+ return f.type;
657
+ if (t.extends)
658
+ queue.push(t.extends);
659
+ }
660
+ }
661
+ }
662
+ return null;
663
+ }
664
+ function resolveJavaArgvIdentifierType(varName, wrapperMethod, sinkLine, types, projectGraph) {
665
+ const direct = resolveIdentifierType(varName, wrapperMethod, [...types]);
666
+ if (direct)
667
+ return direct;
668
+ if (!wrapperMethod)
669
+ return null;
670
+ const hostType = javaHostTypeAtSink(types, wrapperMethod, sinkLine);
671
+ if (!hostType)
672
+ return null;
673
+ return javaProjectFieldType(projectGraph, hostType, varName);
674
+ }
675
+ /**
676
+ * cognium-dev#626 — per-file argv suppression drops the ProcessBuilder sink on
677
+ * isolated `List<String>` wrappers; restore cross-file recall by synthesizing
678
+ * flows (and project taint paths) when a resolved caller passes shell argv.
679
+ */
680
+ export function synthesizeCrossFileArgvWrapperFlows(fileAnalyses, projectGraph, sourceLinesByFile, taintPaths) {
681
+ const getCallerContext = (file) => {
682
+ const ir = projectGraph.getIR(file);
683
+ if (!ir)
684
+ return undefined;
685
+ return {
686
+ types: ir.types,
687
+ calls: ir.calls,
688
+ lines: sourceLinesByFile.get(file) ?? [],
689
+ ir,
690
+ };
691
+ };
692
+ const forEachCrossFileCall = (calleeFile, cb) => {
693
+ for (const fromFile of projectGraph.filePaths) {
694
+ if (fromFile === calleeFile)
695
+ continue;
696
+ for (const rc of projectGraph.resolver.getResolvedCallsFromFile(fromFile)) {
697
+ if (rc.targetFile !== calleeFile)
698
+ continue;
699
+ cb(fromFile, rc.call, rc.targetMethod);
700
+ }
701
+ }
702
+ };
703
+ for (const fa of fileAnalyses) {
704
+ const ir = fa.analysis;
705
+ if (ir.meta.language !== 'java')
706
+ continue;
707
+ const lines = sourceLinesByFile.get(fa.file) ?? [];
708
+ for (const ctorCall of ir.calls) {
709
+ if (ctorCall.method_name !== 'ProcessBuilder')
710
+ continue;
711
+ if (ctorCall.receiver != null && ctorCall.receiver !== undefined)
712
+ continue;
713
+ const sinkLine = ctorCall.location.line;
714
+ const sinkLineText = lines[sinkLine - 1] ?? '';
715
+ if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText))
716
+ continue;
717
+ if (argvFirstLiteralIsShell(sinkLineText))
718
+ continue;
719
+ const arg0 = ctorCall.arguments[0];
720
+ if (!arg0)
721
+ continue;
722
+ const varName = arg0.variable ?? '';
723
+ const argExpr = (arg0.expression ?? '').trim();
724
+ if (!varName || argExpr !== varName || /[(.]/.test(argExpr))
725
+ continue;
726
+ const wrapperMethod = ctorCall.in_method;
727
+ const t = resolveJavaArgvIdentifierType(varName, wrapperMethod, sinkLine, ir.types, projectGraph);
728
+ if (!isArgvContainerType(t))
729
+ continue;
730
+ if (sameFileCallerPassesShellArgv(varName, wrapperMethod, sinkLine, ir.types, ir.calls, lines)) {
731
+ continue;
732
+ }
733
+ const calleeMethod = ir.types
734
+ .flatMap(ty => ty.methods)
735
+ .find(mm => mm.name === wrapperMethod && sinkLine >= mm.start_line && sinkLine <= mm.end_line);
736
+ const pos = calleeMethod?.parameters.findIndex(p => p.name === varName) ?? -1;
737
+ const hostType = wrapperMethod
738
+ ? javaHostTypeAtSink(ir.types, wrapperMethod, sinkLine)
739
+ : undefined;
740
+ const fieldArgv = pos < 0
741
+ && wrapperMethod
742
+ && javaFieldArgvAtSink(projectGraph, ir.types, varName, wrapperMethod, sinkLine);
743
+ if (pos < 0 && !fieldArgv)
744
+ continue;
745
+ const relatedCalleeFiles = javaRelatedCalleeFiles(projectGraph, fa.file, hostType);
746
+ const shellArgvCtx = { projectGraph, sourceLinesByFile };
747
+ for (const calleeFile of relatedCalleeFiles) {
748
+ forEachCrossFileCall(calleeFile, (callerFile, call, targetMethod) => {
749
+ const ctx = getCallerContext(callerFile);
750
+ if (!ctx)
751
+ return;
752
+ const setterName = `set${varName.charAt(0).toUpperCase()}${varName.slice(1)}`;
753
+ let shellProof = false;
754
+ if (pos >= 0) {
755
+ if (!wrapperMethod || !wrapperMethodMatchesTarget(wrapperMethod, targetMethod))
756
+ return;
757
+ if (call.arguments.length <= pos)
758
+ return;
759
+ shellProof = callArgumentIsShellArgv(call, pos, ctx.types, ctx.lines, shellArgvCtx);
760
+ }
761
+ else {
762
+ const callerMethod = ctx.types
763
+ .flatMap(ty => ty.methods)
764
+ .find(mm => call.location.line >= mm.start_line && call.location.line <= mm.end_line);
765
+ if (!callerMethod || !calleeMethod)
766
+ return;
767
+ if (targetMethod.endsWith(`.${setterName}`) || targetMethod.endsWith(setterName)) {
768
+ shellProof =
769
+ call.arguments.length > 0
770
+ && callArgumentIsShellArgv(call, 0, ctx.types, ctx.lines, shellArgvCtx);
771
+ }
772
+ else if (wrapperMethod && wrapperMethodMatchesTarget(wrapperMethod, targetMethod)) {
773
+ shellProof =
774
+ call.arguments.length === calleeMethod.parameters.length
775
+ && javaFieldShellArgvWrittenBeforeLine(varName, call.location.line, callerMethod.start_line, callerMethod.end_line, ctx.lines, ctx.types, shellArgvCtx);
776
+ }
777
+ }
778
+ if (!shellProof)
779
+ return;
780
+ const source = pickCallerTaintSource(ctx.ir, call.location.line);
781
+ if (!source)
782
+ return;
783
+ const flows = ir.taint.flows ?? [];
784
+ const flowExists = flows.some(f => f.sink_type === 'command_injection'
785
+ && f.sink_line === sinkLine
786
+ && f.source_line === source.line
787
+ && f.source_type === source.type);
788
+ if (!flowExists) {
789
+ flows.push({
790
+ source_line: source.line,
791
+ sink_line: sinkLine,
792
+ source_type: source.type,
793
+ sink_type: 'command_injection',
794
+ path: [
795
+ { variable: '', line: source.line, type: 'source' },
796
+ { variable: varName, line: sinkLine, type: 'sink' },
797
+ ],
798
+ confidence: 0.7,
799
+ sanitized: false,
800
+ });
801
+ ir.taint.flows = flows;
802
+ }
803
+ const pathDup = taintPaths.some(tp => tp.source.file === callerFile
804
+ && tp.source.line === source.line
805
+ && tp.sink.file === fa.file
806
+ && tp.sink.line === sinkLine
807
+ && tp.sink.type === 'command_injection');
808
+ if (pathDup)
809
+ return;
810
+ const callerLines = sourceLinesByFile.get(callerFile) ?? [];
811
+ const dupId = `${callerFile}:${source.line}→${fa.file}:${sinkLine}`;
812
+ taintPaths.push({
813
+ id: `cf-argv-626-${dupId}`,
814
+ source: {
815
+ file: callerFile,
816
+ line: source.line,
817
+ type: source.type,
818
+ code: callerLines[source.line - 1] ?? '',
819
+ },
820
+ sink: {
821
+ file: fa.file,
822
+ line: sinkLine,
823
+ type: 'command_injection',
824
+ cwe: 'CWE-78',
825
+ code: sinkLineText,
826
+ },
827
+ hops: [
828
+ {
829
+ file: callerFile,
830
+ method: call.in_method ?? '',
831
+ line: call.location.line,
832
+ code: callerLines[call.location.line - 1] ?? '',
833
+ variable: '',
834
+ },
835
+ {
836
+ file: fa.file,
837
+ method: wrapperMethod ?? '',
838
+ line: sinkLine,
839
+ code: sinkLineText,
840
+ variable: varName,
841
+ },
842
+ ],
843
+ sanitizers_in_path: [],
844
+ path_exists: true,
845
+ confidence: 0.7,
846
+ });
847
+ });
848
+ }
849
+ }
850
+ }
851
+ }
852
+ /** cognium-dev#626 — drop argv-wrapper flows with no shell-argv caller (incl. cross-file). */
853
+ export function filterJavaArgvWrapperFlowsWithoutShellCaller(fileAnalyses, projectGraph, sourceLinesByFile) {
854
+ const getCallerContext = (file) => {
855
+ const ir = projectGraph.getIR(file);
856
+ if (!ir)
857
+ return undefined;
858
+ return {
859
+ types: ir.types,
860
+ calls: ir.calls,
861
+ lines: sourceLinesByFile.get(file) ?? [],
862
+ };
863
+ };
864
+ const forEachCrossFileCall = (calleeFile, cb) => {
865
+ for (const fromFile of projectGraph.filePaths) {
866
+ if (fromFile === calleeFile)
867
+ continue;
868
+ for (const rc of projectGraph.resolver.getResolvedCallsFromFile(fromFile)) {
869
+ if (rc.targetFile !== calleeFile)
870
+ continue;
871
+ cb(fromFile, rc.call, rc.targetMethod);
872
+ }
873
+ }
874
+ };
875
+ for (const fa of fileAnalyses) {
876
+ const ir = fa.analysis;
877
+ if (ir.meta.language !== 'java')
878
+ continue;
879
+ const lines = sourceLinesByFile.get(fa.file) ?? [];
880
+ const flows = ir.taint.flows ?? [];
881
+ ir.taint.flows = flows.filter(flow => {
882
+ if (flow.sink_type !== 'command_injection')
883
+ return true;
884
+ const sinkLineText = lines[flow.sink_line - 1] ?? '';
885
+ if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText))
886
+ return true;
887
+ const ctorCall = ir.calls.find(c => c.method_name === 'ProcessBuilder' &&
888
+ c.location.line === flow.sink_line &&
889
+ (c.receiver === null || c.receiver === undefined));
890
+ if (!ctorCall)
891
+ return true;
892
+ const arg0 = ctorCall.arguments[0];
893
+ if (!arg0)
894
+ return true;
895
+ const varName = arg0.variable ?? '';
896
+ const argExpr = (arg0.expression ?? '').trim();
897
+ if (!varName || argExpr !== varName || /[(.]/.test(argExpr))
898
+ return true;
899
+ const t = resolveJavaArgvIdentifierType(varName, ctorCall.in_method, flow.sink_line, ir.types, projectGraph);
900
+ if (!isArgvContainerType(t))
901
+ return true;
902
+ const hostType = ctorCall.in_method
903
+ ? javaHostTypeAtSink(ir.types, ctorCall.in_method, flow.sink_line)
904
+ : undefined;
905
+ const relatedCalleeFiles = javaRelatedCalleeFiles(projectGraph, fa.file, hostType);
906
+ return projectCallerPassesShellArgvForWrapper(varName, ctorCall.in_method, flow.sink_line, ir.types, ir.calls, lines, projectGraph, sourceLinesByFile, getCallerContext, cb => {
907
+ for (const calleeFile of relatedCalleeFiles) {
908
+ forEachCrossFileCall(calleeFile, cb);
909
+ }
910
+ });
911
+ });
912
+ }
913
+ }
347
914
  // ---------------------------------------------------------------------------
348
915
  // Stage 12 — Java throw-statement FP suppression.
349
916
  // (cognium-dev #157 — Sprint 45)
@@ -490,6 +1057,23 @@ const FETCH_MEMBER_RECEIVER_RE = /[\w$)\]]\s*\.\s*fetch\s*\(/;
490
1057
  // unaffected.
491
1058
  const NOSQL_ARRAY_METHODS = new Set(['find', 'filter', 'some', 'every', 'findIndex', 'findLast']);
492
1059
  const FUNCTION_FIRST_ARG_RE = /\.\s*(?:find|filter|some|every|findIndex|findLast)\s*\(\s*(?:async\s+)?(?:function\b|(?:\([^()]*\)|[A-Za-z_$][\w$]*)\s*=>)/;
1060
+ // cognium-dev#1000 — `recv.find(name)` with a single bare identifier argument.
1061
+ const NAMED_ARRAY_PREDICATE_RE = /([A-Za-z_$][\w$]*)\s*\.\s*(find|filter|some|every|findIndex|findLast)\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/;
1062
+ /** A function declared in this file under `name` (function / const arrow / function expression). */
1063
+ function isDeclaredFunctionName(name, lines) {
1064
+ const n = name.replace(/[$]/g, '\\$');
1065
+ const decls = [
1066
+ new RegExp(`\\bfunction\\s+${n}\\s*[(<]`),
1067
+ new RegExp(`\\b(?:const|let|var)\\s+${n}\\s*(?::[^=]*)?=\\s*(?:async\\s+)?(?:function\\b|\\(|[A-Za-z_$][\\w$]*\\s*=>)`),
1068
+ ];
1069
+ return lines.some(l => decls.some(re => re.test(l)));
1070
+ }
1071
+ /** `name` declared as an array in this file: `= [`, `: T[]`, `Array<…>`, `new Array`, `Array.from`. */
1072
+ function isDeclaredArrayName(name, lines) {
1073
+ const n = name.replace(/[$]/g, '\\$');
1074
+ const re = new RegExp(`\\b${n}\\s*(?:\\??\\s*:\\s*(?:readonly\\s+)?(?:[\\w$.<>,\\s]+\\[\\]|(?:Readonly)?Array\\s*<)|(?::[^=;]*)?=\\s*(?:\\[|new\\s+Array\\b|Array\\.(?:from|of)\\b))`);
1075
+ return lines.some(l => re.test(l));
1076
+ }
493
1077
  // cognium-ai#279 R-1 — an argument that is an object literal with an ORM
494
1078
  // query-builder key (`where`/`select`/`populate`/…) is a *builder*, not a raw
495
1079
  // query string: a value in a `{ field, value }` position binds as a parameter
@@ -519,6 +1103,23 @@ const SERVER_SIGNAL_RE = /["']use server["']|\b(?:getServerSideProps|getStaticPr
519
1103
  const FIXED_HOST_TEMPLATE_RE = /`https?:\/\/[^/${}\s`]+[/`]/;
520
1104
  /** A bash URL word whose scheme and host are literal and closed by `/` (#495). */
521
1105
  const BASH_FIXED_HOST_URL_RE = /^(?:https?|ftp):\/\/[A-Za-z0-9.-]+(?::\d+)?\//;
1106
+ /** Rust `format!("https://host/{}", path)` — literal origin, path-only interpolation (#922). */
1107
+ const RUST_FIXED_HOST_FORMAT_RE = /format!\(\s*"https?:\/\/[^"{}]+\/\{\}"/;
1108
+ function rustSsrfRequestUsesFixedHostFormat(code, sinkLine) {
1109
+ const lines = code.split('\n');
1110
+ const sinkText = lines[sinkLine - 1] ?? '';
1111
+ const argMatch = sinkText.match(/(?:reqwest::)?get\s*\(\s*&?(\w+)/);
1112
+ if (!argMatch)
1113
+ return false;
1114
+ const varName = argMatch[1];
1115
+ const assignRe = new RegExp(`\\blet\\s+${escapeRegex(varName)}\\s*=\\s*format!`);
1116
+ for (let i = sinkLine - 2; i >= Math.max(0, sinkLine - 20); i--) {
1117
+ const line = lines[i] ?? '';
1118
+ if (assignRe.test(line) && RUST_FIXED_HOST_FORMAT_RE.test(line))
1119
+ return true;
1120
+ }
1121
+ return false;
1122
+ }
522
1123
  /**
523
1124
  * True when the string literal that ended at `end` is the WHOLE initializer:
524
1125
  * the next code token is `;`, `,`, end of input, or (ASI) a token on a later
@@ -817,7 +1418,10 @@ function isUnwrittenLocalMapKeyRead(code, source) {
817
1418
  return false;
818
1419
  const [, recv, key] = read;
819
1420
  // (a) locally constructed map
820
- const ctorRe = new RegExp(`\\b${recv}\\s*=\\s*new\\s+(?:Hash|Tree|LinkedHash|ConcurrentHash|Concurrent|Identity|Weak)?Map\\s*(?:<[^>]*>)?\\s*\\(`);
1421
+ const ctorRe = new RegExp(
1422
+ // `new HashMap<>()` or the qualified `new java.util.HashMap<>()` (OWASP
1423
+ // BenchmarkJava writes the qualified form throughout).
1424
+ `\\b${recv}\\s*=\\s*new\\s+(?:java\\.util\\.(?:concurrent\\.)?)?(?:Hash|Tree|LinkedHash|ConcurrentHash|Concurrent|Identity|Weak)?Map\\s*(?:<[^>]*>)?\\s*\\(`);
821
1425
  if (!lines.some(l => ctorRe.test(l)))
822
1426
  return false;
823
1427
  // (b) no escape: the receiver must not appear inside any call's arguments.
@@ -831,10 +1435,11 @@ function isUnwrittenLocalMapKeyRead(code, source) {
831
1435
  if (escapeRe.test(stripped))
832
1436
  return false;
833
1437
  }
834
- // (c2) was this literal key ever written? If so, drop only a literal-only map.
1438
+ // (c2) was this literal key ever written? If so, drop when that key's puts are
1439
+ // all literals (#584 key-shuffle), even if another key holds tainted data.
835
1440
  const putRe = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`);
836
1441
  if (lines.some(l => putRe.test(l)))
837
- return isLiteralOnlyLocalMap(lines, recv);
1442
+ return isLiteralOnlyLocalMapKey(lines, recv, key);
838
1443
  return true;
839
1444
  }
840
1445
  /**
@@ -847,6 +1452,51 @@ function isUnwrittenLocalMapKeyRead(code, source) {
847
1452
  * a value out of sight. Combined with (a) local construction and (b) no escape
848
1453
  * in the caller, a read of such a map yields a literal and is not a source.
849
1454
  */
1455
+ /** Every `put` of `key` on `recv` stores a string literal (other keys ignored). */
1456
+ function isLiteralOnlyLocalMapKey(lines, recv, key) {
1457
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1458
+ const otherMutator = new RegExp(`\\b${recv}\\s*\\.\\s*(?:putAll|putIfAbsent|compute|computeIfAbsent|computeIfPresent|merge|replace|replaceAll|entrySet|values)\\s*\\(`);
1459
+ if (lines.some(l => otherMutator.test(l)))
1460
+ return false;
1461
+ const keyPut = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${escapedKey}"\\s*,\\s*"(?:[^"\\\\]|\\\\.)*"\\s*\\)`);
1462
+ const anyKeyPut = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*"${escapedKey}"`);
1463
+ let putsForKey = 0;
1464
+ let literalPutsForKey = 0;
1465
+ for (const l of lines) {
1466
+ putsForKey += (l.match(anyKeyPut) ?? []).length;
1467
+ literalPutsForKey += (l.match(keyPut) ?? []).length;
1468
+ }
1469
+ return putsForKey > 0 && putsForKey === literalPutsForKey;
1470
+ }
1471
+ /**
1472
+ * cognium-dev#818 (src14) / #888 — `class: 'cache'` seeds `cache.get()` as db_input for
1473
+ * redis `createClient()` bindings. Drop in-memory shapes: local `new Map()`, object
1474
+ * property caches (`scope.cache.get`), and a bare `cache` variable not assigned from
1475
+ * `createClient()`.
1476
+ */
1477
+ function isJsDisguisedCacheDbInputRead(code, source) {
1478
+ if (source.type !== 'db_input')
1479
+ return false;
1480
+ const loc = source.location ?? '';
1481
+ if (!/\.cache\.get\s*\(/i.test(loc) && !/\bcache\.get\s*\(/i.test(loc))
1482
+ return false;
1483
+ const lines = code.split('\n');
1484
+ const lineText = lines[source.line - 1] ?? '';
1485
+ // In-object texture / loader caches: `scope.cache.get`, `this.cache.get`, etc.
1486
+ if (/\w+\.cache\.get\s*\(/.test(lineText))
1487
+ return true;
1488
+ const read = /\b([A-Za-z_]\w*)\s*\.\s*get\s*\(/.exec(lineText);
1489
+ if (!read)
1490
+ return false;
1491
+ const recv = read[1];
1492
+ if (recv !== 'cache')
1493
+ return false;
1494
+ const mapCtor = new RegExp(`\\b${recv}\\s*=\\s*new\\s+(?:Hash|Tree|LinkedHash|ConcurrentHash|Concurrent|Identity|Weak)?Map\\s*(?:<[^>]*>)?\\s*\\(`);
1495
+ if (lines.some(l => mapCtor.test(l)))
1496
+ return true;
1497
+ const redisClient = new RegExp(`\\b${recv}\\s*=\\s*(?:await\\s+)?(?:redis\\.)?createClient\\s*\\(`);
1498
+ return !lines.some(l => redisClient.test(l));
1499
+ }
850
1500
  function isLiteralOnlyLocalMap(lines, recv) {
851
1501
  const anyPut = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(`, 'g');
852
1502
  const literalPut = new RegExp(`\\b${recv}\\s*\\.\\s*put\\s*\\(\\s*(?:"(?:[^"\\\\]|\\\\.)*"|[A-Za-z_][\\w.]*)\\s*,\\s*"(?:[^"\\\\]|\\\\.)*"\\s*\\)`, 'g');
@@ -1396,7 +2046,9 @@ export class SinkFilterPass {
1396
2046
  // of a key that WAS put is also dropped when every value put on that local,
1397
2047
  // non-escaping map is a string literal (`isLiteralOnlyLocalMap`).
1398
2048
  const sources = ctx.code
1399
- ? executableSources.filter(s => !isUnwrittenLocalMapKeyRead(ctx.code, s))
2049
+ ? executableSources
2050
+ .filter(s => !isUnwrittenLocalMapKeyRead(ctx.code, s))
2051
+ .filter(s => !isJsDisguisedCacheDbInputRead(ctx.code, s))
1400
2052
  : executableSources;
1401
2053
  // Build merged sinks, deduplicating JS DOM sinks that may overlap with config sinks.
1402
2054
  const sinks = [...taintMatcher.sinks];
@@ -1951,7 +2603,8 @@ export class SinkFilterPass {
1951
2603
  // cognium-dev#626: a `List<String>` PARAMETER is argv-form only while no
1952
2604
  // same-file caller hands it a shell argv (`/bin/sh -c <script>`): then
1953
2605
  // the tainted element is the script, so the suppression proves nothing.
1954
- if (isArgvContainerType(t) && !sameFileCallerPassesShellArgv(varName, ctorCall.in_method, ctorCall.location.line, irTypes, irCalls)) {
2606
+ // Cross-file shell-argv recall is restored in analyzeProject (#626).
2607
+ if (isArgvContainerType(t) && !sameFileCallerPassesShellArgv(varName, ctorCall.in_method, ctorCall.location.line, irTypes, irCalls, sourceLines)) {
1955
2608
  return false;
1956
2609
  }
1957
2610
  }
@@ -2375,7 +3028,21 @@ export class SinkFilterPass {
2375
3028
  if (!NOSQL_ARRAY_METHODS.has(sink.method ?? ''))
2376
3029
  return true;
2377
3030
  const sinkLineText = sourceLines[sink.line - 1] ?? '';
2378
- return !FUNCTION_FIRST_ARG_RE.test(sinkLineText);
3031
+ if (FUNCTION_FIRST_ARG_RE.test(sinkLineText))
3032
+ return false;
3033
+ // cognium-dev#1000 — `ancestors.find(meaningful)`: a named predicate
3034
+ // declared as a function in this file, or a receiver declared as an
3035
+ // array, is Array.prototype iteration too. A Mongo
3036
+ // `collection.find(query)` with a query variable keeps firing.
3037
+ const named = NAMED_ARRAY_PREDICATE_RE.exec(sinkLineText);
3038
+ if (named) {
3039
+ const [, receiver, method, arg] = named;
3040
+ if (NOSQL_ARRAY_METHODS.has(method) &&
3041
+ (isDeclaredFunctionName(arg, sourceLines) || isDeclaredArrayName(receiver, sourceLines))) {
3042
+ return false;
3043
+ }
3044
+ }
3045
+ return true;
2379
3046
  });
2380
3047
  }
2381
3048
  // Stage 15h — JS/TS sql/nosql: ORM query-builder object argument.
@@ -2573,6 +3240,16 @@ export class SinkFilterPass {
2573
3240
  return bodyCalls.length === 0 || !bodyCalls.every(rustBodyDeclaresNonHtmlType);
2574
3241
  });
2575
3242
  }
3243
+ // Stage 15n — rust ssrf: format! with a literal origin and a single `{}` path
3244
+ // slot (cognium-dev#922). Same intent as Stage 15i/15m: the host is fixed at
3245
+ // compile time, so taint in the path cannot redirect the request.
3246
+ if (language === 'rust') {
3247
+ filtered = filtered.filter(sink => {
3248
+ if (sink.type !== 'ssrf')
3249
+ return true;
3250
+ return !rustSsrfRequestUsesFixedHostFormat(ctx.code, sink.line);
3251
+ });
3252
+ }
2576
3253
  // Stage 15m — bash ssrf: fixed-host URL word (cognium-dev#495, #669).
2577
3254
  // The bash analogue of Stage 15i: `curl -- "https://api.example.com/${p}"`
2578
3255
  // can only reach the literal host, whatever `$p` holds. Every word on the
@@ -3290,7 +3967,8 @@ export function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg
3290
3967
  // same line (e.g. System.getProperty("user.dir") inside r.exec(args,...,new File(...))).
3291
3968
  // sink.method is set by findSinks to call.method_name; language-sources sinks also carry it.
3292
3969
  const relevantCalls = sink.method
3293
- ? callsAtSink.filter(c => c.method_name === sink.method)
3970
+ ? callsAtSink.filter(c => c.method_name === sink.method &&
3971
+ (sink.callColumn === undefined || c.location.column === sink.callColumn))
3294
3972
  : callsAtSink;
3295
3973
  // Whether to trust sink.argPositions for narrowing the cleanness check. In shell-like
3296
3974
  // languages, flag-vs-positional ambiguity makes statically declared argument positions