circle-ir 4.9.26 → 4.9.28

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 (33) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +23 -3
  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 +154 -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 +156 -4
  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 +97 -4
  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 +21 -1
  21. package/dist/analysis/taint-matcher.js.map +1 -1
  22. package/dist/analyzer.d.ts +36 -0
  23. package/dist/analyzer.d.ts.map +1 -1
  24. package/dist/analyzer.js +30 -0
  25. package/dist/analyzer.js.map +1 -1
  26. package/dist/browser/circle-ir.js +360 -13
  27. package/dist/core/circle-ir-core.cjs +94 -5
  28. package/dist/core/circle-ir-core.js +94 -5
  29. package/dist/core/extractors/calls.js +127 -1
  30. package/dist/core/extractors/calls.js.map +1 -1
  31. package/dist/types/index.d.ts +17 -0
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/package.json +3 -3
@@ -6230,6 +6230,31 @@ function extractCSharpCalls(tree, cache) {
6230
6230
  });
6231
6231
  continue;
6232
6232
  }
6233
+ if (left?.type === "identifier" && asn.parent?.type === "initializer_expression") {
6234
+ const creation = asn.parent.parent;
6235
+ if (creation?.type !== "object_creation_expression") continue;
6236
+ const ctorType = creation.childForFieldName("type");
6237
+ const right2 = asn.childForFieldName("right");
6238
+ if (!ctorType || !right2) continue;
6239
+ const typeName = csharpBareName(ctorType);
6240
+ const rhsText2 = getNodeText(right2);
6241
+ calls.push({
6242
+ method_name: getNodeText(left),
6243
+ receiver: typeName,
6244
+ receiver_type: typeName,
6245
+ receiver_type_fqn: null,
6246
+ arguments: [{
6247
+ position: 0,
6248
+ expression: rhsText2,
6249
+ variable: right2.type === "identifier" ? rhsText2 : null,
6250
+ literal: CSHARP_LITERAL_NODE_TYPES.has(right2.type) ? rhsText2 : null,
6251
+ value: null
6252
+ }],
6253
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
6254
+ in_method: findEnclosingMethod(asn)
6255
+ });
6256
+ continue;
6257
+ }
6233
6258
  if (left?.type !== "member_access_expression") continue;
6234
6259
  const nameNode = left.childForFieldName("name");
6235
6260
  if (!nameNode) continue;
@@ -6239,6 +6264,13 @@ function extractCSharpCalls(tree, cache) {
6239
6264
  const recv = exprNode ? getNodeText(exprNode) : null;
6240
6265
  const recvType = recv ? typeMap.get(recv) : void 0;
6241
6266
  if (recvType !== "DirectorySearcher") continue;
6267
+ } else if (propName === "StatusDescription") {
6268
+ const recv = exprNode ? getNodeText(exprNode) : "";
6269
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv)) continue;
6270
+ } else if (propName === "BaseAddress") {
6271
+ const recv = exprNode ? getNodeText(exprNode) : null;
6272
+ const recvType = recv ? typeMap.get(recv) : void 0;
6273
+ if (recvType !== "HttpClient") continue;
6242
6274
  } else if (propName !== "CommandText") {
6243
6275
  continue;
6244
6276
  }
@@ -6261,8 +6293,34 @@ function extractCSharpCalls(tree, cache) {
6261
6293
  in_method: findEnclosingMethod(asn)
6262
6294
  });
6263
6295
  }
6296
+ const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
6297
+ for (const cast of casts) {
6298
+ const typeNode = cast.childForFieldName("type");
6299
+ const valueNode = cast.childForFieldName("value");
6300
+ if (!typeNode || !valueNode) continue;
6301
+ const typeName = csharpBareName(typeNode);
6302
+ if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName)) continue;
6303
+ const valueText = getNodeText(valueNode);
6304
+ calls.push({
6305
+ method_name: typeName,
6306
+ receiver: null,
6307
+ receiver_type: null,
6308
+ receiver_type_fqn: null,
6309
+ arguments: [{
6310
+ position: 0,
6311
+ expression: valueText,
6312
+ variable: valueNode.type === "identifier" ? valueText : null,
6313
+ literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
6314
+ value: null
6315
+ }],
6316
+ location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
6317
+ in_method: findEnclosingMethod(cast),
6318
+ is_constructor: true
6319
+ });
6320
+ }
6264
6321
  return calls;
