cognium-dev 4.13.1 → 4.14.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 (3) hide show
  1. package/README.md +4 -10
  2. package/dist/cli.js +226 -37
  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 },
@@ -11410,6 +11422,7 @@ var DEFAULT_SOURCES = [
11410
11422
  { method: "getFileName", class: "MimeBodyPart", type: "file_input", severity: "high", return_tainted: true },
11411
11423
  { method: "getDisposition", class: "Part", type: "file_input", severity: "medium", return_tainted: true },
11412
11424
  { method: "getName", class: "ZipEntry", type: "file_input", severity: "high", return_tainted: true },
11425
+ { method: "getName", class: "JarEntry", type: "file_input", severity: "high", return_tainted: true },
11413
11426
  { method: "getName", class: "ZipArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
11414
11427
  { method: "getName", class: "TarArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
11415
11428
  { method: "getName", class: "ArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
@@ -11858,7 +11871,7 @@ var DEFAULT_SINKS = [
11858
11871
  { method: "checkout", class: "SCM", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11859
11872
  { method: "retrieve", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11860
11873
  { 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] },
11874
+ { method: "CpsFlowExecution", class: "constructor", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11862
11875
  { method: "CpsFlowDefinition", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11863
11876
  { method: "FlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11864
11877
  { method: "processControlCommand", class: "TransportConnection", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
@@ -14009,12 +14022,14 @@ function isInterproceduralTaintableType(typeName, language) {
14009
14022
  function isParameterizedQueryCall(call, pattern) {
14010
14023
  if (pattern.type !== "sql_injection")
14011
14024
  return false;
14012
- const queryArg = call.arguments.find((a) => a.position === 0);
14025
+ const queryPos = pattern.arg_positions.length > 0 ? Math.min(...pattern.arg_positions) : 0;
14026
+ const queryArg = call.arguments.find((a) => a.position === queryPos);
14013
14027
  if (queryArg) {
14014
14028
  const queryText = queryArg.literal ?? queryArg.expression ?? "";
14015
14029
  const hasPlaceholders = /(\?(?:\s|,|$|\))|\$\d+|:\w+|%s)/.test(queryText);
14016
14030
  const hasConcatenation = /\+\s*[^+]/.test(queryText) || queryText.includes("${");
14017
- if (hasPlaceholders && !hasConcatenation && call.arguments.length >= 2) {
14031
+ const hasBoundArg = queryPos === 0 ? call.arguments.length >= 2 : call.arguments.some((a) => a.position > queryPos);
14032
+ if (hasPlaceholders && !hasConcatenation && hasBoundArg) {
14018
14033
  return true;
14019
14034
  }
14020
14035
  }
@@ -14757,6 +14772,17 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
14757
14772
  }
14758
14773
  return false;
14759
14774
  }
14775
+ function isArgumentlessJsResponseWriter(call, pattern, language) {
14776
+ if (pattern.type !== "xss" || pattern.class !== "Response")
14777
+ return false;
14778
+ if (language !== "javascript" && language !== "typescript")
14779
+ return false;
14780
+ const positions = pattern.arg_positions;
14781
+ if (!positions || positions.length === 0)
14782
+ return false;
14783
+ const argc = call.arguments?.length ?? 0;
14784
+ return positions.every((pos) => pos >= argc);
14785
+ }
14760
14786
  function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
14761
14787
  const patternsForLanguage = language === undefined ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
14762
14788
  const sinkMap = new Map;
@@ -14817,6 +14843,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
14817
14843
  continue;
14818
14844
  }
14819
14845
  }
14846
+ if (isArgumentlessJsResponseWriter(call, pattern, language)) {
14847
+ continue;
14848
+ }
14820
14849
  if ((pattern.type === "command_injection" || pattern.type === "sql_injection") && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
14821
14850
  continue;
14822
14851
  }
@@ -24259,6 +24288,17 @@ class VuePlugin extends BaseLanguagePlugin {
24259
24288
  }
24260
24289
 
24261
24290
  // ../circle-ir/dist/languages/plugins/go.js
24291
+ function sqlSinks(cls, methods) {
24292
+ return methods.map(([method, argPositions]) => ({
24293
+ method,
24294
+ class: cls,
24295
+ type: "sql_injection",
24296
+ cwe: "CWE-89",
24297
+ severity: "critical",
24298
+ argPositions
24299
+ }));
24300
+ }
24301
+
24262
24302
  class GoPlugin extends BaseLanguagePlugin {
24263
24303
  id = "go";
24264
24304
  name = "Go";
@@ -24443,6 +24483,32 @@ class GoPlugin extends BaseLanguagePlugin {
24443
24483
  severity: "critical",
24444
24484
  argPositions: [0]
24445
24485
  },
24486
+ ...sqlSinks("DB", [
24487
+ ["QueryContext", [1]],
24488
+ ["QueryRowContext", [1]],
24489
+ ["ExecContext", [1]],
24490
+ ["Prepare", [0]],
24491
+ ["PrepareContext", [1]]
24492
+ ]),
24493
+ ...sqlSinks("Tx", [
24494
+ ["Exec", [0]],
24495
+ ["QueryRow", [0]],
24496
+ ["QueryContext", [1]],
24497
+ ["QueryRowContext", [1]],
24498
+ ["ExecContext", [1]],
24499
+ ["Prepare", [0]],
24500
+ ["PrepareContext", [1]]
24501
+ ]),
24502
+ ...sqlSinks("Conn", [
24503
+ ["Query", [0]],
24504
+ ["QueryRow", [0]],
24505
+ ["Exec", [0]],
24506
+ ["QueryContext", [1]],
24507
+ ["QueryRowContext", [1]],
24508
+ ["ExecContext", [1]],
24509
+ ["Prepare", [0]],
24510
+ ["PrepareContext", [1]]
24511
+ ]),
24446
24512
  {
24447
24513
  method: "Command",
24448
24514
  class: "exec",
@@ -34356,8 +34422,7 @@ class CSharpCredit {
34356
34422
  }
34357
34423
  if (tainted.size === 0)
34358
34424
  return false;
34359
- const call = withoutStringLiterals(this.sinkText(sinkLine));
34360
- const rest = this.withoutSanitizerCalls(type, lastCallArguments(call) ?? call);
34425
+ const rest = this.withoutSanitizerCalls(type, this.receivedOperands(type, sinkLine));
34361
34426
  if (this.builtinSanitizes(type, rest))
34362
34427
  return false;
34363
34428
  for (const v of tainted)
@@ -34365,6 +34430,23 @@ class CSharpCredit {
34365
34430
  return true;
34366
34431
  return false;
34367
34432
  }
34433
+ receivedOperands(type, sinkLine) {
34434
+ const text = withoutStringLiterals(this.sinkText(sinkLine)).replace(/\s*;?\s*(?:\/\/.*)?$/, "");
34435
+ const property = /^\s*[A-Za-z_][\w.]*\.[A-Za-z_]\w*\s*=(?!=)\s*(.*)$/.exec(text);
34436
+ if (property)
34437
+ return property[1];
34438
+ if (!text.endsWith(")"))
34439
+ return text;
34440
+ const args2 = lastCallArguments(text);
34441
+ if (args2 === null)
34442
+ return text;
34443
+ const open = text.length - args2.length - 2;
34444
+ const callStart = text.slice(0, open).search(/[A-Za-z_][\w.]*\s*$/);
34445
+ const lastCall = callStart < 0 ? "" : text.slice(callStart);
34446
+ if (lastCall && this.withoutSanitizerCalls(type, lastCall).trim() === "")
34447
+ return text;
34448
+ return args2;
34449
+ }
34368
34450
  withoutSanitizerCalls(type, text) {
34369
34451
  const res = [];
34370
34452
  for (const { re, type: t } of CSHARP_SANITIZER_RES)
@@ -39931,6 +40013,8 @@ function analyzeInterprocedural2(graphOrTypes, callsOrSources, dfgOrSinks, sourc
39931
40013
  "QueryRowContext",
39932
40014
  "Exec",
39933
40015
  "ExecContext",
40016
+ "Prepare",
40017
+ "PrepareContext",
39934
40018
  "EscapeString",
39935
40019
  "HTMLEscapeString",
39936
40020
  "JSEscapeString",
@@ -44864,7 +44948,7 @@ function isGeneratedFile(file) {
44864
44948
  var PROVIDER_PATTERNS = [
44865
44949
  {
44866
44950
  name: "AWS access key",
44867
- regex: /\bAKIA[0-9A-Z]{16}\b/,
44951
+ regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
44868
44952
  severity: "critical",
44869
44953
  level: "error",
44870
44954
  fix: "Rotate the AWS access key immediately and move it to an environment variable or AWS Secrets Manager."
@@ -44932,6 +45016,20 @@ var PROVIDER_PATTERNS = [
44932
45016
  level: "warning",
44933
45017
  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."
44934
45018
  },
45019
+ {
45020
+ name: "Stripe test secret key",
45021
+ regex: /\bsk_test_[A-Za-z0-9]{24,}\b/,
45022
+ severity: "high",
45023
+ level: "warning",
45024
+ fix: "Rotate the Stripe test secret key in the Stripe Dashboard and load it from a secrets manager."
45025
+ },
45026
+ {
45027
+ name: "Stripe test restricted key",
45028
+ regex: /\brk_test_[A-Za-z0-9]{24,}\b/,
45029
+ severity: "high",
45030
+ level: "warning",
45031
+ fix: "Rotate the Stripe test restricted key in the Stripe Dashboard and load it from a secrets manager."
45032
+ },
44935
45033
  {
44936
45034
  name: "OpenAI API key",
44937
45035
  regex: /\bsk-[A-Za-z0-9]{48}\b/,
@@ -45276,7 +45374,7 @@ class ScanSecretsPass {
45276
45374
  const m = pattern.regex.exec(lineText);
45277
45375
  if (!m)
45278
45376
  continue;
45279
- if ((pattern.name === "Stripe live restricted key" || pattern.name === "GitLab personal access token") && PLACEHOLDER_RE.test(m[0])) {
45377
+ 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])) {
45280
45378
  continue;
45281
45379
  }
45282
45380
  if (pattern.name === "PEM private key" && !pemHasInlineBody(lines, i2)) {
@@ -50380,6 +50478,27 @@ function longestLineLength(code) {
50380
50478
  return max;
50381
50479
  }
50382
50480
  var MAX_ANALYZABLE_LINE_LENGTH = 1e4;
50481
+ function neutralizeOverlongLines(code) {
50482
+ if (longestLineLength(code) <= MAX_ANALYZABLE_LINE_LENGTH) {
50483
+ return { code, dropped: 0 };
50484
+ }
50485
+ let out2 = "";
50486
+ let start2 = 0;
50487
+ let dropped = 0;
50488
+ for (let i2 = 0;i2 <= code.length; i2++) {
50489
+ if (i2 !== code.length && code.charCodeAt(i2) !== 10)
50490
+ continue;
50491
+ if (i2 - start2 > MAX_ANALYZABLE_LINE_LENGTH)
50492
+ dropped++;
50493
+ else if (i2 > start2)
50494
+ out2 += code.slice(start2, i2);
50495
+ if (i2 < code.length)
50496
+ out2 += `
50497
+ `;
50498
+ start2 = i2 + 1;
50499
+ }
50500
+ return { code: out2, dropped };
50501
+ }
50383
50502
  async function analyze(code, filePath, language, options = {}) {
50384
50503
  if (!initialized) {
50385
50504
  await initAnalyzer(options);
@@ -50398,6 +50517,15 @@ async function analyze(code, filePath, language, options = {}) {
50398
50517
  parseGrammar = "tsx";
50399
50518
  }
50400
50519
  }
50520
+ const neutralized = neutralizeOverlongLines(code);
50521
+ if (neutralized.dropped > 0) {
50522
+ logger.warn("Neutralized over-long lines before analysis", {
50523
+ filePath,
50524
+ language,
50525
+ droppedLines: neutralized.dropped
50526
+ });
50527
+ code = neutralized.code;
50528
+ }
50401
50529
  logger.debug("Analyzing file", { filePath, language, parseGrammar, codeLength: code.length });
50402
50530
  const phaseTimingEnabled = globalThis.__circleIrPassTiming === true;
50403
50531
  const phaseStart = () => phaseTimingEnabled ? Date.now() : 0;
@@ -51970,7 +52098,7 @@ var colors = {
51970
52098
  };
51971
52099
 
51972
52100
  // src/version.ts
51973
- var version = "4.13.1";
52101
+ var version = "4.14.0";
51974
52102
 
51975
52103
  // src/formatters.ts
51976
52104
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
@@ -52431,6 +52559,21 @@ function formatSARIF(results, crossFileData, profileSummary) {
52431
52559
  }
52432
52560
  },
52433
52561
  results: generateSarifResults(results, crossFileData),
52562
+ ...crossFileData?.projectSizeBudgetExceeded ? {
52563
+ invocations: [
52564
+ {
52565
+ executionSuccessful: false,
52566
+ toolExecutionNotifications: [
52567
+ {
52568
+ level: "warning",
52569
+ message: {
52570
+ text: `Project size ceiling reached — analysed ${crossFileData.filesAnalysed ?? "?"} of ${crossFileData.filesTotal ?? "?"} files.`
52571
+ }
52572
+ }
52573
+ ]
52574
+ }
52575
+ ]
52576
+ } : {},
52434
52577
  ...profileSummary ? { properties: { projectProfile: profileSummary } } : {}
52435
52578
  }
52436
52579
  ]
@@ -52522,8 +52665,8 @@ function generateSarifResults(results, crossFileData) {
52522
52665
 
52523
52666
  // src/build-info.ts
52524
52667
  var buildInfo = {
52525
- gitSha: "51b2da7",
52526
- builtAt: "2026-10-08T18:47:29.958Z"
52668
+ gitSha: "c44e104",
52669
+ builtAt: "2026-10-11T06:19:30.837Z"
52527
52670
  };
52528
52671
 
52529
52672
  // src/utils/args.ts
@@ -52765,6 +52908,38 @@ function spinner(text) {
52765
52908
  }
52766
52909
 
52767
52910
  // src/cli.ts
52911
+ function collapseWs(s) {
52912
+ return s.replace(/\s+/g, " ").trim();
52913
+ }
52914
+ function fileMatches(suppFile, findingFile, basePath) {
52915
+ const wanted = suppFile.replace(/^\.\//, "");
52916
+ const relativeFile = relative4(basePath, findingFile) || findingFile;
52917
+ return wanted === relativeFile || wanted === findingFile;
52918
+ }
52919
+ function findingMatchesSuppression(supp, finding, basePath, lineAt) {
52920
+ if (supp.pass !== finding.pass && supp.pass !== finding.alsoPass)
52921
+ return false;
52922
+ if (supp.file && !fileMatches(supp.file, finding.file, basePath))
52923
+ return false;
52924
+ const snippet = supp.snippet ? collapseWs(supp.snippet) : "";
52925
+ if (snippet) {
52926
+ const fromFile = lineAt?.(finding.file, finding.line);
52927
+ const text = fromFile !== undefined ? fromFile : finding.lineText;
52928
+ return text !== undefined && collapseWs(text).includes(snippet);
52929
+ }
52930
+ if (supp.line !== undefined && supp.line !== finding.line)
52931
+ return false;
52932
+ return true;
52933
+ }
52934
+ function staleSuppressions(suppressions, matched) {
52935
+ return suppressions.filter((_, i2) => !matched.has(i2));
52936
+ }
52937
+ function reportStaleSuppressions(suppressions, matched) {
52938
+ for (const supp of staleSuppressions(suppressions, matched)) {
52939
+ const where = [supp.file, supp.snippet ? "snippet" : "", supp.line !== undefined && !supp.snippet ? `line ${supp.line}` : ""].filter(Boolean).join(" ");
52940
+ console.error(colors.dim(`Suppression for ${supp.pass}${where ? ` (${where})` : ""} matched no finding`));
52941
+ }
52942
+ }
52768
52943
  function loadConfig(profilePath) {
52769
52944
  const configPath = profilePath || "cognium.config.json";
52770
52945
  if (!existsSync(configPath)) {
@@ -52817,22 +52992,15 @@ function convertConfigToPassOptions(config) {
52817
52992
  }
52818
52993
  return { passOptions, disabledPasses };
52819
52994
  }
52820
- function applySuppressionsToResults(results, suppressions, basePath) {
52995
+ function applySuppressionsToResults(results, suppressions, basePath, match) {
52821
52996
  if (suppressions.length === 0)
52822
52997
  return results;
52823
52998
  return results.map((result) => {
52824
- const relativeFile = relative4(basePath, result.file) || result.file;
52825
52999
  const filteredVulns = result.vulnerabilities.filter((vuln) => {
52826
- for (const supp of suppressions) {
52827
- if (supp.pass !== vuln.type)
52828
- continue;
52829
- if (supp.file) {
52830
- const suppFile = supp.file.replace(/^\.\//, "");
52831
- if (suppFile !== relativeFile && suppFile !== result.file)
52832
- continue;
52833
- }
52834
- if (supp.line !== undefined && supp.line !== vuln.line)
53000
+ for (let i2 = 0;i2 < suppressions.length; i2++) {
53001
+ if (!findingMatchesSuppression(suppressions[i2], { pass: vuln.type, file: result.file, line: vuln.line }, basePath, match?.lineAt))
52835
53002
  continue;
53003
+ match?.matched?.add(i2);
52836
53004
  return false;
52837
53005
  }
52838
53006
  return true;
@@ -52840,22 +53008,20 @@ function applySuppressionsToResults(results, suppressions, basePath) {
52840
53008
  return { ...result, vulnerabilities: filteredVulns };
52841
53009
  });
52842
53010
  }
52843
- function applySuppressionsToTaintPaths(paths, suppressions, basePath) {
53011
+ function applySuppressionsToTaintPaths(paths, suppressions, basePath, match) {
52844
53012
  if (suppressions.length === 0)
52845
53013
  return paths;
52846
53014
  return paths.filter((path) => {
52847
- for (const supp of suppressions) {
52848
- const sinkType = path.sink.type;
52849
- if (supp.pass !== sinkType && supp.pass !== `cross-file-${sinkType}`)
52850
- continue;
52851
- if (supp.file) {
52852
- const suppFile = supp.file.replace(/^\.\//, "");
52853
- const relativeFile = relative4(basePath, path.sink.file) || path.sink.file;
52854
- if (suppFile !== relativeFile && suppFile !== path.sink.file)
52855
- continue;
52856
- }
52857
- if (supp.line !== undefined && supp.line !== path.sink.line)
52858
- continue;
53015
+ for (let i2 = 0;i2 < suppressions.length; i2++) {
53016
+ if (!findingMatchesSuppression(suppressions[i2], {
53017
+ pass: path.sink.type,
53018
+ alsoPass: `cross-file-${path.sink.type}`,
53019
+ file: path.sink.file,
53020
+ line: path.sink.line,
53021
+ lineText: path.sink.code
53022
+ }, basePath, match?.lineAt))
53023
+ continue;
53024
+ match?.matched?.add(i2);
52859
53025
  return false;
52860
53026
  }
52861
53027
  return true;
@@ -53378,14 +53544,35 @@ async function runScan(targetPath, options) {
53378
53544
  }
53379
53545
  if (suppressions.length > 0) {
53380
53546
  const beforeCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
53381
- results = applySuppressionsToResults(results, suppressions, process.cwd());
53547
+ const matched = new Set;
53548
+ const needsLines = suppressions.some((s) => s.snippet && collapseWs(s.snippet).length > 0);
53549
+ const lineCache = new Map;
53550
+ const lineAt = needsLines ? (file, line) => {
53551
+ let lines = lineCache.get(file);
53552
+ if (lines === undefined) {
53553
+ try {
53554
+ lines = readFileSync(file, "utf-8").split(`
53555
+ `);
53556
+ } catch {
53557
+ lines = null;
53558
+ }
53559
+ lineCache.set(file, lines);
53560
+ }
53561
+ if (!lines || line < 1)
53562
+ return;
53563
+ return lines[line - 1];
53564
+ } : undefined;
53565
+ const match = { lineAt, matched };
53566
+ results = applySuppressionsToResults(results, suppressions, process.cwd(), match);
53382
53567
  if (crossFileData) {
53383
- crossFileData.taintPaths = applySuppressionsToTaintPaths(crossFileData.taintPaths, suppressions, process.cwd());
53568
+ crossFileData.taintPaths = applySuppressionsToTaintPaths(crossFileData.taintPaths, suppressions, process.cwd(), match);
53384
53569
  }
53385
53570
  const afterCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
53386
53571
  if (!options.quiet && beforeCount !== afterCount) {
53387
53572
  console.error(colors.dim(`Suppressed ${beforeCount - afterCount} finding(s) via config`));
53388
53573
  }
53574
+ if (!options.quiet)
53575
+ reportStaleSuppressions(suppressions, matched);
53389
53576
  }
53390
53577
  const severityOrder = ["low", "medium", "high", "critical"];
53391
53578
  if (options.severity) {
@@ -54018,7 +54205,9 @@ main().catch((error) => {
54018
54205
  process.exit(2);
54019
54206
  });
54020
54207
  export {
54208
+ staleSuppressions,
54021
54209
  resolveScanLanguage,
54210
+ reportStaleSuppressions,
54022
54211
  matchesGlob,
54023
54212
  loadConfig,
54024
54213
  isTestFile2 as isTestFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "4.13.1",
3
+ "version": "4.14.0",
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.1"
68
+ "circle-ir": "4.14.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^26.6.4",