@cyclonedx/cdxgen 12.4.0 → 12.4.2

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 (71) 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/evinse.js +15 -0
  5. package/bin/hbom.js +13 -8
  6. package/bin/repl.js +14 -10
  7. package/bin/validate.js +10 -13
  8. package/bin/verify.js +7 -29
  9. package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
  10. package/lib/audit/index.js +2 -1
  11. package/lib/cli/index.js +77 -16
  12. package/lib/cli/index.poku.js +197 -0
  13. package/lib/evinser/evinser.js +118 -3
  14. package/lib/helpers/bomUtils.js +155 -1
  15. package/lib/helpers/bomUtils.poku.js +79 -1
  16. package/lib/helpers/cbomutils.js +162 -2
  17. package/lib/helpers/cbomutils.poku.js +100 -0
  18. package/lib/helpers/ciParsers/githubActions.js +15 -3
  19. package/lib/helpers/ciParsers/githubActions.poku.js +52 -0
  20. package/lib/helpers/dosai.js +433 -0
  21. package/lib/helpers/dosai.poku.js +302 -0
  22. package/lib/helpers/dosaiParsers.js +103 -0
  23. package/lib/helpers/plugins.js +17 -16
  24. package/lib/helpers/protobom.js +53 -0
  25. package/lib/helpers/protobom.poku.js +44 -1
  26. package/lib/helpers/protobomLoader.js +43 -0
  27. package/lib/helpers/protobomLoader.poku.js +31 -0
  28. package/lib/helpers/utils.js +130 -1
  29. package/lib/helpers/utils.poku.js +295 -0
  30. package/lib/server/server.js +2 -1
  31. package/lib/stages/postgen/annotator.js +2 -1
  32. package/lib/stages/postgen/annotator.poku.js +28 -0
  33. package/lib/stages/postgen/postgen.js +219 -12
  34. package/lib/stages/postgen/postgen.poku.js +163 -0
  35. package/lib/validator/bomValidator.js +90 -38
  36. package/lib/validator/bomValidator.poku.js +90 -0
  37. package/lib/validator/complianceRules.js +4 -2
  38. package/lib/validator/index.poku.js +14 -0
  39. package/package.json +12 -12
  40. package/types/bin/repl.d.ts +1 -1
  41. package/types/bin/repl.d.ts.map +1 -1
  42. package/types/lib/audit/index.d.ts.map +1 -1
  43. package/types/lib/cli/index.d.ts.map +1 -1
  44. package/types/lib/evinser/evinser.d.ts +15 -0
  45. package/types/lib/evinser/evinser.d.ts.map +1 -1
  46. package/types/lib/helpers/bomUtils.d.ts +8 -0
  47. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  48. package/types/lib/helpers/cbomutils.d.ts +1 -0
  49. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  50. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  51. package/types/lib/helpers/dosai.d.ts +24 -0
  52. package/types/lib/helpers/dosai.d.ts.map +1 -0
  53. package/types/lib/helpers/dosaiParsers.d.ts +8 -0
  54. package/types/lib/helpers/dosaiParsers.d.ts.map +1 -0
  55. package/types/lib/helpers/hbomAnalysis.d.ts +14 -0
  56. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -1
  57. package/types/lib/helpers/hostTopology.d.ts.map +1 -1
  58. package/types/lib/helpers/plugins.d.ts.map +1 -1
  59. package/types/lib/helpers/protobom.d.ts +2 -0
  60. package/types/lib/helpers/protobom.d.ts.map +1 -1
  61. package/types/lib/helpers/protobomLoader.d.ts +17 -0
  62. package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
  63. package/types/lib/helpers/utils.d.ts.map +1 -1
  64. package/types/lib/server/server.d.ts.map +1 -1
  65. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  66. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  67. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  68. package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
  69. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  70. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  71. package/types/lib/validator/complianceRules.d.ts.map +1 -1
