@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
|
@@ -3,7 +3,12 @@ import { join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { assert, it } from "poku";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
assertProtoSupportedSpecVersion,
|
|
8
|
+
isProtoBomFile,
|
|
9
|
+
readBinary,
|
|
10
|
+
writeBinary,
|
|
11
|
+
} from "./protobom.js";
|
|
7
12
|
import { getTmpDir } from "./utils.js";
|
|
8
13
|
|
|
9
14
|
const testBom = JSON.parse(
|
|
@@ -137,6 +142,44 @@ it("keeps canonical definitions and declarations as objects during proto round-t
|
|
|
137
142
|
cleanupTempDir(tempDir);
|
|
138
143
|
});
|
|
139
144
|
|
|
145
|
+
it("rejects unsupported CycloneDX 2.0 protobuf operations with a clear error", () => {
|
|
146
|
+
const tempDir = createTempDir();
|
|
147
|
+
const binFile = join(tempDir, "unsupported-2.0.cdx");
|
|
148
|
+
assert.throws(
|
|
149
|
+
() =>
|
|
150
|
+
writeBinary(
|
|
151
|
+
{
|
|
152
|
+
specFormat: "CycloneDX",
|
|
153
|
+
specVersion: "2.0",
|
|
154
|
+
version: 1,
|
|
155
|
+
},
|
|
156
|
+
binFile,
|
|
157
|
+
),
|
|
158
|
+
/CycloneDX 2\.0 is not currently supported for protobuf serialization/,
|
|
159
|
+
);
|
|
160
|
+
assert.throws(
|
|
161
|
+
() => assertProtoSupportedSpecVersion("2.0", "protobuf export"),
|
|
162
|
+
/@appthreat\/cdx-proto supports 1\.5, 1\.6, 1\.7 only/,
|
|
163
|
+
);
|
|
164
|
+
assert.throws(
|
|
165
|
+
() =>
|
|
166
|
+
writeBinary(
|
|
167
|
+
{
|
|
168
|
+
bomFormat: "CycloneDX",
|
|
169
|
+
specVersion: "2.0.1",
|
|
170
|
+
version: 1,
|
|
171
|
+
},
|
|
172
|
+
binFile,
|
|
173
|
+
),
|
|
174
|
+
/CycloneDX 2\.0\.1 is not currently supported for protobuf serialization/,
|
|
175
|
+
);
|
|
176
|
+
assert.throws(
|
|
177
|
+
() => assertProtoSupportedSpecVersion("2.0.1", "protobuf export"),
|
|
178
|
+
/CycloneDX 2\.0\.1 is not currently supported for protobuf export/,
|
|
179
|
+
);
|
|
180
|
+
cleanupTempDir(tempDir);
|
|
181
|
+
});
|
|
182
|
+
|
|
140
183
|
it("round-trips real CBOM fixture data with cryptographic assets intact", () => {
|
|
141
184
|
const tempDir = createTempDir();
|
|
142
185
|
const binFile = join(tempDir, "cbom-fixture.cdx");
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"];
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Determine whether a path looks like a CycloneDX protobuf BOM file.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} filePath File path
|
|
7
|
+
* @returns {boolean} true when the path uses a protobuf BOM extension
|
|
8
|
+
*/
|
|
9
|
+
export function isProtoBomPath(filePath) {
|
|
10
|
+
const normalizedPath = `${filePath || ""}`.toLowerCase();
|
|
11
|
+
return PROTO_BOM_FILE_EXTENSIONS.some((extension) =>
|
|
12
|
+
normalizedPath.endsWith(extension),
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Import protobuf BOM helpers and replace optional-dependency loader failures
|
|
18
|
+
* with actionable command-specific messages.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} [commandName="cdxgen"] CLI command name
|
|
21
|
+
* @param {string} [featureDescription="protobuf support"] Feature being used
|
|
22
|
+
* @returns {Promise<object>} Loaded protobom module namespace
|
|
23
|
+
*/
|
|
24
|
+
export async function importProtobomModule(
|
|
25
|
+
commandName = "cdxgen",
|
|
26
|
+
featureDescription = "protobuf support",
|
|
27
|
+
) {
|
|
28
|
+
try {
|
|
29
|
+
return await import("./protobom.js");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const message = `${error?.message || ""}`;
|
|
32
|
+
if (
|
|
33
|
+
error?.code === "ERR_MODULE_NOT_FOUND" ||
|
|
34
|
+
message.includes("@appthreat/cdx-proto") ||
|
|
35
|
+
message.includes("@bufbuild/protobuf")
|
|
36
|
+
) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`${commandName} ${featureDescription} requires the optional '@appthreat/cdx-proto' and '@bufbuild/protobuf' dependencies. Install optional dependencies or use a binary that bundles protobuf support.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import { importProtobomModule, isProtoBomPath } from "./protobomLoader.js";
|
|
4
|
+
|
|
5
|
+
describe("protobomLoader", () => {
|
|
6
|
+
it("detects protobuf BOM file extensions", () => {
|
|
7
|
+
assert.strictEqual(isProtoBomPath("bom.cdx"), true);
|
|
8
|
+
assert.strictEqual(isProtoBomPath("bom.CDX.BIN"), true);
|
|
9
|
+
assert.strictEqual(isProtoBomPath("bom.proto"), true);
|
|
10
|
+
assert.strictEqual(isProtoBomPath("bom.json"), false);
|
|
11
|
+
assert.strictEqual(isProtoBomPath(""), false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("imports the protobuf BOM helper when optional support is installed", async () => {
|
|
15
|
+
let protobomModule;
|
|
16
|
+
try {
|
|
17
|
+
protobomModule = await importProtobomModule(
|
|
18
|
+
"cdx-test",
|
|
19
|
+
"protobuf BOM input",
|
|
20
|
+
);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
assert.match(
|
|
23
|
+
error.message,
|
|
24
|
+
/requires the optional '@appthreat\/cdx-proto' and '@bufbuild\/protobuf' dependencies/u,
|
|
25
|
+
);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
assert.strictEqual(typeof protobomModule.readBinary, "function");
|
|
29
|
+
assert.strictEqual(typeof protobomModule.writeBinary, "function");
|
|
30
|
+
});
|
|
31
|
+
});
|
package/lib/server/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import compression from "compression";
|
|
|
8
8
|
import connect from "connect";
|
|
9
9
|
|
|
10
10
|
import { createBom, submitBom } from "../cli/index.js";
|
|
11
|
+
import { isCycloneDxBom } from "../helpers/bomUtils.js";
|
|
11
12
|
import { normalizeOutputFormats } from "../helpers/exportUtils.js";
|
|
12
13
|
import {
|
|
13
14
|
cleanupSourceDir,
|
|
@@ -467,7 +468,7 @@ const start = (options) => {
|
|
|
467
468
|
let responseBomJson = bomNSData.bomJson;
|
|
468
469
|
if (
|
|
469
470
|
requestedFormats.includes("spdx") &&
|
|
470
|
-
bomNSData?.bomJson
|
|
471
|
+
isCycloneDxBom(bomNSData?.bomJson)
|
|
471
472
|
) {
|
|
472
473
|
responseBomJson = convertCycloneDxToSpdx(bomNSData.bomJson, reqOptions);
|
|
473
474
|
}
|
|
@@ -9,6 +9,12 @@ import {
|
|
|
9
9
|
matchesAiInventoryExcludeType,
|
|
10
10
|
optionIncludesAiInventoryProjectType,
|
|
11
11
|
} from "../../helpers/aiInventory.js";
|
|
12
|
+
import {
|
|
13
|
+
isCycloneDx20SpecVersion,
|
|
14
|
+
normalizeCycloneDxSpecVersion,
|
|
15
|
+
setCycloneDxFormat,
|
|
16
|
+
toCycloneDxSpecVersionString,
|
|
17
|
+
} from "../../helpers/bomUtils.js";
|
|
12
18
|
import { mergeDependencies, mergeServices } from "../../helpers/depsUtils.js";
|
|
13
19
|
import { addFormulationSection } from "../../helpers/formulationParsers.js";
|
|
14
20
|
import { getContainerFileInventoryStats } from "../../helpers/inventoryStats.js";
|
|
@@ -160,10 +166,8 @@ const SERVICE_1_6_ONLY_FIELDS = new Set(["tags"]);
|
|
|
160
166
|
const SERVICE_1_7_ONLY_FIELDS = new Set(["patentAssertions"]);
|
|
161
167
|
const METADATA_1_6_ONLY_FIELDS = new Set(["manufacturer"]);
|
|
162
168
|
const METADATA_1_7_ONLY_FIELDS = new Set(["distributionConstraints"]);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return Number.parseFloat(String(specVersion || 0));
|
|
166
|
-
}
|
|
169
|
+
const METADATA_2_0_REMOVED_FIELDS = new Set(["manufacture"]);
|
|
170
|
+
const COMPONENT_2_0_REMOVED_FIELDS = new Set(["author", "modified"]);
|
|
167
171
|
|
|
168
172
|
function normalizeTlpClassification(tlpClassification) {
|
|
169
173
|
return String(tlpClassification || "")
|
|
@@ -274,9 +278,10 @@ function collectSensitivePropertyViolations(
|
|
|
274
278
|
}
|
|
275
279
|
|
|
276
280
|
function validateTlpClassification(bomJson, options) {
|
|
277
|
-
const specVersion =
|
|
278
|
-
|
|
279
|
-
|
|
281
|
+
const specVersion =
|
|
282
|
+
normalizeCycloneDxSpecVersion(
|
|
283
|
+
bomJson?.specVersion || options?.specVersion,
|
|
284
|
+
) || 0;
|
|
280
285
|
if (specVersion < 1.7) {
|
|
281
286
|
return bomJson;
|
|
282
287
|
}
|
|
@@ -366,6 +371,10 @@ function deleteFields(subject, fields) {
|
|
|
366
371
|
}
|
|
367
372
|
}
|
|
368
373
|
|
|
374
|
+
function isObjectRecord(value) {
|
|
375
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
376
|
+
}
|
|
377
|
+
|
|
369
378
|
function normalizeComponentForSpecVersion(subject, specVersion) {
|
|
370
379
|
if (specVersion < 1.6) {
|
|
371
380
|
deleteFields(subject, COMPONENT_1_6_ONLY_FIELDS);
|
|
@@ -393,6 +402,190 @@ function normalizeMetadataForSpecVersion(subject, specVersion) {
|
|
|
393
402
|
}
|
|
394
403
|
}
|
|
395
404
|
|
|
405
|
+
function authorStringToAuthors(authorValue) {
|
|
406
|
+
if (typeof authorValue !== "string") {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
const authors = authorValue
|
|
410
|
+
.split(",")
|
|
411
|
+
.map((author) => author.trim())
|
|
412
|
+
.filter(Boolean)
|
|
413
|
+
.map((name) => ({ name }));
|
|
414
|
+
return authors.length ? authors : undefined;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function normalizeLegacyToolComponent(tool) {
|
|
418
|
+
if (!isObjectRecord(tool)) {
|
|
419
|
+
return tool;
|
|
420
|
+
}
|
|
421
|
+
if (!tool.type) {
|
|
422
|
+
tool.type = "application";
|
|
423
|
+
}
|
|
424
|
+
if (tool.vendor && !tool.publisher) {
|
|
425
|
+
tool.publisher = tool.vendor;
|
|
426
|
+
}
|
|
427
|
+
delete tool.vendor;
|
|
428
|
+
if (!tool.authors && tool.author) {
|
|
429
|
+
tool.authors = authorStringToAuthors(tool.author);
|
|
430
|
+
}
|
|
431
|
+
deleteFields(tool, COMPONENT_2_0_REMOVED_FIELDS);
|
|
432
|
+
normalizeComponentsForSpecVersion(tool.components);
|
|
433
|
+
return tool;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function hasExplicitSpecVersion(specVersion) {
|
|
437
|
+
return (
|
|
438
|
+
specVersion !== undefined &&
|
|
439
|
+
specVersion !== null &&
|
|
440
|
+
`${specVersion}`.trim() !== ""
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function resolveSpecVersionForCompatibility(bomJson, options) {
|
|
445
|
+
if (hasExplicitSpecVersion(options?.specVersion)) {
|
|
446
|
+
return options.specVersion;
|
|
447
|
+
}
|
|
448
|
+
if (hasExplicitSpecVersion(bomJson?.specVersion)) {
|
|
449
|
+
return bomJson.specVersion;
|
|
450
|
+
}
|
|
451
|
+
return "1.7";
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function normalizeLegacyToolService(service) {
|
|
455
|
+
if (!isObjectRecord(service)) {
|
|
456
|
+
return service;
|
|
457
|
+
}
|
|
458
|
+
if (service.vendor && !service.provider) {
|
|
459
|
+
service.provider =
|
|
460
|
+
typeof service.vendor === "string"
|
|
461
|
+
? { name: service.vendor }
|
|
462
|
+
: service.vendor;
|
|
463
|
+
}
|
|
464
|
+
delete service.vendor;
|
|
465
|
+
deleteFields(service, COMPONENT_2_0_REMOVED_FIELDS);
|
|
466
|
+
normalizeServicesForSpecVersion(service.services);
|
|
467
|
+
return service;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function normalizeComponentsForSpecVersion(components) {
|
|
471
|
+
if (!Array.isArray(components)) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
for (const component of components) {
|
|
475
|
+
normalizeComponentForSpecVersion20(component);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function normalizeComponentForSpecVersion20(component) {
|
|
480
|
+
if (!isObjectRecord(component)) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (!component.authors && component.author) {
|
|
484
|
+
component.authors = authorStringToAuthors(component.author);
|
|
485
|
+
}
|
|
486
|
+
deleteFields(component, COMPONENT_2_0_REMOVED_FIELDS);
|
|
487
|
+
normalizeComponentsForSpecVersion(component.components);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function normalizeServicesForSpecVersion(services) {
|
|
491
|
+
if (!Array.isArray(services)) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
for (const service of services) {
|
|
495
|
+
normalizeLegacyToolService(service);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function normalizeFormulationForSpecVersion(formulation) {
|
|
500
|
+
if (!Array.isArray(formulation)) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
for (const formula of formulation) {
|
|
504
|
+
if (!isObjectRecord(formula)) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
normalizeComponentsForSpecVersion(formula.components);
|
|
508
|
+
normalizeServicesForSpecVersion(formula.services);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function normalizeVulnerabilitiesForSpecVersion(vulnerabilities) {
|
|
513
|
+
if (!Array.isArray(vulnerabilities)) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
for (const vulnerability of vulnerabilities) {
|
|
517
|
+
if (!isObjectRecord(vulnerability?.tools)) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
normalizeComponentsForSpecVersion(vulnerability.tools.components);
|
|
521
|
+
normalizeServicesForSpecVersion(vulnerability.tools.services);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function normalizeDefinitionsForSpecVersion(definitions) {
|
|
526
|
+
if (!isObjectRecord(definitions)) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
normalizeComponentsForSpecVersion(definitions.components);
|
|
530
|
+
normalizeServicesForSpecVersion(definitions.services);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function migrateLegacyManufactureForSpecVersion(metadata) {
|
|
534
|
+
if (!metadata.manufacture) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (isObjectRecord(metadata.component) && !metadata.component.manufacturer) {
|
|
538
|
+
metadata.component.manufacturer = metadata.manufacture;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (!metadata.manufacturer) {
|
|
542
|
+
metadata.manufacturer = metadata.manufacture;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function normalizeToolsForSpecVersion(subject, specVersion) {
|
|
547
|
+
if (!subject || !isCycloneDx20SpecVersion(specVersion)) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (Array.isArray(subject.tools)) {
|
|
551
|
+
subject.tools = {
|
|
552
|
+
components: subject.tools.map((tool) =>
|
|
553
|
+
normalizeLegacyToolComponent(isObjectRecord(tool) ? { ...tool } : tool),
|
|
554
|
+
),
|
|
555
|
+
};
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (!isObjectRecord(subject.tools)) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (Array.isArray(subject.tools.components)) {
|
|
562
|
+
subject.tools.components = subject.tools.components.map((tool) =>
|
|
563
|
+
normalizeLegacyToolComponent(tool),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
if (Array.isArray(subject.tools.services)) {
|
|
567
|
+
subject.tools.services = subject.tools.services.map((service) =>
|
|
568
|
+
normalizeLegacyToolService(service),
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function upgradeSubjectForSpecVersion(subject, specVersion) {
|
|
574
|
+
if (!isObjectRecord(subject) || !isCycloneDx20SpecVersion(specVersion)) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (isObjectRecord(subject.metadata)) {
|
|
578
|
+
migrateLegacyManufactureForSpecVersion(subject.metadata);
|
|
579
|
+
deleteFields(subject.metadata, METADATA_2_0_REMOVED_FIELDS);
|
|
580
|
+
normalizeToolsForSpecVersion(subject.metadata, specVersion);
|
|
581
|
+
normalizeComponentForSpecVersion20(subject.metadata.component);
|
|
582
|
+
}
|
|
583
|
+
normalizeComponentsForSpecVersion(subject.components);
|
|
584
|
+
normalizeFormulationForSpecVersion(subject.formulation);
|
|
585
|
+
normalizeDefinitionsForSpecVersion(subject.definitions);
|
|
586
|
+
normalizeVulnerabilitiesForSpecVersion(subject.vulnerabilities);
|
|
587
|
+
}
|
|
588
|
+
|
|
396
589
|
function downgradeSubjectForSpecVersion(subject, specVersion, parentKey) {
|
|
397
590
|
if (!subject || typeof subject !== "object") {
|
|
398
591
|
return;
|
|
@@ -460,14 +653,28 @@ function downgradeSubjectForSpecVersion(subject, specVersion, parentKey) {
|
|
|
460
653
|
}
|
|
461
654
|
|
|
462
655
|
function applySpecVersionCompatibility(bomJson, options) {
|
|
463
|
-
const
|
|
464
|
-
|
|
656
|
+
const requestedSpecVersion = resolveSpecVersionForCompatibility(
|
|
657
|
+
bomJson,
|
|
658
|
+
options,
|
|
465
659
|
);
|
|
466
|
-
|
|
660
|
+
const normalizedSpecVersion =
|
|
661
|
+
toCycloneDxSpecVersionString(requestedSpecVersion);
|
|
662
|
+
if (!normalizedSpecVersion) {
|
|
663
|
+
thoughtLog(
|
|
664
|
+
"Skipping CycloneDX specVersion compatibility updates for malformed explicit specVersion.",
|
|
665
|
+
{
|
|
666
|
+
specVersion: requestedSpecVersion,
|
|
667
|
+
},
|
|
668
|
+
);
|
|
467
669
|
return bomJson;
|
|
468
670
|
}
|
|
469
|
-
|
|
470
|
-
|
|
671
|
+
const specVersion = normalizeCycloneDxSpecVersion(normalizedSpecVersion);
|
|
672
|
+
if (specVersion < 1.7) {
|
|
673
|
+
downgradeSubjectForSpecVersion(bomJson, specVersion);
|
|
674
|
+
} else if (isCycloneDx20SpecVersion(specVersion)) {
|
|
675
|
+
upgradeSubjectForSpecVersion(bomJson, specVersion);
|
|
676
|
+
}
|
|
677
|
+
return setCycloneDxFormat(bomJson, normalizedSpecVersion);
|
|
471
678
|
}
|
|
472
679
|
|
|
473
680
|
/**
|
|
@@ -241,6 +241,169 @@ it("postProcess passes formulationList from bomNSData into the formulation secti
|
|
|
241
241
|
);
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
+
it("postProcess finalizes CycloneDX 2.0-dev root fields and strips legacy fields", () => {
|
|
245
|
+
const bomNSData = {
|
|
246
|
+
bomJson: {
|
|
247
|
+
bomFormat: "CycloneDX",
|
|
248
|
+
specVersion: "1.7",
|
|
249
|
+
metadata: {
|
|
250
|
+
manufacture: { name: "Legacy Factory" },
|
|
251
|
+
component: "not-a-component-object",
|
|
252
|
+
tools: [
|
|
253
|
+
{
|
|
254
|
+
author: "OWASP Foundation",
|
|
255
|
+
name: "cdxgen",
|
|
256
|
+
vendor: "OWASP Foundation",
|
|
257
|
+
version: "12.4.0",
|
|
258
|
+
components: [
|
|
259
|
+
{
|
|
260
|
+
author: "Nested Tool Author",
|
|
261
|
+
modified: true,
|
|
262
|
+
name: "cdxgen-plugin",
|
|
263
|
+
type: "library",
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
components: [
|
|
270
|
+
{
|
|
271
|
+
author: "Jane Doe",
|
|
272
|
+
modified: false,
|
|
273
|
+
name: "demo-lib",
|
|
274
|
+
type: "library",
|
|
275
|
+
version: "1.0.0",
|
|
276
|
+
components: [
|
|
277
|
+
{
|
|
278
|
+
author: "Nested Author",
|
|
279
|
+
modified: true,
|
|
280
|
+
name: "nested-lib",
|
|
281
|
+
type: "library",
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const result = postProcess(bomNSData, { specVersion: 2.0 });
|
|
290
|
+
const [toolComponent] = result.bomJson.metadata.tools.components;
|
|
291
|
+
const [component] = result.bomJson.components;
|
|
292
|
+
|
|
293
|
+
assert.strictEqual(result.bomJson.specFormat, "CycloneDX");
|
|
294
|
+
assert.strictEqual(result.bomJson.bomFormat, undefined);
|
|
295
|
+
assert.strictEqual(result.bomJson.specVersion, "2.0");
|
|
296
|
+
assert.strictEqual(result.bomJson.metadata.manufacture, undefined);
|
|
297
|
+
assert.deepStrictEqual(result.bomJson.metadata.manufacturer, {
|
|
298
|
+
name: "Legacy Factory",
|
|
299
|
+
});
|
|
300
|
+
assert.strictEqual(
|
|
301
|
+
result.bomJson.metadata.component,
|
|
302
|
+
"not-a-component-object",
|
|
303
|
+
);
|
|
304
|
+
assert.strictEqual(toolComponent.publisher, "OWASP Foundation");
|
|
305
|
+
assert.deepStrictEqual(toolComponent.authors, [{ name: "OWASP Foundation" }]);
|
|
306
|
+
assert.strictEqual(toolComponent.author, undefined);
|
|
307
|
+
assert.deepStrictEqual(toolComponent.components[0].authors, [
|
|
308
|
+
{ name: "Nested Tool Author" },
|
|
309
|
+
]);
|
|
310
|
+
assert.strictEqual(toolComponent.components[0].author, undefined);
|
|
311
|
+
assert.strictEqual(toolComponent.components[0].modified, undefined);
|
|
312
|
+
assert.deepStrictEqual(component.authors, [{ name: "Jane Doe" }]);
|
|
313
|
+
assert.strictEqual(component.author, undefined);
|
|
314
|
+
assert.strictEqual(component.modified, undefined);
|
|
315
|
+
assert.deepStrictEqual(component.components[0].authors, [
|
|
316
|
+
{ name: "Nested Author" },
|
|
317
|
+
]);
|
|
318
|
+
assert.strictEqual(component.components[0].author, undefined);
|
|
319
|
+
assert.strictEqual(component.components[0].modified, undefined);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("postProcess preserves malformed explicit specVersion values instead of coercing them to 1.7", () => {
|
|
323
|
+
const malformedBomResult = postProcess(
|
|
324
|
+
{
|
|
325
|
+
bomJson: {
|
|
326
|
+
bomFormat: "CycloneDX",
|
|
327
|
+
specVersion: "2.0.1",
|
|
328
|
+
components: [],
|
|
329
|
+
dependencies: [],
|
|
330
|
+
metadata: { properties: [] },
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
{},
|
|
334
|
+
);
|
|
335
|
+
assert.strictEqual(malformedBomResult.bomJson.specVersion, "2.0.1");
|
|
336
|
+
assert.strictEqual(malformedBomResult.bomJson.bomFormat, "CycloneDX");
|
|
337
|
+
assert.strictEqual(malformedBomResult.bomJson.specFormat, undefined);
|
|
338
|
+
|
|
339
|
+
const malformedOptionResult = postProcess(
|
|
340
|
+
{
|
|
341
|
+
bomJson: {
|
|
342
|
+
bomFormat: "CycloneDX",
|
|
343
|
+
specVersion: "1.7",
|
|
344
|
+
components: [],
|
|
345
|
+
dependencies: [],
|
|
346
|
+
metadata: { properties: [] },
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{ specVersion: "2.0.1" },
|
|
350
|
+
);
|
|
351
|
+
assert.strictEqual(malformedOptionResult.bomJson.specVersion, "1.7");
|
|
352
|
+
assert.strictEqual(malformedOptionResult.bomJson.bomFormat, "CycloneDX");
|
|
353
|
+
assert.strictEqual(malformedOptionResult.bomJson.specFormat, undefined);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("postProcess migrates CycloneDX 2.0 metadata manufacture and tool services without broad recursion", () => {
|
|
357
|
+
const bomNSData = {
|
|
358
|
+
bomJson: {
|
|
359
|
+
bomFormat: "CycloneDX",
|
|
360
|
+
specVersion: "1.7",
|
|
361
|
+
metadata: {
|
|
362
|
+
manufacture: { name: "Component Factory" },
|
|
363
|
+
component: {
|
|
364
|
+
name: "demo-app",
|
|
365
|
+
type: "application",
|
|
366
|
+
},
|
|
367
|
+
tools: {
|
|
368
|
+
components: [],
|
|
369
|
+
services: [
|
|
370
|
+
{
|
|
371
|
+
author: "Legacy Author",
|
|
372
|
+
name: "scanner-service",
|
|
373
|
+
vendor: "Scanner Vendor",
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
components: [],
|
|
379
|
+
dependencies: [{ ref: "pkg:generic/demo@1.0.0", dependsOn: [] }],
|
|
380
|
+
unrelatedInventory: {
|
|
381
|
+
components: [
|
|
382
|
+
{
|
|
383
|
+
author: "Should Not Be Traversed",
|
|
384
|
+
name: "not-a-component-list",
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const result = postProcess(bomNSData, { specVersion: 2.0 });
|
|
392
|
+
const [toolService] = result.bomJson.metadata.tools.services;
|
|
393
|
+
|
|
394
|
+
assert.strictEqual(result.bomJson.metadata.manufacture, undefined);
|
|
395
|
+
assert.deepStrictEqual(result.bomJson.metadata.component.manufacturer, {
|
|
396
|
+
name: "Component Factory",
|
|
397
|
+
});
|
|
398
|
+
assert.deepStrictEqual(toolService.provider, { name: "Scanner Vendor" });
|
|
399
|
+
assert.strictEqual(toolService.vendor, undefined);
|
|
400
|
+
assert.strictEqual(toolService.author, undefined);
|
|
401
|
+
assert.strictEqual(
|
|
402
|
+
result.bomJson.unrelatedInventory.components[0].author,
|
|
403
|
+
"Should Not Be Traversed",
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
244
407
|
it("postProcess downgrades certificate crypto properties for spec version 1.6", () => {
|
|
245
408
|
const bomNSData = {
|
|
246
409
|
bomJson: {
|