circle-ir 3.176.0 → 3.178.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 (43) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +75 -0
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/constant-propagation/propagator.d.ts +1 -2
  5. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  6. package/dist/analysis/constant-propagation/propagator.js +28 -30
  7. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  8. package/dist/analysis/dependency-versions.d.ts +102 -0
  9. package/dist/analysis/dependency-versions.d.ts.map +1 -0
  10. package/dist/analysis/dependency-versions.js +153 -0
  11. package/dist/analysis/dependency-versions.js.map +1 -0
  12. package/dist/analysis/dfg-walk.d.ts.map +1 -1
  13. package/dist/analysis/dfg-walk.js +28 -1
  14. package/dist/analysis/dfg-walk.js.map +1 -1
  15. package/dist/analysis/note-coalescer.d.ts +46 -0
  16. package/dist/analysis/note-coalescer.d.ts.map +1 -0
  17. package/dist/analysis/note-coalescer.js +106 -0
  18. package/dist/analysis/note-coalescer.js.map +1 -0
  19. package/dist/analysis/passes/deserialization-safety-gate-pass.d.ts +58 -0
  20. package/dist/analysis/passes/deserialization-safety-gate-pass.d.ts.map +1 -0
  21. package/dist/analysis/passes/deserialization-safety-gate-pass.js +122 -0
  22. package/dist/analysis/passes/deserialization-safety-gate-pass.js.map +1 -0
  23. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  24. package/dist/analysis/passes/sink-filter-pass.js +33 -2
  25. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  26. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  27. package/dist/analysis/taint-matcher.js +55 -33
  28. package/dist/analysis/taint-matcher.js.map +1 -1
  29. package/dist/analyzer.d.ts +34 -0
  30. package/dist/analyzer.d.ts.map +1 -1
  31. package/dist/analyzer.js +21 -1
  32. package/dist/analyzer.js.map +1 -1
  33. package/dist/browser/circle-ir.js +351 -47
  34. package/dist/core/circle-ir-core.cjs +197 -45
  35. package/dist/core/circle-ir-core.js +197 -45
  36. package/dist/core/extractors/calls.js +131 -1
  37. package/dist/core/extractors/calls.js.map +1 -1
  38. package/dist/core/extractors/cfg.d.ts.map +1 -1
  39. package/dist/core/extractors/cfg.js +17 -9
  40. package/dist/core/extractors/cfg.js.map +1 -1
  41. package/dist/types/index.d.ts +24 -0
  42. package/dist/types/index.d.ts.map +1 -1
  43. package/package.json +1 -1
@@ -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
@@ -13545,11 +13685,11 @@ function attachSourceLineCode(sources, sinks, code) {
13545
13685
  }
