@cyclonedx/cdxgen 12.4.0 → 12.4.1

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 (47) hide show
  1. package/README.md +6 -4
  2. package/bin/cdxgen.js +32 -11
  3. package/bin/convert.js +12 -8
  4. package/bin/hbom.js +13 -8
  5. package/bin/repl.js +14 -10
  6. package/bin/validate.js +10 -13
  7. package/bin/verify.js +7 -29
  8. package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
  9. package/lib/audit/index.js +2 -1
  10. package/lib/cli/index.js +21 -11
  11. package/lib/cli/index.poku.js +117 -0
  12. package/lib/helpers/bomUtils.js +155 -1
  13. package/lib/helpers/bomUtils.poku.js +79 -1
  14. package/lib/helpers/plugins.js +17 -16
  15. package/lib/helpers/protobom.js +53 -0
  16. package/lib/helpers/protobom.poku.js +44 -1
  17. package/lib/helpers/protobomLoader.js +43 -0
  18. package/lib/helpers/protobomLoader.poku.js +31 -0
  19. package/lib/server/server.js +2 -1
  20. package/lib/stages/postgen/postgen.js +219 -12
  21. package/lib/stages/postgen/postgen.poku.js +163 -0
  22. package/lib/validator/bomValidator.js +90 -38
  23. package/lib/validator/bomValidator.poku.js +90 -0
  24. package/lib/validator/complianceRules.js +4 -2
  25. package/lib/validator/index.poku.js +14 -0
  26. package/package.json +1 -1
  27. package/types/bin/repl.d.ts +1 -1
  28. package/types/bin/repl.d.ts.map +1 -1
  29. package/types/lib/audit/index.d.ts.map +1 -1
  30. package/types/lib/cli/index.d.ts.map +1 -1
  31. package/types/lib/helpers/bomUtils.d.ts +10 -0
  32. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  33. package/types/lib/helpers/hbomAnalysis.d.ts +14 -0
  34. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -1
  35. package/types/lib/helpers/hostTopology.d.ts.map +1 -1
  36. package/types/lib/helpers/plugins.d.ts.map +1 -1
  37. package/types/lib/helpers/protobom.d.ts +2 -0
  38. package/types/lib/helpers/protobom.d.ts.map +1 -1
  39. package/types/lib/helpers/protobomLoader.d.ts +17 -0
  40. package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
  41. package/types/lib/server/server.d.ts.map +1 -1
  42. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  43. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  44. package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
  45. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  46. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  47. package/types/lib/validator/complianceRules.d.ts.map +1 -1
@@ -6,6 +6,7 @@ import process from "node:process";
6
6
  import { createBom } from "../cli/index.js";
7
7
  import { DEFAULT_HBOM_AUDIT_CATEGORIES } from "../helpers/auditCategories.js";
8
8
  import {
9
+ getCycloneDxFormat,
9
10
  getNonCycloneDxErrorMessage,
10
11
  isCycloneDxBom,
11
12
  } from "../helpers/bomUtils.js";
