circle-ir 4.16.0 → 4.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/analysis/constant-propagation/index.d.ts +2 -0
  2. package/dist/analysis/constant-propagation/index.d.ts.map +1 -1
  3. package/dist/analysis/constant-propagation/index.js +10 -0
  4. package/dist/analysis/constant-propagation/index.js.map +1 -1
  5. package/dist/analysis/constant-propagation/propagator.d.ts +11 -0
  6. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  7. package/dist/analysis/constant-propagation/propagator.js +125 -20
  8. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  9. package/dist/analysis/constant-propagation/types.d.ts +2 -0
  10. package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
  11. package/dist/analysis/constant-propagation.d.ts +1 -1
  12. package/dist/analysis/constant-propagation.d.ts.map +1 -1
  13. package/dist/analysis/constant-propagation.js +1 -1
  14. package/dist/analysis/constant-propagation.js.map +1 -1
  15. package/dist/analysis/interprocedural.d.ts.map +1 -1
  16. package/dist/analysis/interprocedural.js +37 -8
  17. package/dist/analysis/interprocedural.js.map +1 -1
  18. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  19. package/dist/analysis/passes/cross-file-pass.js +3 -1
  20. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  21. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +13 -0
  22. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  23. package/dist/analysis/passes/csharp-sanitizer-credit.js +48 -0
  24. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  25. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  26. package/dist/analysis/passes/language-sources-pass.js +80 -8
  27. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  28. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  29. package/dist/analysis/passes/param-source-flow-gate-pass.js +17 -1
  30. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  31. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  32. package/dist/analysis/passes/sink-filter-pass.js +19 -3
  33. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  34. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  35. package/dist/analysis/passes/taint-propagation-pass.js +205 -22
  36. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  37. package/dist/analysis/rust-literal-guard.d.ts.map +1 -1
  38. package/dist/analysis/rust-literal-guard.js +100 -1
  39. package/dist/analysis/rust-literal-guard.js.map +1 -1
  40. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  41. package/dist/analysis/taint-matcher.js +1 -0
  42. package/dist/analysis/taint-matcher.js.map +1 -1
  43. package/dist/browser/circle-ir.js +561 -41
  44. package/dist/core/circle-ir-core.cjs +149 -17
  45. package/dist/core/circle-ir-core.js +149 -17
  46. package/dist/core/extractors/types.js +53 -9
  47. package/dist/core/extractors/types.js.map +1 -1
  48. package/dist/navigation/navigation-index.d.ts +40 -1
  49. package/dist/navigation/navigation-index.d.ts.map +1 -1
  50. package/dist/navigation/navigation-index.js +300 -45
  51. package/dist/navigation/navigation-index.js.map +1 -1
  52. package/dist/resolution/cross-file.d.ts +9 -0
  53. package/dist/resolution/cross-file.d.ts.map +1 -1
  54. package/dist/resolution/cross-file.js +134 -0
  55. package/dist/resolution/cross-file.js.map +1 -1
  56. package/dist/types/index.d.ts +18 -0
  57. package/dist/types/index.d.ts.map +1 -1
  58. package/dist/types/index.js.map +1 -1
  59. package/package.json +1 -1
@@ -4663,14 +4663,43 @@ function extractJavaTypes(tree, cache, options) {
4663
4663
  );
4664
4664
  const path = node ? enclosingTypePath(node) : [];
4665
4665
  if (path.length > 0) t.enclosing_type = path.join(".");
4666
- const params = node?.childForFieldName("type_parameters");
4667
- if (params) {
4668
- const names = params.namedChildren.filter((c) => c?.type === "type_parameter").map((c) => c.namedChildren.find((x) => x?.type === "type_identifier" || x?.type === "identifier")).map((x) => x ? getNodeText(x) : "").filter(Boolean);
4669
- if (names.length > 0) t.type_parameters = names;
4666
+ const tp = typeParameters(node);
4667
+ if (tp.names.length > 0) t.type_parameters = tp.names;
4668
+ if (Object.keys(tp.bounds).length > 0) t.type_parameter_bounds = tp.bounds;
4669
+ }
4670
+ const allMethods = /* @__PURE__ */ new Map();
4671
+ for (const t of types) {
4672
+ for (const m of t.methods) allMethods.set(`${m.start_line}:${m.name}`, m);
4673
+ }
4674
+ for (const kind of ["method_declaration", "constructor_declaration"]) {
4675
+ for (const node of getNodesFromCache(tree.rootNode, kind, cache)) {
4676
+ const name2 = getIdentifier(node, "name");
4677
+ const info2 = name2 ? allMethods.get(`${node.startPosition.row + 1}:${name2}`) : void 0;
4678
+ if (!info2) continue;
4679
+ const tp = typeParameters(node);
4680
+ if (tp.names.length > 0) info2.type_parameters = tp.names;
4681
+ if (Object.keys(tp.bounds).length > 0) info2.type_parameter_bounds = tp.bounds;
4670
4682
  }
4671
4683
  }
4672
4684
  return types;
4673
4685
  }
