cognium-dev 4.9.23 → 4.9.25

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 +128 -28
  2. package/package.json +3 -5
package/dist/cli.js CHANGED
@@ -16846,6 +16846,9 @@ function buildFileIndex(ir) {
16846
16846
  }
16847
16847
  return { callsByLine, defsByLine, usesByLine, callsByMethod, sinksByMethod, defsByMethod };
16848
16848
  }
16849
+ function escapeRegExp(s) {
16850
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16851
+ }
16849
16852
 
16850
16853
  class CrossFileResolver {
16851
16854
  fileIndexes = new WeakMap;
@@ -17214,7 +17217,7 @@ class CrossFileResolver {
17214
17217
  candidates.push(arg.variable);
17215
17218
  if (arg.expression) {
17216
17219
  for (const [name2] of paramNameToIndex) {
17217
- const re = new RegExp(`\\b${name2}\\b`);
17220
+ const re = new RegExp(`\\b${escapeRegExp(name2)}\\b`);
17218
17221
  if (re.test(arg.expression))
17219
17222
  candidates.push(name2);
17220
17223
  }
@@ -17298,7 +17301,7 @@ class CrossFileResolver {
17298
17301
  }
17299
17302
  if (arg.expression) {
17300
17303
  for (const v of reachable.names) {
17301
- if (new RegExp(`\\b${v}\\b`).test(arg.expression)) {
17304
+ if (new RegExp(`\\b${escapeRegExp(v)}\\b`).test(arg.expression)) {
17302
17305
  matchedName = v;
17303
17306
  break;
17304
17307
  }
@@ -17729,7 +17732,7 @@ class CrossFileResolver {
17729
17732
  }
17730
17733
  if (arg.expression) {
17731
17734
  for (const [tv, origin] of tainted) {
17732
- const re = new RegExp(`\\b${tv}\\b`);
17735
+ const re = new RegExp(`\\b${escapeRegExp(tv)}\\b`);
17733
17736
  if (re.test(arg.expression))
17734
17737
  return { var: tv, origin };
17735
17738
  }
@@ -37658,7 +37661,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
37658
37661
  for (const other of taintedVars) {
37659
37662
  if (other === varName)
37660
37663
  continue;
37661
- const re = new RegExp(`\\b${other}\\b`);
37664
+ const re = new RegExp(`\\b${other.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
37662
37665
  if (!re.test(rhs))
37663
37666
  continue;
37664
37667
  anyReferenced = true;
@@ -37889,7 +37892,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
37889
37892
  }
37890
37893
  const assignMatch = sinkCode.match(/^\s*(?:[A-Za-z_][\w.<>[\]\s,?]*\s+)?[A-Za-z_]\w*\s*=(?!=)\s*/);
37891
37894
  const rhs = assignMatch ? sinkCode.slice(assignMatch[0].length) : sinkCode;
37892
- if (new RegExp(`\\b${sourceVar}\\b`).test(rhs)) {
37895
+ if (new RegExp(`\\b${sourceVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(rhs)) {
37893
37896
  continue;
37894
37897
  }
37895
37898
  }
@@ -48608,6 +48611,20 @@ function computeProjectProfileSummary(fileAnalyses) {
48608
48611
  }
48609
48612
  return { byShape, byEnv, totalFiles: fileAnalyses.length };
48610
48613
  }
48614
+ function longestLineLength(code) {
48615
+ let max = 0, start2 = 0;
48616
+ for (let i2 = 0;i2 < code.length; i2++) {
48617
+ if (code.charCodeAt(i2) === 10) {
48618
+ if (i2 - start2 > max)
48619
+ max = i2 - start2;
48620
+ start2 = i2 + 1;
48621
+ }
48622
+ }
48623
+ if (code.length - start2 > max)
48624
+ max = code.length - start2;
48625
+ return max;
48626
+ }
48627
+ var MAX_ANALYZABLE_LINE_LENGTH = 1e4;
48611
48628
  async function analyze(code, filePath, language, options = {}) {
48612
48629
  if (!initialized) {
48613
48630
  await initAnalyzer(options);
@@ -48659,6 +48676,27 @@ async function analyze(code, filePath, language, options = {}) {
48659
48676
  meta.projectProfile = makeProfileResolver2(options.projectProfile)(filePath);
48660
48677
  }
48661
48678
  phaseEnd("extractMeta", tMeta);
48679
+ if (longestLineLength(code) > MAX_ANALYZABLE_LINE_LENGTH) {
48680
+ logger.warn("Skipping analysis of a minified / single-line file", {
48681
+ filePath,
48682
+ language,
48683
+ codeLength: code.length
48684
+ });
48685
+ return {
48686
+ meta,
48687
+ types: [],
48688
+ calls: [],
48689
+ cfg: { blocks: [], edges: [] },
48690
+ dfg: { defs: [], uses: [], chains: [] },
48691
+ taint: { sources: [], sinks: [], sanitizers: [] },
48692
+ imports: [],
48693
+ exports: [],
48694
+ unresolved: [],
48695
+ enriched: {},
48696
+ metrics: { file: filePath, metrics: [] },
48697
+ parse_status: parseStatus
48698
+ };
48699
+ }
48662
48700
  const tTypes = phaseStart();
48663
48701
  const types = extractTypes(tree, nodeCache, language);
48664
48702
  phaseEnd("extractTypes", tTypes);
@@ -50118,19 +50156,30 @@ var YELLOW = "\x1B[93m";
50118
50156
  var BLUE = "\x1B[94m";
50119
50157
  var CYAN = "\x1B[96m";
50120
50158
  var MAGENTA = "\x1B[95m";
50159
+ function colorEnabled() {
50160
+ const noColor = process.env.NO_COLOR;
50161
+ if (noColor !== undefined && noColor !== "")
50162
+ return false;
50163
+ if (process.env.TERM === "dumb")
50164
+ return false;
50165
+ return true;
50166
+ }
50167
+ function paint(code, text) {
50168
+ return colorEnabled() ? `${code}${text}${RESET}` : text;
50169
+ }
50121
50170
  var colors = {
50122
- red: (text) => `${RED}${text}${RESET}`,
50123
- green: (text) => `${GREEN}${text}${RESET}`,
50124
- yellow: (text) => `${YELLOW}${text}${RESET}`,
50125
- blue: (text) => `${BLUE}${text}${RESET}`,
50126
- cyan: (text) => `${CYAN}${text}${RESET}`,
50127
- magenta: (text) => `${MAGENTA}${text}${RESET}`,
50128
- bold: (text) => `${BOLD}${text}${RESET}`,
50171
+ red: (text) => paint(RED, text),
50172
+ green: (text) => paint(GREEN, text),
50173
+ yellow: (text) => paint(YELLOW, text),
50174
+ blue: (text) => paint(BLUE, text),
50175
+ cyan: (text) => paint(CYAN, text),
50176
+ magenta: (text) => paint(MAGENTA, text),
50177
+ bold: (text) => paint(BOLD, text),
50129
50178
  dim: (text) => text
50130
50179
  };
50131
50180
 
50132
50181
  // src/version.ts
50133
- var version = "4.9.23";
50182
+ var version = "4.9.25";
50134
50183
 
50135
50184
  // src/formatters.ts
50136
50185
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
@@ -50675,8 +50724,8 @@ function generateSarifResults(results, crossFileData) {
50675
50724
 
50676
50725
  // src/build-info.ts
50677
50726
  var buildInfo = {
50678
- gitSha: "e7e65f8",
50679
- builtAt: "2026-09-22T22:15:34.355Z"
50727
+ gitSha: "2467260",
50728
+ builtAt: "2026-09-24T06:46:18.058Z"
50680
50729
  };
50681
50730
 
50682
50731
  // src/utils/args.ts
@@ -50735,7 +50784,7 @@ COMMANDS:
50735
50784
  version Display version information
50736
50785
 
50737
50786
  SCAN OPTIONS:
50738
- -l, --language <lang> Scan only files for language (bash|go|html|java|javascript|typescript|python|rust)
50787
+ -l, --language <lang> Scan only files for language (bash|csharp|go|html|java|javascript|typescript|python|rust)
50739
50788
  -f, --format <format> Output format (text|json|sarif) [default: text]
50740
50789
  --threads <n> Parallel analysis threads [default: 4]
50741
50790
  --severity <level> Filter by severity:
@@ -50768,7 +50817,7 @@ SCAN OPTIONS:
50768
50817
  surfaced in output (text warning / JSON / SARIF field).
50769
50818
 
50770
50819
  METRICS OPTIONS:
50771
- -l, --language <lang> Analyze only files for language (bash|go|html|java|javascript|typescript|python|rust)
50820
+ -l, --language <lang> Analyze only files for language (bash|csharp|go|html|java|javascript|typescript|python|rust)
50772
50821
  -f, --format <format> Output format (text|json) [default: text]
50773
50822
  --category <cats> Filter by metric category (comma-separated):
50774
50823
  - Valid categories: complexity, size, coupling,
@@ -50877,19 +50926,19 @@ class Spinner {
50877
50926
  succeed(text) {
50878
50927
  this.stop();
50879
50928
  const message = text || this._text;
50880
- console.error(`\x1B[32m${CHECKMARK}\x1B[0m ${message}`);
50929
+ console.error(`${colors.green(CHECKMARK)} ${message}`);
50881
50930
  return this;
50882
50931
  }
50883
50932
  fail(text) {
50884
50933
  this.stop();
50885
50934
  const message = text || this._text;
50886
- console.error(`\x1B[31m${CROSS}\x1B[0m ${message}`);
50935
+ console.error(`${colors.red(CROSS)} ${message}`);
50887
50936
  return this;
50888
50937
  }
50889
50938
  warn(text) {
50890
50939
  this.stop();
50891
50940
  const message = text || this._text;
50892
- console.error(`\x1B[33m${WARNING}\x1B[0m ${message}`);
50941
+ console.error(`${colors.yellow(WARNING)} ${message}`);
50893
50942
  return this;
50894
50943
  }
50895
50944
  set text(value) {
@@ -50916,8 +50965,7 @@ function loadConfig(profilePath) {
50916
50965
  const content = readFileSync(configPath, "utf-8");
50917
50966
  return JSON.parse(content);
50918
50967
  } catch (err2) {
50919
- console.error(colors.yellow(`Warning: Failed to parse ${configPath}: ${err2}`));
50920
- return null;
50968
+ throw new Error(`Failed to parse ${configPath}: ${err2 instanceof Error ? err2.message : String(err2)}. ` + "Fix the JSON or remove the file.");
50921
50969
  }
50922
50970
  }
50923
50971
  function convertConfigToPassOptions(config) {
@@ -50983,6 +51031,27 @@ function applySuppressionsToResults(results, suppressions, basePath) {
50983
51031
  return { ...result, vulnerabilities: filteredVulns };
50984
51032
  });
50985
51033
  }
51034
+ function applySuppressionsToTaintPaths(paths, suppressions, basePath) {
51035
+ if (suppressions.length === 0)
51036
+ return paths;
51037
+ return paths.filter((path) => {
51038
+ for (const supp of suppressions) {
51039
+ const sinkType = path.sink.type;
51040
+ if (supp.pass !== sinkType && supp.pass !== `cross-file-${sinkType}`)
51041
+ continue;
51042
+ if (supp.file) {
51043
+ const suppFile = supp.file.replace(/^\.\//, "");
51044
+ const relativeFile = relative4(basePath, path.sink.file) || path.sink.file;
51045
+ if (suppFile !== relativeFile && suppFile !== path.sink.file)
51046
+ continue;
51047
+ }
51048
+ if (supp.line !== undefined && supp.line !== path.sink.line)
51049
+ continue;
51050
+ return false;
51051
+ }
51052
+ return true;
51053
+ });
51054
+ }
50986
51055
  var TEST_PATTERNS = [
50987
51056
  /[\/\\]test[\/\\]/i,
50988
51057
  /[\/\\]tests[\/\\]/i,
@@ -51040,9 +51109,30 @@ function detectLanguage7(filePath) {
51040
51109
  const ext = extname(filePath).toLowerCase();
51041
51110
  return LANG_MAP[ext] || null;
51042
51111
  }
51112
+ var SUPPORTED_SCAN_LANGUAGES = [
51113
+ ...new Set(Object.values(LANG_MAP))
51114
+ ].sort();
51043
51115
  function normalizeLanguage(language) {
51044
51116
  return language.trim().toLowerCase();
51045
51117
  }
51118
+ function resolveScanLanguage(raw) {
51119
+ if (raw === undefined || raw === "")
51120
+ return;
51121
+ const normalized = normalizeLanguage(String(raw));
51122
+ if (!SUPPORTED_SCAN_LANGUAGES.includes(normalized)) {
51123
+ throw new Error(`Invalid language: ${normalized}
51124
+ Valid options: ${SUPPORTED_SCAN_LANGUAGES.join(", ")}`);
51125
+ }
51126
+ return normalized;
51127
+ }
51128
+ function languageFromCliOptions(options) {
51129
+ try {
51130
+ return resolveScanLanguage(options.language || options.l);
51131
+ } catch (err2) {
51132
+ console.error(colors.red(`Error: ${err2.message}`));
51133
+ process.exit(1);
51134
+ }
51135
+ }
51046
51136
  function fileMatchesLanguage(filePath, language) {
51047
51137
  const detected = detectLanguage7(filePath);
51048
51138
  if (!detected)
@@ -51053,9 +51143,13 @@ function fileMatchesLanguage(filePath, language) {
51053
51143
  }
51054
51144
  function matchesGlob(filePath, pattern) {
51055
51145
  const normalizedPath = filePath.replace(/\\/g, "/");
51056
- const normalizedPattern = pattern.replace(/\\/g, "/");
51146
+ let normalizedPattern = pattern.replace(/\\/g, "/");
51147
+ normalizedPattern = normalizedPattern.replace(/\*{3,}/g, "**").replace(/(?:\*\*\/)+/g, "**/").replace(/(?:\/\*\*)+/g, "/**");
51148
+ const globstarCount = (normalizedPattern.match(/\*\*/g) ?? []).length;
51149
+ if (globstarCount > 4)
51150
+ return false;
51057
51151
  let regex = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".");
51058
- regex = regex.replace(/\{\{GLOBSTAR\}\}\//g, "(?:.+/)?").replace(/\/\{\{GLOBSTAR\}\}/g, "(?:/.*)?").replace(/\{\{GLOBSTAR\}\}/g, ".*");
51152
+ regex = regex.replace(/\{\{GLOBSTAR\}\}\//g, "(?:[^/]+/)*").replace(/\/\{\{GLOBSTAR\}\}/g, "(?:/[^/]+)*").replace(/\{\{GLOBSTAR\}\}/g, "[^]*");
51059
51153
  return new RegExp("^" + regex + "$").test(normalizedPath);
51060
51154
  }
51061
51155
  function matchesAnyPattern(filePath, patterns) {
@@ -51446,9 +51540,12 @@ async function runScan(targetPath, options) {
51446
51540
  if (spin)
51447
51541
  spin.succeed(`Scanned ${files.length} file(s)`);
51448
51542
  if (suppressions.length > 0) {
51449
- const beforeCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0);
51543
+ const beforeCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
51450
51544
  results = applySuppressionsToResults(results, suppressions, process.cwd());
51451
- const afterCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0);
51545
+ if (crossFileData) {
51546
+ crossFileData.taintPaths = applySuppressionsToTaintPaths(crossFileData.taintPaths, suppressions, process.cwd());
51547
+ }
51548
+ const afterCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
51452
51549
  if (!options.quiet && beforeCount !== afterCount) {
51453
51550
  console.error(colors.dim(`Suppressed ${beforeCount - afterCount} finding(s) via config`));
51454
51551
  }
@@ -51997,7 +52094,7 @@ Usage: cognium-dev metrics <path> [options]`);
51997
52094
  category: options.category,
51998
52095
  output: options.output || options.o,
51999
52096
  quiet: options.quiet === true || options.q === true,
52000
- language: options.language || options.l ? normalizeLanguage(options.language || options.l) : undefined,
52097
+ language: languageFromCliOptions(options),
52001
52098
  excludeTests: options["exclude-tests"] === true,
52002
52099
  profile: options.profile || options.p
52003
52100
  };
@@ -52035,7 +52132,7 @@ Usage: cognium-dev scan <path> [options]`);
52035
52132
  }
52036
52133
  const targetPath = args2[0];
52037
52134
  const scanOptions = {
52038
- language: options.language || options.l ? normalizeLanguage(options.language || options.l) : undefined,
52135
+ language: languageFromCliOptions(options),
52039
52136
  format: options.format || options.f || "text",
52040
52137
  threads: parseInt(options.threads || "4", 10),
52041
52138
  severity: options.severity,
@@ -52069,12 +52166,15 @@ main().catch((error) => {
52069
52166
  process.exit(2);
52070
52167
  });
52071
52168
  export {
52169
+ resolveScanLanguage,
52072
52170
  matchesGlob,
52073
52171
  loadConfig,
52074
52172
  isTestFile2 as isTestFile,
52075
52173
  detectLanguage7 as detectLanguage,
52076
52174
  dedupeVulnerabilities,
52077
52175
  convertConfigToPassOptions,
52176
+ applySuppressionsToTaintPaths,
52078
52177
  applySuppressionsToResults,
52178
+ SUPPORTED_SCAN_LANGUAGES,
52079
52179
  PASS_REGISTRY
52080
52180
  };
package/package.json CHANGED
@@ -1,9 +1,7 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "4.9.23",
3
+ "version": "4.9.25",
4
4
  "description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
5
  "bin": {
8
6
  "cognium-dev": "dist/cli.js"
9
7
  },
@@ -14,7 +12,7 @@
14
12
  "test": "bun test",
15
13
  "clean": "rm -rf dist cognium-dev cognium-dev-* *.bun-build wasm",
16
14
  "typecheck": "tsc --noEmit",
17
- "version": "node -e \"const v = require('./package.json').version; require('fs').writeFileSync('src/version.ts', '/**\\n * Version information\\n *\\n * Kept in sync with package.json via the \\`version\\` npm lifecycle script.\\n * Do not edit manually — use \\`npm version patch|minor|major\\` instead.\\n */\\nexport const version = \\x27' + v + '\\x27;\\n')\" && git add src/version.ts",
15
+ "version": "node -e \"const v = require('./package.json').version; require('fs').writeFileSync('src/version.ts', '/**\\n * Version information\\n *\\n * Kept in sync with package.json via the \\`version\\` npm lifecycle script.\\n * Do not edit manually \u2014 use \\`npm version patch|minor|major\\` instead.\\n */\\nexport const version = \\x27' + v + '\\x27;\\n')\" && git add src/version.ts",
18
16
  "dogfood": "bun run src/cli.ts scan src/ -q",
19
17
  "prepublishOnly": "bun run build",
20
18
  "test:coverage": "bun test --coverage"
@@ -67,7 +65,7 @@
67
65
  },
68
66
  "dependencies": {
69
67
  "@cognium/project-profile-detect": "1.1.1",
70
- "circle-ir": "4.9.23"
68
+ "circle-ir": "4.9.25"
71
69
  },
72
70
  "devDependencies": {
73
71
  "@types/node": "^26.6.1",