circle-ir 4.8.2 → 4.9.8

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 (29) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +11 -0
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/passes/insecure-cookie-pass.d.ts +1 -0
  5. package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
  6. package/dist/analysis/passes/insecure-cookie-pass.js +37 -3
  7. package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
  8. package/dist/analysis/passes/jwt-verify-disabled-pass.d.ts.map +1 -1
  9. package/dist/analysis/passes/jwt-verify-disabled-pass.js +48 -21
  10. package/dist/analysis/passes/jwt-verify-disabled-pass.js.map +1 -1
  11. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/language-sources-pass.js +131 -0
  13. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  14. package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
  15. package/dist/analysis/passes/tls-verify-disabled-pass.js +34 -0
  16. package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
  17. package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -1
  18. package/dist/analysis/passes/weak-crypto-pass.js +32 -19
  19. package/dist/analysis/passes/weak-crypto-pass.js.map +1 -1
  20. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  21. package/dist/analysis/taint-matcher.js +237 -3
  22. package/dist/analysis/taint-matcher.js.map +1 -1
  23. package/dist/browser/circle-ir.js +409 -43
  24. package/dist/core/circle-ir-core.cjs +218 -3
  25. package/dist/core/circle-ir-core.js +218 -3
  26. package/dist/core/extractors/types.js +77 -2
  27. package/dist/core/extractors/types.js.map +1 -1
  28. package/dist/wasm/web-tree-sitter.wasm +0 -0
  29. package/package.json +1 -1
