circle-ir 4.9.27 → 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.
Files changed (31) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +58 -5
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/findings.d.ts.map +1 -1
  5. package/dist/analysis/findings.js +130 -0
  6. package/dist/analysis/findings.js.map +1 -1
  7. package/dist/analysis/interprocedural.d.ts.map +1 -1
  8. package/dist/analysis/interprocedural.js +63 -0
  9. package/dist/analysis/interprocedural.js.map +1 -1
  10. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  11. package/dist/analysis/passes/language-sources-pass.js +162 -8
  12. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  13. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  14. package/dist/analysis/passes/taint-propagation-pass.js +214 -16
  15. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  16. package/dist/analysis/passes/variable-shadowing-pass.d.ts.map +1 -1
  17. package/dist/analysis/passes/variable-shadowing-pass.js +15 -0
  18. package/dist/analysis/passes/variable-shadowing-pass.js.map +1 -1
  19. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  20. package/dist/analysis/taint-matcher.js +62 -1
  21. package/dist/analysis/taint-matcher.js.map +1 -1
  22. package/dist/browser/circle-ir.js +486 -33
  23. package/dist/core/circle-ir-core.cjs +173 -13
  24. package/dist/core/circle-ir-core.js +173 -13
  25. package/dist/core/extractors/calls.js +155 -1
  26. package/dist/core/extractors/calls.js.map +1 -1
  27. package/dist/core/extractors/types.js +50 -7
  28. package/dist/core/extractors/types.js.map +1 -1
  29. package/dist/types/config.d.ts +14 -0
  30. package/dist/types/config.d.ts.map +1 -1
  31. package/package.json +1 -1
@@ -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",
@@ -6184,6 +6204,31 @@ function extractCSharpCalls(tree, cache) {
6184
6204
  });
6185
6205
  continue;
6186
6206
  }
6207
+ if (left?.type === "identifier" && asn.parent?.type === "initializer_expression") {
6208
+ const creation = asn.parent.parent;
6209
+ if (creation?.type !== "object_creation_expression") continue;
6210
+ const ctorType = creation.childForFieldName("type");
6211
+ const right2 = asn.childForFieldName("right");
6212
+ if (!ctorType || !right2) continue;
6213
+ const typeName = csharpBareName(ctorType);
6214
+ const rhsText2 = getNodeText(right2);
6215
+ calls.push({
6216
+ method_name: getNodeText(left),
6217
+ receiver: typeName,
6218
+ receiver_type: typeName,
6219
+ receiver_type_fqn: null,
6220
+ arguments: [{
6221
+ position: 0,
6222
+ expression: rhsText2,
6223
+ variable: right2.type === "identifier" ? rhsText2 : null,
6224
+ literal: CSHARP_LITERAL_NODE_TYPES.has(right2.type) ? rhsText2 : null,
6225
+ value: null
6226
+ }],
6227
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
6228
+ in_method: findEnclosingMethod(asn)
6229
+ });
6230
+ continue;
6231
+ }
6187
6232
  if (left?.type !== "member_access_expression") continue;
6188
6233
  const nameNode = left.childForFieldName("name");
6189
6234
  if (!nameNode) continue;
@@ -6193,6 +6238,14 @@ function extractCSharpCalls(tree, cache) {
6193
6238
  const recv = exprNode ? getNodeText(exprNode) : null;
6194
6239
  const recvType = recv ? typeMap.get(recv) : void 0;
6195
6240
  if (recvType !== "DirectorySearcher") continue;
6241
+ } else if (propName === "StatusDescription") {
6242
+ const recv = exprNode ? getNodeText(exprNode) : "";
6243
+ const recvType = recv ? typeMap.get(recv) : void 0;
6244
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv) && !(recvType && CSHARP_RESPONSE_TYPES.has(recvType))) continue;
6245
+ } else if (propName === "BaseAddress") {
6246
+ const recv = exprNode ? getNodeText(exprNode) : null;
6247
+ const recvType = recv ? typeMap.get(recv) : void 0;
6248
+ if (recvType !== "HttpClient") continue;
6196
6249
  } else if (propName !== "CommandText") {
6197
6250
  continue;
6198
6251
  }
