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
@@ -4375,6 +4375,7 @@ function extractCSharpTypes(tree, cache) {
4375
4375
  const nameNode = node.childForFieldName("name");
4376
4376
  const body2 = node.childForFieldName("body");
4377
4377
  const methods = [];
4378
+ const fields = extractCSharpFields(body2);
4378
4379
  if (body2) {
4379
4380
  for (let i2 = 0; i2 < body2.childCount; i2++) {
4380
4381
  const m = body2.child(i2);
@@ -4392,7 +4393,7 @@ function extractCSharpTypes(tree, cache) {
4392
4393
  parameters.push({
4393
4394
  name: getNodeText(pName),
4394
4395
  type: pType ? getNodeText(pType) : null,
4395
- annotations: [],
4396
+ annotations: extractCSharpParamAnnotations(pnode),
4396
4397
  line: pnode.startPosition.row + 1
4397
4398
  });
4398
4399
  }
@@ -4418,7 +4419,7 @@ function extractCSharpTypes(tree, cache) {
4418
4419
  implements: [],
4419
4420
  annotations: [],
4420
4421
  methods,
4421
- fields: [],
4422
+ fields,
4422
4423
  start_line: node.startPosition.row + 1,
4423
4424
  end_line: node.endPosition.row + 1
4424
4425
  });
@@ -4426,6 +4427,59 @@ function extractCSharpTypes(tree, cache) {
4426
4427
  }
4427
4428
  return types;
4428
4429
  }
4430
+ function extractCSharpFields(body2) {
4431
+ const fields = [];
4432
+ if (!body2) return fields;
4433
+ for (let i2 = 0; i2 < body2.childCount; i2++) {
4434
+ const c = body2.child(i2);
4435
+ if (!c) continue;
4436
+ if (c.type === "field_declaration") {
4437
+ let varDecl = null;
4438
+ for (let k = 0; k < c.childCount; k++) {
4439
+ const cc = c.child(k);
4440
+ if (cc?.type === "variable_declaration") {
4441
+ varDecl = cc;
4442
+ break;
4443
+ }
4444
+ }
4445
+ if (!varDecl) continue;
4446
+ const typeNode = varDecl.childForFieldName("type");
4447
+ const type = typeNode ? getNodeText(typeNode) : null;
4448
+ const modifiers = extractCSharpModifiers(c);
4449
+ for (let k = 0; k < varDecl.childCount; k++) {
4450
+ const decl = varDecl.child(k);
4451
+ if (decl?.type !== "variable_declarator") continue;
4452
+ const nameNode = decl.childForFieldName("name");
4453
+ fields.push({ name: nameNode ? getNodeText(nameNode) : "unknown", type, modifiers, annotations: [] });
4454
+ }
4455
+ } else if (c.type === "property_declaration") {
4456
+ const nameNode = c.childForFieldName("name");
4457
+ if (!nameNode) continue;
4458
+ const typeNode = c.childForFieldName("type");
4459
+ fields.push({
4460
+ name: getNodeText(nameNode),
4461
+ type: typeNode ? getNodeText(typeNode) : null,
4462
+ modifiers: extractCSharpModifiers(c),
4463
+ annotations: []
4464
+ });
4465
+ }
4466
+ }
4467
+ return fields;
4468
+ }
4469
+ function extractCSharpParamAnnotations(param) {
4470
+ const out2 = [];
4471
+ for (let i2 = 0; i2 < param.childCount; i2++) {
4472
+ const list = param.child(i2);
4473
+ if (list?.type !== "attribute_list") continue;
4474
+ for (let j = 0; j < list.childCount; j++) {
4475
+ const attr = list.child(j);
4476
+ if (attr?.type !== "attribute") continue;
4477
+ const name2 = attr.childForFieldName("name");
4478
+ if (name2) out2.push(getNodeText(name2));
4479
+ }
4480
+ }
4481
+ return out2;
4482
+ }
4429
4483
  function extractCSharpModifiers(node) {
4430
4484
  const mods = [];
4431
4485
  for (let i2 = 0; i2 < node.childCount; i2++) {
@@ -13950,6 +14004,17 @@ var DEFAULT_SINKS = [
13950
14004
  { method: "Load", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13951
14005
  { method: "LoadFrom", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13952
14006
  { method: "LoadFile", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
14007
+ // C# server-side template injection (SSTI). Compiling an attacker-controlled
14008
+ // template string is arbitrary code execution — classified as code_injection
14009
+ // (CWE-94), matching how Python (Jinja2/Mako) and Node SSTI are modelled.
14010
+ // Restricted to DISTINCTIVE template-compile APIs so the generic
14011
+ // `Template.Parse` / `.Compile` names (int.Parse, Regex.Compile, …) are not
14012
+ // over-matched. Taint-gated: a constant template never fires. `RunCompile`
14013
+ // = RazorEngine; `CompileRenderStringAsync` = RazorLight; `Compile` is
14014
+ // class-scoped to Handlebars. (cognium-dev#273)
14015
+ { method: "RunCompile", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
14016
+ { method: "CompileRenderStringAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
14017
+ { method: "Compile", class: "Handlebars", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13953
14018
  // C# insecure deserialization — the polymorphic BCL formatters that can
13954
14019
  // instantiate arbitrary types named in the payload (CWE-502, cognium-ai#318).
13955
14020
  // Each of these classes exists only to deserialize and is unsafe on untrusted
@@ -15077,7 +15142,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
15077
15142
  const skipMethods = ["toString", "hashCode", "equals", "compareTo"];
15078
15143
  if (skipMethods.includes(method.name)) continue;
15079
15144
  for (const param of method.parameters) {
15080
- const isTaintable = param.type ? isInterproceduralTaintableType(param.type, language) : true;
15145
+ const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
15146
+ const isTaintable = hasCSharpBindingAttr ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
15081
15147
  if (isTaintable) {
15082
15148
  const paramLine = param.line ?? method.start_line;
15083
15149
  sources.push({
@@ -15197,6 +15263,13 @@ function findSources(calls, types, patterns, sourceLines, language) {
15197
15263
  }
15198
15264
  return result;
15199
15265
  }
15266
+ var CSHARP_BINDING_ATTRS = /* @__PURE__ */ new Set([
15267
+ "FromBody",
15268
+ "FromQuery",
15269
+ "FromRoute",
15270
+ "FromForm",
15271
+ "FromHeader"
15272
+ ]);
15200
15273
  function isInterproceduralTaintableType(typeName, language) {
15201
15274
  const baseType = typeName.split("<")[0].trim();
15202
15275
  const excludedTypes = [
@@ -15486,6 +15559,145 @@ function isSafeCSharpProcessStartCall(call, pattern, language, sourceLines) {
15486
15559
  }
15487
15560
  return false;
15488
15561
  }
15562
+ function stripCsLiterals(line) {
15563
+ return line.replace(/\/\/.*$/, "").replace(/@?"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)'/g, "''");
15564
+ }
15565
+ function csBraceDepthBefore(lines, idx) {
15566
+ let depth = 0;
15567
+ for (let i2 = 0; i2 < idx && i2 < lines.length; i2++) {
15568
+ for (const ch of stripCsLiterals(lines[i2])) {
15569
+ if (ch === "{") depth++;
15570
+ else if (ch === "}") depth--;
15571
+ }
15572
+ }
15573
+ return depth;
15574
+ }
15575
+ function netBraces(fragment) {
15576
+ let n = 0;
15577
+ for (const ch of fragment) {
15578
+ if (ch === "{") n++;
15579
+ else if (ch === "}") n--;
15580
+ }
15581
+ return n;
15582
+ }
15583
+ function splitCsIf(line) {
15584
+ const m = /\bif\s*\(/.exec(line);
15585
+ if (!m) return null;
15586
+ const ifCol = m.index;
15587
+ let i2 = m.index + m[0].length;
15588
+ let depth = 1;
15589
+ let inStr = false;
15590
+ let strCh = "";
15591
+ for (; i2 < line.length; i2++) {
15592
+ const c = line[i2];
15593
+ if (inStr) {
15594
+ if (c === "\\") {
15595
+ i2++;
15596
+ continue;
15597
+ }
15598
+ if (c === strCh) inStr = false;
15599
+ continue;
15600
+ }
15601
+ if (c === '"' || c === "'") {
15602
+ inStr = true;
15603
+ strCh = c;
15604
+ continue;
15605
+ }
15606
+ if (c === "(") depth++;
15607
+ else if (c === ")") {
15608
+ depth--;
15609
+ if (depth === 0) break;
15610
+ }
15611
+ }
15612
+ if (depth !== 0) return null;
15613
+ return { cond: line.slice(m.index + m[0].length, i2), rest: line.slice(i2 + 1), ifCol };
15614
+ }
15615
+ function csEqualityGuard(cond) {
15616
+ const c = cond.trim();
15617
+ let m = /^(.*?)\s*(==|!=)\s*@?"[^"]*"\s*$/.exec(c);
15618
+ if (m) return { op: m[2], exprSide: m[1].trim() };
15619
+ m = /^@?"[^"]*"\s*(==|!=)\s*(.*)$/.exec(c);
15620
+ if (m) return { op: m[1], exprSide: m[2].trim() };
15621
+ return null;
15622
+ }
15623
+ var CS_IDENT_RE = /[A-Za-z_]\w*/g;
15624
+ function csIdentifiers(expr) {
15625
+ return new Set(expr.match(CS_IDENT_RE) ?? []);
15626
+ }
15627
+ function csThenBlock(lines, ifIdx, rest) {
15628
+ const isExit = (s) => /^\s*(?:return|throw|continue|break)\b/.test(s);
15629
+ let firstIdx;
15630
+ let firstText;
15631
+ if (rest.trim()) {
15632
+ firstIdx = ifIdx;
15633
+ firstText = rest;
15634
+ } else {
15635
+ let j = ifIdx + 1;
15636
+ while (j < lines.length && stripCsLiterals(lines[j]).trim() === "") j++;
15637
+ firstIdx = j;
15638
+ firstText = lines[j] ?? "";
15639
+ }
15640
+ if (firstText.trim().startsWith("{")) {
15641
+ let depth = 0;
15642
+ let started = false;
15643
+ let endLine = lines.length - 1;
15644
+ let body2 = "";
15645
+ for (let i2 = firstIdx; i2 < lines.length; i2++) {
15646
+ const stripped = stripCsLiterals(i2 === firstIdx ? firstText : lines[i2]);
15647
+ let broke = false;
15648
+ for (const ch of stripped) {
15649
+ if (ch === "{") {
15650
+ depth++;
15651
+ started = true;
15652
+ if (depth === 1) continue;
15653
+ } else if (ch === "}") {
15654
+ depth--;
15655
+ if (depth === 0) {
15656
+ endLine = i2;
15657
+ broke = true;
15658
+ break;
15659
+ }
15660
+ }
15661
+ if (started && depth >= 1) body2 += ch;
15662
+ }
15663
+ if (broke) break;
15664
+ if (started) body2 += " ";
15665
+ }
15666
+ return { start: ifIdx, end: endLine, earlyExit: isExit(body2.trim()) };
15667
+ }
15668
+ return { start: ifIdx, end: firstIdx, earlyExit: isExit(firstText.trim()) };
15669
+ }
15670
+ function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines) {
15671
+ if (language !== "csharp") return false;
15672
+ if (pattern.type !== "ssrf") return false;
15673
+ if (!sourceLines || sourceLines.length === 0) return false;
15674
+ const candidates = /* @__PURE__ */ new Set();
15675
+ for (const a of call.arguments) {
15676
+ if (a.variable) candidates.add(a.variable);
15677
+ for (const id of csIdentifiers(a.expression ?? "")) candidates.add(id);
15678
+ }
15679
+ if (candidates.size === 0) return false;
15680
+ const sinkIdx = call.location.line - 1;
15681
+ const sinkDepth = csBraceDepthBefore(sourceLines, sinkIdx);
15682
+ for (let i2 = 0; i2 < sinkIdx; i2++) {
15683
+ const parts2 = splitCsIf(sourceLines[i2]);
15684
+ if (!parts2) continue;
15685
+ const guard = csEqualityGuard(parts2.cond);
15686
+ if (!guard) continue;
15687
+ const guardIds = csIdentifiers(guard.exprSide);
15688
+ if (![...guardIds].some((id) => candidates.has(id))) continue;
15689
+ const block = csThenBlock(sourceLines, i2, parts2.rest);
15690
+ if (guard.op === "==") {
15691
+ if (sinkIdx >= block.start && sinkIdx <= block.end) return true;
15692
+ } else {
15693
+ const guardDepth = csBraceDepthBefore(sourceLines, i2) + netBraces(stripCsLiterals(sourceLines[i2]).slice(0, parts2.ifCol));
15694
+ if (block.earlyExit && sinkIdx > block.end && guardDepth === sinkDepth) {
15695
+ return true;
15696
+ }
15697
+ }
15698
+ }
15699
+ return false;
15700
+ }
15489
15701
  function isSafeRustCommandCall(call, pattern, language) {
15490
15702
  if (language !== "rust") return false;
15491
15703
  if (pattern.type !== "command_injection") return false;
@@ -15744,6 +15956,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
15744
15956
  if (isSafeCSharpProcessStartCall(call, pattern, language, sourceLines)) {
15745
15957
  continue;
15746
15958
  }
15959
+ if (isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines)) {
15960
+ continue;
15961
+ }
15747
15962
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at, types)) {
15748
15963
  continue;
15749
15964
  }
@@ -26630,6 +26845,8 @@ var LanguageSourcesPass = class {
26630
26845
  additionalSources.push(...findSetterChainSources(types, code, language));
26631
26846
  additionalSources.push(...findJavaScriptAssignmentSources(code, language));
26632
26847
  additionalSources.push(...findCSharpRequestSources(code, language));
26848
+ additionalSources.push(...findCSharpBindingAttributeSources(code, language));
26849
+ additionalSources.push(...findCSharpMinimalApiSources(code, language));
26633
26850
  additionalSources.push(...findGoArgvSources(code, language));
26634
26851
  const jsDOMSinks = findJavaScriptDOMSinks(code, language);
26635
26852
  for (const s of jsDOMSinks) {
@@ -27337,6 +27554,89 @@ function findCSharpRequestSources(sourceCode, language) {
27337
27554
  }
27338
27555
  return sources;
27339
27556
  }
27557
+ var CSHARP_MINIMAL_API_BINDING = /* @__PURE__ */ new Map([
27558
+ ["FromBody", "http_body"],
27559
+ ["FromForm", "http_body"],
27560
+ ["FromQuery", "http_param"],
27561
+ ["FromRoute", "http_path"],
27562
+ ["FromHeader", "http_param"]
27563
+ ]);
27564
+ var CSHARP_NON_INPUT_TYPES = /* @__PURE__ */ new Set([
27565
+ "HttpContext",
27566
+ "HttpRequest",
27567
+ "HttpResponse",
27568
+ "CancellationToken",
27569
+ "ClaimsPrincipal",
27570
+ "ILogger",
27571
+ "IFormFileCollection"
27572
+ ]);
27573
+ function findCSharpBindingAttributeSources(sourceCode, language) {
27574
+ if (language !== "csharp") return [];
27575
+ const sources = [];
27576
+ const lines = sourceCode.split("\n");
27577
+ const re = /\[\s*(FromQuery|FromBody|FromForm|FromRoute|FromHeader)(?:\s*\([^)]*\))?\s*\]\s*(?:\[[^\]]*\]\s*)*[\w.<>[\]?]+\s+([A-Za-z_]\w*)/g;
27578
+ for (let i2 = 0; i2 < lines.length; i2++) {
27579
+ const lineRe = new RegExp(re.source, "g");
27580
+ let m;
27581
+ while ((m = lineRe.exec(lines[i2])) !== null) {
27582
+ const attr = m[1];
27583
+ const name2 = m[2];
27584
+ const type = attr === "FromBody" || attr === "FromForm" ? "http_body" : attr === "FromRoute" ? "http_path" : "http_param";
27585
+ if (sources.some((s) => s.line === i2 + 1 && s.variable === name2)) continue;
27586
+ sources.push({
27587
+ type,
27588
+ location: `[${attr}] ${name2}`,
27589
+ severity: "high",
27590
+ line: i2 + 1,
27591
+ confidence: 1,
27592
+ variable: name2
27593
+ });
27594
+ }
27595
+ }
27596
+ return sources;
27597
+ }
27598
+ function findCSharpMinimalApiSources(sourceCode, language) {
27599
+ if (language !== "csharp") return [];
27600
+ const sources = [];
27601
+ const lines = sourceCode.split("\n");
27602
+ const mapRe = /\bMap(?:Get|Post|Put|Delete|Patch)\s*\(\s*(?:@?"[^"]*"|[\w.]+)\s*,\s*(?:\[[^\]]*\]\s*)?(?:async\s*)?\(([^)]*)\)\s*=>/;
27603
+ for (let i2 = 0; i2 < lines.length; i2++) {
27604
+ const m = mapRe.exec(lines[i2]);
27605
+ if (!m || !m[1].trim()) continue;
27606
+ for (const rawParam of m[1].split(",")) {
27607
+ const seed = classifyCSharpLambdaParam(rawParam);
27608
+ if (!seed) continue;
27609
+ if (sources.some((s) => s.line === i2 + 1 && s.variable === seed.name)) continue;
27610
+ sources.push({
27611
+ type: seed.type,
27612
+ location: `${seed.name} (Minimal API ${seed.via})`,
27613
+ severity: "high",
27614
+ line: i2 + 1,
27615
+ confidence: 1,
27616
+ variable: seed.name
27617
+ });
27618
+ }
27619
+ }
27620
+ return sources;
27621
+ }
27622
+ function classifyCSharpLambdaParam(raw) {
27623
+ const attrs = [...raw.matchAll(/\[([^\]]*)\]/g)].map((a) => a[1].split("(")[0].trim());
27624
+ if (attrs.includes("FromServices")) return null;
27625
+ if (attrs.some((a) => CSHARP_MINIMAL_API_BINDING.has(a))) return null;
27626
+ const noAttr = raw.replace(/\[[^\]]*\]/g, "").trim();
27627
+ const parts2 = noAttr.split(/\s+/).filter(Boolean);
27628
+ if (parts2.length < 2) return null;
27629
+ const name2 = parts2[parts2.length - 1];
27630
+ if (!/^[A-Za-z_]\w*$/.test(name2)) return null;
27631
+ const baseType = (parts2[parts2.length - 2] ?? "").replace(/[?\[\]]/g, "").split("<")[0].split(".").pop() ?? "";
27632
+ if (CSHARP_NON_INPUT_TYPES.has(baseType)) return null;
27633
+ for (const a of attrs) {
27634
+ const t = CSHARP_MINIMAL_API_BINDING.get(a);
27635
+ if (t) return { type: t, name: name2, via: `[${a}]` };
27636
+ }
27637
+ if (baseType === "string") return { type: "http_param", name: name2, via: "string param" };
27638
+ return null;
27639
+ }
27340
27640
  function findJavaScriptAssignmentSources(sourceCode, language) {
27341
27641
  if (!["javascript", "typescript"].includes(language)) return [];
27342
27642
  const sources = [];
@@ -41662,6 +41962,9 @@ var JAVA_SET_HTTPONLY_TRUE_RE = /\.setHttpOnly\s*\(\s*true\s*\)/;
41662
41962
  var GO_SECURE_TRUE_RE = /\bSecure\s*:\s*true\b/;
41663
41963
  var GO_HTTPONLY_TRUE_RE = /\bHttpOnly\s*:\s*true\b/;
41664
41964
  var RUST_SET_COOKIE_MACRO_RE = /(format!|write!|writeln!)\s*\(([^()]*Set-Cookie[^()]*)\)/gis;
41965
+ var CS_COOKIE_OPTIONS_RE = /\bnew\s+CookieOptions\s*\{([^{}]*)\}/gs;
41966
+ var CS_SECURE_FALSE_RE = /\bSecure\s*=\s*false\b/;
41967
+ var CS_HTTPONLY_FALSE_RE = /\bHttpOnly\s*=\s*false\b/;
41665
41968
  var InsecureCookiePass = class {
41666
41969
  name = "insecure-cookie";
41667
41970
  category = "security";
@@ -41705,9 +42008,29 @@ var InsecureCookiePass = class {
41705
42008
  insecureCookies.push(det);
41706
42009
  this.emit(ctx, file, det, "rust");
41707
42010
  }
42011
+ } else if (language === "csharp") {
42012
+ for (const det of this.detectCSharpCookieOptions(code)) {
42013
+ insecureCookies.push(det);
42014
+ this.emit(ctx, file, det, "csharp");
42015
+ }
41708
42016
  }
41709
42017
  return { insecureCookies };
41710
42018
  }
42019
+ // ---------------- C# ----------------
42020
+ detectCSharpCookieOptions(code) {
42021
+ const out2 = [];
42022
+ const re = new RegExp(CS_COOKIE_OPTIONS_RE.source, CS_COOKIE_OPTIONS_RE.flags);
42023
+ let m;
42024
+ while ((m = re.exec(code)) !== null) {
42025
+ const body2 = m[1] ?? "";
42026
+ const missingSecure = CS_SECURE_FALSE_RE.test(body2);
42027
+ const missingHttpOnly = CS_HTTPONLY_FALSE_RE.test(body2);
42028
+ if (!missingSecure && !missingHttpOnly) continue;
42029
+ const line = code.slice(0, m.index).split("\n").length;
42030
+ out2.push({ line, receiver: "CookieOptions", missingSecure, missingHttpOnly, optionsPresent: true });
42031
+ }
42032
+ return out2;
42033
+ }
41711
42034
  // ---------------- JS / TS ----------------
41712
42035
  detectJs(call) {
41713
42036
  if (call.method_name !== "cookie") return null;
@@ -41809,15 +42132,15 @@ var InsecureCookiePass = class {
41809
42132
  const missing = [];
41810
42133
  if (det.missingSecure) {
41811
42134
  missing.push(
41812
- flavor === "js" ? "`secure: true`" : flavor === "python" ? "`secure=True`" : flavor === "java" ? "`setSecure(true)`" : flavor === "go" ? "`Secure: true`" : "`Secure` attribute"
42135
+ flavor === "js" ? "`secure: true`" : flavor === "python" ? "`secure=True`" : flavor === "java" ? "`setSecure(true)`" : flavor === "go" ? "`Secure: true`" : flavor === "csharp" ? "`Secure = true`" : "`Secure` attribute"
41813
42136
  );
41814
42137
  }
41815
42138
  if (det.missingHttpOnly) {
41816
42139
  missing.push(
41817
- flavor === "js" ? "`httpOnly: true`" : flavor === "python" ? "`httponly=True`" : flavor === "java" ? "`setHttpOnly(true)`" : flavor === "go" ? "`HttpOnly: true`" : "`HttpOnly` attribute"
42140
+ flavor === "js" ? "`httpOnly: true`" : flavor === "python" ? "`httponly=True`" : flavor === "java" ? "`setHttpOnly(true)`" : flavor === "go" ? "`HttpOnly: true`" : flavor === "csharp" ? "`HttpOnly = true`" : "`HttpOnly` attribute"
41818
42141
  );
41819
42142
  }
41820
- const fix = flavor === "js" ? 'Pass `{ secure: true, httpOnly: true, sameSite: "lax" }` as the third argument to `res.cookie()`.' : flavor === "python" ? 'Pass `secure=True, httponly=True, samesite="Lax"` to `response.set_cookie(...)`.' : flavor === "java" ? "After constructing the cookie, call `cookie.setSecure(true)` and `cookie.setHttpOnly(true)` before adding it to the response." : flavor === "go" ? "Set `Secure: true` and `HttpOnly: true` on the `http.Cookie` struct literal passed to `http.SetCookie`." : "Append `; Secure; HttpOnly` to the `Set-Cookie` header string.";
42143
+ const fix = flavor === "js" ? 'Pass `{ secure: true, httpOnly: true, sameSite: "lax" }` as the third argument to `res.cookie()`.' : flavor === "python" ? 'Pass `secure=True, httponly=True, samesite="Lax"` to `response.set_cookie(...)`.' : flavor === "java" ? "After constructing the cookie, call `cookie.setSecure(true)` and `cookie.setHttpOnly(true)` before adding it to the response." : flavor === "go" ? "Set `Secure: true` and `HttpOnly: true` on the `http.Cookie` struct literal passed to `http.SetCookie`." : flavor === "csharp" ? "Set `Secure = true` and `HttpOnly = true` on the `CookieOptions` (or enforce them globally via `CookiePolicyOptions`)." : "Append `; Secure; HttpOnly` to the `Set-Cookie` header string.";
41821
42144
  ctx.addFinding({
41822
42145
  id: `${this.name}-${file}-${det.line}`,
41823
42146
  pass: this.name,
@@ -42288,6 +42611,7 @@ var ISSUE_CWE = {
42288
42611
  "hardcoded-key": "CWE-321",
42289
42612
  "weak-rsa-key": "CWE-326"
42290
42613
  };
42614
+ var CS_CIPHER_MODE_ECB_RE = /\bCipherMode\s*\.\s*ECB\b/;
42291
42615
  var WeakCryptoPass = class {
42292
42616
  name = "weak-crypto";
42293
42617
  category = "security";
@@ -42303,26 +42627,34 @@ var WeakCryptoPass = class {
42303
42627
  const findings = [];
42304
42628
  const constProp = ctx.hasResult("constant-propagation") ? ctx.getResult("constant-propagation") : null;
42305
42629
  const literalBindings = scanLiteralBindings(code, language);
42630
+ const emit = (line, det) => {
42631
+ findings.push({ line, language, ...det });
42632
+ ctx.addFinding({
42633
+ id: `${this.name}-${file}-${line}-${det.issue}`,
42634
+ pass: this.name,
42635
+ category: this.category,
42636
+ rule_id: this.name,
42637
+ cwe: ISSUE_CWE[det.issue],
42638
+ severity: "high",
42639
+ level: "error",
42640
+ message: this.buildMessage(det),
42641
+ file,
42642
+ line,
42643
+ fix: this.buildFix(det.issue),
42644
+ evidence: { ...det, language }
42645
+ });
42646
+ };
42306
42647
  for (const call of graph.ir.calls) {
42307
- const detections = this.detect(call, language, constProp, literalBindings);
42308
- for (const det of detections) {
42309
- const line = call.location.line;
42310
- findings.push({ line, language, ...det });
42311
- const message = this.buildMessage(det);
42312
- ctx.addFinding({
42313
- id: `${this.name}-${file}-${line}-${det.issue}`,
42314
- pass: this.name,
42315
- category: this.category,
42316
- rule_id: this.name,
42317
- cwe: ISSUE_CWE[det.issue],
42318
- severity: "high",
42319
- level: "error",
42320
- message,
42321
- file,
42322
- line,
42323
- fix: this.buildFix(det.issue),
42324
- evidence: { ...det, language }
42325
- });
42648
+ for (const det of this.detect(call, language, constProp, literalBindings)) {
42649
+ emit(call.location.line, det);
42650
+ }
42651
+ }
42652
+ if (language === "csharp") {
42653
+ const lines = code.split("\n");
42654
+ for (let i2 = 0; i2 < lines.length; i2++) {
42655
+ if (CS_CIPHER_MODE_ECB_RE.test(lines[i2])) {
42656
+ emit(i2 + 1, { issue: "ecb-mode", detail: "CipherMode.ECB", api: "SymmetricAlgorithm.Mode" });
42657
+ }
42326
42658
  }
42327
42659
  }
42328
42660
  return { findings };
@@ -43874,6 +44206,8 @@ var VERIFY_FALSE_RE = /\bverify\s*=\s*False\b/;
43874
44206
  var REJECT_UNAUTHORIZED_FALSE_RE = /\brejectUnauthorized\s*:\s*false\b/;
43875
44207
  var INSECURE_SKIP_VERIFY_TRUE_RE = /\bInsecureSkipVerify\s*:\s*true\b/;
43876
44208
  var HOSTNAME_LAMBDA_TRUE_RE = /\(\s*\w+\s*,\s*\w+\s*\)\s*->\s*true\b/;
44209
+ var CS_CERT_CALLBACK_TRUE_RE = /\b(ServerCertificateValidationCallback|ServerCertificateCustomValidationCallback|RemoteCertificateValidationCallback)\s*(?:\+?=|\()\s*(?:\([^)]*\)|\w+)\s*=>\s*(?:true\b|\{\s*return\s+true\b)/;
44210
+ var CS_DANGEROUS_ACCEPT_RE = /\bDangerousAcceptAnyServerCertificateValidator\b/;
43877
44211
  var ALLOW_ALL_HOSTNAME_VERIFIERS = /* @__PURE__ */ new Set([
43878
44212
  "NoopHostnameVerifier.INSTANCE",
43879
44213
  "new AllowAllHostnameVerifier()",
@@ -44018,6 +44352,21 @@ var TlsVerifyDisabledPass = class {
44018
44352
  }
44019
44353
  }
44020
44354
  }
44355
+ if (language === "csharp") {
44356
+ for (let i2 = 0; i2 < lines.length; i2++) {
44357
+ const l = lines[i2];
44358
+ const m = CS_CERT_CALLBACK_TRUE_RE.exec(l);
44359
+ if (m) {
44360
+ out2.push({ line: i2 + 1, pattern: `${m[1]} => true`, api: m[1] });
44361
+ } else if (CS_DANGEROUS_ACCEPT_RE.test(l)) {
44362
+ out2.push({
44363
+ line: i2 + 1,
44364
+ pattern: "DangerousAcceptAnyServerCertificateValidator",
44365
+ api: "HttpClientHandler"
44366
+ });
44367
+ }
44368
+ }
44369
+ }
44021
44370
  return out2;
44022
44371
  }
44023
44372
  fixFor(language, pattern) {
@@ -44039,6 +44388,9 @@ var TlsVerifyDisabledPass = class {
44039
44388
  if (pattern.includes("ssl._create_unverified_context")) {
44040
44389
  return "Do not use `_create_unverified_context()`. Use `ssl.create_default_context()`.";
44041
44390
  }
44391
+ if (language === "csharp") {
44392
+ return "Do not accept every certificate. Remove the always-true validation callback (and `DangerousAcceptAnyServerCertificateValidator`); rely on the platform default. To trust a private CA, validate the chain against it in the callback instead of returning true.";
44393
+ }
44042
44394
  return "Restore TLS certificate and hostname verification.";
44043
44395
  }
44044
44396
  };
@@ -44542,6 +44894,8 @@ var PY_VERIFY_SIGNATURE_FALSE_RE = /["']verify_signature["']\s*:\s*False\b/;
44542
44894
  var PY_VERIFY_KW_FALSE_RE = /\bverify\s*=\s*False\b/;
44543
44895
  var PY_ALG_NONE_RE = /\balgorithms\s*=\s*[\[\(]\s*["']none["']/i;
44544
44896
  var JS_ALG_NONE_RE = /\balgorithms\s*:\s*\[\s*["']none["']/i;
44897
+ var CS_REQUIRE_SIGNED_FALSE_RE = /\bRequireSignedTokens\s*=\s*false\b/;
44898
+ var CS_SIGNATURE_VALIDATOR_BYPASS_RE = /\bSignatureValidator\s*=\s*[^;]*=>\s*new\s+JwtSecurityToken\b/;
44545
44899
  var JwtVerifyDisabledPass = class {
44546
44900
  name = "jwt-verify-disabled";
44547
44901
  category = "security";
@@ -44549,25 +44903,34 @@ var JwtVerifyDisabledPass = class {
44549
44903
  const { graph, language } = ctx;
44550
44904
  const file = graph.ir.meta.file;
44551
44905
  const findings = [];
44906
+ const emit = (line, det) => {
44907
+ findings.push({ line, language, ...det });
44908
+ ctx.addFinding({
44909
+ id: `${this.name}-${file}-${line}-${det.pattern}`,
44910
+ pass: this.name,
44911
+ category: this.category,
44912
+ rule_id: this.name,
44913
+ cwe: "CWE-347",
44914
+ severity: "critical",
44915
+ level: "error",
44916
+ message: `JWT signature verification disabled via \`${det.pattern}\` in \`${det.api}\`. Any attacker can forge a token with arbitrary claims (user id, roles, expiry) since the signature is not checked.`,
44917
+ file,
44918
+ line,
44919
+ fix: this.fixFor(language),
44920
+ evidence: { ...det, language }
44921
+ });
44922
+ };
44552
44923
  for (const call of graph.ir.calls) {
44553
- const detections = this.detect(call, language);
44554
- for (const det of detections) {
44555
- const line = call.location.line;
44556
- findings.push({ line, language, ...det });
44557
- ctx.addFinding({
44558
- id: `${this.name}-${file}-${line}-${det.pattern}`,
44559
- pass: this.name,
44560
- category: this.category,
44561
- rule_id: this.name,
44562
- cwe: "CWE-347",
44563
- severity: "critical",
44564
- level: "error",
44565
- message: `JWT signature verification disabled via \`${det.pattern}\` in \`${det.api}\`. Any attacker can forge a token with arbitrary claims (user id, roles, expiry) since the signature is not checked.`,
44566
- file,
44567
- line,
44568
- fix: this.fixFor(language),
44569
- evidence: { ...det, language }
44570
- });
44924
+ for (const det of this.detect(call, language)) emit(call.location.line, det);
44925
+ }
44926
+ if (language === "csharp") {
44927
+ const lines = ctx.code.split("\n");
44928
+ for (let i2 = 0; i2 < lines.length; i2++) {
44929
+ if (CS_REQUIRE_SIGNED_FALSE_RE.test(lines[i2])) {
44930
+ emit(i2 + 1, { pattern: "RequireSignedTokens = false", api: "TokenValidationParameters" });
44931
+ } else if (CS_SIGNATURE_VALIDATOR_BYPASS_RE.test(lines[i2])) {
44932
+ emit(i2 + 1, { pattern: "SignatureValidator returns an unvalidated token", api: "TokenValidationParameters" });
44933
+ }
44571
44934
  }
44572
44935
  }
44573
44936
  return { findings };
@@ -44639,6 +45002,9 @@ var JwtVerifyDisabledPass = class {
44639
45002
  if (language === "java") {
44640
45003
  return "For auth0/java-jwt: use `JWT.require(Algorithm.HMAC256(secret))` or an RSA algorithm. For jjwt: call `parseClaimsJws(token)` (signature enforced) rather than `parse(token)` (signature ignored).";
44641
45004
  }
45005
+ if (language === "csharp") {
45006
+ return "Leave `RequireSignedTokens = true` and do not install a custom `SignatureValidator` that returns the token unverified. Configure `IssuerSigningKey`/`IssuerSigningKeys` and let the handler validate the signature.";
45007
+ }
44642
45008
  return "Enforce JWT signature verification with a concrete algorithm (HS256/RS256/ES256). Never accept `alg: none`.";
44643
45009
  }
44644
45010
  };