cognium-dev 3.201.0 → 3.202.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 (2) hide show
  1. package/dist/cli.js +370 -9
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -3,9 +3,9 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/cli.ts
6
- import { readFileSync, existsSync } from "fs";
6
+ import { readFileSync, existsSync, writeFileSync } from "fs";
7
7
  import { stat as stat2, readdir as readdir2 } from "fs/promises";
8
- import { join as join2, dirname as dirname3, extname, resolve as resolve2, relative as relative4 } from "path";
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
10
 
11
11
  // ../../node_modules/web-tree-sitter/web-tree-sitter.js
@@ -45904,6 +45904,244 @@ function deriveProjectRoot(paths) {
45904
45904
  }
45905
45905
  return common.join("/") || "/";
45906
45906
  }
45907
+ // ../circle-ir/dist/analysis/sbom.js
45908
+ function purlVersion(spec) {
45909
+ const stripped = spec.trim().replace(/^[\s^~=<>]+/, "");
45910
+ if (/^v?[0-9][\w.\-+]*$/.test(stripped))
45911
+ return stripped;
45912
+ return;
45913
+ }
45914
+ function encodePurlName(name2) {
45915
+ return name2.split("/").map((seg) => encodeURIComponent(seg)).join("/");
45916
+ }
45917
+ function makePurl(ecosystem, name2, version) {
45918
+ const v = purlVersion(version);
45919
+ const path = ecosystem === "maven" ? name2.replace(":", "/") : name2;
45920
+ const base = `pkg:${ecosystem}/${encodePurlName(path)}`;
45921
+ return v ? `${base}@${encodeURIComponent(v)}` : base;
45922
+ }
45923
+ function dep(ecosystem, name2, version, scope) {
45924
+ const v = version.trim() || "unknown";
45925
+ return { name: name2, version: v, ecosystem, scope, purl: makePurl(ecosystem, name2, v) };
45926
+ }
45927
+ function parseNpmDependencies(packageJson) {
45928
+ if (!packageJson)
45929
+ return [];
45930
+ let parsed;
45931
+ try {
45932
+ parsed = JSON.parse(packageJson);
45933
+ } catch {
45934
+ return [];
45935
+ }
45936
+ if (!parsed || typeof parsed !== "object")
45937
+ return [];
45938
+ const obj = parsed;
45939
+ const out2 = [];
45940
+ const sections = [
45941
+ ["dependencies", "required"],
45942
+ ["optionalDependencies", "optional"],
45943
+ ["peerDependencies", "optional"],
45944
+ ["devDependencies", "dev"]
45945
+ ];
45946
+ for (const [key, scope] of sections) {
45947
+ const block = obj[key];
45948
+ if (!block || typeof block !== "object")
45949
+ continue;
45950
+ for (const [name2, spec] of Object.entries(block)) {
45951
+ out2.push(dep("npm", name2, typeof spec === "string" ? spec : "unknown", scope));
45952
+ }
45953
+ }
45954
+ return out2;
45955
+ }
45956
+ function parsePypiDependencies(requirementsTxt) {
45957
+ if (!requirementsTxt)
45958
+ return [];
45959
+ const out2 = [];
45960
+ for (const raw of requirementsTxt.split(/\r?\n/)) {
45961
+ let line = raw.trim();
45962
+ if (!line || line.startsWith("#"))
45963
+ continue;
45964
+ if (line.startsWith("-") || /^[a-z]+:\/\//i.test(line))
45965
+ continue;
45966
+ line = line.split("#")[0].trim();
45967
+ line = line.split(";")[0].trim();
45968
+ if (!line)
45969
+ continue;
45970
+ const m = line.match(/^([A-Za-z0-9._-]+)\s*(?:\[[^\]]*\])?\s*(.*)$/);
45971
+ if (!m)
45972
+ continue;
45973
+ const name2 = m[1];
45974
+ const rest = m[2].trim();
45975
+ const verMatch = rest.match(/^(?:==|>=|<=|~=|!=|>|<|===)?\s*([^,\s]+)/);
45976
+ const version = verMatch && verMatch[1] ? verMatch[1] : "unknown";
45977
+ out2.push(dep("pypi", name2, version, "required"));
45978
+ }
45979
+ return out2;
45980
+ }
45981
+ function parseMavenDependencies(pomXml) {
45982
+ if (!pomXml)
45983
+ return [];
45984
+ const out2 = [];
45985
+ const depRe = /<dependency>([\s\S]*?)<\/dependency>/g;
45986
+ let m;
45987
+ while ((m = depRe.exec(pomXml)) !== null) {
45988
+ const block = m[1];
45989
+ const gid = block.match(/<groupId>\s*([^<\s]+)\s*<\/groupId>/)?.[1];
45990
+ const aid = block.match(/<artifactId>\s*([^<\s]+)\s*<\/artifactId>/)?.[1];
45991
+ if (!gid || !aid)
45992
+ continue;
45993
+ let version = block.match(/<version>\s*([^<\s]+)\s*<\/version>/)?.[1] ?? "unknown";
45994
+ if (version.startsWith("${"))
45995
+ version = "unknown";
45996
+ const mvnScope = block.match(/<scope>\s*([^<\s]+)\s*<\/scope>/)?.[1];
45997
+ const optional = /<optional>\s*true\s*<\/optional>/.test(block);
45998
+ const scope = mvnScope === "test" || mvnScope === "provided" ? "dev" : optional ? "optional" : "required";
45999
+ out2.push(dep("maven", `${gid}:${aid}`, version, scope));
46000
+ }
46001
+ return out2;
46002
+ }
46003
+ function parseCargoDependencies(cargoToml) {
46004
+ if (!cargoToml)
46005
+ return [];
46006
+ const out2 = [];
46007
+ let scope = null;
46008
+ for (const raw of cargoToml.split(/\r?\n/)) {
46009
+ const line = raw.trim();
46010
+ if (!line || line.startsWith("#"))
46011
+ continue;
46012
+ const section = line.match(/^\[([^\]]+)\]$/);
46013
+ if (section) {
46014
+ const s = section[1];
46015
+ if (s === "dependencies")
46016
+ scope = "required";
46017
+ else if (s === "dev-dependencies")
46018
+ scope = "dev";
46019
+ else if (s === "build-dependencies")
46020
+ scope = "optional";
46021
+ else
46022
+ scope = null;
46023
+ continue;
46024
+ }
46025
+ if (!scope)
46026
+ continue;
46027
+ const kv = line.match(/^([A-Za-z0-9._-]+)\s*=\s*(.+)$/);
46028
+ if (!kv)
46029
+ continue;
46030
+ const name2 = kv[1];
46031
+ const value = kv[2].trim();
46032
+ let version = "unknown";
46033
+ if (value.startsWith('"') || value.startsWith("'")) {
46034
+ version = value.replace(/^["']|["'].*$/g, "");
46035
+ } else {
46036
+ const inline = value.match(/version\s*=\s*["']([^"']+)["']/);
46037
+ if (inline)
46038
+ version = inline[1];
46039
+ }
46040
+ out2.push(dep("cargo", name2, version, scope));
46041
+ }
46042
+ return out2;
46043
+ }
46044
+ function parseGoDependencies(goMod) {
46045
+ if (!goMod)
46046
+ return [];
46047
+ const out2 = [];
46048
+ let inBlock = false;
46049
+ for (const raw of goMod.split(/\r?\n/)) {
46050
+ const line = raw.trim();
46051
+ if (!line || line.startsWith("//"))
46052
+ continue;
46053
+ if (!inBlock && /^require\s*\($/.test(line)) {
46054
+ inBlock = true;
46055
+ continue;
46056
+ }
46057
+ if (inBlock && line === ")") {
46058
+ inBlock = false;
46059
+ continue;
46060
+ }
46061
+ let spec;
46062
+ if (inBlock)
46063
+ spec = line;
46064
+ else if (/^require\s+/.test(line))
46065
+ spec = line.replace(/^require\s+/, "");
46066
+ else
46067
+ continue;
46068
+ const indirect = /\/\/\s*indirect/.test(spec);
46069
+ spec = spec.replace(/\/\/.*$/, "").trim();
46070
+ const m = spec.match(/^(\S+)\s+(\S+)$/);
46071
+ if (!m)
46072
+ continue;
46073
+ out2.push(dep("golang", m[1], m[2], indirect ? "optional" : "required"));
46074
+ }
46075
+ return out2;
46076
+ }
46077
+ function toCycloneDx(deps, meta = {}) {
46078
+ const bom = {
46079
+ bomFormat: "CycloneDX",
46080
+ specVersion: "1.5",
46081
+ version: 1
46082
+ };
46083
+ if (meta.serialNumber)
46084
+ bom.serialNumber = meta.serialNumber;
46085
+ const metadata2 = {};
46086
+ if (meta.timestamp)
46087
+ metadata2.timestamp = meta.timestamp;
46088
+ metadata2.tools = [{ vendor: "Cognium", name: meta.tool ?? "circle-ir" }];
46089
+ if (meta.name) {
46090
+ metadata2.component = {
46091
+ type: "application",
46092
+ name: meta.name,
46093
+ ...meta.version ? { version: meta.version } : {}
46094
+ };
46095
+ }
46096
+ bom.metadata = metadata2;
46097
+ bom.components = deps.map((d) => ({
46098
+ type: "library",
46099
+ name: d.name,
46100
+ version: d.version,
46101
+ purl: d.purl,
46102
+ scope: d.scope === "dev" ? "excluded" : d.scope === "optional" ? "optional" : "required",
46103
+ "bom-ref": d.purl
46104
+ }));
46105
+ return bom;
46106
+ }
46107
+ function spdxId(raw) {
46108
+ return raw.replace(/[^a-zA-Z0-9.-]/g, "-");
46109
+ }
46110
+ function toSpdx(deps, meta = {}) {
46111
+ const docName = meta.name ?? "document";
46112
+ const packages = deps.map((d, i2) => ({
46113
+ SPDXID: `SPDXRef-Package-${spdxId(d.ecosystem)}-${spdxId(d.name)}-${i2}`,
46114
+ name: d.name,
46115
+ versionInfo: d.version,
46116
+ downloadLocation: "NOASSERTION",
46117
+ filesAnalyzed: false,
46118
+ externalRefs: [
46119
+ {
46120
+ referenceCategory: "PACKAGE-MANAGER",
46121
+ referenceType: "purl",
46122
+ referenceLocator: d.purl
46123
+ }
46124
+ ]
46125
+ }));
46126
+ const relationships = packages.map((p) => ({
46127
+ spdxElementId: "SPDXRef-DOCUMENT",
46128
+ relatedSpdxElement: p.SPDXID,
46129
+ relationshipType: "DESCRIBES"
46130
+ }));
46131
+ return {
46132
+ spdxVersion: "SPDX-2.3",
46133
+ dataLicense: "CC0-1.0",
46134
+ SPDXID: "SPDXRef-DOCUMENT",
46135
+ name: docName,
46136
+ documentNamespace: meta.namespace ?? `https://cognium.dev/spdxdocs/${spdxId(docName)}`,
46137
+ creationInfo: {
46138
+ created: meta.timestamp ?? "1970-01-01T00:00:00Z",
46139
+ creators: [`Tool: ${meta.tool ?? "circle-ir"}`]
46140
+ },
46141
+ packages,
46142
+ relationships
46143
+ };
46144
+ }
45907
46145
  // ../project-profile-detect/dist/index.js
45908
46146
  import { relative as relative3 } from "path";
45909
46147
 
@@ -46515,7 +46753,7 @@ var colors = {
46515
46753
  };
46516
46754
 
46517
46755
  // src/version.ts
46518
- var version = "3.201.0";
46756
+ var version = "3.202.0";
46519
46757
 
46520
46758
  // src/formatters.ts
46521
46759
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
@@ -47116,6 +47354,7 @@ USAGE:
47116
47354
  COMMANDS:
47117
47355
  scan <path> Scan files or directories for security vulnerabilities
47118
47356
  metrics <path> Report software quality metrics for files or directories
47357
+ sbom <path> Generate a Software Bill of Materials from project manifests
47119
47358
  list-passes [cat] List all analysis passes (optionally filter by category)
47120
47359
  init Initialize a configuration file in your project
47121
47360
  version Display version information
@@ -47164,6 +47403,17 @@ METRICS OPTIONS:
47164
47403
  -o, --output <file> Write results to file
47165
47404
  -q, --quiet Suppress progress output
47166
47405
 
47406
+ SBOM OPTIONS:
47407
+ -f, --format <format> SBOM format (cyclonedx|spdx) [default: cyclonedx]
47408
+ - cyclonedx: CycloneDX 1.5 JSON (OWASP)
47409
+ - spdx: SPDX 2.3 JSON (Linux Foundation)
47410
+ --name <name> Project name in the document [default: package.json name or dir]
47411
+ --prod-only Exclude dev/test dependencies
47412
+ --deterministic Omit timestamp / serialNumber / namespace for reproducible output
47413
+ -o, --output <file> Write the document to a file (default: stdout)
47414
+ Discovers package.json / requirements.txt / pom.xml / Cargo.toml / go.mod.
47415
+ CVE matching is out of scope (deterministic SAST — no network).
47416
+
47167
47417
  EXAMPLES:
47168
47418
  cognium-dev scan src/
47169
47419
  cognium-dev scan app.java -f json -o results.json
@@ -47181,6 +47431,9 @@ EXAMPLES:
47181
47431
  cognium-dev metrics src/
47182
47432
  cognium-dev metrics src/ --category complexity
47183
47433
  cognium-dev metrics src/ --format json --profile custom-config.json
47434
+ cognium-dev sbom . # CycloneDX 1.5 to stdout
47435
+ cognium-dev sbom . -f spdx -o sbom.spdx.json # SPDX 2.3 to a file
47436
+ cognium-dev sbom . --prod-only --deterministic # reproducible, no dev deps
47184
47437
  cognium-dev list-passes
47185
47438
  cognium-dev list-passes reliability
47186
47439
  cognium-dev init
@@ -47886,8 +48139,8 @@ async function runScan(targetPath, options) {
47886
48139
  output = formatResults(results, options.verbose, crossFileData, profileSummary);
47887
48140
  }
47888
48141
  if (options.output) {
47889
- const { writeFileSync } = await import("fs");
47890
- writeFileSync(options.output, output);
48142
+ const { writeFileSync: writeFileSync2 } = await import("fs");
48143
+ writeFileSync2(options.output, output);
47891
48144
  console.error(colors.green(`Results written to ${options.output}`));
47892
48145
  } else if (output.trim()) {
47893
48146
  console.log(output);
@@ -48040,8 +48293,8 @@ async function runMetrics(targetPath, options) {
48040
48293
  `);
48041
48294
  }
48042
48295
  if (options.output) {
48043
- const { writeFileSync } = await import("fs");
48044
- writeFileSync(options.output, output);
48296
+ const { writeFileSync: writeFileSync2 } = await import("fs");
48297
+ writeFileSync2(options.output, output);
48045
48298
  console.error(colors.green(`Results written to ${options.output}`));
48046
48299
  } else {
48047
48300
  console.log(output);
@@ -48149,8 +48402,8 @@ async function handleInit() {
48149
48402
  severity: "low",
48150
48403
  categories: ["security", "reliability", "performance", "maintainability", "architecture"]
48151
48404
  };
48152
- const { writeFileSync } = await import("fs");
48153
- writeFileSync(configPath, JSON.stringify(config, null, 2));
48405
+ const { writeFileSync: writeFileSync2 } = await import("fs");
48406
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
48154
48407
  console.log(colors.green(`Created ${configPath}`));
48155
48408
  }
48156
48409
  function applyLogLevel(cliValue) {
@@ -48196,6 +48449,92 @@ function parseCrossFileBudgetMs(raw) {
48196
48449
  }
48197
48450
  return n;
48198
48451
  }
48452
+ var SBOM_MANIFESTS = {
48453
+ "package.json": parseNpmDependencies,
48454
+ "requirements.txt": parsePypiDependencies,
48455
+ "pom.xml": parseMavenDependencies,
48456
+ "Cargo.toml": parseCargoDependencies,
48457
+ "go.mod": parseGoDependencies
48458
+ };
48459
+ var SBOM_SKIP_DIRS = /^(node_modules|vendor|target|dist|build|out|coverage)$/;
48460
+ async function collectManifestFiles(targetPath) {
48461
+ const found = [];
48462
+ const pathStat = await stat2(targetPath);
48463
+ if (pathStat.isFile()) {
48464
+ if (SBOM_MANIFESTS[basename(targetPath)])
48465
+ found.push(targetPath);
48466
+ return found;
48467
+ }
48468
+ const walk2 = async (dir) => {
48469
+ for (const e of await readdir2(dir, { withFileTypes: true })) {
48470
+ if (e.name.startsWith("."))
48471
+ continue;
48472
+ if (e.isDirectory()) {
48473
+ if (SBOM_SKIP_DIRS.test(e.name))
48474
+ continue;
48475
+ await walk2(join2(dir, e.name));
48476
+ } else if (SBOM_MANIFESTS[e.name]) {
48477
+ found.push(join2(dir, e.name));
48478
+ }
48479
+ }
48480
+ };
48481
+ await walk2(targetPath);
48482
+ return found;
48483
+ }
48484
+ async function runSbom(targetPath, options) {
48485
+ const absPath = resolve2(targetPath);
48486
+ if (!existsSync(absPath)) {
48487
+ console.error(colors.red(`Error: path not found: ${targetPath}`));
48488
+ process.exit(2);
48489
+ }
48490
+ const manifests = await collectManifestFiles(absPath);
48491
+ if (manifests.length === 0) {
48492
+ console.error(colors.red("Error: no supported manifests found (package.json, requirements.txt, pom.xml, Cargo.toml, go.mod)"));
48493
+ process.exit(1);
48494
+ }
48495
+ let deps = [];
48496
+ let projectName = options.name;
48497
+ for (const m of manifests) {
48498
+ const content = readFileSync(m, "utf-8");
48499
+ deps.push(...SBOM_MANIFESTS[basename(m)](content));
48500
+ if (!projectName && basename(m) === "package.json") {
48501
+ try {
48502
+ const n = JSON.parse(content).name;
48503
+ if (typeof n === "string" && n)
48504
+ projectName = n;
48505
+ } catch {}
48506
+ }
48507
+ }
48508
+ const seen = new Set;
48509
+ deps = deps.filter((d) => {
48510
+ const key = `${d.ecosystem}|${d.name}|${d.version}|${d.scope}`;
48511
+ if (seen.has(key))
48512
+ return false;
48513
+ seen.add(key);
48514
+ return true;
48515
+ });
48516
+ if (options.prodOnly)
48517
+ deps = deps.filter((d) => d.scope !== "dev");
48518
+ if (!projectName)
48519
+ projectName = basename(absPath) || "project";
48520
+ const meta = { name: projectName, tool: "cognium-dev" };
48521
+ if (!options.deterministic) {
48522
+ meta.timestamp = new Date().toISOString();
48523
+ const { randomUUID } = await import("crypto");
48524
+ meta.serialNumber = `urn:uuid:${randomUUID()}`;
48525
+ meta.namespace = `https://cognium.dev/spdxdocs/${projectName}-${randomUUID()}`;
48526
+ }
48527
+ const doc = options.format === "spdx" ? toSpdx(deps, meta) : toCycloneDx(deps, meta);
48528
+ const out2 = JSON.stringify(doc, null, 2);
48529
+ if (options.output) {
48530
+ writeFileSync(options.output, out2);
48531
+ console.error(colors.green(`SBOM written to ${options.output} — ${deps.length} dependencies from ${manifests.length} manifest(s)`));
48532
+ } else {
48533
+ console.log(out2);
48534
+ console.error(colors.green(`SBOM: ${deps.length} dependencies from ${manifests.length} manifest(s)`));
48535
+ }
48536
+ process.exit(0);
48537
+ }
48199
48538
  async function main() {
48200
48539
  const { command, args: args2, options } = parseArgs(process.argv.slice(2));
48201
48540
  applyLogLevel(options["log-level"]);
@@ -48236,6 +48575,28 @@ Usage: cognium-dev metrics <path> [options]`);
48236
48575
  await runMetrics(targetPath, metricsOptions);
48237
48576
  return;
48238
48577
  }
48578
+ if (command === "sbom") {
48579
+ if (args2.length === 0) {
48580
+ console.error(colors.red("Error: sbom command requires a path argument"));
48581
+ console.error(`
48582
+ Usage: cognium-dev sbom <path> [--format cyclonedx|spdx] [--output <file>]`);
48583
+ process.exit(1);
48584
+ }
48585
+ const rawFormat = (options.format || options.f || "cyclonedx").toLowerCase();
48586
+ if (rawFormat !== "cyclonedx" && rawFormat !== "spdx") {
48587
+ console.error(colors.red(`Error: unknown SBOM format '${rawFormat}' (expected 'cyclonedx' or 'spdx')`));
48588
+ process.exit(1);
48589
+ }
48590
+ const sbomOptions = {
48591
+ format: rawFormat,
48592
+ output: options.output || options.o,
48593
+ name: options.name,
48594
+ prodOnly: options["prod-only"] === true || options.prod === true,
48595
+ deterministic: options.deterministic === true
48596
+ };
48597
+ await runSbom(args2[0], sbomOptions);
48598
+ return;
48599
+ }
48239
48600
  if (command === "scan") {
48240
48601
  if (args2.length === 0) {
48241
48602
  console.error(colors.red("Error: scan command requires a path argument"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.201.0",
3
+ "version": "3.202.0",
4
4
  "description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@cognium/project-profile-detect": "^1.1.0",
69
- "circle-ir": "^3.201.0"
69
+ "circle-ir": "^3.202.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/node": "^25.5.0",