@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.
- package/README.md +6 -4
- package/bin/cdxgen.js +32 -11
- package/bin/convert.js +12 -8
- package/bin/hbom.js +13 -8
- package/bin/repl.js +14 -10
- package/bin/validate.js +10 -13
- package/bin/verify.js +7 -29
- package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
- package/lib/audit/index.js +2 -1
- package/lib/cli/index.js +21 -11
- package/lib/cli/index.poku.js +117 -0
- package/lib/helpers/bomUtils.js +155 -1
- package/lib/helpers/bomUtils.poku.js +79 -1
- package/lib/helpers/plugins.js +17 -16
- package/lib/helpers/protobom.js +53 -0
- package/lib/helpers/protobom.poku.js +44 -1
- package/lib/helpers/protobomLoader.js +43 -0
- package/lib/helpers/protobomLoader.poku.js +31 -0
- package/lib/server/server.js +2 -1
- package/lib/stages/postgen/postgen.js +219 -12
- package/lib/stages/postgen/postgen.poku.js +163 -0
- package/lib/validator/bomValidator.js +90 -38
- package/lib/validator/bomValidator.poku.js +90 -0
- package/lib/validator/complianceRules.js +4 -2
- package/lib/validator/index.poku.js +14 -0
- package/package.json +1 -1
- package/types/bin/repl.d.ts +1 -1
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/bomUtils.d.ts +10 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -1
- package/types/lib/helpers/hbomAnalysis.d.ts +14 -0
- package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -1
- package/types/lib/helpers/hostTopology.d.ts.map +1 -1
- package/types/lib/helpers/plugins.d.ts.map +1 -1
- package/types/lib/helpers/protobom.d.ts +2 -0
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- package/types/lib/helpers/protobomLoader.d.ts +17 -0
- package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
- package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
package/lib/audit/index.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
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
|
|
package/lib/cli/index.poku.js
CHANGED
|
@@ -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
|
}
|
package/lib/helpers/bomUtils.js
CHANGED
|
@@ -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
|
|
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
|
});
|
package/lib/helpers/plugins.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
package/lib/helpers/protobom.js
CHANGED
|
@@ -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
|
}
|