@@ -230,7 +231,7 @@ export async function runDirectBomAuditFromBoms(inputBoms, options = {}) {
230
231
  const findings = await auditBom(inputBom.bomJson, directAuditOptions);
231
232
  results.push({
232
233
  auditOptions: directAuditOptions,
233
- bomFormat: inputBom.bomJson?.bomFormat,
234
+ bomFormat: getCycloneDxFormat(inputBom.bomJson),
234
235
  findings,
235
236
  serialNumber: inputBom.bomJson?.serialNumber,
236
237
  source: inputBom.source,
package/lib/cli/index.js CHANGED
@@ -41,6 +41,10 @@ import {
41
41
  rewriteExtractedArchivePaths,
42
42
  } from "../helpers/asarutils.js";
43
43
  import { expandBomAuditCategories } from "../helpers/auditCategories.js";
44
+ import {
45
+ setCycloneDxFormat,
46
+ toCycloneDxSpecVersionString,
47
+ } from "../helpers/bomUtils.js";
44
48
  import { parseCaxaMetadata } from "../helpers/caxa.js";
45
49
  import { collectSourceCryptoComponents } from "../helpers/cbomutils.js";
46
50
  import {
@@ -1393,13 +1397,16 @@ const buildBomNSData = (options, pkgInfo, ptype, context) => {
1393
1397
  // CycloneDX Json Template
1394
1398
  const jsonTpl = {
1395
1399
  bomFormat: "CycloneDX",
1396
- specVersion: `${options.specVersion || "1.7"}`,
1400
+ specVersion: toCycloneDxSpecVersionString(options.specVersion || "1.7"),
1397
1401
  serialNumber: serialNum,
1398
1402
  version: 1,
1399
1403
  metadata: metadata,
1400
1404
  components,
1401
1405
  dependencies,
1402
1406
  };
1407
+ setCycloneDxFormat(jsonTpl, jsonTpl.specVersion, {
1408
+ preserveLegacyBomFormat: true,
1409
+ });
1403
1410
  if (services.length) {
1404
1411
  jsonTpl.services = mergeServices([], services);
1405
1412
  }
@@ -8392,16 +8399,19 @@ export function dedupeBom(options, components, parentComponent, dependencies) {
8392
8399
  options,
8393
8400
  parentComponent,
8394
8401
  components,
8395
- bomJson: {
8396
- bomFormat: "CycloneDX",
8397
- specVersion: `${options.specVersion || 1.7}`,
8398
- serialNumber: serialNum,
8399
- version: 1,
8400
- metadata: addMetadata(parentComponent, options, {}),
8401
- components,
8402
- services: options.services || [],
8403
- dependencies,
8404
- },
8402
+ bomJson: setCycloneDxFormat(
8403
+ {
8404
+ specVersion: toCycloneDxSpecVersionString(options.specVersion || 1.7),
8405
+ serialNumber: serialNum,
8406
+ version: 1,
8407
+ metadata: addMetadata(parentComponent, options, {}),
8408
+ components,
8409
+ services: options.services || [],
8410
+ dependencies,
8411
+ },
8412
+ options.specVersion || 1.7,
8413
+ { preserveLegacyBomFormat: true },
8414
+ ),
8405
8415
  };
8406
8416
  }
8407
8417
 
@@ -623,6 +623,7 @@ describe("CLI tests", () => {
623
623
  false,
624
624
  );
625
625
  },
626
+ isolateDepsSlicesFile: true,
626
627
  expectedSpecVersion: (specVersion) => specVersion,
627
628
  name: "cbom",
628
629
  },
@@ -651,6 +652,13 @@ describe("CLI tests", () => {
651
652
  fixtureRoot,
652
653
  `${scenario.name}-${specVersion}.spdx.json`,
653
654
  );
655
+ const depsSlicesPath = join(
656
+ fixtureRoot,
657
+ `${scenario.name}-${specVersion}.deps.slices.json`,
658
+ );
659
+ const depsSlicesArgs = scenario.isolateDepsSlicesFile
660
+ ? ["--deps-slices-file", depsSlicesPath]
661
+ : [];
654
662
  const generateResult = spawnSync(
655
663
  process.execPath,
656
664
  [
@@ -660,6 +668,7 @@ describe("CLI tests", () => {
660
668
  jsonPath,
661
669
  "--spec-version",
662
670
  specVersion,
671
+ ...depsSlicesArgs,
663
672
  "--export-proto",
664
673
  "--proto-bin-file",
665
674
  protoPath,
@@ -738,6 +747,114 @@ describe("CLI tests", () => {
738
747
  scenario.assertRoundTrip(roundTrippedBom);
739
748
  }
740
749
  }
750
+ assert.strictEqual(
751
+ existsSync(join(repoDir, "deps.slices.json")),
752
+ false,
753
+ "protobuf round-trip tests must not leave deps.slices.json in the repository root",
754
+ );
755
+ } finally {
756
+ rmSync(join(repoDir, "deps.slices.json"), { force: true });
757
+ rmSync(fixtureRoot, { force: true, recursive: true });
758
+ }
759
+ });
760
+ });
761
+
762
+ describe("CycloneDX 2.0 JSON output", () => {
763
+ it("generates valid experimental 2.0-dev JSON with specFormat", () => {
764
+ const fixtureRoot = mkdtempSync(join(tmpdir(), "cdxgen-json20-"));
765
+ try {
766
+ const jsonPath = join(fixtureRoot, "bom-2.0.json");
767
+ const generateResult = spawnSync(
768
+ process.execPath,
769
+ [
770
+ join(repoDir, "bin", "cdxgen.js"),
771
+ "-t",
772
+ "js",
773
+ mcpFixtureDir,
774
+ "-o",
775
+ jsonPath,
776
+ "--spec-version",
777
+ "2.0",
778
+ "--no-banner",
779
+ ],
780
+ {
781
+ cwd: repoDir,
782
+ encoding: "utf8",
783
+ env: buildMinimalCliEnv(),
784
+ },
785
+ );
786
+ assert.strictEqual(
787
+ generateResult.status,
788
+ 0,
789
+ `${generateResult.stdout}${generateResult.stderr}`,
790
+ );
791
+
792
+ const generatedBom = JSON.parse(readFileSync(jsonPath, "utf8"));
793
+ assert.strictEqual(generatedBom.specFormat, "CycloneDX");
794
+ assert.strictEqual(generatedBom.bomFormat, undefined);
795
+ assert.strictEqual(generatedBom.specVersion, "2.0");
796
+ assert.ok(Array.isArray(generatedBom.metadata?.tools?.components));
797
+
798
+ const validateResult = spawnSync(
799
+ process.execPath,
800
+ [
801
+ join(repoDir, "bin", "validate.js"),
802
+ "-i",
803
+ jsonPath,
804
+ "--fail-severity",
805
+ "critical",
806
+ "--no-deep",
807
+ "--report",
808
+ "json",
809
+ ],
810
+ {
811
+ cwd: repoDir,
812
+ encoding: "utf8",
813
+ env: buildMinimalCliEnv(),
814
+ },
815
+ );
816
+ assert.strictEqual(
817
+ validateResult.status,
818
+ 0,
819
+ `${validateResult.stdout}${validateResult.stderr}`,
820
+ );
821
+ } finally {
822
+ rmSync(fixtureRoot, { force: true, recursive: true });
823
+ }
824
+ });
825
+
826
+ it("rejects experimental 2.0-dev protobuf export until cdx-proto supports it", () => {
827
+ const fixtureRoot = mkdtempSync(join(tmpdir(), "cdxgen-proto20-"));
828
+ try {
829
+ const jsonPath = join(fixtureRoot, "bom-2.0.json");
830
+ const protoPath = join(fixtureRoot, "bom-2.0.cdx");
831
+ const generateResult = spawnSync(
832
+ process.execPath,
833
+ [
834
+ join(repoDir, "bin", "cdxgen.js"),
835
+ "-t",
836
+ "js",
837
+ mcpFixtureDir,
838
+ "-o",
839
+ jsonPath,
840
+ "--spec-version",
841
+ "2.0",
842
+ "--export-proto",
843
+ "--proto-bin-file",
844
+ protoPath,
845
+ "--no-banner",
846
+ ],
847
+ {
848
+ cwd: repoDir,
849
+ encoding: "utf8",
850
+ env: buildMinimalCliEnv(),
851
+ },
852
+ );
853
+ assert.strictEqual(generateResult.status, 1);
854
+ assert.match(
855
+ `${generateResult.stdout}${generateResult.stderr}`,
856
+ /CycloneDX 2\.0 is not currently supported for protobuf export/i,
857
+ );
741
858
  } finally {
742
859
  rmSync(fixtureRoot, { force: true, recursive: true });
743
860
  }
@@ -1,8 +1,16 @@
1
1
  const SPDX_CONTEXT_PREFIX = "https://spdx.org/rdf/";
2
2
  const CYCLONEDX_FORMAT = "CycloneDX";
3
+ const LEGACY_CYCLONEDX_ROOT_KEY = "bomFormat";
4
+ const MODERN_CYCLONEDX_ROOT_KEY = "specFormat";
3
5
  const BOM_FORMAT_CYCLONEDX = "cyclonedx";
4
6
  const BOM_FORMAT_SPDX = "spdx";
5
7
  const BOM_FORMAT_UNKNOWN = "unknown";
8
+ const CYCLONEDX_SPEC_VERSION_PATTERN = /^(\d+)(?:\.(\d+))?$/u;
9
+ const CYCLONEDX_FORMAT_KEYS = new Set([
10
+ LEGACY_CYCLONEDX_ROOT_KEY,
11
+ MODERN_CYCLONEDX_ROOT_KEY,
12
+ "specVersion",
13
+ ]);
6
14
 
7
15
  export const isSpdxJsonLd = (bomJson) =>
8
16
  Boolean(
@@ -11,8 +19,154 @@ export const isSpdxJsonLd = (bomJson) =>
11
19
  bomJson["@graph"].some((element) => element?.type === "SpdxDocument"),
12
20
  );
13
21
 
22
+ const parseCycloneDxSpecVersion = (specVersion) => {
23
+ const match = `${specVersion ?? ""}`
24
+ .trim()
25
+ .match(CYCLONEDX_SPEC_VERSION_PATTERN);
26
+ if (!match) {
27
+ return undefined;
28
+ }
29
+ return {
30
+ major: Number.parseInt(match[1], 10),
31
+ minor: Number.parseInt(match[2] || "0", 10),
32
+ minorText: match[2] || "0",
33
+ };
34
+ };
35
+
36
+ export const normalizeCycloneDxSpecVersion = (specVersion) => {
37
+ const parsed = parseCycloneDxSpecVersion(specVersion);
38
+ if (!parsed) {
39
+ return undefined;
40
+ }
41
+ return Number(`${parsed.major}.${parsed.minor}`);
42
+ };
43
+
44
+ export const toCycloneDxSpecVersionString = (specVersion) => {
45
+ const parsed = parseCycloneDxSpecVersion(specVersion);
46
+ if (!parsed) {
47
+ return undefined;
48
+ }
49
+ if (typeof specVersion === "string" && parsed.minorText !== "0") {
50
+ return `${parsed.major}.${parsed.minorText}`;
51
+ }
52
+ return `${parsed.major}.${parsed.minor}`;
53
+ };
54
+
55
+ export const isCycloneDxSpecVersionAtLeast = (specVersion, minimumVersion) => {
56
+ const parsedSpecVersion = parseCycloneDxSpecVersion(specVersion);
57
+ const parsedMinimumVersion = parseCycloneDxSpecVersion(minimumVersion);
58
+ if (!parsedSpecVersion || !parsedMinimumVersion) {
59
+ return false;
60
+ }
61
+ if (parsedSpecVersion.major !== parsedMinimumVersion.major) {
62
+ return parsedSpecVersion.major > parsedMinimumVersion.major;
63
+ }
64
+ return parsedSpecVersion.minor >= parsedMinimumVersion.minor;
65
+ };
66
+
67
+ export const isCycloneDx20SpecVersion = (specVersion) =>
68
+ isCycloneDxSpecVersionAtLeast(specVersion, 2);
69
+
70
+ export const getCycloneDxRootFormatKey = (specVersionOrBom) => {
71
+ const specVersion =
72
+ specVersionOrBom && typeof specVersionOrBom === "object"
73
+ ? specVersionOrBom.specVersion
74
+ : specVersionOrBom;
75
+ return isCycloneDx20SpecVersion(specVersion)
76
+ ? MODERN_CYCLONEDX_ROOT_KEY
77
+ : LEGACY_CYCLONEDX_ROOT_KEY;
78
+ };
79
+
80
+ export const getCycloneDxFormat = (bomJson) =>
81
+ bomJson?.specFormat || bomJson?.bomFormat;
82
+
83
+ export const hasCycloneDxFormat = (bomJson) =>
84
+ getCycloneDxFormat(bomJson) === CYCLONEDX_FORMAT;
85
+
14
86
  export const isCycloneDxBom = (bomJson) =>
15
- bomJson?.bomFormat === CYCLONEDX_FORMAT && Boolean(bomJson?.specVersion);
87
+ hasCycloneDxFormat(bomJson) &&
88
+ normalizeCycloneDxSpecVersion(bomJson?.specVersion) !== undefined;
89
+
90
+ const rewriteCycloneDxRootFields = (
91
+ bomJson,
92
+ rootKey,
93
+ specVersion,
94
+ preserveLegacyBomFormat,
95
+ ) => {
96
+ const remainingEntries = Object.entries(bomJson).filter(
97
+ ([key]) => !CYCLONEDX_FORMAT_KEYS.has(key),
98
+ );
99
+ for (const key of Object.keys(bomJson)) {
100
+ delete bomJson[key];
101
+ }
102
+ if (rootKey === LEGACY_CYCLONEDX_ROOT_KEY) {
103
+ bomJson.bomFormat = CYCLONEDX_FORMAT;
104
+ if (specVersion !== undefined) {
105
+ bomJson.specVersion = specVersion;
106
+ }
107
+ } else if (preserveLegacyBomFormat) {
108
+ bomJson.bomFormat = CYCLONEDX_FORMAT;
109
+ if (specVersion !== undefined) {
110
+ bomJson.specVersion = specVersion;
111
+ }
112
+ bomJson.specFormat = CYCLONEDX_FORMAT;
113
+ } else {
114
+ bomJson.specFormat = CYCLONEDX_FORMAT;
115
+ if (specVersion !== undefined) {
116
+ bomJson.specVersion = specVersion;
117
+ }
118
+ }
119
+ for (const [key, value] of remainingEntries) {
120
+ bomJson[key] = value;
121
+ }
122
+ };
123
+
124
+ /**
125
+ * Mutates a CycloneDX BOM object so the appropriate root format key is present
126
+ * for the requested spec version, while preserving conventional serialized
127
+ * root-key ordering (`bomFormat`/`specFormat` and `specVersion` first). Only the currently
128
+ * supported CycloneDX major.minor version shape is accepted; multi-component
129
+ * future versions such as `2.0.1` intentionally return `undefined` from the
130
+ * normalizer rather than being silently truncated.
131
+ *
132
+ * @param {object} bomJson BOM JSON object to mutate.
133
+ * @param {string|number} specVersion Desired CycloneDX spec version.
134
+ * @param {object} options Root-key compatibility options.
135
+ * @returns {object} The same `bomJson` object, after in-place mutation.
136
+ */
137
+ export const setCycloneDxFormat = (
138
+ bomJson,
139
+ specVersion,
140
+ { preserveLegacyBomFormat = false } = {},
141
+ ) => {
142
+ if (!bomJson || typeof bomJson !== "object" || Array.isArray(bomJson)) {
143
+ return bomJson;
144
+ }
145
+ const resolvedSpecVersion =
146
+ toCycloneDxSpecVersionString(specVersion ?? bomJson.specVersion) ||
147
+ bomJson.specVersion;
148
+ if (resolvedSpecVersion !== undefined) {
149
+ bomJson.specVersion = resolvedSpecVersion;
150
+ }
151
+ if (
152
+ getCycloneDxRootFormatKey(resolvedSpecVersion) === MODERN_CYCLONEDX_ROOT_KEY
153
+ ) {
154
+ rewriteCycloneDxRootFields(
155
+ bomJson,
156
+ MODERN_CYCLONEDX_ROOT_KEY,
157
+ resolvedSpecVersion,
158
+ preserveLegacyBomFormat,
159
+ );
160
+ return bomJson;
161
+ }
162
+ rewriteCycloneDxRootFields(
163
+ bomJson,
164
+ LEGACY_CYCLONEDX_ROOT_KEY,
165
+ resolvedSpecVersion,
166
+ false,
167
+ );
168
+ return bomJson;
169
+ };
16
170
 
17
171
  export const detectBomFormat = (bomJson) => {
18
172
  if (isCycloneDxBom(bomJson)) {
@@ -2,9 +2,16 @@ import { assert, describe, it } from "poku";
2
2
 
3
3
  import {
4
4
  detectBomFormat,
5
+ getCycloneDxFormat,
6
+ getCycloneDxRootFormatKey,
5
7
  getNonCycloneDxErrorMessage,
8
+ isCycloneDx20SpecVersion,
6
9
  isCycloneDxBom,
10
+ isCycloneDxSpecVersionAtLeast,
7
11
  isSpdxJsonLd,
12
+ normalizeCycloneDxSpecVersion,
13
+ setCycloneDxFormat,
14
+ toCycloneDxSpecVersionString,
8
15
  } from "./bomUtils.js";
9
16
 
10
17
  const sampleSpdx = {
@@ -13,7 +20,7 @@ const sampleSpdx = {
13
20
  };
14
21
 
15
22
  describe("bomUtils", () => {
16
- it("detects CycloneDX documents", () => {
23
+ it("detects CycloneDX documents across root format styles", () => {
17
24
  assert.strictEqual(
18
25
  isCycloneDxBom({
19
26
  bomFormat: "CycloneDX",
@@ -21,6 +28,13 @@ describe("bomUtils", () => {
21
28
  }),
22
29
  true,
23
30
  );
31
+ assert.strictEqual(
32
+ isCycloneDxBom({
33
+ specFormat: "CycloneDX",
34
+ specVersion: "2.0",
35
+ }),
36
+ true,
37
+ );
24
38
  assert.strictEqual(isCycloneDxBom(sampleSpdx), false);
25
39
  });
26
40
 
@@ -35,6 +49,10 @@ describe("bomUtils", () => {
35
49
  detectBomFormat({ bomFormat: "CycloneDX", specVersion: "1.6" }),
36
50
  "cyclonedx",
37
51
  );
52
+ assert.strictEqual(
53
+ detectBomFormat({ specFormat: "CycloneDX", specVersion: "2.0" }),
54
+ "cyclonedx",
55
+ );
38
56
  assert.strictEqual(detectBomFormat({ foo: "bar" }), "unknown");
39
57
  });
40
58
 
@@ -48,4 +66,64 @@ describe("bomUtils", () => {
48
66
  "cdx-sign expects a CycloneDX JSON BOM.",
49
67
  );
50
68
  });
69
+
70
+ it("normalizes CycloneDX spec versions and capability checks", () => {
71
+ assert.strictEqual(normalizeCycloneDxSpecVersion("2.0"), 2);
72
+ assert.strictEqual(normalizeCycloneDxSpecVersion(undefined), undefined);
73
+ assert.strictEqual(normalizeCycloneDxSpecVersion("2.0.1"), undefined);
74
+ assert.strictEqual(toCycloneDxSpecVersionString(2), "2.0");
75
+ assert.strictEqual(toCycloneDxSpecVersionString("1.10"), "1.10");
76
+ assert.strictEqual(toCycloneDxSpecVersionString("2.0.1"), undefined);
77
+ assert.strictEqual(isCycloneDx20SpecVersion("2.0"), true);
78
+ assert.strictEqual(isCycloneDx20SpecVersion("1.7"), false);
79
+ assert.strictEqual(isCycloneDxSpecVersionAtLeast("2.0", 1.7), true);
80
+ assert.strictEqual(isCycloneDxSpecVersionAtLeast("1.10", "1.7"), true);
81
+ assert.strictEqual(isCycloneDxSpecVersionAtLeast(undefined, 1.7), false);
82
+ });
83
+
84
+ it("selects and writes the correct CycloneDX root format key", () => {
85
+ assert.strictEqual(getCycloneDxRootFormatKey("1.7"), "bomFormat");
86
+ assert.strictEqual(getCycloneDxRootFormatKey("2.0"), "specFormat");
87
+
88
+ const bom17 = setCycloneDxFormat(
89
+ { name: "demo", specVersion: "1.7" },
90
+ "1.7",
91
+ );
92
+ assert.strictEqual(bom17.bomFormat, "CycloneDX");
93
+ assert.strictEqual(bom17.specFormat, undefined);
94
+ assert.strictEqual(getCycloneDxFormat(bom17), "CycloneDX");
95
+ assert.deepStrictEqual(Object.keys(bom17), [
96
+ "bomFormat",
97
+ "specVersion",
98
+ "name",
99
+ ]);
100
+
101
+ const bom20Input = { name: "demo", specVersion: 2 };
102
+ const bom20 = setCycloneDxFormat(bom20Input, 2);
103
+ assert.strictEqual(bom20, bom20Input);
104
+ assert.strictEqual(bom20.specFormat, "CycloneDX");
105
+ assert.strictEqual(bom20.bomFormat, undefined);
106
+ assert.strictEqual(bom20.specVersion, "2.0");
107
+ assert.deepStrictEqual(Object.keys(bom20), [
108
+ "specFormat",
109
+ "specVersion",
110
+ "name",
111
+ ]);
112
+
113
+ const internalBom20 = setCycloneDxFormat(
114
+ { name: "demo", specVersion: 2 },
115
+ 2,
116
+ {
117
+ preserveLegacyBomFormat: true,
118
+ },
119
+ );
120
+ assert.strictEqual(internalBom20.specFormat, "CycloneDX");
121
+ assert.strictEqual(internalBom20.bomFormat, "CycloneDX");
122
+ assert.deepStrictEqual(Object.keys(internalBom20), [
123
+ "bomFormat",
124
+ "specVersion",
125
+ "specFormat",
126
+ "name",
127
+ ]);
128
+ });
51
129
  });
@@ -24,6 +24,21 @@ function isMusl() {
24
24
  return result?.stdout?.includes("musl") || result?.stderr?.includes("musl");
25
25
  }
26
26
 
27
+ function hasUsablePluginsDir(pluginsDir) {
28
+ return (
29
+ safeExistsSync(pluginsDir) &&
30
+ (safeExistsSync(join(pluginsDir, "plugins-manifest.json")) ||
31
+ [
32
+ "cargo-auditable",
33
+ "dosai",
34
+ "osquery",
35
+ "sourcekitten",
36
+ "trivy",
37
+ "trustinspector",
38
+ ].some((pluginName) => safeExistsSync(join(pluginsDir, pluginName))))
39
+ );
40
+ }
41
+
27
42
  /**
28
43
  * Determine the normalized plugin target tuple for the current runtime.
29
44
  *
@@ -81,17 +96,13 @@ export function resolveCdxgenPlugins() {
81
96
  let pluginsDir = process.env.CDXGEN_PLUGINS_DIR || "";
82
97
  let extraNMBinPath;
83
98
 
84
- if (
85
- !pluginsDir &&
86
- safeExistsSync(join(dirNameStr, "plugins")) &&
87
- safeExistsSync(join(dirNameStr, "plugins", "trivy"))
88
- ) {
99
+ if (!pluginsDir && hasUsablePluginsDir(join(dirNameStr, "plugins"))) {
89
100
  pluginsDir = join(dirNameStr, "plugins");
90
101
  }
91
102
 
92
103
  if (
93
104
  !pluginsDir &&
94
- safeExistsSync(
105
+ hasUsablePluginsDir(
95
106
  join(
96
107
  dirNameStr,
97
108
  "node_modules",
@@ -99,16 +110,6 @@ export function resolveCdxgenPlugins() {
99
110
  `cdxgen-plugins-bin${target.pluginsBinSuffix}`,
100
111
  "plugins",
101
112
  ),
102
- ) &&
103
- safeExistsSync(
104
- join(
105
- dirNameStr,
106
- "node_modules",
107
- "@cdxgen",
108
- `cdxgen-plugins-bin${target.pluginsBinSuffix}`,
109
- "plugins",
110
- "trivy",
111
- ),
112
113
  )
113
114
  ) {
114
115
  pluginsDir = join(
@@ -11,6 +11,7 @@ import {
11
11
  supportedSpecVersions,
12
12
  } from "@appthreat/cdx-proto";
13
13
 
14
+ import { toCycloneDxSpecVersionString } from "./bomUtils.js";
14
15
  import { safeExistsSync, safeWriteSync } from "./utils.js";
15
16
 
16
17
  const JSON_READ_OPTIONS = {
@@ -29,6 +30,11 @@ const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"];
29
30
 
30
31
  const DEFAULT_SPEC_VERSION =
31
32
  supportedSpecVersions[supportedSpecVersions.length - 1];
33
+ const PROTO_SUPPORTED_SPEC_VERSIONS = new Set(
34
+ supportedSpecVersions.map((specVersion) =>
35
+ toCycloneDxSpecVersionString(specVersion),
36
+ ),
37
+ );
32
38
 
33
39
  const isProtoMessageBom = (bom) =>
34
40
  Boolean(
@@ -47,6 +53,42 @@ const hasExplicitSpecVersion = (bomJson) =>
47
53
  (bomJson.specVersion !== undefined || bomJson.spec_version !== undefined),
48
54
  );
49
55
 
56
+ const resolveExplicitSpecVersion = (bomJson) =>
57
+ bomJson?.specVersion ?? bomJson?.spec_version;
58
+
59
+ const hasProvidedSpecVersion = (specVersion) =>
60
+ specVersion !== undefined &&
61
+ specVersion !== null &&
62
+ `${specVersion}`.trim() !== "";
63
+
64
+ export const isProtoSupportedSpecVersion = (specVersion) => {
65
+ if (!hasProvidedSpecVersion(specVersion)) {
66
+ return true;
67
+ }
68
+ const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion);
69
+ return (
70
+ normalizedSpecVersion !== undefined &&
71
+ PROTO_SUPPORTED_SPEC_VERSIONS.has(normalizedSpecVersion)
72
+ );
73
+ };
74
+
75
+ export const assertProtoSupportedSpecVersion = (
76
+ specVersion,
77
+ operation = "protobuf operations",
78
+ ) => {
79
+ if (!hasProvidedSpecVersion(specVersion)) {
80
+ return;
81
+ }
82
+ const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion);
83
+ if (isProtoSupportedSpecVersion(specVersion)) {
84
+ return;
85
+ }
86
+ const displaySpecVersion = normalizedSpecVersion || `${specVersion}`.trim();
87
+ throw new Error(
88
+ `CycloneDX ${displaySpecVersion} is not currently supported for ${operation}. @appthreat/cdx-proto supports ${supportedSpecVersions.join(", ")} only.`,
89
+ );
90
+ };
91
+
50
92
  const OBJECT_WRAPPED_LIST_FIELDS = ["declarations", "definitions"];
51
93
 
52
94
  const isPlainObject = (value) =>
@@ -121,15 +163,25 @@ const resolveBomMessage = (bomJson, specVersion = DEFAULT_SPEC_VERSION) => {
121
163
  JSON.parse(`${bomJson}`),
122
164
  );
123
165
  if (hasExplicitSpecVersion(parsedBomJson)) {
166
+ assertProtoSupportedSpecVersion(
167
+ resolveExplicitSpecVersion(parsedBomJson),
168
+ "protobuf serialization",
169
+ );
124
170
  return parseBomJson(parsedBomJson, JSON_READ_OPTIONS);
125
171
  }
172
+ assertProtoSupportedSpecVersion(specVersion, "protobuf serialization");
126
173
  return decodeBomJson(specVersion, parsedBomJson, JSON_READ_OPTIONS);
127
174
  }
128
175
  if (bomJson && typeof bomJson === "object" && !Array.isArray(bomJson)) {
129
176
  const normalizedBomJson = normalizeObjectWrappedListsForProto(bomJson);
130
177
  if (hasExplicitSpecVersion(normalizedBomJson)) {
178
+ assertProtoSupportedSpecVersion(
179
+ resolveExplicitSpecVersion(normalizedBomJson),
180
+ "protobuf serialization",
181
+ );
131
182
  return parseBomJson(normalizedBomJson, JSON_READ_OPTIONS);
132
183
  }
184
+ assertProtoSupportedSpecVersion(specVersion, "protobuf serialization");
133
185
  return decodeBomJson(specVersion, normalizedBomJson, JSON_READ_OPTIONS);
134
186
  }
135
187
  return createBom(specVersion);
@@ -175,6 +227,7 @@ export const writeBinary = (
175
227
  */
176
228
  export const readBinary = (binFile, asJson, specVersion) => {
177
229
  asJson = asJson ?? true;
230
+ assertProtoSupportedSpecVersion(specVersion, "protobuf decoding");
178
231
  if (!safeExistsSync(binFile)) {
179
232
  return undefined;
180
233
  }