cognium-dev 4.13.2 → 4.14.1

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 (3) hide show
  1. package/README.md +4 -10
  2. package/dist/cli.js +290 -60
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -307,9 +307,9 @@ Filter to security findings only: `cognium-dev scan ./src --category security`
307
307
  | Rust | `.rs` | Actix-web, Rocket, Axum |
308
308
  | Bash | `.sh`, `.bash` | Shell scripts |
309
309
  | HTML | `.html`, `.htm` | Web extraction preprocessor |
310
- | C#/.NET _(experimental)_ | `.cs` | ASP.NET Core, ADO.NET, EF Core |
310
+ | C#/.NET _(Preview)_ | `.cs` | ASP.NET Core, ADO.NET (EF Core not yet verified) |
311
311
 
312
- > **C#/.NET is experimental / preview** (since 4.0.0). Straight-line taint
312
+ > **C#/.NET is Preview** (since 4.0.0). Straight-line taint
313
313
  > analysis across 10 CWE families (SQLi, command injection, path traversal,
314
314
  > SSRF, code injection, XSS, deserialization, LDAP, XPath, XXE). Not yet
315
315
  > benchmark-verified — expect gaps in branch/alias precision and detector breadth.
@@ -455,15 +455,9 @@ Cognium is built for speed:
455
455
  - **Core Engine**: [circle-ir](https://github.com/cogniumhq/cognium-dev/tree/main/packages/circle-ir) - High-performance SAST library
456
456
  - **Dependencies**: Only 1 runtime dependency (circle-ir)
457
457
 
458
- ## LLM Enhancement (Optional)
458
+ ## Data handling
459
459
 
460
- The core static analysis engine runs deterministically without any LLM. Optionally, you can enable LLM-based discovery modes for enhanced detection:
461
-
462
- - **Discovery Mode**: LLM reads source code to locate vulnerable methods from scratch
463
- - **Verification Mode**: Confirms whether static findings are actually exploitable
464
- - **Semantic Extraction**: Extracts design intent for automated gap analysis
465
-
466
- For details on LLM integration and benchmark improvements (42.5% → 78.3% on CWE-Bench with Claude Opus), visit [cognium.net](https://cognium.net).
460
+ cognium-dev is fully deterministic and runs entirely on your machine: source code is not sent to Cognium or any third-party service. Optional AI-assisted analysis is a separate product, [cognium-ai](https://cognium.net), which can send code snippets to a configured model endpoint; use cognium-dev (or cognium-ai with AI-assisted mode off and no remote engine configured) for air-gapped scanning.
467
461
 
468
462
  ## Benchmark Results
469
463
 
package/dist/cli.js CHANGED
@@ -5515,6 +5515,14 @@ function csharpBareName(node) {
5515
5515
  }
5516
5516
  return getNodeText(node);
5517
5517
  }
5518
+ function csharpCreatedSimpleType(creation) {
5519
+ const typeNode = creation.childForFieldName("type");
5520
+ if (!typeNode)
5521
+ return null;
5522
+ const bare = csharpBareName(typeNode).replace(/<[^>]*>/g, "");
5523
+ const simple = bare.split(".").pop()?.trim() ?? "";
5524
+ return simple && simple !== "var" ? simple : null;
5525
+ }
5518
5526
  function extractCSharpCalls(tree, cache) {
5519
5527
  const calls = [];
5520
5528
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -5523,11 +5531,15 @@ function extractCSharpCalls(tree, cache) {
5523
5531
  const fn = inv.childForFieldName("function");
5524
5532
  let methodName = "unknown";
5525
5533
  let receiver = null;
5534
+ let receiverTypeFromCreation = null;
5526
5535
  if (fn?.type === "member_access_expression") {
5527
5536
  const nameNode = fn.childForFieldName("name");
5528
5537
  const exprNode = fn.childForFieldName("expression");
5529
5538
  methodName = nameNode ? csharpBareName(nameNode) : "unknown";
5530
5539
  receiver = exprNode ? getNodeText(exprNode) : null;
5540
+ if (exprNode?.type === "object_creation_expression") {
5541
+ receiverTypeFromCreation = csharpCreatedSimpleType(exprNode);
5542
+ }
5531
5543
  } else if (fn) {
5532
5544
  methodName = csharpBareName(fn);
5533
5545
  }
@@ -5545,7 +5557,7 @@ function extractCSharpCalls(tree, cache) {
5545
5557
  calls.push({
5546
5558
  method_name: methodName,
5547
5559
  receiver,
5548
- receiver_type: receiver ? typeMap.get(receiver) ?? null : null,
5560
+ receiver_type: receiverTypeFromCreation ?? (receiver ? typeMap.get(receiver) ?? null : null),
5549
5561
  receiver_type_fqn: null,
5550
5562
  arguments: args2,
5551
5563
  location: { line: inv.startPosition.row + 1, column: inv.startPosition.column },
@@ -9629,6 +9641,38 @@ function extractFieldDefs(body2, startId) {
9629
9641
  function extractJSParameterDefs(params, startId) {
9630
9642
  const defs = [];
9631
9643
  let id = startId;
9644
+ const addPatternDefs = (patternNode, line) => {
9645
+ if (patternNode.type === "identifier" || patternNode.type === "shorthand_property_identifier_pattern") {
9646
+ defs.push({
9647
+ id: id++,
9648
+ variable: getNodeText(patternNode),
9649
+ line,
9650
+ kind: "param"
9651
+ });
9652
+ return;
9653
+ }
9654
+ if (patternNode.type === "rest_pattern" || patternNode.type === "rest_element") {
9655
+ const nameNode = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
9656
+ if (!nameNode)
9657
+ return;
9658
+ addPatternDefs(nameNode, line);
9659
+ return;
9660
+ }
9661
+ if (patternNode.type === "assignment_pattern") {
9662
+ const leftNode = patternNode.childForFieldName("left");
9663
+ if (leftNode)
9664
+ addPatternDefs(leftNode, line);
9665
+ return;
9666
+ }
9667
+ if (patternNode.type === "object_pattern" || patternNode.type === "array_pattern") {
9668
+ const destructDefs = extractDestructuringDefs(patternNode, id);
9669
+ for (const def of destructDefs) {
9670
+ def.kind = "param";
9671
+ defs.push(def);
9672
+ id++;
9673
+ }
9674
+ }
9675
+ };
9632
9676
  for (let i2 = 0;i2 < params.childCount; i2++) {
9633
9677
  const param = params.child(i2);
9634
9678
  if (!param)
@@ -9678,14 +9722,8 @@ function extractJSParameterDefs(params, startId) {
9678
9722
  }
9679
9723
  } else if (param.type === "required_parameter" || param.type === "optional_parameter") {
9680
9724
  const patternNode = param.childForFieldName("pattern");
9681
- if (patternNode && patternNode.type === "identifier") {
9682
- defs.push({
9683
- id: id++,
9684
- variable: getNodeText(patternNode),
9685
- line: param.startPosition.row + 1,
9686
- kind: "param"
9687
- });
9688
- }
9725
+ if (patternNode)
9726
+ addPatternDefs(patternNode, param.startPosition.row + 1);
9689
9727
  }
9690
9728
  }
9691
9729
  return defs;
@@ -11410,6 +11448,7 @@ var DEFAULT_SOURCES = [
11410
11448
  { method: "getFileName", class: "MimeBodyPart", type: "file_input", severity: "high", return_tainted: true },
11411
11449
  { method: "getDisposition", class: "Part", type: "file_input", severity: "medium", return_tainted: true },
11412
11450
  { method: "getName", class: "ZipEntry", type: "file_input", severity: "high", return_tainted: true },
11451
+ { method: "getName", class: "JarEntry", type: "file_input", severity: "high", return_tainted: true },
11413
11452
  { method: "getName", class: "ZipArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
11414
11453
  { method: "getName", class: "TarArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
11415
11454
  { method: "getName", class: "ArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
@@ -11858,7 +11897,7 @@ var DEFAULT_SINKS = [
11858
11897
  { method: "checkout", class: "SCM", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11859
11898
  { method: "retrieve", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11860
11899
  { method: "add", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11861
- { method: "CpsFlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11900
+ { method: "CpsFlowExecution", class: "constructor", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11862
11901
  { method: "CpsFlowDefinition", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11863
11902
  { method: "FlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11864
11903
  { method: "processControlCommand", class: "TransportConnection", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
@@ -13795,7 +13834,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
13795
13834
  continue;
13796
13835
  for (const param of method.parameters) {
13797
13836
  const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
13798
- const isTaintable = hasCSharpBindingAttr ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
13837
+ const isDestructureBinding = param.name.startsWith("{") || param.name.startsWith("[");
13838
+ const isTaintable = hasCSharpBindingAttr ? true : isDestructureBinding ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
13799
13839
  if (isTaintable) {
13800
13840
  const paramLine = param.line ?? method.start_line;
13801
13841
  sources.push({
@@ -14009,12 +14049,14 @@ function isInterproceduralTaintableType(typeName, language) {
14009
14049
  function isParameterizedQueryCall(call, pattern) {
14010
14050
  if (pattern.type !== "sql_injection")
14011
14051
  return false;
14012
- const queryArg = call.arguments.find((a) => a.position === 0);
14052
+ const queryPos = pattern.arg_positions.length > 0 ? Math.min(...pattern.arg_positions) : 0;
14053
+ const queryArg = call.arguments.find((a) => a.position === queryPos);
14013
14054
  if (queryArg) {
14014
14055
  const queryText = queryArg.literal ?? queryArg.expression ?? "";
14015
14056
  const hasPlaceholders = /(\?(?:\s|,|$|\))|\$\d+|:\w+|%s)/.test(queryText);
14016
14057
  const hasConcatenation = /\+\s*[^+]/.test(queryText) || queryText.includes("${");
14017
- if (hasPlaceholders && !hasConcatenation && call.arguments.length >= 2) {
14058
+ const hasBoundArg = queryPos === 0 ? call.arguments.length >= 2 : call.arguments.some((a) => a.position > queryPos);
14059
+ if (hasPlaceholders && !hasConcatenation && hasBoundArg) {
14018
14060
  return true;
14019
14061
  }
14020
14062
  }
@@ -14757,6 +14799,17 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
14757
14799
  }
14758
14800
  return false;
14759
14801
  }
14802
+ function isArgumentlessJsResponseWriter(call, pattern, language) {
14803
+ if (pattern.type !== "xss" || pattern.class !== "Response")
14804
+ return false;
14805
+ if (language !== "javascript" && language !== "typescript")
14806
+ return false;
14807
+ const positions = pattern.arg_positions;
14808
+ if (!positions || positions.length === 0)
14809
+ return false;
14810
+ const argc = call.arguments?.length ?? 0;
14811
+ return positions.every((pos) => pos >= argc);
14812
+ }
14760
14813
  function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
14761
14814
  const patternsForLanguage = language === undefined ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
14762
14815
  const sinkMap = new Map;
@@ -14817,6 +14870,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
14817
14870
  continue;
14818
14871
  }
14819
14872
  }
14873
+ if (isArgumentlessJsResponseWriter(call, pattern, language)) {
14874
+ continue;
14875
+ }
14820
14876
  if ((pattern.type === "command_injection" || pattern.type === "sql_injection") && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
14821
14877
  continue;
14822
14878
  }
@@ -18235,6 +18291,9 @@ function buildSanitizersByLine(sanitizers) {
18235
18291
  }
18236
18292
  return out2;
18237
18293
  }
18294
+ function concatOutsideStrings(expression) {
18295
+ return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
18296
+ }
18238
18297
  function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
18239
18298
  let graph;
18240
18299
  let sources;
@@ -18266,6 +18325,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
18266
18325
  }
18267
18326
  const taintedVars = [];
18268
18327
  const flows = [];
18328
+ const javaConcat = graph.ir.meta.language === "java";
18269
18329
  const reachableSinks = new Map;
18270
18330
  const defsByLine = graph.defsByLine;
18271
18331
  const usesByLine = graph.usesByLine;
@@ -18298,12 +18358,14 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
18298
18358
  continue;
18299
18359
  }
18300
18360
  }
18301
- const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
18361
+ const expr = typeof arg.expression === "string" ? arg.expression : "";
18362
+ const compoundConcat = javaConcat && !!arg.variable && expr !== arg.variable && concatOutsideStrings(expr);
18363
+ const candidateUses = arg.variable && !compoundConcat ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
18302
18364
  {
18303
18365
  for (const use of candidateUses) {
18304
18366
  if (use.def_id !== null) {
18305
18367
  if (allTaintedDefIds.has(use.def_id)) {
18306
- if (!arg.variable) {
18368
+ if (!arg.variable || compoundConcat) {
18307
18369
  if (typeof arg.expression !== "string" || arg.expression.length === 0)
18308
18370
  continue;
18309
18371
  const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
@@ -24259,6 +24321,17 @@ class VuePlugin extends BaseLanguagePlugin {
24259
24321
  }
24260
24322
 
24261
24323
  // ../circle-ir/dist/languages/plugins/go.js
24324
+ function sqlSinks(cls, methods) {
24325
+ return methods.map(([method, argPositions]) => ({
24326
+ method,
24327
+ class: cls,
24328
+ type: "sql_injection",
24329
+ cwe: "CWE-89",
24330
+ severity: "critical",
24331
+ argPositions
24332
+ }));
24333
+ }
24334
+
24262
24335
  class GoPlugin extends BaseLanguagePlugin {
24263
24336
  id = "go";
24264
24337
  name = "Go";
@@ -24443,6 +24516,32 @@ class GoPlugin extends BaseLanguagePlugin {
24443
24516
  severity: "critical",
24444
24517
  argPositions: [0]
24445
24518
  },
24519
+ ...sqlSinks("DB", [
24520
+ ["QueryContext", [1]],
24521
+ ["QueryRowContext", [1]],
24522
+ ["ExecContext", [1]],
24523
+ ["Prepare", [0]],
24524
+ ["PrepareContext", [1]]
24525
+ ]),
24526
+ ...sqlSinks("Tx", [
24527
+ ["Exec", [0]],
24528
+ ["QueryRow", [0]],
24529
+ ["QueryContext", [1]],
24530
+ ["QueryRowContext", [1]],
24531
+ ["ExecContext", [1]],
24532
+ ["Prepare", [0]],
24533
+ ["PrepareContext", [1]]
24534
+ ]),
24535
+ ...sqlSinks("Conn", [
24536
+ ["Query", [0]],
24537
+ ["QueryRow", [0]],
24538
+ ["Exec", [0]],
24539
+ ["QueryContext", [1]],
24540
+ ["QueryRowContext", [1]],
24541
+ ["ExecContext", [1]],
24542
+ ["Prepare", [0]],
24543
+ ["PrepareContext", [1]]
24544
+ ]),
24446
24545
  {
24447
24546
  method: "Command",
24448
24547
  class: "exec",
@@ -36411,6 +36510,22 @@ function evaluateSimpleExpression(expr, symbols) {
36411
36510
  return String(Math.floor(result));
36412
36511
  return expr;
36413
36512
  }
36513
+ var ARGUMENT_IDENT_SKIP = new Set(["new", "true", "false", "null", "this", "super"]);
36514
+ function isStringConcat(expression) {
36515
+ return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
36516
+ }
36517
+ function argumentIdentifiers(expression) {
36518
+ const stripped = expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ");
36519
+ const found = stripped.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? [];
36520
+ const out2 = [];
36521
+ for (const id of found) {
36522
+ if (ARGUMENT_IDENT_SKIP.has(id))
36523
+ continue;
36524
+ if (!out2.includes(id))
36525
+ out2.push(id);
36526
+ }
36527
+ return out2;
36528
+ }
36414
36529
  function isStringLiteralExpression(expr) {
36415
36530
  const trimmed = expr.trim();
36416
36531
  if (trimmed.length < 2)
@@ -36489,22 +36604,30 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
36489
36604
  if (language === "bash" && arg.expression === call.method_name && !arg.variable && arg.literal == null)
36490
36605
  continue;
36491
36606
  if (arg.variable && !arg.expression?.includes("[")) {
36492
- const varName = arg.variable;
36493
- const scopedName = methodName ? `${methodName}:${varName}` : varName;
36494
- if (fieldNames.has(varName) && !isInSynchronizedBlock) {
36495
- allArgsAreClean = false;
36496
- continue;
36607
+ const expr = arg.expression ?? "";
36608
+ const concatNames = language === "java" && isStringConcat(expr) ? argumentIdentifiers(expr) : [];
36609
+ const names = concatNames.length > 0 ? concatNames : [arg.variable];
36610
+ let argClean = true;
36611
+ for (const varName of names) {
36612
+ const scopedName = methodName ? `${methodName}:${varName}` : varName;
36613
+ if (fieldNames.has(varName) && !isInSynchronizedBlock) {
36614
+ argClean = false;
36615
+ break;
36616
+ }
36617
+ if (sanitizedVars?.has(scopedName) || sanitizedVars?.has(varName))
36618
+ continue;
36619
+ if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
36620
+ argClean = false;
36621
+ break;
36622
+ }
36623
+ const symbolValue = symbols.get(scopedName) ?? symbols.get(varName);
36624
+ if (symbolValue && symbolValue.type !== "unknown")
36625
+ continue;
36626
+ argClean = false;
36627
+ break;
36497
36628
  }
36498
- if (sanitizedVars?.has(scopedName) || sanitizedVars?.has(varName))
36499
- continue;
36500
- if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
36629
+ if (!argClean)
36501
36630
  allArgsAreClean = false;
36502
- continue;
36503
- }
36504
- const symbolValue = symbols.get(scopedName) ?? symbols.get(varName);
36505
- if (symbolValue && symbolValue.type !== "unknown")
36506
- continue;
36507
- allArgsAreClean = false;
36508
36631
  } else {
36509
36632
  if (arg.literal != null)
36510
36633
  continue;
@@ -39947,6 +40070,8 @@ function analyzeInterprocedural2(graphOrTypes, callsOrSources, dfgOrSinks, sourc
39947
40070
  "QueryRowContext",
39948
40071
  "Exec",
39949
40072
  "ExecContext",
40073
+ "Prepare",
40074
+ "PrepareContext",
39950
40075
  "EscapeString",
39951
40076
  "HTMLEscapeString",
39952
40077
  "JSEscapeString",
@@ -44880,7 +45005,7 @@ function isGeneratedFile(file) {
44880
45005
  var PROVIDER_PATTERNS = [
44881
45006
  {
44882
45007
  name: "AWS access key",
44883
- regex: /\bAKIA[0-9A-Z]{16}\b/,
45008
+ regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
44884
45009
  severity: "critical",
44885
45010
  level: "error",
44886
45011
  fix: "Rotate the AWS access key immediately and move it to an environment variable or AWS Secrets Manager."
@@ -44948,6 +45073,20 @@ var PROVIDER_PATTERNS = [
44948
45073
  level: "warning",
44949
45074
  fix: "Publishable keys are not secret but should still not be checked in to back-end source files; verify front-end vs back-end context."
44950
45075
  },
45076
+ {
45077
+ name: "Stripe test secret key",
45078
+ regex: /\bsk_test_[A-Za-z0-9]{24,}\b/,
45079
+ severity: "high",
45080
+ level: "warning",
45081
+ fix: "Rotate the Stripe test secret key in the Stripe Dashboard and load it from a secrets manager."
45082
+ },
45083
+ {
45084
+ name: "Stripe test restricted key",
45085
+ regex: /\brk_test_[A-Za-z0-9]{24,}\b/,
45086
+ severity: "high",
45087
+ level: "warning",
45088
+ fix: "Rotate the Stripe test restricted key in the Stripe Dashboard and load it from a secrets manager."
45089
+ },
44951
45090
  {
44952
45091
  name: "OpenAI API key",
44953
45092
  regex: /\bsk-[A-Za-z0-9]{48}\b/,
@@ -45292,7 +45431,7 @@ class ScanSecretsPass {
45292
45431
  const m = pattern.regex.exec(lineText);
45293
45432
  if (!m)
45294
45433
  continue;
45295
- if ((pattern.name === "Stripe live restricted key" || pattern.name === "GitLab personal access token") && PLACEHOLDER_RE.test(m[0])) {
45434
+ if ((pattern.name === "Stripe live restricted key" || pattern.name === "Stripe test secret key" || pattern.name === "Stripe test restricted key" || pattern.name === "GitLab personal access token") && PLACEHOLDER_RE.test(m[0])) {
45296
45435
  continue;
45297
45436
  }
45298
45437
  if (pattern.name === "PEM private key" && !pemHasInlineBody(lines, i2)) {
@@ -50396,6 +50535,27 @@ function longestLineLength(code) {
50396
50535
  return max;
50397
50536
  }
50398
50537
  var MAX_ANALYZABLE_LINE_LENGTH = 1e4;
50538
+ function neutralizeOverlongLines(code) {
50539
+ if (longestLineLength(code) <= MAX_ANALYZABLE_LINE_LENGTH) {
50540
+ return { code, dropped: 0 };
50541
+ }
50542
+ let out2 = "";
50543
+ let start2 = 0;
50544
+ let dropped = 0;
50545
+ for (let i2 = 0;i2 <= code.length; i2++) {
50546
+ if (i2 !== code.length && code.charCodeAt(i2) !== 10)
50547
+ continue;
50548
+ if (i2 - start2 > MAX_ANALYZABLE_LINE_LENGTH)
50549
+ dropped++;
50550
+ else if (i2 > start2)
50551
+ out2 += code.slice(start2, i2);
50552
+ if (i2 < code.length)
50553
+ out2 += `
50554
+ `;
50555
+ start2 = i2 + 1;
50556
+ }
50557
+ return { code: out2, dropped };
50558
+ }
50399
50559
  async function analyze(code, filePath, language, options = {}) {
50400
50560
  if (!initialized) {
50401
50561
  await initAnalyzer(options);
@@ -50414,6 +50574,15 @@ async function analyze(code, filePath, language, options = {}) {
50414
50574
  parseGrammar = "tsx";
50415
50575
  }
50416
50576
  }
50577
+ const neutralized = neutralizeOverlongLines(code);
50578
+ if (neutralized.dropped > 0) {
50579
+ logger.warn("Neutralized over-long lines before analysis", {
50580
+ filePath,
50581
+ language,
50582
+ droppedLines: neutralized.dropped
50583
+ });
50584
+ code = neutralized.code;
50585
+ }
50417
50586
  logger.debug("Analyzing file", { filePath, language, parseGrammar, codeLength: code.length });
50418
50587
  const phaseTimingEnabled = globalThis.__circleIrPassTiming === true;
50419
50588
  const phaseStart = () => phaseTimingEnabled ? Date.now() : 0;
@@ -51986,7 +52155,7 @@ var colors = {
51986
52155
  };
51987
52156
 
51988
52157
  // src/version.ts
51989
- var version = "4.13.2";
52158
+ var version = "4.14.1";
51990
52159
 
51991
52160
  // src/formatters.ts
51992
52161
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
@@ -52447,6 +52616,21 @@ function formatSARIF(results, crossFileData, profileSummary) {
52447
52616
  }
52448
52617
  },
52449
52618
  results: generateSarifResults(results, crossFileData),
52619
+ ...crossFileData?.projectSizeBudgetExceeded ? {
52620
+ invocations: [
52621
+ {
52622
+ executionSuccessful: false,
52623
+ toolExecutionNotifications: [
52624
+ {
52625
+ level: "warning",
52626
+ message: {
52627
+ text: `Project size ceiling reached — analysed ${crossFileData.filesAnalysed ?? "?"} of ${crossFileData.filesTotal ?? "?"} files.`
52628
+ }
52629
+ }
52630
+ ]
52631
+ }
52632
+ ]
52633
+ } : {},
52450
52634
  ...profileSummary ? { properties: { projectProfile: profileSummary } } : {}
52451
52635
  }
52452
52636
  ]
@@ -52538,8 +52722,8 @@ function generateSarifResults(results, crossFileData) {
52538
52722
 
52539
52723
  // src/build-info.ts
52540
52724
  var buildInfo = {
52541
- gitSha: "3937b7b",
52542
- builtAt: "2026-10-08T19:28:03.582Z"
52725
+ gitSha: "c8c74e9",
52726
+ builtAt: "2026-10-11T07:15:24.915Z"
52543
52727
  };
52544
52728
 
52545
52729
  // src/utils/args.ts
@@ -52781,6 +52965,38 @@ function spinner(text) {
52781
52965
  }
52782
52966
 
52783
52967
  // src/cli.ts
52968
+ function collapseWs(s) {
52969
+ return s.replace(/\s+/g, " ").trim();
52970
+ }
52971
+ function fileMatches(suppFile, findingFile, basePath) {
52972
+ const wanted = suppFile.replace(/^\.\//, "");
52973
+ const relativeFile = relative4(basePath, findingFile) || findingFile;
52974
+ return wanted === relativeFile || wanted === findingFile;
52975
+ }
52976
+ function findingMatchesSuppression(supp, finding, basePath, lineAt) {
52977
+ if (supp.pass !== finding.pass && supp.pass !== finding.alsoPass)
52978
+ return false;
52979
+ if (supp.file && !fileMatches(supp.file, finding.file, basePath))
52980
+ return false;
52981
+ const snippet = supp.snippet ? collapseWs(supp.snippet) : "";
52982
+ if (snippet) {
52983
+ const fromFile = lineAt?.(finding.file, finding.line);
52984
+ const text = fromFile !== undefined ? fromFile : finding.lineText;
52985
+ return text !== undefined && collapseWs(text).includes(snippet);
52986
+ }
52987
+ if (supp.line !== undefined && supp.line !== finding.line)
52988
+ return false;
52989
+ return true;
52990
+ }
52991
+ function staleSuppressions(suppressions, matched) {
52992
+ return suppressions.filter((_, i2) => !matched.has(i2));
52993
+ }
52994
+ function reportStaleSuppressions(suppressions, matched) {
52995
+ for (const supp of staleSuppressions(suppressions, matched)) {
52996
+ const where = [supp.file, supp.snippet ? "snippet" : "", supp.line !== undefined && !supp.snippet ? `line ${supp.line}` : ""].filter(Boolean).join(" ");
52997
+ console.error(colors.dim(`Suppression for ${supp.pass}${where ? ` (${where})` : ""} matched no finding`));
52998
+ }
52999
+ }
52784
53000
  function loadConfig(profilePath) {
52785
53001
  const configPath = profilePath || "cognium.config.json";
52786
53002
  if (!existsSync(configPath)) {
@@ -52833,22 +53049,15 @@ function convertConfigToPassOptions(config) {
52833
53049
  }
52834
53050
  return { passOptions, disabledPasses };
52835
53051
  }
52836
- function applySuppressionsToResults(results, suppressions, basePath) {
53052
+ function applySuppressionsToResults(results, suppressions, basePath, match) {
52837
53053
  if (suppressions.length === 0)
52838
53054
  return results;
52839
53055
  return results.map((result) => {
52840
- const relativeFile = relative4(basePath, result.file) || result.file;
52841
53056
  const filteredVulns = result.vulnerabilities.filter((vuln) => {
52842
- for (const supp of suppressions) {
52843
- if (supp.pass !== vuln.type)
52844
- continue;
52845
- if (supp.file) {
52846
- const suppFile = supp.file.replace(/^\.\//, "");
52847
- if (suppFile !== relativeFile && suppFile !== result.file)
52848
- continue;
52849
- }
52850
- if (supp.line !== undefined && supp.line !== vuln.line)
53057
+ for (let i2 = 0;i2 < suppressions.length; i2++) {
53058
+ if (!findingMatchesSuppression(suppressions[i2], { pass: vuln.type, file: result.file, line: vuln.line }, basePath, match?.lineAt))
52851
53059
  continue;
53060
+ match?.matched?.add(i2);
52852
53061
  return false;
52853
53062
  }
52854
53063
  return true;
@@ -52856,22 +53065,20 @@ function applySuppressionsToResults(results, suppressions, basePath) {
52856
53065
  return { ...result, vulnerabilities: filteredVulns };
52857
53066
  });
52858
53067
  }
52859
- function applySuppressionsToTaintPaths(paths, suppressions, basePath) {
53068
+ function applySuppressionsToTaintPaths(paths, suppressions, basePath, match) {
52860
53069
  if (suppressions.length === 0)
52861
53070
  return paths;
52862
53071
  return paths.filter((path) => {
52863
- for (const supp of suppressions) {
52864
- const sinkType = path.sink.type;
52865
- if (supp.pass !== sinkType && supp.pass !== `cross-file-${sinkType}`)
52866
- continue;
52867
- if (supp.file) {
52868
- const suppFile = supp.file.replace(/^\.\//, "");
52869
- const relativeFile = relative4(basePath, path.sink.file) || path.sink.file;
52870
- if (suppFile !== relativeFile && suppFile !== path.sink.file)
52871
- continue;
52872
- }
52873
- if (supp.line !== undefined && supp.line !== path.sink.line)
52874
- continue;
53072
+ for (let i2 = 0;i2 < suppressions.length; i2++) {
53073
+ if (!findingMatchesSuppression(suppressions[i2], {
53074
+ pass: path.sink.type,
53075
+ alsoPass: `cross-file-${path.sink.type}`,
53076
+ file: path.sink.file,
53077
+ line: path.sink.line,
53078
+ lineText: path.sink.code
53079
+ }, basePath, match?.lineAt))
53080
+ continue;
53081
+ match?.matched?.add(i2);
52875
53082
  return false;
52876
53083
  }
52877
53084
  return true;
@@ -53394,14 +53601,35 @@ async function runScan(targetPath, options) {
53394
53601
  }
53395
53602
  if (suppressions.length > 0) {
53396
53603
  const beforeCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
53397
- results = applySuppressionsToResults(results, suppressions, process.cwd());
53604
+ const matched = new Set;
53605
+ const needsLines = suppressions.some((s) => s.snippet && collapseWs(s.snippet).length > 0);
53606
+ const lineCache = new Map;
53607
+ const lineAt = needsLines ? (file, line) => {
53608
+ let lines = lineCache.get(file);
53609
+ if (lines === undefined) {
53610
+ try {
53611
+ lines = readFileSync(file, "utf-8").split(`
53612
+ `);
53613
+ } catch {
53614
+ lines = null;
53615
+ }
53616
+ lineCache.set(file, lines);
53617
+ }
53618
+ if (!lines || line < 1)
53619
+ return;
53620
+ return lines[line - 1];
53621
+ } : undefined;
53622
+ const match = { lineAt, matched };
53623
+ results = applySuppressionsToResults(results, suppressions, process.cwd(), match);
53398
53624
  if (crossFileData) {
53399
- crossFileData.taintPaths = applySuppressionsToTaintPaths(crossFileData.taintPaths, suppressions, process.cwd());
53625
+ crossFileData.taintPaths = applySuppressionsToTaintPaths(crossFileData.taintPaths, suppressions, process.cwd(), match);
53400
53626
  }
53401
53627
  const afterCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
53402
53628
  if (!options.quiet && beforeCount !== afterCount) {
53403
53629
  console.error(colors.dim(`Suppressed ${beforeCount - afterCount} finding(s) via config`));
53404
53630
  }
53631
+ if (!options.quiet)
53632
+ reportStaleSuppressions(suppressions, matched);
53405
53633
  }
53406
53634
  const severityOrder = ["low", "medium", "high", "critical"];
53407
53635
  if (options.severity) {
@@ -54034,7 +54262,9 @@ main().catch((error) => {
54034
54262
  process.exit(2);
54035
54263
  });
54036
54264
  export {
54265
+ staleSuppressions,
54037
54266
  resolveScanLanguage,
54267
+ reportStaleSuppressions,
54038
54268
  matchesGlob,
54039
54269
  loadConfig,
54040
54270
  isTestFile2 as isTestFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "4.13.2",
3
+ "version": "4.14.1",
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,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@cognium/project-profile-detect": "1.1.2",
68
- "circle-ir": "4.13.2"
68
+ "circle-ir": "4.14.1"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^26.6.4",