@@ -6215,8 +6268,34 @@ function extractCSharpCalls(tree, cache) {
6215
6268
  in_method: findEnclosingMethod(asn)
6216
6269
  });
6217
6270
  }
6271
+ const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
6272
+ for (const cast of casts) {
6273
+ const typeNode = cast.childForFieldName("type");
6274
+ const valueNode = cast.childForFieldName("value");
6275
+ if (!typeNode || !valueNode) continue;
6276
+ const typeName = csharpBareName(typeNode);
6277
+ if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName)) continue;
6278
+ const valueText = getNodeText(valueNode);
6279
+ calls.push({
6280
+ method_name: typeName,
6281
+ receiver: null,
6282
+ receiver_type: null,
6283
+ receiver_type_fqn: null,
6284
+ arguments: [{
6285
+ position: 0,
6286
+ expression: valueText,
6287
+ variable: valueNode.type === "identifier" ? valueText : null,
6288
+ literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
6289
+ value: null
6290
+ }],
6291
+ location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
6292
+ in_method: findEnclosingMethod(cast),
6293
+ is_constructor: true
6294
+ });
6295
+ }
6218
6296
  return calls;
6219
6297
  }
6298
+ var CSHARP_RAW_MARKUP_CAST_TYPES = /* @__PURE__ */ new Set(["MarkupString"]);
6220
6299
  function extractCSharpArguments(argsNode) {
6221
6300
  const args2 = [];
6222
6301
  let position = 0;
@@ -6236,12 +6315,22 @@ function extractCSharpArguments(argsNode) {
6236
6315
  position: position++,
6237
6316
  expression: text,
6238
6317
  variable: expr?.type === "identifier" ? text : null,
6239
- literal: expr?.type === "string_literal" ? text : null,
6318
+ literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
6240
6319
  value: null
6241
6320
  });
6242
6321
  }
6243
6322
  return args2;
6244
6323
  }
