cognium-dev 4.9.26 → 4.9.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +463 -29
  2. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { readFileSync, existsSync, writeFileSync } from "fs";
7
7
  import { stat as stat2, readdir as readdir2 } from "fs/promises";
8
8
  import { join as join2, dirname as dirname3, extname, resolve as resolve2, relative as relative4, basename } from "path";
9
9
  import { createRequire as createRequire2 } from "module";
10
+ import { getHeapStatistics } from "v8";
10
11
 
11
12
  // ../../node_modules/web-tree-sitter/web-tree-sitter.js
12
13
  var __defProp = Object.defineProperty;
@@ -5505,6 +5506,33 @@ function extractCSharpCalls(tree, cache) {
5505
5506
  });
5506
5507
  continue;
5507
5508
  }
5509
+ if (left?.type === "identifier" && asn.parent?.type === "initializer_expression") {
5510
+ const creation = asn.parent.parent;
5511
+ if (creation?.type !== "object_creation_expression")
5512
+ continue;
5513
+ const ctorType = creation.childForFieldName("type");
5514
+ const right2 = asn.childForFieldName("right");
5515
+ if (!ctorType || !right2)
5516
+ continue;
5517
+ const typeName = csharpBareName(ctorType);
5518
+ const rhsText2 = getNodeText(right2);
5519
+ calls.push({
5520
+ method_name: getNodeText(left),
5521
+ receiver: typeName,
5522
+ receiver_type: typeName,
5523
+ receiver_type_fqn: null,
5524
+ arguments: [{
5525
+ position: 0,
5526
+ expression: rhsText2,
5527
+ variable: right2.type === "identifier" ? rhsText2 : null,
5528
+ literal: CSHARP_LITERAL_NODE_TYPES.has(right2.type) ? rhsText2 : null,
5529
+ value: null
5530
+ }],
5531
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
5532
+ in_method: findEnclosingMethod(asn)
5533
+ });
5534
+ continue;
5535
+ }
5508
5536
  if (left?.type !== "member_access_expression")
5509
5537
  continue;
5510
5538
  const nameNode = left.childForFieldName("name");
@@ -5517,6 +5545,15 @@ function extractCSharpCalls(tree, cache) {
5517
5545
  const recvType = recv ? typeMap.get(recv) : undefined;
5518
5546
  if (recvType !== "DirectorySearcher")
5519
5547
  continue;
5548
+ } else if (propName === "StatusDescription") {
5549
+ const recv = exprNode ? getNodeText(exprNode) : "";
5550
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv))
5551
+ continue;
5552
+ } else if (propName === "BaseAddress") {
5553
+ const recv = exprNode ? getNodeText(exprNode) : null;
5554
+ const recvType = recv ? typeMap.get(recv) : undefined;
5555
+ if (recvType !== "HttpClient")
5556
+ continue;
5520
5557
  } else if (propName !== "CommandText") {
5521
5558
  continue;
5522
5559
  }
@@ -5540,8 +5577,36 @@ function extractCSharpCalls(tree, cache) {
5540
5577
  in_method: findEnclosingMethod(asn)
5541
5578
  });
5542
5579
  }
5580
+ const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
5581
+ for (const cast of casts) {
5582
+ const typeNode = cast.childForFieldName("type");
5583
+ const valueNode = cast.childForFieldName("value");
5584
+ if (!typeNode || !valueNode)
5585
+ continue;
5586
+ const typeName = csharpBareName(typeNode);
5587
+ if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName))
5588
+ continue;
5589
+ const valueText = getNodeText(valueNode);
5590
+ calls.push({
5591
+ method_name: typeName,
5592
+ receiver: null,
5593
+ receiver_type: null,
5594
+ receiver_type_fqn: null,
5595
+ arguments: [{
5596
+ position: 0,
5597
+ expression: valueText,
5598
+ variable: valueNode.type === "identifier" ? valueText : null,
5599
+ literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
5600
+ value: null
5601
+ }],
5602
+ location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
5603
+ in_method: findEnclosingMethod(cast),
5604
+ is_constructor: true
5605
+ });
5606
+ }
5543
5607
  return calls;
5544
5608
  }
5609
+ var CSHARP_RAW_MARKUP_CAST_TYPES = new Set(["MarkupString"]);
5545
5610
  function extractCSharpArguments(argsNode) {
5546
5611
  const args2 = [];
5547
5612
  let position = 0;
@@ -5562,12 +5627,22 @@ function extractCSharpArguments(argsNode) {
5562
5627
  position: position++,
5563
5628
  expression: text,
5564
5629
  variable: expr?.type === "identifier" ? text : null,
5565
- literal: expr?.type === "string_literal" ? text : null,
5630
+ literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
5566
5631
  value: null
5567
5632
  });
5568
5633
  }
5569
5634
  return args2;
5570
5635
  }