@@ -4421,6 +4421,7 @@ function extractCSharpTypes(tree, cache) {
4421
4421
  const nameNode = node.childForFieldName("name");
4422
4422
  const body2 = node.childForFieldName("body");
4423
4423
  const methods = [];
4424
+ const fields = extractCSharpFields(body2);
4424
4425
  if (body2) {
4425
4426
  for (let i2 = 0; i2 < body2.childCount; i2++) {
4426
4427
  const m = body2.child(i2);
@@ -4438,7 +4439,7 @@ function extractCSharpTypes(tree, cache) {
4438
4439
  parameters.push({
4439
4440
  name: getNodeText(pName),
4440
4441
  type: pType ? getNodeText(pType) : null,
4441
- annotations: [],
4442
+ annotations: extractCSharpParamAnnotations(pnode),
4442
4443
  line: pnode.startPosition.row + 1
4443
4444
  });
4444
4445
  }
@@ -4464,7 +4465,7 @@ function extractCSharpTypes(tree, cache) {
4464
4465
  implements: [],
4465
4466
  annotations: [],
4466
4467
  methods,
4467
- fields: [],
4468
+ fields,
4468
4469
  start_line: node.startPosition.row + 1,
4469
4470
  end_line: node.endPosition.row + 1
4470
4471
  });
@@ -4472,6 +4473,59 @@ function extractCSharpTypes(tree, cache) {
4472
4473
  }
4473
4474
  return types;
4474
4475
  }
4476
+ function extractCSharpFields(body2) {
4477
+ const fields = [];
4478
+ if (!body2) return fields;
4479
+ for (let i2 = 0; i2 < body2.childCount; i2++) {
4480
+ const c = body2.child(i2);
4481
+ if (!c) continue;
4482
+ if (c.type === "field_declaration") {
4483
+ let varDecl = null;
4484
+ for (let k = 0; k < c.childCount; k++) {
4485
+ const cc = c.child(k);
4486
+ if (cc?.type === "variable_declaration") {
4487
+ varDecl = cc;
4488
+ break;
4489
+ }
4490
+ }
4491
+ if (!varDecl) continue;
4492
+ const typeNode = varDecl.childForFieldName("type");
4493
+ const type = typeNode ? getNodeText(typeNode) : null;
4494
+ const modifiers = extractCSharpModifiers(c);
4495
+ for (let k = 0; k < varDecl.childCount; k++) {
4496
+ const decl = varDecl.child(k);
4497
+ if (decl?.type !== "variable_declarator") continue;
4498
+ const nameNode = decl.childForFieldName("name");
4499
+ fields.push({ name: nameNode ? getNodeText(nameNode) : "unknown", type, modifiers, annotations: [] });
4500
+ }
4501
+ } else if (c.type === "property_declaration") {
4502
+ const nameNode = c.childForFieldName("name");
4503
+ if (!nameNode) continue;
4504
+ const typeNode = c.childForFieldName("type");
4505
+ fields.push({
4506
+ name: getNodeText(nameNode),
4507
+ type: typeNode ? getNodeText(typeNode) : null,
4508
+ modifiers: extractCSharpModifiers(c),
4509
+ annotations: []
4510
+ });
4511
+ }
4512
+ }
4513
+ return fields;
4514
+ }
4515
+ function extractCSharpParamAnnotations(param) {
4516
+ const out2 = [];
4517
+ for (let i2 = 0; i2 < param.childCount; i2++) {
4518
+ const list = param.child(i2);
4519
+ if (list?.type !== "attribute_list") continue;
4520
+ for (let j = 0; j < list.childCount; j++) {
4521
+ const attr = list.child(j);
4522
+ if (attr?.type !== "attribute") continue;
4523
+ const name2 = attr.childForFieldName("name");
4524
+ if (name2) out2.push(getNodeText(name2));
4525
+ }
4526
+ }
4527
+ return out2;
4528
+ }
4475
4529
  function extractCSharpModifiers(node) {
4476
4530
  const mods = [];
4477
4531
  for (let i2 = 0; i2 < node.childCount; i2++) {
@@ -13345,6 +13399,17 @@ var DEFAULT_SINKS = [
13345
13399
  { method: "Load", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13346
13400
  { method: "LoadFrom", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13347
13401
  { method: "LoadFile", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13402
+ // C# server-side template injection (SSTI). Compiling an attacker-controlled
13403
+ // template string is arbitrary code execution — classified as code_injection
13404
+ // (CWE-94), matching how Python (Jinja2/Mako) and Node SSTI are modelled.
13405
+ // Restricted to DISTINCTIVE template-compile APIs so the generic
13406
+ // `Template.Parse` / `.Compile` names (int.Parse, Regex.Compile, …) are not
13407
+ // over-matched. Taint-gated: a constant template never fires. `RunCompile`
13408
+ // = RazorEngine; `CompileRenderStringAsync` = RazorLight; `Compile` is
13409
+ // class-scoped to Handlebars. (cognium-dev#273)
13410
+ { method: "RunCompile", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13411
+ { method: "CompileRenderStringAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
13412
+ { method: "Compile", class: "Handlebars", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13348
13413
  // C# insecure deserialization — the polymorphic BCL formatters that can
13349
13414
  // instantiate arbitrary types named in the payload (CWE-502, cognium-ai#318).
13350
13415
  // Each of these classes exists only to deserialize and is unsafe on untrusted
@@ -14377,7 +14442,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
14377
14442
  const skipMethods = ["toString", "hashCode", "equals", "compareTo"];
14378
14443
  if (skipMethods.includes(method.name)) continue;
14379
14444
  for (const param of method.parameters) {
14380
- const isTaintable = param.type ? isInterproceduralTaintableType(param.type, language) : true;
14445
+ const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
14446
+ const isTaintable = hasCSharpBindingAttr ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
14381
14447
  if (isTaintable) {
14382
14448
  const paramLine = param.line ?? method.start_line;
14383
14449
  sources.push({
@@ -14497,6 +14563,13 @@ function findSources(calls, types, patterns, sourceLines, language) {
14497
14563
  }
14498
14564
  return result;
14499
14565
  }
14566
+ var CSHARP_BINDING_ATTRS = /* @__PURE__ */ new Set([
14567
+ "FromBody",
14568
+ "FromQuery",
14569
+ "FromRoute",
14570
+ "FromForm",
14571
+ "FromHeader"
14572
+ ]);
14500
14573
  function isInterproceduralTaintableType(typeName, language) {
14501
14574
  const baseType = typeName.split("<")[0].trim();
14502
14575
  const excludedTypes = [
@@ -14786,6 +14859,145 @@ function isSafeCSharpProcessStartCall(call, pattern, language, sourceLines) {
14786
14859
  }
14787
14860
  return false;
14788
14861
  }
14862
+ function stripCsLiterals(line) {
14863
+ return line.replace(/\/\/.*$/, "").replace(/@?"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)'/g, "''");
14864
+ }
14865
+ function csBraceDepthBefore(lines, idx) {
14866
+ let depth = 0;
14867
+ for (let i2 = 0; i2 < idx && i2 < lines.length; i2++) {
14868
+ for (const ch of stripCsLiterals(lines[i2])) {
14869
+ if (ch === "{") depth++;
14870
+ else if (ch === "}") depth--;
14871
+ }
14872
+ }
14873
+ return depth;
14874
+ }
14875
+ function netBraces(fragment) {
14876
+ let n = 0;
14877
+ for (const ch of fragment) {
14878
+ if (ch === "{") n++;
14879
+ else if (ch === "}") n--;
14880
+ }
14881
+ return n;
14882
+ }
14883
+ function splitCsIf(line) {
14884
+ const m = /\bif\s*\(/.exec(line);
14885
+ if (!m) return null;
14886
+ const ifCol = m.index;
14887
+ let i2 = m.index + m[0].length;
14888
+ let depth = 1;
14889
+ let inStr = false;
14890
+ let strCh = "";
14891
+ for (; i2 < line.length; i2++) {
14892
+ const c = line[i2];
14893
+ if (inStr) {
14894
+ if (c === "\\") {
14895
+ i2++;
14896
+ continue;
14897
+ }
14898
+ if (c === strCh) inStr = false;
14899
+ continue;
14900
+ }
14901
+ if (c === '"' || c === "'") {
14902
+ inStr = true;
14903
+ strCh = c;
14904
+ continue;
14905
+ }
14906
+ if (c === "(") depth++;
14907
+ else if (c === ")") {
14908
+ depth--;
14909
+ if (depth === 0) break;
14910
+ }
14911
+ }
14912
+ if (depth !== 0) return null;
14913
+ return { cond: line.slice(m.index + m[0].length, i2), rest: line.slice(i2 + 1), ifCol };
14914
+ }
14915
+ function csEqualityGuard(cond) {
14916
+ const c = cond.trim();
14917
+ let m = /^(.*?)\s*(==|!=)\s*@?"[^"]*"\s*$/.exec(c);
14918
+ if (m) return { op: m[2], exprSide: m[1].trim() };
14919
+ m = /^@?"[^"]*"\s*(==|!=)\s*(.*)$/.exec(c);
14920
+ if (m) return { op: m[1], exprSide: m[2].trim() };
14921
+ return null;
14922
+ }
14923
+ var CS_IDENT_RE = /[A-Za-z_]\w*/g;
14924
+ function csIdentifiers(expr) {
14925
+ return new Set(expr.match(CS_IDENT_RE) ?? []);
14926
+ }
14927
+ function csThenBlock(lines, ifIdx, rest) {
14928
+ const isExit = (s) => /^\s*(?:return|throw|continue|break)\b/.test(s);
14929
+ let firstIdx;
14930
+ let firstText;
14931
+ if (rest.trim()) {
14932
+ firstIdx = ifIdx;
14933
+ firstText = rest;
14934
+ } else {
14935
+ let j = ifIdx + 1;
14936
+ while (j < lines.length && stripCsLiterals(lines[j]).trim() === "") j++;
14937
+ firstIdx = j;
14938
+ firstText = lines[j] ?? "";
14939
+ }
14940
+ if (firstText.trim().startsWith("{")) {
14941
+ let depth = 0;
14942
+ let started = false;
14943
+ let endLine = lines.length - 1;
14944
+ let body2 = "";
14945
+ for (let i2 = firstIdx; i2 < lines.length; i2++) {
14946
+ const stripped = stripCsLiterals(i2 === firstIdx ? firstText : lines[i2]);
14947
+ let broke = false;
14948
+ for (const ch of stripped) {
14949
+ if (ch === "{") {
14950
+ depth++;
14951
+ started = true;
14952
+ if (depth === 1) continue;
14953
+ } else if (ch === "}") {
14954
+ depth--;
14955
+ if (depth === 0) {
14956
+ endLine = i2;
14957
+ broke = true;
14958
+ break;
14959
+ }
14960
+ }
14961
+ if (started && depth >= 1) body2 += ch;
14962
+ }
14963
+ if (broke) break;
14964
+ if (started) body2 += " ";
14965
+ }
14966
+ return { start: ifIdx, end: endLine, earlyExit: isExit(body2.trim()) };
14967
+ }
14968
+ return { start: ifIdx, end: firstIdx, earlyExit: isExit(firstText.trim()) };
14969
+ }
14970
+ function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines) {
14971
+ if (language !== "csharp") return false;
14972
+ if (pattern.type !== "ssrf") return false;
14973
+ if (!sourceLines || sourceLines.length === 0) return false;
14974
+ const candidates = /* @__PURE__ */ new Set();
14975
+ for (const a of call.arguments) {
14976
+ if (a.variable) candidates.add(a.variable);
14977
+ for (const id of csIdentifiers(a.expression ?? "")) candidates.add(id);
14978
+ }
14979
+ if (candidates.size === 0) return false;
14980
+ const sinkIdx = call.location.line - 1;
14981
+ const sinkDepth = csBraceDepthBefore(sourceLines, sinkIdx);
14982
+ for (let i2 = 0; i2 < sinkIdx; i2++) {
14983
+ const parts2 = splitCsIf(sourceLines[i2]);
14984
+ if (!parts2) continue;
14985
+ const guard = csEqualityGuard(parts2.cond);
14986
+ if (!guard) continue;
14987
+ const guardIds = csIdentifiers(guard.exprSide);
14988
+ if (![...guardIds].some((id) => candidates.has(id))) continue;
14989
+ const block = csThenBlock(sourceLines, i2, parts2.rest);
14990
+ if (guard.op === "==") {
14991
+ if (sinkIdx >= block.start && sinkIdx <= block.end) return true;
14992
+ } else {
14993
+ const guardDepth = csBraceDepthBefore(sourceLines, i2) + netBraces(stripCsLiterals(sourceLines[i2]).slice(0, parts2.ifCol));
14994
+ if (block.earlyExit && sinkIdx > block.end && guardDepth === sinkDepth) {
14995
+ return true;
14996
+ }
14997
+ }
14998
+ }
14999
+ return false;
15000
+ }
14789
15001
  function isSafeRustCommandCall(call, pattern, language) {
14790
15002
  if (language !== "rust") return false;
14791
15003
  if (pattern.type !== "command_injection") return false;
@@ -15044,6 +15256,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
15044
15256
  if (isSafeCSharpProcessStartCall(call, pattern, language, sourceLines)) {
15045
15257
  continue;
15046
15258
  }
15259
+ if (isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines)) {
15260
+ continue;
15261
+ }
15047
15262
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at, types)) {
15048
15263
  continue;
15049
15264
  }
@@ -4355,6 +4355,7 @@ function extractCSharpTypes(tree, cache) {
4355
4355
  const nameNode = node.childForFieldName("name");
4356
4356
  const body2 = node.childForFieldName("body");
4357
4357
  const methods = [];
4358
+ const fields = extractCSharpFields(body2);
4358
4359
  if (body2) {
4359
4360
  for (let i2 = 0; i2 < body2.childCount; i2++) {
4360
4361
  const m = body2.child(i2);
@@ -4372,7 +4373,7 @@ function extractCSharpTypes(tree, cache) {
4372
4373
  parameters.push({
4373
4374
  name: getNodeText(pName),
4374
4375
  type: pType ? getNodeText(pType) : null,
4375
- annotations: [],
4376
+ annotations: extractCSharpParamAnnotations(pnode),
4376
4377
  line: pnode.startPosition.row + 1
4377
4378
  });
4378
4379
  }
@@ -4398,7 +4399,7 @@ function extractCSharpTypes(tree, cache) {
4398
4399
  implements: [],
4399
4400
  annotations: [],
4400
4401
  methods,
4401
- fields: [],
4402
+ fields,
4402
4403
  start_line: node.startPosition.row + 1,
4403
4404
  end_line: node.endPosition.row + 1
4404
4405
  });
@@ -4406,6 +4407,59 @@ function extractCSharpTypes(tree, cache) {
4406
4407
  }
4407
4408
  return types;
4408
4409
  }
4410
+ function extractCSharpFields(body2) {
4411
+ const fields = [];
4412
+ if (!body2) return fields;
4413
+ for (let i2 = 0; i2 < body2.childCount; i2++) {
4414
+ const c = body2.child(i2);
4415
+ if (!c) continue;
4416
+ if (c.type === "field_declaration") {
4417
+ let varDecl = null;
4418
+ for (let k = 0; k < c.childCount; k++) {
4419
+ const cc = c.child(k);
4420
+ if (cc?.type === "variable_declaration") {
4421
+ varDecl = cc;
4422
+ break;
4423
+ }
4424
+ }
4425
+ if (!varDecl) continue;
4426
+ const typeNode = varDecl.childForFieldName("type");
4427
+ const type = typeNode ? getNodeText(typeNode) : null;
4428
+ const modifiers = extractCSharpModifiers(c);
4429
+ for (let k = 0; k < varDecl.childCount; k++) {
4430
+ const decl = varDecl.child(k);
4431
+ if (decl?.type !== "variable_declarator") continue;
4432
+ const nameNode = decl.childForFieldName("name");
4433
+ fields.push({ name: nameNode ? getNodeText(nameNode) : "unknown", type, modifiers, annotations: [] });
4434
+ }
4435
+ } else if (c.type === "property_declaration") {
4436
+ const nameNode = c.childForFieldName("name");
4437
+ if (!nameNode) continue;
4438
+ const typeNode = c.childForFieldName("type");
4439
+ fields.push({
4440
+ name: getNodeText(nameNode),
4441
+ type: typeNode ? getNodeText(typeNode) : null,
4442
+ modifiers: extractCSharpModifiers(c),
4443
+ annotations: []
4444
+ });
4445
+ }
4446
+ }
4447
+ return fields;
4448
+ }
4449
+ function extractCSharpParamAnnotations(param) {
4450
+ const out2 = [];
4451
+ for (let i2 = 0; i2 < param.childCount; i2++) {
4452
+ const list = param.child(i2);
4453
+ if (list?.type !== "attribute_list") continue;
4454
+ for (let j = 0; j < list.childCount; j++) {
4455
+ const attr = list.child(j);
4456
+ if (attr?.type !== "attribute") continue;
4457
+ const name2 = attr.childForFieldName("name");
4458
+ if (name2) out2.push(getNodeText(name2));
4459
+ }
4460
+ }
4461
+ return out2;
4462
+ }
4409
4463
  function extractCSharpModifiers(node) {
4410
4464
  const mods = [];
4411
4465
  for (let i2 = 0; i2 < node.childCount; i2++) {
@@ -13279,6 +13333,17 @@ var DEFAULT_SINKS = [
13279
13333
  { method: "Load", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13280
13334
  { method: "LoadFrom", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13281
13335
  { method: "LoadFile", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13336
+ // C# server-side template injection (SSTI). Compiling an attacker-controlled
13337
+ // template string is arbitrary code execution — classified as code_injection
13338
+ // (CWE-94), matching how Python (Jinja2/Mako) and Node SSTI are modelled.
13339
+ // Restricted to DISTINCTIVE template-compile APIs so the generic
13340
+ // `Template.Parse` / `.Compile` names (int.Parse, Regex.Compile, …) are not
13341
+ // over-matched. Taint-gated: a constant template never fires. `RunCompile`
13342
+ // = RazorEngine; `CompileRenderStringAsync` = RazorLight; `Compile` is
13343
+ // class-scoped to Handlebars. (cognium-dev#273)
13344
+ { method: "RunCompile", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13345
+ { method: "CompileRenderStringAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
13346
+ { method: "Compile", class: "Handlebars", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13282
13347
  // C# insecure deserialization — the polymorphic BCL formatters that can
13283
13348
  // instantiate arbitrary types named in the payload (CWE-502, cognium-ai#318).
13284
13349
  // Each of these classes exists only to deserialize and is unsafe on untrusted
@@ -14311,7 +14376,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
14311
14376
  const skipMethods = ["toString", "hashCode", "equals", "compareTo"];
14312
14377
  if (skipMethods.includes(method.name)) continue;
14313
14378
  for (const param of method.parameters) {
14314
- const isTaintable = param.type ? isInterproceduralTaintableType(param.type, language) : true;
14379
+ const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
14380
+ const isTaintable = hasCSharpBindingAttr ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
14315
14381
  if (isTaintable) {
14316
14382
  const paramLine = param.line ?? method.start_line;
14317
14383
  sources.push({
@@ -14431,6 +14497,13 @@ function findSources(calls, types, patterns, sourceLines, language) {
14431
14497
  }
14432
14498
  return result;
14433
14499
  }
14500
+ var CSHARP_BINDING_ATTRS = /* @__PURE__ */ new Set([
14501
+ "FromBody",
14502
+ "FromQuery",
14503
+ "FromRoute",
14504
+ "FromForm",
14505
+ "FromHeader"
14506
+ ]);
14434
14507
  function isInterproceduralTaintableType(typeName, language) {
14435
14508
  const baseType = typeName.split("<")[0].trim();
14436
14509
  const excludedTypes = [
@@ -14720,6 +14793,145 @@ function isSafeCSharpProcessStartCall(call, pattern, language, sourceLines) {
14720
14793
  }
14721
14794
  return false;
14722
14795
  }
14796
+ function stripCsLiterals(line) {
14797
+ return line.replace(/\/\/.*$/, "").replace(/@?"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)'/g, "''");
14798
+ }
14799
+ function csBraceDepthBefore(lines, idx) {
14800
+ let depth = 0;
14801
+ for (let i2 = 0; i2 < idx && i2 < lines.length; i2++) {
14802
+ for (const ch of stripCsLiterals(lines[i2])) {
14803
+ if (ch === "{") depth++;
14804
+ else if (ch === "}") depth--;
14805
+ }
14806
+ }
14807
+ return depth;
14808
+ }
14809
+ function netBraces(fragment) {
14810
+ let n = 0;
14811
+ for (const ch of fragment) {
14812
+ if (ch === "{") n++;
14813
+ else if (ch === "}") n--;
14814
+ }
14815
+ return n;
14816
+ }
14817
+ function splitCsIf(line) {
14818
+ const m = /\bif\s*\(/.exec(line);
14819
+ if (!m) return null;
14820
+ const ifCol = m.index;
14821
+ let i2 = m.index + m[0].length;
14822
+ let depth = 1;
14823
+ let inStr = false;
14824
+ let strCh = "";
14825
+ for (; i2 < line.length; i2++) {
14826
+ const c = line[i2];
14827
+ if (inStr) {
14828
+ if (c === "\\") {
14829
+ i2++;
14830
+ continue;
14831
+ }
14832
+ if (c === strCh) inStr = false;
14833
+ continue;
14834
+ }
14835
+ if (c === '"' || c === "'") {
14836
+ inStr = true;
14837
+ strCh = c;
14838
+ continue;
14839
+ }
14840
+ if (c === "(") depth++;
14841
+ else if (c === ")") {
14842
+ depth--;
14843
+ if (depth === 0) break;
14844
+ }
14845
+ }
14846
+ if (depth !== 0) return null;
14847
+ return { cond: line.slice(m.index + m[0].length, i2), rest: line.slice(i2 + 1), ifCol };
14848
+ }
14849
+ function csEqualityGuard(cond) {
14850
+ const c = cond.trim();
14851
+ let m = /^(.*?)\s*(==|!=)\s*@?"[^"]*"\s*$/.exec(c);
14852
+ if (m) return { op: m[2], exprSide: m[1].trim() };
14853
+ m = /^@?"[^"]*"\s*(==|!=)\s*(.*)$/.exec(c);
14854
+ if (m) return { op: m[1], exprSide: m[2].trim() };
14855
+ return null;
14856
+ }
14857
+ var CS_IDENT_RE = /[A-Za-z_]\w*/g;
14858
+ function csIdentifiers(expr) {
14859
+ return new Set(expr.match(CS_IDENT_RE) ?? []);
14860
+ }
14861
+ function csThenBlock(lines, ifIdx, rest) {
14862
+ const isExit = (s) => /^\s*(?:return|throw|continue|break)\b/.test(s);
14863
+ let firstIdx;
14864
+ let firstText;
14865
+ if (rest.trim()) {
14866
+ firstIdx = ifIdx;
14867
+ firstText = rest;
14868
+ } else {
14869
+ let j = ifIdx + 1;
14870
+ while (j < lines.length && stripCsLiterals(lines[j]).trim() === "") j++;
14871
+ firstIdx = j;
14872
+ firstText = lines[j] ?? "";
14873
+ }
14874
+ if (firstText.trim().startsWith("{")) {
14875
+ let depth = 0;
14876
+ let started = false;
14877
+ let endLine = lines.length - 1;
14878
+ let body2 = "";
14879
+ for (let i2 = firstIdx; i2 < lines.length; i2++) {
14880
+ const stripped = stripCsLiterals(i2 === firstIdx ? firstText : lines[i2]);
14881
+ let broke = false;
14882
+ for (const ch of stripped) {
14883
+ if (ch === "{") {
14884
+ depth++;
14885
+ started = true;
14886
+ if (depth === 1) continue;
14887
+ } else if (ch === "}") {
14888
+ depth--;
14889
+ if (depth === 0) {
14890
+ endLine = i2;
14891
+ broke = true;
14892
+ break;
14893
+ }
14894
+ }
14895
+ if (started && depth >= 1) body2 += ch;
14896
+ }
14897
+ if (broke) break;
14898
+ if (started) body2 += " ";
14899
+ }
14900
+ return { start: ifIdx, end: endLine, earlyExit: isExit(body2.trim()) };
14901
+ }
14902
+ return { start: ifIdx, end: firstIdx, earlyExit: isExit(firstText.trim()) };
14903
+ }
14904
+ function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines) {
14905
+ if (language !== "csharp") return false;
14906
+ if (pattern.type !== "ssrf") return false;
14907
+ if (!sourceLines || sourceLines.length === 0) return false;
14908
+ const candidates = /* @__PURE__ */ new Set();
14909
+ for (const a of call.arguments) {
14910
+ if (a.variable) candidates.add(a.variable);
14911
+ for (const id of csIdentifiers(a.expression ?? "")) candidates.add(id);
14912
+ }
14913
+ if (candidates.size === 0) return false;
14914
+ const sinkIdx = call.location.line - 1;
14915
+ const sinkDepth = csBraceDepthBefore(sourceLines, sinkIdx);
14916
+ for (let i2 = 0; i2 < sinkIdx; i2++) {
14917
+ const parts2 = splitCsIf(sourceLines[i2]);
14918
+ if (!parts2) continue;
14919
+ const guard = csEqualityGuard(parts2.cond);
14920
+ if (!guard) continue;
14921
+ const guardIds = csIdentifiers(guard.exprSide);
14922
+ if (![...guardIds].some((id) => candidates.has(id))) continue;
14923
+ const block = csThenBlock(sourceLines, i2, parts2.rest);
14924
+ if (guard.op === "==") {
14925
+ if (sinkIdx >= block.start && sinkIdx <= block.end) return true;
14926
+ } else {
14927
+ const guardDepth = csBraceDepthBefore(sourceLines, i2) + netBraces(stripCsLiterals(sourceLines[i2]).slice(0, parts2.ifCol));
14928
+ if (block.earlyExit && sinkIdx > block.end && guardDepth === sinkDepth) {
14929
+ return true;
14930
+ }
14931
+ }
14932
+ }
14933
+ return false;
14934
+ }
14723
14935
  function isSafeRustCommandCall(call, pattern, language) {
14724
14936
  if (language !== "rust") return false;
14725
14937
  if (pattern.type !== "command_injection") return false;
@@ -14978,6 +15190,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
14978
15190
  if (isSafeCSharpProcessStartCall(call, pattern, language, sourceLines)) {
14979
15191
  continue;
14980
15192
  }
15193
+ if (isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines)) {
15194
+ continue;
15195
+ }
14981
15196
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at, types)) {
14982
15197
  continue;
14983
15198
  }
@@ -94,6 +94,7 @@ function extractCSharpTypes(tree, cache) {
94
94
  const nameNode = node.childForFieldName('name');
95
95
  const body = node.childForFieldName('body');
96
96
  const methods = [];
97
+ const fields = extractCSharpFields(body);
97
98
  if (body) {
98
99
  for (let i = 0; i < body.childCount; i++) {
99
100
  const m = body.child(i);
@@ -113,7 +114,7 @@ function extractCSharpTypes(tree, cache) {
113
114
  parameters.push({
114
115
  name: getNodeText(pName),
115
116
  type: pType ? getNodeText(pType) : null,
116
- annotations: [],
117
+ annotations: extractCSharpParamAnnotations(pnode),
117
118
  line: pnode.startPosition.row + 1,
118
119
  });
119
120
  }
@@ -139,7 +140,7 @@ function extractCSharpTypes(tree, cache) {
139
140
  implements: [],
140
141
  annotations: [],
141
142
  methods,
142
- fields: [],
143
+ fields,
143
144
  start_line: node.startPosition.row + 1,
144
145
  end_line: node.endPosition.row + 1,
145
146
  });
@@ -147,6 +148,80 @@ function extractCSharpTypes(tree, cache) {
147
148
  }
148
149
  return types;
149
150
  }
151
+ /**
152
+ * Extract C# instance state — `field_declaration`s (incl. multi-declarator
153
+ * `int a, b;`) and `property_declaration`s (auto-properties hold state too).
154
+ * Both are class members that methods reference by name, so both count toward
155
+ * cohesion metrics (LCOM). Returns [] for a null body (e.g. expression-bodied
156
+ * or partial declarations without an inline body).
157
+ */
158
+ function extractCSharpFields(body) {
159
+ const fields = [];
160
+ if (!body)
161
+ return fields;
162
+ for (let i = 0; i < body.childCount; i++) {
163
+ const c = body.child(i);
164
+ if (!c)
165
+ continue;
166
+ if (c.type === 'field_declaration') {
167
+ let varDecl = null;
168
+ for (let k = 0; k < c.childCount; k++) {
169
+ const cc = c.child(k);
170
+ if (cc?.type === 'variable_declaration') {
171
+ varDecl = cc;
172
+ break;
173
+ }
174
+ }
175
+ if (!varDecl)
176
+ continue;
177
+ const typeNode = varDecl.childForFieldName('type');
178
+ const type = typeNode ? getNodeText(typeNode) : null;
179
+ const modifiers = extractCSharpModifiers(c);
180
+ for (let k = 0; k < varDecl.childCount; k++) {
181
+ const decl = varDecl.child(k);
182
+ if (decl?.type !== 'variable_declarator')
183
+ continue;
184
+ const nameNode = decl.childForFieldName('name');
185
+ fields.push({ name: nameNode ? getNodeText(nameNode) : 'unknown', type, modifiers, annotations: [] });
186
+ }
187
+ }
188
+ else if (c.type === 'property_declaration') {
189
+ const nameNode = c.childForFieldName('name');
190
+ if (!nameNode)
191
+ continue;
192
+ const typeNode = c.childForFieldName('type');
193
+ fields.push({
194
+ name: getNodeText(nameNode),
195
+ type: typeNode ? getNodeText(typeNode) : null,
196
+ modifiers: extractCSharpModifiers(c),
197
+ annotations: [],
198
+ });
199
+ }
200
+ }
201
+ return fields;
202
+ }
203
+ /**
204
+ * Attribute names on a C# `parameter` node — e.g. `[FromBody]` / `[FromQuery]`
205
+ * → `['FromBody']` / `['FromQuery']`. Used to recognise ASP.NET model-binding
206
+ * parameters (including DTO types) as taint sources.
207
+ */
208
+ function extractCSharpParamAnnotations(param) {
209
+ const out = [];
210
+ for (let i = 0; i < param.childCount; i++) {
211
+ const list = param.child(i);
212
+ if (list?.type !== 'attribute_list')
213
+ continue;
214
+ for (let j = 0; j < list.childCount; j++) {
215
+ const attr = list.child(j);
216
+ if (attr?.type !== 'attribute')
217
+ continue;
218
+ const name = attr.childForFieldName('name');
219
+ if (name)
220
+ out.push(getNodeText(name));
221
+ }
222
+ }
223
+ return out;
224
+ }
150
225
  /** Leading `modifier` tokens (public/static/…) of a C# declaration node. */
151
226
  function extractCSharpModifiers(node) {
152
227
  const mods = [];