cognium-dev 3.54.0 → 3.56.0

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 (2) hide show
  1. package/dist/cli.js +535 -14
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -10476,8 +10476,8 @@ var DEFAULT_SINKS = [
10476
10476
  { method: "print", class: "ServletOutputStream", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
10477
10477
  { method: "println", class: "ServletOutputStream", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
10478
10478
  { method: "sendError", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10479
- { method: "setHeader", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10480
- { method: "addHeader", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10479
+ { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10480
+ { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10481
10481
  { method: "setContentType", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
10482
10482
  { method: "setAttribute", class: "PageContext", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10483
10483
  { method: "addAttribute", class: "Model", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
@@ -11163,7 +11163,18 @@ var DEFAULT_SINKS = [
11163
11163
  { method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11164
11164
  { method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11165
11165
  { method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11166
- { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] }
11166
+ { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
11167
+ { method: "setHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
11168
+ { method: "writeHead", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [2], languages: ["javascript", "typescript"] },
11169
+ { method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
11170
+ { method: "location", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
11171
+ { method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
11172
+ { method: "Set", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11173
+ { method: "Add", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11174
+ { method: "assign", class: "Object", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11175
+ { method: "merge", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11176
+ { method: "extend", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11177
+ { method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] }
11167
11178
  ];
11168
11179
  var DEFAULT_SANITIZERS = [
11169
11180
  { method: "setString", class: "PreparedStatement", removes: ["sql_injection"] },
@@ -21503,7 +21514,9 @@ var KNOWN_SINK_TYPES = new Set([
21503
21514
  "code_injection",
21504
21515
  "mybatis_mapper_call",
21505
21516
  "redos",
21506
- "format_string"
21517
+ "format_string",
21518
+ "crlf",
21519
+ "mass_assignment"
21507
21520
  ]);
21508
21521
  function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
21509
21522
  const sanitizersAtTarget = sanitizersByLine.get(toLine);
@@ -21558,19 +21571,19 @@ function buildTaintFlow(source, sink, taintInfo) {
21558
21571
  // ../circle-ir/dist/analysis/findings.js
21559
21572
  function canSourceReachSink(sourceType, sinkType) {
21560
21573
  const sourceToSinkMapping = {
21561
- http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection"],
21562
- http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call"],
21563
- http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection"],
21564
- http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection"],
21574
+ http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment"],
21575
+ http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment"],
21576
+ http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf"],
21577
+ http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf"],
21565
21578
  http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call"],
21566
- http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection"],
21579
+ http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment"],
21567
21580
  io_input: ["command_injection", "path_traversal", "deserialization", "xxe", "code_injection", "xss"],
21568
21581
  env_input: ["command_injection", "path_traversal"],
21569
21582
  db_input: ["xss", "sql_injection"],
21570
21583
  file_input: ["deserialization", "xxe", "path_traversal", "command_injection", "code_injection"],
21571
21584
  network_input: ["sql_injection", "command_injection", "xss", "ssrf"],
21572
21585
  config_param: ["sql_injection", "command_injection", "path_traversal", "xss", "ssrf"],
21573
- interprocedural_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "code_injection", "mybatis_mapper_call"],
21586
+ interprocedural_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment"],
21574
21587
  plugin_param: ["sql_injection", "command_injection", "path_traversal", "xss", "code_injection"]
21575
21588
  };
21576
21589
  const validSinks = sourceToSinkMapping[sourceType];
@@ -27501,6 +27514,113 @@ function detectHardcodedKeyJava(call) {
27501
27514
  return `literal string`;
27502
27515
  return null;
27503
27516
  }
27517
+ function detectHardcodedKeyPython(call, constProp, literalBindings) {
27518
+ const arg = call.arguments.find((a) => a.position === 0);
27519
+ if (!arg)
27520
+ return null;
27521
+ const expr = (arg.expression ?? arg.literal ?? "").trim();
27522
+ if (!expr)
27523
+ return null;
27524
+ if (/^[bB][rR]?["'][^"']*["']$/.test(expr) || /^[rR][bB]["'][^"']*["']$/.test(expr)) {
27525
+ return `literal bytes ${expr.slice(0, 24)}${expr.length > 24 ? "…" : ""}`;
27526
+ }
27527
+ if (/^["'][^"']*["']$/.test(expr)) {
27528
+ return `literal string ${expr.slice(0, 24)}${expr.length > 24 ? "…" : ""}`;
27529
+ }
27530
+ if (arg.variable && constProp) {
27531
+ const sym = constProp.symbols.get(arg.variable);
27532
+ if (sym && sym.type === "string" && typeof sym.value === "string") {
27533
+ return `constant-propagated bytes from \`${arg.variable}\``;
27534
+ }
27535
+ }
27536
+ if (arg.variable) {
27537
+ const lit = literalBindings.get(arg.variable);
27538
+ if (lit) {
27539
+ return `literal-bound ${arg.variable} = ${lit.slice(0, 24)}${lit.length > 24 ? "…" : ""}`;
27540
+ }
27541
+ }
27542
+ return null;
27543
+ }
27544
+ function detectHardcodedKeyGo(call, constProp, literalBindings) {
27545
+ const arg = call.arguments.find((a) => a.position === 0);
27546
+ if (!arg)
27547
+ return null;
27548
+ const expr = (arg.literal ?? arg.expression ?? "").trim();
27549
+ if (!expr)
27550
+ return null;
27551
+ if (/^\[\s*\]\s*byte\s*\(\s*["'`][^"'`]*["'`]\s*\)$/.test(expr)) {
27552
+ return `literal []byte("…")`;
27553
+ }
27554
+ if (/^\[\s*\]\s*byte\s*\{[^}]*\}$/.test(expr)) {
27555
+ return `literal []byte{…} composite`;
27556
+ }
27557
+ if (arg.variable && constProp) {
27558
+ const sym = constProp.symbols.get(arg.variable);
27559
+ if (sym && sym.type === "string" && typeof sym.value === "string") {
27560
+ return `constant-propagated key from \`${arg.variable}\``;
27561
+ }
27562
+ }
27563
+ if (arg.variable) {
27564
+ const lit = literalBindings.get(arg.variable);
27565
+ if (lit) {
27566
+ return `literal-bound ${arg.variable} = ${lit.slice(0, 24)}${lit.length > 24 ? "…" : ""}`;
27567
+ }
27568
+ }
27569
+ return null;
27570
+ }
27571
+ function parseWeakRsaKeySizePython(call) {
27572
+ for (const arg of call.arguments) {
27573
+ const expr = (arg.expression ?? "").trim();
27574
+ const lit = (arg.literal ?? "").trim();
27575
+ const m = expr.match(/^key_size\s*=\s*(-?\d+)\s*$/);
27576
+ if (m && m[1]) {
27577
+ const n = parseInt(m[1], 10);
27578
+ if (Number.isFinite(n) && n > 0 && n < 2048)
27579
+ return n;
27580
+ return null;
27581
+ }
27582
+ if (/^key_size\s*=/.test(expr) && lit) {
27583
+ const n = parseInt(lit, 10);
27584
+ if (Number.isFinite(n) && n > 0 && n < 2048)
27585
+ return n;
27586
+ }
27587
+ }
27588
+ return null;
27589
+ }
27590
+ function scanLiteralBindings(code, language) {
27591
+ const out2 = new Map;
27592
+ if (!code)
27593
+ return out2;
27594
+ if (language === "python") {
27595
+ const re = /^[ \t]*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(b[rR]?["'][^"']*["']|[rR]?b["'][^"']*["']|["'][^"']*["'])\s*(?:$|#)/gm;
27596
+ let m;
27597
+ while ((m = re.exec(code)) !== null) {
27598
+ if (m[1] && m[2])
27599
+ out2.set(m[1], m[2]);
27600
+ }
27601
+ return out2;
27602
+ }
27603
+ if (language === "go") {
27604
+ const reByte = /^[ \t]*(?:var\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*(?::=|=)\s*(\[\s*\]\s*byte\s*\(\s*["'`][^"'`]*["'`]\s*\))/gm;
27605
+ let m;
27606
+ while ((m = reByte.exec(code)) !== null) {
27607
+ if (m[1] && m[2])
27608
+ out2.set(m[1], m[2]);
27609
+ }
27610
+ const reStr = /^[ \t]*(?:var|const)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(["'`][^"'`]*["'`])/gm;
27611
+ while ((m = reStr.exec(code)) !== null) {
27612
+ if (m[1] && m[2])
27613
+ out2.set(m[1], m[2]);
27614
+ }
27615
+ const reShort = /^[ \t]*([A-Za-z_][A-Za-z0-9_]*)\s*:=\s*(["'`][^"'`]*["'`])/gm;
27616
+ while ((m = reShort.exec(code)) !== null) {
27617
+ if (m[1] && m[2])
27618
+ out2.set(m[1], m[2]);
27619
+ }
27620
+ return out2;
27621
+ }
27622
+ return out2;
27623
+ }
27504
27624
  var ISSUE_CWE = {
27505
27625
  "weak-cipher": "CWE-327",
27506
27626
  "ecb-mode": "CWE-327",
@@ -27514,11 +27634,13 @@ class WeakCryptoPass {
27514
27634
  name = "weak-crypto";
27515
27635
  category = "security";
27516
27636
  run(ctx) {
27517
- const { graph, language } = ctx;
27637
+ const { graph, language, code } = ctx;
27518
27638
  const file = graph.ir.meta.file;
27519
27639
  const findings = [];
27640
+ const constProp = ctx.hasResult("constant-propagation") ? ctx.getResult("constant-propagation") : null;
27641
+ const literalBindings = scanLiteralBindings(code, language);
27520
27642
  for (const call of graph.ir.calls) {
27521
- const detections = this.detect(call, language);
27643
+ const detections = this.detect(call, language, constProp, literalBindings);
27522
27644
  for (const det of detections) {
27523
27645
  const line = call.location.line;
27524
27646
  findings.push({ line, language, ...det });
@@ -27571,7 +27693,7 @@ class WeakCryptoPass {
27571
27693
  return "Use AES-GCM (authenticated) or ChaCha20-Poly1305. Avoid DES, " + "3DES, RC2, RC4, Blowfish, and ECB mode. For asymmetric encryption " + "use RSA-OAEP with ≥2048-bit keys or modern curve-based schemes.";
27572
27694
  }
27573
27695
  }
27574
- detect(call, language) {
27696
+ detect(call, language, constProp, literalBindings) {
27575
27697
  const method = call.method_name;
27576
27698
  const receiver = call.receiver ?? "";
27577
27699
  const out2 = [];
@@ -27631,6 +27753,12 @@ class WeakCryptoPass {
27631
27753
  out2.push({ issue: "ecb-mode", detail: "AES.MODE_ECB", api: `${receiver}.new` });
27632
27754
  }
27633
27755
  }
27756
+ if (lastSeg === "aes" || lastSeg.endsWith(".aes") || WEAK_CIPHER_BASES.has(lastSeg)) {
27757
+ const keyDetail = detectHardcodedKeyPython(call, constProp, literalBindings);
27758
+ if (keyDetail) {
27759
+ out2.push({ issue: "hardcoded-key", detail: keyDetail, api: `${receiver}.new` });
27760
+ }
27761
+ }
27634
27762
  }
27635
27763
  const isHazmatAlgos = receiver === "algorithms" || receiver.endsWith(".algorithms");
27636
27764
  if (isHazmatAlgos) {
@@ -27639,6 +27767,25 @@ class WeakCryptoPass {
27639
27767
  if (WEAK_CIPHER_BASES.has(normalized)) {
27640
27768
  out2.push({ issue: "weak-cipher", detail: normalized, api: `algorithms.${method}` });
27641
27769
  }
27770
+ if (m === "aes") {
27771
+ const keyDetail = detectHardcodedKeyPython(call, constProp, literalBindings);
27772
+ if (keyDetail) {
27773
+ out2.push({ issue: "hardcoded-key", detail: keyDetail, api: `algorithms.${method}` });
27774
+ }
27775
+ }
27776
+ }
27777
+ if (method === "ECB" && (receiver === "modes" || receiver.endsWith(".modes"))) {
27778
+ out2.push({ issue: "ecb-mode", detail: "modes.ECB()", api: `${receiver}.ECB` });
27779
+ }
27780
+ if (method === "generate_private_key" && (receiver === "rsa" || receiver === "dsa" || receiver.endsWith(".rsa") || receiver.endsWith(".dsa"))) {
27781
+ const n = parseWeakRsaKeySizePython(call);
27782
+ if (n !== null) {
27783
+ out2.push({
27784
+ issue: "weak-rsa-key",
27785
+ detail: String(n),
27786
+ api: `${receiver}.generate_private_key`
27787
+ });
27788
+ }
27642
27789
  }
27643
27790
  return out2;
27644
27791
  }
@@ -27680,6 +27827,24 @@ class WeakCryptoPass {
27680
27827
  if ((method === "NewECBEncrypter" || method === "NewECBDecrypter") && receiver === "cipher") {
27681
27828
  out2.push({ issue: "ecb-mode", detail: method, api: `cipher.${method}` });
27682
27829
  }
27830
+ if (receiver === "aes" && method === "NewCipher" || receiver === "des" && (method === "NewCipher" || method === "NewTripleDESCipher") || receiver === "rc4" && method === "NewCipher") {
27831
+ const keyDetail = detectHardcodedKeyGo(call, constProp, literalBindings);
27832
+ if (keyDetail) {
27833
+ out2.push({ issue: "hardcoded-key", detail: keyDetail, api: `${receiver}.${method}` });
27834
+ }
27835
+ }
27836
+ if (receiver === "rsa" && method === "GenerateKey") {
27837
+ const bitsArg = call.arguments.find((a) => a.position === 1);
27838
+ const expr = (bitsArg?.literal ?? bitsArg?.expression ?? "").trim();
27839
+ const n = parseInt(expr, 10);
27840
+ if (Number.isFinite(n) && n > 0 && n < 2048) {
27841
+ out2.push({
27842
+ issue: "weak-rsa-key",
27843
+ detail: String(n),
27844
+ api: "rsa.GenerateKey"
27845
+ });
27846
+ }
27847
+ }
27683
27848
  return out2;
27684
27849
  }
27685
27850
  return out2;
@@ -28154,6 +28319,356 @@ class JwtVerifyDisabledPass {
28154
28319
  }
28155
28320
  }
28156
28321
 
28322
+ // ../circle-ir/dist/analysis/passes/csrf-protection-disabled-pass.js
28323
+ var JAVA_CSRF_DISABLE_RE = /\.csrf\s*\([^)]*\)\s*\.\s*disable\b/;
28324
+ var JAVA_CSRF_LAMBDA_DISABLE_RE = /\bcsrf\s*\(\s*\w+\s*->\s*\w+\s*\.\s*disable\s*\(/;
28325
+ var JAVA_CSRF_METHODREF_RE = /\bcsrf\s*\(\s*[\w.]+::disable\s*\)/;
28326
+ var JAVA_CSRF_NULL_REPO_RE = /\.csrfTokenRepository\s*\(\s*null\s*\)/;
28327
+
28328
+ class CsrfProtectionDisabledPass {
28329
+ name = "csrf-protection-disabled";
28330
+ category = "security";
28331
+ run(ctx) {
28332
+ const { graph, language } = ctx;
28333
+ const file = graph.ir.meta.file;
28334
+ const findings = [];
28335
+ for (const call of graph.ir.calls) {
28336
+ const detections = this.detectCall(call, language);
28337
+ for (const det of detections) {
28338
+ const line = call.location.line;
28339
+ findings.push({ line, language, ...det });
28340
+ ctx.addFinding({
28341
+ id: `${this.name}-${file}-${line}-${det.pattern}`,
28342
+ pass: this.name,
28343
+ category: this.category,
28344
+ rule_id: this.name,
28345
+ cwe: "CWE-352",
28346
+ severity: "critical",
28347
+ level: "error",
28348
+ message: `CSRF protection explicitly disabled via \`${det.pattern}\` ` + `(${det.api}). Any browser session can be silently used to ` + "perform state-changing requests from a malicious origin.",
28349
+ file,
28350
+ line,
28351
+ fix: this.fixFor(language),
28352
+ evidence: { ...det, language }
28353
+ });
28354
+ }
28355
+ }
28356
+ if (language === "java") {
28357
+ const src = ctx.code ?? "";
28358
+ if (src) {
28359
+ const lines = src.split(`
28360
+ `);
28361
+ for (let i2 = 0;i2 < lines.length; i2++) {
28362
+ const line = i2 + 1;
28363
+ const text = lines[i2] ?? "";
28364
+ let det = null;
28365
+ if (JAVA_CSRF_LAMBDA_DISABLE_RE.test(text)) {
28366
+ det = { pattern: "csrf(c -> c.disable())", api: "HttpSecurity.csrf" };
28367
+ } else if (JAVA_CSRF_METHODREF_RE.test(text)) {
28368
+ det = { pattern: "csrf(::disable)", api: "HttpSecurity.csrf" };
28369
+ } else if (JAVA_CSRF_NULL_REPO_RE.test(text)) {
28370
+ det = { pattern: "csrfTokenRepository(null)", api: "HttpSecurity.csrfTokenRepository" };
28371
+ } else if (JAVA_CSRF_DISABLE_RE.test(text)) {
28372
+ det = { pattern: "csrf().disable()", api: "HttpSecurity.csrf" };
28373
+ }
28374
+ if (det && !findings.some((f) => f.line === line && f.pattern === det.pattern)) {
28375
+ findings.push({ line, language, ...det });
28376
+ ctx.addFinding({
28377
+ id: `${this.name}-${file}-${line}-${det.pattern}`,
28378
+ pass: this.name,
28379
+ category: this.category,
28380
+ rule_id: this.name,
28381
+ cwe: "CWE-352",
28382
+ severity: "critical",
28383
+ level: "error",
28384
+ message: `CSRF protection explicitly disabled via \`${det.pattern}\` ` + `(${det.api}). Any browser session can be silently used to ` + "perform state-changing requests from a malicious origin.",
28385
+ file,
28386
+ line,
28387
+ fix: this.fixFor(language),
28388
+ evidence: { ...det, language }
28389
+ });
28390
+ }
28391
+ }
28392
+ }
28393
+ }
28394
+ if (language === "python") {
28395
+ const src = ctx.code ?? "";
28396
+ if (src) {
28397
+ const lines = src.split(`
28398
+ `);
28399
+ for (let i2 = 0;i2 < lines.length; i2++) {
28400
+ const text = lines[i2] ?? "";
28401
+ if (/^\s*@csrf_exempt\b/.test(text)) {
28402
+ const line = i2 + 1;
28403
+ const det = { pattern: "@csrf_exempt", api: "django.views.decorators.csrf" };
28404
+ findings.push({ line, language, ...det });
28405
+ ctx.addFinding({
28406
+ id: `${this.name}-${file}-${line}-${det.pattern}`,
28407
+ pass: this.name,
28408
+ category: this.category,
28409
+ rule_id: this.name,
28410
+ cwe: "CWE-352",
28411
+ severity: "critical",
28412
+ level: "error",
28413
+ message: "Django view is decorated with `@csrf_exempt`, bypassing the " + "framework CSRF middleware for this endpoint. Any browser " + "session can be silently used to invoke this handler from " + "a malicious origin.",
28414
+ file,
28415
+ line,
28416
+ fix: this.fixFor(language),
28417
+ evidence: { ...det, language }
28418
+ });
28419
+ }
28420
+ }
28421
+ }
28422
+ }
28423
+ return { findings };
28424
+ }
28425
+ detectCall(call, language) {
28426
+ const out2 = [];
28427
+ if (language !== "java")
28428
+ return out2;
28429
+ if (call.method_name === "disable") {
28430
+ const recv = call.receiver ?? "";
28431
+ if (/\bcsrf\s*\(\s*\)\s*$/.test(recv) || recv.endsWith(".csrf()")) {
28432
+ out2.push({ pattern: "csrf().disable()", api: "HttpSecurity.csrf" });
28433
+ }
28434
+ }
28435
+ if (call.method_name === "csrfTokenRepository") {
28436
+ const arg = call.arguments.find((a) => a.position === 0);
28437
+ const expr = (arg?.expression ?? arg?.literal ?? "").trim();
28438
+ if (expr === "null") {
28439
+ out2.push({
28440
+ pattern: "csrfTokenRepository(null)",
28441
+ api: "HttpSecurity.csrfTokenRepository"
28442
+ });
28443
+ }
28444
+ }
28445
+ return out2;
28446
+ }
28447
+ fixFor(language) {
28448
+ if (language === "java") {
28449
+ return "Leave Spring Security CSRF protection enabled. If you need to " + "exempt a specific endpoint (e.g. webhook), use " + '`.csrf(c -> c.ignoringRequestMatchers("/webhook"))` rather than ' + "`.disable()`. For stateless APIs, prefer a per-request token over " + "disabling CSRF entirely.";
28450
+ }
28451
+ if (language === "python") {
28452
+ return "Remove `@csrf_exempt`. For stateless API endpoints, use Django REST " + "Framework with a token / session auth backend that does not rely on " + "cookies. For webhook receivers, verify a shared-secret signature " + "instead of disabling CSRF.";
28453
+ }
28454
+ return "Re-enable framework CSRF protection or replace with origin / token validation.";
28455
+ }
28456
+ }
28457
+
28458
+ // ../circle-ir/dist/analysis/passes/xml-entity-expansion-pass.js
28459
+ var JAVA_FACTORIES = new Set([
28460
+ "SAXParserFactory",
28461
+ "DocumentBuilderFactory",
28462
+ "XMLInputFactory",
28463
+ "SchemaFactory",
28464
+ "TransformerFactory"
28465
+ ]);
28466
+ var JAVA_SAFE_EVIDENCE_RE = /(disallow-doctype-decl|external-general-entities|external-parameter-entities|SUPPORT_DTD|ACCESS_EXTERNAL_DTD|ACCESS_EXTERNAL_SCHEMA|setXIncludeAware\s*\(\s*false\s*\)|setExpandEntityReferences\s*\(\s*false\s*\))/;
28467
+ var PY_LXML_PARSER_INSECURE_DEFAULT_RE = /\bresolve_entities\s*=\s*False\b/;
28468
+
28469
+ class XmlEntityExpansionPass {
28470
+ name = "xml-entity-expansion";
28471
+ category = "security";
28472
+ run(ctx) {
28473
+ const { graph, language } = ctx;
28474
+ const file = graph.ir.meta.file;
28475
+ const findings = [];
28476
+ const code = ctx.code ?? "";
28477
+ if (language === "java") {
28478
+ const safeInFile = JAVA_SAFE_EVIDENCE_RE.test(code);
28479
+ if (safeInFile)
28480
+ return { findings };
28481
+ for (const call of graph.ir.calls) {
28482
+ const det = this.detectJavaCall(call);
28483
+ if (!det)
28484
+ continue;
28485
+ const line = call.location.line;
28486
+ findings.push({ line, language, ...det });
28487
+ ctx.addFinding({
28488
+ id: `${this.name}-${file}-${line}-${det.api}`,
28489
+ pass: this.name,
28490
+ category: this.category,
28491
+ rule_id: this.name,
28492
+ cwe: det.cwe,
28493
+ severity: "high",
28494
+ level: "error",
28495
+ message: `${det.api} created without disabling DTD / external-entity ` + "processing. Vulnerable to billion-laughs / quadratic " + "blow-up DoS (CWE-776) and external-entity disclosure " + '(CWE-611). Add `setFeature("http://apache.org/xml/features/' + 'disallow-doctype-decl", true)` (or the equivalent) before ' + "parsing.",
28496
+ file,
28497
+ line,
28498
+ fix: this.fixForJava(det.api),
28499
+ evidence: { ...det, language, safeFeatureInFile: false }
28500
+ });
28501
+ }
28502
+ return { findings };
28503
+ }
28504
+ if (language === "python") {
28505
+ const safeInFile = PY_LXML_PARSER_INSECURE_DEFAULT_RE.test(code) || /\bdefusedxml\b/.test(code);
28506
+ if (safeInFile)
28507
+ return { findings };
28508
+ for (const call of graph.ir.calls) {
28509
+ const det = this.detectPythonCall(call);
28510
+ if (!det)
28511
+ continue;
28512
+ const line = call.location.line;
28513
+ findings.push({ line, language, ...det });
28514
+ ctx.addFinding({
28515
+ id: `${this.name}-${file}-${line}-${det.api}`,
28516
+ pass: this.name,
28517
+ category: this.category,
28518
+ rule_id: this.name,
28519
+ cwe: det.cwe,
28520
+ severity: "high",
28521
+ level: "error",
28522
+ message: `${det.api} called without an entity-safe parser. Vulnerable ` + "to billion-laughs / quadratic blow-up DoS (CWE-776) and " + "external-entity disclosure (CWE-611). Use `defusedxml` or pass " + "an `XMLParser(resolve_entities=False)` to lxml.",
28523
+ file,
28524
+ line,
28525
+ fix: this.fixForPython(det.api),
28526
+ evidence: { ...det, language, safeFeatureInFile: false }
28527
+ });
28528
+ }
28529
+ return { findings };
28530
+ }
28531
+ return { findings };
28532
+ }
28533
+ detectJavaCall(call) {
28534
+ if (call.method_name !== "newInstance")
28535
+ return null;
28536
+ const recv = call.receiver ?? "";
28537
+ const recvType = call.receiver_type ?? "";
28538
+ for (const factory of JAVA_FACTORIES) {
28539
+ if (recv === factory || recvType === factory || recv.endsWith("." + factory) || recvType.endsWith("." + factory)) {
28540
+ return {
28541
+ pattern: `${factory}.newInstance()`,
28542
+ api: factory,
28543
+ cwe: "CWE-776"
28544
+ };
28545
+ }
28546
+ }
28547
+ return null;
28548
+ }
28549
+ detectPythonCall(call) {
28550
+ const recv = call.receiver ?? "";
28551
+ const method = call.method_name;
28552
+ if ((method === "parse" || method === "fromstring" || method === "XML") && (recv === "etree" || recv.endsWith(".etree"))) {
28553
+ return {
28554
+ pattern: `etree.${method}`,
28555
+ api: `lxml.etree.${method}`,
28556
+ cwe: "CWE-776"
28557
+ };
28558
+ }
28559
+ if ((method === "parse" || method === "fromstring") && (recv === "ET" || recv === "ElementTree" || recv.endsWith(".ElementTree"))) {
28560
+ return {
28561
+ pattern: `ElementTree.${method}`,
28562
+ api: `xml.etree.ElementTree.${method}`,
28563
+ cwe: "CWE-776"
28564
+ };
28565
+ }
28566
+ return null;
28567
+ }
28568
+ fixForJava(api) {
28569
+ if (api === "SAXParserFactory") {
28570
+ return 'Call `factory.setFeature("http://apache.org/xml/features/' + 'disallow-doctype-decl", true)` and ' + "`factory.setXIncludeAware(false)` before `newSAXParser()`.";
28571
+ }
28572
+ if (api === "DocumentBuilderFactory") {
28573
+ return 'Call `factory.setFeature("http://apache.org/xml/features/' + 'disallow-doctype-decl", true)` and ' + "`factory.setExpandEntityReferences(false)` before " + "`newDocumentBuilder()`.";
28574
+ }
28575
+ if (api === "XMLInputFactory") {
28576
+ return "Call `factory.setProperty(XMLInputFactory.SUPPORT_DTD, false)` " + "and `factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_" + "ENTITIES, false)` before `createXMLStreamReader`.";
28577
+ }
28578
+ return "Use `XMLConstants.FEATURE_SECURE_PROCESSING` and explicitly disable " + "DTD / external-entity loading on the factory before parsing.";
28579
+ }
28580
+ fixForPython(api) {
28581
+ if (api.startsWith("lxml.etree")) {
28582
+ return "Pass an explicit parser: " + "`etree.parse(src, parser=etree.XMLParser(resolve_entities=False, " + "no_network=True))`. Even better, use the `defusedxml.lxml` wrapper.";
28583
+ }
28584
+ return "Replace `xml.etree.ElementTree` with `defusedxml.ElementTree`, which " + "disables DTD / entity processing by default.";
28585
+ }
28586
+ }
28587
+
28588
+ // ../circle-ir/dist/analysis/passes/mass-assignment-pass.js
28589
+ var PY_KWARGS_SPLAT_RE = /\*\*\s*(?:request|self\.request|flask\.request|ctx|self)\s*\.\s*(?:form|args|values|json|get_json\s*\(\s*\)|files|data)/;
28590
+ var JS_OBJECT_SPREAD_RE = /\{\s*\.\.\.\s*(?:req|request|ctx|context)(?:\.request)?\s*\.\s*(?:body|query|params|form)\b/;
28591
+
28592
+ class MassAssignmentPass {
28593
+ name = "mass-assignment";
28594
+ category = "security";
28595
+ run(ctx) {
28596
+ const { graph, language } = ctx;
28597
+ const file = graph.ir.meta.file;
28598
+ const findings = [];
28599
+ const code = ctx.code ?? "";
28600
+ if (!code)
28601
+ return { findings };
28602
+ const lines = code.split(`
28603
+ `);
28604
+ if (language === "python") {
28605
+ for (let i2 = 0;i2 < lines.length; i2++) {
28606
+ const text = lines[i2] ?? "";
28607
+ const m = PY_KWARGS_SPLAT_RE.exec(text);
28608
+ if (!m)
28609
+ continue;
28610
+ const line = i2 + 1;
28611
+ const det = {
28612
+ pattern: "**request.<bag>",
28613
+ match: m[0]
28614
+ };
28615
+ findings.push({
28616
+ line,
28617
+ language,
28618
+ pattern: det.pattern,
28619
+ snippet: text.trim().slice(0, 200)
28620
+ });
28621
+ ctx.addFinding({
28622
+ id: `${this.name}-${file}-${line}`,
28623
+ pass: this.name,
28624
+ category: this.category,
28625
+ rule_id: this.name,
28626
+ cwe: "CWE-915",
28627
+ severity: "high",
28628
+ level: "error",
28629
+ message: `HTTP request bag splatted into constructor / ORM helper via ` + `\`${det.match}\`. Every form field becomes a settable attribute ` + "on the domain object, including ones the endpoint did not " + "intend to expose (e.g. `is_admin`, `role`, `owner_id`).",
28630
+ file,
28631
+ line,
28632
+ fix: "Replace the `**` splat with an explicit allow-list: " + "`Model(name=request.form['name'], email=request.form['email'])`. " + "For Django, use a `ModelForm` / serializer with `fields = [...]`.",
28633
+ evidence: { pattern: det.pattern, match: det.match, language }
28634
+ });
28635
+ }
28636
+ return { findings };
28637
+ }
28638
+ if (language === "javascript" || language === "typescript") {
28639
+ for (let i2 = 0;i2 < lines.length; i2++) {
28640
+ const text = lines[i2] ?? "";
28641
+ const m = JS_OBJECT_SPREAD_RE.exec(text);
28642
+ if (!m)
28643
+ continue;
28644
+ const line = i2 + 1;
28645
+ findings.push({
28646
+ line,
28647
+ language,
28648
+ pattern: "{...req.<bag>}",
28649
+ snippet: text.trim().slice(0, 200)
28650
+ });
28651
+ ctx.addFinding({
28652
+ id: `${this.name}-${file}-${line}`,
28653
+ pass: this.name,
28654
+ category: this.category,
28655
+ rule_id: this.name,
28656
+ cwe: "CWE-915",
28657
+ severity: "high",
28658
+ level: "error",
28659
+ message: `HTTP request bag spread into object literal via \`${m[0]}\`. ` + "Every body field becomes a settable property on the resulting " + "object, including ones the endpoint did not intend to expose " + "(e.g. `isAdmin`, `role`, `ownerId`).",
28660
+ file,
28661
+ line,
28662
+ fix: "Replace the spread with an explicit pick: " + "`const { name, email } = req.body; const user = { name, email };`. " + "For ORMs, use a DTO / Zod schema with `.pick(...)` or " + "allow-list serializers.",
28663
+ evidence: { pattern: "{...req.<bag>}", match: m[0], language }
28664
+ });
28665
+ }
28666
+ return { findings };
28667
+ }
28668
+ return { findings };
28669
+ }
28670
+ }
28671
+
28157
28672
  // ../circle-ir/dist/graph/import-graph.js
28158
28673
  function dirname(filePath) {
28159
28674
  const idx = filePath.lastIndexOf("/");
@@ -29276,6 +29791,12 @@ async function analyze(code, filePath, language, options = {}) {
29276
29791
  pipeline.add(new TlsVerifyDisabledPass);
29277
29792
  if (!disabledPasses.has("jwt-verify-disabled"))
29278
29793
  pipeline.add(new JwtVerifyDisabledPass);
29794
+ if (!disabledPasses.has("csrf-protection-disabled"))
29795
+ pipeline.add(new CsrfProtectionDisabledPass);
29796
+ if (!disabledPasses.has("xml-entity-expansion"))
29797
+ pipeline.add(new XmlEntityExpansionPass);
29798
+ if (!disabledPasses.has("mass-assignment"))
29799
+ pipeline.add(new MassAssignmentPass);
29279
29800
  const { results, findings } = pipeline.run(graph, code, language, config);
29280
29801
  const sinkFilter = results.get("sink-filter");
29281
29802
  const interProc = results.get("interprocedural");
@@ -29469,7 +29990,7 @@ var colors = {
29469
29990
  };
29470
29991
 
29471
29992
  // src/version.ts
29472
- var version = "3.54.0";
29993
+ var version = "3.56.0";
29473
29994
 
29474
29995
  // src/formatters.ts
29475
29996
  var SINK_SEVERITY = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.54.0",
3
+ "version": "3.56.0",
4
4
  "description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -65,7 +65,7 @@
65
65
  "registry": "https://registry.npmjs.org/"
66
66
  },
67
67
  "dependencies": {
68
- "circle-ir": "^3.54.0"
68
+ "circle-ir": "^3.56.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.5.0",