@@ -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: {
@@ -6,6 +6,10 @@ import Ajv2020 from "ajv/dist/2020.js";
6
6
  import addFormats from "ajv-formats";
7
7
  import { PackageURL } from "packageurl-js";
8
8
 
9
+ import {
10
+ isCycloneDxSpecVersionAtLeast,
11
+ toCycloneDxSpecVersionString,
12
+ } from "../helpers/bomUtils.js";
9
13
  import { thoughtLog } from "../helpers/logger.js";
10
14
  import { DEBUG_MODE, dirNameStr, isPartialTree } from "../helpers/utils.js";
11
15
  import {
@@ -14,6 +18,13 @@ import {
14
18
  } from "../stages/postgen/spdxConverter.js";
15
19
 
16
20
  const dirName = dirNameStr;
21
+ const SUPPORTED_CYCLONEDX_SCHEMA_VERSIONS = new Set([
22
+ "1.4",
23
+ "1.5",
24
+ "1.6",
25
+ "1.7",
26
+ "2.0",
27
+ ]);
17
28
  const PLACEHOLDER_COMPONENT_NAMES = new Set(["app", "application", "project"]);
18
29
  const SPDX_EXPORT_TYPES = new Set([
19
30
  "CreationInfo",
@@ -23,6 +34,74 @@ const SPDX_EXPORT_TYPES = new Set([
23
34
  "software_Package",
24
35
  ]);
25
36
  let spdxExportSchemaValidator;
37
+ const cycloneDxSchemaValidators = new Map();
38
+
39
+ const AJV_OPTIONS = {
40
+ strict: false,
41
+ logger: false,
42
+ verbose: true,
43
+ code: {
44
+ source: true,
45
+ lines: true,
46
+ optimize: true,
47
+ },
48
+ };
49
+
50
+ const readJsonSchema = (fileName) =>
51
+ JSON.parse(readFileSync(join(dirName, "data", fileName), "utf-8"));
52
+
53
+ const addDraft2020BundledSchema = (ajv, schema, schemaId) => {
54
+ const bundledSchema = { ...schema };
55
+ delete bundledSchema.$schema;
56
+ bundledSchema.$id = schemaId;
57
+ ajv.addSchema(bundledSchema);
58
+ };
59
+
60
+ const getCycloneDxSchemaValidator = (specVersion) => {
61
+ if (cycloneDxSchemaValidators.has(specVersion)) {
62
+ return cycloneDxSchemaValidators.get(specVersion);
63
+ }
64
+ let validate;
65
+ if (isCycloneDxSpecVersionAtLeast(specVersion, "2.0")) {
66
+ const ajv = new Ajv2020(AJV_OPTIONS);
67
+ addFormats(ajv);
68
+ addDraft2020BundledSchema(
69
+ ajv,
70
+ readJsonSchema("cryptography-defs.schema.json"),
71
+ "https://cyclonedx.org/schema/cryptography-defs.schema.json",
72
+ );
73
+ addDraft2020BundledSchema(
74
+ ajv,
75
+ readJsonSchema("jsf-0.82.schema.json"),
76
+ "https://cyclonedx.org/schema/jsf-0.82.schema.json",
77
+ );
78
+ addDraft2020BundledSchema(
79
+ ajv,
80
+ readJsonSchema("spdx.schema.json"),
81
+ "https://cyclonedx.org/schema/spdx.schema.json",
82
+ );
83
+ validate = ajv.compile(readJsonSchema("cyclonedx-2.0-bundled.schema.json"));
84
+ } else {
85
+ const schemas = [
86
+ readJsonSchema(`bom-${specVersion}.schema.json`),
87
+ readJsonSchema("jsf-0.82.schema.json"),
88
+ readJsonSchema("spdx.schema.json"),
89
+ ];
90
+ if (isCycloneDxSpecVersionAtLeast(specVersion, "1.7")) {
91
+ schemas.push(readJsonSchema("cryptography-defs.schema.json"));
92
+ }
93
+ const ajv = new Ajv({
94
+ ...AJV_OPTIONS,
95
+ schemas,
96
+ });
97
+ addFormats(ajv);
98
+ validate = ajv.getSchema(
99
+ `http://cyclonedx.org/schema/bom-${specVersion}.schema.json`,
100
+ );
101
+ }
102
+ cycloneDxSchemaValidators.set(specVersion, validate);
103
+ return validate;
104
+ };
26
105
 
27
106
  const getSpdxElementId = (element) => element?.spdxId || element?.["@id"];
28
107
 
@@ -36,6 +115,7 @@ const getSpdxExportSchemaValidator = () => {
36
115
  );
37
116
  const ajv = new Ajv2020({
38
117
  strict: false,
118
+ validateSchema: false,
39
119
  logger: false,
40
120
  verbose: true,
41
121
  code: {
@@ -63,44 +143,16 @@ export const validateBom = (bomJson) => {
63
143
  if (!bomJson) {
64
144
  return true;
65
145
  }
66
- const specVersion = bomJson.specVersion;
67
- const schema = JSON.parse(
68
- readFileSync(
69
- join(dirName, "data", `bom-${specVersion}.schema.json`),
70
- "utf-8",
71
- ),
72
- );
73
- const defsSchema = JSON.parse(
74
- readFileSync(join(dirName, "data", "jsf-0.82.schema.json"), "utf-8"),
75
- );
76
- const spdxSchema = JSON.parse(
77
- readFileSync(join(dirName, "data", "spdx.schema.json"), "utf-8"),
78
- );
79
- const cryptoDefSchema = JSON.parse(
80
- readFileSync(
81
- join(dirName, "data", "cryptography-defs.schema.json"),
82
- "utf-8",
83
- ),
84
- );
85
- const schemas = [schema, defsSchema, spdxSchema];
86
- if (specVersion >= 1.7) {
87
- schemas.push(cryptoDefSchema);
88
- }
89
- const ajv = new Ajv({
90
- schemas,
91
- strict: false,
92
- logger: false,
93
- verbose: true,
94
- code: {
95
- source: true,
96
- lines: true,
97
- optimize: true,
98
- },
99
- });
100
- addFormats(ajv);
101
- const validate = ajv.getSchema(
102
- `http://cyclonedx.org/schema/bom-${specVersion}.schema.json`,
103
- );
146
+ const specVersion = toCycloneDxSpecVersionString(bomJson.specVersion);
147
+ if (!SUPPORTED_CYCLONEDX_SCHEMA_VERSIONS.has(specVersion)) {
148
+ console.log(
149
+ `Unsupported CycloneDX specVersion '${bomJson.specVersion}'. Supported versions are ${[
150
+ ...SUPPORTED_CYCLONEDX_SCHEMA_VERSIONS,
151
+ ].join(", ")}.`,
152
+ );
153
+ return false;
154
+ }
155
+ const validate = getCycloneDxSchemaValidator(specVersion);
104
156
  const isValid = validate(bomJson);
105
157
  if (!isValid) {
106
158
  if (bomJson.metadata?.component?.name) {
@@ -1,7 +1,97 @@
1
+ import { readFileSync as actualReadFileSync } from "node:fs";
2
+
1
3
  import esmock from "esmock";
2
4
  import { assert, describe, it } from "poku";
3
5
  import sinon from "sinon";
4
6
 
7
+ import { validateBom } from "./bomValidator.js";
8
+
9
+ const validCycloneDx20Bom = {
10
+ specFormat: "CycloneDX",
11
+ specVersion: "2.0",
12
+ serialNumber: "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
13
+ version: 1,
14
+ metadata: {
15
+ component: {
16
+ "bom-ref": "pkg:generic/demo@1.0.0",
17
+ name: "demo",
18
+ purl: "pkg:generic/demo@1.0.0",
19
+ type: "application",
20
+ version: "1.0.0",
21
+ },
22
+ tools: {
23
+ components: [{ type: "application", name: "cdxgen", version: "12.4.0" }],
24
+ },
25
+ },
26
+ components: [
27
+ {
28
+ "bom-ref": "pkg:npm/lodash@4.17.21",
29
+ name: "lodash",
30
+ purl: "pkg:npm/lodash@4.17.21",
31
+ type: "library",
32
+ version: "4.17.21",
33
+ },
34
+ ],
35
+ dependencies: [
36
+ {
37
+ ref: "pkg:generic/demo@1.0.0",
38
+ dependsOn: ["pkg:npm/lodash@4.17.21"],
39
+ },
40
+ { ref: "pkg:npm/lodash@4.17.21", dependsOn: [] },
41
+ ],
42
+ };
43
+
44
+ describe("validateBom()", () => {
45
+ it("validates CycloneDX 2.0-dev JSON against the bundled schema", () => {
46
+ assert.strictEqual(validateBom(validCycloneDx20Bom), true);
47
+ });
48
+
49
+ it("returns a clear validation failure for unsupported spec versions", async () => {
50
+ const readFileSyncStub = sinon.stub();
51
+ const consoleLogStub = sinon.stub(console, "log");
52
+ try {
53
+ const { validateBom } = await esmock("./bomValidator.js", {
54
+ "node:fs": {
55
+ readFileSync: readFileSyncStub,
56
+ },
57
+ });
58
+
59
+ assert.strictEqual(
60
+ validateBom({ bomFormat: "CycloneDX", specVersion: "2.0.1" }),
61
+ false,
62
+ );
63
+ sinon.assert.notCalled(readFileSyncStub);
64
+ sinon.assert.calledWithMatch(
65
+ consoleLogStub,
66
+ "Unsupported CycloneDX specVersion '2.0.1'.",
67
+ );
68
+ } finally {
69
+ consoleLogStub.restore();
70
+ }
71
+ });
72
+
73
+ it("caches compiled CycloneDX schema validators by spec version", async () => {
74
+ const readFileSyncStub = sinon
75
+ .stub()
76
+ .callsFake((...args) => actualReadFileSync(...args));
77
+ const { validateBom } = await esmock("./bomValidator.js", {
78
+ "node:fs": {
79
+ readFileSync: readFileSyncStub,
80
+ },
81
+ });
82
+
83
+ assert.strictEqual(validateBom(validCycloneDx20Bom), true);
84
+ const readCountAfterFirstValidation = readFileSyncStub.callCount;
85
+ assert.ok(readCountAfterFirstValidation > 0);
86
+
87
+ assert.strictEqual(validateBom(validCycloneDx20Bom), true);
88
+ assert.strictEqual(
89
+ readFileSyncStub.callCount,
90
+ readCountAfterFirstValidation,
91
+ );
92
+ });
93
+ });
94
+
5
95
  describe("validateSpdx()", () => {
6
96
  it("lazy-loads the bundled SPDX export schema on first validation call", async () => {
7
97
  const readFileSyncStub = sinon