@cyclonedx/cdxgen 12.4.3 → 12.4.4

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 (38) hide show
  1. package/README.md +6 -0
  2. package/bin/audit.js +7 -0
  3. package/bin/cdxgen.js +48 -2
  4. package/bin/evinse.js +7 -0
  5. package/lib/audit/index.js +165 -2
  6. package/lib/audit/index.poku.js +462 -0
  7. package/lib/cli/index.js +317 -169
  8. package/lib/evinser/evinser.js +31 -9
  9. package/lib/helpers/analyzer.js +890 -0
  10. package/lib/helpers/analyzer.poku.js +341 -0
  11. package/lib/helpers/atomUtils.js +445 -0
  12. package/lib/helpers/atomUtils.poku.js +137 -0
  13. package/lib/helpers/bomUtils.js +71 -0
  14. package/lib/helpers/bomUtils.poku.js +45 -0
  15. package/lib/helpers/depsUtils.js +146 -0
  16. package/lib/helpers/depsUtils.poku.js +183 -0
  17. package/lib/helpers/utils.js +585 -191
  18. package/lib/helpers/utils.poku.js +357 -4
  19. package/lib/managers/binary.js +18 -9
  20. package/lib/stages/postgen/postgen.js +215 -0
  21. package/lib/stages/postgen/postgen.poku.js +218 -3
  22. package/lib/validator/bomValidator.js +11 -2
  23. package/package.json +8 -8
  24. package/types/lib/audit/index.d.ts.map +1 -1
  25. package/types/lib/cli/index.d.ts.map +1 -1
  26. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  27. package/types/lib/helpers/atomUtils.d.ts +18 -0
  28. package/types/lib/helpers/atomUtils.d.ts.map +1 -0
  29. package/types/lib/helpers/bomUtils.d.ts +10 -0
  30. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  31. package/types/lib/helpers/depsUtils.d.ts +9 -0
  32. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  33. package/types/lib/helpers/utils.d.ts +19 -0
  34. package/types/lib/helpers/utils.d.ts.map +1 -1
  35. package/types/lib/managers/binary.d.ts +2 -1
  36. package/types/lib/managers/binary.d.ts.map +1 -1
  37. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  38. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -5,10 +5,13 @@ import {
5
5
  getCycloneDxFormat,
6
6
  getCycloneDxRootFormatKey,
7
7
  getNonCycloneDxErrorMessage,
8
+ getSupportedCycloneDxComponentTypes,
8
9
  isCycloneDx20SpecVersion,
9
10
  isCycloneDxBom,
11
+ isCycloneDxComponentTypeEnabled,
10
12
  isCycloneDxSpecVersionAtLeast,
11
13
  isSpdxJsonLd,
14
+ normalizeCycloneDxComponentTypeFilter,
12
15
  normalizeCycloneDxSpecVersion,
13
16
  setCycloneDxFormat,
14
17
  toCycloneDxSpecVersionString,
@@ -81,6 +84,48 @@ describe("bomUtils", () => {
81
84
  assert.strictEqual(isCycloneDxSpecVersionAtLeast(undefined, 1.7), false);
82
85
  });
83
86
 
87
+ it("reports supported component types by CycloneDX spec version", () => {
88
+ assert.strictEqual(
89
+ getSupportedCycloneDxComponentTypes(1.5).includes("cryptographic-asset"),
90
+ false,
91
+ );
92
+ assert.strictEqual(
93
+ getSupportedCycloneDxComponentTypes(1.6).includes("cryptographic-asset"),
94
+ true,
95
+ );
96
+ assert.strictEqual(
97
+ getSupportedCycloneDxComponentTypes(1.7).includes("data"),
98
+ true,
99
+ );
100
+ });
101
+
102
+ it("normalizes and applies requested CycloneDX component type filters", () => {
103
+ assert.deepStrictEqual(
104
+ normalizeCycloneDxComponentTypeFilter(["library", "", "library"]),
105
+ ["library"],
106
+ );
107
+ assert.strictEqual(
108
+ isCycloneDxComponentTypeEnabled("cryptographic-asset", {
109
+ specVersion: 1.5,
110
+ }),
111
+ false,
112
+ );
113
+ assert.strictEqual(
114
+ isCycloneDxComponentTypeEnabled("cryptographic-asset", {
115
+ componentType: ["library"],
116
+ specVersion: 1.7,
117
+ }),
118
+ false,
119
+ );
120
+ assert.strictEqual(
121
+ isCycloneDxComponentTypeEnabled("library", {
122
+ componentType: ["library"],
123
+ specVersion: 1.5,
124
+ }),
125
+ true,
126
+ );
127
+ });
128
+
84
129
  it("selects and writes the correct CycloneDX root format key", () => {
85
130
  assert.strictEqual(getCycloneDxRootFormatKey("1.7"), "bomFormat");
86
131
  assert.strictEqual(getCycloneDxRootFormatKey("2.0"), "specFormat");
@@ -82,6 +82,102 @@ export function mergeDependencies(
82
82
  return retlist;
83
83
  }
84
84
 
85
+ const NPM_NON_RUNTIME_SCOPE_PROPERTIES = new Set([
86
+ "cdx:npm:package:development",
87
+ "cdx:npm:package:optional",
88
+ "cdx:npm:package:peer",
89
+ ]);
90
+
91
+ function hasNonRuntimeNpmScope(component) {
92
+ return (component?.properties || []).some(
93
+ (property) =>
94
+ NPM_NON_RUNTIME_SCOPE_PROPERTIES.has(property.name) &&
95
+ property.value === "true",
96
+ );
97
+ }
98
+
99
+ function normalizeBomRef(ref) {
100
+ const refString = String(ref || "");
101
+ try {
102
+ return decodeURIComponent(refString).toLowerCase();
103
+ } catch {
104
+ return refString.toLowerCase();
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Propagates required scope through a dependency graph.
110
+ *
111
+ * If component A has `scope: "required"` and dependency metadata says A depends
112
+ * on B, B is also runtime-relevant. Keep packages optional when lockfile/parser
113
+ * metadata explicitly identifies them as development, optional, or peer-only.
114
+ *
115
+ * @param {Object[]} components CycloneDX component objects
116
+ * @param {Object[]} dependencies CycloneDX dependency entries
117
+ * @returns {Object[]} The same component array with scopes updated in place
118
+ */
119
+ export function propagateRequiredScopeFromDependencies(
120
+ components = [],
121
+ dependencies = [],
122
+ ) {
123
+ if (!components?.length || !dependencies?.length) {
124
+ return components;
125
+ }
126
+ const componentByRef = new Map();
127
+ for (const component of components) {
128
+ for (const ref of [component?.["bom-ref"], component?.purl]) {
129
+ if (ref) {
130
+ componentByRef.set(normalizeBomRef(ref), component);
131
+ }
132
+ }
133
+ }
134
+ if (!componentByRef.size) {
135
+ return components;
136
+ }
137
+ const dependencyMap = new Map();
138
+ for (const dependency of dependencies || []) {
139
+ const ref = normalizeBomRef(dependency?.ref);
140
+ if (!ref) {
141
+ continue;
142
+ }
143
+ const dependsOn = (dependency.dependsOn || [])
144
+ .map((depRef) => normalizeBomRef(depRef))
145
+ .filter(Boolean);
146
+ dependencyMap.set(
147
+ ref,
148
+ Array.from(new Set([...(dependencyMap.get(ref) || []), ...dependsOn])),
149
+ );
150
+ }
151
+ const requiredStack = [];
152
+ const visited = new Set();
153
+ for (const component of components) {
154
+ if (component?.scope === "required") {
155
+ const ref = normalizeBomRef(component["bom-ref"] || component.purl);
156
+ if (ref) {
157
+ requiredStack.push(ref);
158
+ }
159
+ }
160
+ }
161
+ while (requiredStack.length) {
162
+ const requiredRef = requiredStack.pop();
163
+ if (visited.has(requiredRef)) {
164
+ continue;
165
+ }
166
+ visited.add(requiredRef);
167
+ for (const childRef of dependencyMap.get(requiredRef) || []) {
168
+ const childComponent = componentByRef.get(childRef);
169
+ if (!childComponent || hasNonRuntimeNpmScope(childComponent)) {
170
+ continue;
171
+ }
172
+ if (childComponent.scope !== "required") {
173
+ childComponent.scope = "required";
174
+ }
175
+ requiredStack.push(childRef);
176
+ }
177
+ }
178
+ return components;
179
+ }
180
+
85
181
  function serviceIdentityKey(service) {
86
182
  if (service?.["bom-ref"]) {
87
183
  return service["bom-ref"].toLowerCase();
@@ -330,3 +426,53 @@ export function trimComponents(components) {
330
426
  }
331
427
  return filteredComponents;
332
428
  }
429
+
430
+ /**
431
+ * Filter out invalid cryptographic-asset components from a component list.
432
+ * Removes algorithm components without a valid cryptoProperties.oid and
433
+ * certificate components without cryptoProperties.algorithmProperties.
434
+ *
435
+ * @param {Object[] | undefined | null} components Array of CycloneDX components
436
+ * @returns {Object[]} Filtered array with invalid crypto components removed
437
+ */
438
+ export function filterInvalidCryptoComponents(components) {
439
+ if (!components?.length) {
440
+ return [];
441
+ }
442
+ return components.filter((comp) => {
443
+ if (comp.type !== "cryptographic-asset") {
444
+ return true;
445
+ }
446
+ if (!comp.cryptoProperties) {
447
+ if (DEBUG_MODE) {
448
+ console.log(
449
+ `Removing cryptographic-asset '${comp.name}' without cryptoProperties`,
450
+ );
451
+ }
452
+ return false;
453
+ }
454
+ if (
455
+ comp.cryptoProperties.assetType === "algorithm" &&
456
+ !comp.cryptoProperties.oid
457
+ ) {
458
+ if (DEBUG_MODE) {
459
+ console.log(
460
+ `Removing cryptographic-asset algorithm '${comp.name}' without OID`,
461
+ );
462
+ }
463
+ return false;
464
+ }
465
+ if (
466
+ comp.cryptoProperties.assetType === "certificate" &&
467
+ !comp.cryptoProperties.algorithmProperties
468
+ ) {
469
+ if (DEBUG_MODE) {
470
+ console.log(
471
+ `Removing cryptographic-asset certificate '${comp.name}' without algorithmProperties`,
472
+ );
473
+ }
474
+ return false;
475
+ }
476
+ return true;
477
+ });
478
+ }
@@ -1,8 +1,10 @@
1
1
  import { assert, describe, it } from "poku";
2
2
 
3
3
  import {
4
+ filterInvalidCryptoComponents,
4
5
  mergeDependencies,
5
6
  mergeServices,
7
+ propagateRequiredScopeFromDependencies,
6
8
  trimComponents,
7
9
  } from "./depsUtils.js";
8
10
 
@@ -153,6 +155,105 @@ describe("mergeDependencies()", () => {
153
155
  });
154
156
  });
155
157
 
158
+ describe("propagateRequiredScopeFromDependencies()", () => {
159
+ it("marks transitive dependencies of required components as required", () => {
160
+ const components = [
161
+ {
162
+ name: "app-lib",
163
+ "bom-ref": "pkg:npm/app-lib@1.0.0",
164
+ scope: "required",
165
+ },
166
+ {
167
+ name: "runtime-a",
168
+ "bom-ref": "pkg:npm/runtime-a@1.0.0",
169
+ scope: "optional",
170
+ },
171
+ {
172
+ name: "runtime-b",
173
+ "bom-ref": "pkg:npm/runtime-b@1.0.0",
174
+ scope: "optional",
175
+ },
176
+ ];
177
+ const dependencies = [
178
+ { ref: "pkg:npm/app-lib@1.0.0", dependsOn: ["pkg:npm/runtime-a@1.0.0"] },
179
+ {
180
+ ref: "pkg:npm/runtime-a@1.0.0",
181
+ dependsOn: ["pkg:npm/runtime-b@1.0.0"],
182
+ },
183
+ ];
184
+
185
+ propagateRequiredScopeFromDependencies(components, dependencies);
186
+
187
+ assert.strictEqual(components[1].scope, "required");
188
+ assert.strictEqual(components[2].scope, "required");
189
+ });
190
+
191
+ it("preserves known development, optional, and peer-only packages", () => {
192
+ const components = [
193
+ {
194
+ name: "app-lib",
195
+ "bom-ref": "pkg:npm/app-lib@1.0.0",
196
+ scope: "required",
197
+ },
198
+ {
199
+ name: "dev-tool",
200
+ "bom-ref": "pkg:npm/dev-tool@1.0.0",
201
+ scope: "optional",
202
+ properties: [{ name: "cdx:npm:package:development", value: "true" }],
203
+ },
204
+ {
205
+ name: "optional-runtime",
206
+ "bom-ref": "pkg:npm/optional-runtime@1.0.0",
207
+ scope: "optional",
208
+ properties: [{ name: "cdx:npm:package:optional", value: "true" }],
209
+ },
210
+ {
211
+ name: "peer-runtime",
212
+ "bom-ref": "pkg:npm/peer-runtime@1.0.0",
213
+ scope: "optional",
214
+ properties: [{ name: "cdx:npm:package:peer", value: "true" }],
215
+ },
216
+ ];
217
+ const dependencies = [
218
+ {
219
+ ref: "pkg:npm/app-lib@1.0.0",
220
+ dependsOn: [
221
+ "pkg:npm/dev-tool@1.0.0",
222
+ "pkg:npm/optional-runtime@1.0.0",
223
+ "pkg:npm/peer-runtime@1.0.0",
224
+ ],
225
+ },
226
+ ];
227
+
228
+ propagateRequiredScopeFromDependencies(components, dependencies);
229
+
230
+ assert.strictEqual(components[1].scope, "optional");
231
+ assert.strictEqual(components[2].scope, "optional");
232
+ assert.strictEqual(components[3].scope, "optional");
233
+ });
234
+
235
+ it("handles decoded and encoded bom-ref variants", () => {
236
+ const components = [
237
+ {
238
+ name: "scoped",
239
+ "bom-ref": "pkg:npm/@scope/scoped@1.0.0",
240
+ scope: "required",
241
+ },
242
+ { name: "child", purl: "pkg:npm/@scope/child@1.0.0", scope: "optional" },
243
+ ];
244
+ const dependencies = [
245
+ {
246
+ ref: "pkg:npm/%40scope/scoped@1.0.0",
247
+ dependsOn: ["pkg:npm/%40scope/child@1.0.0"],
248
+ },
249
+ ];
250
+
251
+ propagateRequiredScopeFromDependencies(components, dependencies);
252
+
253
+ assert.strictEqual(components[1].scope, "required");
254
+ });
255
+ });
256
+
156
257
  describe("trimComponents()", () => {
157
258
  it("retains hashes from duplicate components", () => {
158
259
  const components = [
@@ -331,3 +432,85 @@ describe("mergeServices()", () => {
331
432
  assert.deepStrictEqual(result[0].endpoints, ["/mcp", "/health"]);
332
433
  });
333
434
  });
435
+
436
+ describe("filterInvalidCryptoComponents()", () => {
437
+ it("removes algorithm components without OID", () => {
438
+ const components = [
439
+ {
440
+ name: "sha-256",
441
+ type: "cryptographic-asset",
442
+ cryptoProperties: {
443
+ assetType: "algorithm",
444
+ oid: "2.16.840.1.101.3.4.2.1",
445
+ },
446
+ },
447
+ {
448
+ name: "unknown-algo",
449
+ type: "cryptographic-asset",
450
+ cryptoProperties: { assetType: "algorithm" },
451
+ },
452
+ { name: "express", type: "library", purl: "pkg:npm/express@4.18.0" },
453
+ ];
454
+ const result = filterInvalidCryptoComponents(components);
455
+ assert.strictEqual(result.length, 2);
456
+ assert.strictEqual(result[0].name, "sha-256");
457
+ assert.strictEqual(result[1].name, "express");
458
+ });
459
+
460
+ it("removes crypto components without cryptoProperties", () => {
461
+ const components = [
462
+ { name: "bad-cert", type: "cryptographic-asset" },
463
+ {
464
+ name: "good-cert",
465
+ type: "cryptographic-asset",
466
+ cryptoProperties: {
467
+ assetType: "certificate",
468
+ algorithmProperties: {
469
+ executionEnvironment: "unknown",
470
+ implementationPlatform: "unknown",
471
+ },
472
+ },
473
+ },
474
+ ];
475
+ const result = filterInvalidCryptoComponents(components);
476
+ assert.strictEqual(result.length, 1);
477
+ assert.strictEqual(result[0].name, "good-cert");
478
+ });
479
+
480
+ it("removes certificate components without algorithmProperties", () => {
481
+ const components = [
482
+ {
483
+ name: "bad-cert",
484
+ type: "cryptographic-asset",
485
+ cryptoProperties: { assetType: "certificate" },
486
+ },
487
+ ];
488
+ const result = filterInvalidCryptoComponents(components);
489
+ assert.strictEqual(result.length, 0);
490
+ });
491
+
492
+ it("preserves related-crypto-material components", () => {
493
+ const components = [
494
+ {
495
+ name: "gpg-key",
496
+ type: "cryptographic-asset",
497
+ cryptoProperties: {
498
+ assetType: "related-crypto-material",
499
+ relatedCryptoMaterialProperties: {
500
+ type: "public-key",
501
+ id: "abc",
502
+ state: "active",
503
+ },
504
+ },
505
+ },
506
+ ];
507
+ const result = filterInvalidCryptoComponents(components);
508
+ assert.strictEqual(result.length, 1);
509
+ });
510
+
511
+ it("returns an empty array for empty/falsy input", () => {
512
+ assert.strictEqual(filterInvalidCryptoComponents([]).length, 0);
513
+ assert.deepStrictEqual(filterInvalidCryptoComponents(undefined), []);
514
+ assert.deepStrictEqual(filterInvalidCryptoComponents(null), []);
515
+ });
516
+ });