4686
+ function typeParameters(node) {
4687
+ const names = [];
4688
+ const bounds = {};
4689
+ const list = node ? node.childForFieldName("type_parameters") ?? node.namedChildren.find((c) => c?.type === "type_parameters") ?? null : null;
4690
+ if (!list) return { names, bounds };
4691
+ for (const c of list.namedChildren) {
4692
+ if (c?.type !== "type_parameter") continue;
4693
+ const id = c.namedChildren.find((x) => x?.type === "type_identifier" || x?.type === "identifier");
4694
+ const name2 = id ? getNodeText(id) : "";
4695
+ if (!name2) continue;
4696
+ names.push(name2);
4697
+ const bound = c.namedChildren.find((x) => x?.type === "type_bound");
4698
+ const first = bound?.namedChildren.find((x) => x !== null && x.type !== "annotation" && x.type !== "marker_annotation");
4699
+ if (first) bounds[name2] = getNodeText(first);
4700
+ }
4701
+ return { names, bounds };
4702
+ }
4674
4703
  function recordComponents(node) {
4675
4704
  const out2 = [];
4676
4705
  const params = node.childForFieldName("parameters");
@@ -16735,6 +16764,85 @@ function passesMutParam(line, n) {
16735
16764
  if (!new RegExp(String.raw`(?<![\w.])(?:[A-Za-z_]\w*(?:\s*::\s*[A-Za-z_]\w*)*)\s*\((?:[^)]*,\s*)?${n}\b`).test(line)) return false;
16736
16765
  return !new RegExp(String.raw`\b${n}\s*\.\s*\w+\s*\(`).test(line) || new RegExp(String.raw`(?<![\w.])\w+\s*\(\s*${n}\b`).test(line);
16737
16766
  }
16767
+ function findRustAllowlistTernarySanitizers(code) {
16768
+ const masked = maskRust(code);
16769
+ const origLines = code.split("\n");
16770
+ const maskedLines = masked.split("\n");
16771
+ const out2 = [];
16772
+ const allowlistVar = (cond) => {
16773
+ const c = cond.trim();
16774
+ let m = new RegExp(String.raw`^!?(\w+)\s*\.\s*contains\s*\(\s*&\s*(\w+)(?:\s*\.\s*as_str\s*\(\s*\))?\s*\)$`).exec(c);
16775
+ if (m) return m[2];
16776
+ if (m = new RegExp(String.raw`^!\s*\[\s*${STR}(?:\s*,\s*${STR})*\s*,?\s*\]\s*\.\s*contains\s*\(\s*&\s*${VAR}\s*\)$`).exec(c)) {
16777
+ return m[1];
16778
+ }
16779
+ return guardedVariable(c);
16780
+ };
16781
+ const LITERAL_CHAIN = /* @__PURE__ */ new Set(["to_string", "into", "to_owned", "clone"]);
16782
+ const literalOnly = (block) => {
16783
+ const maskedB = maskRust(block.trim());
16784
+ if (!maskedB.trim()) return false;
16785
+ const re = /[A-Za-z_]\w*/g;
16786
+ let m;
16787
+ while ((m = re.exec(maskedB)) !== null) {
16788
+ const id = m[0];
16789
+ if (KEYWORDS.has(id) || /[A-Z]/.test(id[0]) || LITERAL_CHAIN.has(id)) continue;
16790
+ return false;
16791
+ }
16792
+ return true;
16793
+ };
16794
+ const usesOnlyVar = (block, name2) => {
16795
+ const b = block.trim();
16796
+ if (!b) return false;
16797
+ const maskedB = maskRust(b);
16798
+ const re = new RegExp(String.raw`(?<![\w.])${name2}\b`);
16799
+ if (!re.test(maskedB)) return false;
16800
+ return !readsOtherValue(b, maskedB, name2);
16801
+ };
16802
+ for (const m of masked.matchAll(/\blet\s+(\w+)\s*=\s*if\b/g)) {
16803
+ const bound = m[1];
16804
+ const at = m.index + m[0].length - 2;
16805
+ let depth = 0;
16806
+ let open = -1;
16807
+ for (let i2 = at + 2; i2 < masked.length; i2++) {
16808
+ const ch = masked[i2];
16809
+ if (ch === "(" || ch === "[") depth++;
16810
+ else if (ch === ")" || ch === "]") depth--;
16811
+ else if (ch === "{" && depth === 0) {
16812
+ open = i2;
16813
+ break;
16814
+ } else if (ch === ";" || ch === "}") break;
16815
+ }
16816
+ if (open < 0) continue;
16817
+ const cond = code.slice(at + 2, open).trim();
16818
+ const guarded = allowlistVar(cond);
16819
+ if (!guarded) continue;
16820
+ const thenClose = matchBrace(masked, open);
16821
+ if (thenClose < 0) continue;
16822
+ const thenBody = code.slice(open + 1, thenClose);
16823
+ if (!/^\s*else\b/.test(masked.slice(thenClose + 1, thenClose + 10))) continue;
16824
+ const elseOpen = masked.indexOf("{", thenClose + 1);
16825
+ if (elseOpen < 0) continue;
16826
+ const elseClose = matchBrace(masked, elseOpen);
16827
+ if (elseClose < 0) continue;
16828
+ const elseBody = code.slice(elseOpen + 1, elseClose);
16829
+ const inverted = /^\s*!/.test(cond);
16830
+ const ok = inverted ? literalOnly(thenBody) && usesOnlyVar(elseBody, guarded) : usesOnlyVar(thenBody, guarded) && literalOnly(elseBody);
16831
+ if (!ok) continue;
16832
+ const assignLine = masked.slice(0, at).split("\n").length;
16833
+ const mention = new RegExp(String.raw`(?<![\w.])${bound}\b`);
16834
+ for (let l = assignLine; l < origLines.length; l++) {
16835
+ if (!mention.test(maskedLines[l])) continue;
16836
+ out2.push({
16837
+ type: "rust_allowlist_ternary",
16838
+ method: "guard",
16839
+ line: l + 1,
16840
+ sanitizes: CREDITED_TYPES
16841
+ });
16842
+ }
16843
+ }
16844
+ return out2;
16845
+ }
16738
16846
  function findRustLiteralEqualityGuardSanitizers(code) {
16739
16847
  const masked = maskRust(code);
16740
16848
  const origLines = code.split("\n");
@@ -16855,7 +16963,7 @@ function findRustLiteralEqualityGuardSanitizers(code) {
16855
16963
  });
16856
16964
  }
16857
16965
  }
16858
- return out2;
16966
+ return [...out2, ...findRustAllowlistTernarySanitizers(code)];
16859
16967
  }
