circle-ir 4.9.28 → 4.9.29

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.
@@ -4434,6 +4434,21 @@ function extractTypes(tree, cache, language) {
4434
4434
  }
4435
4435
  return extractJavaTypes(tree, cache);
4436
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;
4451
+ }
4437
4452
  function extractCSharpTypes(tree, cache) {
4438
4453
  const types = [];
4439
4454
  const KINDS = {
@@ -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++) {
@@ -6039,6 +6051,13 @@ function extractCalls(tree, cache, language) {
6039
6051
  function buildCSharpReceiverTypeMap(tree, cache) {
6040
6052
  const map = /* @__PURE__ */ new Map();
6041
6053
  const simple = (t) => t.replace(/<[^>]*>/g, "").split(".").pop()?.trim() ?? t;
6054
+ for (const prm of getNodesFromCache(tree.rootNode, "parameter", cache)) {
6055
+ const nameNode = prm.childForFieldName("name");
6056
+ const typeNode = prm.childForFieldName("type");
6057
+ if (nameNode?.type !== "identifier" || !typeNode) continue;
6058
+ const t = getNodeText(typeNode);
6059
+ if (t && t !== "var") map.set(getNodeText(nameNode), simple(t));
6060
+ }
6042
6061
  for (const vd of getNodesFromCache(tree.rootNode, "variable_declaration", cache)) {
6043
6062
  const typeNode = vd.childForFieldName("type");
6044
6063
  const declaredType = typeNode ? getNodeText(typeNode) : null;
@@ -6068,6 +6087,7 @@ function findFirstDescendant(node, type) {
6068
6087
  }
6069
6088
  return null;
6070
6089
  }
6090
+ var CSHARP_RESPONSE_TYPES = /* @__PURE__ */ new Set(["HttpResponse", "HttpResponseBase"]);
6071
6091
  var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
6072
6092
  "ExecuteScalar",
6073
6093
  "ExecuteReader",
@@ -6220,7 +6240,8 @@ function extractCSharpCalls(tree, cache) {
6220
6240
  if (recvType !== "DirectorySearcher") continue;
6221
6241
  } else if (propName === "StatusDescription") {
6222
6242
  const recv = exprNode ? getNodeText(exprNode) : "";
6223
- if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv)) continue;
6243
+ const recvType = recv ? typeMap.get(recv) : void 0;
6244
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv) && !(recvType && CSHARP_RESPONSE_TYPES.has(recvType))) continue;
6224
6245
  } else if (propName === "BaseAddress") {
6225
6246
  const recv = exprNode ? getNodeText(exprNode) : null;
6226
6247
  const recvType = recv ? typeMap.get(recv) : void 0;
@@ -11720,6 +11741,23 @@ var DEFAULT_SOURCES = [
11720
11741
  { method: "next", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11721
11742
  { method: "nextInt", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11722
11743
  // Database result sources
11744
+ // C# I/O, network and database reads (cognium-dev#502). Mirrors the Java
11745
+ // rows above one-for-one, including the severity split: console/file I/O is
11746
+ // `io_input` at high, a database row read is `db_input` at medium. There is
11747
+ // no separate confidence tier — `type` + `severity` IS the tier, exactly as
11748
+ // for `BufferedReader.readLine` vs `ResultSet.getString`.
11749
+ { method: "ReadLine", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11750
+ { method: "ReadLineAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11751
+ { method: "ReadToEnd", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11752
+ { method: "ReadToEndAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11753
+ // Socket and HTTP-client reads. `GetStream` / `AcceptTcpClient` /
11754
+ // `OpenRead` return the channel; the read off it is covered by the
11755
+ // StreamReader rows above, and seeding the channel too lets a flow form when
11756
+ // the read is inlined.
11757
+ { method: "GetStream", class: "TcpClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11758
+ { method: "AcceptTcpClient", class: "TcpListener", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11759
+ { method: "OpenRead", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11760
+ { method: "DownloadString", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11723
11761
  { method: "getString", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11724
11762
  { method: "getObject", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11725
11763
  { method: "getInt", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
@@ -14284,8 +14322,12 @@ var DEFAULT_SINKS = [
14284
14322
  { method: "OpenRead", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
14285
14323
  { method: "OpenWrite", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
14286
14324
  { 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"] },
14325
+ // #502: the `(Stream)` overload takes no path — `new StreamReader(tcp.GetStream())`
14326
+ // is not CWE-22. The `(string path)` overload still matches.
14327
+ { method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
14328
+ // #502: the `(Stream)` overload takes no path — `new StreamWriter(tcp.GetStream())`
14329
+ // is not CWE-22. The `(string path)` overload still matches.
14330
+ { method: "StreamWriter", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
14289
14331
  // Remaining System.IO.File path APIs — class-scoped so they don't collide
14290
14332
  // with unrelated methods. Copy/Move take a source AND destination path (both
14291
14333
  // attacker-controllable → read-anywhere / write-anywhere), hence [0, 1].
@@ -14329,6 +14371,23 @@ var DEFAULT_SINKS = [
14329
14371
  // `new RestClient(userUrl)` points every later request at attacker-controlled
14330
14372
  // infrastructure. Ctor arg 0, taint-gated.
14331
14373
  { method: "RestClient", class: "constructor", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
14374
+ // C# code injection — CodeDOM runtime compilation (CWE-94), cognium-dev#503
14375
+ // part 2. `provider.CompileAssemblyFromSource(parameters, source)` compiles
14376
+ // and can then execute attacker-supplied C#, so the source argument is the
14377
+ // injection point. The signature is
14378
+ // `CompileAssemblyFromSource(CompilerParameters, params string[] sources)`,
14379
+ // so the sources begin at position 1 — position 0 is the options object.
14380
+ //
14381
+ // These rows are only useful together with the #530 StringBuilder
14382
+ // propagation fix: the Juliet CWE-94 corpus accumulates the compiled source
14383
+ // through `sb.Append(...)` and reads it back with `ToString()`, so before
14384
+ // that fix the sink registered but no taint ever reached it.
14385
+ { method: "CompileAssemblyFromSource", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
14386
+ { method: "CompileAssemblyFromSourceBatch", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
14387
+ // Roslyn's equivalent pair. `ParseText` turns text into a syntax tree and
14388
+ // `Create` compiles it; either is the point where attacker text becomes code.
14389
+ { method: "ParseText", class: "CSharpSyntaxTree", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
14390
+ { method: "ParseText", class: "SyntaxFactory", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
14332
14391
  // C# code injection — dynamic script/assembly loading (CWE-94).
14333
14392
  { method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
14334
14393
  { method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -16229,6 +16288,20 @@ function escapeRe(s) {
16229
16288
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16230
16289
  }
16231
16290
  var TYPE_TOKEN_RE = /^new\s+(?:TypeReference|TypeToken)\s*<[\s\S]*>\s*\(\s*\)\s*\{\s*\}$/;
16291
+ function argIsStreamExpression(call, position) {
16292
+ const arg = call.arguments.find((a) => a.position === position);
16293
+ if (!arg) return false;
16294
+ const expr = (arg.expression ?? "").trim();
16295
+ if (!expr) return false;
16296
+ return (
16297
+ // `new MemoryStream(...)`, `new FileStream(...)`, `new GZipStream(...)`
16298
+ /^new\s+[A-Za-z_][\w.]*Stream\s*\(/.test(expr) || // `tcp.GetStream()`, `resp.GetResponseStream()`, `file.OpenReadStream()`
16299
+ /\.\s*Get(?:Response)?Stream\s*\(/.test(expr) || /\.\s*OpenReadStream\s*\(/.test(expr) || // `wc.OpenRead(url)` returns a Stream (WebClient), as does `File.OpenRead`
16300
+ /\.\s*OpenRead\s*\(/.test(expr) || // `Request.Body`, `x.BaseStream`
16301
+ /\.\s*(?:Body|BaseStream)\s*$/.test(expr) || // a local whose name is stream-ish and is not a path string
16302
+ /^(?:[A-Za-z_]\w*\.)*[A-Za-z_]*[sS]tream\d*$/.test(expr)
16303
+ );
16304
+ }
16232
16305
  function argIsStringLiteral(call, position) {
16233
16306
  const arg = call.arguments.find((a) => a.position === position);
16234
16307
  if (!arg) return false;
@@ -16466,6 +16539,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
16466
16539
  if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
16467
16540
  continue;
16468
16541
  }
16542
+ if (pattern.safe_if_stream_arg_at !== void 0 && argIsStreamExpression(call, pattern.safe_if_stream_arg_at)) {
16543
+ continue;
16544
+ }
16469
16545
  if (pattern.type === "command_injection") {
16470
16546
  if (call.is_constructor) {
16471
16547
  if (!CWE_78_RECEIVER_ALLOWLIST.has(call.method_name)) {
@@ -28647,23 +28723,35 @@ function findCSharpRequestSources(sourceCode, language) {
28647
28723
  const formFileDeclRe = /\bIFormFile\s+([A-Za-z_]\w*)/g;
28648
28724
  const httpRequestReceivers = /* @__PURE__ */ new Set(["Request"]);
28649
28725
  const httpRequestDeclRe = /\b(?:HttpRequest|HttpRequestBase)\??\s+([A-Za-z_]\w*)/g;
28726
+ const streamReaderVars = /* @__PURE__ */ new Set();
28727
+ const streamReaderDeclRe = /\b(?:StreamReader|TextReader|BinaryReader)\??\s+([A-Za-z_]\w*)/g;
28728
+ const dbReaderVars = /* @__PURE__ */ new Set();
28729
+ const dbReaderDeclRe = /\b(?:SqlDataReader|DbDataReader|IDataReader|OdbcDataReader|OleDbDataReader)\??\s+([A-Za-z_]\w*)/g;
28650
28730
  for (const line of lines) {
28651
28731
  const formRe = new RegExp(formFileDeclRe.source, "g");
28652
28732
  let d;
28653
28733
  while ((d = formRe.exec(line)) !== null) formFileParams.add(d[1]);
28654
28734
  const reqRe = new RegExp(httpRequestDeclRe.source, "g");
28655
28735
  while ((d = reqRe.exec(line)) !== null) httpRequestReceivers.add(d[1]);
28736
+ const srRe = new RegExp(streamReaderDeclRe.source, "g");
28737
+ while ((d = srRe.exec(line)) !== null) streamReaderVars.add(d[1]);
28738
+ const dbRe = new RegExp(dbReaderDeclRe.source, "g");
28739
+ while ((d = dbRe.exec(line)) !== null) dbReaderVars.add(d[1]);
28656
28740
  }
28741
+ const streamReadRe = streamReaderVars.size > 0 ? new RegExp(`\\b(?:${[...streamReaderVars].join("|")})\\s*\\.\\s*(?:ReadLine|ReadToEnd|ReadLineAsync|ReadToEndAsync|ReadBlock|Read)\\s*\\(`) : null;
28742
+ const dbReadRe = dbReaderVars.size > 0 ? new RegExp(`\\b(?:${[...dbReaderVars].join("|")})\\s*\\.\\s*(?:GetString|GetValue|GetInt32|GetInt64|GetDecimal|GetDateTime|GetFieldValue|\\[)`) : null;
28657
28743
  const formFileReadRe = formFileParams.size > 0 ? new RegExp(`\\b(?:${[...formFileParams].join("|")})\\s*\\.\\s*(?:FileName|ContentType)\\b`) : null;
28658
28744
  const requestReadRe = new RegExp(
28659
28745
  `\\b(?:${[...httpRequestReceivers].join("|")})\\s*\\.\\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\\b`
28660
28746
  );
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;
28747
+ 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
28748
  const inlineReads = [
28663
28749
  [requestReadRe, "http_param"],
28664
28750
  [formFileReadRe, "http_param"],
28665
28751
  [consoleReadRe, "io_input"],
28666
- [envReadRe, "env_input"]
28752
+ [envReadRe, "env_input"],
28753
+ [streamReadRe, "io_input"],
28754
+ [dbReadRe, "db_input"]
28667
28755
  ];
28668
28756
  const inlineRead = (text) => {
28669
28757
  const argsStart = text.indexOf("(");
@@ -28684,7 +28772,8 @@ function findCSharpRequestSources(sourceCode, language) {
28684
28772
  const embeddedAssignRe = /(?:^|[{;])\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*([^;{}]+)/;
28685
28773
  for (let i2 = 0; i2 < lines.length; i2++) {
28686
28774
  const m = assignRe.exec(lines[i2]);
28687
- if (!m) {
28775
+ const rhsCrossesBlock = m !== null && m[2].includes("{");
28776
+ if (!m || rhsCrossesBlock) {
28688
28777
  const stmt = lines[i2];
28689
28778
  if (stmt.includes("{") && !/^\s*(?:\/\/|\/\*|\*)/.test(stmt)) {
28690
28779
  const emb = new RegExp(embeddedAssignRe.source, "g");
@@ -28700,7 +28789,8 @@ function findCSharpRequestSources(sourceCode, language) {
28700
28789
  sources.push({
28701
28790
  type: type3,
28702
28791
  location: `${varName2} = ${rhs2.trim().substring(0, 50)}${rhs2.length > 50 ? "..." : ""}`,
28703
- severity: "high",
28792
+ // #502 — Java parity: a database row read is medium, I/O is high.
28793
+ severity: type3 === "db_input" ? "medium" : "high",
28704
28794
  line: lineNumber3,
28705
28795
  confidence: 1,
28706
28796
  variable: varName2
@@ -28719,7 +28809,7 @@ function findCSharpRequestSources(sourceCode, language) {
28719
28809
  sources.push({
28720
28810
  type: type2,
28721
28811
  location: `${read} in ${code.substring(0, 50)}${code.length > 50 ? "..." : ""}`,
28722
- severity: "high",
28812
+ severity: type2 === "db_input" ? "medium" : "high",
28723
28813
  line: lineNumber2,
28724
28814
  confidence: 1,
28725
28815
  variable: "<inline>",
@@ -28735,7 +28825,8 @@ function findCSharpRequestSources(sourceCode, language) {
28735
28825
  sources.push({
28736
28826
  type,
28737
28827
  location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
28738
- severity: "high",
28828
+ // #502 — Java parity: a database row read is medium, I/O is high.
28829
+ severity: type === "db_input" ? "medium" : "high",
28739
28830
  line: lineNumber,
28740
28831
  confidence: 1,
28741
28832
  variable: varName
@@ -28902,7 +28993,8 @@ function findPythonAssignmentSources(sourceCode, language) {
28902
28993
  sources.push({
28903
28994
  type,
28904
28995
  location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
28905
- severity: "high",
28996
+ // #502 — Java parity: a database row read is medium, I/O is high.
28997
+ severity: type === "db_input" ? "medium" : "high",
28906
28998
  line: lineNumber,
28907
28999
  confidence: 0.95,
28908
29000
  variable: varName
@@ -38432,13 +38524,22 @@ function maskStringLiteralsForTokenScan(expr) {
38432
38524
  }
38433
38525
  return out2;
38434
38526
  }
38527
+ function argsReferenceTaintedVar(expr, sources) {
38528
+ const codePart = expr.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
38529
+ return sources.some(
38530
+ (s) => new RegExp(
38531
+ `(?<![\\p{L}\\p{N}_])${s.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\p{L}\\p{N}_])`,
38532
+ "u"
38533
+ ).test(codePart)
38534
+ );
38535
+ }
38435
38536
  function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
38436
38537
  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) => {
38538
+ const methodRanges = (langs) => language && langs.includes(language) && types ? types.flatMap((t) => t.methods.map((m) => [m.start_line, m.end_line])) : [];
38539
+ const enclosingStart = (ranges, line) => {
38439
38540
  let best = -1;
38440
38541
  let bestSpan = -1;
38441
- for (const [start2, end] of goFnRanges) {
38542
+ for (const [start2, end] of ranges) {
38442
38543
  if (line >= start2 && line <= end && end - start2 > bestSpan) {
38443
38544
  best = start2;
38444
38545
  bestSpan = end - start2;
@@ -38446,6 +38547,10 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38446
38547
  }
38447
38548
  return best;
38448
38549
  };
38550
+ const goFnRanges = methodRanges(["go"]);
38551
+ const goFnStart = (line) => enclosingStart(goFnRanges, line);
38552
+ const csFnRanges = methodRanges(["csharp"]);
38553
+ const csFnStart = (line) => enclosingStart(csFnRanges, line);
38449
38554
  const sourcesWithVar = sources.filter(
38450
38555
  (s) => typeof s.variable === "string" && s.variable.length > 0
38451
38556
  );
@@ -38747,16 +38852,47 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38747
38852
  if (!m) continue;
38748
38853
  const [, obj, rhs] = m;
38749
38854
  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;
38855
+ if (!argsReferenceTaintedVar(rhs, sourcesWithVar)) continue;
38755
38856
  sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
38756
38857
  added = true;
38757
38858
  }
38758
38859
  }
38759
38860
  }
38861
+ const builderSeeds = /* @__PURE__ */ new Set();
38862
+ if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0) {
38863
+ const lines = code.split("\n");
38864
+ const builders = /* @__PURE__ */ new Set();
38865
+ for (const line of lines) {
38866
+ for (const m of line.matchAll(/\bStringBuilder\s+([\p{L}_][\p{L}\p{N}_]*)/gu)) builders.add(m[1]);
38867
+ for (const m of line.matchAll(/\b([\p{L}_][\p{L}\p{N}_]*)\s*=\s*new\s+StringBuilder\b/gu)) builders.add(m[1]);
38868
+ }
38869
+ if (builders.size > 0) {
38870
+ const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
38871
+ const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
38872
+ let added = true;
38873
+ let guard = 0;
38874
+ while (added && guard < lines.length + 2) {
38875
+ added = false;
38876
+ guard++;
38877
+ const known = new Set(sourcesWithVar.map((s) => s.variable));
38878
+ for (let i2 = 0; i2 < lines.length; i2++) {
38879
+ const m = appendRe.exec(lines[i2]);
38880
+ if (!m) continue;
38881
+ const [, obj, args2] = m;
38882
+ if (!builders.has(obj) || known.has(obj)) continue;
38883
+ const appendFn = csFnStart(i2 + 1);
38884
+ const reachable = sourcesWithVar.filter(
38885
+ (sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn
38886
+ );
38887
+ if (reachable.length > 0 && argsReferenceTaintedVar(args2, reachable)) {
38888
+ sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
38889
+ builderSeeds.add(`${obj}@${i2 + 1}`);
38890
+ added = true;
38891
+ }
38892
+ }
38893
+ }
38894
+ }
38895
+ }
38760
38896
  const SIMPLE_IDENT = /^[\p{L}\p{N}_]+$/u;
38761
38897
  const reCache = /* @__PURE__ */ new Map();
38762
38898
  for (const s of sourcesWithVar) {
@@ -38819,6 +38955,11 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38819
38955
  const sinkFn = goFnStart(sink.line);
38820
38956
  if (srcFn !== -1 && sinkFn !== -1 && srcFn !== sinkFn) continue;
38821
38957
  }
38958
+ if (csFnRanges.length > 0 && builderSeeds.has(`${source.variable}@${source.line}`)) {
38959
+ const srcFn = csFnStart(source.line);
38960
+ const sinkFn = csFnStart(sink.line);
38961
+ if (srcFn !== sinkFn) continue;
38962
+ }
38822
38963
  if (complex) {
38823
38964
  const re = reCache.get(source.variable);
38824
38965
  if (!re || !re.test(expr)) continue;
@@ -4480,6 +4480,21 @@ function extractTypes(tree, cache, language) {
4480
4480
  }
4481
4481
  return extractJavaTypes(tree, cache);
4482
4482
  }
4483
+ function csharpMemberNodes(body2) {
4484
+ if (!body2) return [];
4485
+ const out2 = [];
4486
+ const visit = (parent, depth) => {
4487
+ if (depth > 32) return;
4488
+ for (let i2 = 0; i2 < parent.childCount; i2++) {
4489
+ const c = parent.child(i2);
4490
+ if (!c) continue;
4491
+ if (c.type.startsWith("preproc_")) visit(c, depth + 1);
4492
+ else out2.push(c);
4493
+ }
4494
+ };
4495
+ visit(body2, 0);
4496
+ return out2;
4497
+ }
4483
4498
  function extractCSharpTypes(tree, cache) {
4484
4499
  const types = [];
4485
4500
  const KINDS = {
@@ -4496,9 +4511,8 @@ function extractCSharpTypes(tree, cache) {
4496
4511
  const methods = [];
4497
4512
  const fields = extractCSharpFields(body2);
4498
4513
  if (body2) {
4499
- for (let i2 = 0; i2 < body2.childCount; i2++) {
4500
- const m = body2.child(i2);
4501
- if (!m || m.type !== "method_declaration") continue;
4514
+ for (const m of csharpMemberNodes(body2)) {
4515
+ if (m.type !== "method_declaration") continue;
4502
4516
  const mName = m.childForFieldName("name");
4503
4517
  const paramList = m.childForFieldName("parameters");
4504
4518
  const parameters = [];
@@ -4549,9 +4563,7 @@ function extractCSharpTypes(tree, cache) {
4549
4563
  function extractCSharpFields(body2) {
4550
4564
  const fields = [];
4551
4565
  if (!body2) return fields;
4552
- for (let i2 = 0; i2 < body2.childCount; i2++) {
4553
- const c = body2.child(i2);
4554
- if (!c) continue;
4566
+ for (const c of csharpMemberNodes(body2)) {
4555
4567
  if (c.type === "field_declaration") {
4556
4568
  let varDecl = null;
4557
4569
  for (let k = 0; k < c.childCount; k++) {
@@ -6085,6 +6097,13 @@ function extractCalls(tree, cache, language) {
6085
6097
  function buildCSharpReceiverTypeMap(tree, cache) {
6086
6098
  const map = /* @__PURE__ */ new Map();
6087
6099
  const simple = (t) => t.replace(/<[^>]*>/g, "").split(".").pop()?.trim() ?? t;
6100
+ for (const prm of getNodesFromCache(tree.rootNode, "parameter", cache)) {
6101
+ const nameNode = prm.childForFieldName("name");
6102
+ const typeNode = prm.childForFieldName("type");
6103
+ if (nameNode?.type !== "identifier" || !typeNode) continue;
6104
+ const t = getNodeText(typeNode);
6105
+ if (t && t !== "var") map.set(getNodeText(nameNode), simple(t));
6106
+ }
6088
6107
  for (const vd of getNodesFromCache(tree.rootNode, "variable_declaration", cache)) {
6089
6108
  const typeNode = vd.childForFieldName("type");
6090
6109
  const declaredType = typeNode ? getNodeText(typeNode) : null;
@@ -6114,6 +6133,7 @@ function findFirstDescendant(node, type) {
6114
6133
  }
6115
6134
  return null;
6116
6135
  }
6136
+ var CSHARP_RESPONSE_TYPES = /* @__PURE__ */ new Set(["HttpResponse", "HttpResponseBase"]);
6117
6137
  var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
6118
6138
  "ExecuteScalar",
6119
6139
  "ExecuteReader",
@@ -6266,7 +6286,8 @@ function extractCSharpCalls(tree, cache) {
6266
6286
  if (recvType !== "DirectorySearcher") continue;
6267
6287
  } else if (propName === "StatusDescription") {
6268
6288
  const recv = exprNode ? getNodeText(exprNode) : "";
6269
- if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv)) continue;
6289
+ const recvType = recv ? typeMap.get(recv) : void 0;
6290
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv) && !(recvType && CSHARP_RESPONSE_TYPES.has(recvType))) continue;
6270
6291
  } else if (propName === "BaseAddress") {
6271
6292
  const recv = exprNode ? getNodeText(exprNode) : null;
6272
6293
  const recvType = recv ? typeMap.get(recv) : void 0;
@@ -11139,6 +11160,23 @@ var DEFAULT_SOURCES = [
11139
11160
  { method: "next", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11140
11161
  { method: "nextInt", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11141
11162
  // Database result sources
11163
+ // C# I/O, network and database reads (cognium-dev#502). Mirrors the Java
11164
+ // rows above one-for-one, including the severity split: console/file I/O is
11165
+ // `io_input` at high, a database row read is `db_input` at medium. There is
11166
+ // no separate confidence tier — `type` + `severity` IS the tier, exactly as
11167
+ // for `BufferedReader.readLine` vs `ResultSet.getString`.
11168
+ { method: "ReadLine", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11169
+ { method: "ReadLineAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11170
+ { method: "ReadToEnd", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11171
+ { method: "ReadToEndAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11172
+ // Socket and HTTP-client reads. `GetStream` / `AcceptTcpClient` /
11173
+ // `OpenRead` return the channel; the read off it is covered by the
11174
+ // StreamReader rows above, and seeding the channel too lets a flow form when
11175
+ // the read is inlined.
11176
+ { method: "GetStream", class: "TcpClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11177
+ { method: "AcceptTcpClient", class: "TcpListener", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11178
+ { method: "OpenRead", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11179
+ { method: "DownloadString", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11142
11180
  { method: "getString", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11143
11181
  { method: "getObject", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11144
11182
  { method: "getInt", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
@@ -13703,8 +13741,12 @@ var DEFAULT_SINKS = [
13703
13741
  { method: "OpenRead", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13704
13742
  { method: "OpenWrite", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13705
13743
  { method: "FileStream", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13706
- { method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13707
- { method: "StreamWriter", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13744
+ // #502: the `(Stream)` overload takes no path — `new StreamReader(tcp.GetStream())`
13745
+ // is not CWE-22. The `(string path)` overload still matches.
13746
+ { method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
13747
+ // #502: the `(Stream)` overload takes no path — `new StreamWriter(tcp.GetStream())`
13748
+ // is not CWE-22. The `(string path)` overload still matches.
13749
+ { method: "StreamWriter", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
13708
13750
  // Remaining System.IO.File path APIs — class-scoped so they don't collide
13709
13751
  // with unrelated methods. Copy/Move take a source AND destination path (both
13710
13752
  // attacker-controllable → read-anywhere / write-anywhere), hence [0, 1].
@@ -13748,6 +13790,23 @@ var DEFAULT_SINKS = [
13748
13790
  // `new RestClient(userUrl)` points every later request at attacker-controlled
13749
13791
  // infrastructure. Ctor arg 0, taint-gated.
13750
13792
  { method: "RestClient", class: "constructor", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13793
+ // C# code injection — CodeDOM runtime compilation (CWE-94), cognium-dev#503
13794
+ // part 2. `provider.CompileAssemblyFromSource(parameters, source)` compiles
13795
+ // and can then execute attacker-supplied C#, so the source argument is the
13796
+ // injection point. The signature is
13797
+ // `CompileAssemblyFromSource(CompilerParameters, params string[] sources)`,
13798
+ // so the sources begin at position 1 — position 0 is the options object.
13799
+ //
13800
+ // These rows are only useful together with the #530 StringBuilder
13801
+ // propagation fix: the Juliet CWE-94 corpus accumulates the compiled source
13802
+ // through `sb.Append(...)` and reads it back with `ToString()`, so before
13803
+ // that fix the sink registered but no taint ever reached it.
13804
+ { method: "CompileAssemblyFromSource", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
13805
+ { method: "CompileAssemblyFromSourceBatch", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
13806
+ // Roslyn's equivalent pair. `ParseText` turns text into a syntax tree and
13807
+ // `Create` compiles it; either is the point where attacker text becomes code.
13808
+ { method: "ParseText", class: "CSharpSyntaxTree", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13809
+ { method: "ParseText", class: "SyntaxFactory", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13751
13810
  // C# code injection — dynamic script/assembly loading (CWE-94).
13752
13811
  { method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13753
13812
  { method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -15529,6 +15588,20 @@ function escapeRe(s) {
15529
15588
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15530
15589
  }
15531
15590
  var TYPE_TOKEN_RE = /^new\s+(?:TypeReference|TypeToken)\s*<[\s\S]*>\s*\(\s*\)\s*\{\s*\}$/;
15591
+ function argIsStreamExpression(call, position) {
15592
+ const arg = call.arguments.find((a) => a.position === position);
15593
+ if (!arg) return false;
15594
+ const expr = (arg.expression ?? "").trim();
15595
+ if (!expr) return false;
15596
+ return (
15597
+ // `new MemoryStream(...)`, `new FileStream(...)`, `new GZipStream(...)`
15598
+ /^new\s+[A-Za-z_][\w.]*Stream\s*\(/.test(expr) || // `tcp.GetStream()`, `resp.GetResponseStream()`, `file.OpenReadStream()`
15599
+ /\.\s*Get(?:Response)?Stream\s*\(/.test(expr) || /\.\s*OpenReadStream\s*\(/.test(expr) || // `wc.OpenRead(url)` returns a Stream (WebClient), as does `File.OpenRead`
15600
+ /\.\s*OpenRead\s*\(/.test(expr) || // `Request.Body`, `x.BaseStream`
15601
+ /\.\s*(?:Body|BaseStream)\s*$/.test(expr) || // a local whose name is stream-ish and is not a path string
15602
+ /^(?:[A-Za-z_]\w*\.)*[A-Za-z_]*[sS]tream\d*$/.test(expr)
15603
+ );
15604
+ }
15532
15605
  function argIsStringLiteral(call, position) {
15533
15606
  const arg = call.arguments.find((a) => a.position === position);
15534
15607
  if (!arg) return false;
@@ -15766,6 +15839,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
15766
15839
  if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
15767
15840
  continue;
15768
15841
  }
15842
+ if (pattern.safe_if_stream_arg_at !== void 0 && argIsStreamExpression(call, pattern.safe_if_stream_arg_at)) {
15843
+ continue;
15844
+ }
15769
15845
  if (pattern.type === "command_injection") {
15770
15846
  if (call.is_constructor) {
15771
15847
  if (!CWE_78_RECEIVER_ALLOWLIST.has(call.method_name)) {