circle-ir 4.9.28 → 4.10.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 (46) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +63 -2
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  5. package/dist/analysis/passes/language-sources-pass.js +54 -7
  6. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  7. package/dist/analysis/passes/taint-propagation-pass.js +117 -12
  8. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  9. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  10. package/dist/analysis/taint-matcher.js +41 -0
  11. package/dist/analysis/taint-matcher.js.map +1 -1
  12. package/dist/analyzer.d.ts +12 -0
  13. package/dist/analyzer.d.ts.map +1 -1
  14. package/dist/analyzer.js +1 -1
  15. package/dist/analyzer.js.map +1 -1
  16. package/dist/browser/circle-ir.js +260 -28
  17. package/dist/core/circle-ir-core.cjs +179 -12
  18. package/dist/core/circle-ir-core.js +179 -12
  19. package/dist/core/extractors/calls.js +29 -1
  20. package/dist/core/extractors/calls.js.map +1 -1
  21. package/dist/core/extractors/types.d.ts +28 -1
  22. package/dist/core/extractors/types.d.ts.map +1 -1
  23. package/dist/core/extractors/types.js +136 -10
  24. package/dist/core/extractors/types.js.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/navigation/index.d.ts +8 -0
  30. package/dist/navigation/index.d.ts.map +1 -0
  31. package/dist/navigation/index.js +6 -0
  32. package/dist/navigation/index.js.map +1 -0
  33. package/dist/navigation/navigation-index.d.ts +208 -0
  34. package/dist/navigation/navigation-index.d.ts.map +1 -0
  35. package/dist/navigation/navigation-index.js +968 -0
  36. package/dist/navigation/navigation-index.js.map +1 -0
  37. package/dist/navigation/types.d.ts +160 -0
  38. package/dist/navigation/types.d.ts.map +1 -0
  39. package/dist/navigation/types.js +17 -0
  40. package/dist/navigation/types.js.map +1 -0
  41. package/dist/types/config.d.ts +14 -0
  42. package/dist/types/config.d.ts.map +1 -1
  43. package/dist/types/index.d.ts +19 -0
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/dist/types/index.js.map +1 -1
  46. package/package.json +1 -1
@@ -4412,7 +4412,7 @@ function detectLanguage(tree) {
4412
4412
  if (pythonScore > jsScore && pythonScore > javaScore) return "python";
4413
4413
  return jsScore > javaScore ? "javascript" : "java";
4414
4414
  }