6324
+ var CSHARP_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set([
6325
+ "string_literal",
6326
+ "verbatim_string_literal",
6327
+ "raw_string_literal",
6328
+ "character_literal",
6329
+ "integer_literal",
6330
+ "real_literal",
6331
+ "boolean_literal",
6332
+ "null_literal"
6333
+ ]);
6245
6334
  function extractJavaScriptCalls(tree, cache) {
6246
6335
  const calls = [];
6247
6336
  const context = buildJSResolutionContext(tree, cache);
@@ -11652,6 +11741,23 @@ var DEFAULT_SOURCES = [
11652
11741
  { method: "next", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11653
11742
  { method: "nextInt", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11654
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"] },
11655
11761
  { method: "getString", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11656
11762
  { method: "getObject", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11657
11763
  { method: "getInt", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
@@ -13892,9 +13998,14 @@ var DEFAULT_SINKS = [
13892
13998
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13893
13999
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13894
14000
  { method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13895
- // Generic deserialization patterns
13896
- { method: "from_str", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
13897
- { method: "from_slice", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
14001
+ // #294/#484: no classless `from_str` / `from_slice` deserialization sink.
14002
+ // Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
14003
+ // `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
14004
+ // parsing, which cannot instantiate an attacker-chosen type. Removing them
14005
+ // was previously blocked because the class-scoped rows below could not match
14006
+ // the IMPORTED call form (`use serde_json::from_str; from_str(&body)`); the
14007
+ // `::` path-separator fix in taint-matcher.ts makes them match, so the
14008
+ // classless rows are no longer load-bearing.
13898
14009
  // Rust XSS (actix-web, rocket, axum response body)
13899
14010
  { method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
13900
14011
  { method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -14211,8 +14322,12 @@ var DEFAULT_SINKS = [
14211
14322
  { method: "OpenRead", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
14212
14323
  { method: "OpenWrite", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
14213
14324
  { method: "FileStream", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
14214
- { method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
14215
- { 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"] },
14216
14331
  // Remaining System.IO.File path APIs — class-scoped so they don't collide
14217
14332
  // with unrelated methods. Copy/Move take a source AND destination path (both
14218
14333
  // attacker-controllable → read-anywhere / write-anywhere), hence [0, 1].
@@ -14256,6 +14371,23 @@ var DEFAULT_SINKS = [
14256
14371
  // `new RestClient(userUrl)` points every later request at attacker-controlled
14257
14372
  // infrastructure. Ctor arg 0, taint-gated.
14258
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"] },
14259
14391
  // C# code injection — dynamic script/assembly loading (CWE-94).
14260
14392
  { method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
14261
14393
  { method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -14300,6 +14432,10 @@ var DEFAULT_SINKS = [
14300
14432
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
14301
14433
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
14302
14434
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
14435
+ // #503 (part 1): `Response.StatusDescription = "Bad " + data` writes
14436
+ // attacker text into the HTTP status line (CWE-81). Surfaced by the
14437
+ // property-assignment synthesiser in the C# extractor, receiver-gated there.
14438
+ { method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
14303
14439
  // `return Content(html, "text/html")` on a controller writes arg 0 to the
14304
14440
  // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
14305
14441
  // `Content(constantString)` and the JSON/plain-text content types are
@@ -14314,6 +14450,12 @@ var DEFAULT_SINKS = [
14314
14450
  // filter is the constructor argument.
14315
14451
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
14316
14452
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
14453
+ // #336: a tainted `HttpClient.BaseAddress` gives the attacker the scheme,
14454
+ // host and port of every RELATIVE request made on that client, so the
14455
+ // request arguments can all be constant and the destination still be
14456
+ // attacker-chosen. Surfaced by the property-assignment synthesiser, which
14457
+ // class-gates it to a resolved `HttpClient`.
14458
+ { method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
14317
14459
  // C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
14318
14460
  // cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
14319
14461
  // an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
@@ -16146,6 +16288,20 @@ function escapeRe(s) {
16146
16288
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16147
16289
  }
16148
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
+ }
16149
16305
  function argIsStringLiteral(call, position) {
16150
16306
  const arg = call.arguments.find((a) => a.position === position);
16151
16307
  if (!arg) return false;
@@ -16383,6 +16539,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
16383
16539
  if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
16384
16540
  continue;
16385
16541
  }
16542
+ if (pattern.safe_if_stream_arg_at !== void 0 && argIsStreamExpression(call, pattern.safe_if_stream_arg_at)) {
16543
+ continue;
16544
+ }
16386
16545
  if (pattern.type === "command_injection") {
16387
16546
  if (call.is_constructor) {
16388
16547
  if (!CWE_78_RECEIVER_ALLOWLIST.has(call.method_name)) {
@@ -16777,8 +16936,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
16777
16936
  return false;
16778
16937
  } else if (!call.receiver && !call.receiver_type) {
16779
16938
  const target = call.resolution?.target;
16939
+ const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
16780
16940
  const expectedTail = `${pattern.class}.${pattern.method}`;
16781
- if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
16941
+ if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
16782
16942
  } else {
16783
16943
  return false;
16784
16944
  }
@@ -19345,6 +19505,122 @@ var GO_PURE_PACKAGES = /* @__PURE__ */ new Set([
19345
19505
  "hex",
19346
19506
  "base64"
19347
19507
  ]);
19508
+ var CSHARP_PURE_STATIC_RECEIVERS = /* @__PURE__ */ new Set([
19509
+ // formatting / conversion / parsing — return a derived value
19510
+ "string",
19511
+ "String",
19512
+ "Convert",
19513
+ "Encoding",
19514
+ "BitConverter",
19515
+ "Math",
19516
+ "Int16",
19517
+ "Int32",
19518
+ "Int64",
19519
+ "UInt16",
19520
+ "UInt32",
19521
+ "UInt64",
19522
+ "Double",
19523
+ "Single",
19524
+ "Decimal",
19525
+ "Boolean",
19526
+ "Byte",
19527
+ "SByte",
19528
+ "Char",
19529
+ "Guid",
19530
+ "DateTime",
19531
+ "DateTimeOffset",
19532
+ "TimeSpan",
19533
+ "Enum",
19534
+ "Array",
19535
+ "Buffer",
19536
+ "Nullable",
19537
+ "CultureInfo",
19538
+ "StringComparer",
19539
+ "Comparer",
19540
+ "EqualityComparer",
19541
+ // logging / diagnostics receivers
19542
+ "Console",
19543
+ "Debug",
19544
+ "Trace",
19545
+ "Logger",
19546
+ "logger",
19547
+ "_logger",
19548
+ "Log"
19549
+ ]);
19550
+ var CSHARP_NON_ESCAPE_METHODS = /* @__PURE__ */ new Set([
19551
+ // string / value derivation
19552
+ "ToString",
19553
+ "ToArray",
19554
+ "ToList",
19555
+ "ToCharArray",
19556
+ "GetHashCode",
19557
+ "Equals",
19558
+ "CompareTo",
19559
+ "Trim",
19560
+ "TrimStart",
19561
+ "TrimEnd",
19562
+ "Substring",
19563
+ "Split",
19564
+ "Join",
19565
+ "Concat",
19566
+ "PadLeft",
19567
+ "PadRight",
19568
+ "ToLower",
19569
+ "ToUpper",
19570
+ "ToLowerInvariant",
19571
+ "ToUpperInvariant",
19572
+ "StartsWith",
19573
+ "EndsWith",
19574
+ "Contains",
19575
+ "IndexOf",
19576
+ "LastIndexOf",
19577
+ "Insert",
19578
+ "Remove",
19579
+ "Normalize",
19580
+ "Append",
19581
+ "AppendLine",
19582
+ "AppendFormat",
19583
+ "TryParse",
19584
+ "Parse",
19585
+ // structured logging (Microsoft.Extensions.Logging, Serilog, NLog, log4net)
19586
+ "LogInformation",
19587
+ "LogWarning",
19588
+ "LogError",
19589
+ "LogDebug",
19590
+ "LogTrace",
19591
+ "LogCritical",
19592
+ "Information",
19593
+ "Warning",
19594
+ "Verbose",
19595
+ "Fatal",
19596
+ "BeginScope",
19597
+ // Collection stores. A value put into a container has not left the program —
19598
+ // the same reasoning that puts Go's `append` in GO_BUILTIN_CALLS. Measured
19599
+ // on the 10 scored Juliet C# families these were 1,296 of the 2,368 escape
19600
+ // rows remaining after the first pass of this gate (#474): `Add` 864,
19601
+ // `AddLast` 432.
19602
+ //
19603
+ // Project methods are deliberately NOT gated, matching the Go gate's rule
19604
+ // that an unknown call must still escape: Juliet's own `BadSink` /
19605
+ // `GoodB2GSink` / `Action` dispatches account for a further 840 rows and are
19606
+ // left alone, because "the callee is defined in this file" is a different
19607
+ // claim needing its own change.
19608
+ "Add",
19609
+ "AddLast",
19610
+ "AddFirst",
19611
+ "AddRange",
19612
+ "TryAdd",
19613
+ "Insert",
19614
+ "Push",
19615
+ "Enqueue"
19616
+ ]);
19617
+ function isCSharpNonEscapeCall(call) {
19618
+ const name2 = call.method_name;
19619
+ const recv = (call.receiver ?? "").split(".").pop() ?? "";
19620
+ if (CSHARP_PURE_STATIC_RECEIVERS.has(recv)) return true;
19621
+ if (CSHARP_NON_ESCAPE_METHODS.has(name2)) return true;
19622
+ return false;
19623
+ }
19348
19624
  function isGoNonEscapeCall(call) {
19349
19625
  const name2 = call.method_name;
19350
19626
  const recv = (call.receiver ?? "").split(".").pop() ?? "";
@@ -19611,6 +19887,9 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
19611
19887
  if (graph.ir.meta.language === "go" && isGoNonEscapeCall(call)) {
19612
19888
  continue;
19613
19889
  }
19890
+ if (graph.ir.meta.language === "csharp" && isCSharpNonEscapeCall(call)) {
19891
+ continue;
19892
+ }
19614
19893
  const bashSafeBuiltins = /* @__PURE__ */ new Set([
19615
19894
  "echo",
19616
19895
  "printf",
@@ -27786,7 +28065,8 @@ var LanguageSourcesPass = class {
27786
28065
  }
27787
28066
  if (language === "csharp") {
27788
28067
  additionalSanitizers.push(
27789
- ...findCSharpFullPathStartsWithGuardSanitizers(code)
28068
+ ...findCSharpFullPathStartsWithGuardSanitizers(code),
28069
+ ...findCSharpEarlyReturnGuardSanitizers(code, language)
27790
28070
  );
27791
28071
  }
27792
28072
  if (language === "java") {
@@ -28443,23 +28723,35 @@ function findCSharpRequestSources(sourceCode, language) {
28443
28723
  const formFileDeclRe = /\bIFormFile\s+([A-Za-z_]\w*)/g;
28444
28724
  const httpRequestReceivers = /* @__PURE__ */ new Set(["Request"]);
28445
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;
28446
28730
  for (const line of lines) {
28447
28731
  const formRe = new RegExp(formFileDeclRe.source, "g");
28448
28732
  let d;
28449
28733
  while ((d = formRe.exec(line)) !== null) formFileParams.add(d[1]);
28450
28734
  const reqRe = new RegExp(httpRequestDeclRe.source, "g");
28451
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]);
28452
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;
28453
28743
  const formFileReadRe = formFileParams.size > 0 ? new RegExp(`\\b(?:${[...formFileParams].join("|")})\\s*\\.\\s*(?:FileName|ContentType)\\b`) : null;
28454
28744
  const requestReadRe = new RegExp(
28455
28745
  `\\b(?:${[...httpRequestReceivers].join("|")})\\s*\\.\\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\\b`
28456
28746
  );
28457
- 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;
28458
28748
  const inlineReads = [
28459
28749
  [requestReadRe, "http_param"],
28460
28750
  [formFileReadRe, "http_param"],
28461
28751
  [consoleReadRe, "io_input"],
28462
- [envReadRe, "env_input"]
28752
+ [envReadRe, "env_input"],
28753
+ [streamReadRe, "io_input"],
28754
+ [dbReadRe, "db_input"]
28463
28755
  ];
28464
28756
  const inlineRead = (text) => {
28465
28757
  const argsStart = text.indexOf("(");
@@ -28480,7 +28772,8 @@ function findCSharpRequestSources(sourceCode, language) {
28480
28772
  const embeddedAssignRe = /(?:^|[{;])\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*([^;{}]+)/;
28481
28773
  for (let i2 = 0; i2 < lines.length; i2++) {
28482
28774
  const m = assignRe.exec(lines[i2]);
28483
- if (!m) {
28775
+ const rhsCrossesBlock = m !== null && m[2].includes("{");
28776
+ if (!m || rhsCrossesBlock) {
28484
28777
  const stmt = lines[i2];
28485
28778
  if (stmt.includes("{") && !/^\s*(?:\/\/|\/\*|\*)/.test(stmt)) {
28486
28779
  const emb = new RegExp(embeddedAssignRe.source, "g");
@@ -28496,7 +28789,8 @@ function findCSharpRequestSources(sourceCode, language) {
28496
28789
  sources.push({
28497
28790
  type: type3,
28498
28791
  location: `${varName2} = ${rhs2.trim().substring(0, 50)}${rhs2.length > 50 ? "..." : ""}`,
28499
- severity: "high",
28792
+ // #502 — Java parity: a database row read is medium, I/O is high.
28793
+ severity: type3 === "db_input" ? "medium" : "high",
28500
28794
  line: lineNumber3,
28501
28795
  confidence: 1,
28502
28796
  variable: varName2
@@ -28515,7 +28809,7 @@ function findCSharpRequestSources(sourceCode, language) {
28515
28809
  sources.push({
28516
28810
  type: type2,
28517
28811
  location: `${read} in ${code.substring(0, 50)}${code.length > 50 ? "..." : ""}`,
28518
- severity: "high",
28812
+ severity: type2 === "db_input" ? "medium" : "high",
28519
28813
  line: lineNumber2,
28520
28814
  confidence: 1,
28521
28815
  variable: "<inline>",
@@ -28531,7 +28825,8 @@ function findCSharpRequestSources(sourceCode, language) {
28531
28825
  sources.push({
28532
28826
  type,
28533
28827
  location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
28534
- severity: "high",
28828
+ // #502 — Java parity: a database row read is medium, I/O is high.
28829
+ severity: type === "db_input" ? "medium" : "high",
28535
28830
  line: lineNumber,
28536
28831
  confidence: 1,
28537
28832
  variable: varName
@@ -28698,7 +28993,8 @@ function findPythonAssignmentSources(sourceCode, language) {
28698
28993
  sources.push({
28699
28994
  type,
28700
28995
  location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
28701
- severity: "high",
28996
+ // #502 — Java parity: a database row read is medium, I/O is high.
28997
+ severity: type === "db_input" ? "medium" : "high",
28702
28998
  line: lineNumber,
28703
28999
  confidence: 0.95,
28704
29000
  variable: varName
@@ -30902,6 +31198,66 @@ function guardRejects(lines, i2, terminatorRe, window2) {
30902
31198
  }
30903
31199
  return false;
30904
31200
  }
31201
+ function findCSharpEarlyReturnGuardSanitizers(code, language) {
31202
+ if (language !== "csharp") return [];
31203
+ const sanitizers = [];
31204
+ const lines = code.split("\n");
31205
+ const terminatorRe = /\b(throw|return)\b/;
31206
+ const allowlistRes = [
31207
+ /\bif\s*\(\s*Array\s*\.\s*IndexOf\s*\([^,]+,\s*([A-Za-z_][\w.]*)\s*\)\s*<\s*0\s*\)/,
31208
+ /\bif\s*\(\s*!\s*[A-Za-z_][\w.]*\s*\.\s*Contains\s*\(\s*([A-Za-z_][\w.]*)\s*\)\s*\)/
31209
+ ];
31210
+ const charClassRe = /\bif\s*\(\s*!\s*([A-Za-z_][\w.]*)\s*\.\s*All\s*\(\s*char\s*\.\s*Is[A-Za-z]+\s*\)\s*\)/;
31211
+ for (let i2 = 0; i2 < lines.length; i2++) {
31212
+ let guardedExpr = null;
31213
+ for (const re of allowlistRes) {
31214
+ const m = re.exec(lines[i2]);
31215
+ if (m) {
31216
+ guardedExpr = m[1];
31217
+ break;
31218
+ }
31219
+ }
31220
+ if (!guardedExpr) {
31221
+ const m = charClassRe.exec(lines[i2]);
31222
+ if (m) guardedExpr = m[1];
31223
+ }
31224
+ if (!guardedExpr) continue;
31225
+ if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
31226
+ const root = guardedExpr.split(".")[0];
31227
+ const guarded = /* @__PURE__ */ new Set([guardedExpr, root]);
31228
+ const assignRe = /^\s*(?:(?:var|string)\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/;
31229
+ for (let b = i2 - 1; b >= 0; b--) {
31230
+ const a = assignRe.exec(lines[b]);
31231
+ if (!a || !guarded.has(a[1])) continue;
31232
+ for (const id of a[2].match(/[A-Za-z_]\w*/g) ?? []) guarded.add(id);
31233
+ }
31234
+ const mentions = (t) => {
31235
+ for (const n of guarded) if (new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(t)) return true;
31236
+ return false;
31237
+ };
31238
+ for (let l = i2 + 1; l < lines.length; l++) {
31239
+ const a = assignRe.exec(lines[l]);
31240
+ if (a && mentions(a[2])) guarded.add(a[1]);
31241
+ if (mentions(lines[l])) {
31242
+ sanitizers.push({
31243
+ type: "csharp_early_return_guard",
31244
+ method: "guard",
31245
+ line: l + 1,
31246
+ sanitizes: [
31247
+ "ssrf",
31248
+ "xpath_injection",
31249
+ "ldap_injection",
31250
+ "path_traversal",
31251
+ "command_injection",
31252
+ "sql_injection",
31253
+ "external_taint_escape"
31254
+ ]
31255
+ });
31256
+ }
31257
+ }
31258
+ }
31259
+ return sanitizers;
31260
+ }
30905
31261
  function findCSharpFullPathStartsWithGuardSanitizers(code) {
30906
31262
  const sanitizers = [];
30907
31263
  const lines = code.split("\n");
@@ -37299,6 +37655,53 @@ var LibraryProfileXssGatePass = class {
37299
37655
  };
37300
37656
 
37301
37657
  // src/analysis/passes/taint-propagation-pass.ts
37658
+ var CONTAINER_READ_SOURCES = /* @__PURE__ */ new Set(["plugin_param", "config_param"]);
37659
+ function makeConstPropVeto(constProp, graph, sources, code) {
37660
+ const lines = typeof code === "string" ? code.split("\n") : [];
37661
+ const selfReassign = /* @__PURE__ */ new Map();
37662
+ const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37663
+ for (const [line, defs] of graph.defsByLine) {
37664
+ const text = lines[line - 1];
37665
+ if (!text) continue;
37666
+ for (const d of defs) {
37667
+ if (d.kind !== "local" && d.kind !== "param") continue;
37668
+ const v = escape(d.variable);
37669
+ const m = new RegExp(`(?:^|[^\\w.$])${v}\\s*(?:([-+*/%&|^]|<<|>>)=|=(?!=))(.*)$`).exec(text);
37670
+ if (!m) continue;
37671
+ if (m[1] || new RegExp(`(?:^|[^\\w.$])${v}\\b`).test(m[2])) {
37672
+ const arr = selfReassign.get(d.variable) ?? [];
37673
+ arr.push(line);
37674
+ selfReassign.set(d.variable, arr);
37675
+ }
37676
+ }
37677
+ }
37678
+ const reassignedAt = (v, line) => (selfReassign.get(v) ?? []).includes(line);
37679
+ const defAt = (v, line) => {
37680
+ const own = (graph.defsByLine.get(line) ?? []).find((d) => d.variable === v);
37681
+ if (own) return own;
37682
+ const use = (graph.usesByLine.get(line) ?? []).find((u) => u.variable === v && u.def_id != null);
37683
+ return use ? graph.defById.get(use.def_id) : void 0;
37684
+ };
37685
+ return (step, sourceLine, sinkLine) => {
37686
+ const r = isFalsePositive(constProp, step.line, step.variable);
37687
+ if (!r.isFalsePositive) return false;
37688
+ if (r.reason !== "variable_not_tainted") return true;
37689
+ const lo = Math.min(sourceLine, sinkLine);
37690
+ let d = defAt(step.variable, step.line);
37691
+ let selfHops = 0;
37692
+ for (let guard = 0; d && d.line > lo && reassignedAt(d.variable, d.line) && guard < 32; guard++) {
37693
+ const here = d.id;
37694
+ const prior = (graph.usesByLine.get(d.line) ?? []).find((u) => u.variable === d.variable && u.def_id != null && u.def_id !== here);
37695
+ if (!prior) break;
37696
+ selfHops++;
37697
+ d = graph.defById.get(prior.def_id);
37698
+ }
37699
+ if (!d) return true;
37700
+ if (d.line === sourceLine && sources.some((s) => s.line === sourceLine && !CONTAINER_READ_SOURCES.has(s.type))) return false;
37701
+ if (selfHops > 0 && d.line >= lo) return false;
37702
+ return true;
37703
+ };
37704
+ }
37302
37705
  var TaintPropagationPass = class {
37303
37706
  name = "taint-propagation";
37304
37707
  category = "security";
@@ -37316,11 +37719,11 @@ var TaintPropagationPass = class {
37316
37719
  return { flows: [] };
37317
37720
  }
37318
37721
  const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
37722
+ const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
37319
37723
  const verifiedFlows = propagationResult.flows.filter((flow) => {
37320
37724
  if (constProp.unreachableLines.has(flow.sink.line)) return false;
37321
37725
  for (const step of flow.path) {
37322
- const fpCheck = isFalsePositive(constProp, step.line, step.variable);
37323
- if (fpCheck.isFalsePositive) return false;
37726
+ if (isVetoed(step, flow.source.line, flow.sink.line)) return false;
37324
37727
  }
37325
37728
  if (isCorrelatedPredicateFP(constProp, flow)) return false;
37326
37729
  return true;
@@ -37413,7 +37816,7 @@ var TaintPropagationPass = class {
37413
37816
  if (isCorrelatedPredicateFP(constProp, flowForCheck)) continue;
37414
37817
  let isFP = false;
37415
37818
  for (const step of f.path) {
37416
- if (isFalsePositive(constProp, step.line, step.variable).isFalsePositive) {
37819
+ if (isVetoed(step, f.source_line, f.sink_line)) {
37417
37820
  isFP = true;
37418
37821
  break;
37419
37822
  }
@@ -37436,7 +37839,7 @@ var TaintPropagationPass = class {
37436
37839
  if (isCorrelatedPredicateFP(constProp, flowForCheck)) continue;
37437
37840
  let isFP = false;
37438
37841
  for (const step of f.path) {
37439
- if (isFalsePositive(constProp, step.line, step.variable).isFalsePositive) {
37842
+ if (isVetoed(step, f.source_line, f.sink_line)) {
37440
37843
  isFP = true;
37441
37844
  break;
37442
37845
  }
@@ -38121,13 +38524,22 @@ function maskStringLiteralsForTokenScan(expr) {
38121
38524
  }
38122
38525
  return out2;
38123
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
+ }
38124
38536
  function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
38125
38537
  const flows = [];
38126
- const goFnRanges = language === "go" && types ? types.flatMap((t) => t.methods.map((m) => [m.start_line, m.end_line])) : [];
38127
- 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) => {
38128
38540
  let best = -1;
38129
38541
  let bestSpan = -1;
38130
- for (const [start2, end] of goFnRanges) {
38542
+ for (const [start2, end] of ranges) {
38131
38543
  if (line >= start2 && line <= end && end - start2 > bestSpan) {
38132
38544
  best = start2;
38133
38545
  bestSpan = end - start2;
@@ -38135,6 +38547,10 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38135
38547
  }
38136
38548
  return best;
38137
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);
38138
38554
  const sourcesWithVar = sources.filter(
38139
38555
  (s) => typeof s.variable === "string" && s.variable.length > 0
38140
38556
  );
@@ -38436,16 +38852,47 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38436
38852
  if (!m) continue;
38437
38853
  const [, obj, rhs] = m;
38438
38854
  if (known.has(obj)) continue;
38439
- const codePart = rhs.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ");
38440
- const rhsTainted = sourcesWithVar.some(
38441
- (s) => new RegExp(`(?<![\\p{L}\\p{N}_])${s.variable.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}(?![\\p{L}\\p{N}_])`, "u").test(codePart)
38442
- );
38443
- if (!rhsTainted) continue;
38855
+ if (!argsReferenceTaintedVar(rhs, sourcesWithVar)) continue;
38444
38856
  sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
38445
38857
  added = true;
38446
38858
  }
38447
38859
  }
38448
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
+ }
38449
38896
  const SIMPLE_IDENT = /^[\p{L}\p{N}_]+$/u;
38450
38897
  const reCache = /* @__PURE__ */ new Map();
38451
38898
  for (const s of sourcesWithVar) {
@@ -38508,6 +38955,11 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
38508
38955
  const sinkFn = goFnStart(sink.line);
38509
38956
  if (srcFn !== -1 && sinkFn !== -1 && srcFn !== sinkFn) continue;
38510
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
+ }
38511
38963
  if (complex) {
38512
38964
  const re = reCache.get(source.variable);
38513
38965
  if (!re || !re.test(expr)) continue;
@@ -40009,6 +40461,7 @@ var VariableShadowingPass = class {
40009
40461
  const outerEntry = declLocals[0];
40010
40462
  for (let i2 = 1; i2 < numDeclLocals; i2++) {
40011
40463
  const inner = declLocals[i2];
40464
+ if (inner.def.line <= outerEntry.def.line) continue;
40012
40465
  if (!isInNestedScope(codeLines, outerEntry.def.line, inner.def.line)) continue;
40013
40466
  const key = `${variable}-${inner.def.line}`;
40014
40467
  if (reported.has(key)) continue;