@cyclonedx/cdxgen 12.2.0 → 12.2.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 (34) hide show
  1. package/README.md +5 -2
  2. package/bin/cdxgen.js +19 -1
  3. package/lib/cli/index.js +122 -57
  4. package/lib/cli/index.poku.js +117 -0
  5. package/lib/helpers/analyzer.js +606 -3
  6. package/lib/helpers/analyzer.poku.js +230 -0
  7. package/lib/helpers/depsUtils.js +16 -0
  8. package/lib/helpers/depsUtils.poku.js +58 -1
  9. package/lib/helpers/display.js +4 -2
  10. package/lib/helpers/remote/dependency-track.js +84 -0
  11. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  12. package/lib/helpers/table.js +384 -0
  13. package/lib/helpers/table.poku.js +186 -0
  14. package/lib/helpers/utils.js +184 -10
  15. package/lib/helpers/utils.poku.js +118 -11
  16. package/lib/server/openapi.yaml +33 -0
  17. package/lib/server/server.js +10 -2
  18. package/lib/server/server.poku.js +209 -0
  19. package/lib/stages/postgen/auditBom.js +1 -2
  20. package/lib/validator/reporters/console.js +2 -2
  21. package/package.json +1 -2
  22. package/types/lib/cli/index.d.ts.map +1 -1
  23. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  24. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  25. package/types/lib/helpers/display.d.ts.map +1 -1
  26. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  27. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  28. package/types/lib/helpers/table.d.ts +6 -0
  29. package/types/lib/helpers/table.d.ts.map +1 -0
  30. package/types/lib/helpers/utils.d.ts +1 -0
  31. package/types/lib/helpers/utils.d.ts.map +1 -1
  32. package/types/lib/server/server.d.ts +1 -0
  33. package/types/lib/server/server.d.ts.map +1 -1
  34. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
package/README.md CHANGED
@@ -107,7 +107,7 @@ docker run --rm -e CDXGEN_DEBUG_MODE=debug -v /tmp:/tmp -v $(pwd):/app:rw -t ghc
107
107
  In deno applications, cdxgen could be directly imported without any conversion. Please see the section on [integration as a library](#integration-as-library)
108
108
 
109
109
  ```ts
110
- import { createBom, submitBom } from "npm:@cyclonedx/cdxgen@^12.2.0";
110
+ import { createBom, submitBom } from "npm:@cyclonedx/cdxgen@^12.2.1";
111
111
  ```
112
112
 
113
113
  ## Getting Help
@@ -139,7 +139,10 @@ Options:
139
139
  --project-tag Dependency track project tag. Multiple values allowed. [array]
140
140
  --project-id Dependency track project id. Either provide the id or the project name and version tog
141
141
  ether [string]
142
- --parent-project-id Dependency track parent project id [string]
142
+ --parent-project-id Dependency track parent project id. You must provide the id or both
143
+ parent project name and parent project version. [string]
144
+ --parent-project-name Dependency track parent project name [string]
145
+ --parent-project-version Dependency track parent project version [string]
143
146
  --required-only Include only the packages with required scope on the SBOM. Would set compositions.aggr
144
147
  egate to incomplete unless --no-auto-compositions is passed. [boolean]
145
148
  --fail-on-error Fail if any dependency extractor fails. [boolean]
package/bin/cdxgen.js CHANGED
@@ -171,6 +171,24 @@ const args = _yargs
171
171
  description: "Dependency track parent project id",
172
172
  type: "string",
173
173
  })
174
+ .option("parent-project-name", {
175
+ description: "Dependency track parent project name",
176
+ type: "string",
177
+ })
178
+ .option("parent-project-version", {
179
+ description: "Dependency track parent project version",
180
+ type: "string",
181
+ })
182
+ .option("auto-create", {
183
+ description: "Dependency track autoCreate value for BOM uploads",
184
+ type: "boolean",
185
+ hidden: true,
186
+ })
187
+ .option("is-latest", {
188
+ description: "Dependency track isLatest value for BOM uploads",
189
+ type: "boolean",
190
+ hidden: true,
191
+ })
174
192
  .option("required-only", {
175
193
  type: "boolean",
176
194
  description:
@@ -249,7 +267,7 @@ const args = _yargs
249
267
  hidden: true,
250
268
  })
