cognium-dev 4.9.26 → 4.9.27

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 +142 -20
  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;
@@ -12703,6 +12704,7 @@ var DEFAULT_SINKS = [
12703
12704
  { method: "Deserialize", class: "ObjectStateFormatter", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["csharp"] },
12704
12705
  { method: "Raw", class: "Html", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12705
12706
  { method: "Write", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12707
+ { method: "WriteAsync", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12706
12708
  { method: "HtmlString", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12707
12709
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12708
12710
  { method: "Content", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -26783,18 +26785,23 @@ function findCSharpRequestSources(sourceCode, language) {
26783
26785
  const lines = sourceCode.split(`
26784
26786
  `);
26785
26787
  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
26788
  const consoleReadRe = /\bConsole\s*\.\s*ReadLine\s*\(/;
26788
26789
  const envReadRe = /\bEnvironment\s*\.\s*GetEnvironmentVariables?\s*(?:\(|\[)/;
26789
26790
  const formFileParams = new Set;
26790
26791
  const formFileDeclRe = /\bIFormFile\s+([A-Za-z_]\w*)/g;
26792
+ const httpRequestReceivers = new Set(["Request"]);
26793
+ const httpRequestDeclRe = /\b(?:HttpRequest|HttpRequestBase)\??\s+([A-Za-z_]\w*)/g;
26791
26794
  for (const line of lines) {
26792
- const re = new RegExp(formFileDeclRe.source, "g");
26795
+ const formRe = new RegExp(formFileDeclRe.source, "g");
26793
26796
  let d;
26794
- while ((d = re.exec(line)) !== null)
26797
+ while ((d = formRe.exec(line)) !== null)
26795
26798
  formFileParams.add(d[1]);
26799
+ const reqRe = new RegExp(httpRequestDeclRe.source, "g");
26800
+ while ((d = reqRe.exec(line)) !== null)
26801
+ httpRequestReceivers.add(d[1]);
26796
26802
  }
26797
26803
  const formFileReadRe = formFileParams.size > 0 ? new RegExp(`\\b(?:${[...formFileParams].join("|")})\\s*\\.\\s*(?:FileName|ContentType)\\b`) : null;
26804
+ const requestReadRe = new RegExp(`\\b(?:${[...httpRequestReceivers].join("|")})\\s*\\.\\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\\b`);
26798
26805
  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
26806
  const inlineReads = [
26800
26807
  [requestReadRe, "http_param"],
@@ -26819,10 +26826,37 @@ function findCSharpRequestSources(sourceCode, language) {
26819
26826
  };
26820
26827
  const exprStmtCallRe = /^\s*(?:await\s+)?[A-Za-z_][\w.]*(?:<[^>]*>)?\s*\(.*\)\s*;\s*$/;
26821
26828
  const nonCallKeywordRe = /^\s*(?:await\s+)?(?:if|while|for|foreach|switch|using|lock|return|throw|catch|when|nameof|typeof|sizeof|default)\b/;
26829
+ const embeddedAssignRe = /(?:^|[{;])\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*([^;{}]+)/;
26822
26830
  for (let i2 = 0;i2 < lines.length; i2++) {
26823
26831
  const m = assignRe.exec(lines[i2]);
26824
26832
  if (!m) {
26825
26833
  const stmt = lines[i2];
26834
+ if (stmt.includes("{") && !/^\s*(?:\/\/|\/\*|\*)/.test(stmt)) {
26835
+ const emb = new RegExp(embeddedAssignRe.source, "g");
26836
+ let em;
26837
+ let seeded = false;
26838
+ while ((em = emb.exec(stmt)) !== null) {
26839
+ const varName2 = em[1];
26840
+ const rhs2 = em[2];
26841
+ const type3 = classify(rhs2);
26842
+ if (!type3)
26843
+ continue;
26844
+ const lineNumber3 = i2 + 1;
26845
+ if (sources.some((s) => s.line === lineNumber3 && s.variable === varName2))
26846
+ continue;
26847
+ sources.push({
26848
+ type: type3,
26849
+ location: `${varName2} = ${rhs2.trim().substring(0, 50)}${rhs2.length > 50 ? "..." : ""}`,
26850
+ severity: "high",
26851
+ line: lineNumber3,
26852
+ confidence: 1,
26853
+ variable: varName2
26854
+ });
26855
+ seeded = true;
26856
+ }
26857
+ if (seeded)
26858
+ continue;
26859
+ }
26826
26860
  if (!exprStmtCallRe.test(stmt) || nonCallKeywordRe.test(stmt))
26827
26861
  continue;
26828
26862
  const hit = inlineRead(stmt);
@@ -49135,7 +49169,30 @@ async function analyzeProject(files, options = {}) {
49135
49169
  const perFileBudgetMs = options.perFileBudgetMs ?? 0;
49136
49170
  const perFileStart = Date.now();
49137
49171
  let perFileBudgetExceeded = false;
49172
+ const maxProjectSourceChars = options.maxProjectSourceChars ?? 0;
49173
+ const maxProjectFiles = options.maxProjectFiles ?? 0;
49174
+ let projectSourceChars = 0;
49175
+ let projectSizeBudgetExceeded = false;
49138
49176
  for (const { code, filePath, language } of files) {
49177
+ if (maxProjectFiles > 0 && fileAnalyses.length >= maxProjectFiles) {
49178
+ projectSizeBudgetExceeded = true;
49179
+ logger.warn("analyzeProject: file-count ceiling reached, skipping remaining files", {
49180
+ analysed: fileAnalyses.length,
49181
+ total: files.length,
49182
+ maxProjectFiles
49183
+ });
49184
+ break;
49185
+ }
49186
+ if (maxProjectSourceChars > 0 && projectSourceChars + code.length > maxProjectSourceChars) {
49187
+ projectSizeBudgetExceeded = true;
49188
+ logger.warn("analyzeProject: source-size ceiling reached, skipping remaining files", {
49189
+ analysed: fileAnalyses.length,
49190
+ total: files.length,
49191
+ sourceChars: projectSourceChars,
49192
+ maxProjectSourceChars
49193
+ });
49194
+ break;
49195
+ }
49139
49196
  if (perFileBudgetMs > 0 && Date.now() - perFileStart > perFileBudgetMs) {
49140
49197
  perFileBudgetExceeded = true;
49141
49198
  logger.warn("analyzeProject: per-file budget exceeded, skipping remaining files", {
@@ -49150,6 +49207,7 @@ async function analyzeProject(files, options = {}) {
49150
49207
  projectGraph.addFile(filePath, new CodeGraph(ir));
49151
49208
  sourceLinesByFile.set(filePath, code.split(`
49152
49209
  `));
49210
+ projectSourceChars += code.length;
49153
49211
  }
49154
49212
  const crossFileBudgetMs = options.crossFileBudgetMs ?? 300000;
49155
49213
  const crossFileResult = new CrossFilePass().run(projectGraph, sourceLinesByFile, { budgetMs: crossFileBudgetMs });
@@ -49203,6 +49261,9 @@ async function analyzeProject(files, options = {}) {
49203
49261
  if (perFileBudgetExceeded) {
49204
49262
  projectAnalysis.per_file_budget_exceeded = true;
49205
49263
  }
49264
+ if (projectSizeBudgetExceeded) {
49265
+ projectAnalysis.project_size_budget_exceeded = true;
49266
+ }
49206
49267
  return projectAnalysis;
49207
49268
  }
49208
49269
  function deriveProjectName(paths) {
@@ -50296,7 +50357,7 @@ var colors = {
50296
50357
  };
50297
50358
 
50298
50359
  // src/version.ts
50299
- var version = "4.9.26";
50360
+ var version = "4.9.27";
50300
50361
 
50301
50362
  // src/formatters.ts
50302
50363
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
@@ -50694,6 +50755,12 @@ function formatResults(results, verbose, crossFileData, profileSummary) {
50694
50755
  lines.push("");
50695
50756
  lines.push(formatCrossFilePaths(crossFileData.taintPaths));
50696
50757
  }
50758
+ if (crossFileData?.projectSizeBudgetExceeded) {
50759
+ lines.push("");
50760
+ lines.push(colors.yellow(`⚠ Project size ceiling reached — analysed ${crossFileData.filesAnalysed ?? "?"} of ${crossFileData.filesTotal ?? "?"} files.`));
50761
+ lines.push(colors.yellow(" Results cover the analysed files only; this is NOT a clean scan of the project."));
50762
+ lines.push(colors.yellow(" Raise or disable the cap with --max-project-source-chars <n> (0 = unlimited)."));
50763
+ }
50697
50764
  if (crossFileData?.budgetExceeded) {
50698
50765
  lines.push("");
50699
50766
  lines.push(colors.yellow("⚠ Cross-file budget exceeded — some cross-file taint paths may be missing."));
@@ -50714,6 +50781,7 @@ function formatJSON(results, crossFileData, profileSummary) {
50714
50781
  cross_file_taint_paths: crossFileData?.taintPaths ?? [],
50715
50782
  cross_file_calls: crossFileData?.crossFileCalls ?? [],
50716
50783
  cross_file_budget_exceeded: crossFileData?.budgetExceeded ?? false,
50784
+ project_size_budget_exceeded: crossFileData?.projectSizeBudgetExceeded ?? false,
50717
50785
  ...profileSummary ? { project_profile: profileSummary } : {},
50718
50786
  summary: {
50719
50787
  filesScanned: results.length,
@@ -50841,8 +50909,8 @@ function generateSarifResults(results, crossFileData) {
50841
50909
 
50842
50910
  // src/build-info.ts
50843
50911
  var buildInfo = {
50844
- gitSha: "178c681",
50845
- builtAt: "2026-09-24T18:11:45.249Z"
50912
+ gitSha: "39d07d9",
50913
+ builtAt: "2026-09-29T23:22:05.194Z"
50846
50914
  };
50847
50915
 
50848
50916
  // src/utils/args.ts
@@ -50932,6 +51000,17 @@ SCAN OPTIONS:
50932
51000
  On exceed: partial taint paths kept, remaining
50933
51001
  cross-file phases skipped, cross_file_budget_exceeded
50934
51002
  surfaced in output (text warning / JSON / SARIF field).
51003
+ --max-project-source-chars <n>
51004
+ Cap the per-file phase by total source characters
51005
+ [default: scaled to this process's V8 heap limit
51006
+ (~7000 chars per MB, so ~29M at Node's default
51007
+ 4 GB heap) — 0 = unlimited. Raise it together
51008
+ with --max-old-space-size, not on its own.]
51009
+ A project large enough to exhaust the V8 heap would
51010
+ otherwise abort the process with no results at all.
51011
+ On exceed: remaining files skipped, results kept for
51012
+ the analysed subset, project_size_budget_exceeded
51013
+ surfaced in output (text warning / JSON field).
50935
51014
 
50936
51015
  METRICS OPTIONS:
50937
51016
  -l, --language <lang> Analyze only files for language (bash|csharp|go|html|java|javascript|typescript|python|rust)
@@ -51350,12 +51429,35 @@ async function scanFile(filePath, language, analyzeOpts) {
51350
51429
  };
51351
51430
  }
51352
51431
  }
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
- }));
51432
+ var CHARS_PER_MB_OF_HEAP = 7000;
51433
+ function defaultMaxProjectSourceChars() {
51434
+ try {
51435
+ const heapMb = getHeapStatistics().heap_size_limit / (1024 * 1024);
51436
+ if (!Number.isFinite(heapMb) || heapMb <= 0)
51437
+ return 32000000;
51438
+ return Math.floor(heapMb * CHARS_PER_MB_OF_HEAP);
51439
+ } catch {
51440
+ return 32000000;
51441
+ }
51442
+ }
51443
+ async function scanProject(files, language, analyzeOpts, crossFileBudgetMs, maxProjectSourceChars) {
51444
+ const sourceCap = maxProjectSourceChars ?? defaultMaxProjectSourceChars();
51445
+ const filesWithCode = [];
51446
+ let sourceChars = 0;
51447
+ let readTruncated = false;
51448
+ for (const f of files) {
51449
+ const code = readFileSync(f, "utf-8");
51450
+ if (sourceCap > 0 && sourceChars + code.length > sourceCap && filesWithCode.length > 0) {
51451
+ readTruncated = true;
51452
+ break;
51453
+ }
51454
+ sourceChars += code.length;
51455
+ filesWithCode.push({
51456
+ code,
51457
+ filePath: f,
51458
+ language: language || detectLanguage7(f)
51459
+ });
51460
+ }
51359
51461
  const projectResult = await analyzeProject(filesWithCode, {
51360
51462
  passOptions: analyzeOpts?.passOptions,
51361
51463
  disabledPasses: analyzeOpts?.disabledPasses,
@@ -51391,7 +51493,10 @@ async function scanProject(files, language, analyzeOpts, crossFileBudgetMs) {
51391
51493
  crossFileData: {
51392
51494
  taintPaths: projectResult.taint_paths,
51393
51495
  crossFileCalls: projectResult.cross_file_calls,
51394
- budgetExceeded: projectResult.cross_file_budget_exceeded === true
51496
+ budgetExceeded: projectResult.cross_file_budget_exceeded === true,
51497
+ projectSizeBudgetExceeded: readTruncated || projectResult.project_size_budget_exceeded === true,
51498
+ filesAnalysed: projectResult.files.length,
51499
+ filesTotal: files.length
51395
51500
  }
51396
51501
  };
51397
51502
  }
@@ -51622,7 +51727,7 @@ async function runScan(targetPath, options) {
51622
51727
  if ((await stat2(absPath)).isDirectory()) {
51623
51728
  if (spin)
51624
51729
  spin.text = `Running project analysis on ${files.length} file(s)...`;
51625
- const projectScan = await scanProject(files, options.language, analyzeOpts, options.crossFileBudgetMs);
51730
+ const projectScan = await scanProject(files, options.language, analyzeOpts, options.crossFileBudgetMs, options.maxProjectSourceChars);
51626
51731
  results = projectScan.results;
51627
51732
  crossFileData = projectScan.crossFileData;
51628
51733
  } else {
@@ -51654,8 +51759,10 @@ async function runScan(targetPath, options) {
51654
51759
  spin.text = `Scanning... (${processed}/${files.length})`;
51655
51760
  }
51656
51761
  }
51657
- if (spin)
51658
- spin.succeed(`Scanned ${files.length} file(s)`);
51762
+ if (spin) {
51763
+ const analysed = crossFileData?.projectSizeBudgetExceeded ? crossFileData.filesAnalysed ?? files.length : files.length;
51764
+ spin.succeed(analysed === files.length ? `Scanned ${files.length} file(s)` : `Scanned ${analysed} of ${files.length} file(s) — stopped at the project size ceiling`);
51765
+ }
51659
51766
  if (suppressions.length > 0) {
51660
51767
  const beforeCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
51661
51768
  results = applySuppressionsToResults(results, suppressions, process.cwd());
@@ -51731,7 +51838,8 @@ async function runScan(targetPath, options) {
51731
51838
  const qualityCount = allVulns.filter((v) => v.category !== "security").length;
51732
51839
  const errors = results.filter((r) => r.error).length;
51733
51840
  const crossFilePaths = crossFileData?.taintPaths.length ?? 0;
51734
- const shouldOutput = totalVulns > 0 || crossFilePaths > 0 || errors > 0 || options.verbose || options.output || options.format !== "text";
51841
+ const truncated = crossFileData?.projectSizeBudgetExceeded === true;
51842
+ const shouldOutput = totalVulns > 0 || crossFilePaths > 0 || errors > 0 || options.verbose || options.output || options.format !== "text" || truncated;
51735
51843
  const profileSummary = buildProfileSummary(absPath, detectedModules, resolvedProfiles);
51736
51844
  if (resolvedProfiles) {
51737
51845
  for (const r of results) {
@@ -51763,15 +51871,17 @@ async function runScan(targetPath, options) {
51763
51871
  }
51764
51872
  if (!options.quiet && options.format === "text") {
51765
51873
  console.log();
51874
+ const scanned = crossFileData?.projectSizeBudgetExceeded ? crossFileData.filesAnalysed ?? files.length : files.length;
51875
+ const scannedLabel = crossFileData?.projectSizeBudgetExceeded ? `${scanned} of ${crossFileData.filesTotal ?? files.length} file(s) analysed` : `${scanned} file(s)`;
51766
51876
  if (securityCount > 0) {
51767
- console.log(colors.red(`Found ${securityCount} security finding(s) in ${files.length} file(s)`));
51877
+ console.log(colors.red(`Found ${securityCount} security finding(s) in ${scannedLabel}`));
51768
51878
  }
51769
51879
  if (qualityCount > 0) {
51770
51880
  const label = securityCount > 0 ? "Also found" : "Found";
51771
- console.log(colors.yellow(`${label} ${qualityCount} code quality finding(s) in ${files.length} file(s)`));
51881
+ console.log(colors.yellow(`${label} ${qualityCount} code quality finding(s) in ${scannedLabel}`));
51772
51882
  }
51773
51883
  if (securityCount === 0 && qualityCount === 0 && options.verbose) {
51774
- console.log(colors.green(`No findings in ${files.length} file(s)`));
51884
+ console.log(colors.green(`No findings in ${scannedLabel}`));
51775
51885
  }
51776
51886
  if (errors > 0) {
51777
51887
  console.log(colors.yellow(`${errors} file(s) had errors during analysis`));
@@ -52065,6 +52175,17 @@ function parseCrossFileBudgetMs(raw) {
52065
52175
  }
52066
52176
  return n;
52067
52177
  }
52178
+ function parseMaxProjectSourceChars(raw) {
52179
+ if (raw === undefined || raw === true || raw === "")
52180
+ return;
52181
+ const s = String(raw);
52182
+ const n = Number.parseInt(s, 10);
52183
+ if (!Number.isFinite(n) || n < 0 || String(n) !== s) {
52184
+ console.error(colors.yellow(`Warning: invalid --max-project-source-chars "${s}" (expected non-negative integer, 0 = unlimited); ignoring`));
52185
+ return;
52186
+ }
52187
+ return n;
52188
+ }
52068
52189
  var SBOM_MANIFESTS = {
52069
52190
  "package.json": parseNpmDependencies,
52070
52191
  "package-lock.json": parseNpmLockDependencies,
@@ -52262,6 +52383,7 @@ Usage: cognium-dev scan <path> [options]`);
52262
52383
  profile: options.profile || options.p,
52263
52384
  disablePass: options["disable-pass"],
52264
52385
  crossFileBudgetMs: parseCrossFileBudgetMs(options["cross-file-budget-ms"]),
52386
+ maxProjectSourceChars: parseMaxProjectSourceChars(options["max-project-source-chars"]),
52265
52387
  projectProfile: parseProjectProfileArg(options["project-profile"]),
52266
52388
  noProjectProfile: options["no-project-profile"] === true,
52267
52389
  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.27",
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.27"
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",