cognium-dev 4.9.25 → 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 +273 -34
  2. package/package.json +4 -4
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;
@@ -3349,8 +3350,9 @@ async function initParser(options = {}) {
3349
3350
  });
3350
3351
  }
3351
3352
  parserInitialized = true;
3353
+ })().finally(() => {
3352
3354
  parserInitializing = null;
3353
- })();
3355
+ });
3354
3356
  return parserInitializing;
3355
3357
  }
3356
3358
  async function loadLanguage(language, wasmPath) {
@@ -12702,6 +12704,7 @@ var DEFAULT_SINKS = [
12702
12704
  { method: "Deserialize", class: "ObjectStateFormatter", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["csharp"] },
12703
12705
  { method: "Raw", class: "Html", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12704
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"] },
12705
12708
  { method: "HtmlString", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12706
12709
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
12707
12710
  { method: "Content", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -26727,6 +26730,52 @@ function findGoRequestBodySources(sourceCode, language) {
26727
26730
  continue;
26728
26731
  }
26729
26732
  }
26733
+ const wrapRe = new RegExp(`^\\s*(?:var\\s+)?([A-Za-z_]\\w*)\\s*:?=\\s*bufio\\s*\\.\\s*New(?:Scanner|Reader|ReaderSize)\\s*\\(.*${bodyRef}`);
26734
+ const braceDelta = (s) => {
26735
+ const code = s.replace(/"(?:[^"\\]|\\.)*"|`[^`]*`|'(?:[^'\\]|\\.)*'/g, '""').replace(/\/\/.*$/, "");
26736
+ let d = 0;
26737
+ for (const ch of code) {
26738
+ if (ch === "{")
26739
+ d++;
26740
+ else if (ch === "}")
26741
+ d--;
26742
+ }
26743
+ return d;
26744
+ };
26745
+ for (let i2 = 0;i2 < lines.length; i2++) {
26746
+ const w = wrapRe.exec(lines[i2]);
26747
+ if (!w)
26748
+ continue;
26749
+ const wrapper = w[1];
26750
+ if (wrapper === "_")
26751
+ continue;
26752
+ const readRe = new RegExp(`(?<![\\w.])${wrapper}\\s*\\.\\s*(Text|Bytes|ReadString|ReadBytes|ReadLine|ReadSlice)\\s*\\(`);
26753
+ const assignRe = new RegExp(`^\\s*(?:var\\s+)?([A-Za-z_][\\w\\s,]*?)\\s*:?=(?!=)\\s*(?:string\\s*\\(\\s*)?${wrapper}\\s*\\.\\s*(?:Text|Bytes|ReadString|ReadBytes|ReadLine|ReadSlice)\\s*\\(`);
26754
+ const rebindRe = new RegExp(`(?<![\\w.])${wrapper}\\s*:?=(?!=)`);
26755
+ let depth = 0;
26756
+ for (let j = i2 + 1;j < lines.length; j++) {
26757
+ depth += braceDelta(lines[j - 1]);
26758
+ if (depth < 0)
26759
+ break;
26760
+ const line = lines[j];
26761
+ if (/^\s*\/\//.test(line))
26762
+ continue;
26763
+ if (rebindRe.test(line))
26764
+ break;
26765
+ const rd = readRe.exec(line);
26766
+ if (!rd)
26767
+ continue;
26768
+ const as = assignRe.exec(line);
26769
+ let variable = "<inline>";
26770
+ if (as) {
26771
+ const bound = as[1].split(",").map((v) => v.trim()).find((v) => v !== "_" && v !== "err" && /^[A-Za-z_]\w*$/.test(v));
26772
+ if (!bound)
26773
+ continue;
26774
+ variable = bound;
26775
+ }
26776
+ push(variable, j + 1, `bufio ${wrapper}.${rd[1]}() over req.Body`);
26777
+ }
26778
+ }
26730
26779
  return sources;
26731
26780
  }
26732
26781
  function findCSharpRequestSources(sourceCode, language) {
@@ -26736,24 +26785,101 @@ function findCSharpRequestSources(sourceCode, language) {
26736
26785
  const lines = sourceCode.split(`
26737
26786
  `);
26738
26787
  const assignRe = /^\s*(?:await\s+)?(?:using\s*\(?\s*)?(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
26739
- const requestReadRe = /\bRequest\s*\.\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\b/;
26740
26788
  const consoleReadRe = /\bConsole\s*\.\s*ReadLine\s*\(/;
26741
26789
  const envReadRe = /\bEnvironment\s*\.\s*GetEnvironmentVariables?\s*(?:\(|\[)/;
26742
26790
  const formFileParams = new Set;
26743
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;
26744
26794
  for (const line of lines) {
26745
- const re = new RegExp(formFileDeclRe.source, "g");
26795
+ const formRe = new RegExp(formFileDeclRe.source, "g");
26746
26796
  let d;
26747
- while ((d = re.exec(line)) !== null)
26797
+ while ((d = formRe.exec(line)) !== null)
26748
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]);
26749
26802
  }
26750
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`);
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;
26806
+ const inlineReads = [
26807
+ [requestReadRe, "http_param"],
26808
+ [formFileReadRe, "http_param"],
26809
+ [consoleReadRe, "io_input"],
26810
+ [envReadRe, "env_input"]
26811
+ ];
26812
+ const inlineRead = (text) => {
26813
+ const argsStart = text.indexOf("(");
26814
+ for (const [re, type] of inlineReads) {
26815
+ if (!re)
26816
+ continue;
26817
+ const g = new RegExp(re.source, "g");
26818
+ let hit;
26819
+ while ((hit = g.exec(text)) !== null) {
26820
+ if (hit.index > argsStart) {
26821
+ return { type, read: hit[0].replace(/\s+/g, "").replace(/[([]$/, "") };
26822
+ }
26823
+ }
26824
+ }
26825
+ return null;
26826
+ };
26827
+ const exprStmtCallRe = /^\s*(?:await\s+)?[A-Za-z_][\w.]*(?:<[^>]*>)?\s*\(.*\)\s*;\s*$/;
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*([^;{}]+)/;
26751
26830
  for (let i2 = 0;i2 < lines.length; i2++) {
26752
26831
  const m = assignRe.exec(lines[i2]);
26753
- if (!m)
26832
+ if (!m) {
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
+ }
26860
+ if (!exprStmtCallRe.test(stmt) || nonCallKeywordRe.test(stmt))
26861
+ continue;
26862
+ const hit = inlineRead(stmt);
26863
+ if (!hit)
26864
+ continue;
26865
+ const { type: type2, read } = hit;
26866
+ const lineNumber2 = i2 + 1;
26867
+ if (sources.some((s) => s.line === lineNumber2))
26868
+ continue;
26869
+ const code = stmt.trim();
26870
+ sources.push({
26871
+ type: type2,
26872
+ location: `${read} in ${code.substring(0, 50)}${code.length > 50 ? "..." : ""}`,
26873
+ severity: "high",
26874
+ line: lineNumber2,
26875
+ confidence: 1,
26876
+ variable: "<inline>",
26877
+ code
26878
+ });
26754
26879
  continue;
26880
+ }
26755
26881
  const [, varName, rhs] = m;
26756
- const type = requestReadRe.test(rhs) ? "http_param" : formFileReadRe?.test(rhs) ? "http_param" : consoleReadRe.test(rhs) ? "io_input" : envReadRe.test(rhs) ? "env_input" : null;
26882
+ const type = classify(rhs);
26757
26883
  if (!type)
26758
26884
  continue;
26759
26885
  const lineNumber = i2 + 1;
@@ -37425,6 +37551,18 @@ function maskStringLiteralsForTokenScan(expr) {
37425
37551
  }
37426
37552
  function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
37427
37553
  const flows = [];
37554
+ const goFnRanges = language === "go" && types ? types.flatMap((t) => t.methods.map((m) => [m.start_line, m.end_line])) : [];
37555
+ const goFnStart = (line) => {
37556
+ let best = -1;
37557
+ let bestSpan = -1;
37558
+ for (const [start2, end] of goFnRanges) {
37559
+ if (line >= start2 && line <= end && end - start2 > bestSpan) {
37560
+ best = start2;
37561
+ bestSpan = end - start2;
37562
+ }
37563
+ }
37564
+ return best;
37565
+ };
37428
37566
  const sourcesWithVar = sources.filter((s) => typeof s.variable === "string" && s.variable.length > 0);
37429
37567
  const aliasSanitizedFor = new Map;
37430
37568
  if (language === "python" && typeof code === "string") {
@@ -37833,6 +37971,12 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
37833
37971
  if (source.in_method && call.in_method && source.in_method !== call.in_method) {
37834
37972
  continue;
37835
37973
  }
37974
+ if (goFnRanges.length > 0) {
37975
+ const srcFn = goFnStart(source.line);
37976
+ const sinkFn = goFnStart(sink.line);
37977
+ if (srcFn !== -1 && sinkFn !== -1 && srcFn !== sinkFn)
37978
+ continue;
37979
+ }
37836
37980
  if (complex) {
37837
37981
  const re = reCache.get(source.variable);
37838
37982
  if (!re || !re.test(expr))
@@ -49025,7 +49169,30 @@ async function analyzeProject(files, options = {}) {
49025
49169
  const perFileBudgetMs = options.perFileBudgetMs ?? 0;
49026
49170
  const perFileStart = Date.now();
49027
49171
  let perFileBudgetExceeded = false;
49172
+ const maxProjectSourceChars = options.maxProjectSourceChars ?? 0;
49173
+ const maxProjectFiles = options.maxProjectFiles ?? 0;
49174
+ let projectSourceChars = 0;
49175
+ let projectSizeBudgetExceeded = false;
49028
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
+ }
49029
49196
  if (perFileBudgetMs > 0 && Date.now() - perFileStart > perFileBudgetMs) {
49030
49197
  perFileBudgetExceeded = true;
49031
49198
  logger.warn("analyzeProject: per-file budget exceeded, skipping remaining files", {
@@ -49040,6 +49207,7 @@ async function analyzeProject(files, options = {}) {
49040
49207
  projectGraph.addFile(filePath, new CodeGraph(ir));
49041
49208
  sourceLinesByFile.set(filePath, code.split(`
49042
49209
  `));
49210
+ projectSourceChars += code.length;
49043
49211
  }
49044
49212
  const crossFileBudgetMs = options.crossFileBudgetMs ?? 300000;
49045
49213
  const crossFileResult = new CrossFilePass().run(projectGraph, sourceLinesByFile, { budgetMs: crossFileBudgetMs });
@@ -49093,6 +49261,9 @@ async function analyzeProject(files, options = {}) {
49093
49261
  if (perFileBudgetExceeded) {
49094
49262
  projectAnalysis.per_file_budget_exceeded = true;
49095
49263
  }
49264
+ if (projectSizeBudgetExceeded) {
49265
+ projectAnalysis.project_size_budget_exceeded = true;
49266
+ }
49096
49267
  return projectAnalysis;
49097
49268
  }
49098
49269
  function deriveProjectName(paths) {
@@ -49945,18 +50116,24 @@ async function enumerate(dir, out2) {
49945
50116
  }
49946
50117
  }
49947
50118
  }
49948
- function ownerOf(file, modules) {
49949
- let best;
49950
- let bestLen = -1;
50119
+ function createOwnerIndex(modules) {
50120
+ const byRoot = new Map;
49951
50121
  for (const m of modules) {
49952
- if (file === m.root || file.startsWith(m.root + "/")) {
49953
- if (m.root.length > bestLen) {
49954
- best = m;
49955
- bestLen = m.root.length;
49956
- }
50122
+ if (!byRoot.has(m.root))
50123
+ byRoot.set(m.root, m);
50124
+ }
50125
+ return (file) => {
50126
+ let p = file;
50127
+ for (;; ) {
50128
+ const hit = byRoot.get(p);
50129
+ if (hit)
50130
+ return hit;
50131
+ const slash = p.lastIndexOf("/");
50132
+ if (slash <= 0)
50133
+ return;
50134
+ p = p.slice(0, slash);
49957
50135
  }
49958
- }
49959
- return best;
50136
+ };
49960
50137
  }
49961
50138
 
49962
50139
  // ../project-profile-detect/dist/publication-detect.js
@@ -50115,6 +50292,7 @@ async function detectProjectProfiles(scanRoot, options = {}) {
50115
50292
  }
50116
50293
  const profileByFile = new Map;
50117
50294
  const unknownFiles = [];
50295
+ const ownerIndex = createOwnerIndex(modules);
50118
50296
  for (const file of files) {
50119
50297
  const rel = relative3(scanRoot, file);
50120
50298
  const ov = applyOverrides(rel, compiledOverrides);
@@ -50128,7 +50306,7 @@ async function detectProjectProfiles(scanRoot, options = {}) {
50128
50306
  profileByFile.set(file, `${shape2}/${env2}`);
50129
50307
  continue;
50130
50308
  }
50131
- const owner = ownerOf(file, modules);
50309
+ const owner = ownerIndex(file);
50132
50310
  if (!owner) {
50133
50311
  profileByFile.set(file, "unknown");
50134
50312
  unknownFiles.push(file);
@@ -50179,7 +50357,7 @@ var colors = {
50179
50357
  };
50180
50358
 
50181
50359
  // src/version.ts
50182
- var version = "4.9.25";
50360
+ var version = "4.9.27";
50183
50361
 
50184
50362
  // src/formatters.ts
50185
50363
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
@@ -50577,6 +50755,12 @@ function formatResults(results, verbose, crossFileData, profileSummary) {
50577
50755
  lines.push("");
50578
50756
  lines.push(formatCrossFilePaths(crossFileData.taintPaths));
50579
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
+ }
50580
50764
  if (crossFileData?.budgetExceeded) {
50581
50765
  lines.push("");
50582
50766
  lines.push(colors.yellow("⚠ Cross-file budget exceeded — some cross-file taint paths may be missing."));
@@ -50597,6 +50781,7 @@ function formatJSON(results, crossFileData, profileSummary) {
50597
50781
  cross_file_taint_paths: crossFileData?.taintPaths ?? [],
50598
50782
  cross_file_calls: crossFileData?.crossFileCalls ?? [],
50599
50783
  cross_file_budget_exceeded: crossFileData?.budgetExceeded ?? false,
50784
+ project_size_budget_exceeded: crossFileData?.projectSizeBudgetExceeded ?? false,
50600
50785
  ...profileSummary ? { project_profile: profileSummary } : {},
50601
50786
  summary: {
50602
50787
  filesScanned: results.length,
@@ -50724,8 +50909,8 @@ function generateSarifResults(results, crossFileData) {
50724
50909
 
50725
50910
  // src/build-info.ts
50726
50911
  var buildInfo = {
50727
- gitSha: "2467260",
50728
- builtAt: "2026-09-24T06:46:18.058Z"
50912
+ gitSha: "39d07d9",
50913
+ builtAt: "2026-09-29T23:22:05.194Z"
50729
50914
  };
50730
50915
 
50731
50916
  // src/utils/args.ts
@@ -50815,6 +51000,17 @@ SCAN OPTIONS:
50815
51000
  On exceed: partial taint paths kept, remaining
50816
51001
  cross-file phases skipped, cross_file_budget_exceeded
50817
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).
50818
51014
 
50819
51015
  METRICS OPTIONS:
50820
51016
  -l, --language <lang> Analyze only files for language (bash|csharp|go|html|java|javascript|typescript|python|rust)
@@ -51233,12 +51429,35 @@ async function scanFile(filePath, language, analyzeOpts) {
51233
51429
  };
51234
51430
  }
51235
51431
  }
51236
- async function scanProject(files, language, analyzeOpts, crossFileBudgetMs) {
51237
- const filesWithCode = files.map((f) => ({
51238
- code: readFileSync(f, "utf-8"),
51239
- filePath: f,
51240
- language: language || detectLanguage7(f)
51241
- }));
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
+ }
51242
51461
  const projectResult = await analyzeProject(filesWithCode, {
51243
51462
  passOptions: analyzeOpts?.passOptions,
51244
51463
  disabledPasses: analyzeOpts?.disabledPasses,
@@ -51274,7 +51493,10 @@ async function scanProject(files, language, analyzeOpts, crossFileBudgetMs) {
51274
51493
  crossFileData: {
51275
51494
  taintPaths: projectResult.taint_paths,
51276
51495
  crossFileCalls: projectResult.cross_file_calls,
51277
- 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
51278
51500
  }
51279
51501
  };
51280
51502
  }
@@ -51505,7 +51727,7 @@ async function runScan(targetPath, options) {
51505
51727
  if ((await stat2(absPath)).isDirectory()) {
51506
51728
  if (spin)
51507
51729
  spin.text = `Running project analysis on ${files.length} file(s)...`;
51508
- const projectScan = await scanProject(files, options.language, analyzeOpts, options.crossFileBudgetMs);
51730
+ const projectScan = await scanProject(files, options.language, analyzeOpts, options.crossFileBudgetMs, options.maxProjectSourceChars);
51509
51731
  results = projectScan.results;
51510
51732
  crossFileData = projectScan.crossFileData;
51511
51733
  } else {
@@ -51537,8 +51759,10 @@ async function runScan(targetPath, options) {
51537
51759
  spin.text = `Scanning... (${processed}/${files.length})`;
51538
51760
  }
51539
51761
  }
51540
- if (spin)
51541
- 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
+ }
51542
51766
  if (suppressions.length > 0) {
51543
51767
  const beforeCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
51544
51768
  results = applySuppressionsToResults(results, suppressions, process.cwd());
@@ -51614,7 +51838,8 @@ async function runScan(targetPath, options) {
51614
51838
  const qualityCount = allVulns.filter((v) => v.category !== "security").length;
51615
51839
  const errors = results.filter((r) => r.error).length;
51616
51840
  const crossFilePaths = crossFileData?.taintPaths.length ?? 0;
51617
- 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;
51618
51843
  const profileSummary = buildProfileSummary(absPath, detectedModules, resolvedProfiles);
51619
51844
  if (resolvedProfiles) {
51620
51845
  for (const r of results) {
@@ -51646,15 +51871,17 @@ async function runScan(targetPath, options) {
51646
51871
  }
51647
51872
  if (!options.quiet && options.format === "text") {
51648
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)`;
51649
51876
  if (securityCount > 0) {
51650
- 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}`));
51651
51878
  }
51652
51879
  if (qualityCount > 0) {
51653
51880
  const label = securityCount > 0 ? "Also found" : "Found";
51654
- 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}`));
51655
51882
  }
51656
51883
  if (securityCount === 0 && qualityCount === 0 && options.verbose) {
51657
- console.log(colors.green(`No findings in ${files.length} file(s)`));
51884
+ console.log(colors.green(`No findings in ${scannedLabel}`));
51658
51885
  }
51659
51886
  if (errors > 0) {
51660
51887
  console.log(colors.yellow(`${errors} file(s) had errors during analysis`));
@@ -51948,6 +52175,17 @@ function parseCrossFileBudgetMs(raw) {
51948
52175
  }
51949
52176
  return n;
51950
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
+ }
51951
52189
  var SBOM_MANIFESTS = {
51952
52190
  "package.json": parseNpmDependencies,
51953
52191
  "package-lock.json": parseNpmLockDependencies,
@@ -52145,6 +52383,7 @@ Usage: cognium-dev scan <path> [options]`);
52145
52383
  profile: options.profile || options.p,
52146
52384
  disablePass: options["disable-pass"],
52147
52385
  crossFileBudgetMs: parseCrossFileBudgetMs(options["cross-file-budget-ms"]),
52386
+ maxProjectSourceChars: parseMaxProjectSourceChars(options["max-project-source-chars"]),
52148
52387
  projectProfile: parseProjectProfileArg(options["project-profile"]),
52149
52388
  noProjectProfile: options["no-project-profile"] === true,
52150
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.25",
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"
@@ -64,11 +64,11 @@
64
64
  "registry": "https://registry.npmjs.org/"
65
65
  },
66
66
  "dependencies": {
67
- "@cognium/project-profile-detect": "1.1.1",
68
- "circle-ir": "4.9.25"
67
+ "@cognium/project-profile-detect": "1.1.2",
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",