6265
6322
  }
6323
+ var CSHARP_RAW_MARKUP_CAST_TYPES = /* @__PURE__ */ new Set(["MarkupString"]);
6266
6324
  function extractCSharpArguments(argsNode) {
6267
6325
  const args2 = [];
6268
6326
  let position = 0;
@@ -6282,12 +6340,22 @@ function extractCSharpArguments(argsNode) {
6282
6340
  position: position++,
6283
6341
  expression: text,
6284
6342
  variable: expr?.type === "identifier" ? text : null,
6285
- literal: expr?.type === "string_literal" ? text : null,
6343
+ literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
6286
6344
  value: null
6287
6345
  });
6288
6346
  }
6289
6347
  return args2;
6290
6348
  }
6349
+ var CSHARP_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set([
6350
+ "string_literal",
6351
+ "verbatim_string_literal",
6352
+ "raw_string_literal",
6353
+ "character_literal",
6354
+ "integer_literal",
6355
+ "real_literal",
6356
+ "boolean_literal",
6357
+ "null_literal"
6358
+ ]);
6291
6359
  function extractJavaScriptCalls(tree, cache) {
6292
6360
  const calls = [];
6293
6361
  const context = buildJSResolutionContext(tree, cache);
@@ -13311,9 +13379,14 @@ var DEFAULT_SINKS = [
13311
13379
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13312
13380
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13313
13381
  { method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13314
- // Generic deserialization patterns
13315
- { method: "from_str", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
13316
- { method: "from_slice", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
13382
+ // #294/#484: no classless `from_str` / `from_slice` deserialization sink.
13383
+ // Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
13384
+ // `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
13385
+ // parsing, which cannot instantiate an attacker-chosen type. Removing them
13386
+ // was previously blocked because the class-scoped rows below could not match
13387
+ // the IMPORTED call form (`use serde_json::from_str; from_str(&body)`); the
13388
+ // `::` path-separator fix in taint-matcher.ts makes them match, so the
13389
+ // classless rows are no longer load-bearing.
13317
13390
  // Rust XSS (actix-web, rocket, axum response body)
13318
13391
  { method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
13319
13392
  { method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -13710,10 +13783,19 @@ var DEFAULT_SINKS = [
13710
13783
  // C# XSS — raw HTML output (CWE-79).
13711
13784
  { method: "Raw", class: "Html", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13712
13785
  { method: "Write", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13786
+ // ASP.NET Core writes the response body through the `HttpResponseWritingExtensions`
13787
+ // extension methods rather than `Response.Write`, so the System.Web row above does
13788
+ // not cover it. Receiver-scoped to `Response` so the many unrelated `WriteAsync`
13789
+ // overloads (Stream, StreamWriter, TextWriter, PipeWriter) are untouched. (#503)
13790
+ { method: "WriteAsync", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13713
13791
  { method: "HtmlString", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13714
13792
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
13715
13793
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
13716
13794
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13795
+ // #503 (part 1): `Response.StatusDescription = "Bad " + data` writes
13796
+ // attacker text into the HTTP status line (CWE-81). Surfaced by the
13797
+ // property-assignment synthesiser in the C# extractor, receiver-gated there.
13798
+ { method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13717
13799
  // `return Content(html, "text/html")` on a controller writes arg 0 to the
13718
13800
  // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
13719
13801
  // `Content(constantString)` and the JSON/plain-text content types are
@@ -13728,6 +13810,12 @@ var DEFAULT_SINKS = [
13728
13810
  // filter is the constructor argument.
13729
13811
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13730
13812
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13813
+ // #336: a tainted `HttpClient.BaseAddress` gives the attacker the scheme,
13814
+ // host and port of every RELATIVE request made on that client, so the
13815
+ // request arguments can all be constant and the destination still be
13816
+ // attacker-chosen. Surfaced by the property-assignment synthesiser, which
13817
+ // class-gates it to a resolved `HttpClient`.
13818
+ { method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13731
13819
  // C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
13732
13820
  // cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
13733
13821
  // an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
@@ -16072,8 +16160,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
16072
16160
  return false;
16073
16161
  } else if (!call.receiver && !call.receiver_type) {
16074
16162
  const target = call.resolution?.target;
16163
+ const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
16075
16164
  const expectedTail = `${pattern.class}.${pattern.method}`;
16076
- if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
16165
+ if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
16077
16166
  } else {
16078
16167
  return false;
16079
16168
  }
@@ -6164,6 +6164,31 @@ function extractCSharpCalls(tree, cache) {
6164
6164
  });
6165
6165
  continue;
6166
6166
  }
6167
+ if (left?.type === "identifier" && asn.parent?.type === "initializer_expression") {
6168
+ const creation = asn.parent.parent;
6169
+ if (creation?.type !== "object_creation_expression") continue;
6170
+ const ctorType = creation.childForFieldName("type");
6171
+ const right2 = asn.childForFieldName("right");
6172
+ if (!ctorType || !right2) continue;
6173
+ const typeName = csharpBareName(ctorType);
6174
+ const rhsText2 = getNodeText(right2);
6175
+ calls.push({
6176
+ method_name: getNodeText(left),
6177
+ receiver: typeName,
6178
+ receiver_type: typeName,
6179
+ receiver_type_fqn: null,
6180
+ arguments: [{
6181
+ position: 0,
6182
+ expression: rhsText2,
6183
+ variable: right2.type === "identifier" ? rhsText2 : null,
6184
+ literal: CSHARP_LITERAL_NODE_TYPES.has(right2.type) ? rhsText2 : null,
6185
+ value: null
6186
+ }],
6187
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
6188
+ in_method: findEnclosingMethod(asn)
6189
+ });
6190
+ continue;
6191
+ }
6167
6192
  if (left?.type !== "member_access_expression") continue;
6168
6193
  const nameNode = left.childForFieldName("name");
6169
6194
  if (!nameNode) continue;
@@ -6173,6 +6198,13 @@ function extractCSharpCalls(tree, cache) {
6173
6198
  const recv = exprNode ? getNodeText(exprNode) : null;
6174
6199
  const recvType = recv ? typeMap.get(recv) : void 0;
6175
6200
  if (recvType !== "DirectorySearcher") continue;
6201
+ } else if (propName === "StatusDescription") {
6202
+ const recv = exprNode ? getNodeText(exprNode) : "";
6203
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv)) continue;
6204
+ } else if (propName === "BaseAddress") {
6205
+ const recv = exprNode ? getNodeText(exprNode) : null;
6206
+ const recvType = recv ? typeMap.get(recv) : void 0;
6207
+ if (recvType !== "HttpClient") continue;
6176
6208
  } else if (propName !== "CommandText") {
6177
6209
  continue;
6178
6210
  }
@@ -6195,8 +6227,34 @@ function extractCSharpCalls(tree, cache) {
6195
6227
  in_method: findEnclosingMethod(asn)
6196
6228
  });
6197
6229
  }
6230
+ const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
6231
+ for (const cast of casts) {
6232
+ const typeNode = cast.childForFieldName("type");
6233
+ const valueNode = cast.childForFieldName("value");
6234
+ if (!typeNode || !valueNode) continue;
6235
+ const typeName = csharpBareName(typeNode);
6236
+ if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName)) continue;
6237
+ const valueText = getNodeText(valueNode);
6238
+ calls.push({
6239
+ method_name: typeName,
6240
+ receiver: null,
6241
+ receiver_type: null,
6242
+ receiver_type_fqn: null,
6243
+ arguments: [{
6244
+ position: 0,
6245
+ expression: valueText,
6246
+ variable: valueNode.type === "identifier" ? valueText : null,
6247
+ literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
6248
+ value: null
6249
+ }],
6250
+ location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
6251
+ in_method: findEnclosingMethod(cast),
6252
+ is_constructor: true
6253
+ });
6254
+ }
6198
6255
  return calls;
6199
6256
  }
6257
+ var CSHARP_RAW_MARKUP_CAST_TYPES = /* @__PURE__ */ new Set(["MarkupString"]);
6200
6258
  function extractCSharpArguments(argsNode) {
6201
6259
  const args2 = [];
6202
6260
  let position = 0;
@@ -6216,12 +6274,22 @@ function extractCSharpArguments(argsNode) {
6216
6274
  position: position++,
6217
6275
  expression: text,
6218
6276
  variable: expr?.type === "identifier" ? text : null,
6219
- literal: expr?.type === "string_literal" ? text : null,
6277
+ literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
6220
6278
  value: null
6221
6279
  });
6222
6280
  }
6223
6281
  return args2;
6224
6282
  }
6283
+ var CSHARP_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set([
6284
+ "string_literal",
6285
+ "verbatim_string_literal",
6286
+ "raw_string_literal",
6287
+ "character_literal",
6288
+ "integer_literal",
6289
+ "real_literal",
6290
+ "boolean_literal",
6291
+ "null_literal"
6292
+ ]);
6225
6293
  function extractJavaScriptCalls(tree, cache) {
6226
6294
  const calls = [];
6227
6295
  const context = buildJSResolutionContext(tree, cache);
@@ -13245,9 +13313,14 @@ var DEFAULT_SINKS = [
13245
13313
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13246
13314
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13247
13315
  { method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13248
- // Generic deserialization patterns
13249
- { method: "from_str", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
13250
- { method: "from_slice", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
13316
+ // #294/#484: no classless `from_str` / `from_slice` deserialization sink.
13317
+ // Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
13318
+ // `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
13319
+ // parsing, which cannot instantiate an attacker-chosen type. Removing them
13320
+ // was previously blocked because the class-scoped rows below could not match
13321
+ // the IMPORTED call form (`use serde_json::from_str; from_str(&body)`); the
13322
+ // `::` path-separator fix in taint-matcher.ts makes them match, so the
13323
+ // classless rows are no longer load-bearing.
13251
13324
  // Rust XSS (actix-web, rocket, axum response body)
13252
13325
  { method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
13253
13326
  { method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -13644,10 +13717,19 @@ var DEFAULT_SINKS = [
13644
13717
  // C# XSS — raw HTML output (CWE-79).
13645
13718
  { method: "Raw", class: "Html", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13646
13719
  { method: "Write", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13720
+ // ASP.NET Core writes the response body through the `HttpResponseWritingExtensions`
13721
+ // extension methods rather than `Response.Write`, so the System.Web row above does
13722
+ // not cover it. Receiver-scoped to `Response` so the many unrelated `WriteAsync`
13723
+ // overloads (Stream, StreamWriter, TextWriter, PipeWriter) are untouched. (#503)
13724
+ { method: "WriteAsync", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13647
13725
  { method: "HtmlString", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13648
13726
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
13649
13727
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
13650
13728
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13729
+ // #503 (part 1): `Response.StatusDescription = "Bad " + data` writes
13730
+ // attacker text into the HTTP status line (CWE-81). Surfaced by the
13731
+ // property-assignment synthesiser in the C# extractor, receiver-gated there.
13732
+ { method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13651
13733
  // `return Content(html, "text/html")` on a controller writes arg 0 to the
13652
13734
  // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
13653
13735
  // `Content(constantString)` and the JSON/plain-text content types are
@@ -13662,6 +13744,12 @@ var DEFAULT_SINKS = [
13662
13744
  // filter is the constructor argument.
13663
13745
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13664
13746
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13747
+ // #336: a tainted `HttpClient.BaseAddress` gives the attacker the scheme,
13748
+ // host and port of every RELATIVE request made on that client, so the
13749
+ // request arguments can all be constant and the destination still be
13750
+ // attacker-chosen. Surfaced by the property-assignment synthesiser, which
13751
+ // class-gates it to a resolved `HttpClient`.
13752
+ { method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13665
13753
  // C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
13666
13754
  // cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
13667
13755
  // an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
@@ -16006,8 +16094,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
16006
16094
  return false;
16007
16095
  } else if (!call.receiver && !call.receiver_type) {
16008
16096
  const target = call.resolution?.target;
16097
+ const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
16009
16098
  const expectedTail = `${pattern.class}.${pattern.method}`;
16010
- if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
16099
+ if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
16011
16100
  } else {
16012
16101
  return false;
16013
16102
  }
@@ -320,6 +320,44 @@ function extractCSharpCalls(tree, cache) {
320
320
  });
321
321
  continue;
322
322
  }
323
+ // cognium-dev#336 — inside an OBJECT INITIALIZER the assignment's left side
324
+ // is a bare identifier, not a member access:
325
+ //
326
+ // new HttpClient { BaseAddress = new Uri(input) }
327
+ // object_creation_expression
328
+ // initializer_expression
329
+ // assignment_expression left = identifier 'BaseAddress'
330
+ //
331
+ // so the member-access branch below skipped it entirely. Surface it with
332
+ // the CONSTRUCTED TYPE as the receiver, which is what the class-scoped
333
+ // registry row needs to match.
334
+ if (left?.type === 'identifier' && asn.parent?.type === 'initializer_expression') {
335
+ const creation = asn.parent.parent;
336
+ if (creation?.type !== 'object_creation_expression')
337
+ continue;
338
+ const ctorType = creation.childForFieldName('type');
339
+ const right = asn.childForFieldName('right');
340
+ if (!ctorType || !right)
341
+ continue;
342
+ const typeName = csharpBareName(ctorType);
343
+ const rhsText = getNodeText(right);
344
+ calls.push({
345
+ method_name: getNodeText(left),
346
+ receiver: typeName,
347
+ receiver_type: typeName,
348
+ receiver_type_fqn: null,
349
+ arguments: [{
350
+ position: 0,
351
+ expression: rhsText,
352
+ variable: right.type === 'identifier' ? rhsText : null,
353
+ literal: CSHARP_LITERAL_NODE_TYPES.has(right.type) ? rhsText : null,
354
+ value: null,
355
+ }],
356
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
357
+ in_method: findEnclosingMethod(asn),
358
+ });
359
+ continue;
360
+ }
323
361
  if (left?.type !== 'member_access_expression')
324
362
  continue;
325
363
  const nameNode = left.childForFieldName('name');
@@ -337,6 +375,25 @@ function extractCSharpCalls(tree, cache) {
337
375
  if (recvType !== 'DirectorySearcher')
338
376
  continue;
339
377
  }
378
+ else if (propName === 'StatusDescription') {
379
+ // cognium-dev#503 (part 1) — `Response.StatusDescription = "Bad " + data`
380
+ // writes attacker text into the status line (CWE-81). Receiver-gated to
381
+ // a Response-shaped name so an unrelated `.StatusDescription =` on some
382
+ // other type is not surfaced.
383
+ const recv = exprNode ? getNodeText(exprNode) : '';
384
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv))
385
+ continue;
386
+ }
387
+ else if (propName === 'BaseAddress') {
388
+ // cognium-dev#336 — `new HttpClient { BaseAddress = new Uri(input) }`
389
+ // hands the attacker the scheme, host and port of every relative request
390
+ // on that client (CWE-918). Class-gated to a resolved HttpClient so an
391
+ // unrelated `.BaseAddress =` is not surfaced.
392
+ const recv = exprNode ? getNodeText(exprNode) : null;
393
+ const recvType = recv ? typeMap.get(recv) : undefined;
394
+ if (recvType !== 'HttpClient')
395
+ continue;
396
+ }
340
397
  else if (propName !== 'CommandText') {
341
398
  continue;
342
399
  }
@@ -360,8 +417,51 @@ function extractCSharpCalls(tree, cache) {
360
417
  in_method: findEnclosingMethod(asn),
361
418
  });
362
419
  }
420
+ // cognium-dev#340 — a C# CAST is not a call node, so `(MarkupString)html`
421
+ // could not be reached by any registry row while `new MarkupString(html)`
422
+ // could. The two are interchangeable in Blazor and the cast is the more
423
+ // common of the pair, since `@((MarkupString)html)` in a `.razor` component
424
+ // compiles from it. Emitted as a constructor-shaped call so the EXISTING
425
+ // `{ method: 'MarkupString', class: 'constructor' }` row matches, which keeps
426
+ // the taint engine in charge: a constant operand carries no taint and so
427
+ // still reports nothing.
428
+ //
429
+ // Restricted to an allowlist rather than surfacing every cast: `(string)x`,
430
+ // `(int)x` and friends are ubiquitous and must not become synthetic calls.
431
+ const casts = getNodesFromCache(tree.rootNode, 'cast_expression', cache);
432
+ for (const cast of casts) {
433
+ const typeNode = cast.childForFieldName('type');
434
+ const valueNode = cast.childForFieldName('value');
435
+ if (!typeNode || !valueNode)
436
+ continue;
437
+ const typeName = csharpBareName(typeNode);
438
+ if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName))
439
+ continue;
440
+ const valueText = getNodeText(valueNode);
441
+ calls.push({
442
+ method_name: typeName,
443
+ receiver: null,
444
+ receiver_type: null,
445
+ receiver_type_fqn: null,
446
+ arguments: [{
447
+ position: 0,
448
+ expression: valueText,
449
+ variable: valueNode.type === 'identifier' ? valueText : null,
450
+ literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
451
+ value: null,
452
+ }],
453
+ location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
454
+ in_method: findEnclosingMethod(cast),
455
+ is_constructor: true,
456
+ });
457
+ }
363
458
  return calls;
364
459
  }
460
+ /**
461
+ * Cast target types whose cast renders its operand as raw markup, so the cast
462
+ * is exactly as dangerous as the constructor. Deliberately tiny — see #340.
463
+ */
464
+ const CSHARP_RAW_MARKUP_CAST_TYPES = new Set(['MarkupString']);
365
465
  /** Extract args from a C# `argument_list` (each child is an `argument` wrapper). */
366
466
  function extractCSharpArguments(argsNode) {
367
467
  const args = [];
@@ -384,12 +484,38 @@ function extractCSharpArguments(argsNode) {
384
484
  position: position++,
385
485
  expression: text,
386
486
  variable: expr?.type === 'identifier' ? text : null,
387
- literal: expr?.type === 'string_literal' ? text : null,
487
+ literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
388
488
  value: null,
389
489
  });
390
490
  }
391
491
  return args;
392
492
  }
493
+ /**
494
+ * C# compile-time literal node types — cognium-dev#502.
495
+ *
496
+ * Only `string_literal` was recognised, so `null`, `true`, `42` and `'c'` all
497
+ * came back with `literal: null` and were indistinguishable from a variable.
498
+ * A sink taint-gated on that argument therefore registered: the measured case
499
+ * is Juliet's `new SqlCommand(null, connection)` DB-setup helper, reported as
500
+ * CWE-89 with a SQL argument that is literally `null` — 1,523 signatures over
501
+ * the 10 scored CWE families once #502 gave those files a source.
502
+ *
503
+ * A compile-time constant cannot carry taint, so registering a sink on one is
504
+ * always wrong; widening this removes false positives only.
505
+ *
506
+ * `interpolated_string_expression` is deliberately ABSENT: `$"SELECT {input}"`
507
+ * interpolates and is the genuine C# injection shape.
508
+ */
509
+ const CSHARP_LITERAL_NODE_TYPES = new Set([
510
+ 'string_literal',
511
+ 'verbatim_string_literal',
512
+ 'raw_string_literal',
513
+ 'character_literal',
514
+ 'integer_literal',
515
+ 'real_literal',
516
+ 'boolean_literal',
517
+ 'null_literal',
518
+ ]);
393
519
  function extractJavaScriptCalls(tree, cache) {
394
520
  const calls = [];
395
521
  // Build JS resolution context