@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.
- package/README.md +6 -0
- package/bin/audit.js +7 -0
- package/bin/cdxgen.js +48 -2
- package/bin/evinse.js +7 -0
- package/lib/audit/index.js +165 -2
- package/lib/audit/index.poku.js +462 -0
- package/lib/cli/index.js +317 -169
- package/lib/evinser/evinser.js +31 -9
- package/lib/helpers/analyzer.js +890 -0
- package/lib/helpers/analyzer.poku.js +341 -0
- package/lib/helpers/atomUtils.js +445 -0
- package/lib/helpers/atomUtils.poku.js +137 -0
- package/lib/helpers/bomUtils.js +71 -0
- package/lib/helpers/bomUtils.poku.js +45 -0
- package/lib/helpers/depsUtils.js +146 -0
- package/lib/helpers/depsUtils.poku.js +183 -0
- package/lib/helpers/utils.js +585 -191
- package/lib/helpers/utils.poku.js +357 -4
- package/lib/managers/binary.js +18 -9
- package/lib/stages/postgen/postgen.js +215 -0
- package/lib/stages/postgen/postgen.poku.js +218 -3
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +8 -8
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/atomUtils.d.ts +18 -0
- package/types/lib/helpers/atomUtils.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +10 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -1
- package/types/lib/helpers/depsUtils.d.ts +9 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +19 -0
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +2 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- 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");
|
package/lib/helpers/depsUtils.js
CHANGED
|
@@ -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
|
+
});
|