circle-ir 3.175.0 → 3.177.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.
@@ -7369,8 +7369,16 @@ function extractGoCallInfo(node) {
7369
7369
  if (funcNode.type === "selector_expression") {
7370
7370
  const operand = funcNode.childForFieldName("operand");
7371
7371
  const field = funcNode.childForFieldName("field");
7372
- receiver = operand ? getNodeText(operand) : null;
7373
7372
  methodName = field ? getNodeText(field) : getNodeText(funcNode);
7373
+ if (operand) {
7374
+ const opText = getNodeText(operand);
7375
+ if (operand.type === "identifier") {
7376
+ const resolved = resolveGoLocalReceiverType(opText, node);
7377
+ receiver = resolved !== null ? resolved : opText;
7378
+ } else {
7379
+ receiver = opText;
7380
+ }
7381
+ }
7374
7382
  } else if (funcNode.type === "identifier") {
7375
7383
  methodName = getNodeText(funcNode);
7376
7384
  } else {
@@ -7448,6 +7456,80 @@ function findGoEnclosingFunction(node) {
7448
7456
  }
7449
7457
  return null;
7450
7458
  }
7459
+ function resolveGoLocalReceiverType(operandName, callNode) {
7460
+ let cur = callNode.parent;
7461
+ while (cur) {
7462
+ if (cur.type === "method_declaration") {
7463
+ const receiver = cur.childForFieldName("receiver");
7464
+ if (receiver) {
7465
+ const t = extractGoParamTypeForName(receiver, operandName);
7466
+ if (t !== null) return t;
7467
+ }
7468
+ const params = cur.childForFieldName("parameters");
7469
+ if (params) {
7470
+ const t = extractGoParamTypeForName(params, operandName);
7471
+ if (t !== null) return t;
7472
+ }
7473
+ return null;
7474
+ }
7475
+ if (cur.type === "function_declaration" || cur.type === "func_literal") {
7476
+ const params = cur.childForFieldName("parameters");
7477
+ if (params) {
7478
+ const t = extractGoParamTypeForName(params, operandName);
7479
+ if (t !== null) return t;
7480
+ }
7481
+ return null;
7482
+ }
7483
+ cur = cur.parent;
7484
+ }
7485
+ return null;
7486
+ }
7487
+ function extractGoParamTypeForName(list, operandName) {
7488
+ for (let i2 = 0; i2 < list.namedChildCount; i2++) {
7489
+ const child = list.namedChild(i2);
7490
+ if (!child || child.type !== "parameter_declaration") continue;
7491
+ const typeNode = child.childForFieldName("type");
7492
+ if (!typeNode) continue;
7493
+ let matched = false;
7494
+ for (let j = 0; j < child.namedChildCount; j++) {
7495
+ const c = child.namedChild(j);
7496
+ if (!c) continue;
7497
+ if (c.type === "identifier" && getNodeText(c) === operandName) {
7498
+ matched = true;
7499
+ break;
7500
+ }
7501
+ if (c.type === "identifier_list") {
7502
+ for (let k = 0; k < c.namedChildCount; k++) {
7503
+ const id = c.namedChild(k);
7504
+ if (id && id.type === "identifier" && getNodeText(id) === operandName) {
7505
+ matched = true;
7506
+ break;
7507
+ }
7508
+ }
7509
+ if (matched) break;
7510
+ }
7511
+ }
7512
+ if (!matched) continue;
7513
+ return extractGoTypeLastSegment(typeNode);
7514
+ }
7515
+ return null;
7516
+ }
7517
+ function extractGoTypeLastSegment(typeNode) {
7518
+ if (typeNode.type === "pointer_type") {
7519
+ const inner = typeNode.namedChild(0);
7520
+ return inner ? extractGoTypeLastSegment(inner) : null;
7521
+ }
7522
+ if (typeNode.type === "qualified_type") {
7523
+ const name2 = typeNode.childForFieldName("name");
7524
+ if (name2) return getNodeText(name2);
7525
+ const last = typeNode.namedChild(typeNode.namedChildCount - 1);
7526
+ return last ? getNodeText(last) : null;
7527
+ }
7528
+ if (typeNode.type === "type_identifier" || typeNode.type === "identifier") {
7529
+ return getNodeText(typeNode);
7530
+ }
7531
+ return null;
7532
+ }
7451
7533
 
7452
7534
  // src/core/extractors/imports.ts
7453
7535
  function detectLanguage2(tree) {
@@ -8196,10 +8278,10 @@ function buildCFG(tree, language, cache) {
8196
8278
  const allEdges = [];
8197
8279
  let blockIdCounter = 0;
8198
8280
  if (effectiveLanguage === "bash") {
8199
- return buildBashCFG(tree, blockIdCounter);
8281
+ return buildBashCFG(tree, blockIdCounter, cache);
8200
8282
  }
8201
8283
  if (effectiveLanguage === "go") {
8202
- return buildGoCFG(tree, blockIdCounter);
8284
+ return buildGoCFG(tree, blockIdCounter, cache);
8203
8285
  }
8204
8286
  if (isJavaScript) {
8205
8287
  const functions = [
@@ -8556,11 +8638,11 @@ function processSwitchStatement(stmt, startId, blocks, edges, isJavaScript) {
8556
8638
  nextId: currentId
8557
8639
  };
8558
8640
  }
8559
- function buildBashCFG(tree, startId) {
8641
+ function buildBashCFG(tree, startId, cache) {
8560
8642
  const allBlocks = [];
8561
8643
  const allEdges = [];
8562
8644
  let blockIdCounter = startId;
8563
- const functions = findNodes(tree.rootNode, "function_definition");
8645
+ const functions = getNodesFromCache(tree.rootNode, "function_definition", cache);
8564
8646
  for (const func2 of functions) {
8565
8647
  const body2 = func2.childForFieldName("body");
8566
8648
  if (!body2) continue;
@@ -8677,12 +8759,12 @@ function isStatement(node, isJavaScript) {
8677
8759
  ]);
8678
8760
  return isJavaScript ? jsStatementTypes.has(node.type) : javaStatementTypes.has(node.type);
8679
8761
  }
8680
- function buildGoCFG(tree, blockIdCounter) {
8762
+ function buildGoCFG(tree, blockIdCounter, cache) {
8681
8763
  const allBlocks = [];
8682
8764
  const allEdges = [];
8683
8765
  const functions = [
8684
- ...findNodes(tree.rootNode, "function_declaration"),
8685
- ...findNodes(tree.rootNode, "method_declaration")
8766
+ ...getNodesFromCache(tree.rootNode, "function_declaration", cache),
8767
+ ...getNodesFromCache(tree.rootNode, "method_declaration", cache)
8686
8768
  ];
8687
8769
  for (const func2 of functions) {
8688
8770
  const body2 = func2.childForFieldName("body");
@@ -11247,6 +11329,59 @@ var OPEN_REDIRECT_FRAMEWORK_SINKS = [
11247
11329
  { method: "Redirect", class: "Context", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [1], languages: ["go"] },
11248
11330
  { method: "Redirect", class: "Ctx", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["go"] }
11249
11331
  ];
11332
+ var DESERIALIZATION_FRAMEWORK_SINKS = [
11333
+ // --- Python: stdlib + popular third-party --------------------------------
11334
+ // pickle: known-dangerous, any unpickle on untrusted bytes is RCE.
11335
+ { method: "loads", class: "pickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
11336
+ { method: "load", class: "pickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
11337
+ // cPickle alias (Python 2 name, still around in older codebases).
11338
+ { method: "loads", class: "cPickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
11339
+ { method: "load", class: "cPickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
11340
+ // marshal: stdlib code-object deserializer. Loading a tainted bytestring
11341
+ // as a code object followed by `exec` is arbitrary-code execution.
11342
+ { method: "loads", class: "marshal", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
11343
+ { method: "load", class: "marshal", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
11344
+ // dill: pickle superset — same RCE profile.
11345
+ { method: "loads", class: "dill", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
11346
+ { method: "load", class: "dill", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
11347
+ // jsonpickle: JSON wrapper around pickle — trusts `py/object` marker.
11348
+ { method: "decode", class: "jsonpickle", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["python"] },
11349
+ // --- Go: encoding/gob + yaml.Unmarshal -----------------------------------
11350
+ // gob.NewDecoder(r).Decode(&v): tainted io.Reader → arbitrary Go values.
11351
+ { method: "Decode", class: "Decoder", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0], languages: ["go"] },
11352
+ // gopkg.in/yaml.v2 + v3 top-level function; interface{} target is unsafe.
11353
+ { method: "Unmarshal", class: "yaml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0], languages: ["go"] },
11354
+ // --- JS/TS: node-serialize ------------------------------------------------
11355
+ // Known-dangerous — accepts embedded IIFE that runs during deserialize.
11356
+ { method: "unserialize", class: "nodeSerialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] }
11357
+ ];
11358
+ var NOSQL_FRAMEWORK_SINKS = [
11359
+ // --- Python: pymongo Collection ------------------------------------------
11360
+ // Every filter-taking Collection method; filter arg[0] is the query dict.
11361
+ { method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
11362
+ { method: "find_one", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
11363
+ { method: "aggregate", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
11364
+ { method: "update_one", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["python"] },
11365
+ { method: "update_many", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["python"] },
11366
+ { method: "delete_one", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
11367
+ { method: "delete_many", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
11368
+ { method: "count_documents", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["python"] },
11369
+ // --- Java: Spring Data MongoTemplate + native MongoCollection ------------
11370
+ { method: "find", class: "MongoTemplate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
11371
+ { method: "findOne", class: "MongoTemplate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
11372
+ { method: "findAll", class: "MongoTemplate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
11373
+ { method: "find", class: "MongoCollection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
11374
+ { method: "aggregate", class: "MongoCollection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["java"] },
11375
+ // --- Go: go.mongodb.org/mongo-driver Collection --------------------------
11376
+ // These fire once Go local-receiver type resolution lands (see
11377
+ // taint-matcher.ts + #240 ship 2 Go receiver work). Same gate as the
11378
+ // gin/fiber Ctx sinks in ship 1.
11379
+ { method: "Find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1], languages: ["go"] },
11380
+ { method: "FindOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1], languages: ["go"] },
11381
+ { method: "UpdateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1, 2], languages: ["go"] },
11382
+ { method: "UpdateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1, 2], languages: ["go"] },
11383
+ { method: "DeleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [1], languages: ["go"] }
11384
+ ];
11250
11385
  var TRUST_BOUNDARY_FRAMEWORK_SINKS = [
11251
11386
  // --- Python: Django cache write -----------------------------------------
11252
11387
  { method: "set", class: "cache", type: "trust_boundary", cwe: "CWE-501", severity: "medium", arg_positions: [1], languages: ["python"] },
@@ -12930,7 +13065,12 @@ var DEFAULT_SINKS = [
12930
13065
  // keep the DEFAULT_SINKS literal within TypeScript's union-type
12931
13066
  // inference complexity limit (TS2590). See ~lines 661-752.
12932
13067
  ...OPEN_REDIRECT_FRAMEWORK_SINKS,
12933
- ...TRUST_BOUNDARY_FRAMEWORK_SINKS
13068
+ ...TRUST_BOUNDARY_FRAMEWORK_SINKS,
13069
+ // cognium-dev #240 ship 2 — extended framework sinks for
13070
+ // deserialization (CWE-502) and nosql_injection (CWE-943). Same
13071
+ // pattern: constants defined near the ship-1 blocks and spread here.
13072
+ ...DESERIALIZATION_FRAMEWORK_SINKS,
13073
+ ...NOSQL_FRAMEWORK_SINKS
12934
13074
  ];
12935
13075
  var DEFAULT_SANITIZERS = [
12936
13076
  // SQL Injection - proper parameter binding sanitizes input
@@ -13360,6 +13500,72 @@ var DEFAULT_HEADER_RULES = [
13360
13500
  }
13361
13501
  ];
13362
13502
 
13503
+ // src/analysis/arg-type-resolver.ts
13504
+ function findEnclosingType(callInMethod, types) {
13505
+ if (!callInMethod) return null;
13506
+ for (const t of types) {
13507
+ for (const m of t.methods) {
13508
+ if (m.name === callInMethod) return t;
13509
+ }
13510
+ }
13511
+ return null;
13512
+ }
13513
+ function resolveIdentifierType(name2, callInMethod, types) {
13514
+ if (!name2) return null;
13515
+ const enclosing = findEnclosingType(callInMethod, types);
13516
+ if (enclosing) {
13517
+ for (const m of enclosing.methods) {
13518
+ if (m.name !== callInMethod) continue;
13519
+ for (const p of m.parameters) {
13520
+ if (p.name === name2 && p.type) return p.type;
13521
+ }
13522
+ }
13523
+ for (const f of enclosing.fields) {
13524
+ if (f.name === name2 && f.type) return f.type;
13525
+ }
13526
+ return null;
13527
+ }
13528
+ for (const t of types) {
13529
+ for (const m of t.methods) {
13530
+ for (const p of m.parameters) {
13531
+ if (p.name === name2 && p.type) return p.type;
13532
+ }
13533
+ }
13534
+ for (const f of t.fields) {
13535
+ if (f.name === name2 && f.type) return f.type;
13536
+ }
13537
+ }
13538
+ return null;
13539
+ }
13540
+ function resolveCallReturnType(calleeName, callInMethod, types) {
13541
+ if (!calleeName) return null;
13542
+ const enclosing = findEnclosingType(callInMethod, types);
13543
+ if (enclosing) {
13544
+ for (const m of enclosing.methods) {
13545
+ if (m.name === calleeName && m.return_type) return m.return_type;
13546
+ }
13547
+ }
13548
+ for (const t of types) {
13549
+ for (const m of t.methods) {
13550
+ if (m.name === calleeName && m.return_type) return m.return_type;
13551
+ }
13552
+ }
13553
+ return null;
13554
+ }
13555
+ function isBoundedClassType(rawType) {
13556
+ if (!rawType) return false;
13557
+ const stripped = rawType.trim().replace(/^(?:java\.lang\.)+/, "");
13558
+ return /^Class(?:\s*<[\s\S]*>)?$/.test(stripped);
13559
+ }
13560
+ function isArgvContainerType(rawType) {
13561
+ if (!rawType) return false;
13562
+ const s = rawType.trim().replace(/^(?:java\.util\.|java\.lang\.)+/, "");
13563
+ if (/^String\s*(?:\[\s*\]|\.\.\.)$/.test(s)) return true;
13564
+ return /^(?:List|ArrayList|LinkedList|Collection|Iterable|Deque|Queue)\s*<\s*(?:String|CharSequence|java\.lang\.String)\b/.test(
13565
+ s
13566
+ );
13567
+ }
13568
+
13363
13569
  // src/analysis/taint-matcher.ts
13364
13570
  var PYTHON_TAINTED_PATTERNS = [
13365
13571
  { pattern: /\brequest\.args\b/, sourceType: "http_param" },
@@ -13385,7 +13591,7 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
13385
13591
  const sources = findSources(calls, types, config.sources, sourceLines, language);
13386
13592
  let sinkPatterns = expandPromisifyAliases(config.sinks, sourceLines, language);
13387
13593
  sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
13388
- const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines);
13594
+ const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
13389
13595
  const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
13390
13596
  return { sources, sinks, sanitizers };
13391
13597
  }
@@ -13479,11 +13685,11 @@ function attachSourceLineCode(sources, sinks, code) {
13479
13685
  }
13480
13686
  function findSources(calls, types, patterns, sourceLines, language) {
13481
13687
  const sources = [];
13688
+ const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
13689
+ (p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
13690
+ );
13482
13691
  for (const call of calls) {
13483
- for (const pattern of patterns) {
13484
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
13485
- continue;
13486
- }
13692
+ for (const pattern of patternsForLanguage) {
13487
13693
  if (matchesSourcePattern(call, pattern)) {
13488
13694
  sources.push({
13489
13695
  type: pattern.type,
@@ -13499,11 +13705,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
13499
13705
  for (const type of types) {
13500
13706
  for (const method of type.methods) {
13501
13707
  for (const param of method.parameters) {
13502
- for (const pattern of patterns) {
13708
+ for (const pattern of patternsForLanguage) {
13503
13709
  if (pattern.annotation && pattern.param_tainted) {
13504
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
13505
- continue;
13506
- }
13507
13710
  if (matchesAnnotation(param.annotations, pattern.annotation)) {
13508
13711
  const paramLine = param.line ?? method.start_line;
13509
13712
  sources.push({
@@ -13522,11 +13725,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
13522
13725
  }
13523
13726
  for (const type of types) {
13524
13727
  for (const method of type.methods) {
13525
- for (const pattern of patterns) {
13728
+ for (const pattern of patternsForLanguage) {
13526
13729
  if (!pattern.method_annotation) continue;
13527
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
13528
- continue;
13529
- }
13530
13730
  if (!matchesAnnotation(method.annotations, pattern.method_annotation)) continue;
13531
13731
  for (const param of method.parameters) {
13532
13732
  const paramLine = param.line ?? method.start_line;
@@ -13963,13 +14163,30 @@ function isSafeRustCommandCall(call, pattern, language) {
13963
14163
  return false;
13964
14164
  }
13965
14165
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
13966
- function argIsClassLiteral(call, position) {
14166
+ function argIsClassLiteral(call, position, types) {
13967
14167
  const arg = call.arguments.find((a) => a.position === position);
13968
14168
  if (!arg) return false;
13969
14169
  const expr = (arg.literal ?? arg.expression ?? "").trim();
13970
14170
  if (!expr) return false;
13971
14171
  if (CLASS_LITERAL_RE.test(expr)) return true;
13972
- return TYPE_TOKEN_RE.test(expr);
14172
+ if (TYPE_TOKEN_RE.test(expr)) return true;
14173
+ if (!types || types.length === 0) return false;
14174
+ const argExpr = (arg.expression ?? "").trim();
14175
+ const varName = arg.variable ?? "";
14176
+ const isBareIdentifier = varName !== "" && argExpr === varName && !/[(.]/.test(argExpr);
14177
+ if (isBareIdentifier) {
14178
+ const t = resolveIdentifierType(varName, call.in_method, types);
14179
+ if (isBoundedClassType(t)) return true;
14180
+ }
14181
+ const isCallExpr = varName !== "" && new RegExp(`^${escapeRe(varName)}\\s*\\(`).test(argExpr);
14182
+ if (isCallExpr) {
14183
+ const t = resolveCallReturnType(varName, call.in_method, types);
14184
+ if (isBoundedClassType(t)) return true;
14185
+ }
14186
+ return false;
14187
+ }
14188
+ function escapeRe(s) {
14189
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13973
14190
  }
13974
14191
  var TYPE_TOKEN_RE = /^new\s+(?:TypeReference|TypeToken)\s*<[\s\S]*>\s*\(\s*\)\s*\{\s*\}$/;
13975
14192
  function argIsStringLiteral(call, position) {
@@ -14130,10 +14347,13 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
14130
14347
  }
14131
14348
  return false;
14132
14349
  }
14133
- function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
14350
+ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
14351
+ const patternsForLanguage = language === void 0 ? patterns : patterns.filter(
14352
+ (p) => !p.languages || p.languages.length === 0 || p.languages.includes(language)
14353
+ );
14134
14354
  const sinkMap = /* @__PURE__ */ new Map();
14135
14355
  for (const call of calls) {
14136
- for (const pattern of patterns) {
14356
+ for (const pattern of patternsForLanguage) {
14137
14357
  if (matchesSinkPattern(call, pattern, typeHierarchy, language)) {
14138
14358
  if (isParameterizedQueryCall(call, pattern)) {
14139
14359
  continue;
@@ -14150,7 +14370,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
14150
14370
  if (isSafeJSChildProcessCall(call, pattern, language)) {
14151
14371
  continue;
14152
14372
  }
14153
- if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
14373
+ if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at, types)) {
14154
14374
  continue;
14155
14375
  }
14156
14376
  if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
@@ -14551,7 +14771,20 @@ function matchesAnnotation(annotations, targetAnnotation) {
14551
14771
  }
14552
14772
  return false;
14553
14773
  }
14774
+ var RECEIVER_MIGHT_BE_CLASS_CACHE = /* @__PURE__ */ new Map();
14775
+ var RECEIVER_MIGHT_BE_CLASS_CACHE_CAP = 1e4;
14554
14776
  function receiverMightBeClass(receiver, className) {
14777
+ const key = receiver + "\0" + className;
14778
+ const cached = RECEIVER_MIGHT_BE_CLASS_CACHE.get(key);
14779
+ if (cached !== void 0) return cached;
14780
+ const result = receiverMightBeClassImpl(receiver, className);
14781
+ if (RECEIVER_MIGHT_BE_CLASS_CACHE.size >= RECEIVER_MIGHT_BE_CLASS_CACHE_CAP) {
14782
+ RECEIVER_MIGHT_BE_CLASS_CACHE.clear();
14783
+ }
14784
+ RECEIVER_MIGHT_BE_CLASS_CACHE.set(key, result);
14785
+ return result;
14786
+ }
14787
+ function receiverMightBeClassImpl(receiver, className) {
14555
14788
  if (className.startsWith("*") && className.length > 1) {
14556
14789
  const suffix = className.slice(1).toLowerCase();
14557
14790
  let simpleReceiver = receiver;
@@ -16570,8 +16803,15 @@ var AnalysisPipeline = class {
16570
16803
  };
16571
16804
 
16572
16805
  // src/analysis/dfg-walk.ts
16806
+ var walkBackwardDefsMemo = /* @__PURE__ */ new WeakMap();
16573
16807
  function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
16574
16808
  const maxHops = options.maxHops ?? 32;
16809
+ let perFile = walkBackwardDefsMemo.get(chainsByToDef);
16810
+ if (perFile !== void 0) {
16811
+ const key = `${startDefId}|${maxHops}`;
16812
+ const hit = perFile.get(key);
16813
+ if (hit !== void 0) return hit;
16814
+ }
16575
16815
  const visited = /* @__PURE__ */ new Set();
16576
16816
  const lines = /* @__PURE__ */ new Set();
16577
16817
  let hopCapReached = false;
@@ -16603,7 +16843,13 @@ function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
16603
16843
  queue.push(fromId);
16604
16844
  }
16605
16845
  }
16606
- return { visited, lines, hopCapReached };
16846
+ const result = { visited, lines, hopCapReached };
16847
+ if (perFile === void 0) {
16848
+ perFile = /* @__PURE__ */ new Map();
16849
+ walkBackwardDefsMemo.set(chainsByToDef, perFile);
16850
+ }
16851
+ perFile.set(`${startDefId}|${maxHops}`, result);
16852
+ return result;
16607
16853
  }
16608
16854
 
16609
16855
  // src/analysis/sanitizer-index.ts
@@ -18113,7 +18359,7 @@ var ConstantPropagator = class _ConstantPropagator {
18113
18359
  this.constructorParamPositions.clear();
18114
18360
  this.safePatternFieldsCache = null;
18115
18361
  this.isTaintedExpressionCache = null;
18116
- this.collectClassFields(tree.rootNode);
18362
+ const prePassMethods = this.collectClassFieldsAndMethods(tree.rootNode);
18117
18363
  for (const methodName of sanitizerMethods) {
18118
18364
  this.methodReturnsSanitized.add(methodName);
18119
18365
  }
@@ -18121,7 +18367,7 @@ var ConstantPropagator = class _ConstantPropagator {
18121
18367
  this.source,
18122
18368
  (name2) => this.lookupSymbol(name2)
18123
18369
  );
18124
- this.analyzeMethodReturns(tree.rootNode);
18370
+ this.analyzeMethodReturns(prePassMethods);
18125
18371
  this.seedPythonModuleConstants(tree.rootNode);
18126
18372
  this.visit(tree.rootNode);
18127
18373
  this.refineTaintFromConstants();
@@ -18191,8 +18437,7 @@ var ConstantPropagator = class _ConstantPropagator {
18191
18437
  /**
18192
18438
  * Pre-pass: Analyze all methods to detect those that always return constants or sanitized values.
18193
18439
  */
18194
- analyzeMethodReturns(root) {
18195
- const methods = this.findAllMethods(root);
18440
+ analyzeMethodReturns(methods) {
18196
18441
  for (const method of methods) {
18197
18442
  const methodName = this.getMethodName(method);
18198
18443
  if (!methodName) continue;
@@ -18456,11 +18701,15 @@ var ConstantPropagator = class _ConstantPropagator {
18456
18701
  * Collect all class field names (instance/static variables declared at class level).
18457
18702
  * These are variables declared directly in the class body, not inside methods.
18458
18703
  */
18459
- collectClassFields(root) {
18704
+ collectClassFieldsAndMethods(root) {
18705
+ const methods = [];
18460
18706
  const stack = [root];
18461
18707
  while (stack.length > 0) {
18462
18708
  const n = stack.pop();
18463
18709
  if (!n) continue;
18710
+ if (n.type === "method_declaration" || n.type === "function_declaration") {
18711
+ methods.push(n);
18712
+ }
18464
18713
  if (n.type === "class_body") {
18465
18714
  for (const child of n.children) {
18466
18715
  if (child.type === "field_declaration") {
@@ -18474,14 +18723,15 @@ var ConstantPropagator = class _ConstantPropagator {
18474
18723
  }
18475
18724
  }
18476
18725
  }
18477
- stack.push(child);
18726
+ if (child) stack.push(child);
18478
18727
  }
18479
18728
  continue;
18480
18729
  }
18481
18730
  for (const child of n.children) {
18482
- stack.push(child);
18731
+ if (child) stack.push(child);
18483
18732
  }
18484
18733
  }
18734
+ return methods;
18485
18735
  }
18486
18736
  /**
18487
18737
  * Sprint 9 #55 — seed the symbol table with Python module-level constant
@@ -18639,21 +18889,6 @@ var ConstantPropagator = class _ConstantPropagator {
18639
18889
  this.symbols.set(name2, value);
18640
18890
  }
18641
18891
  }
18642
- findAllMethods(node) {
18643
- const methods = [];
18644
- const stack = [node];
18645
- while (stack.length > 0) {
18646
- const n = stack.pop();
18647
- if (!n) continue;
18648
- if (n.type === "method_declaration" || n.type === "function_declaration") {
18649
- methods.push(n);
18650
- }
18651
- for (const child of n.children) {
18652
- if (child) stack.push(child);
18653
- }
18654
- }
18655
- return methods;
18656
- }
18657
18892
  getMethodName(method) {
18658
18893
  const nameNode = method.childForFieldName("name");
18659
18894
  if (nameNode) {
@@ -31145,6 +31380,9 @@ var MyBatisAnnotationSqlSinkPass = class {
31145
31380
  };
31146
31381
 
31147
31382
  // src/analysis/passes/sink-filter-pass.ts
31383
+ function escapeReSf(s) {
31384
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31385
+ }
31148
31386
  var JS_XSS_SANITIZERS = [
31149
31387
  /\bDOMPurify\.sanitize\s*\(/,
31150
31388
  /\bsanitizeHtml\s*\(/,
@@ -31201,6 +31439,12 @@ var DATA_PARSER_TYPES = /* @__PURE__ */ new Set([
31201
31439
  "OptionParser",
31202
31440
  "CmdLineParser"
31203
31441
  ]);
31442
+ var JAVA_EVAL_PARSER_DENYLIST = /* @__PURE__ */ new Set([
31443
+ "GroovyShell",
31444
+ "GroovyClassLoader",
31445
+ "ScriptEngine",
31446
+ "CronParser"
31447
+ ]);
31204
31448
  var COMPILED_TEMPLATE_TYPES = /* @__PURE__ */ new Set([
31205
31449
  "Template",
31206
31450
  // Freemarker, Velocity
@@ -31643,7 +31887,12 @@ var SinkFilterPass = class {
31643
31887
  const method = sink.method ?? receiverMatch?.[2];
31644
31888
  if (method === "parse" && receiver) {
31645
31889
  const recvType = resolveJavaReceiverType(receiver, sink.line, sourceLines);
31646
- if (recvType && DATA_PARSER_TYPES.has(recvType)) return false;
31890
+ if (recvType) {
31891
+ if (DATA_PARSER_TYPES.has(recvType)) return false;
31892
+ if (recvType.endsWith("Parser") && !JAVA_EVAL_PARSER_DENYLIST.has(recvType)) {
31893
+ return false;
31894
+ }
31895
+ }
31647
31896
  }
31648
31897
  if ((method === "render" || method === "process" || method === "merge" || method === "renderTo") && receiver) {
31649
31898
  const recvType = resolveJavaReceiverType(receiver, sink.line, sourceLines);
@@ -31740,12 +31989,30 @@ var SinkFilterPass = class {
31740
31989
  }
31741
31990
  if (language === "java") {
31742
31991
  const sourceLines = ctx.code.split("\n");
31992
+ const irCalls = ctx.graph.ir.calls;
31993
+ const irTypes = ctx.graph.ir.types;
31743
31994
  filtered = filtered.filter((sink) => {
31744
31995
  if (sink.type !== "command_injection") return true;
31745
31996
  if (sink.method !== "ProcessBuilder" && sink.method !== "start") return true;
31746
31997
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
31747
31998
  if (!/\bnew\s+ProcessBuilder\s*\(/.test(sinkLineText)) return true;
31748
31999
  if (PROCESS_BUILDER_ARGV_FORM_RE.test(sinkLineText)) return false;
32000
+ const ctorCall = irCalls.find(
32001
+ (c) => c.method_name === "ProcessBuilder" && c.location.line === sink.line && (c.receiver === null || c.receiver === void 0)
32002
+ );
32003
+ if (!ctorCall || ctorCall.arguments.length === 0) return true;
32004
+ const arg0 = ctorCall.arguments[0];
32005
+ if (!arg0) return true;
32006
+ const argExpr = (arg0.expression ?? "").trim();
32007
+ const varName = arg0.variable ?? "";
32008
+ if (varName !== "" && argExpr === varName && !/[(.]/.test(argExpr)) {
32009
+ const t = resolveIdentifierType(varName, ctorCall.in_method, irTypes);
32010
+ if (isArgvContainerType(t)) return false;
32011
+ }
32012
+ if (varName !== "" && new RegExp(`^${escapeReSf(varName)}\\s*\\(`).test(argExpr)) {
32013
+ const t = resolveCallReturnType(varName, ctorCall.in_method, irTypes);
32014
+ if (isArgvContainerType(t)) return false;
32015
+ }
31749
32016
  return true;
31750
32017
  });
31751
32018
  }