@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
@@ -3,7 +3,12 @@ import { join } from "node:path";
3
3
 
4
4
  import { assert, it } from "poku";
5
5
 
6
- import { isProtoBomFile, readBinary, writeBinary } from "./protobom.js";
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
+ });
@@ -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?.bomFormat === "CycloneDX"
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
- function normalizeSpecVersion(specVersion) {
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 = normalizeSpecVersion(
278
- bomJson?.specVersion || options?.specVersion,
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 specVersion = normalizeSpecVersion(
464
- options?.specVersion || bomJson?.specVersion || 1.7,
656
+ const requestedSpecVersion = resolveSpecVersionForCompatibility(
657
+ bomJson,
658
+ options,
465
659
  );
466
- if (specVersion >= 1.7) {
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
- downgradeSubjectForSpecVersion(bomJson, specVersion);
470
- return bomJson;
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: {