5636
+ var CSHARP_LITERAL_NODE_TYPES = new Set([
5637
+ "string_literal",
5638
+ "verbatim_string_literal",
5639
+ "raw_string_literal",
5640
+ "character_literal",
5641
+ "integer_literal",
5642
+ "real_literal",
5643
+ "boolean_literal",
5644
+ "null_literal"
5645
+ ]);
5571
5646
  function extractJavaScriptCalls(tree, cache) {
5572
5647
  const calls = [];
5573
5648
  const context = buildJSResolutionContext(tree, cache);
@@ -12481,8 +12556,6 @@ var DEFAULT_SINKS = [
12481
12556
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
12482
12557
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
12483
12558
  { method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
12484
- { method: "from_str", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
12485
- { method: "from_slice", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
12486
12559
  { method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
12487
12560
  { method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
12488
12561
  { method: "Html", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -12703,12 +12776,15 @@ var DEFAULT_SINKS = [
12703
12776
  { method: "Deserialize", class: "ObjectStateFormatter", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["csharp"] },
12704
12777
  { method: "Raw", class: "Html", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12705
12778
  { method: "Write", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12779
+ { method: "WriteAsync", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12706
12780
  { method: "HtmlString", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12707
12781
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12782
+ { method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
12708
12783
  { method: "Content", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12709
12784
  { method: "Format", class: "String", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["csharp"] },
12710
12785
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
12711
12786
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
12787
+ { method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
12712
12788
  { method: "Redirect", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["csharp"] },
12713
12789
  { method: "RedirectPermanent", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["csharp"] },
12714
12790
  { method: "AddHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["csharp"] },
@@ -14918,8 +14994,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
14918
14994
  return false;
14919
14995
  } else if (!call.receiver && !call.receiver_type) {
14920
14996
  const target = call.resolution?.target;
14997
+ const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
14921
14998
  const expectedTail = `${pattern.class}.${pattern.method}`;
14922
- if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {} else {
14999
+ if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {} else {
14923
15000
  return false;
14924
15001
  }
14925
15002
  }
@@ -26077,7 +26154,7 @@ class LanguageSourcesPass {
26077
26154
  }
26078
26155
  }
26079
26156
  if (language === "csharp") {
26080
- additionalSanitizers.push(...findCSharpFullPathStartsWithGuardSanitizers(code));
26157
+ additionalSanitizers.push(...findCSharpFullPathStartsWithGuardSanitizers(code), ...findCSharpEarlyReturnGuardSanitizers(code, language));
26081
26158
  }
26082
26159
  if (language === "java") {
26083
26160
  additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
@@ -26783,18 +26860,23 @@ function findCSharpRequestSources(sourceCode, language) {
26783
26860
  const lines = sourceCode.split(`
26784
26861
  `);
26785
26862
  const assignRe = /^\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
26786
- const requestReadRe = /\bRequest\s*\.\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\b/;
26787
26863
  const consoleReadRe = /\bConsole\s*\.\s*ReadLine\s*\(/;
26788
26864
  const envReadRe = /\bEnvironment\s*\.\s*GetEnvironmentVariables?\s*(?:\(|\[)/;
26789
26865
  const formFileParams = new Set;
26790
26866
  const formFileDeclRe = /\bIFormFile\s+([A-Za-z_]\w*)/g;
26867
+ const httpRequestReceivers = new Set(["Request"]);
26868
+ const httpRequestDeclRe = /\b(?:HttpRequest|HttpRequestBase)\??\s+([A-Za-z_]\w*)/g;
26791
26869
  for (const line of lines) {
26792
- const re = new RegExp(formFileDeclRe.source, "g");
26870
+ const formRe = new RegExp(formFileDeclRe.source, "g");
26793
26871
  let d;
26794
- while ((d = re.exec(line)) !== null)
26872
+ while ((d = formRe.exec(line)) !== null)
26795
26873
  formFileParams.add(d[1]);
26874
+ const reqRe = new RegExp(httpRequestDeclRe.source, "g");
26875
+ while ((d = reqRe.exec(line)) !== null)
26876
+ httpRequestReceivers.add(d[1]);
26796
26877
  }
26797
26878
  const formFileReadRe = formFileParams.size > 0 ? new RegExp(`\\b(?:${[...formFileParams].join("|")})\\s*\\.\\s*(?:FileName|ContentType)\\b`) : null;
26879
+ const requestReadRe = new RegExp(`\\b(?:${[...httpRequestReceivers].join("|")})\\s*\\.\\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\\b`);
26798
26880
  const classify = (text) => requestReadRe.test(text) ? "http_param" : formFileReadRe?.test(text) ? "http_param" : consoleReadRe.test(text) ? "io_input" : envReadRe.test(text) ? "env_input" : null;
26799
26881
  const inlineReads = [
26800
26882
  [requestReadRe, "http_param"],
@@ -26819,10 +26901,37 @@ function findCSharpRequestSources(sourceCode, language) {
26819
26901
  };
26820
26902
  const exprStmtCallRe = /^\s*(?:await\s+)?[A-Za-z_][\w.]*(?:<[^>]*>)?\s*\(.*\)\s*;\s*$/;
26821
26903
  const nonCallKeywordRe = /^\s*(?:await\s+)?(?:if|while|for|foreach|switch|using|lock|return|throw|catch|when|nameof|typeof|sizeof|default)\b/;
26904
+ const embeddedAssignRe = /(?:^|[{;])\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*([^;{}]+)/;
26822
26905
  for (let i2 = 0;i2 < lines.length; i2++) {
26823
26906
  const m = assignRe.exec(lines[i2]);
26824
26907
  if (!m) {
26825
26908
  const stmt = lines[i2];
26909
+ if (stmt.includes("{") && !/^\s*(?:\/\/|\/\*|\*)/.test(stmt)) {
26910
+ const emb = new RegExp(embeddedAssignRe.source, "g");
26911
+ let em;
26912
+ let seeded = false;
26913
+ while ((em = emb.exec(stmt)) !== null) {
26914
+ const varName2 = em[1];
26915
+ const rhs2 = em[2];
26916
+ const type3 = classify(rhs2);
26917
+ if (!type3)
26918
+ continue;
26919
+ const lineNumber3 = i2 + 1;
26920
+ if (sources.some((s) => s.line === lineNumber3 && s.variable === varName2))
26921
+ continue;
26922
+ sources.push({
26923
+ type: type3,
26924
+ location: `${varName2} = ${rhs2.trim().substring(0, 50)}${rhs2.length > 50 ? "..." : ""}`,
26925
+ severity: "high",
26926
+ line: lineNumber3,
26927
+ confidence: 1,
26928
+ variable: varName2
26929
+ });
26930
+ seeded = true;
26931
+ }
26932
+ if (seeded)
26933
+ continue;
26934
+ }
26826
26935
  if (!exprStmtCallRe.test(stmt) || nonCallKeywordRe.test(stmt))
26827
26936
  continue;
26828
26937
  const hit = inlineRead(stmt);
@@ -29540,6 +29649,76 @@ function guardRejects(lines, i2, terminatorRe, window2) {
29540
29649
  }
29541
29650
  return false;
29542
29651
  }
29652
+ function findCSharpEarlyReturnGuardSanitizers(code, language) {
29653
+ if (language !== "csharp")
29654
+ return [];
29655
+ const sanitizers = [];
29656
+ const lines = code.split(`
29657
+ `);
29658
+ const terminatorRe = /\b(throw|return)\b/;
29659
+ const allowlistRes = [
29660
+ /\bif\s*\(\s*Array\s*\.\s*IndexOf\s*\([^,]+,\s*([A-Za-z_][\w.]*)\s*\)\s*<\s*0\s*\)/,
29661
+ /\bif\s*\(\s*!\s*[A-Za-z_][\w.]*\s*\.\s*Contains\s*\(\s*([A-Za-z_][\w.]*)\s*\)\s*\)/
29662
+ ];
29663
+ const charClassRe = /\bif\s*\(\s*!\s*([A-Za-z_][\w.]*)\s*\.\s*All\s*\(\s*char\s*\.\s*Is[A-Za-z]+\s*\)\s*\)/;
29664
+ for (let i2 = 0;i2 < lines.length; i2++) {
29665
+ let guardedExpr = null;
29666
+ for (const re of allowlistRes) {
29667
+ const m = re.exec(lines[i2]);
29668
+ if (m) {
29669
+ guardedExpr = m[1];
29670
+ break;
29671
+ }
29672
+ }
29673
+ if (!guardedExpr) {
29674
+ const m = charClassRe.exec(lines[i2]);
29675
+ if (m)
29676
+ guardedExpr = m[1];
29677
+ }
29678
+ if (!guardedExpr)
29679
+ continue;
29680
+ if (!guardRejects(lines, i2, terminatorRe, 6))
29681
+ continue;
29682
+ const root = guardedExpr.split(".")[0];
29683
+ const guarded = new Set([guardedExpr, root]);
29684
+ const assignRe = /^\s*(?:(?:var|string)\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/;
29685
+ for (let b = i2 - 1;b >= 0; b--) {
29686
+ const a = assignRe.exec(lines[b]);
29687
+ if (!a || !guarded.has(a[1]))
29688
+ continue;
29689
+ for (const id of a[2].match(/[A-Za-z_]\w*/g) ?? [])
29690
+ guarded.add(id);
29691
+ }
29692
+ const mentions = (t) => {
29693
+ for (const n of guarded)
29694
+ if (new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(t))
29695
+ return true;
29696
+ return false;
29697
+ };
29698
+ for (let l = i2 + 1;l < lines.length; l++) {
29699
+ const a = assignRe.exec(lines[l]);
29700
+ if (a && mentions(a[2]))
29701
+ guarded.add(a[1]);
29702
+ if (mentions(lines[l])) {
29703
+ sanitizers.push({
29704
+ type: "csharp_early_return_guard",
29705
+ method: "guard",
29706
+ line: l + 1,
29707
+ sanitizes: [
29708
+ "ssrf",
29709
+ "xpath_injection",
29710
+ "ldap_injection",
29711
+ "path_traversal",
29712
+ "command_injection",
29713
+ "sql_injection",
29714
+ "external_taint_escape"
29715
+ ]
29716
+ });
29717
+ }
29718
+ }
29719
+ }
29720
+ return sanitizers;
29721
+ }
29543
29722
  function findCSharpFullPathStartsWithGuardSanitizers(code) {
29544
29723
  const sanitizers = [];
29545
29724
  const lines = code.split(`
@@ -36613,6 +36792,65 @@ class LibraryProfileXssGatePass {
36613
36792
  }
36614
36793
 
36615
36794
  // ../circle-ir/dist/analysis/passes/taint-propagation-pass.js
36795
+ var CONTAINER_READ_SOURCES = new Set(["plugin_param", "config_param"]);
36796
+ function makeConstPropVeto(constProp, graph, sources, code) {
36797
+ const lines = typeof code === "string" ? code.split(`
36798
+ `) : [];
36799
+ const selfReassign = new Map;
36800
+ const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36801
+ for (const [line, defs] of graph.defsByLine) {
36802
+ const text = lines[line - 1];
36803
+ if (!text)
36804
+ continue;
36805
+ for (const d of defs) {
36806
+ if (d.kind !== "local" && d.kind !== "param")
36807
+ continue;
36808
+ const v = escape(d.variable);
36809
+ const m = new RegExp(`(?:^|[^\\w.$])${v}\\s*(?:([-+*/%&|^]|<<|>>)=|=(?!=))(.*)$`).exec(text);
36810
+ if (!m)
36811
+ continue;
36812
+ if (m[1] || new RegExp(`(?:^|[^\\w.$])${v}\\b`).test(m[2])) {
36813
+ const arr = selfReassign.get(d.variable) ?? [];
36814
+ arr.push(line);
36815
+ selfReassign.set(d.variable, arr);
36816
+ }
36817
+ }
36818
+ }
36819
+ const reassignedAt = (v, line) => (selfReassign.get(v) ?? []).includes(line);
36820
+ const defAt = (v, line) => {
36821
+ const own = (graph.defsByLine.get(line) ?? []).find((d) => d.variable === v);
36822
+ if (own)
36823
+ return own;
36824
+ const use = (graph.usesByLine.get(line) ?? []).find((u) => u.variable === v && u.def_id != null);
36825
+ return use ? graph.defById.get(use.def_id) : undefined;
36826
+ };
36827
+ return (step, sourceLine, sinkLine) => {
36828
+ const r = isFalsePositive(constProp, step.line, step.variable);
36829
+ if (!r.isFalsePositive)
36830
+ return false;
36831
+ if (r.reason !== "variable_not_tainted")
36832
+ return true;
36833
+ const lo = Math.min(sourceLine, sinkLine);
36834
+ let d = defAt(step.variable, step.line);
36835
+ let selfHops = 0;
36836
+ for (let guard = 0;d && d.line > lo && reassignedAt(d.variable, d.line) && guard < 32; guard++) {
36837
+ const here = d.id;
36838
+ const prior = (graph.usesByLine.get(d.line) ?? []).find((u) => u.variable === d.variable && u.def_id != null && u.def_id !== here);
36839
+ if (!prior)
36840
+ break;
36841
+ selfHops++;
36842
+ d = graph.defById.get(prior.def_id);
36843
+ }
36844
+ if (!d)
36845
+ return true;
36846
+ if (d.line === sourceLine && sources.some((s) => s.line === sourceLine && !CONTAINER_READ_SOURCES.has(s.type)))
36847
+ return false;
36848
+ if (selfHops > 0 && d.line >= lo)
36849
+ return false;
36850
+ return true;
36851
+ };
36852
+ }
36853
+
36616
36854
  class TaintPropagationPass {
36617
36855
  name = "taint-propagation";
36618
36856
  category = "security";
@@ -36630,12 +36868,12 @@ class TaintPropagationPass {
36630
36868
  return { flows: [] };
36631
36869
  }
36632
36870
  const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
36871
+ const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
36633
36872
  const verifiedFlows = propagationResult.flows.filter((flow) => {
36634
36873
  if (constProp.unreachableLines.has(flow.sink.line))
36635
36874
  return false;
36636
36875
  for (const step of flow.path) {
36637
- const fpCheck = isFalsePositive(constProp, step.line, step.variable);
36638
- if (fpCheck.isFalsePositive)
36876
+ if (isVetoed(step, flow.source.line, flow.sink.line))
36639
36877
  return false;
36640
36878
  }
36641
36879
  if (isCorrelatedPredicateFP(constProp, flow))
@@ -36741,7 +36979,7 @@ class TaintPropagationPass {
36741
36979
  continue;
36742
36980
  let isFP = false;
36743
36981
  for (const step of f.path) {
36744
- if (isFalsePositive(constProp, step.line, step.variable).isFalsePositive) {
36982
+ if (isVetoed(step, f.source_line, f.sink_line)) {
36745
36983
  isFP = true;
36746
36984
  break;
36747
36985
  }
@@ -36767,7 +37005,7 @@ class TaintPropagationPass {
36767
37005
  continue;
36768
37006
  let isFP = false;
36769
37007
  for (const step of f.path) {
36770
- if (isFalsePositive(constProp, step.line, step.variable).isFalsePositive) {
37008
+ if (isVetoed(step, f.source_line, f.sink_line)) {
36771
37009
  isFP = true;
36772
37010
  break;
36773
37011
  }
@@ -38171,6 +38409,109 @@ var GO_PURE_PACKAGES = new Set([
38171
38409
  "hex",
38172
38410
  "base64"
38173
38411
  ]);
38412
+ var CSHARP_PURE_STATIC_RECEIVERS = new Set([
38413
+ "string",
38414
+ "String",
38415
+ "Convert",
38416
+ "Encoding",
38417
+ "BitConverter",
38418
+ "Math",
38419
+ "Int16",
38420
+ "Int32",
38421
+ "Int64",
38422
+ "UInt16",
38423
+ "UInt32",
38424
+ "UInt64",
38425
+ "Double",
38426
+ "Single",
38427
+ "Decimal",
38428
+ "Boolean",
38429
+ "Byte",
38430
+ "SByte",
38431
+ "Char",
38432
+ "Guid",
38433
+ "DateTime",
38434
+ "DateTimeOffset",
38435
+ "TimeSpan",
38436
+ "Enum",
38437
+ "Array",
38438
+ "Buffer",
38439
+ "Nullable",
38440
+ "CultureInfo",
38441
+ "StringComparer",
38442
+ "Comparer",
38443
+ "EqualityComparer",
38444
+ "Console",
38445
+ "Debug",
38446
+ "Trace",
38447
+ "Logger",
38448
+ "logger",
38449
+ "_logger",
38450
+ "Log"
38451
+ ]);
38452
+ var CSHARP_NON_ESCAPE_METHODS = new Set([
38453
+ "ToString",
38454
+ "ToArray",
38455
+ "ToList",
38456
+ "ToCharArray",
38457
+ "GetHashCode",
38458
+ "Equals",
38459
+ "CompareTo",
38460
+ "Trim",
38461
+ "TrimStart",
38462
+ "TrimEnd",
38463
+ "Substring",
38464
+ "Split",
38465
+ "Join",
38466
+ "Concat",
38467
+ "PadLeft",
38468
+ "PadRight",
38469
+ "ToLower",
38470
+ "ToUpper",
38471
+ "ToLowerInvariant",
38472
+ "ToUpperInvariant",
38473
+ "StartsWith",
38474
+ "EndsWith",
38475
+ "Contains",
38476
+ "IndexOf",
38477
+ "LastIndexOf",
38478
+ "Insert",
38479
+ "Remove",
38480
+ "Normalize",
38481
+ "Append",
38482
+ "AppendLine",
38483
+ "AppendFormat",
38484
+ "TryParse",
38485
+ "Parse",
38486
+ "LogInformation",
38487
+ "LogWarning",
38488
+ "LogError",
38489
+ "LogDebug",
38490
+ "LogTrace",
38491
+ "LogCritical",
38492
+ "Information",
38493
+ "Warning",
38494
+ "Verbose",
38495
+ "Fatal",
38496
+ "BeginScope",
38497
+ "Add",
38498
+ "AddLast",
38499
+ "AddFirst",
38500
+ "AddRange",
38501
+ "TryAdd",
38502
+ "Insert",
38503
+ "Push",
38504
+ "Enqueue"
38505
+ ]);
38506
+ function isCSharpNonEscapeCall(call) {
38507
+ const name2 = call.method_name;
38508
+ const recv = (call.receiver ?? "").split(".").pop() ?? "";
38509
+ if (CSHARP_PURE_STATIC_RECEIVERS.has(recv))
38510
+ return true;
38511
+ if (CSHARP_NON_ESCAPE_METHODS.has(name2))
38512
+ return true;
38513
+ return false;
38514
+ }
38174
38515
  function isGoNonEscapeCall(call) {
38175
38516
  const name2 = call.method_name;
38176
38517
  const recv = (call.receiver ?? "").split(".").pop() ?? "";
@@ -38418,6 +38759,9 @@ function analyzeInterprocedural2(graphOrTypes, callsOrSources, dfgOrSinks, sourc
38418
38759
  if (graph.ir.meta.language === "go" && isGoNonEscapeCall(call)) {
38419
38760
  continue;
38420
38761
  }
38762
+ if (graph.ir.meta.language === "csharp" && isCSharpNonEscapeCall(call)) {
38763
+ continue;
38764
+ }
38421
38765
  const bashSafeBuiltins = new Set([
38422
38766
  "echo",
38423
38767
  "printf",
@@ -40052,6 +40396,8 @@ class VariableShadowingPass {
40052
40396
  const outerEntry = declLocals[0];
40053
40397
  for (let i2 = 1;i2 < numDeclLocals; i2++) {
40054
40398
  const inner = declLocals[i2];
40399
+ if (inner.def.line <= outerEntry.def.line)
40400
+ continue;
40055
40401
  if (!isInNestedScope(codeLines, outerEntry.def.line, inner.def.line))
40056
40402
  continue;
40057
40403
  const key = `${variable}-${inner.def.line}`;
@@ -49135,7 +49481,30 @@ async function analyzeProject(files, options = {}) {
49135
49481
  const perFileBudgetMs = options.perFileBudgetMs ?? 0;
49136
49482
  const perFileStart = Date.now();
49137
49483
  let perFileBudgetExceeded = false;
49484
+ const maxProjectSourceChars = options.maxProjectSourceChars ?? 0;
49485
+ const maxProjectFiles = options.maxProjectFiles ?? 0;
49486
+ let projectSourceChars = 0;
49487
+ let projectSizeBudgetExceeded = false;
49138
49488
  for (const { code, filePath, language } of files) {
49489
+ if (maxProjectFiles > 0 && fileAnalyses.length >= maxProjectFiles) {
49490
+ projectSizeBudgetExceeded = true;
49491
+ logger.warn("analyzeProject: file-count ceiling reached, skipping remaining files", {
49492
+ analysed: fileAnalyses.length,
49493
+ total: files.length,
49494
+ maxProjectFiles
49495
+ });
49496
+ break;
49497
+ }
49498
+ if (maxProjectSourceChars > 0 && projectSourceChars + code.length > maxProjectSourceChars) {
49499
+ projectSizeBudgetExceeded = true;
49500
+ logger.warn("analyzeProject: source-size ceiling reached, skipping remaining files", {
49501
+ analysed: fileAnalyses.length,
49502
+ total: files.length,
49503
+ sourceChars: projectSourceChars,
49504
+ maxProjectSourceChars
49505
+ });
49506
+ break;
49507
+ }
49139
49508
  if (perFileBudgetMs > 0 && Date.now() - perFileStart > perFileBudgetMs) {
49140
49509
  perFileBudgetExceeded = true;
49141
49510
  logger.warn("analyzeProject: per-file budget exceeded, skipping remaining files", {
@@ -49150,6 +49519,7 @@ async function analyzeProject(files, options = {}) {
49150
49519
  projectGraph.addFile(filePath, new CodeGraph(ir));
49151
49520
  sourceLinesByFile.set(filePath, code.split(`
49152
49521
  `));
49522
+ projectSourceChars += code.length;
49153
49523
  }
49154
49524
  const crossFileBudgetMs = options.crossFileBudgetMs ?? 300000;
49155
49525
  const crossFileResult = new CrossFilePass().run(projectGraph, sourceLinesByFile, { budgetMs: crossFileBudgetMs });
@@ -49203,6 +49573,9 @@ async function analyzeProject(files, options = {}) {
49203
49573
  if (perFileBudgetExceeded) {
49204
49574
  projectAnalysis.per_file_budget_exceeded = true;
49205
49575
  }
49576
+ if (projectSizeBudgetExceeded) {
49577
+ projectAnalysis.project_size_budget_exceeded = true;
49578
+ }
49206
49579
  return projectAnalysis;
49207
49580
  }
49208
49581
  function deriveProjectName(paths) {
@@ -50296,7 +50669,7 @@ var colors = {
50296
50669
  };
50297
50670
 
50298
50671
  // src/version.ts
50299
- var version = "4.9.26";
50672
+ var version = "4.9.28";
50300
50673
 
50301
50674
  // src/formatters.ts
50302
50675
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
@@ -50694,6 +51067,12 @@ function formatResults(results, verbose, crossFileData, profileSummary) {
50694
51067
  lines.push("");
50695
51068
  lines.push(formatCrossFilePaths(crossFileData.taintPaths));
50696
51069
  }
51070
+ if (crossFileData?.projectSizeBudgetExceeded) {
51071
+ lines.push("");
51072
+ lines.push(colors.yellow(`⚠ Project size ceiling reached — analysed ${crossFileData.filesAnalysed ?? "?"} of ${crossFileData.filesTotal ?? "?"} files.`));
51073
+ lines.push(colors.yellow(" Results cover the analysed files only; this is NOT a clean scan of the project."));
51074
+ lines.push(colors.yellow(" Raise or disable the cap with --max-project-source-chars <n> (0 = unlimited)."));
51075
+ }
50697
51076
  if (crossFileData?.budgetExceeded) {
50698
51077
  lines.push("");
50699
51078
  lines.push(colors.yellow("⚠ Cross-file budget exceeded — some cross-file taint paths may be missing."));
@@ -50714,6 +51093,7 @@ function formatJSON(results, crossFileData, profileSummary) {
50714
51093
  cross_file_taint_paths: crossFileData?.taintPaths ?? [],
50715
51094
  cross_file_calls: crossFileData?.crossFileCalls ?? [],
50716
51095
  cross_file_budget_exceeded: crossFileData?.budgetExceeded ?? false,
51096
+ project_size_budget_exceeded: crossFileData?.projectSizeBudgetExceeded ?? false,
50717
51097
  ...profileSummary ? { project_profile: profileSummary } : {},
50718
51098
  summary: {
50719
51099
  filesScanned: results.length,
@@ -50841,8 +51221,8 @@ function generateSarifResults(results, crossFileData) {
50841
51221
 
50842
51222
  // src/build-info.ts
50843
51223
  var buildInfo = {
50844
- gitSha: "178c681",
50845
- builtAt: "2026-09-24T18:11:45.249Z"
51224
+ gitSha: "7bfe5de",
51225
+ builtAt: "2026-10-01T00:24:15.337Z"
50846
51226
  };
50847
51227
 
50848
51228
  // src/utils/args.ts
@@ -50932,6 +51312,17 @@ SCAN OPTIONS:
50932
51312
  On exceed: partial taint paths kept, remaining
50933
51313
  cross-file phases skipped, cross_file_budget_exceeded
50934
51314
  surfaced in output (text warning / JSON / SARIF field).
51315
+ --max-project-source-chars <n>
51316
+ Cap the per-file phase by total source characters
51317
+ [default: scaled to this process's V8 heap limit
51318
+ (~7000 chars per MB, so ~29M at Node's default
51319
+ 4 GB heap) — 0 = unlimited. Raise it together
51320
+ with --max-old-space-size, not on its own.]
51321
+ A project large enough to exhaust the V8 heap would
51322
+ otherwise abort the process with no results at all.
51323
+ On exceed: remaining files skipped, results kept for
51324
+ the analysed subset, project_size_budget_exceeded
51325
+ surfaced in output (text warning / JSON field).
50935
51326
 
50936
51327
  METRICS OPTIONS:
50937
51328
  -l, --language <lang> Analyze only files for language (bash|csharp|go|html|java|javascript|typescript|python|rust)
@@ -51350,12 +51741,35 @@ async function scanFile(filePath, language, analyzeOpts) {
51350
51741
  };
51351
51742
  }
51352
51743
  }
51353
- async function scanProject(files, language, analyzeOpts, crossFileBudgetMs) {
51354
- const filesWithCode = files.map((f) => ({
51355
- code: readFileSync(f, "utf-8"),
51356
- filePath: f,
51357
- language: language || detectLanguage7(f)
51358
- }));
51744
+ var CHARS_PER_MB_OF_HEAP = 7000;
51745
+ function defaultMaxProjectSourceChars() {
51746
+ try {
51747
+ const heapMb = getHeapStatistics().heap_size_limit / (1024 * 1024);
51748
+ if (!Number.isFinite(heapMb) || heapMb <= 0)
51749
+ return 32000000;
51750
+ return Math.floor(heapMb * CHARS_PER_MB_OF_HEAP);
51751
+ } catch {
51752
+ return 32000000;
51753
+ }
51754
+ }
51755
+ async function scanProject(files, language, analyzeOpts, crossFileBudgetMs, maxProjectSourceChars) {
51756
+ const sourceCap = maxProjectSourceChars ?? defaultMaxProjectSourceChars();
51757
+ const filesWithCode = [];
51758
+ let sourceChars = 0;
51759
+ let readTruncated = false;
51760
+ for (const f of files) {
51761
+ const code = readFileSync(f, "utf-8");
51762
+ if (sourceCap > 0 && sourceChars + code.length > sourceCap && filesWithCode.length > 0) {
51763
+ readTruncated = true;
51764
+ break;
51765
+ }
51766
+ sourceChars += code.length;
51767
+ filesWithCode.push({
51768
+ code,
51769
+ filePath: f,
51770
+ language: language || detectLanguage7(f)
51771
+ });
51772
+ }
51359
51773
  const projectResult = await analyzeProject(filesWithCode, {
51360
51774
  passOptions: analyzeOpts?.passOptions,
51361
51775
  disabledPasses: analyzeOpts?.disabledPasses,
@@ -51391,7 +51805,10 @@ async function scanProject(files, language, analyzeOpts, crossFileBudgetMs) {
51391
51805
  crossFileData: {
51392
51806
  taintPaths: projectResult.taint_paths,
51393
51807
  crossFileCalls: projectResult.cross_file_calls,
51394
- budgetExceeded: projectResult.cross_file_budget_exceeded === true
51808
+ budgetExceeded: projectResult.cross_file_budget_exceeded === true,
51809
+ projectSizeBudgetExceeded: readTruncated || projectResult.project_size_budget_exceeded === true,
51810
+ filesAnalysed: projectResult.files.length,
51811
+ filesTotal: files.length
51395
51812
  }
51396
51813
  };
51397
51814
  }
@@ -51622,7 +52039,7 @@ async function runScan(targetPath, options) {
51622
52039
  if ((await stat2(absPath)).isDirectory()) {
51623
52040
  if (spin)
51624
52041
  spin.text = `Running project analysis on ${files.length} file(s)...`;
51625
- const projectScan = await scanProject(files, options.language, analyzeOpts, options.crossFileBudgetMs);
52042
+ const projectScan = await scanProject(files, options.language, analyzeOpts, options.crossFileBudgetMs, options.maxProjectSourceChars);
51626
52043
  results = projectScan.results;
51627
52044
  crossFileData = projectScan.crossFileData;
51628
52045
  } else {
@@ -51654,8 +52071,10 @@ async function runScan(targetPath, options) {
51654
52071
  spin.text = `Scanning... (${processed}/${files.length})`;
51655
52072
  }
51656
52073
  }
51657
- if (spin)
51658
- spin.succeed(`Scanned ${files.length} file(s)`);
52074
+ if (spin) {
52075
+ const analysed = crossFileData?.projectSizeBudgetExceeded ? crossFileData.filesAnalysed ?? files.length : files.length;
52076
+ spin.succeed(analysed === files.length ? `Scanned ${files.length} file(s)` : `Scanned ${analysed} of ${files.length} file(s) — stopped at the project size ceiling`);
52077
+ }
51659
52078
  if (suppressions.length > 0) {
51660
52079
  const beforeCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
51661
52080
  results = applySuppressionsToResults(results, suppressions, process.cwd());
@@ -51731,7 +52150,8 @@ async function runScan(targetPath, options) {
51731
52150
  const qualityCount = allVulns.filter((v) => v.category !== "security").length;
51732
52151
  const errors = results.filter((r) => r.error).length;
51733
52152
  const crossFilePaths = crossFileData?.taintPaths.length ?? 0;
51734
- const shouldOutput = totalVulns > 0 || crossFilePaths > 0 || errors > 0 || options.verbose || options.output || options.format !== "text";
52153
+ const truncated = crossFileData?.projectSizeBudgetExceeded === true;
52154
+ const shouldOutput = totalVulns > 0 || crossFilePaths > 0 || errors > 0 || options.verbose || options.output || options.format !== "text" || truncated;
51735
52155
  const profileSummary = buildProfileSummary(absPath, detectedModules, resolvedProfiles);
51736
52156
  if (resolvedProfiles) {
51737
52157
  for (const r of results) {
@@ -51763,15 +52183,17 @@ async function runScan(targetPath, options) {
51763
52183
  }
51764
52184
  if (!options.quiet && options.format === "text") {
51765
52185
  console.log();
52186
+ const scanned = crossFileData?.projectSizeBudgetExceeded ? crossFileData.filesAnalysed ?? files.length : files.length;
52187
+ const scannedLabel = crossFileData?.projectSizeBudgetExceeded ? `${scanned} of ${crossFileData.filesTotal ?? files.length} file(s) analysed` : `${scanned} file(s)`;
51766
52188
  if (securityCount > 0) {
51767
- console.log(colors.red(`Found ${securityCount} security finding(s) in ${files.length} file(s)`));
52189
+ console.log(colors.red(`Found ${securityCount} security finding(s) in ${scannedLabel}`));
51768
52190
  }
51769
52191
  if (qualityCount > 0) {
51770
52192
  const label = securityCount > 0 ? "Also found" : "Found";
51771
- console.log(colors.yellow(`${label} ${qualityCount} code quality finding(s) in ${files.length} file(s)`));
52193
+ console.log(colors.yellow(`${label} ${qualityCount} code quality finding(s) in ${scannedLabel}`));
51772
52194
  }
51773
52195
  if (securityCount === 0 && qualityCount === 0 && options.verbose) {
51774
- console.log(colors.green(`No findings in ${files.length} file(s)`));
52196
+ console.log(colors.green(`No findings in ${scannedLabel}`));
51775
52197
  }
51776
52198
  if (errors > 0) {
51777
52199
  console.log(colors.yellow(`${errors} file(s) had errors during analysis`));
@@ -52065,6 +52487,17 @@ function parseCrossFileBudgetMs(raw) {
52065
52487
  }
52066
52488
  return n;
52067
52489
  }
52490
+ function parseMaxProjectSourceChars(raw) {
52491
+ if (raw === undefined || raw === true || raw === "")
52492
+ return;
52493
+ const s = String(raw);
52494
+ const n = Number.parseInt(s, 10);
52495
+ if (!Number.isFinite(n) || n < 0 || String(n) !== s) {
52496
+ console.error(colors.yellow(`Warning: invalid --max-project-source-chars "${s}" (expected non-negative integer, 0 = unlimited); ignoring`));
52497
+ return;
52498
+ }
52499
+ return n;
52500
+ }
52068
52501
  var SBOM_MANIFESTS = {
52069
52502
  "package.json": parseNpmDependencies,
52070
52503
  "package-lock.json": parseNpmLockDependencies,
@@ -52262,6 +52695,7 @@ Usage: cognium-dev scan <path> [options]`);
52262
52695
  profile: options.profile || options.p,
52263
52696
  disablePass: options["disable-pass"],
52264
52697
  crossFileBudgetMs: parseCrossFileBudgetMs(options["cross-file-budget-ms"]),
52698
+ maxProjectSourceChars: parseMaxProjectSourceChars(options["max-project-source-chars"]),
52265
52699
  projectProfile: parseProjectProfileArg(options["project-profile"]),
52266
52700
  noProjectProfile: options["no-project-profile"] === true,
52267
52701
  projectProfileExplain: options["project-profile-explain"] === true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "4.9.26",
3
+ "version": "4.9.28",
4
4
  "description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
5
5
  "bin": {
6
6
  "cognium-dev": "dist/cli.js"
@@ -65,10 +65,10 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@cognium/project-profile-detect": "1.1.2",
68
- "circle-ir": "4.9.26"
68
+ "circle-ir": "4.9.28"
69
69
  },
70
70
  "devDependencies": {
71
- "@types/node": "^26.6.1",
71
+ "@types/node": "^26.6.2",
72
72
  "ajv": "^8.17.1",
73
73
  "ajv-draft-04": "^1.0.0",
74
74
  "bun-types": "^1.4.2",