13546
13686
  function findSources(calls, types, patterns, sourceLines, language) {
13547
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
+ );
13548
13691
  for (const call of calls) {
13549
- for (const pattern of patterns) {
13550
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
13551
- continue;
13552
- }
13692
+ for (const pattern of patternsForLanguage) {
13553
13693
  if (matchesSourcePattern(call, pattern)) {
13554
13694
  sources.push({
13555
13695
  type: pattern.type,
@@ -13565,11 +13705,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
13565
13705
  for (const type of types) {
13566
13706
  for (const method of type.methods) {
13567
13707
  for (const param of method.parameters) {
13568
- for (const pattern of patterns) {
13708
+ for (const pattern of patternsForLanguage) {
13569
13709
  if (pattern.annotation && pattern.param_tainted) {
13570
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
13571
- continue;
13572
- }
13573
13710
  if (matchesAnnotation(param.annotations, pattern.annotation)) {
13574
13711
  const paramLine = param.line ?? method.start_line;
13575
13712
  sources.push({
@@ -13588,11 +13725,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
13588
13725
  }
13589
13726
  for (const type of types) {
13590
13727
  for (const method of type.methods) {
13591
- for (const pattern of patterns) {
13728
+ for (const pattern of patternsForLanguage) {
13592
13729
  if (!pattern.method_annotation) continue;
13593
- if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
13594
- continue;
13595
- }
13596
13730
  if (!matchesAnnotation(method.annotations, pattern.method_annotation)) continue;
13597
13731
  for (const param of method.parameters) {
13598
13732
  const paramLine = param.line ?? method.start_line;
@@ -14214,9 +14348,12 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
14214
14348
  return false;
14215
14349
  }
14216
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
+ );
14217
14354
  const sinkMap = /* @__PURE__ */ new Map();
14218
14355
  for (const call of calls) {
14219
- for (const pattern of patterns) {
14356
+ for (const pattern of patternsForLanguage) {
14220
14357
  if (matchesSinkPattern(call, pattern, typeHierarchy, language)) {
14221
14358
  if (isParameterizedQueryCall(call, pattern)) {
14222
14359
  continue;
@@ -14634,7 +14771,20 @@ function matchesAnnotation(annotations, targetAnnotation) {
14634
14771
  }
14635
14772
  return false;
14636
14773
  }
14774
+ var RECEIVER_MIGHT_BE_CLASS_CACHE = /* @__PURE__ */ new Map();
14775
+ var RECEIVER_MIGHT_BE_CLASS_CACHE_CAP = 1e4;
14637
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) {
14638
14788
  if (className.startsWith("*") && className.length > 1) {
14639
14789
  const suffix = className.slice(1).toLowerCase();
14640
14790
  let simpleReceiver = receiver;
@@ -16653,8 +16803,15 @@ var AnalysisPipeline = class {
16653
16803
  };
16654
16804
 
16655
16805
  // src/analysis/dfg-walk.ts
16806
+ var walkBackwardDefsMemo = /* @__PURE__ */ new WeakMap();
16656
16807
  function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
16657
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
+ }
16658
16815
  const visited = /* @__PURE__ */ new Set();
16659
16816
  const lines = /* @__PURE__ */ new Set();
16660
16817
  let hopCapReached = false;
@@ -16686,7 +16843,13 @@ function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
16686
16843
  queue.push(fromId);
16687
16844
  }
16688
16845
  }
16689
- 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;
16690
16853
  }
16691
16854
 
16692
16855
  // src/analysis/sanitizer-index.ts
@@ -18196,7 +18359,7 @@ var ConstantPropagator = class _ConstantPropagator {
18196
18359
  this.constructorParamPositions.clear();
18197
18360
  this.safePatternFieldsCache = null;
18198
18361
  this.isTaintedExpressionCache = null;
18199
- this.collectClassFields(tree.rootNode);
18362
+ const prePassMethods = this.collectClassFieldsAndMethods(tree.rootNode);
18200
18363
  for (const methodName of sanitizerMethods) {
18201
18364
  this.methodReturnsSanitized.add(methodName);
18202
18365
  }
@@ -18204,7 +18367,7 @@ var ConstantPropagator = class _ConstantPropagator {
18204
18367
  this.source,
18205
18368
  (name2) => this.lookupSymbol(name2)
18206
18369
  );
18207
- this.analyzeMethodReturns(tree.rootNode);
18370
+ this.analyzeMethodReturns(prePassMethods);
18208
18371
  this.seedPythonModuleConstants(tree.rootNode);
18209
18372
  this.visit(tree.rootNode);
18210
18373
  this.refineTaintFromConstants();
@@ -18274,8 +18437,7 @@ var ConstantPropagator = class _ConstantPropagator {
18274
18437
  /**
18275
18438
  * Pre-pass: Analyze all methods to detect those that always return constants or sanitized values.
18276
18439
  */
18277
- analyzeMethodReturns(root) {
18278
- const methods = this.findAllMethods(root);
18440
+ analyzeMethodReturns(methods) {
18279
18441
  for (const method of methods) {
18280
18442
  const methodName = this.getMethodName(method);
18281
18443
  if (!methodName) continue;
@@ -18539,11 +18701,15 @@ var ConstantPropagator = class _ConstantPropagator {
18539
18701
  * Collect all class field names (instance/static variables declared at class level).
18540
18702
  * These are variables declared directly in the class body, not inside methods.
18541
18703
  */
18542
- collectClassFields(root) {
18704
+ collectClassFieldsAndMethods(root) {
18705
+ const methods = [];
18543
18706
  const stack = [root];
18544
18707
  while (stack.length > 0) {
18545
18708
  const n = stack.pop();
18546
18709
  if (!n) continue;
18710
+ if (n.type === "method_declaration" || n.type === "function_declaration") {
18711
+ methods.push(n);
18712
+ }
18547
18713
  if (n.type === "class_body") {
18548
18714
  for (const child of n.children) {
18549
18715
  if (child.type === "field_declaration") {
@@ -18557,14 +18723,15 @@ var ConstantPropagator = class _ConstantPropagator {
18557
18723
  }
18558
18724
  }
18559
18725
  }
18560
- stack.push(child);
18726
+ if (child) stack.push(child);
18561
18727
  }
18562
18728
  continue;
18563
18729
  }
18564
18730
  for (const child of n.children) {
18565
- stack.push(child);
18731
+ if (child) stack.push(child);
18566
18732
  }
18567
18733
  }
18734
+ return methods;
18568
18735
  }
18569
18736
  /**
18570
18737
  * Sprint 9 #55 — seed the symbol table with Python module-level constant
@@ -18722,21 +18889,6 @@ var ConstantPropagator = class _ConstantPropagator {
18722
18889
  this.symbols.set(name2, value);
18723
18890
  }
18724
18891
  }
18725
- findAllMethods(node) {
18726
- const methods = [];
18727
- const stack = [node];
18728
- while (stack.length > 0) {
18729
- const n = stack.pop();
18730
- if (!n) continue;
18731
- if (n.type === "method_declaration" || n.type === "function_declaration") {
18732
- methods.push(n);
18733
- }
18734
- for (const child of n.children) {
18735
- if (child) stack.push(child);
18736
- }
18737
- }
18738
- return methods;
18739
- }
18740
18892
  getMethodName(method) {
18741
18893
  const nameNode = method.childForFieldName("name");
18742
18894
  if (nameNode) {
@@ -20353,6 +20505,49 @@ function applyLibraryApiSurfaceDowngrade(findings) {
20353
20505
  });
20354
20506
  }
20355
20507
 
20508
+ // src/analysis/note-coalescer.ts
20509
+ function coalesceNoteLevelFindings(findings) {
20510
+ if (findings.length < 2) return [...findings];
20511
+ const groups = /* @__PURE__ */ new Map();
20512
+ const order = [];
20513
+ for (const f of findings) {
20514
+ const key = `${f.file}\0${f.line}`;
20515
+ const bucket = groups.get(key);
20516
+ if (bucket) {
20517
+ bucket.push(f);
20518
+ } else {
20519
+ groups.set(key, [f]);
20520
+ order.push(key);
20521
+ }
20522
+ }
20523
+ const out2 = [];
20524
+ for (const key of order) {
20525
+ const bucket = groups.get(key);
20526
+ if (bucket.length === 1) {
20527
+ out2.push(bucket[0]);
20528
+ continue;
20529
+ }
20530
+ const allNote = bucket.every((f) => f.level === "note");
20531
+ if (!allNote) {
20532
+ for (const f of bucket) out2.push(f);
20533
+ continue;
20534
+ }
20535
+ const uniqueRuleIds = new Set(bucket.map((f) => f.rule_id));
20536
+ if (uniqueRuleIds.size < 2) {
20537
+ for (const f of bucket) out2.push(f);
20538
+ continue;
20539
+ }
20540
+ const sorted = [...bucket].sort((a, b) => a.rule_id.localeCompare(b.rule_id));
20541
+ const primary = sorted[0];
20542
+ const additional = sorted.slice(1).map((f) => f.rule_id).concat(...sorted.map((f) => f.labels ?? []));
20543
+ const uniqueLabels = Array.from(new Set(additional)).filter(
20544
+ (l) => l !== primary.rule_id
20545
+ );
20546
+ out2.push({ ...primary, labels: uniqueLabels });
20547
+ }
20548
+ return out2;
20549
+ }
20550
+
20356
20551
  // src/analysis/entry-point-detection.ts
20357
20552
  var TIER_1_METHOD_ANNOTATIONS = /* @__PURE__ */ new Set([
20358
20553
  // Spring MVC
@@ -31287,6 +31482,12 @@ var DATA_PARSER_TYPES = /* @__PURE__ */ new Set([
31287
31482
  "OptionParser",
31288
31483
  "CmdLineParser"
31289
31484
  ]);
31485
+ var JAVA_EVAL_PARSER_DENYLIST = /* @__PURE__ */ new Set([
31486
+ "GroovyShell",
31487
+ "GroovyClassLoader",
31488
+ "ScriptEngine",
31489
+ "CronParser"
31490
+ ]);
31290
31491
  var COMPILED_TEMPLATE_TYPES = /* @__PURE__ */ new Set([
31291
31492
  "Template",
31292
31493
  // Freemarker, Velocity
@@ -31729,7 +31930,12 @@ var SinkFilterPass = class {
31729
31930
  const method = sink.method ?? receiverMatch?.[2];
31730
31931
  if (method === "parse" && receiver) {
31731
31932
  const recvType = resolveJavaReceiverType(receiver, sink.line, sourceLines);
31732
- if (recvType && DATA_PARSER_TYPES.has(recvType)) return false;
31933
+ if (recvType) {
31934
+ if (DATA_PARSER_TYPES.has(recvType)) return false;
31935
+ if (recvType.endsWith("Parser") && !JAVA_EVAL_PARSER_DENYLIST.has(recvType)) {
31936
+ return false;
31937
+ }
31938
+ }
31733
31939
  }
31734
31940
  if ((method === "render" || method === "process" || method === "merge" || method === "renderTo") && receiver) {
31735
31941
  const recvType = resolveJavaReceiverType(receiver, sink.line, sourceLines);
@@ -32634,6 +32840,101 @@ var SinkSemanticsPass = class {
32634
32840
  }
32635
32841
  };
32636
32842
 
32843
+ // src/analysis/dependency-versions.ts
32844
+ function resolveFastjsonFromPom(pomXml) {
32845
+ if (!pomXml) return null;
32846
+ const propMatch = pomXml.match(/<fastjson\.version>\s*([^<\s]+)\s*<\/fastjson\.version>/);
32847
+ if (propMatch) {
32848
+ const version = propMatch[1];
32849
+ return { version, noneAutotype: /_noneautotype/i.test(version) };
32850
+ }
32851
+ const depRe = /<dependency>[\s\S]*?<\/dependency>/g;
32852
+ let m;
32853
+ while ((m = depRe.exec(pomXml)) !== null) {
32854
+ const block = m[0];
32855
+ const gid = block.match(/<groupId>\s*([^<\s]+)\s*<\/groupId>/)?.[1];
32856
+ const aid = block.match(/<artifactId>\s*([^<\s]+)\s*<\/artifactId>/)?.[1];
32857
+ if (gid !== "com.alibaba") continue;
32858
+ if (aid !== "fastjson" && aid !== "fastjson2") continue;
32859
+ const ver = block.match(/<version>\s*([^<\s]+)\s*<\/version>/)?.[1];
32860
+ if (!ver) continue;
32861
+ if (/^\$\{/.test(ver)) return null;
32862
+ return { version: ver, noneAutotype: /_noneautotype/i.test(ver) };
32863
+ }
32864
+ return null;
32865
+ }
32866
+ function fileReenablesFastjsonAutotype(source) {
32867
+ if (!source) return false;
32868
+ return /\bsetAutoTypeSupport\s*\(\s*true\b/.test(source);
32869
+ }
32870
+ function fileEnablesJacksonPolymorphism(source) {
32871
+ if (!source) return false;
32872
+ if (/\benableDefaultTyping\s*\(/.test(source)) return true;
32873
+ if (/\bactivateDefaultTyping\s*\(/.test(source)) return true;
32874
+ if (/@JsonTypeInfo\b/.test(source)) return true;
32875
+ return false;
32876
+ }
32877
+ function fileConfiguresSnakeYamlSafely(source) {
32878
+ if (!source) return false;
32879
+ if (/\bnew\s+SafeConstructor\s*\(/.test(source)) return true;
32880
+ if (/\bSafeConstructor\s+\w+\s*=/.test(source)) return true;
32881
+ return false;
32882
+ }
32883
+
32884
+ // src/analysis/passes/deserialization-safety-gate-pass.ts
32885
+ var FASTJSON_METHODS = /* @__PURE__ */ new Set(["parseObject", "parse"]);
32886
+ var FASTJSON_CLASSES = /* @__PURE__ */ new Set(["JSON", "JSONObject"]);
32887
+ var JACKSON_METHODS = /* @__PURE__ */ new Set(["readValue", "convertValue", "treeToValue"]);
32888
+ var JACKSON_CLASSES = /* @__PURE__ */ new Set(["ObjectMapper", "ObjectReader"]);
32889
+ var SNAKEYAML_METHODS = /* @__PURE__ */ new Set(["load", "loadAs", "loadAll"]);
32890
+ var SNAKEYAML_CLASSES = /* @__PURE__ */ new Set(["Yaml"]);
32891
+ var DeserializationSafetyGatePass = class {
32892
+ constructor(dependencyContext) {
32893
+ this.dependencyContext = dependencyContext;
32894
+ }
32895
+ dependencyContext;
32896
+ name = "deserialization-safety-gate";
32897
+ category = "security";
32898
+ run(ctx) {
32899
+ const { graph, language, code } = ctx;
32900
+ if (language !== "java") {
32901
+ return { droppedFastjson: 0, droppedJackson: 0, droppedSnakeYaml: 0 };
32902
+ }
32903
+ const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
32904
+ const pomXml = this.dependencyContext?.java?.pomXml;
32905
+ const fastjson = pomXml ? resolveFastjsonFromPom(pomXml) : null;
32906
+ const fastjsonHardened = fastjson?.noneAutotype === true && !fileReenablesFastjsonAutotype(code);
32907
+ const jacksonSafe = !fileEnablesJacksonPolymorphism(code);
32908
+ const snakeYamlSafe = fileConfiguresSnakeYamlSafely(code);
32909
+ let droppedFastjson = 0;
32910
+ let droppedJackson = 0;
32911
+ let droppedSnakeYaml = 0;
32912
+ const kept = sinks.filter((sink) => {
32913
+ if (sink.type !== "deserialization") return true;
32914
+ if (!sink.method) return true;
32915
+ if (fastjsonHardened && FASTJSON_METHODS.has(sink.method) && (sink.class === void 0 || FASTJSON_CLASSES.has(sink.class))) {
32916
+ droppedFastjson++;
32917
+ return false;
32918
+ }
32919
+ if (jacksonSafe && JACKSON_METHODS.has(sink.method) && sink.class !== void 0 && JACKSON_CLASSES.has(sink.class)) {
32920
+ droppedJackson++;
32921
+ return false;
32922
+ }
32923
+ if (snakeYamlSafe && SNAKEYAML_METHODS.has(sink.method) && sink.class !== void 0 && SNAKEYAML_CLASSES.has(sink.class)) {
32924
+ droppedSnakeYaml++;
32925
+ return false;
32926
+ }
32927
+ return true;
32928
+ });
32929
+ const totalDropped = droppedFastjson + droppedJackson + droppedSnakeYaml;
32930
+ if (totalDropped > 0) {
32931
+ sinks.length = 0;
32932
+ sinks.push(...kept);
32933
+ }
32934
+ return { droppedFastjson, droppedJackson, droppedSnakeYaml };
32935
+ }
32936
+ };
32937
+
32637
32938
  // src/analysis/passes/cli-main-reflection-suppress-pass.ts
32638
32939
  var REFLECTION_SINK_METHODS = /* @__PURE__ */ new Set([
32639
32940
  "forName",
@@ -42729,6 +43030,8 @@ async function analyze(code, filePath, language, options = {}) {
42729
43030
  pipeline.add(new MyBatisAnnotationSqlSinkPass());
42730
43031
  pipeline.add(new SinkFilterPass());
42731
43032
  if (!disabledPasses.has("sink-semantics")) pipeline.add(new SinkSemanticsPass());
43033
+ if (!disabledPasses.has("deserialization-safety-gate"))
43034
+ pipeline.add(new DeserializationSafetyGatePass(options.dependencyContext));
42732
43035
  if (!disabledPasses.has("cli-main-reflection-suppress"))
42733
43036
  pipeline.add(new CliMainReflectionSuppressPass());
42734
43037
  if (!disabledPasses.has("library-profile-sink-gate"))
@@ -42840,9 +43143,10 @@ async function analyze(code, filePath, language, options = {}) {
42840
43143
  downgradedFindings,
42841
43144
  makeProfileResolver(options.projectProfile)
42842
43145
  );
43146
+ const coalescedFindings = coalesceNoteLevelFindings(profiledFindings);
42843
43147
  const cappedFindings = applyPerFileFindingCap(
42844
43148
  filePath,
42845
- profiledFindings,
43149
+ coalescedFindings,
42846
43150
  options.perFileFindingCap ?? DEFAULT_PER_FILE_FINDING_CAP
42847
43151
  );
42848
43152
  return {