251
269
  .option("spec-version", {
252
- description: "CycloneDX Specification version to use. Defaults to 1.6",
270
+ description: "CycloneDX Specification version to use. Defaults to 1.7",
253
271
  default: 1.7,
254
272
  type: "number",
255
273
  choices: [1.4, 1.5, 1.6, 1.7],
package/lib/cli/index.js CHANGED
@@ -19,7 +19,6 @@ import got from "got";
19
19
  import { PackageURL } from "packageurl-js";
20
20
  import { gte, lte } from "semver";
21
21
  import { parse } from "ssri";
22
- import { table } from "table";
23
22
  import { v4 as uuidv4 } from "uuid";
24
23
  import { parse as loadYaml } from "yaml";
25
24
 
@@ -28,6 +27,11 @@ import { parseCaxaMetadata } from "../helpers/caxa.js";
28
27
  import { mergeDependencies, trimComponents } from "../helpers/depsUtils.js";
29
28
  import { GIT_COMMAND } from "../helpers/envcontext.js";
30
29
  import { thoughtLog } from "../helpers/logger.js";
30
+ import {
31
+ buildDependencyTrackBomPayload,
32
+ getDependencyTrackBomUrl,
33
+ } from "../helpers/remote/dependency-track.js";
34
+ import { table } from "../helpers/table.js";
31
35
  import {
32
36
  addEvidenceForDotnet,
33
37
  addEvidenceForImports,
@@ -1252,7 +1256,7 @@ const buildBomNSData = (options, pkgInfo, ptype, context) => {
1252
1256
  // CycloneDX Json Template
1253
1257
  const jsonTpl = {
1254
1258
  bomFormat: "CycloneDX",
1255
- specVersion: `${options.specVersion || "1.5"}`,
1259
+ specVersion: `${options.specVersion || "1.7"}`,
1256
1260
  serialNumber: serialNum,
1257
1261
  version: 1,
1258
1262
  metadata: metadata,
@@ -1490,6 +1494,8 @@ export async function createJavaBom(path, options) {
1490
1494
  }
1491
1495
  let result;
1492
1496
  let mvnArgs;
1497
+ // FIXME: How do we motivate everyone to upgrade to 1.7?
1498
+ const toolsSpecVersion = 1.6;
1493
1499
  if (isQuarkus) {
1494
1500
  thoughtLog(
1495
1501
  "This appears to be a Quarkus project. Let's use the right Maven plugin.",
@@ -1500,12 +1506,14 @@ export async function createJavaBom(path, options) {
1500
1506
  "quarkus:dependency-sbom",
1501
1507
  "-Dquarkus.analytics.disabled=true",
1502
1508
  ];
1503
- if (options.specVersion) {
1509
+ if (options.specVersion >= 1.6) {
1504
1510
  mvnArgs = mvnArgs.concat(
1505
- `-Dquarkus.dependency.sbom.schema-version=${options.specVersion}`,
1511
+ `-Dquarkus.dependency.sbom.schema-version=${toolsSpecVersion}`,
1506
1512
  );
1507
1513
  }
1508
1514
  } else {
1515
+ // FIXME: The last maven plugin release was on November 28th, 2024.
1516
+ // Should we fork this repo and maintain it ourselves?
1509
1517
  const cdxMavenPlugin =
1510
1518
  process.env.CDX_MAVEN_PLUGIN ||
1511
1519
  "org.cyclonedx:cyclonedx-maven-plugin:2.9.1";
@@ -1527,9 +1535,11 @@ export async function createJavaBom(path, options) {
1527
1535
  const addArgs = process.env.MVN_ARGS.split(" ");
1528
1536
  mvnArgs = mvnArgs.concat(addArgs);
1529
1537
  }
1530
- // specVersion 1.4 doesn't support externalReferences.type=disribution-intake
1538
+ // specVersion 1.4 doesn't support externalReferences.type=distribution-intake
1531
1539
  // so we need to run the plugin with the correct version
1532
- if (options.specVersion) {
1540
+ if (options.specVersion >= 1.6) {
1541
+ mvnArgs = mvnArgs.concat(`-DschemaVersion=${toolsSpecVersion}`);
1542
+ } else if (options.specVersion > 1.4) {
1533
1543
  mvnArgs = mvnArgs.concat(`-DschemaVersion=${options.specVersion}`);
1534
1544
  }
1535
1545
  }
@@ -2723,6 +2733,21 @@ export async function createNodejsBom(path, options) {
2723
2733
  `${options.multiProject ? "**/" : ""}bower.json`,
2724
2734
  options,
2725
2735
  );
2736
+ if (DEBUG_MODE) {
2737
+ const wasmFiles = getAllFiles(
2738
+ path,
2739
+ `${options.multiProject ? "**/" : ""}*.wasm`,
2740
+ {
2741
+ ...options,
2742
+ includeNodeModulesDir: true,
2743
+ },
2744
+ );
2745
+ if (wasmFiles?.length) {
2746
+ console.log(
2747
+ `Found ${wasmFiles.length} wasm files in this project. cdxgen will make a best attempt to identify the exports and imports from these files.`,
2748
+ );
2749
+ }
2750
+ }
2726
2751
  // Parse min js files
2727
2752
  if (minJsFiles?.length) {
2728
2753
  manifestFiles = manifestFiles.concat(minJsFiles);
@@ -3242,6 +3267,7 @@ export async function createNodejsBom(path, options) {
3242
3267
  const pnpmLock = join(path, "common", "config", "rush", "pnpm-lock.yaml");
3243
3268
  if (safeExistsSync(swFile)) {
3244
3269
  let pkgList = await parseNodeShrinkwrap(swFile);
3270
+ pkgList = addWasmComponentsFromImports(pkgList, allImports);
3245
3271
  if (allImports && Object.keys(allImports).length) {
3246
3272
  pkgList = await addEvidenceForImports(
3247
3273
  pkgList,
@@ -3258,9 +3284,13 @@ export async function createNodejsBom(path, options) {
3258
3284
  }
3259
3285
  if (safeExistsSync(pnpmLock)) {
3260
3286
  const pnpmLockObj = await parsePnpmLock(pnpmLock);
3287
+ let pkgList = addWasmComponentsFromImports(
3288
+ pnpmLockObj.pkgList,
3289
+ allImports,
3290
+ );
3261
3291
  if (allImports && Object.keys(allImports).length) {
3262
3292
  pkgList = await addEvidenceForImports(
3263
- pnpmLockObj.pkgList,
3293
+ pkgList,
3264
3294
  allImports,
3265
3295
  allExports,
3266
3296
  options.deep,
@@ -3537,6 +3567,7 @@ export async function createNodejsBom(path, options) {
3537
3567
  options.parentComponent = parentComponent;
3538
3568
  }
3539
3569
  if (allImports && Object.keys(allImports).length) {
3570
+ pkgList = addWasmComponentsFromImports(pkgList, allImports);
3540
3571
  pkgList = await addEvidenceForImports(
3541
3572
  pkgList,
3542
3573
  allImports,
@@ -3552,6 +3583,84 @@ export async function createNodejsBom(path, options) {
3552
3583
  });
3553
3584
  }
3554
3585
 
3586
+ const WASM_IMPORT_PATTERN = /\.wasm([?#].*)?$/i;
3587
+
3588
+ /**
3589
+ * Adds generic wasm components from discovered source imports.
3590
+ *
3591
+ * @param {Array<Object>} pkgList Node.js package list
3592
+ * @param {Object} allImports analyzer imports map
3593
+ * @returns {Array<Object>} pkgList enriched with wasm components
3594
+ */
3595
+ const addWasmComponentsFromImports = (pkgList, allImports) => {
3596
+ if (!allImports || !Object.keys(allImports).length) {
3597
+ return pkgList;
3598
+ }
3599
+ const existingPurls = new Set();
3600
+ for (const pkg of pkgList) {
3601
+ if (pkg?.purl) {
3602
+ existingPurls.add(pkg.purl);
3603
+ }
3604
+ }
3605
+ for (const [importPath, occurrences] of Object.entries(allImports)) {
3606
+ if (!WASM_IMPORT_PATTERN.test(importPath)) {
3607
+ continue;
3608
+ }
3609
+ const cleanImportPath = importPath.replace(/[?#].*$/, "");
3610
+ const normalizedImportPath = cleanImportPath
3611
+ .replace(/\\/g, "/")
3612
+ .replace(/^\.\//, "");
3613
+ const wasmComponentName = normalizedImportPath || cleanImportPath;
3614
+ const wasmFileName = basename(cleanImportPath);
3615
+ if (!allImports[wasmComponentName]) {
3616
+ allImports[wasmComponentName] = new Set();
3617
+ }
3618
+ for (const occurrence of occurrences) {
3619
+ allImports[wasmComponentName].add(occurrence);
3620
+ }
3621
+ const wasmPurl = new PackageURL(
3622
+ "generic",
3623
+ "",
3624
+ wasmFileName,
3625
+ "",
3626
+ normalizedImportPath ? { path: normalizedImportPath } : undefined,
3627
+ undefined,
3628
+ ).toString();
3629
+ if (existingPurls.has(wasmPurl)) {
3630
+ continue;
3631
+ }
3632
+ const firstOccurrence = Array.from(occurrences)[0];
3633
+ const srcFile = firstOccurrence?.importedAs || importPath;
3634
+ pkgList.push({
3635
+ name: wasmComponentName,
3636
+ type: "library",
3637
+ purl: wasmPurl,
3638
+ "bom-ref": wasmPurl,
3639
+ properties: [
3640
+ {
3641
+ name: "SrcFile",
3642
+ value: srcFile,
3643
+ },
3644
+ ],
3645
+ evidence: {
3646
+ identity: {
3647
+ field: "purl",
3648
+ confidence: 0.3,
3649
+ methods: [
3650
+ {
3651
+ technique: "filename",
3652
+ confidence: 0.3,
3653
+ value: srcFile,
3654
+ },
3655
+ ],
3656
+ },
3657
+ },
3658
+ });
3659
+ existingPurls.add(wasmPurl);
3660
+ }
3661
+ return pkgList;
3662
+ };
3663
+
3555
3664
  /**
3556
3665
  * Function to create bom string for Projects that use Pixi package manager.
3557
3666
  * createPixiBom is based on createPythonBom.
@@ -7350,7 +7459,7 @@ export function dedupeBom(options, components, parentComponent, dependencies) {
7350
7459
  components,
7351
7460
  bomJson: {
7352
7461
  bomFormat: "CycloneDX",
7353
- specVersion: `${options.specVersion || 1.5}`,
7462
+ specVersion: `${options.specVersion || 1.7}`,
7354
7463
  serialNumber: serialNum,
7355
7464
  version: 1,
7356
7465
  metadata: addMetadata(parentComponent, options, {}),
@@ -8856,66 +8965,22 @@ export async function createBom(path, options) {
8856
8965
  * @throws {Error} if the request fails
8857
8966
  */
8858
8967
  export async function submitBom(args, bomContents) {
8859
- const serverUrl = `${args.serverUrl.replace(/\/$/, "")}/api/v1/bom`;
8860
- let encodedBomContents = Buffer.from(JSON.stringify(bomContents)).toString(
8861
- "base64",
8862
- );
8863
- if (encodedBomContents.startsWith("77u/")) {
8864
- encodedBomContents = encodedBomContents.substring(4);
8865
- }
8866
- const bomPayload = {
8867
- autoCreate: "true",
8868
- bom: encodedBomContents,
8869
- };
8870
- const projectVersion = args.projectVersion || "main";
8871
- if (
8872
- typeof args.projectId !== "undefined" ||
8873
- (typeof args.projectName !== "undefined" &&
8874
- typeof projectVersion !== "undefined")
8875
- ) {
8876
- if (typeof args.projectId !== "undefined") {
8877
- bomPayload.project = args.projectId;
8878
- }
8879
- if (typeof args.projectName !== "undefined") {
8880
- bomPayload.projectName = args.projectName;
8881
- }
8882
- if (typeof projectVersion !== "undefined") {
8883
- bomPayload.projectVersion = projectVersion;
8884
- }
8885
- } else {
8968
+ const serverUrl = getDependencyTrackBomUrl(args.serverUrl);
8969
+ const bomPayload = buildDependencyTrackBomPayload(args, bomContents);
8970
+ if (!bomPayload) {
8886
8971
  console.log(
8887
- "projectId, projectName and projectVersion, or all three must be provided.",
8972
+ "Invalid Dependency-Track submission arguments. Provide projectId or projectName (projectVersion defaults to main) and specify parent project either by UUID or by parent project name + version.",
8888
8973
  );
8889
8974
  args.failOnError && process.exit(1);
8890
8975
  return;
8891
8976
  }
8892
- if (
8893
- typeof args.parentProjectId !== "undefined" ||
8894
- typeof args.parentUUID !== "undefined"
8895
- ) {
8896
- bomPayload.parentUUID = args.parentProjectId || args.parentUUID;
8897
- }
8898
- // Add project tags if provided
8899
- // see https://docs.dependencytrack.org/2024/10/01/v4.12.0/
8900
- // corresponding API usage documentation can be found on the
8901
- // API docs site of your instance, see
8902
- // https://docs.dependencytrack.org/integrations/rest-api/
8903
- // or public instance see https://yoursky.blue/documentation/rest-api
8904
- if (typeof args.projectTag !== "undefined") {
8905
- // If args.projectTag is not an array, convert it to an array
8906
- // Attention, array items should be of form { name: "tagName " }
8907
- // see https://yoursky.blue/documentation/rest-api#tag/bom/operation/UploadBomBase64Encoded
8908
- bomPayload.projectTags = (
8909
- Array.isArray(args.projectTag) ? args.projectTag : [args.projectTag]
8910
- ).map((tag) => ({ name: tag }));
8911
- }
8912
8977
  if (DEBUG_MODE) {
8913
8978
  console.log(
8914
8979
  "Submitting BOM to",
8915
8980
  serverUrl,
8916
8981
  "params",
8917
8982
  args.projectName,
8918
- projectVersion,
8983
+ bomPayload.projectVersion,
8919
8984
  );
8920
8985
  }
8921
8986
  try {
@@ -120,5 +120,122 @@ describe("CLI tests", () => {
120
120
  assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
121
121
  assert.deepEqual(options.json, expectedRequestPayload);
122
122
  });
123
+
124
+ it("should include parentName and parentVersion when parent project name and version are passed", async () => {
125
+ const fakeGotResponse = {
126
+ json: sinon.stub().resolves({ success: true }),
127
+ };
128
+
129
+ const gotStub = sinon.stub().returns(fakeGotResponse);
130
+ gotStub.extend = sinon.stub().returns(gotStub);
131
+
132
+ const { submitBom } = await esmock("./index.js", {
133
+ got: { default: gotStub },
134
+ });
135
+
136
+ const serverUrl = "https://dtrack.example.com";
137
+ const projectName = "cdxgen-test-project";
138
+ const projectVersion = "2.0.0";
139
+ const parentProjectName = "parent-project";
140
+ const parentProjectVersion = "1.0.0";
141
+ const bomContent = {
142
+ bom: "test3",
143
+ };
144
+ const apiKey = "TEST_API_KEY";
145
+ const skipDtTlsCheck = false;
146
+
147
+ const expectedRequestPayload = {
148
+ autoCreate: "true",
149
+ bom: "eyJib20iOiJ0ZXN0MyJ9", // stringified and base64 encoded bomContent
150
+ parentName: parentProjectName,
151
+ parentVersion: parentProjectVersion,
152
+ projectName,
153
+ projectVersion,
154
+ };
155
+
156
+ await submitBom(
157
+ {
158
+ serverUrl,
159
+ projectName,
160
+ projectVersion,
161
+ parentProjectName,
162
+ parentProjectVersion,
163
+ apiKey,
164
+ skipDtTlsCheck,
165
+ },
166
+ bomContent,
167
+ );
168
+
169
+ sinon.assert.calledOnce(gotStub);
170
+ const [calledUrl, options] = gotStub.firstCall.args;
171
+
172
+ assert.equal(calledUrl, `${serverUrl}/api/v1/bom`);
173
+ assert.equal(options.method, "PUT");
174
+ assert.equal(options.https.rejectUnauthorized, !skipDtTlsCheck);
175
+ assert.equal(options.headers["X-Api-Key"], apiKey);
176
+ assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
177
+ assert.deepEqual(options.json, expectedRequestPayload);
178
+ });
179
+
180
+ it("should include configurable autoCreate and isLatest values in payload", async () => {
181
+ const fakeGotResponse = {
182
+ json: sinon.stub().resolves({ success: true }),
183
+ };
184
+
185
+ const gotStub = sinon.stub().returns(fakeGotResponse);
186
+ gotStub.extend = sinon.stub().returns(gotStub);
187
+
188
+ const { submitBom } = await esmock("./index.js", {
189
+ got: { default: gotStub },
190
+ });
191
+
192
+ const serverUrl = "https://dtrack.example.com";
193
+ const projectName = "cdxgen-test-project";
194
+ const apiKey = "TEST_API_KEY";
195
+
196
+ await submitBom(
197
+ {
198
+ serverUrl,
199
+ projectName,
200
+ apiKey,
201
+ autoCreate: false,
202
+ isLatest: true,
203
+ },
204
+ { bom: "test4" },
205
+ );
206
+
207
+ sinon.assert.calledOnce(gotStub);
208
+ const [_calledUrl, options] = gotStub.firstCall.args;
209
+ assert.equal(options.json.autoCreate, "false");
210
+ assert.equal(options.json.isLatest, true);
211
+ assert.equal(options.json.projectVersion, "main");
212
+ });
213
+
214
+ it("should reject invalid mixed parent modes before making network request", async () => {
215
+ const fakeGotResponse = {
216
+ json: sinon.stub().resolves({ success: true }),
217
+ };
218
+
219
+ const gotStub = sinon.stub().returns(fakeGotResponse);
220
+ gotStub.extend = sinon.stub().returns(gotStub);
221
+
222
+ const { submitBom } = await esmock("./index.js", {
223
+ got: { default: gotStub },
224
+ });
225
+
226
+ const response = await submitBom(
227
+ {
228
+ serverUrl: "https://dtrack.example.com",
229
+ projectName: "cdxgen-test-project",
230
+ parentProjectId: "5103b8b4-4ca3-46ea-8051-036a3b2ab17e",
231
+ parentProjectName: "parent",
232
+ parentProjectVersion: "1.0.0",
233
+ },
234
+ { bom: "test5" },
235
+ );
236
+
237
+ assert.equal(response, undefined);
238
+ sinon.assert.notCalled(gotStub);
239
+ });
123
240
  });
124
241
  });