16860
16968
  function statementSpan(code, masked, lineStart, endIdx) {
16861
16969
  let start2 = lineStart;
@@ -18233,6 +18341,7 @@ function findCSharpRequestSources(sourceCode, language) {
18233
18341
  const assignRe = /^\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
18234
18342
  const arrayElementAssignRe = /^\s*([A-Za-z_]\w*)\s*\[[^\]]+\]\s*=\s*(.+?);?\s*$/;
18235
18343
  const memberFieldAssignRe = /^\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
18344
+ const collectionAddRe = /^\s*([A-Za-z_]\w*)\s*\.\s*Add\s*\(\s*(.+?)\s*\)\s*;?\s*$/;
18236
18345
  const consoleReadRe = /\bConsole\s*\.\s*ReadLine\s*\(/;
18237
18346
  const envReadRe = /\bEnvironment\s*\.\s*GetEnvironmentVariables?\s*(?:\(|\[)/;
18238
18347
  const formFileParams = /* @__PURE__ */ new Set();
@@ -18326,6 +18435,25 @@ function findCSharpRequestSources(sourceCode, language) {
18326
18435
  continue;
18327
18436
  }
18328
18437
  }
18438
+ const addEl = collectionAddRe.exec(stmt);
18439
+ if (addEl) {
18440
+ const [, baseName, rhs2] = addEl;
18441
+ const type2 = classify(rhs2);
18442
+ if (type2 && baseName) {
18443
+ const lineNumber2 = i2 + 1;
18444
+ if (!sources.some((s) => s.line === lineNumber2 && s.variable === baseName)) {
18445
+ sources.push({
18446
+ type: type2,
18447
+ location: `${baseName}.Add(${rhs2.trim().substring(0, 50)}${rhs2.length > 50 ? "..." : ""})`,
18448
+ severity: type2 === "db_input" ? "medium" : "high",
18449
+ line: lineNumber2,
18450
+ confidence: 1,
18451
+ variable: baseName
18452
+ });
18453
+ }
18454
+ continue;
18455
+ }
18456
+ }
18329
18457
  const m = assignRe.exec(stmt);
18330
18458
  const rhsCrossesBlock = m !== null && m[2].includes("{");
18331
18459
  if (!m || rhsCrossesBlock) {
@@ -21865,14 +21993,17 @@ function findCSharpEarlyReturnGuardSanitizers(code, language) {
21865
21993
  const sanitizers = [];
21866
21994
  const lines = code.split("\n");
21867
21995
  const terminatorRe = /\b(throw|return)\b/;
21996
+ const literalArrays = collectCSharpLiteralArrayFields(code);
21997
+ const indexOfRe = /\bif\s*\(\s*(?:System\s*\.\s*)?Array\s*\.\s*IndexOf\s*\(\s*([A-Za-z_]\w*)\s*,\s*([A-Za-z_][\w.]*)\s*\)\s*(?:<\s*0|==\s*-\s*1)\s*\)/;
21868
21998
  const allowlistRes = [
21869
- /\bif\s*\(\s*Array\s*\.\s*IndexOf\s*\([^,]+,\s*([A-Za-z_][\w.]*)\s*\)\s*<\s*0\s*\)/,
21870
21999
  /\bif\s*\(\s*!\s*[A-Za-z_][\w.]*\s*\.\s*Contains\s*\(\s*([A-Za-z_][\w.]*)\s*\)\s*\)/
21871
22000
  ];
21872
22001
  const charClassRe = /\bif\s*\(\s*!\s*([A-Za-z_][\w.]*)\s*\.\s*All\s*\(\s*char\s*\.\s*Is[A-Za-z]+\s*\)\s*\)/;
21873
22002
  for (let i2 = 0; i2 < lines.length; i2++) {
21874
22003
  let guardedExpr = null;
21875
- for (const re of allowlistRes) {
22004
+ const im = indexOfRe.exec(lines[i2]);
22005
+ if (im && literalArrays.has(im[1])) guardedExpr = im[2];
22006
+ if (!guardedExpr) for (const re of allowlistRes) {
21876
22007
  const m = re.exec(lines[i2]);
21877
22008
  if (m) {
21878
22009
  guardedExpr = m[1];
@@ -21900,6 +22031,7 @@ function findCSharpEarlyReturnGuardSanitizers(code, language) {
21900
22031
  for (let l = i2 + 1; l < lines.length; l++) {
21901
22032
  const a = assignRe.exec(lines[l]);
21902
22033
  if (a && mentions2(a[2])) guarded.add(a[1]);
22034
+ else if (a) guarded.delete(a[1]);
21903
22035
  if (mentions2(lines[l])) {
21904
22036
  sanitizers.push({
21905
22037
  type: "csharp_early_return_guard",
@@ -21920,6 +22052,25 @@ function findCSharpEarlyReturnGuardSanitizers(code, language) {
21920
22052
  }
21921
22053
  return sanitizers;
21922
22054
  }
22055
+ function collectCSharpLiteralArrayFields(code) {
22056
+ const names = /* @__PURE__ */ new Set();
22057
+ const lit = String.raw`(?:@"(?:[^"]|"")*"|"(?:[^"\\\n]|\\.)*")`;
22058
+ const elems = new RegExp(String.raw`^\s*${lit}(?:\s*,\s*${lit})*\s*,?\s*$`);
22059
+ const declRe = /((?:\b(?:public|private|protected|internal|static|readonly)\s+)+)string\s*\[\s*\]\s+([A-Za-z_]\w*)\s*=\s*(?:new\s+(?:string\s*)?\[\s*\]\s*)?\{([^{}]*)\}\s*;/g;
22060
+ for (const m of code.matchAll(declRe)) {
22061
+ const mods = m[1];
22062
+ const name2 = m[2];
22063
+ if (!/\bstatic\b/.test(mods) || !/\breadonly\b/.test(mods)) continue;
22064
+ if (!elems.test(m[3])) continue;
22065
+ const rest = code.slice(0, m.index) + code.slice(m.index + m[0].length);
22066
+ const written = new RegExp(
22067
+ String.raw`\b${name2}\s*(?:\[[^\]]*\]\s*)?(?:[-+*/%&|^]|\?\?)?=(?!=)` + String.raw`|\b(?:ref|out)\s+${name2}\b` + String.raw`|\b${name2}\s*\.\s*SetValue\s*\(` + String.raw`|\bArray\s*\.\s*(?:Clear|Copy|ConstrainedCopy|Fill|Resize|Reverse|Sort)\s*\([^;]*\b${name2}\b`
22068
+ );
22069
+ if (written.test(rest)) continue;
22070
+ names.add(name2);
22071
+ }
22072
+ return names;
22073
+ }
21923
22074
  function findCSharpFullPathStartsWithGuardSanitizers(code) {
21924
22075
  const sanitizers = [];
21925
22076
  const lines = code.split("\n");
@@ -26381,6 +26532,9 @@ function isInterproceduralTaintableType(typeName, language) {
26381
26532
  "IEnumerable",
26382
26533
  "ICollection",
26383
26534
  "IList",
26535
+ "List",
26536
+ "Hashtable",
26537
+ "ArrayList",
26384
26538
  "Dictionary",
26385
26539
  "IDictionary",
26386
26540
  "IReadOnlyList",
@@ -30968,10 +31122,12 @@ function isJavaStaticFormatterCall(call, declaredTypeNames, imports, calls) {
30968
31122
  if (other === call || other.method_name === "format") continue;
30969
31123
  if (other.arguments.some((a) => a.expression.includes(needle))) return false;
30970
31124
  }
30971
- const receiver = call.receiver;
31125
+ return isJdkClassReceiver(call.receiver, JAVA_FORMATTER_CLASSES, declaredTypeNames, imports);
31126
+ }
31127
+ function isJdkClassReceiver(receiver, classes, declaredTypeNames, imports) {
30972
31128
  const dot = receiver.lastIndexOf(".");
30973
31129
  const simple = dot < 0 ? receiver : receiver.slice(dot + 1);
30974
- const pkg = JAVA_FORMATTER_CLASSES.get(simple);
31130
+ const pkg = classes.get(simple);
30975
31131
  if (pkg === void 0) return false;
30976
31132
  if (dot >= 0) return receiver === `${pkg}.${simple}`;
30977
31133
  if (declaredTypeNames.has(simple)) return false;
@@ -30982,8 +31138,41 @@ function isJavaStaticFormatterCall(call, declaredTypeNames, imports, calls) {
30982
31138
  if (pkg === "java.lang") return true;
30983
31139
  return imports.some((imp) => imp.from_package === pkg && (imp.imported_name === simple || imp.is_wildcard));
30984
31140
  }
31141
+ var JAVA_CHARACTER_CLASS = /* @__PURE__ */ new Map([["Character", "java.lang"]]);
31142
+ var JAVA_CHARACTER_PREDICATES = /* @__PURE__ */ new Set([
31143
+ "isLetter",
31144
+ "isDigit",
31145
+ "isLetterOrDigit",
31146
+ "isAlphabetic",
31147
+ "isWhitespace",
31148
+ "isSpaceChar",
31149
+ "isUpperCase",
31150
+ "isLowerCase",
31151
+ "isTitleCase",
31152
+ "isISOControl",
31153
+ "isDefined",
31154
+ "isIdeographic",
31155
+ "isMirrored",
31156
+ "isJavaIdentifierStart",
31157
+ "isJavaIdentifierPart",
31158
+ "isUnicodeIdentifierStart",
31159
+ "isUnicodeIdentifierPart",
31160
+ "isIdentifierIgnorable",
31161
+ "isHighSurrogate",
31162
+ "isLowSurrogate",
31163
+ "isSurrogate",
31164
+ "isSurrogatePair",
31165
+ "isValidCodePoint",
31166
+ "isBmpCodePoint",
31167
+ "isSupplementaryCodePoint"
31168
+ ]);
31169
+ function isJavaCharacterPredicateCall(call, declaredTypeNames, imports) {
31170
+ if (!call.receiver || !JAVA_CHARACTER_PREDICATES.has(call.method_name)) return false;
31171
+ return isJdkClassReceiver(call.receiver, JAVA_CHARACTER_CLASS, declaredTypeNames, imports);
31172
+ }
30985
31173
  function isJavaNonEscapeCall(call, declaredTypeNames, imports, calls) {
30986
31174
  if (isJavaStaticFormatterCall(call, declaredTypeNames, imports, calls)) return true;
31175
+ if (isJavaCharacterPredicateCall(call, declaredTypeNames, imports)) return true;
30987
31176
  if (call.receiver) return false;
30988
31177
  const isCtor = call.is_constructor === true || (call.resolution?.target ?? "").endsWith(".<init>");
30989
31178
  if (!isCtor) return false;
@@ -31969,6 +32158,11 @@ var ConstantPropagator = class _ConstantPropagator {
31969
32158
  taintedArrayElements = /* @__PURE__ */ new Map();
31970
32159
  // Track current method name for scoping local variables
31971
32160
  currentMethod = null;
32161
+ /** Start lines of nested `{ ... }` blocks — disambiguate same-named locals (#476 Collections10). */
32162
+ blockScopeStack = [];
32163
+ lineBlockScopes = /* @__PURE__ */ new Map();
32164
+ /** Skip block-scope push on the method/constructor body `{ ... }` (#476). */
32165
+ skipMethodBodyBlockScope = false;
31972
32166
  // Track conditional taints: which variables were tainted under which conditions
31973
32167
  // Maps condition expression string → set of variables tainted under that condition
31974
32168
  conditionalTaints = /* @__PURE__ */ new Map();
@@ -31982,6 +32176,8 @@ var ConstantPropagator = class _ConstantPropagator {
31982
32176
  inSynchronizedBlock = false;
31983
32177
  // Track iterator sources: iterator variable name → collection name it was created from
31984
32178
  iteratorSources = /* @__PURE__ */ new Map();
32179
+ /** Vars assigned from `iter.next()` — do not retro-clear on later iterator rebind (#476). */
32180
+ taintedFromIteratorNext = /* @__PURE__ */ new Set();
31985
32181
  // Track class field names (declared at class level, not local variables)
31986
32182
  classFields = /* @__PURE__ */ new Set();
31987
32183
  // Track tainted method parameters for inter-procedural analysis
@@ -32039,12 +32235,16 @@ var ConstantPropagator = class _ConstantPropagator {
32039
32235
  this.sanitizedVars.clear();
32040
32236
  this.sanitizedVarUncovered.clear();
32041
32237
  this.currentMethod = null;
32238
+ this.blockScopeStack = [];
32239
+ this.lineBlockScopes.clear();
32240
+ this.skipMethodBodyBlockScope = false;
32042
32241
  this.conditionalTaints.clear();
32043
32242
  this.conditionStack = [];
32044
32243
  this.lineConditions.clear();
32045
32244
  this.synchronizedLines.clear();
32046
32245
  this.inSynchronizedBlock = false;
32047
32246
  this.iteratorSources.clear();
32247
+ this.taintedFromIteratorNext.clear();
32048
32248
  this.classFields.clear();
32049
32249
  this.instanceFieldTaint.clear();
32050
32250
  this.currentClassName = null;
@@ -32069,6 +32269,9 @@ var ConstantPropagator = class _ConstantPropagator {
32069
32269
  const resultSymbols = new Map(this.symbols);
32070
32270
  for (const name2 of this.tainted) {
32071
32271
  if (name2.includes(":")) {
32272
+ if (/^[^:]+:b\d+:/.test(name2)) {
32273
+ continue;
32274
+ }
32072
32275
  const unscoped = name2.substring(name2.indexOf(":") + 1);
32073
32276
  resultTainted.add(unscoped);
32074
32277
  }
@@ -32083,6 +32286,9 @@ var ConstantPropagator = class _ConstantPropagator {
32083
32286
  for (const [name2, value] of this.symbols) {
32084
32287
  if (this.goScopedNames.has(name2)) continue;
32085
32288
  if (name2.includes(":")) {
32289
+ if (/^[^:]+:b\d+:/.test(name2)) {
32290
+ continue;
32291
+ }
32086
32292
  const unscoped = name2.substring(name2.indexOf(":") + 1);
32087
32293
  if (!resultSymbols.has(unscoped)) {
32088
32294
  resultSymbols.set(unscoped, value);
@@ -32092,6 +32298,7 @@ var ConstantPropagator = class _ConstantPropagator {
32092
32298
  return {
32093
32299
  symbols: resultSymbols,
32094
32300
  tainted: resultTainted,
32301
+ lineBlockScopes: new Map(this.lineBlockScopes),
32095
32302
  unreachableLines: new Set(this.unreachableLines),
32096
32303
  taintedCollections: new Map(this.taintedCollections),
32097
32304
  taintedArrayElements: new Map(this.taintedArrayElements),
@@ -32635,6 +32842,18 @@ var ConstantPropagator = class _ConstantPropagator {
32635
32842
  // ===========================================================================
32636
32843
  // Taint Refinement
32637
32844
  // ===========================================================================
32845
+ /** True when `node` is `iter.next()` (possibly under a cast). */
32846
+ isIteratorNextExpression(node) {
32847
+ const text = getNodeText2(node, this.source);
32848
+ if (/\.\s*next\s*\(/.test(text)) return true;
32849
+ let current = node;
32850
+ if (current.type === "cast_expression" || current.type === "parenthesized_expression") {
32851
+ current = current.childForFieldName("operand") ?? current.childForFieldName("expression");
32852
+ }
32853
+ if (!current || current.type !== "method_invocation") return false;
32854
+ const nameNode = current.childForFieldName("name");
32855
+ return nameNode ? getNodeText2(nameNode, this.source) === "next" : false;
32856
+ }
32638
32857
  refineTaintFromConstants() {
32639
32858
  let changed = true;
32640
32859
  let iterations = 0;
@@ -32644,6 +32863,9 @@ var ConstantPropagator = class _ConstantPropagator {
32644
32863
  iterations++;
32645
32864
  const toRemove = [];
32646
32865
  for (const varName of this.tainted) {
32866
+ if (this.taintedFromIteratorNext.has(varName)) {
32867
+ continue;
32868
+ }
32647
32869
  const symbol = this.symbols.get(varName);
32648
32870
  if (symbol && symbol.type !== "unknown") {
32649
32871
  toRemove.push(varName);
@@ -32692,6 +32914,9 @@ var ConstantPropagator = class _ConstantPropagator {
32692
32914
  if (this.unreachableLines.has(line)) {
32693
32915
  return true;
32694
32916
  }
32917
+ if (this.blockScopeStack.length > 0) {
32918
+ this.lineBlockScopes.set(line, this.blockScopeStack[this.blockScopeStack.length - 1]);
32919
+ }
32695
32920
  if (this.conditionStack.length > 0 && !this.lineConditions.has(line)) {
32696
32921
  this.lineConditions.set(line, this.conditionStack[this.conditionStack.length - 1]);
32697
32922
  }
@@ -32723,6 +32948,15 @@ var ConstantPropagator = class _ConstantPropagator {
32723
32948
  case "if_statement":
32724
32949
  this.handleIfStatement(node);
32725
32950
  return true;
32951
+ case "block": {
32952
+ if (this.skipMethodBodyBlockScope) {
32953
+ this.skipMethodBodyBlockScope = false;
32954
+ }
32955
+ for (const child of node.children) {
32956
+ this.visit(child);
32957
+ }
32958
+ return true;
32959
+ }
32726
32960
  case "switch_expression":
32727
32961
  case "switch_statement":
32728
32962
  this.handleSwitch(node);
@@ -32808,7 +33042,9 @@ var ConstantPropagator = class _ConstantPropagator {
32808
33042
  }
32809
33043
  const body2 = node.childForFieldName("body");
32810
33044
  if (body2) {
33045
+ this.skipMethodBodyBlockScope = true;
32811
33046
  this.visit(body2);
33047
+ this.skipMethodBodyBlockScope = false;
32812
33048
  }
32813
33049
  this.currentMethod = prevMethod;
32814
33050
  this.inConstructor = prevInConstructor;
@@ -32822,10 +33058,31 @@ var ConstantPropagator = class _ConstantPropagator {
32822
33058
  if (varName.includes(".") || varName.includes(":")) {
32823
33059
  return varName;
32824
33060
  }
33061
+ const blockId = this.blockScopeStack.length > 0 ? this.blockScopeStack[this.blockScopeStack.length - 1] : void 0;
32825
33062
  if (this.currentMethod) {
32826
- return `${this.currentMethod}:${varName}`;
33063
+ return blockId !== void 0 ? `${this.currentMethod}:b${blockId}:${varName}` : `${this.currentMethod}:${varName}`;
32827
33064
  }
32828
- return varName;
33065
+ return blockId !== void 0 ? `b${blockId}:${varName}` : varName;
33066
+ }
33067
+ /** Innermost block → method → unscoped, for reads that may bind to an outer declaration. */
33068
+ getScopedNameCandidates(varName) {
33069
+ if (varName.includes(".") || varName.includes(":")) {
33070
+ return [varName];
33071
+ }
33072
+ const out2 = [];
33073
+ for (let i2 = this.blockScopeStack.length - 1; i2 >= 0; i2--) {
33074
+ const blockId = this.blockScopeStack[i2];
33075
+ if (this.currentMethod) {
33076
+ out2.push(`${this.currentMethod}:b${blockId}:${varName}`);
33077
+ } else {
33078
+ out2.push(`b${blockId}:${varName}`);
33079
+ }
33080
+ }
33081
+ if (this.currentMethod) {
33082
+ out2.push(`${this.currentMethod}:${varName}`);
33083
+ }
33084
+ out2.push(varName);
33085
+ return out2;
32829
33086
  }
32830
33087
  /**
32831
33088
  * Look up a variable value, checking both scoped and unscoped names.
@@ -32833,11 +33090,12 @@ var ConstantPropagator = class _ConstantPropagator {
32833
33090
  * either local (scoped) or global (unscoped, like class fields).
32834
33091
  */
32835
33092
  lookupSymbol(varName) {
32836
- if (this.currentMethod && !varName.includes(".") && !varName.includes(":")) {
32837
- const scopedName = `${this.currentMethod}:${varName}`;
32838
- const scopedValue = this.symbols.get(scopedName);
32839
- if (scopedValue) {
32840
- return scopedValue;
33093
+ if (!varName.includes(".") && !varName.includes(":")) {
33094
+ for (const scopedName of this.getScopedNameCandidates(varName)) {
33095
+ const scopedValue = this.symbols.get(scopedName);
33096
+ if (scopedValue) {
33097
+ return scopedValue;
33098
+ }
32841
33099
  }
32842
33100
  }
32843
33101
  return this.symbols.get(varName);
@@ -32889,8 +33147,22 @@ var ConstantPropagator = class _ConstantPropagator {
32889
33147
  this.trackIteratorsInNode(initNode);
32890
33148
  }
32891
33149
  }
32892
- for (const child of node.children) {
32893
- this.visit(child);
33150
+ if (node.type === "while_statement" || node.type === "do_statement") {
33151
+ const body2 = node.childForFieldName("body");
33152
+ for (const child of node.children) {
33153
+ if (child !== body2) this.visit(child);
33154
+ }
33155
+ if (body2?.type === "block") {
33156
+ this.blockScopeStack.push(getNodeLine(body2));
33157
+ this.visit(body2);
33158
+ this.blockScopeStack.pop();
33159
+ } else if (body2) {
33160
+ this.visit(body2);
33161
+ }
33162
+ } else {
33163
+ for (const child of node.children) {
33164
+ this.visit(child);
33165
+ }
32894
33166
  }
32895
33167
  for (const varName of loopVarNames) {
32896
33168
  this.symbols.set(varName, createUnknown(getNodeLine(node)));
@@ -33032,6 +33304,10 @@ var ConstantPropagator = class _ConstantPropagator {
33032
33304
  this.sanitizedVars.delete(scopedName);
33033
33305
  this.sanitizedVarUncovered.delete(scopedName);
33034
33306
  this.definitionNodes.set(scopedName, valueNode);
33307
+ if (this.isIteratorNextExpression(valueNode)) {
33308
+ this.taintedFromIteratorNext.add(scopedName);
33309
+ this.taintedFromIteratorNext.add(varName);
33310
+ }
33035
33311
  this.symbols.set(scopedName, createUnknown(line));
33036
33312
  } else {
33037
33313
  if (this.inConditionalBranch) {
@@ -33159,6 +33435,12 @@ var ConstantPropagator = class _ConstantPropagator {
33159
33435
  this.sanitizedVars.delete(scopedName);
33160
33436
  this.sanitizedVarUncovered.delete(scopedName);
33161
33437
  this.definitionNodes.set(scopedName, right);
33438
+ if (this.isIteratorNextExpression(right)) {
33439
+ this.taintedFromIteratorNext.add(scopedName);
33440
+ if (!varName.includes(".")) {
33441
+ this.taintedFromIteratorNext.add(varName);
33442
+ }
33443
+ }
33162
33444
  this.symbols.set(scopedName, createUnknown(line));
33163
33445
  if (this.inConstructor && varName.startsWith("this.")) {
33164
33446
  const fieldName = varName.substring(5);
@@ -34149,10 +34431,17 @@ var ConstantPropagator = class _ConstantPropagator {
34149
34431
  }
34150
34432
  }
34151
34433
  if (node.type === "identifier") {
34152
- const scopedName = this.currentMethod ? `${this.currentMethod}:${text}` : text;
34153
- const isTainted = this.tainted.has(scopedName) || this.tainted.has(text);
34154
- if (isTainted) {
34155
- if (this.isExcludedByCondition(scopedName) || this.isExcludedByCondition(text)) {
34434
+ let isTainted = false;
34435
+ let boundName = null;
34436
+ for (const candidate of this.getScopedNameCandidates(text)) {
34437
+ if (this.tainted.has(candidate)) {
34438
+ isTainted = true;
34439
+ boundName = candidate;
34440
+ break;
34441
+ }
34442
+ }
34443
+ if (isTainted && boundName) {
34444
+ if (this.isExcludedByCondition(boundName) || this.isExcludedByCondition(text)) {
34156
34445
  return false;
34157
34446
  }
34158
34447
  }
@@ -34359,6 +34648,14 @@ function analyzeConstantPropagation(tree, sourceCode, options = {}) {
34359
34648
  options.taintedParameters ?? []
34360
34649
  );
34361
34650
  }
34651
+ function constPropScopedVar(method, varName, line, result) {
34652
+ if (!method) return varName;
34653
+ const blockId = result.lineBlockScopes?.get(line);
34654
+ if (blockId !== void 0) {
34655
+ return `${method}:b${blockId}:${varName}`;
34656
+ }
34657
+ return `${method}:${varName}`;
34658
+ }
34362
34659
  function isFalsePositive(result, sinkLine, taintedVar) {
34363
34660
  if (result.unreachableLines.has(sinkLine)) {
34364
34661
  return { isFalsePositive: true, reason: "sink_in_dead_code" };
@@ -38627,6 +38924,11 @@ function bareIdentifiers(text) {
38627
38924
  for (const m of stripped.matchAll(/\b([A-Za-z_]\w*)\b/g)) out2.push(m[1]);
38628
38925
  return out2;
38629
38926
  }
38927
+ function isLiteralValue(rhs) {
38928
+ if (/\$"/.test(rhs)) return false;
38929
+ const ids = bareIdentifiers(rhs);
38930
+ return ids.every((id) => id === "null" || id === "true" || id === "false" || id === "default") || /^\s*(?:string|String)\s*\.\s*Empty\s*$/.test(rhs);
38931
+ }
38630
38932
  function insideParens(text, open) {
38631
38933
  let depth = 0;
38632
38934
  for (let i2 = open; i2 < text.length; i2++) {
@@ -38807,6 +39109,36 @@ var CSharpCredit = class {
38807
39109
  sanitizedVarsAt(type, sinkLine) {
38808
39110
  return this.sanitizedVarsIn(type, this.methodAt(sinkLine)?.start_line ?? 1, sinkLine, true);
38809
39111
  }
39112
+ /**
39113
+ * Class fields judged across every method that writes them (cognium-dev#808).
39114
+ * A field is read in one method and written in others, so each write is
39115
+ * judged in its own method with the same in-method credit as a local, and
39116
+ * the field is safe for `type` only when every write is: `sanitized` holds
39117
+ * the fields with at least one credited write and no uncredited one,
39118
+ * `unsanitized` the fields with an uncredited write. A literal write
39119
+ * (`_v = "";`, `_v = null;`) is neutral.
39120
+ */
39121
+ fieldWrites(type, fields) {
39122
+ const credited = /* @__PURE__ */ new Set();
39123
+ const unsanitized = /* @__PURE__ */ new Set();
39124
+ for (const m of this.methods) {
39125
+ const sanitized = /* @__PURE__ */ new Set();
39126
+ for (const st of this.statements(m.start_line, m.end_line + 1)) {
39127
+ if (this.methodAt(st.line) !== m) continue;
39128
+ for (const op of this.creditOpsInStatement(st.text)) {
39129
+ const lhs = op.lhs;
39130
+ const keeps = op.kind === "append" ? sanitized.has(lhs) && this.rhsKeepsSanitizerCredit(type, op.rhs, sanitized, true, true) : this.rhsKeepsSanitizerCredit(type, op.rhs, sanitized, true, false);
39131
+ if (keeps) sanitized.add(lhs);
39132
+ else sanitized.delete(lhs);
39133
+ if (!fields.has(lhs) || isLiteralValue(op.rhs)) continue;
39134
+ if (keeps) credited.add(lhs);
39135
+ else unsanitized.add(lhs);
39136
+ }
39137
+ }
39138
+ }
39139
+ for (const f of unsanitized) credited.delete(f);
39140
+ return { sanitized: credited, unsanitized };
39141
+ }
38810
39142
  /**
38811
39143
  * True when the sink on `sinkLine` also receives a tainted value that no
38812
39144
  * sanitizer covers. In `Write(HtmlEncode(a) + b)` the encoder covers `a`
@@ -41832,6 +42164,15 @@ var SinkFilterPass = class {
41832
42164
  const csFields = new Set(ctx.graph.ir.types.flatMap((t) => (t.fields ?? []).map((f) => f.name)));
41833
42165
  const credit = new CSharpCredit(ctx.code, ctx.graph.ir.types.flatMap((t) => t.methods));
41834
42166
  const csSeeds = mergedSources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
42167
+ const fieldWritesByType = /* @__PURE__ */ new Map();
42168
+ const fieldWritesFor = (type) => {
42169
+ let w = fieldWritesByType.get(type);
42170
+ if (!w) {
42171
+ w = credit.fieldWrites(type, csFields);
42172
+ fieldWritesByType.set(type, w);
42173
+ }
42174
+ return w;
42175
+ };
41835
42176
  const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
41836
42177
  filtered = filtered.filter((sink) => {
41837
42178
  if (sink.type === "xxe" && xxeHardened) return false;
@@ -41841,9 +42182,13 @@ var SinkFilterPass = class {
41841
42182
  const scoped = credit.sanitizedVarsAt(sink.type, sink.line);
41842
42183
  const fileWide = sanitizedByType.get(sink.type);
41843
42184
  const sanitizedVars = new Set(scoped);
42185
+ const fieldWrites = fieldWritesFor(sink.type);
41844
42186
  if (fileWide) {
41845
- for (const v of fileWide) if (csFields.has(v)) sanitizedVars.add(v);
42187
+ for (const v of fileWide) {
42188
+ if (csFields.has(v) && !fieldWrites.unsanitized.has(v)) sanitizedVars.add(v);
42189
+ }
41846
42190
  }
42191
+ for (const v of fieldWrites.sanitized) sanitizedVars.add(v);
41847
42192
  for (const v of credit.guardedVarsAt(sink.type, sink.line)) sanitizedVars.add(v);
41848
42193
  for (const v of credit.safeBuilders(sanitizedVars, sink.line)) sanitizedVars.add(v);
41849
42194
  const received = credit.sinkText(sink.line);
@@ -43190,6 +43535,15 @@ function isReactHtmlComponent(m, f, lines) {
43190
43535
  const sig = lines[m.start_line - 1] ?? "";
43191
43536
  return /\bfunction\s+[A-Z][A-Za-z0-9]*\s*\(\s*\{/.test(sig);
43192
43537
  }
43538
+ function isPythonYamlLoadParamDeser(f, lines) {
43539
+ if (f.source_type !== "interprocedural_param" || f.sink_type !== "deserialization") return false;
43540
+ const head = f.path[0]?.variable;
43541
+ if (!head || f.path.length < 2) return false;
43542
+ const sinkLine = lines[f.sink_line - 1] ?? "";
43543
+ if (!/\byaml\.load\s*\(/.test(sinkLine) || /\bLoader\s*=\s*yaml\.SafeLoader/.test(sinkLine)) return false;
43544
+ const esc = head.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43545
+ return new RegExp(`\\byaml\\.load\\s*\\(\\s*${esc}\\b`).test(sinkLine);
43546
+ }
43193
43547
  var FIELD_CARRIED_SOURCE = /constructor-injected field|returns tainted field/;
43194
43548
  var EXECUTOR_CALLEES = /* @__PURE__ */ new Map([
43195
43549
  ["submit", 0],
@@ -43329,7 +43683,7 @@ var ParamSourceFlowGatePass = class {
43329
43683
  let keep = true;
43330
43684
  if (f.source_type === "interprocedural_param" && !realSourceLines.has(f.source_line)) {
43331
43685
  const m = enclosingMethod(methods, f.source_line);
43332
- keep = m !== void 0 && (isHandler(m) || fedByRealSource(f, m) || isReactHtmlComponent(m, f, lines));
43686
+ keep = m !== void 0 && (isHandler(m) || fedByRealSource(f, m) || isReactHtmlComponent(m, f, lines) || ctx.language === "python" && isPythonYamlLoadParamDeser(f, lines));
43333
43687
  }
43334
43688
  if (keep) flows[w++] = f;
43335
43689
  else {
@@ -43954,21 +44308,56 @@ function resolveJavaDerivedAnchor(varName, derived, realSourceByVar, codeLines,
43954
44308
  }
43955
44309
  return best;
43956
44310
  }
43957
- function javaReceiverChainDerivedVar(code, localVar, seedVar) {
44311
+ function javaReceiverChainDerivedVar(rhsByLocal, localVar, seedVar) {
43958
44312
  if (!seedVar || localVar === seedVar) return false;
44313
+ const rhsList = rhsByLocal.get(localVar);
44314
+ if (!rhsList) return false;
43959
44315
  const esc = seedVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43960
44316
  const receiverOnRhs = new RegExp(`(?:^|[^\\w.$])${esc}\\s*\\.`, "u");
44317
+ return rhsList.some((rhs) => receiverOnRhs.test(rhs));
44318
+ }
44319
+ function defLineForUse(graph, varName, useLine) {
44320
+ const own = (graph.defsByLine.get(useLine) ?? []).find((d2) => d2.variable === varName);
44321
+ if (own) return own.line;
44322
+ const use = (graph.usesByLine.get(useLine) ?? []).find((u) => u.variable === varName && u.def_id != null);
44323
+ const d = use ? graph.defById.get(use.def_id) : void 0;
44324
+ return d?.line ?? useLine;
44325
+ }
44326
+ function constPropFlowLookup(language, method, varName, blockLine, constProp, preferMethodScoped = false) {
44327
+ if (language === "go") {
44328
+ return constPropScopedVar(method, varName, blockLine, constProp);
44329
+ }
44330
+ if (!method || varName.includes(":")) return varName;
44331
+ const blockScoped = constPropScopedVar(method, varName, blockLine, constProp);
44332
+ const hasTaintBinding = (name2) => constProp.tainted.has(name2) || constProp.sanitizedVars.has(name2);
44333
+ if (blockScoped.includes(":b") && hasTaintBinding(blockScoped)) {
44334
+ return blockScoped;
44335
+ }
44336
+ const methodScoped = `${method}:${varName}`;
44337
+ if (hasTaintBinding(methodScoped)) {
44338
+ return methodScoped;
44339
+ }
44340
+ if (preferMethodScoped && method) {
44341
+ return methodScoped;
44342
+ }
44343
+ return constProp.lineBlockScopes?.has(blockLine) ? blockScoped : varName;
44344
+ }
44345
+ function indexJavaLocalAssignments(code) {
43961
44346
  const declRe = /^\s*(?:public|private|protected|static|final|volatile|transient|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
43962
44347
  const assignRe = /^\s*([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
44348
+ const index = /* @__PURE__ */ new Map();
43963
44349
  for (const line of code.split("\n")) {
43964
44350
  const trimmed = line.trimStart();
43965
44351
  if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
44352
+ if (!line.includes("=") || !line.includes(";")) continue;
43966
44353
  const m = declRe.exec(line) ?? assignRe.exec(line);
43967
- if (!m || m[1] !== localVar) continue;
44354
+ if (!m) continue;
44355
+ const list = index.get(m[1]);
43968
44356
  const rhs = stripJavaTrailingComment(m[2]);
43969
- if (receiverOnRhs.test(rhs)) return true;
44357
+ if (list) list.push(rhs);
44358
+ else index.set(m[1], [rhs]);
43970
44359
  }
43971
- return false;
44360
+ return index;
43972
44361
  }
43973
44362
  function makeConstPropVeto(constProp, graph, sources, code, language, sanitizers) {
43974
44363
  const lines = typeof code === "string" ? code.split("\n") : [];
@@ -43998,10 +44387,24 @@ function makeConstPropVeto(constProp, graph, sources, code, language, sanitizers
43998
44387
  };
43999
44388
  return (step, sourceLine, sinkLine, sinkType) => {
44000
44389
  const method = methodAtLine(graph, step.line);
44001
- const scopedVar = method ? `${method}:${step.variable}` : step.variable;
44002
- const r = isFalsePositive(constProp, step.line, language === "go" ? scopedVar : step.variable);
44390
+ const blockLine = defLineForUse(graph, step.variable, step.line);
44391
+ const fpLookup = constPropFlowLookup(
44392
+ language,
44393
+ method,
44394
+ step.variable,
44395
+ blockLine,
44396
+ constProp
44397
+ );
44398
+ const r = isFalsePositive(constProp, step.line, fpLookup);
44003
44399
  if (!r.isFalsePositive) return false;
44004
44400
  if (r.reason !== "variable_not_tainted") return true;
44401
+ if (r.reason === "variable_not_tainted" && language === "java" && method && fpLookup === step.variable && !step.variable.includes(".")) {
44402
+ const v = step.variable;
44403
+ if (constProp.tainted.has(`${method}:${v}`)) return false;
44404
+ for (const t of constProp.tainted) {
44405
+ if (t.startsWith(`${method}:b`) && t.endsWith(`:${v}`)) return false;
44406
+ }
44407
+ }
44005
44408
  if (sinkType !== void 0 && constProp.sanitizedVarUncovered) {
44006
44409
  const unc = constProp.sanitizedVarUncovered;
44007
44410
  if (step.variable.includes(":")) {
@@ -44009,7 +44412,9 @@ function makeConstPropVeto(constProp, graph, sources, code, language, sanitizers
44009
44412
  } else {
44010
44413
  const method2 = methodAtLine(graph, step.line);
44011
44414
  if (method2) {
44012
- if (unc.get(`${method2}:${step.variable}`)?.has(sinkType)) return false;
44415
+ if (unc.get(fpLookup)?.has(sinkType)) {
44416
+ return false;
44417
+ }
44013
44418
  } else if (unc.get(step.variable)?.has(sinkType)) {
44014
44419
  return false;
44015
44420
  }
@@ -44047,7 +44452,7 @@ function makeConstPropVeto(constProp, graph, sources, code, language, sanitizers
44047
44452
  }
44048
44453
  if (!d) return true;
44049
44454
  if (d.line === sourceLine && sources.some((s) => s.line === sourceLine && !CONTAINER_READ_SOURCES.has(s.type))) {
44050
- if (step.line === sinkLine && !constProp.tainted.has(scopedVar)) {
44455
+ if (step.line === sinkLine && !constProp.tainted.has(fpLookup)) {
44051
44456
  for (let ln = sourceLine; ln <= sinkLine; ln++) {
44052
44457
  const calleeMatch = (lines[ln - 1] ?? "").match(/:=\s*(\w+)\s*\(/);
44053
44458
  if (calleeMatch && constProp.constantReturningMethods?.has(calleeMatch[1])) {
@@ -44071,13 +44476,17 @@ var TaintPropagationPass = class {
44071
44476
  const sinkFilter = ctx.getResult("sink-filter");
44072
44477
  const { sources, sinks, sanitizers } = sinkFilter;
44073
44478
  if (ctx.language === "java" && typeof ctx.code === "string") {
44479
+ const rhsByLocal = indexJavaLocalAssignments(ctx.code);
44480
+ const seen = /* @__PURE__ */ new Set();
44074
44481
  for (const s of sources) {
44075
- if (!s.variable) continue;
44076
- for (const [v] of buildJavaTaintedVars(ctx.code, /* @__PURE__ */ new Set([s.variable]))) {
44077
- if (v === s.variable) continue;
44078
- if (javaReceiverChainDerivedVar(ctx.code, v, s.variable)) {
44079
- constProp.tainted.add(v);
44080
- }
44482
+ if (!s.variable || seen.has(s.variable)) continue;
44483
+ seen.add(s.variable);
44484
+ const seedVar = s.variable;
44485
+ const candidates = [...rhsByLocal.keys()].filter((v) => !constProp.tainted.has(v) && javaReceiverChainDerivedVar(rhsByLocal, v, seedVar));
44486
+ if (candidates.length === 0) continue;
44487
+ const derived = buildJavaTaintedVars(ctx.code, /* @__PURE__ */ new Set([seedVar]));
44488
+ for (const v of candidates) {
44489
+ if (derived.has(v)) constProp.tainted.add(v);
44081
44490
  }
44082
44491
  }
44083
44492
  }
@@ -44222,7 +44631,15 @@ var TaintPropagationPass = class {
44222
44631
  for (const f of arrayFlows) {
44223
44632
  pushIfNew(f);
44224
44633
  }
44225
- const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code, types) ?? [];
44634
+ const collectionFlows = detectCollectionFlows(
44635
+ calls,
44636
+ sources,
44637
+ sinks,
44638
+ constProp,
44639
+ ctx.code,
44640
+ types,
44641
+ graph
44642
+ ) ?? [];
44226
44643
  for (const f of collectionFlows) {
44227
44644
  if (flowKeys.has(flowKey(f))) continue;
44228
44645
  const flowForCheck = {
@@ -44276,6 +44693,17 @@ var TaintPropagationPass = class {
44276
44693
  pushIfNew(f);
44277
44694
  }
44278
44695
  }
44696
+ if ((ctx.language === "javascript" || ctx.language === "typescript") && typeof ctx.code === "string") {
44697
+ const jsModuleFlows = detectJsModuleScopeFlows(
44698
+ ctx.code,
44699
+ sources,
44700
+ sinks,
44701
+ constProp.unreachableLines
44702
+ ) ?? [];
44703
+ for (const f of jsModuleFlows) {
44704
+ pushIfNew(f);
44705
+ }
44706
+ }
44279
44707
  const sanitizedNames = constProp.sanitizedVars;
44280
44708
  const sanitizedUncovered = constProp.sanitizedVarUncovered;
44281
44709
  let finalFlows = sanitizedNames.size === 0 ? flows : flows.filter((f) => {
@@ -44689,6 +45117,85 @@ function detectGoPackageGlobalFlows(code, _calls, sources, sinks, unreachableLin
44689
45117
  }
44690
45118
  return flows;
44691
45119
  }
45120
+ function detectJsModuleScopeFlows(code, sources, sinks, unreachableLines) {
45121
+ const flows = [];
45122
+ if (!code || sinks.length === 0) return flows;
45123
+ const lines = code.split("\n");
45124
+ const moduleVars = /* @__PURE__ */ new Set();
45125
+ for (const line of lines) {
45126
+ const stripped = line.replace(/\s+$/, "");
45127
+ if (!stripped || /^[ \t]/.test(line)) continue;
45128
+ const m = stripped.match(/^(?:let|var|const)\s+([A-Za-z_$][\w$]*)\b/);
45129
+ if (m) moduleVars.add(m[1]);
45130
+ }
45131
+ if (moduleVars.size === 0) return flows;
45132
+ const sourceVarNames = /* @__PURE__ */ new Set();
45133
+ for (const s of sources) {
45134
+ if (typeof s.variable === "string" && s.variable.length > 0) {
45135
+ sourceVarNames.add(s.variable);
45136
+ }
45137
+ }
45138
+ const JS_HTTP_SOURCE_RE = /\b(?:req|request)\.(?:body|query|params|headers|cookies|get)\b|\breq\.(?:param|query|body)\(/;
45139
+ const writes = [];
45140
+ for (let i2 = 0; i2 < lines.length; i2++) {
45141
+ const line = lines[i2];
45142
+ if (unreachableLines.has(i2 + 1)) continue;
45143
+ for (const lhs of moduleVars) {
45144
+ const declRe = new RegExp(`\\b(?:let|const|var)\\s+${lhs.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
45145
+ if (declRe.test(line)) continue;
45146
+ const m = line.match(
45147
+ new RegExp(`\\b${lhs.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*([^;]+)`)
45148
+ );
45149
+ if (!m) continue;
45150
+ const rhs = m[1];
45151
+ let taintedBy = null;
45152
+ const onLine = sources.filter((s) => s.line === i2 + 1);
45153
+ if (onLine.length > 0) {
45154
+ taintedBy = { line: onLine[0].line, type: onLine[0].type };
45155
+ } else if (JS_HTTP_SOURCE_RE.test(rhs)) {
45156
+ taintedBy = { line: i2 + 1, type: "http_body" };
45157
+ } else {
45158
+ for (const v of sourceVarNames) {
45159
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`);
45160
+ if (re.test(rhs)) {
45161
+ const src = sources.find((s) => s.variable === v);
45162
+ if (src) {
45163
+ taintedBy = { line: src.line, type: src.type };
45164
+ break;
45165
+ }
45166
+ }
45167
+ }
45168
+ }
45169
+ if (!taintedBy) continue;
45170
+ writes.push({ varName: lhs, line: i2 + 1, sourceLine: taintedBy.line, sourceType: taintedBy.type });
45171
+ }
45172
+ }
45173
+ if (writes.length === 0) return flows;
45174
+ for (const sink of sinks) {
45175
+ if (unreachableLines.has(sink.line)) continue;
45176
+ const sinkLineText = lines[sink.line - 1] ?? "";
45177
+ if (!sinkLineText) continue;
45178
+ for (const w of writes) {
45179
+ if (w.line >= sink.line) continue;
45180
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${w.varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`);
45181
+ if (!re.test(sinkLineText)) continue;
45182
+ flows.push({
45183
+ source_line: w.sourceLine,
45184
+ sink_line: sink.line,
45185
+ source_type: w.sourceType,
45186
+ sink_type: sink.type,
45187
+ path: [
45188
+ { variable: w.varName, line: w.sourceLine, type: "source" },
45189
+ { variable: w.varName, line: w.line, type: "assignment" },
45190
+ { variable: w.varName, line: sink.line, type: "sink" }
45191
+ ],
45192
+ confidence: 0.9,
45193
+ sanitized: false
45194
+ });
45195
+ }
45196
+ }
45197
+ return flows;
45198
+ }
44692
45199
  function methodHasLocalDecl(code, methodName, varName, atLine) {
44693
45200
  if (!code || !methodName) return false;
44694
45201
  const esc = methodName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -44756,7 +45263,9 @@ function isTaintedInMethod(taintedVars, varName, scopedName, fieldNames, isField
44756
45263
  }
44757
45264
  return true;
44758
45265
  }
44759
- function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code, types) {
45266
+ function detectCollectionFlows(calls, sources, sinks, constProp, code, types, graph) {
45267
+ const taintedVars = constProp.tainted;
45268
+ const unreachableLines = constProp.unreachableLines;
44760
45269
  const flows = [];
44761
45270
  const fieldNames = /* @__PURE__ */ new Set();
44762
45271
  for (const t of types ?? []) for (const f of t.fields ?? []) fieldNames.add(f.name);
@@ -44776,7 +45285,18 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
44776
45285
  }
44777
45286
  if (arg.variable) {
44778
45287
  const varName = arg.variable;
44779
- const scopedName = call.in_method ? `${call.in_method}:${varName}` : varName;
45288
+ let scopedName = varName;
45289
+ if (call.in_method) {
45290
+ const blockLine = graph ? defLineForUse(graph, varName, call.location.line) : call.location.line;
45291
+ scopedName = constPropFlowLookup(
45292
+ "java",
45293
+ call.in_method,
45294
+ varName,
45295
+ blockLine,
45296
+ constProp,
45297
+ true
45298
+ );
45299
+ }
44780
45300
  const expr = arg.expression ?? "";
44781
45301
  const explicitField = isFieldUseExpression(
44782
45302
  expr,