4415
- function extractTypes(tree, cache, language) {
4415
+ function extractTypes(tree, cache, language, options) {
4416
4416
  const effectiveLanguage = language ?? detectLanguage(tree);
4417
4417
  const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
4418
4418
  const isPython = effectiveLanguage === "python";
@@ -4432,7 +4432,22 @@ function extractTypes(tree, cache, language) {
4432
4432
  if (effectiveLanguage === "csharp") {
4433
4433
  return extractCSharpTypes(tree, cache);
4434
4434
  }
4435
- return extractJavaTypes(tree, cache);
4435
+ return extractJavaTypes(tree, cache, options);
4436
+ }
4437
+ function csharpMemberNodes(body2) {
4438
+ if (!body2) return [];
4439
+ const out2 = [];
4440
+ const visit = (parent, depth) => {
4441
+ if (depth > 32) return;
4442
+ for (let i2 = 0; i2 < parent.childCount; i2++) {
4443
+ const c = parent.child(i2);
4444
+ if (!c) continue;
4445
+ if (c.type.startsWith("preproc_")) visit(c, depth + 1);
4446
+ else out2.push(c);
4447
+ }
4448
+ };
4449
+ visit(body2, 0);
4450
+ return out2;
4436
4451
  }
4437
4452
  function extractCSharpTypes(tree, cache) {
4438
4453
  const types = [];
@@ -4450,9 +4465,8 @@ function extractCSharpTypes(tree, cache) {
4450
4465
  const methods = [];
4451
4466
  const fields = extractCSharpFields(body2);
4452
4467
  if (body2) {
4453
- for (let i2 = 0; i2 < body2.childCount; i2++) {
4454
- const m = body2.child(i2);
4455
- if (!m || m.type !== "method_declaration") continue;
4468
+ for (const m of csharpMemberNodes(body2)) {
4469
+ if (m.type !== "method_declaration") continue;
4456
4470
  const mName = m.childForFieldName("name");
4457
4471
  const paramList = m.childForFieldName("parameters");
4458
4472
  const parameters = [];
@@ -4503,9 +4517,7 @@ function extractCSharpTypes(tree, cache) {
4503
4517
  function extractCSharpFields(body2) {
4504
4518
  const fields = [];
4505
4519
  if (!body2) return fields;
4506
- for (let i2 = 0; i2 < body2.childCount; i2++) {
4507
- const c = body2.child(i2);
4508
- if (!c) continue;
4520
+ for (const c of csharpMemberNodes(body2)) {
4509
4521
  if (c.type === "field_declaration") {
4510
4522
  let varDecl = null;
4511
4523
  for (let k = 0; k < c.childCount; k++) {
@@ -4561,7 +4573,7 @@ function extractCSharpModifiers(node) {
4561
4573
  }
4562
4574
  return mods;
4563
4575
  }
4564
- function extractJavaTypes(tree, cache) {
4576
+ function extractJavaTypes(tree, cache, options) {
4565
4577
  const types = [];
4566
4578
  const classes = getNodesFromCache(tree.rootNode, "class_declaration", cache);
4567
4579
  for (const cls of classes) {
@@ -4575,8 +4587,76 @@ function extractJavaTypes(tree, cache) {
4575
4587
  for (const enumDecl of enums) {
4576
4588
  types.push(extractEnumInfo(enumDecl));
4577
4589
  }
4590
+ if (!options?.navigationTypes) return types;
4591
+ for (const rec of getNodesFromCache(tree.rootNode, "record_declaration", cache)) {
4592
+ const info2 = extractClassInfo(rec);
4593
+ info2.is_record = true;
4594
+ for (const comp of recordComponents(rec)) {
4595
+ if (!info2.fields.some((f) => f.name === comp.name)) {
4596
+ info2.fields.push({
4597
+ name: comp.name,
4598
+ type: comp.type,
4599
+ modifiers: ["private", "final"],
4600
+ annotations: []
4601
+ });
4602
+ }
4603
+ if (!info2.methods.some((m) => m.name === comp.name)) {
4604
+ info2.methods.push({
4605
+ name: comp.name,
4606
+ return_type: comp.type,
4607
+ parameters: [],
4608
+ annotations: [],
4609
+ modifiers: ["public"],
4610
+ start_line: rec.startPosition.row + 1,
4611
+ end_line: rec.startPosition.row + 1
4612
+ });
4613
+ }
4614
+ }
4615
+ types.push(info2);
4616
+ }
4617
+ const declNodes = [
4618
+ ...classes,
4619
+ ...interfaces,
4620
+ ...enums,
4621
+ ...getNodesFromCache(tree.rootNode, "record_declaration", cache)
4622
+ ];
4623
+ for (const t of types) {
4624
+ const node = declNodes.find(
4625
+ (n) => n.startPosition.row + 1 === t.start_line && getIdentifier(n, "name") === t.name
4626
+ );
4627
+ const path = node ? enclosingTypePath(node) : [];
4628
+ if (path.length > 0) t.enclosing_type = path.join(".");
4629
+ }
4578
4630
  return types;
4579
4631
  }
4632
+ function recordComponents(node) {
4633
+ const out2 = [];
4634
+ const params = node.childForFieldName("parameters");
4635
+ if (!params) return out2;
4636
+ for (let i2 = 0; i2 < params.childCount; i2++) {
4637
+ const c = params.child(i2);
4638
+ if (!c || c.type !== "formal_parameter") continue;
4639
+ const name2 = c.childForFieldName("name");
4640
+ const type = c.childForFieldName("type");
4641
+ if (name2) out2.push({ name: getNodeText(name2), type: type ? getNodeText(type) : null });
4642
+ }
4643
+ return out2;
4644
+ }
4645
+ function enclosingTypePath(node) {
4646
+ const DECLS = /* @__PURE__ */ new Set([
4647
+ "class_declaration",
4648
+ "interface_declaration",
4649
+ "enum_declaration",
4650
+ "record_declaration"
4651
+ ]);
4652
+ const path = [];
4653
+ for (let p = node.parent; p; p = p.parent) {
4654
+ if (!DECLS.has(p.type)) continue;
4655
+ const n = getIdentifier(p, "name");
4656
+ if (n) path.unshift(n);
4657
+ }
4658
+ return path;
4659
+ }
4580
4660
  function extractJavaScriptTypes(tree, cache) {
4581
4661
  const types = [];
4582
4662
  const classes = getNodesFromCache(tree.rootNode, "class_declaration", cache);
@@ -6039,6 +6119,13 @@ function extractCalls(tree, cache, language) {
6039
6119
  function buildCSharpReceiverTypeMap(tree, cache) {
6040
6120
  const map = /* @__PURE__ */ new Map();
6041
6121
  const simple = (t) => t.replace(/<[^>]*>/g, "").split(".").pop()?.trim() ?? t;
6122
+ for (const prm of getNodesFromCache(tree.rootNode, "parameter", cache)) {
6123
+ const nameNode = prm.childForFieldName("name");
6124
+ const typeNode = prm.childForFieldName("type");
6125
+ if (nameNode?.type !== "identifier" || !typeNode) continue;
6126
+ const t = getNodeText(typeNode);
6127
+ if (t && t !== "var") map.set(getNodeText(nameNode), simple(t));
6128
+ }
6042
6129
  for (const vd of getNodesFromCache(tree.rootNode, "variable_declaration", cache)) {
6043
6130
  const typeNode = vd.childForFieldName("type");
6044
6131
  const declaredType = typeNode ? getNodeText(typeNode) : null;
@@ -6068,6 +6155,7 @@ function findFirstDescendant(node, type) {
6068
6155
  }
6069
6156
  return null;
6070
6157
  }
6158
+ var CSHARP_RESPONSE_TYPES = /* @__PURE__ */ new Set(["HttpResponse", "HttpResponseBase"]);
6071
6159
  var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
6072
6160
  "ExecuteScalar",
6073
6161
  "ExecuteReader",
@@ -6220,7 +6308,8 @@ function extractCSharpCalls(tree, cache) {
6220
6308
  if (recvType !== "DirectorySearcher") continue;
6221
6309
  } else if (propName === "StatusDescription") {
6222
6310
  const recv = exprNode ? getNodeText(exprNode) : "";
6223
- if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv)) continue;
6311
+ const recvType = recv ? typeMap.get(recv) : void 0;
6312
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv) && !(recvType && CSHARP_RESPONSE_TYPES.has(recvType))) continue;
6224
6313
  } else if (propName === "BaseAddress") {
6225
6314
  const recv = exprNode ? getNodeText(exprNode) : null;
6226
6315
  const recvType = recv ? typeMap.get(recv) : void 0;
@@ -11720,6 +11809,23 @@ var DEFAULT_SOURCES = [
11720
11809
  { method: "next", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11721
11810
  { method: "nextInt", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11722
11811
  // Database result sources
11812
+ // C# I/O, network and database reads (cognium-dev#502). Mirrors the Java
11813
+ // rows above one-for-one, including the severity split: console/file I/O is
11814
+ // `io_input` at high, a database row read is `db_input` at medium. There is
11815
+ // no separate confidence tier — `type` + `severity` IS the tier, exactly as
11816
+ // for `BufferedReader.readLine` vs `ResultSet.getString`.
11817
+ { method: "ReadLine", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11818
+ { method: "ReadLineAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11819
+ { method: "ReadToEnd", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11820
+ { method: "ReadToEndAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11821
+ // Socket and HTTP-client reads. `GetStream` / `AcceptTcpClient` /
11822
+ // `OpenRead` return the channel; the read off it is covered by the
11823
+ // StreamReader rows above, and seeding the channel too lets a flow form when
11824
+ // the read is inlined.
11825
+ { method: "GetStream", class: "TcpClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11826
+ { method: "AcceptTcpClient", class: "TcpListener", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11827
+ { method: "OpenRead", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11828
+ { method: "DownloadString", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11723
11829
  { method: "getString", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11724
11830
  { method: "getObject", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11725
11831
  { method: "getInt", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
@@ -13960,6 +14066,29 @@ var DEFAULT_SINKS = [
13960
14066
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13961
14067
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13962
14068
  { method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14069
+ // cognium-dev#541 — the crates the removed classless rows were silently
14070
+ // covering. #484 replaced a classless `from_str` / `from_slice` with
14071
+ // class-scoped rows, but only for the four formats that already had entries;
14072
+ // every other serde data format lost its sink, and `rmp_serde::from_slice`
14073
+ // on bytes read straight off a `TcpStream` stopped being reported. Each row
14074
+ // below is a serde *data format* entry point — a function whose whole job is
14075
+ // to build a typed value out of untrusted bytes or text.
14076
+ { method: "from_slice", class: "rmp_serde", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14077
+ { method: "from_read", class: "rmp_serde", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14078
+ { method: "from_read_ref", class: "rmp_serde", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14079
+ { method: "from_slice", class: "serde_yaml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14080
+ { method: "from_slice", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14081
+ { method: "from_reader", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14082
+ { method: "from_bytes", class: "postcard", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14083
+ { method: "from_slice", class: "serde_cbor", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14084
+ { method: "from_reader", class: "serde_cbor", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14085
+ { method: "from_reader", class: "ciborium", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14086
+ { method: "from_str", class: "serde_xml_rs", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14087
+ { method: "from_reader", class: "serde_xml_rs", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14088
+ { method: "from_str", class: "quick_xml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14089
+ { method: "from_reader", class: "quick_xml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
14090
+ { method: "from_slice", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
14091
+ { method: "deserialize_from", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13963
14092
  // #294/#484: no classless `from_str` / `from_slice` deserialization sink.
13964
14093
  // Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
13965
14094
  // `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
@@ -14284,8 +14413,12 @@ var DEFAULT_SINKS = [
14284
14413
  { method: "OpenRead", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
14285
14414
  { method: "OpenWrite", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
14286
14415
  { method: "FileStream", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
14287
- { method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
14288
- { method: "StreamWriter", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
14416
+ // #502: the `(Stream)` overload takes no path — `new StreamReader(tcp.GetStream())`
14417
+ // is not CWE-22. The `(string path)` overload still matches.
14418
+ { method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
14419
+ // #502: the `(Stream)` overload takes no path — `new StreamWriter(tcp.GetStream())`
14420
+ // is not CWE-22. The `(string path)` overload still matches.
14421
+ { method: "StreamWriter", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
14289
14422
  // Remaining System.IO.File path APIs — class-scoped so they don't collide
14290
14423
  // with unrelated methods. Copy/Move take a source AND destination path (both
14291
14424
  // attacker-controllable → read-anywhere / write-anywhere), hence [0, 1].
@@ -14329,6 +14462,23 @@ var DEFAULT_SINKS = [
14329
14462
  // `new RestClient(userUrl)` points every later request at attacker-controlled
14330
14463
  // infrastructure. Ctor arg 0, taint-gated.
14331
14464
  { method: "RestClient", class: "constructor", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
14465
+ // C# code injection — CodeDOM runtime compilation (CWE-94), cognium-dev#503
14466
+ // part 2. `provider.CompileAssemblyFromSource(parameters, source)` compiles
14467
+ // and can then execute attacker-supplied C#, so the source argument is the
14468
+ // injection point. The signature is
14469
+ // `CompileAssemblyFromSource(CompilerParameters, params string[] sources)`,
14470
+ // so the sources begin at position 1 — position 0 is the options object.
14471
+ //
14472
+ // These rows are only useful together with the #530 StringBuilder
14473
+ // propagation fix: the Juliet CWE-94 corpus accumulates the compiled source
14474
+ // through `sb.Append(...)` and reads it back with `ToString()`, so before
14475
+ // that fix the sink registered but no taint ever reached it.
14476
+ { method: "CompileAssemblyFromSource", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
14477
+ { method: "CompileAssemblyFromSourceBatch", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
14478
+ // Roslyn's equivalent pair. `ParseText` turns text into a syntax tree and
14479
+ // `Create` compiles it; either is the point where attacker text becomes code.
14480
+ { method: "ParseText", class: "CSharpSyntaxTree", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
14481
+ { method: "ParseText", class: "SyntaxFactory", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
14332
14482
  // C# code injection — dynamic script/assembly loading (CWE-94).
14333
14483
  { method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
14334
14484
  { method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -16229,6 +16379,20 @@ function escapeRe(s) {
16229
16379
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16230
16380
  }
16231
16381
  var TYPE_TOKEN_RE = /^new\s+(?:TypeReference|TypeToken)\s*<[\s\S]*>\s*\(\s*\)\s*\{\s*\}$/;
16382
+ function argIsStreamExpression(call, position) {
16383
+ const arg = call.arguments.find((a) => a.position === position);
16384
+ if (!arg) return false;
16385
+ const expr = (arg.expression ?? "").trim();
16386
+ if (!expr) return false;
16387
+ return (
16388
+ // `new MemoryStream(...)`, `new FileStream(...)`, `new GZipStream(...)`
16389
+ /^new\s+[A-Za-z_][\w.]*Stream\s*\(/.test(expr) || // `tcp.GetStream()`, `resp.GetResponseStream()`, `file.OpenReadStream()`
16390
+ /\.\s*Get(?:Response)?Stream\s*\(/.test(expr) || /\.\s*OpenReadStream\s*\(/.test(expr) || // `wc.OpenRead(url)` returns a Stream (WebClient), as does `File.OpenRead`
16391
+ /\.\s*OpenRead\s*\(/.test(expr) || // `Request.Body`, `x.BaseStream`
16392
+ /\.\s*(?:Body|BaseStream)\s*$/.test(expr) || // a local whose name is stream-ish and is not a path string
16393
+ /^(?:[A-Za-z_]\w*\.)*[A-Za-z_]*[sS]tream\d*$/.test(expr)
16394
+ );
16395
+ }
16232
16396
  function argIsStringLiteral(call, position) {
16233
16397
  const arg = call.arguments.find((a) => a.position === position);
16234
16398
  if (!arg) return false;
@@ -16466,6 +16630,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
16466
16630
  if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
16467
16631
  continue;
16468
16632
  }
16633
+ if (pattern.safe_if_stream_arg_at !== void 0 && argIsStreamExpression(call, pattern.safe_if_stream_arg_at)) {
16634
+ continue;
16635
+ }
16469
16636
  if (pattern.type === "command_injection") {
16470
16637
  if (call.is_constructor) {
16471
16638
  if (!CWE_78_RECEIVER_ALLOWLIST.has(call.method_name)) {
@@ -28647,23 +28814,35 @@ function findCSharpRequestSources(sourceCode, language) {
28647
28814
  const formFileDeclRe = /\bIFormFile\s+([A-Za-z_]\w*)/g;
28648
28815
  const httpRequestReceivers = /* @__PURE__ */ new Set(["Request"]);
28649
28816
  const httpRequestDeclRe = /\b(?:HttpRequest|HttpRequestBase)\??\s+([A-Za-z_]\w*)/g;
28817
+ const streamReaderVars = /* @__PURE__ */ new Set();
28818
+ const streamReaderDeclRe = /\b(?:StreamReader|TextReader|BinaryReader)\??\s+([A-Za-z_]\w*)/g;
28819
+ const dbReaderVars = /* @__PURE__ */ new Set();
28820
+ const dbReaderDeclRe = /\b(?:SqlDataReader|DbDataReader|IDataReader|OdbcDataReader|OleDbDataReader)\??\s+([A-Za-z_]\w*)/g;
28650
28821
  for (const line of lines) {
28651
28822
  const formRe = new RegExp(formFileDeclRe.source, "g");
28652
28823
  let d;
28653
28824
  while ((d = formRe.exec(line)) !== null) formFileParams.add(d[1]);
28654
28825
  const reqRe = new RegExp(httpRequestDeclRe.source, "g");
28655
28826
  while ((d = reqRe.exec(line)) !== null) httpRequestReceivers.add(d[1]);
28827
+ const srRe = new RegExp(streamReaderDeclRe.source, "g");
28828
+ while ((d = srRe.exec(line)) !== null) streamReaderVars.add(d[1]);
28829
+ const dbRe = new RegExp(dbReaderDeclRe.source, "g");
28830
+ while ((d = dbRe.exec(line)) !== null) dbReaderVars.add(d[1]);
28656
28831
  }
28832
+ const streamReadRe = streamReaderVars.size > 0 ? new RegExp(`\\b(?:${[...streamReaderVars].join("|")})\\s*\\.\\s*(?:ReadLine|ReadToEnd|ReadLineAsync|ReadToEndAsync|ReadBlock|Read)\\s*\\(`) : null;
28833
+ const dbReadRe = dbReaderVars.size > 0 ? new RegExp(`\\b(?:${[...dbReaderVars].join("|")})\\s*\\.\\s*(?:GetString|GetValue|GetInt32|GetInt64|GetDecimal|GetDateTime|GetFieldValue|\\[)`) : null;
28657
28834
  const formFileReadRe = formFileParams.size > 0 ? new RegExp(`\\b(?:${[...formFileParams].join("|")})\\s*\\.\\s*(?:FileName|ContentType)\\b`) : null;
28658
28835
  const requestReadRe = new RegExp(
28659
28836
  `\\b(?:${[...httpRequestReceivers].join("|")})\\s*\\.\\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\\b`
28660
28837
  );
28661
- const classify = (text) => requestReadRe.test(text) ? "http_param" : formFileReadRe?.test(text) ? "http_param" : consoleReadRe.test(text) ? "io_input" : envReadRe.test(text) ? "env_input" : null;
28838
+ const classify = (text) => requestReadRe.test(text) ? "http_param" : formFileReadRe?.test(text) ? "http_param" : consoleReadRe.test(text) ? "io_input" : envReadRe.test(text) ? "env_input" : streamReadRe?.test(text) ? "io_input" : dbReadRe?.test(text) ? "db_input" : null;
28662
28839
  const inlineReads = [
28663
28840
  [requestReadRe, "http_param"],
28664
28841
  [formFileReadRe, "http_param"],
28665
28842
  [consoleReadRe, "io_input"],
28666
- [envReadRe, "env_input"]
28843
+ [envReadRe, "env_input"],
28844
+ [streamReadRe, "io_input"],
28845
+ [dbReadRe, "db_input"]
28667
28846
  ];
28668
28847
  const inlineRead = (text) => {
28669
28848
  const argsStart = text.indexOf("(");
@@ -28684,7 +28863,8 @@ function findCSharpRequestSources(sourceCode, language) {
28684
28863
  const embeddedAssignRe = /(?:^|[{;])\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*([^;{}]+)/;
28685
28864
  for (let i2 = 0; i2 < lines.length; i2++) {
28686
28865
  const m = assignRe.exec(lines[i2]);
28687
- if (!m) {
28866
+ const rhsCrossesBlock = m !== null && m[2].includes("{");
28867
+ if (!m || rhsCrossesBlock) {
28688
28868
  const stmt = lines[i2];
28689
28869
  if (stmt.includes("{") && !/^\s*(?:\/\/|\/\*|\*)/.test(stmt)) {
28690
28870
  const emb = new RegExp(embeddedAssignRe.source, "g");
@@ -28700,7 +28880,8 @@ function findCSharpRequestSources(sourceCode, language) {
28700
28880
  sources.push({
28701
28881
  type: type3,
28702
28882
  location: `${varName2} = ${rhs2.trim().substring(0, 50)}${rhs2.length > 50 ? "..." : ""}`,
28703
- severity: "high",
28883
+ // #502 — Java parity: a database row read is medium, I/O is high.
28884
+ severity: type3 === "db_input" ? "medium" : "high",
28704
28885
  line: lineNumber3,
28705
28886
  confidence: 1,
28706
28887
  variable: varName2
@@ -28719,7 +28900,7 @@ function findCSharpRequestSources(sourceCode, language) {
28719
28900
  sources.push({
28720
28901
  type: type2,
28721
28902
  location: `${read} in ${code.substring(0, 50)}${code.length > 50 ? "..." : ""}`,
28722
- severity: "high",
28903
+ severity: type2 === "db_input" ? "medium" : "high",
28723
28904
  line: lineNumber2,
28724
28905
  confidence: 1,
28725
28906
  variable: "<inline>",
@@ -28735,7 +28916,8 @@ function findCSharpRequestSources(sourceCode, language) {
28735
28916
  sources.push({
28736
28917
  type,
28737
28918
  location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
28738
- severity: "high",
28919
+ // #502 — Java parity: a database row read is medium, I/O is high.
28920
+ severity: type === "db_input" ? "medium" : "high",
28739
28921
  line: lineNumber,
28740
28922
  confidence: 1,
28741
28923
  variable: varName
@@ -28902,7 +29084,8 @@ function findPythonAssignmentSources(sourceCode, language) {
28902
29084
  sources.push({
28903
29085
  type,
28904
29086
  location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
28905
- severity: "high",
29087
+ // #502 — Java parity: a database row read is medium, I/O is high.
29088
+ severity: type === "db_input" ? "medium" : "high",
28906
29089
  line: lineNumber,
28907
29090
  confidence: 0.95,
28908
29091
  variable: varName
@@ -38432,13 +38615,22 @@ function maskStringLiteralsForTokenScan(expr) {
38432
38615
  }
38433
38616
  return out2;
38434
38617
  }
38618
+ function argsReferenceTaintedVar(expr, sources) {
38619
+ const codePart = expr.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
38620
+ return sources.some(
38621
+ (s) => new RegExp(
38622
+ `(?<![\\p{L}\\p{N}_])${s.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\p{L}\\p{N}_])`,
38623
+ "u"
38624
+ ).test(codePart)
38625
+ );
38626
+ }
38435
38627
  function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
38436
38628
  const flows = [];
38437
- const goFnRanges = language === "go" && types ? types.flatMap((t) => t.methods.map((m) => [m.start_line, m.end_line])) : [];
38438
- const goFnStart = (line) => {
38629
+ const methodRanges = (langs) => language && langs.includes(language) && types ? types.flatMap((t) => t.methods.map((m) => [m.start_line, m.end_line])) : [];
38630
+ const enclosingStart = (ranges, line) => {
38439
38631
  let best = -1;
38440
38632
  let bestSpan = -1;
38441
- for (const [start2, end] of goFnRanges) {
38633
+ for (const [start2, end] of ranges) {
38442
38634
  if (line >= start2 && line <= end && end - start2 > bestSpan) {
38443
38635
  best = start2;
38444
38636
  bestSpan = end - start2;
@@ -38446,6 +38638,10 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38446
38638
  }
38447
38639
  return best;
38448
38640
  };
38641
+ const goFnRanges = methodRanges(["go"]);
38642
+ const goFnStart = (line) => enclosingStart(goFnRanges, line);
38643
+ const csFnRanges = methodRanges(["csharp"]);
38644
+ const csFnStart = (line) => enclosingStart(csFnRanges, line);
38449
38645
  const sourcesWithVar = sources.filter(
38450
38646
  (s) => typeof s.variable === "string" && s.variable.length > 0
38451
38647
  );
@@ -38747,16 +38943,47 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38747
38943
  if (!m) continue;
38748
38944
  const [, obj, rhs] = m;
38749
38945
  if (known.has(obj)) continue;
38750
- const codePart = rhs.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ");
38751
- const rhsTainted = sourcesWithVar.some(
38752
- (s) => new RegExp(`(?<![\\p{L}\\p{N}_])${s.variable.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}(?![\\p{L}\\p{N}_])`, "u").test(codePart)
38753
- );
38754
- if (!rhsTainted) continue;
38946
+ if (!argsReferenceTaintedVar(rhs, sourcesWithVar)) continue;
38755
38947
  sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
38756
38948
  added = true;
38757
38949
  }
38758
38950
  }
38759
38951
  }
38952
+ const builderSeeds = /* @__PURE__ */ new Set();
38953
+ if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0) {
38954
+ const lines = code.split("\n");
38955
+ const builders = /* @__PURE__ */ new Set();
38956
+ for (const line of lines) {
38957
+ for (const m of line.matchAll(/\bStringBuilder\s+([\p{L}_][\p{L}\p{N}_]*)/gu)) builders.add(m[1]);
38958
+ for (const m of line.matchAll(/\b([\p{L}_][\p{L}\p{N}_]*)\s*=\s*new\s+StringBuilder\b/gu)) builders.add(m[1]);
38959
+ }
38960
+ if (builders.size > 0) {
38961
+ const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
38962
+ const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
38963
+ let added = true;
38964
+ let guard = 0;
38965
+ while (added && guard < lines.length + 2) {
38966
+ added = false;
38967
+ guard++;
38968
+ const known = new Set(sourcesWithVar.map((s) => s.variable));
38969
+ for (let i2 = 0; i2 < lines.length; i2++) {
38970
+ const m = appendRe.exec(lines[i2]);
38971
+ if (!m) continue;
38972
+ const [, obj, args2] = m;
38973
+ if (!builders.has(obj) || known.has(obj)) continue;
38974
+ const appendFn = csFnStart(i2 + 1);
38975
+ const reachable = sourcesWithVar.filter(
38976
+ (sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn
38977
+ );
38978
+ if (reachable.length > 0 && argsReferenceTaintedVar(args2, reachable)) {
38979
+ sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
38980
+ builderSeeds.add(`${obj}@${i2 + 1}`);
38981
+ added = true;
38982
+ }
38983
+ }
38984
+ }
38985
+ }
38986
+ }
38760
38987
  const SIMPLE_IDENT = /^[\p{L}\p{N}_]+$/u;
38761
38988
  const reCache = /* @__PURE__ */ new Map();
38762
38989
  for (const s of sourcesWithVar) {
@@ -38819,6 +39046,11 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38819
39046
  const sinkFn = goFnStart(sink.line);
38820
39047
  if (srcFn !== -1 && sinkFn !== -1 && srcFn !== sinkFn) continue;
38821
39048
  }
39049
+ if (csFnRanges.length > 0 && builderSeeds.has(`${source.variable}@${source.line}`)) {
39050
+ const srcFn = csFnStart(source.line);
39051
+ const sinkFn = csFnStart(sink.line);
39052
+ if (srcFn !== sinkFn) continue;
39053
+ }
38822
39054
  if (complex) {
38823
39055
  const re = reCache.get(source.variable);
38824
39056
  if (!re || !re.test(expr)) continue;
@@ -48179,7 +48411,7 @@ async function analyze(code, filePath, language, options = {}) {
48179
48411
  };
48180
48412
  }
48181
48413
  const tTypes = phaseStart();
48182
- const types = extractTypes(tree, nodeCache, language);
48414
+ const types = extractTypes(tree, nodeCache, language, { navigationTypes: options.navigationTypes });
48183
48415
  phaseEnd("extractTypes", tTypes);
48184
48416
  const tCalls = phaseStart();
48185
48417
  const calls = extractCalls(tree, nodeCache, language);