@cyclonedx/cdxgen 12.4.2 → 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 (43) 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 +320 -172
  8. package/lib/cli/index.poku.js +81 -0
  9. package/lib/evinser/evinser.js +31 -9
  10. package/lib/helpers/analyzer.js +890 -0
  11. package/lib/helpers/analyzer.poku.js +341 -0
  12. package/lib/helpers/atomUtils.js +445 -0
  13. package/lib/helpers/atomUtils.poku.js +137 -0
  14. package/lib/helpers/bomUtils.js +71 -0
  15. package/lib/helpers/bomUtils.poku.js +45 -0
  16. package/lib/helpers/depsUtils.js +146 -0
  17. package/lib/helpers/depsUtils.poku.js +183 -0
  18. package/lib/helpers/display.js +12 -6
  19. package/lib/helpers/display.poku.js +38 -0
  20. package/lib/helpers/utils.js +653 -191
  21. package/lib/helpers/utils.poku.js +414 -4
  22. package/lib/managers/binary.js +18 -9
  23. package/lib/stages/postgen/postgen.js +215 -0
  24. package/lib/stages/postgen/postgen.poku.js +218 -3
  25. package/lib/validator/bomValidator.js +11 -2
  26. package/package.json +8 -8
  27. package/types/lib/audit/index.d.ts.map +1 -1
  28. package/types/lib/cli/index.d.ts.map +1 -1
  29. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  30. package/types/lib/helpers/atomUtils.d.ts +18 -0
  31. package/types/lib/helpers/atomUtils.d.ts.map +1 -0
  32. package/types/lib/helpers/bomUtils.d.ts +10 -0
  33. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  34. package/types/lib/helpers/depsUtils.d.ts +9 -0
  35. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  36. package/types/lib/helpers/display.d.ts.map +1 -1
  37. package/types/lib/helpers/dosaiParsers.d.ts.map +1 -1
  38. package/types/lib/helpers/utils.d.ts +19 -0
  39. package/types/lib/helpers/utils.d.ts.map +1 -1
  40. package/types/lib/managers/binary.d.ts +2 -1
  41. package/types/lib/managers/binary.d.ts.map +1 -1
  42. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  43. 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
+ });
@@ -42,6 +42,7 @@ const MULTIVALUE_ACTIVITY_TARGET_KEYS = new Set([
42
42
  "SrcFiles",
43
43
  ]);
44
44
  const PATH_SEPARATOR_REGEX = /[\\/]+/;
45
+ const SUSPICIOUS_SHELL_PATH_LABEL = "⚠ shell-metacharacters";
45
46
  const ENV_AUDIT_SEVERITY_RANK = {
46
47
  low: 1,
47
48
  medium: 2,
@@ -1220,16 +1221,21 @@ export function printActivitySummary(reportType = undefined) {
1220
1221
  }
1221
1222
  return;
1222
1223
  }
1223
- const formatActivityTarget = (target) => {
1224
+ const formatActivityTarget = (activity) => {
1225
+ const target = activity?.target;
1226
+ const suspiciousPrefix =
1227
+ activity?.risk === "shell-metacharacters"
1228
+ ? `${SUSPICIOUS_SHELL_PATH_LABEL}\n`
1229
+ : "";
1224
1230
  if (typeof target !== "string" || !target.includes(",")) {
1225
- return target || "";
1231
+ return `${suspiciousPrefix}${target || ""}`;
1226
1232
  }
1227
1233
  const targetEntries = splitCommaSeparatedActivityEntries(target);
1228
1234
  if (isLikelyActivityPathList(targetEntries)) {
1229
- return sortActivityTargetEntries(targetEntries).join("\n");
1235
+ return `${suspiciousPrefix}${sortActivityTargetEntries(targetEntries).join("\n")}`;
1230
1236
  }
1231
1237
  if (!(target.includes(":") || target.includes("="))) {
1232
- return target || "";
1238
+ return `${suspiciousPrefix}${target || ""}`;
1233
1239
  }
1234
1240
  const targetSegments = target.split(/,\s*(?=[A-Za-z][\w-]*\s*[:=])/);
1235
1241
  let didFormat = false;
@@ -1249,7 +1255,7 @@ export function printActivitySummary(reportType = undefined) {
1249
1255
  .map((entry) => `- ${entry}`)
1250
1256
  .join("\n")}`;
1251
1257
  });
1252
- return didFormat ? renderedSegments.join("\n") : target;
1258
+ return `${suspiciousPrefix}${didFormat ? renderedSegments.join("\n") : target}`;
1253
1259
  };
1254
1260
  const formatActivityType = (type) => {
1255
1261
  if (typeof type !== "string" || !type.includes(",")) {
@@ -1275,7 +1281,7 @@ export function printActivitySummary(reportType = undefined) {
1275
1281
  formatActivityType(activity.projectType),
1276
1282
  activity.packageType || "",
1277
1283
  activity.kind || "",
1278
- formatActivityTarget(activity.target),
1284
+ formatActivityTarget(activity),
1279
1285
  activity.reason
1280
1286
  ? `${formatStatus(activity.status)}\n${activity.reason}`.trim()
1281
1287
  : formatStatus(activity.status),
@@ -481,6 +481,44 @@ it("renders plain comma-separated activity paths one per line sorted by depth",
481
481
  }
482
482
  });
483
483
 
484
+ it("highlights suspicious shell-metacharacter paths in the activity summary", async () => {
485
+ const tableStub = sinon.stub().returns("activity-table");
486
+ const shellIfs = "$" + "{IFS}";
487
+ try {
488
+ const { printActivitySummary: printActivitySummaryMocked } = await esmock(
489
+ "./display.js",
490
+ {
491
+ "./table.js": {
492
+ createStream: sinon.stub(),
493
+ table: tableStub,
494
+ },
495
+ "./utils.js": {
496
+ getRecordedActivities: sinon.stub().returns([
497
+ {
498
+ identifier: "ACT-0005",
499
+ kind: "inspect",
500
+ reason: "Suspicious path contains shell metacharacters.",
501
+ risk: "shell-metacharacters",
502
+ status: "completed",
503
+ target: `/tmp/repo/evil;cd${shellIfs}..;printf${shellIfs}marker>CDXGEN_GITURL_E2E_MARKER;#/pom.xml`,
504
+ },
505
+ ]),
506
+ isDryRun: true,
507
+ isSecureMode: false,
508
+ safeExistsSync: sinon.stub(),
509
+ toCamel: sinon.stub(),
510
+ },
511
+ },
512
+ );
513
+ printActivitySummaryMocked();
514
+ const [data] = tableStub.firstCall.args;
515
+ assert.ok(data[1][4].startsWith("⚠ shell-metacharacters\n"));
516
+ assert.ok(data[1][4].includes(`evil;cd${shellIfs}..`));
517
+ } finally {
518
+ sinon.restore();
519
+ }
520
+ });
521
+
484
522
  it("prints grouped environment audit findings in a secure-mode panel", async () => {
485
523
  const tableStub = sinon.stub().returns("env-audit-table");
486
524
  try {