@cdxgen/cdxgen 13.0.0 → 13.1.0

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 (170) hide show
  1. package/README.md +26 -13
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +140 -8
  4. package/bin/convert.js +7 -0
  5. package/bin/evinse.js +1 -1
  6. package/bin/hbom.js +13 -4
  7. package/bin/tracebom.js +9 -1
  8. package/data/component-tags.json +8 -1
  9. package/data/frameworks-list.json +86 -1
  10. package/data/remediations.json +1144 -0
  11. package/data/rules/asar-archives.yaml +1 -1
  12. package/data/rules/build-fidelity.yaml +782 -0
  13. package/data/rules/package-integrity.yaml +13 -13
  14. package/lib/audit/index.js +48 -4
  15. package/lib/audit/scoring.js +2 -8
  16. package/lib/cli/bomAssembly.js +83 -15
  17. package/lib/cli/cliOptions.js +124 -7
  18. package/lib/cli/index.js +76 -48
  19. package/lib/cli/jsBom.js +32 -3
  20. package/lib/cli/jvmBom.js +1494 -1315
  21. package/lib/cli/managedBom.js +97 -28
  22. package/lib/cli/nativeBom.js +189 -10
  23. package/lib/core/buildLedger.js +799 -0
  24. package/lib/core/deferredExit.js +141 -0
  25. package/lib/core/env.js +69 -1
  26. package/lib/core/fs.js +60 -0
  27. package/lib/core/logger.js +76 -25
  28. package/lib/core/severity.js +16 -0
  29. package/lib/core/syncFileWriter.js +106 -0
  30. package/lib/core/ui.js +46 -2
  31. package/lib/ecosystems/ecosystems.js +9 -0
  32. package/lib/ecosystems/gradleutils.js +355 -15
  33. package/lib/ecosystems/parsers-js.js +26 -0
  34. package/lib/ecosystems/parsers-jvm.js +9 -0
  35. package/lib/ecosystems/parsers-misc.js +16 -2
  36. package/lib/ecosystems/parsers-python.js +171 -35
  37. package/lib/ecosystems/parsers-rust.js +12 -2
  38. package/lib/ecosystems/pypiClassifiers.js +326 -0
  39. package/lib/ecosystems/pythonutils.js +49 -1
  40. package/lib/ecosystems/rubyutils.js +13 -0
  41. package/lib/ecosystems/utils.js +9 -1
  42. package/lib/evinser/evinser.js +30 -12
  43. package/lib/helpers/remote/dependency-track.js +81 -14
  44. package/lib/inventory/auditCategories.js +9 -0
  45. package/lib/inventory/depsUtils.js +72 -0
  46. package/lib/inventory/dosai.js +431 -22
  47. package/lib/inventory/envcontext.js +211 -19
  48. package/lib/inventory/fetchBatch.js +57 -0
  49. package/lib/inventory/jvmToolEnv.js +593 -0
  50. package/lib/inventory/osinfo.js +123 -16
  51. package/lib/inventory/table.js +9 -5
  52. package/lib/inventory/toolRequirements.js +903 -0
  53. package/lib/managers/binary.js +216 -40
  54. package/lib/managers/docker.js +26 -5
  55. package/lib/server/server.js +1 -1
  56. package/lib/stages/postgen/annotationBuilder.js +97 -0
  57. package/lib/stages/postgen/auditBom.js +76 -79
  58. package/lib/stages/postgen/introspection/annotate.js +320 -0
  59. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  60. package/lib/stages/postgen/introspection/emit.js +192 -0
  61. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  62. package/lib/stages/postgen/introspection/markers.js +227 -0
  63. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  64. package/lib/stages/postgen/introspection/report.js +1208 -0
  65. package/lib/stages/postgen/introspection/score.js +1067 -0
  66. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  67. package/lib/stages/postgen/parentEdge.js +156 -0
  68. package/lib/stages/postgen/postgen.js +115 -2
  69. package/lib/stages/postgen/ruleEngine.js +17 -0
  70. package/lib/stages/pregen/envAudit.js +42 -2
  71. package/lib/stages/pregen/pregen.js +1171 -6
  72. package/lib/validator/bomValidator.js +11 -2
  73. package/package.json +14 -14
  74. package/types/lib/audit/index.d.ts.map +1 -1
  75. package/types/lib/audit/scoring.d.ts +2 -8
  76. package/types/lib/audit/scoring.d.ts.map +1 -1
  77. package/types/lib/cli/bomAssembly.d.ts +14 -0
  78. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  79. package/types/lib/cli/cliOptions.d.ts +2 -1
  80. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  81. package/types/lib/cli/index.d.ts +8 -0
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/cli/jsBom.d.ts.map +1 -1
  84. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  85. package/types/lib/cli/managedBom.d.ts.map +1 -1
  86. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  87. package/types/lib/core/buildLedger.d.ts +272 -0
  88. package/types/lib/core/buildLedger.d.ts.map +1 -0
  89. package/types/lib/core/deferredExit.d.ts +84 -0
  90. package/types/lib/core/deferredExit.d.ts.map +1 -0
  91. package/types/lib/core/env.d.ts +22 -1
  92. package/types/lib/core/env.d.ts.map +1 -1
  93. package/types/lib/core/fs.d.ts +21 -0
  94. package/types/lib/core/fs.d.ts.map +1 -1
  95. package/types/lib/core/logger.d.ts +14 -1
  96. package/types/lib/core/logger.d.ts.map +1 -1
  97. package/types/lib/core/severity.d.ts +11 -0
  98. package/types/lib/core/severity.d.ts.map +1 -0
  99. package/types/lib/core/syncFileWriter.d.ts +33 -0
  100. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  101. package/types/lib/core/ui.d.ts +1 -1
  102. package/types/lib/core/ui.d.ts.map +1 -1
  103. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  104. package/types/lib/ecosystems/gradleutils.d.ts +33 -0
  105. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  106. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  109. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  110. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  111. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  112. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  113. package/types/lib/ecosystems/pythonutils.d.ts +20 -0
  114. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  115. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  116. package/types/lib/ecosystems/utils.d.ts +4 -4
  117. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  118. package/types/lib/evinser/evinser.d.ts +5 -1
  119. package/types/lib/evinser/evinser.d.ts.map +1 -1
  120. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  121. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  122. package/types/lib/inventory/auditCategories.d.ts +8 -0
  123. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  125. package/types/lib/inventory/dosai.d.ts +20 -0
  126. package/types/lib/inventory/dosai.d.ts.map +1 -1
  127. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  128. package/types/lib/inventory/fetchBatch.d.ts +9 -0
  129. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  130. package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
  131. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  132. package/types/lib/inventory/osinfo.d.ts +83 -2
  133. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  134. package/types/lib/inventory/table.d.ts +4 -1
  135. package/types/lib/inventory/table.d.ts.map +1 -1
  136. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  137. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  138. package/types/lib/managers/binary.d.ts +71 -0
  139. package/types/lib/managers/binary.d.ts.map +1 -1
  140. package/types/lib/managers/docker.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  142. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  144. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  145. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  146. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  147. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  148. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  149. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  150. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  151. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  152. package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
  153. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  154. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  155. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  156. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  157. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  158. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  159. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  160. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  161. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  162. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  163. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  164. package/types/lib/stages/postgen/postgen.d.ts +2 -2
  165. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  166. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  167. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  168. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  169. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  170. package/types/lib/validator/bomValidator.d.ts.map +1 -1
package/lib/cli/jvmBom.js CHANGED
@@ -8,6 +8,11 @@ import { build } from "@cdxgen/cdx-purl";
8
8
  import { gte, lte } from "semver";
9
9
 
10
10
  import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
11
+ import { commandOutputText, recordDegradation } from "../core/buildLedger.js";
12
+ import {
13
+ deferFailOnError,
14
+ isDeferredFailOnError,
15
+ } from "../core/deferredExit.js";
11
16
  import {
12
17
  hasAnyProjectType,
13
18
  includeMavenTestScope,
@@ -39,10 +44,12 @@ import {
39
44
  getGradleCommand,
40
45
  getMavenCommand,
41
46
  getMillCommand,
47
+ hasGradleInvocationFailure,
42
48
  parseGradleDep,
43
49
  parseGradleInfoLogsForUrls,
44
50
  parseGradleProperties,
45
51
  parseGradleResolvedDistributions,
52
+ recordGradleInvocationFailure,
46
53
  splitOutputByGradleProjects,
47
54
  } from "../ecosystems/gradleutils.js";
48
55
  import {
@@ -77,6 +84,7 @@ import {
77
84
  attachIdentityTools,
78
85
  extractToolRefs,
79
86
  } from "../inventory/evidenceUtils.js";
87
+ import { readGradleWrapperVersion } from "../inventory/jvmToolEnv.js";
80
88
  import { applyPurl, mavenPurl } from "../inventory/purl.js";
81
89
  import {
82
90
  buildBomNSData,
@@ -263,7 +271,25 @@ export function discoverSbtProjectsFromCmd(basePath, sbtCmd, env) {
263
271
  "sbt projects command did not succeed. Falling back to heuristic project discovery.",
264
272
  );
265
273
  }
274
+ // Reached only when the sbt projects command did not yield usable
275
+ // subprojects; both success paths above returned early.
276
+ recordDegradation("jvm.sbt.project-discovery", {
277
+ ecosystem: "java",
278
+ tool: "sbt",
279
+ impact: "components",
280
+ command: `${basename(sbtCmd)} -batch -no-colors "projects"`,
281
+ detail:
282
+ "The sbt projects command did not yield usable project ids, so project discovery falls back to the build-file heuristic.",
283
+ });
266
284
  } catch (err) {
285
+ recordDegradation("jvm.sbt.project-discovery", {
286
+ ecosystem: "java",
287
+ tool: "sbt",
288
+ kind: "command.failed",
289
+ impact: "components",
290
+ command: `${basename(sbtCmd)} -batch -no-colors "projects"`,
291
+ detail: "The sbt projects command could not be executed.",
292
+ });
267
293
  if (DEBUG_MODE) {
268
294
  console.log("Unable to run sbt projects command.", err);
269
295
  }
@@ -291,269 +317,245 @@ export async function createJavaBom(path, options) {
291
317
  let tools;
292
318
  let possible_misses = false;
293
319
  let parallelPropTaskOut = "";
294
- // war/ear mode
295
- if (path.endsWith(".war") || path.endsWith(".jar")) {
296
- // Check if the file exists
297
- if (safeExistsSync(path)) {
298
- if (DEBUG_MODE) {
299
- console.log(`Retrieving packages from ${path}`);
300
- }
301
- const tempDir = safeMkdtempSync(join(getTmpDir(), "war-deps-"));
302
- jarNSMapping = await collectJarNS(tempDir);
303
- pkgList = await extractJarArchive(path, tempDir, jarNSMapping);
304
- if (pkgList.length) {
305
- pkgList = await getMvnMetadata(pkgList);
306
- }
307
- // Clean up
308
- if (tempDir?.startsWith(getTmpDir())) {
309
- console.log(`Cleaning up ${tempDir}`);
310
- safeRmSync(tempDir, { recursive: true, force: true });
311
- }
312
- } else {
313
- console.log(`${path} doesn't exist`);
314
- }
315
- return buildBomNSData(options, pkgList, "maven", {
316
- src: dirname(path),
317
- filename: path,
318
- nsMapping: jarNSMapping,
319
- dependencies,
320
- parentComponent,
321
- });
322
- }
323
- // -t quarkus is supported
324
- let isQuarkus = options?.projectType?.includes("quarkus");
325
- let useMavenDepsTree = isQuarkus ? false : PREFER_MAVEN_DEPS_TREE;
326
- // Is this a multi-module project
327
- let rootModules;
328
- // maven - pom.xml
329
- const pomFiles = getAllFiles(
330
- path,
331
- `${options.multiProject ? "**/" : ""}pom.xml`,
332
- options,
333
- );
334
- // gradle
335
- const gradleFiles = getAllFiles(
336
- path,
337
- `${options.multiProject ? "**/" : ""}build.gradle*`,
338
- options,
339
- );
340
- // mill
341
- const millFiles = getAllFiles(
342
- path,
343
- `${options.multiProject ? "**/" : ""}build.mill`,
344
- options,
345
- );
346
- let bomJsonFiles = [];
347
- if (
348
- pomFiles?.length &&
349
- isPackageManagerAllowed(
350
- "maven",
351
- ["bazel", "sbt", "gradle", "mill"],
352
- options,
353
- )
354
- ) {
355
- if (gradleFiles.length) {
356
- thoughtLog(
357
- `Is this a Gradle project? I recommend invoking cdxgen with the "-t gradle" option if you're encountering build errors.`,
358
- );
359
- }
360
- if (!isQuarkus) {
361
- // Quarkus projects require special treatment. To detect quarkus, we parse the first 3 maven file to look for a hit
362
- for (const pf of pomFiles.slice(0, 3)) {
363
- const pomMap = parsePom(pf);
364
- if (!rootModules && pomMap?.modules?.length) {
365
- rootModules = pomMap.modules;
320
+ try {
321
+ // war/ear mode
322
+ if (path.endsWith(".war") || path.endsWith(".jar")) {
323
+ // Check if the file exists
324
+ if (safeExistsSync(path)) {
325
+ if (DEBUG_MODE) {
326
+ console.log(`Retrieving packages from ${path}`);
327
+ }
328
+ const tempDir = safeMkdtempSync(join(getTmpDir(), "war-deps-"));
329
+ jarNSMapping = await collectJarNS(tempDir);
330
+ pkgList = await extractJarArchive(path, tempDir, jarNSMapping);
331
+ if (pkgList.length) {
332
+ pkgList = await getMvnMetadata(pkgList);
366
333
  }
367
- // In quarkus mode, we cannot use the maven deps tree
368
- if (pomMap.isQuarkus) {
369
- isQuarkus = true;
370
- useMavenDepsTree = false;
371
- break;
334
+ // Clean up
335
+ if (tempDir?.startsWith(getTmpDir())) {
336
+ console.log(`Cleaning up ${tempDir}`);
337
+ safeRmSync(tempDir, { recursive: true, force: true });
372
338
  }
339
+ } else {
340
+ console.log(`${path} doesn't exist`);
373
341
  }
342
+ return buildBomNSData(options, pkgList, "maven", {
343
+ src: dirname(path),
344
+ filename: path,
345
+ nsMapping: jarNSMapping,
346
+ dependencies,
347
+ parentComponent,
348
+ });
374
349
  }
375
- let result;
376
- let mvnArgs;
377
- // FIXME: How do we motivate everyone to upgrade to 1.7?
378
- const toolsSpecVersion = 1.6;
379
- if (isQuarkus) {
380
- thoughtLog(
381
- "This appears to be a Quarkus project. Let's use the right Maven plugin.",
382
- );
383
- // disable analytics. See: https://quarkus.io/usage/
384
- mvnArgs = [
385
- "-fn",
386
- "quarkus:dependency-sbom",
387
- "-Dquarkus.analytics.disabled=true",
388
- ];
389
- if (options.specVersion >= 1.6) {
390
- mvnArgs = mvnArgs.concat(
391
- `-Dquarkus.dependency.sbom.schema-version=${toolsSpecVersion}`,
350
+ // -t quarkus is supported
351
+ let isQuarkus = options?.projectType?.includes("quarkus");
352
+ let useMavenDepsTree = isQuarkus ? false : PREFER_MAVEN_DEPS_TREE;
353
+ // Is this a multi-module project
354
+ let rootModules;
355
+ // maven - pom.xml
356
+ const pomFiles = getAllFiles(
357
+ path,
358
+ `${options.multiProject ? "**/" : ""}pom.xml`,
359
+ options,
360
+ );
361
+ // gradle
362
+ const gradleFiles = getAllFiles(
363
+ path,
364
+ `${options.multiProject ? "**/" : ""}build.gradle*`,
365
+ options,
366
+ );
367
+ // mill
368
+ const millFiles = getAllFiles(
369
+ path,
370
+ `${options.multiProject ? "**/" : ""}build.mill`,
371
+ options,
372
+ );
373
+ let bomJsonFiles = [];
374
+ if (
375
+ pomFiles?.length &&
376
+ isPackageManagerAllowed(
377
+ "maven",
378
+ ["bazel", "sbt", "gradle", "mill"],
379
+ options,
380
+ )
381
+ ) {
382
+ if (gradleFiles.length) {
383
+ thoughtLog(
384
+ `Is this a Gradle project? I recommend invoking cdxgen with the "-t gradle" option if you're encountering build errors.`,
392
385
  );
393
386
  }
394
- } else {
395
- // FIXME: The last maven plugin release was on November 28th, 2024.
396
- // Should we fork this repo and maintain it ourselves?
397
- const cdxMavenPlugin =
398
- readEnvironmentVariable("CDX_MAVEN_PLUGIN") ||
399
- "org.cyclonedx:cyclonedx-maven-plugin:2.9.1";
400
- const cdxMavenGoal =
401
- readEnvironmentVariable("CDX_MAVEN_GOAL") || "makeAggregateBom";
402
- mvnArgs = [
403
- "-fn",
404
- `${cdxMavenPlugin}:${cdxMavenGoal}`,
405
- "-DoutputName=bom",
406
- ];
407
- if (includeMavenTestScope) {
408
- mvnArgs.push("-DincludeTestScope=true");
409
- }
410
- // By using quiet mode we can reduce the maxBuffer used and avoid crashes
411
- if (!DEBUG_MODE) {
412
- mvnArgs.push("-q");
413
- }
414
- // Support for passing additional settings and profile to maven
415
- if (readEnvironmentVariable("MVN_ARGS")) {
416
- const addArgs = parseMavenArgs(readEnvironmentVariable("MVN_ARGS"));
417
- mvnArgs = mvnArgs.concat(addArgs);
418
- }
419
- // specVersion 1.4 doesn't support externalReferences.type=distribution-intake
420
- // so we need to run the plugin with the correct version
421
- if (options.specVersion >= 1.6) {
422
- mvnArgs = mvnArgs.concat(`-DschemaVersion=${toolsSpecVersion}`);
423
- } else if (options.specVersion > 1.4) {
424
- mvnArgs = mvnArgs.concat(`-DschemaVersion=${options.specVersion}`);
425
- }
426
- }
427
- const firstPom = pomFiles.length ? pomFiles[0] : undefined;
428
- let mavenCmd = getMavenCommand(path, path);
429
- for (const f of pomFiles) {
430
- const basePath = dirname(f);
431
- if (
432
- isQuarkus &&
433
- !options.deep &&
434
- rootModules?.includes(basename(basePath))
435
- ) {
436
- if (DEBUG_MODE) {
437
- console.log("Skipped sub-module", basePath);
387
+ if (!isQuarkus) {
388
+ // Quarkus projects require special treatment. To detect quarkus, we parse the first 3 maven file to look for a hit
389
+ for (const pf of pomFiles.slice(0, 3)) {
390
+ const pomMap = parsePom(pf);
391
+ if (!rootModules && pomMap?.modules?.length) {
392
+ rootModules = pomMap.modules;
393
+ }
394
+ // In quarkus mode, we cannot use the maven deps tree
395
+ if (pomMap.isQuarkus) {
396
+ isQuarkus = true;
397
+ useMavenDepsTree = false;
398
+ break;
399
+ }
438
400
  }
439
- continue;
440
401
  }
441
- const settingsXml = join(basePath, "settings.xml");
442
- if (safeExistsSync(settingsXml)) {
443
- console.log(
444
- `maven settings.xml found in ${basePath}. Please set the MVN_ARGS environment variable based on the full mvn build command used for this project.\nExample: MVN_ARGS='--settings ${settingsXml}'`,
445
- );
446
- }
447
- if (mavenCmd?.endsWith("mvn")) {
448
- mavenCmd = getMavenCommand(basePath, path);
449
- }
450
- // Should we attempt to resolve class names
451
- if (options.resolveClass || options.deep) {
452
- const tmpjarNSMapping = await collectMvnDependencies(
453
- mavenCmd,
454
- basePath,
455
- true,
456
- false,
402
+ let result;
403
+ let mvnArgs;
404
+ // FIXME: How do we motivate everyone to upgrade to 1.7?
405
+ const toolsSpecVersion = 1.6;
406
+ if (isQuarkus) {
407
+ thoughtLog(
408
+ "This appears to be a Quarkus project. Let's use the right Maven plugin.",
457
409
  );
458
- if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
459
- jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
410
+ // disable analytics. See: https://quarkus.io/usage/
411
+ mvnArgs = [
412
+ "-fn",
413
+ "quarkus:dependency-sbom",
414
+ "-Dquarkus.analytics.disabled=true",
415
+ ];
416
+ if (options.specVersion >= 1.6) {
417
+ mvnArgs = mvnArgs.concat(
418
+ `-Dquarkus.dependency.sbom.schema-version=${toolsSpecVersion}`,
419
+ );
420
+ }
421
+ } else {
422
+ // FIXME: The last maven plugin release was on November 28th, 2024.
423
+ // Should we fork this repo and maintain it ourselves?
424
+ const cdxMavenPlugin =
425
+ readEnvironmentVariable("CDX_MAVEN_PLUGIN") ||
426
+ "org.cyclonedx:cyclonedx-maven-plugin:2.9.1";
427
+ const cdxMavenGoal =
428
+ readEnvironmentVariable("CDX_MAVEN_GOAL") || "makeAggregateBom";
429
+ mvnArgs = [
430
+ "-fn",
431
+ `${cdxMavenPlugin}:${cdxMavenGoal}`,
432
+ "-DoutputName=bom",
433
+ ];
434
+ if (includeMavenTestScope) {
435
+ mvnArgs.push("-DincludeTestScope=true");
436
+ }
437
+ // By using quiet mode we can reduce the maxBuffer used and avoid crashes
438
+ if (!DEBUG_MODE) {
439
+ mvnArgs.push("-q");
440
+ }
441
+ // Support for passing additional settings and profile to maven
442
+ if (readEnvironmentVariable("MVN_ARGS")) {
443
+ const addArgs = parseMavenArgs(readEnvironmentVariable("MVN_ARGS"));
444
+ mvnArgs = mvnArgs.concat(addArgs);
445
+ }
446
+ // specVersion 1.4 doesn't support externalReferences.type=distribution-intake
447
+ // so we need to run the plugin with the correct version
448
+ if (options.specVersion >= 1.6) {
449
+ mvnArgs = mvnArgs.concat(`-DschemaVersion=${toolsSpecVersion}`);
450
+ } else if (options.specVersion > 1.4) {
451
+ mvnArgs = mvnArgs.concat(`-DschemaVersion=${options.specVersion}`);
460
452
  }
461
453
  }
462
- // Use the cyclonedx maven plugin if there is no preference for maven deps tree
463
- if (!useMavenDepsTree) {
464
- thoughtLog("The user wants me to use the cyclonedx-maven plugin.");
465
- console.log(`Executing '${mavenCmd}' in`, basePath);
466
- result = safeSpawnSync(mavenCmd, mvnArgs, {
467
- cwd: basePath,
468
- shell: isWin,
469
- });
470
- // Check if the cyclonedx plugin created the required bom.json file
471
- // Sometimes the plugin fails silently for complex maven projects
472
- bomJsonFiles = getAllFiles(
473
- path,
474
- "**/target/*{cdx,bom,cyclonedx}*.json",
475
- options,
476
- );
477
- // Check if the bom json files got created in a directory other than target
478
- if (!bomJsonFiles.length) {
454
+ const firstPom = pomFiles.length ? pomFiles[0] : undefined;
455
+ let mavenCmd = getMavenCommand(path, path);
456
+ for (const f of pomFiles) {
457
+ const basePath = dirname(f);
458
+ if (
459
+ isQuarkus &&
460
+ !options.deep &&
461
+ rootModules?.includes(basename(basePath))
462
+ ) {
463
+ if (DEBUG_MODE) {
464
+ console.log("Skipped sub-module", basePath);
465
+ }
466
+ continue;
467
+ }
468
+ const settingsXml = join(basePath, "settings.xml");
469
+ if (safeExistsSync(settingsXml)) {
470
+ console.log(
471
+ `maven settings.xml found in ${basePath}. Please set the MVN_ARGS environment variable based on the full mvn build command used for this project.\nExample: MVN_ARGS='--settings ${settingsXml}'`,
472
+ );
473
+ }
474
+ if (mavenCmd?.endsWith("mvn")) {
475
+ mavenCmd = getMavenCommand(basePath, path);
476
+ }
477
+ // Should we attempt to resolve class names
478
+ if (options.resolveClass || options.deep) {
479
+ const tmpjarNSMapping = await collectMvnDependencies(
480
+ mavenCmd,
481
+ basePath,
482
+ true,
483
+ false,
484
+ );
485
+ if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
486
+ jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
487
+ }
488
+ }
489
+ // Use the cyclonedx maven plugin if there is no preference for maven deps tree
490
+ if (!useMavenDepsTree) {
491
+ thoughtLog("The user wants me to use the cyclonedx-maven plugin.");
492
+ console.log(`Executing '${mavenCmd}' in`, basePath);
493
+ result = safeSpawnSync(mavenCmd, mvnArgs, {
494
+ cwd: basePath,
495
+ shell: isWin,
496
+ });
497
+ // Check if the cyclonedx plugin created the required bom.json file
498
+ // Sometimes the plugin fails silently for complex maven projects
479
499
  bomJsonFiles = getAllFiles(
480
500
  path,
481
- "target/**/*{cdx,bom,cyclonedx}*.json",
501
+ "**/target/*{cdx,bom,cyclonedx}*.json",
482
502
  options,
483
503
  );
484
- }
485
- }
486
- // Also check if the user has a preference for maven deps tree command
487
- if (
488
- useMavenDepsTree ||
489
- !bomJsonFiles.length ||
490
- result?.status !== 0 ||
491
- result?.error
492
- ) {
493
- const tempRoot = getTmpDir();
494
- const tempDir = safeMkdtempSync(join(tempRoot, "cdxgen-mvn-"));
495
- const tempMvnTree = join(tempDir, "cdxgen-mvn-tree.json");
496
- const tempMvnTreeText = join(tempDir, "cdxgen-mvn-tree.txt");
497
- const tempMvnParentTree = join(tempDir, "cdxgen-mvn-parent-tree.json");
498
- const tempMvnParentTreeText = join(
499
- tempDir,
500
- "cdxgen-mvn-parent-tree.txt",
501
- );
502
- try {
503
- let mvnTreeArgs = [
504
- "dependency:tree",
505
- `-DoutputFile=${tempMvnTree}`,
506
- "-DoutputType=json",
507
- ];
508
- let addArgs = [];
509
- if (readEnvironmentVariable("MVN_ARGS")) {
510
- addArgs = parseMavenArgs(readEnvironmentVariable("MVN_ARGS"));
511
- mvnTreeArgs = mvnTreeArgs.concat(addArgs);
512
- }
513
- // Automatically use settings.xml to improve the success for fallback
514
- if (safeExistsSync(settingsXml)) {
515
- mvnTreeArgs.push("-s");
516
- mvnTreeArgs.push(settingsXml);
517
- }
518
- // For the first pom alone, we need to execute first in non-recursive mode to capture
519
- // the parent component. Then, we execute all of them in recursive mode
520
- if (f === firstPom) {
521
- thoughtLog(
522
- "What is the parent component here? Let's use maven command to find out.",
504
+ // Check if the bom json files got created in a directory other than target
505
+ if (!bomJsonFiles.length) {
506
+ bomJsonFiles = getAllFiles(
507
+ path,
508
+ "target/**/*{cdx,bom,cyclonedx}*.json",
509
+ options,
523
510
  );
524
- let findParentComponentArgs = [
511
+ }
512
+ }
513
+ // Also check if the user has a preference for maven deps tree command
514
+ if (
515
+ useMavenDepsTree ||
516
+ !bomJsonFiles.length ||
517
+ result?.status !== 0 ||
518
+ result?.error
519
+ ) {
520
+ const tempRoot = getTmpDir();
521
+ const tempDir = safeMkdtempSync(join(tempRoot, "cdxgen-mvn-"));
522
+ const tempMvnTree = join(tempDir, "cdxgen-mvn-tree.json");
523
+ const tempMvnTreeText = join(tempDir, "cdxgen-mvn-tree.txt");
524
+ const tempMvnParentTree = join(
525
+ tempDir,
526
+ "cdxgen-mvn-parent-tree.json",
527
+ );
528
+ const tempMvnParentTreeText = join(
529
+ tempDir,
530
+ "cdxgen-mvn-parent-tree.txt",
531
+ );
532
+ try {
533
+ let mvnTreeArgs = [
525
534
  "dependency:tree",
526
- "-N",
527
- `-DoutputFile=${tempMvnParentTree}`,
535
+ `-DoutputFile=${tempMvnTree}`,
528
536
  "-DoutputType=json",
529
537
  ];
530
- if (addArgs.length) {
531
- findParentComponentArgs = findParentComponentArgs.concat(addArgs);
538
+ let addArgs = [];
539
+ if (readEnvironmentVariable("MVN_ARGS")) {
540
+ addArgs = parseMavenArgs(readEnvironmentVariable("MVN_ARGS"));
541
+ mvnTreeArgs = mvnTreeArgs.concat(addArgs);
532
542
  }
533
- result = safeSpawnSync(mavenCmd, findParentComponentArgs, {
534
- cwd: basePath,
535
- shell: isWin,
536
- });
537
- // If json is empty or unparseable, fallback to text parsing
538
- let emptyJson = !safeExistsSync(tempMvnParentTree);
539
- if (safeExistsSync(tempMvnParentTree)) {
540
- const mvnTreeString = readFileSync(tempMvnParentTree, {
541
- encoding: "utf-8",
542
- });
543
- const parsedList = parseMavenTreeJson(mvnTreeString, f);
544
- if (!parsedList?.pkgList?.length) {
545
- emptyJson = true;
546
- // Remove the invalid json file so text output is preferred below.
547
- if (safeExistsSync(tempMvnParentTree)) {
548
- safeUnlinkSync(tempMvnParentTree);
549
- }
550
- }
543
+ // Automatically use settings.xml to improve the success for fallback
544
+ if (safeExistsSync(settingsXml)) {
545
+ mvnTreeArgs.push("-s");
546
+ mvnTreeArgs.push(settingsXml);
551
547
  }
552
- if (result.status !== 0 || result.error || emptyJson) {
553
- findParentComponentArgs = [
548
+ // For the first pom alone, we need to execute first in non-recursive mode to capture
549
+ // the parent component. Then, we execute all of them in recursive mode
550
+ if (f === firstPom) {
551
+ thoughtLog(
552
+ "What is the parent component here? Let's use maven command to find out.",
553
+ );
554
+ let findParentComponentArgs = [
554
555
  "dependency:tree",
555
556
  "-N",
556
- `-DoutputFile=${tempMvnParentTreeText}`,
557
+ `-DoutputFile=${tempMvnParentTree}`,
558
+ "-DoutputType=json",
557
559
  ];
558
560
  if (addArgs.length) {
559
561
  findParentComponentArgs =
@@ -563,349 +565,407 @@ export async function createJavaBom(path, options) {
563
565
  cwd: basePath,
564
566
  shell: isWin,
565
567
  });
566
- }
567
- if (result.status === 0) {
568
- const parentTreeFile = safeExistsSync(tempMvnParentTreeText)
569
- ? tempMvnParentTreeText
570
- : tempMvnParentTree;
571
- if (safeExistsSync(parentTreeFile)) {
572
- const mvnTreeString = readFileSync(parentTreeFile, {
568
+ // If json is empty or unparseable, fallback to text parsing
569
+ let emptyJson = !safeExistsSync(tempMvnParentTree);
570
+ if (safeExistsSync(tempMvnParentTree)) {
571
+ const mvnTreeString = readFileSync(tempMvnParentTree, {
573
572
  encoding: "utf-8",
574
573
  });
575
- const parsedList = parentTreeFile.endsWith(".json")
576
- ? parseMavenTreeJson(mvnTreeString, f)
577
- : parseMavenTree(mvnTreeString, f);
578
- const dlist = parsedList.pkgList || [];
579
- if (dlist.length) {
580
- const tmpParentComponent = dlist.splice(0, 1)[0];
581
- tmpParentComponent.type = "application";
582
- parentComponent = tmpParentComponent;
583
- parentComponent.components = [];
584
- if (parentComponent.name) {
585
- thoughtLog(
586
- `Parent component is called ${parentComponent.name}!`,
587
- );
574
+ const parsedList = parseMavenTreeJson(mvnTreeString, f);
575
+ if (!parsedList?.pkgList?.length) {
576
+ emptyJson = true;
577
+ // Remove the invalid json file so text output is preferred below.
578
+ if (safeExistsSync(tempMvnParentTree)) {
579
+ safeUnlinkSync(tempMvnParentTree);
588
580
  }
589
581
  }
590
582
  }
591
- }
592
- }
593
- thoughtLog(
594
- `**MAVEN**: Let's use Maven to collect packages from ${basePath}.`,
595
- );
596
- if (DEBUG_MODE) {
597
- console.log(
598
- `Executing '${basename(mavenCmd)} dependency:tree ...' in ${basePath}`,
599
- );
600
- }
601
- result = safeSpawnSync(mavenCmd, mvnTreeArgs, {
602
- cwd: basePath,
603
- shell: isWin,
604
- });
605
- let emptyJson = !safeExistsSync(tempMvnTree);
606
- if (safeExistsSync(tempMvnTree)) {
607
- const mvnTreeString = readFileSync(tempMvnTree, {
608
- encoding: "utf-8",
609
- });
610
- const parsedList = parseMavenTreeJson(mvnTreeString, f);
611
- if (!parsedList?.pkgList?.length) {
612
- emptyJson = true;
613
- // Remove the invalid json file so text output is preferred below.
614
- if (safeExistsSync(tempMvnTree)) {
615
- safeUnlinkSync(tempMvnTree);
583
+ if (result.status !== 0 || result.error || emptyJson) {
584
+ findParentComponentArgs = [
585
+ "dependency:tree",
586
+ "-N",
587
+ `-DoutputFile=${tempMvnParentTreeText}`,
588
+ ];
589
+ if (addArgs.length) {
590
+ findParentComponentArgs =
591
+ findParentComponentArgs.concat(addArgs);
592
+ }
593
+ result = safeSpawnSync(mavenCmd, findParentComponentArgs, {
594
+ cwd: basePath,
595
+ shell: isWin,
596
+ });
597
+ }
598
+ if (result.status === 0) {
599
+ const parentTreeFile = safeExistsSync(tempMvnParentTreeText)
600
+ ? tempMvnParentTreeText
601
+ : tempMvnParentTree;
602
+ if (safeExistsSync(parentTreeFile)) {
603
+ const mvnTreeString = readFileSync(parentTreeFile, {
604
+ encoding: "utf-8",
605
+ });
606
+ const parsedList = parentTreeFile.endsWith(".json")
607
+ ? parseMavenTreeJson(mvnTreeString, f)
608
+ : parseMavenTree(mvnTreeString, f);
609
+ const dlist = parsedList.pkgList || [];
610
+ if (dlist.length) {
611
+ const tmpParentComponent = dlist.splice(0, 1)[0];
612
+ tmpParentComponent.type = "application";
613
+ parentComponent = tmpParentComponent;
614
+ parentComponent.components = [];
615
+ if (parentComponent.name) {
616
+ thoughtLog(
617
+ `Parent component is called ${parentComponent.name}!`,
618
+ );
619
+ }
620
+ }
621
+ }
616
622
  }
617
623
  }
618
- }
619
- if (result.status !== 0 || result.error || emptyJson) {
620
- mvnTreeArgs = [
621
- "dependency:tree",
622
- `-DoutputFile=${tempMvnTreeText}`,
623
- ];
624
- if (addArgs.length) {
625
- mvnTreeArgs = mvnTreeArgs.concat(addArgs);
626
- }
627
- if (safeExistsSync(settingsXml)) {
628
- mvnTreeArgs.push("-s");
629
- mvnTreeArgs.push(settingsXml);
624
+ thoughtLog(
625
+ `**MAVEN**: Let's use Maven to collect packages from ${basePath}.`,
626
+ );
627
+ if (DEBUG_MODE) {
628
+ console.log(
629
+ `Executing '${basename(mavenCmd)} dependency:tree ...' in ${basePath}`,
630
+ );
630
631
  }
631
632
  result = safeSpawnSync(mavenCmd, mvnTreeArgs, {
632
633
  cwd: basePath,
633
634
  shell: isWin,
634
635
  });
635
- }
636
- if (result.status !== 0 || result.error) {
637
- possible_misses = true;
638
- // Our approach to recursively invoking the maven plugin for each sub-module is bound to result in failures
639
- // These could be due to a range of reasons that are covered below.
640
- if (pomFiles.length === 1 || DEBUG_MODE || PREFER_MAVEN_DEPS_TREE) {
641
- if (result.stdout) {
642
- console.log(result.stdout);
636
+ let emptyJson = !safeExistsSync(tempMvnTree);
637
+ if (safeExistsSync(tempMvnTree)) {
638
+ const mvnTreeString = readFileSync(tempMvnTree, {
639
+ encoding: "utf-8",
640
+ });
641
+ const parsedList = parseMavenTreeJson(mvnTreeString, f);
642
+ if (!parsedList?.pkgList?.length) {
643
+ emptyJson = true;
644
+ // Remove the invalid json file so text output is preferred below.
645
+ if (safeExistsSync(tempMvnTree)) {
646
+ safeUnlinkSync(tempMvnTree);
647
+ }
648
+ }
649
+ }
650
+ if (result.status !== 0 || result.error || emptyJson) {
651
+ mvnTreeArgs = [
652
+ "dependency:tree",
653
+ `-DoutputFile=${tempMvnTreeText}`,
654
+ ];
655
+ if (addArgs.length) {
656
+ mvnTreeArgs = mvnTreeArgs.concat(addArgs);
643
657
  }
644
- if (result.stderr) {
645
- console.log(result.stderr);
646
- console.log("The above build errors could be due to:\n");
658
+ if (safeExistsSync(settingsXml)) {
659
+ mvnTreeArgs.push("-s");
660
+ mvnTreeArgs.push(settingsXml);
647
661
  }
662
+ result = safeSpawnSync(mavenCmd, mvnTreeArgs, {
663
+ cwd: basePath,
664
+ shell: isWin,
665
+ });
666
+ }
667
+ if (result.status !== 0 || result.error) {
668
+ possible_misses = true;
669
+ // Our approach to recursively invoking the maven plugin for each sub-module is bound to result in failures
670
+ // These could be due to a range of reasons that are covered below.
648
671
  if (
649
- result.stdout &&
650
- (result.stdout.includes("Non-resolvable parent POM") ||
651
- result.stdout.includes("points at wrong local POM"))
652
- ) {
653
- console.log(
654
- "1. Check if the pom.xml contains valid settings for parent and modules. Some projects can be built only from a specific directory.",
655
- );
656
- } else if (
657
- result.stdout &&
658
- (result.stdout.includes("Could not resolve dependencies") ||
659
- result.stdout.includes(
660
- "no dependency information available",
661
- ) ||
662
- result.stdout.includes(
663
- "The following artifacts could not be resolved",
664
- ))
665
- ) {
666
- console.log(
667
- "1. Try building the project with 'mvn package -Dmaven.test.skip=true' using the correct version of Java and maven before invoking cdxgen.",
668
- );
669
- } else if (
670
- result.stdout?.includes(
671
- "Could not resolve target platform specification",
672
- )
672
+ pomFiles.length === 1 ||
673
+ DEBUG_MODE ||
674
+ PREFER_MAVEN_DEPS_TREE
673
675
  ) {
676
+ if (result.stdout) {
677
+ console.log(result.stdout);
678
+ }
679
+ if (result.stderr) {
680
+ console.log(result.stderr);
681
+ console.log("The above build errors could be due to:\n");
682
+ }
683
+ if (
684
+ result.stdout &&
685
+ (result.stdout.includes("Non-resolvable parent POM") ||
686
+ result.stdout.includes("points at wrong local POM"))
687
+ ) {
688
+ console.log(
689
+ "1. Check if the pom.xml contains valid settings for parent and modules. Some projects can be built only from a specific directory.",
690
+ );
691
+ } else if (
692
+ result.stdout &&
693
+ (result.stdout.includes("Could not resolve dependencies") ||
694
+ result.stdout.includes(
695
+ "no dependency information available",
696
+ ) ||
697
+ result.stdout.includes(
698
+ "The following artifacts could not be resolved",
699
+ ))
700
+ ) {
701
+ recordDegradation("jvm.maven.build-first", {
702
+ ecosystem: "java",
703
+ tool: "maven",
704
+ impact: "transitive-deps",
705
+ command: `${basename(mavenCmd)} ${mvnTreeArgs.join(" ")}`,
706
+ exitCode:
707
+ typeof result.status === "number"
708
+ ? result.status
709
+ : undefined,
710
+ detail:
711
+ "Maven could not resolve the project dependencies, so the dependency tree is incomplete.",
712
+ outputExcerpt: commandOutputText(result),
713
+ });
714
+ console.log(
715
+ "1. Try building the project with 'mvn package -Dmaven.test.skip=true' using the correct version of Java and maven before invoking cdxgen.",
716
+ );
717
+ } else if (
718
+ result.stdout?.includes(
719
+ "Could not resolve target platform specification",
720
+ )
721
+ ) {
722
+ console.log(
723
+ "1. Some projects can be built only from the root directory. Invoke cdxgen with --no-recurse option",
724
+ );
725
+ } else {
726
+ console.log(
727
+ "1. Java version requirement: cdxgen container image bundles Java 24 with maven 3.9 which might be incompatible. Try running cdxgen with the custom JDK11-based image `ghcr.io/cdxgen/cdxgen-java11:v13`.",
728
+ );
729
+ }
674
730
  console.log(
675
- "1. Some projects can be built only from the root directory. Invoke cdxgen with --no-recurse option",
731
+ "2. Private dependencies cannot be downloaded: Check if any additional arguments must be passed to maven and set them via MVN_ARGS environment variable.",
676
732
  );
677
- } else {
678
733
  console.log(
679
- "1. Java version requirement: cdxgen container image bundles Java 24 with maven 3.9 which might be incompatible. Try running cdxgen with the custom JDK11-based image `ghcr.io/cdxgen/cdxgen-java11:v13`.",
734
+ "3. Check if all required environment variables including any maven profile arguments are passed correctly to this tool.",
680
735
  );
681
736
  }
737
+ // The degradation is recorded before the fail-on-error handling
738
+ // below, so the run that stops here still reports why maven
739
+ // failed and what fixes it.
740
+ recordDegradation("jvm.maven.manifest-fallback", {
741
+ ecosystem: "java",
742
+ tool: "maven",
743
+ impact: "transitive-deps",
744
+ command: `${basename(mavenCmd)} ${mvnTreeArgs.join(" ")}`,
745
+ exitCode:
746
+ typeof result.status === "number" ? result.status : undefined,
747
+ detail:
748
+ "The maven dependency tree did not succeed, so at best the direct dependencies parsed from pom.xml are available.",
749
+ outputExcerpt: commandOutputText(result),
750
+ });
751
+ // Do not fall back to methods that can produce incomplete results when failOnError is set
752
+ deferFailOnError(options, {
753
+ ecosystem: "java",
754
+ tool: "maven",
755
+ exitCode:
756
+ typeof result.status === "number" ? result.status : undefined,
757
+ command: `${mavenCmd} ${mvnTreeArgs.join(" ")}`,
758
+ outputExcerpt: commandOutputText(result),
759
+ detail: "the maven dependency tree command failed",
760
+ });
682
761
  console.log(
683
- "2. Private dependencies cannot be downloaded: Check if any additional arguments must be passed to maven and set them via MVN_ARGS environment variable.",
762
+ "\nFalling back to parsing pom.xml files. Only direct dependencies would get included!",
684
763
  );
685
- console.log(
686
- "3. Check if all required environment variables including any maven profile arguments are passed correctly to this tool.",
764
+ thoughtLog(
765
+ "**MAVEN**: There appear to be build errors, so the SBOM will be incomplete.",
687
766
  );
688
- }
689
- // Do not fall back to methods that can produce incomplete results when failOnError is set
690
- options.failOnError && process.exit(1);
691
- console.log(
692
- "\nFalling back to parsing pom.xml files. Only direct dependencies would get included!",
693
- );
694
- thoughtLog(
695
- "**MAVEN**: There appear to be build errors, so the SBOM will be incomplete.",
696
- );
697
- const pomMap = parsePom(f);
698
- const dlist = pomMap?.dependencies || [];
699
- if (dlist.length) {
700
- pkgList = pkgList.concat(dlist);
701
- }
702
- } else {
703
- const treeFile = safeExistsSync(tempMvnTreeText)
704
- ? tempMvnTreeText
705
- : tempMvnTree;
706
- if (safeExistsSync(treeFile)) {
707
- const mvnTreeString = readFileSync(treeFile, {
708
- encoding: "utf-8",
709
- });
710
- const parsedList = treeFile.endsWith(".json")
711
- ? parseMavenTreeJson(mvnTreeString, f)
712
- : parseMavenTree(mvnTreeString, f);
713
- const dlist = parsedList.pkgList || [];
714
- const tmpParentComponent = dlist.splice(0, 1)[0];
715
- if (tmpParentComponent) {
716
- tmpParentComponent.type = "application";
717
- }
767
+ const pomMap = parsePom(f);
768
+ const dlist = pomMap?.dependencies || [];
718
769
  if (dlist.length) {
719
770
  pkgList = pkgList.concat(dlist);
720
- if (dlist.length > 1) {
721
- thoughtLog(`Obtained ${dlist.length} components from maven.`);
722
- } else {
771
+ }
772
+ } else {
773
+ const treeFile = safeExistsSync(tempMvnTreeText)
774
+ ? tempMvnTreeText
775
+ : tempMvnTree;
776
+ if (safeExistsSync(treeFile)) {
777
+ const mvnTreeString = readFileSync(treeFile, {
778
+ encoding: "utf-8",
779
+ });
780
+ const parsedList = treeFile.endsWith(".json")
781
+ ? parseMavenTreeJson(mvnTreeString, f)
782
+ : parseMavenTree(mvnTreeString, f);
783
+ const dlist = parsedList.pkgList || [];
784
+ const tmpParentComponent = dlist.splice(0, 1)[0];
785
+ if (tmpParentComponent) {
786
+ tmpParentComponent.type = "application";
787
+ }
788
+ if (dlist.length) {
789
+ pkgList = pkgList.concat(dlist);
790
+ if (dlist.length > 1) {
791
+ thoughtLog(
792
+ `Obtained ${dlist.length} components from maven.`,
793
+ );
794
+ } else {
795
+ thoughtLog(
796
+ `"Received very few components from the maven dependency tree command for ${basePath}."`,
797
+ );
798
+ }
799
+ }
800
+ // Retain the parent hierarchy
801
+ if (!tmpParentComponent) {
723
802
  thoughtLog(
724
- `"Received very few components from the maven dependency tree command for ${basePath}."`,
803
+ `No parseable components were found after executing '${basename(mavenCmd)}'.`,
725
804
  );
805
+ } else if (!Object.keys(parentComponent).length) {
806
+ parentComponent = tmpParentComponent;
807
+ parentComponent.components = [];
808
+ } else {
809
+ parentComponent.components.push(tmpParentComponent);
726
810
  }
727
- }
728
- // Retain the parent hierarchy
729
- if (!tmpParentComponent) {
730
- thoughtLog(
731
- `No parseable components were found after executing '${basename(mavenCmd)}'.`,
732
- );
733
- } else if (!Object.keys(parentComponent).length) {
734
- parentComponent = tmpParentComponent;
735
- parentComponent.components = [];
736
- } else {
737
- parentComponent.components.push(tmpParentComponent);
738
- }
739
- if (parsedList?.dependenciesList?.length) {
740
- dependencies = mergeDependencies(
741
- dependencies,
742
- parsedList.dependenciesList,
743
- tmpParentComponent,
744
- );
745
- } else {
746
- if (dlist?.length) {
747
- thoughtLog(
748
- `Hmm, I didn't find any dependencies after executing '${basename(mavenCmd)}'. However, I did get ${dlist.length} components, which is confusing.`,
811
+ if (parsedList?.dependenciesList?.length) {
812
+ dependencies = mergeDependencies(
813
+ dependencies,
814
+ parsedList.dependenciesList,
815
+ tmpParentComponent,
749
816
  );
817
+ } else {
818
+ if (dlist?.length) {
819
+ thoughtLog(
820
+ `Hmm, I didn't find any dependencies after executing '${basename(mavenCmd)}'. However, I did get ${dlist.length} components, which is confusing.`,
821
+ );
822
+ }
750
823
  }
751
824
  }
752
825
  }
753
- }
754
- } finally {
755
- if (!DEBUG_MODE && tempDir?.startsWith(tempRoot)) {
756
- safeRmSync(tempDir, { recursive: true, force: true });
826
+ } finally {
827
+ if (!DEBUG_MODE && tempDir?.startsWith(tempRoot)) {
828
+ safeRmSync(tempDir, { recursive: true, force: true });
829
+ }
757
830
  }
758
831
  }
759
- }
760
- } // for
761
- // Locate and parse all bom.json files from the maven plugin
762
- if (!useMavenDepsTree) {
763
- for (const abjson of bomJsonFiles) {
764
- let bomJsonObj;
765
- try {
766
- if (DEBUG_MODE) {
767
- console.log(`Extracting data from generated bom file ${abjson}`);
768
- }
769
- bomJsonObj = JSON.parse(
770
- readFileSync(abjson, {
771
- encoding: "utf-8",
772
- }),
773
- );
774
- if (bomJsonObj) {
775
- if (
776
- !tools &&
777
- bomJsonObj.metadata &&
778
- bomJsonObj.metadata.tools &&
779
- (Array.isArray(bomJsonObj.metadata.tools) ||
780
- bomJsonObj.metadata.tools.components ||
781
- bomJsonObj.metadata.tools.services)
782
- ) {
783
- tools = bomJsonObj.metadata.tools;
832
+ } // for
833
+ // Locate and parse all bom.json files from the maven plugin
834
+ if (!useMavenDepsTree) {
835
+ for (const abjson of bomJsonFiles) {
836
+ let bomJsonObj;
837
+ try {
838
+ if (DEBUG_MODE) {
839
+ console.log(`Extracting data from generated bom file ${abjson}`);
784
840
  }
785
- const toolRefs = extractToolRefs(
786
- bomJsonObj?.metadata?.tools,
787
- (tool) => tool?.name !== "cdxgen",
841
+ bomJsonObj = JSON.parse(
842
+ readFileSync(abjson, {
843
+ encoding: "utf-8",
844
+ }),
788
845
  );
789
- if (
790
- bomJsonObj.metadata?.component &&
791
- !Object.keys(parentComponent).length
792
- ) {
793
- parentComponent = bomJsonObj.metadata.component;
794
- options.parentComponent = parentComponent;
795
- }
796
- if (bomJsonObj.components) {
797
- // Inject evidence into the components. #994
798
- if (options.specVersion >= 1.5) {
799
- // maven would usually generate a target directory closest to the pom.xml
800
- // I am sure there would be cases where this assumption is not true :)
801
- const srcPomFile = join(dirname(abjson), "..", "pom.xml");
802
- for (const acomp of bomJsonObj.components) {
803
- if (!acomp.evidence) {
804
- acomp.evidence = {
805
- identity: {
806
- field: "purl",
807
- confidence: 0.8,
808
- methods: [
809
- {
810
- technique: "manifest-analysis",
811
- confidence: 0.8,
812
- value: srcPomFile,
813
- },
814
- ],
815
- },
816
- };
817
- }
818
- if (!acomp.properties) {
819
- acomp.properties = [];
846
+ if (bomJsonObj) {
847
+ if (
848
+ !tools &&
849
+ bomJsonObj.metadata &&
850
+ bomJsonObj.metadata.tools &&
851
+ (Array.isArray(bomJsonObj.metadata.tools) ||
852
+ bomJsonObj.metadata.tools.components ||
853
+ bomJsonObj.metadata.tools.services)
854
+ ) {
855
+ tools = bomJsonObj.metadata.tools;
856
+ }
857
+ const toolRefs = extractToolRefs(
858
+ bomJsonObj?.metadata?.tools,
859
+ (tool) => tool?.name !== "cdxgen",
860
+ );
861
+ if (
862
+ bomJsonObj.metadata?.component &&
863
+ !Object.keys(parentComponent).length
864
+ ) {
865
+ parentComponent = bomJsonObj.metadata.component;
866
+ options.parentComponent = parentComponent;
867
+ }
868
+ if (bomJsonObj.components) {
869
+ // Inject evidence into the components. #994
870
+ if (options.specVersion >= 1.5) {
871
+ // maven would usually generate a target directory closest to the pom.xml
872
+ // I am sure there would be cases where this assumption is not true :)
873
+ const srcPomFile = join(dirname(abjson), "..", "pom.xml");
874
+ for (const acomp of bomJsonObj.components) {
875
+ if (!acomp.evidence) {
876
+ acomp.evidence = {
877
+ identity: {
878
+ field: "purl",
879
+ confidence: 0.8,
880
+ methods: [
881
+ {
882
+ technique: "manifest-analysis",
883
+ confidence: 0.8,
884
+ value: srcPomFile,
885
+ },
886
+ ],
887
+ },
888
+ };
889
+ }
890
+ if (!acomp.properties) {
891
+ acomp.properties = [];
892
+ }
893
+ acomp.properties.push({
894
+ name: "internal:SrcFile",
895
+ value: srcPomFile,
896
+ });
897
+ attachIdentityTools(acomp, toolRefs);
820
898
  }
821
- acomp.properties.push({
822
- name: "internal:SrcFile",
823
- value: srcPomFile,
824
- });
825
- attachIdentityTools(acomp, toolRefs);
826
899
  }
900
+ pkgList = pkgList.concat(bomJsonObj.components);
901
+ }
902
+ if (bomJsonObj.dependencies) {
903
+ dependencies = mergeDependencies(
904
+ dependencies,
905
+ bomJsonObj.dependencies,
906
+ parentComponent,
907
+ );
827
908
  }
828
- pkgList = pkgList.concat(bomJsonObj.components);
829
909
  }
830
- if (bomJsonObj.dependencies) {
831
- dependencies = mergeDependencies(
832
- dependencies,
833
- bomJsonObj.dependencies,
834
- parentComponent,
835
- );
910
+ } catch (err) {
911
+ if (options.failOnError || DEBUG_MODE) {
912
+ console.log(err);
836
913
  }
837
- }
838
- } catch (err) {
839
- if (options.failOnError || DEBUG_MODE) {
840
- console.log(err);
841
- options.failOnError && process.exit(1);
914
+ deferFailOnError(options, {
915
+ ecosystem: "java",
916
+ tool: "maven",
917
+ detail:
918
+ "reading the BOM generated by the cyclonedx maven plugin failed",
919
+ });
842
920
  }
843
921
  }
844
922
  }
845
- }
846
- if (possible_misses) {
847
- if (gradleFiles.length) {
848
- console.log(
849
- "Is this a gradle project? Try running cdxgen with `-t gradle`.",
850
- );
851
- } else if (!DEBUG_MODE) {
852
- console.warn(
853
- "Multiple errors occurred while building this project with maven. The SBOM is therefore incomplete!",
854
- );
923
+ if (possible_misses) {
924
+ if (gradleFiles.length) {
925
+ console.log(
926
+ "Is this a gradle project? Try running cdxgen with `-t gradle`.",
927
+ );
928
+ } else if (!DEBUG_MODE) {
929
+ console.warn(
930
+ "Multiple errors occurred while building this project with maven. The SBOM is therefore incomplete!",
931
+ );
932
+ }
855
933
  }
856
934
  }
857
- }
858
- const allProjects = [];
859
- const allProjectsAddedPurls = [];
860
- const rootDependsOn = new Set();
861
- const gradleModules = new Map();
862
- // Determine the root path for gradle
863
- // Fixes gradle invocation for microservices-demo
864
- let gradleRootPath = path;
865
- if (
866
- gradleFiles?.length &&
867
- !safeExistsSync(join(path, "settings.gradle")) &&
868
- !safeExistsSync(join(path, "settings.gradle.kts")) &&
869
- !safeExistsSync(join(path, "build.gradle")) &&
870
- !safeExistsSync(join(path, "build.gradle.kts"))
871
- ) {
872
- gradleRootPath = dirname(gradleFiles[0]);
873
- }
874
- if (safeExistsSync(join(gradleRootPath, "gradle.properties"))) {
875
- thoughtLog(
876
- "Hmm, there is a gradle.properties file. Do we need any private modules or custom JVM arguments for this project 🤔?",
877
- );
878
- }
879
- // Execute gradle properties
880
- if (
881
- gradleFiles?.length &&
882
- isPackageManagerAllowed(
883
- "gradle",
884
- ["maven", "bazel", "sbt", "mill"],
885
- options,
886
- )
887
- ) {
888
- const wrapperPropPath = join(
889
- gradleRootPath,
890
- "gradle",
891
- "wrapper",
892
- "gradle-wrapper.properties",
893
- );
894
- if (safeExistsSync(wrapperPropPath)) {
895
- try {
896
- const propContent = readFileSync(wrapperPropPath, "utf-8");
897
- const distUrlLine = propContent
898
- .split("\n")
899
- .find((l) => l.startsWith("distributionUrl="));
900
- if (distUrlLine) {
901
- const distributionUrl = distUrlLine
902
- .split("=")[1]
903
- .trim()
904
- .replace(/\\:/g, ":");
905
- const versionMatch = distributionUrl.match(
906
- /gradle-(.*?)-(bin|all)\.zip/,
907
- );
908
- const gradleVersion = versionMatch ? versionMatch[1] : "unknown";
935
+ const allProjects = [];
936
+ const allProjectsAddedPurls = [];
937
+ const rootDependsOn = new Set();
938
+ const gradleModules = new Map();
939
+ // Determine the root path for gradle
940
+ // Fixes gradle invocation for microservices-demo
941
+ let gradleRootPath = path;
942
+ if (
943
+ gradleFiles?.length &&
944
+ !safeExistsSync(join(path, "settings.gradle")) &&
945
+ !safeExistsSync(join(path, "settings.gradle.kts")) &&
946
+ !safeExistsSync(join(path, "build.gradle")) &&
947
+ !safeExistsSync(join(path, "build.gradle.kts"))
948
+ ) {
949
+ gradleRootPath = dirname(gradleFiles[0]);
950
+ }
951
+ if (safeExistsSync(join(gradleRootPath, "gradle.properties"))) {
952
+ thoughtLog(
953
+ "Hmm, there is a gradle.properties file. Do we need any private modules or custom JVM arguments for this project 🤔?",
954
+ );
955
+ }
956
+ // Execute gradle properties
957
+ if (
958
+ gradleFiles?.length &&
959
+ isPackageManagerAllowed(
960
+ "gradle",
961
+ ["maven", "bazel", "sbt", "mill"],
962
+ options,
963
+ )
964
+ ) {
965
+ const wrapperInfo = readGradleWrapperVersion(gradleRootPath);
966
+ if (wrapperInfo) {
967
+ try {
968
+ const { version: gradleVersion, distributionUrl } = wrapperInfo;
909
969
  thoughtLog(
910
970
  `Found Gradle wrapper with distributionUrl: ${distributionUrl}, version: ${gradleVersion}`,
911
971
  );
@@ -921,20 +981,14 @@ export async function createJavaBom(path, options) {
921
981
  }
922
982
  }
923
983
  if (!toolsArr.find((c) => c.name === "gradle")) {
924
- const distShaLine = propContent
925
- .split("\n")
926
- .find((l) => l.startsWith("distributionSha256Sum="));
927
984
  let hashes;
928
- if (distShaLine) {
929
- const sha256Sum = distShaLine.split("=")[1].trim();
930
- if (sha256Sum) {
931
- hashes = [
932
- {
933
- alg: "SHA-256",
934
- content: sha256Sum,
935
- },
936
- ];
937
- }
985
+ if (wrapperInfo.distributionSha256Sum) {
986
+ hashes = [
987
+ {
988
+ alg: "SHA-256",
989
+ content: wrapperInfo.distributionSha256Sum,
990
+ },
991
+ ];
938
992
  }
939
993
  const gradleComponent = {
940
994
  type: "application",
@@ -965,875 +1019,914 @@ export async function createJavaBom(path, options) {
965
1019
  } else {
966
1020
  tools.components = toolsArr;
967
1021
  }
1022
+ } catch (_err) {
1023
+ // ignore
968
1024
  }
969
- } catch (_err) {
970
- // ignore
971
1025
  }
972
- }
973
- let includedBuilds = [];
974
- let allProjectsStr = [];
975
- if (readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")) {
976
- includedBuilds = readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")
977
- .split(",")
978
- .map((b) => (!b.startsWith(":") ? `:${b}` : b));
979
- }
980
- const resolveGradleDistribution = isFeatureEnabled(
981
- options,
982
- "resolve-gradle-distribution",
983
- );
984
- const gradleInitArgs = readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")
985
- ? []
986
- : ["--init-script", GRADLE_INIT_SCRIPT];
987
- if (resolveGradleDistribution && gradleInitArgs.length) {
988
- gradleInitArgs.push("-PcdxgenResolveDistribution=true");
989
- }
990
- parallelPropTaskOut = executeParallelGradleProperties(
991
- gradleRootPath,
992
- [null].concat(includedBuilds),
993
- gradleInitArgs,
994
- );
995
- if (readEnvironmentVariable("GRADLE_INCLUDED_BUILDS") === undefined) {
996
- const outputLines = parallelPropTaskOut.split("\n");
997
- for (const [_i, line] of outputLines.entries()) {
998
- if (line.startsWith("Root project '") || line.startsWith("Project '")) {
999
- break;
1000
- }
1001
- if (line.startsWith("<CDXGEN:includedBuild>")) {
1002
- const includedBuild = line.split(">");
1003
- if (!includedBuilds.includes(includedBuild[1].trim())) {
1004
- includedBuilds.push(includedBuild[1].trim());
1005
- }
1006
- }
1026
+ let includedBuilds = [];
1027
+ let allProjectsStr = [];
1028
+ if (readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")) {
1029
+ includedBuilds = readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")
1030
+ .split(",")
1031
+ .map((b) => (!b.startsWith(":") ? `:${b}` : b));
1007
1032
  }
1008
- if (includedBuilds.length > 0) {
1009
- thoughtLog(
1010
- `Wait, this gradle project uses composite builds. I must carefully process these ${includedBuilds.length} projects, in addition to the root.`,
1011
- );
1012
- if (DEBUG_MODE) {
1013
- console.log(`Composite builds: ${includedBuilds.join(" ").trim()}.`);
1014
- }
1015
- parallelPropTaskOut = parallelPropTaskOut.concat(
1016
- "\n",
1017
- executeParallelGradleProperties(gradleRootPath, includedBuilds),
1018
- );
1019
- }
1020
- }
1021
- const splitPropTaskOut = splitOutputByGradleProjects(parallelPropTaskOut, [
1022
- "properties",
1023
- ]);
1024
- for (const [key, propTaskOut] of splitPropTaskOut.entries()) {
1025
- const retMap = parseGradleProperties(propTaskOut);
1026
- const rootProject = retMap.rootProject;
1027
- if (rootProject) {
1028
- const rootComponent = await buildObjectForGradleModule(
1029
- rootProject,
1030
- retMap.metadata,
1031
- );
1032
- if (!includedBuilds.includes(key)) {
1033
- parentComponent = rootComponent;
1034
- }
1035
- gradleModules.set(key, rootComponent);
1036
- if (!allProjectsAddedPurls.includes(rootComponent["purl"])) {
1037
- allProjects.push(rootComponent);
1038
- rootDependsOn.add(rootComponent["bom-ref"]);
1039
- allProjectsAddedPurls.push(rootComponent["purl"]);
1040
- }
1041
- allProjectsStr = allProjectsStr.concat(retMap.projects);
1042
- }
1043
- }
1044
- // Get the sub-project properties and set the root dependencies
1045
- if (allProjectsStr?.length) {
1046
- const modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
1047
- ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
1048
- : [];
1049
- if (modulesToSkip.length) {
1050
- thoughtLog(
1051
- `Good news. I know there are ${allProjectsStr.length} gradle modules at ${gradleRootPath}. I must skip ${modulesToSkip.length} out of these.`,
1052
- );
1033
+ const resolveGradleDistribution = isFeatureEnabled(
1034
+ options,
1035
+ "resolve-gradle-distribution",
1036
+ );
1037
+ const gradleInitArgs = readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")
1038
+ ? []
1039
+ : ["--init-script", GRADLE_INIT_SCRIPT];
1040
+ if (resolveGradleDistribution && gradleInitArgs.length) {
1041
+ gradleInitArgs.push("-PcdxgenResolveDistribution=true");
1053
1042
  }
1054
1043
  parallelPropTaskOut = executeParallelGradleProperties(
1055
1044
  gradleRootPath,
1056
- allProjectsStr.filter((module) => !modulesToSkip.includes(module)),
1045
+ [null].concat(includedBuilds),
1046
+ gradleInitArgs,
1057
1047
  );
1048
+ if (readEnvironmentVariable("GRADLE_INCLUDED_BUILDS") === undefined) {
1049
+ const outputLines = parallelPropTaskOut.split("\n");
1050
+ for (const [_i, line] of outputLines.entries()) {
1051
+ if (
1052
+ line.startsWith("Root project '") ||
1053
+ line.startsWith("Project '")
1054
+ ) {
1055
+ break;
1056
+ }
1057
+ if (line.startsWith("<CDXGEN:includedBuild>")) {
1058
+ const includedBuild = line.split(">");
1059
+ if (!includedBuilds.includes(includedBuild[1].trim())) {
1060
+ includedBuilds.push(includedBuild[1].trim());
1061
+ }
1062
+ }
1063
+ }
1064
+ if (includedBuilds.length > 0) {
1065
+ thoughtLog(
1066
+ `Wait, this gradle project uses composite builds. I must carefully process these ${includedBuilds.length} projects, in addition to the root.`,
1067
+ );
1068
+ if (DEBUG_MODE) {
1069
+ console.log(
1070
+ `Composite builds: ${includedBuilds.join(" ").trim()}.`,
1071
+ );
1072
+ }
1073
+ parallelPropTaskOut = parallelPropTaskOut.concat(
1074
+ "\n",
1075
+ executeParallelGradleProperties(gradleRootPath, includedBuilds),
1076
+ );
1077
+ }
1078
+ }
1058
1079
  const splitPropTaskOut = splitOutputByGradleProjects(
1059
1080
  parallelPropTaskOut,
1060
1081
  ["properties"],
1061
1082
  );
1062
-
1063
- for (const subProject of allProjectsStr) {
1064
- const retMap = parseGradleProperties(
1065
- splitPropTaskOut.get(subProject),
1066
- subProject,
1067
- );
1068
- const rootSubProject = retMap.rootProject;
1069
- if (rootSubProject) {
1070
- const rootSubProjectObj = await buildObjectForGradleModule(
1071
- rootSubProject === "root" ? subProject : rootSubProject,
1083
+ for (const [key, propTaskOut] of splitPropTaskOut.entries()) {
1084
+ const retMap = parseGradleProperties(propTaskOut);
1085
+ const rootProject = retMap.rootProject;
1086
+ if (rootProject) {
1087
+ const rootComponent = await buildObjectForGradleModule(
1088
+ rootProject,
1072
1089
  retMap.metadata,
1073
1090
  );
1074
- if (!allProjectsAddedPurls.includes(rootSubProjectObj["purl"])) {
1075
- allProjects.push(rootSubProjectObj);
1076
- rootDependsOn.add(rootSubProjectObj["bom-ref"]);
1077
- allProjectsAddedPurls.push(rootSubProjectObj["purl"]);
1091
+ if (!includedBuilds.includes(key)) {
1092
+ parentComponent = rootComponent;
1093
+ }
1094
+ gradleModules.set(key, rootComponent);
1095
+ if (!allProjectsAddedPurls.includes(rootComponent["purl"])) {
1096
+ allProjects.push(rootComponent);
1097
+ rootDependsOn.add(rootComponent["bom-ref"]);
1098
+ allProjectsAddedPurls.push(rootComponent["purl"]);
1078
1099
  }
1079
- gradleModules.set(subProject, rootSubProjectObj);
1100
+ allProjectsStr = allProjectsStr.concat(retMap.projects);
1080
1101
  }
1081
1102
  }
1082
- // Bug #317 fix
1083
- parentComponent.components = allProjects.flatMap((s) => {
1084
- delete s.qualifiers;
1085
- delete s.evidence;
1086
- return s;
1087
- });
1088
- dependencies.push({
1089
- ref: parentComponent["bom-ref"],
1090
- dependsOn: [...rootDependsOn].sort(),
1091
- });
1092
- }
1093
- }
1094
- if (
1095
- gradleFiles?.length &&
1096
- options.installDeps &&
1097
- isPackageManagerAllowed(
1098
- "gradle",
1099
- ["maven", "bazel", "sbt", "mill"],
1100
- options,
1101
- )
1102
- ) {
1103
- allProjects.push(parentComponent);
1104
- const gradleCmd = getGradleCommand(gradleRootPath, null);
1105
- const gradleDepTask = readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1106
- ? readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1107
- : "dependencies";
1108
-
1109
- const gradleSubCommands = [];
1110
- let modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
1111
- ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
1112
- : [];
1113
- if (readEnvironmentVariable("GRADLE_SKIP_MODULE_DEPENDENCIES")) {
1114
- modulesToSkip = modulesToSkip.concat(
1115
- readEnvironmentVariable("GRADLE_SKIP_MODULE_DEPENDENCIES").split(","),
1116
- );
1117
- }
1118
- if (!modulesToSkip.includes("root")) {
1119
- gradleSubCommands.push(gradleDepTask);
1120
- }
1121
- for (const [key, sp] of gradleModules) {
1122
- //create single command for dependencies tasks on all subprojects
1123
- if (sp.purl !== parentComponent.purl && !modulesToSkip.includes(key)) {
1124
- gradleSubCommands.push(`${key}:${gradleDepTask}`);
1125
- }
1126
- }
1127
- const gradleArguments = buildGradleCommandArguments(
1128
- readEnvironmentVariable("GRADLE_ARGS")
1129
- ? readEnvironmentVariable("GRADLE_ARGS").split(" ")
1130
- : [],
1131
- gradleSubCommands,
1132
- readEnvironmentVariable("GRADLE_ARGS_DEPENDENCIES")
1133
- ? readEnvironmentVariable("GRADLE_ARGS_DEPENDENCIES").split(" ")
1134
- : [],
1135
- gradleCmd.length,
1136
- );
1137
- const allOutputs = [];
1138
- for (const gradleArg of gradleArguments) {
1139
- if (DEBUG_MODE) {
1140
- console.log(
1141
- `Executing ${gradleCmd} with arguments ${gradleArg.join(" ").substring(0, 150)}... in ${gradleRootPath}`,
1142
- );
1143
- }
1144
- thoughtLog(
1145
- `Let's invoke '${basename(gradleCmd)}' with the arguments '${gradleArg.join(" ").substring(0, 100)} ...'.`,
1146
- );
1147
- const finalGradleArg = gradleArg.includes("--info")
1148
- ? gradleArg
1149
- : gradleArg.concat(["--info"]);
1150
- const sresult = safeSpawnSync(gradleCmd, finalGradleArg, {
1151
- cwd: gradleRootPath,
1152
- shell: isWin,
1153
- });
1154
- if (sresult.status !== 0 || sresult.error) {
1155
- if (options.failOnError || DEBUG_MODE) {
1156
- console.error(sresult.stdout, sresult.stderr);
1103
+ // Get the sub-project properties and set the root dependencies
1104
+ if (allProjectsStr?.length) {
1105
+ const modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
1106
+ ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
1107
+ : [];
1108
+ if (modulesToSkip.length) {
1109
+ thoughtLog(
1110
+ `Good news. I know there are ${allProjectsStr.length} gradle modules at ${gradleRootPath}. I must skip ${modulesToSkip.length} out of these.`,
1111
+ );
1157
1112
  }
1158
- options.failOnError && process.exit(1);
1159
- }
1160
- if (sresult.stdout !== null) {
1161
- allOutputs.push(sresult.stdout);
1162
- }
1163
- }
1164
- const sstdout = allOutputs.join("\n");
1165
- if (sstdout) {
1166
- const cmdOutput = Buffer.from(sstdout).toString();
1167
- const perProjectOutput = splitOutputByGradleProjects(cmdOutput, [
1168
- gradleDepTask,
1169
- ]);
1170
- for (const key of gradleModules.keys()) {
1171
- const parsedList = await parseGradleDep(
1172
- perProjectOutput.has(key) ? perProjectOutput.get(key) : "",
1173
- key,
1174
- gradleModules,
1113
+ parallelPropTaskOut = executeParallelGradleProperties(
1175
1114
  gradleRootPath,
1115
+ allProjectsStr.filter((module) => !modulesToSkip.includes(module)),
1176
1116
  );
1177
- const dlist = parsedList.pkgList;
1178
- if (parsedList?.dependenciesList?.length) {
1179
- dependencies = mergeDependencies(
1180
- dependencies,
1181
- parsedList.dependenciesList,
1182
- parentComponent,
1117
+ const splitPropTaskOut = splitOutputByGradleProjects(
1118
+ parallelPropTaskOut,
1119
+ ["properties"],
1120
+ );
1121
+
1122
+ for (const subProject of allProjectsStr) {
1123
+ const retMap = parseGradleProperties(
1124
+ splitPropTaskOut.get(subProject),
1125
+ subProject,
1183
1126
  );
1184
- } else {
1185
- if (dlist?.length) {
1186
- thoughtLog(
1187
- `Hmm, I didn't find any dependencies after executing '${basename(gradleCmd)}' for the project ${key}. However, I did get ${dlist.length} components, which is confusing.`,
1127
+ const rootSubProject = retMap.rootProject;
1128
+ if (rootSubProject) {
1129
+ const rootSubProjectObj = await buildObjectForGradleModule(
1130
+ rootSubProject === "root" ? subProject : rootSubProject,
1131
+ retMap.metadata,
1188
1132
  );
1133
+ if (!allProjectsAddedPurls.includes(rootSubProjectObj["purl"])) {
1134
+ allProjects.push(rootSubProjectObj);
1135
+ rootDependsOn.add(rootSubProjectObj["bom-ref"]);
1136
+ allProjectsAddedPurls.push(rootSubProjectObj["purl"]);
1137
+ }
1138
+ gradleModules.set(subProject, rootSubProjectObj);
1189
1139
  }
1190
1140
  }
1191
- if (dlist?.length) {
1192
- pkgList = pkgList.concat(dlist);
1193
- }
1141
+ // Bug #317 fix
1142
+ parentComponent.components = allProjects.flatMap((s) => {
1143
+ delete s.qualifiers;
1144
+ delete s.evidence;
1145
+ return s;
1146
+ });
1147
+ dependencies.push({
1148
+ ref: parentComponent["bom-ref"],
1149
+ dependsOn: [...rootDependsOn].sort(),
1150
+ });
1194
1151
  }
1195
1152
  }
1196
- if (pkgList.length) {
1197
- const fileToUrlMap = parseGradleInfoLogsForUrls(sstdout);
1198
- // When the `resolve-gradle-distribution` feature flag is enabled, the init script
1199
- // probes each repository for the actual artifact and emits the resolved URL keyed
1200
- // by `group:name:version`. This is more accurate than guessing the first repository.
1201
- const resolvedDistributions =
1202
- parseGradleResolvedDistributions(parallelPropTaskOut);
1153
+ if (
1154
+ gradleFiles?.length &&
1155
+ options.installDeps &&
1156
+ isPackageManagerAllowed(
1157
+ "gradle",
1158
+ ["maven", "bazel", "sbt", "mill"],
1159
+ options,
1160
+ )
1161
+ ) {
1162
+ allProjects.push(parentComponent);
1163
+ const gradleCmd = getGradleCommand(gradleRootPath, null);
1164
+ const gradleDepTask = readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1165
+ ? readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1166
+ : "dependencies";
1203
1167
 
1204
- for (const p of pkgList) {
1205
- if (!p.externalReferences) {
1206
- p.externalReferences = [];
1168
+ const gradleSubCommands = [];
1169
+ let modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
1170
+ ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
1171
+ : [];
1172
+ if (readEnvironmentVariable("GRADLE_SKIP_MODULE_DEPENDENCIES")) {
1173
+ modulesToSkip = modulesToSkip.concat(
1174
+ readEnvironmentVariable("GRADLE_SKIP_MODULE_DEPENDENCIES").split(","),
1175
+ );
1176
+ }
1177
+ if (!modulesToSkip.includes("root")) {
1178
+ gradleSubCommands.push(gradleDepTask);
1179
+ }
1180
+ for (const [key, sp] of gradleModules) {
1181
+ //create single command for dependencies tasks on all subprojects
1182
+ if (sp.purl !== parentComponent.purl && !modulesToSkip.includes(key)) {
1183
+ gradleSubCommands.push(`${key}:${gradleDepTask}`);
1207
1184
  }
1208
- if (!p.externalReferences.find((r) => r.type === "distribution")) {
1209
- const groupPath = p.group.replace(/\./g, "/");
1210
- const matchPath = `${groupPath}/${p.name}/${p.version}/`;
1211
- let matchedUrl = Object.values(fileToUrlMap).find((url) =>
1212
- url.includes(matchPath),
1185
+ }
1186
+ const gradleArguments = buildGradleCommandArguments(
1187
+ readEnvironmentVariable("GRADLE_ARGS")
1188
+ ? readEnvironmentVariable("GRADLE_ARGS").split(" ")
1189
+ : [],
1190
+ gradleSubCommands,
1191
+ readEnvironmentVariable("GRADLE_ARGS_DEPENDENCIES")
1192
+ ? readEnvironmentVariable("GRADLE_ARGS_DEPENDENCIES").split(" ")
1193
+ : [],
1194
+ gradleCmd.length,
1195
+ );
1196
+ const allOutputs = [];
1197
+ for (const gradleArg of gradleArguments) {
1198
+ if (DEBUG_MODE) {
1199
+ console.log(
1200
+ `Executing ${gradleCmd} with arguments ${gradleArg.join(" ").substring(0, 150)}... in ${gradleRootPath}`,
1213
1201
  );
1214
- if (!matchedUrl) {
1215
- matchedUrl =
1216
- resolvedDistributions[`${p.group}:${p.name}:${p.version}`];
1217
- }
1218
- if (matchedUrl) {
1219
- p.externalReferences.push({
1220
- type: "distribution",
1221
- url: matchedUrl,
1222
- });
1202
+ }
1203
+ thoughtLog(
1204
+ `Let's invoke '${basename(gradleCmd)}' with the arguments '${gradleArg.join(" ").substring(0, 100)} ...'.`,
1205
+ );
1206
+ const finalGradleArg = gradleArg.includes("--info")
1207
+ ? gradleArg
1208
+ : gradleArg.concat(["--info"]);
1209
+ const sresult = safeSpawnSync(gradleCmd, finalGradleArg, {
1210
+ cwd: gradleRootPath,
1211
+ shell: isWin,
1212
+ });
1213
+ if (sresult.status !== 0 || sresult.error) {
1214
+ if (options.failOnError || DEBUG_MODE) {
1215
+ console.error(sresult.stdout, sresult.stderr);
1223
1216
  }
1217
+ recordGradleInvocationFailure(sresult, {
1218
+ command: `${gradleCmd} ${finalGradleArg.join(" ")}`,
1219
+ });
1220
+ deferFailOnError(options, {
1221
+ ecosystem: "java",
1222
+ tool: "gradle",
1223
+ exitCode:
1224
+ typeof sresult.status === "number" ? sresult.status : undefined,
1225
+ command: `${gradleCmd} ${finalGradleArg.join(" ")}`,
1226
+ detail: "the gradle dependencies task failed",
1227
+ });
1228
+ }
1229
+ if (sresult.stdout !== null) {
1230
+ allOutputs.push(sresult.stdout);
1224
1231
  }
1225
1232
  }
1226
- if (parentComponent.components?.length) {
1227
- for (const subProj of parentComponent.components) {
1228
- pkgList = pkgList.filter(
1229
- (pkg) =>
1230
- pkg["bom-ref"] !== subProj["bom-ref"] &&
1231
- pkg["bom-ref"] !== parentComponent["bom-ref"],
1233
+ const sstdout = allOutputs.join("\n");
1234
+ if (sstdout) {
1235
+ const cmdOutput = Buffer.from(sstdout).toString();
1236
+ const perProjectOutput = splitOutputByGradleProjects(cmdOutput, [
1237
+ gradleDepTask,
1238
+ ]);
1239
+ for (const key of gradleModules.keys()) {
1240
+ const parsedList = await parseGradleDep(
1241
+ perProjectOutput.has(key) ? perProjectOutput.get(key) : "",
1242
+ key,
1243
+ gradleModules,
1244
+ gradleRootPath,
1232
1245
  );
1246
+ const dlist = parsedList.pkgList;
1247
+ if (parsedList?.dependenciesList?.length) {
1248
+ dependencies = mergeDependencies(
1249
+ dependencies,
1250
+ parsedList.dependenciesList,
1251
+ parentComponent,
1252
+ );
1253
+ } else {
1254
+ if (dlist?.length) {
1255
+ thoughtLog(
1256
+ `Hmm, I didn't find any dependencies after executing '${basename(gradleCmd)}' for the project ${key}. However, I did get ${dlist.length} components, which is confusing.`,
1257
+ );
1258
+ }
1259
+ }
1260
+ if (dlist?.length) {
1261
+ pkgList = pkgList.concat(dlist);
1262
+ }
1233
1263
  }
1234
1264
  }
1235
- thoughtLog(
1236
- `Obtained ${pkgList.length} components by executing the '${basename(gradleCmd)}' command.`,
1237
- );
1238
- if (DEBUG_MODE) {
1239
- console.log("Obtained", pkgList.length, "from this gradle project.");
1240
- }
1241
- } else {
1242
- thoughtLog(
1243
- "**GRADLE:** SBOM is incomplete. I recommend troubleshooting the issue to improve the BOM precision.",
1244
- );
1245
- if (!DEBUG_MODE) {
1246
- console.log(
1247
- "No packages found. Set the environment variable 'CDXGEN_DEBUG_MODE=debug' to troubleshoot any gradle related errors.",
1265
+ if (pkgList.length) {
1266
+ const fileToUrlMap = parseGradleInfoLogsForUrls(sstdout);
1267
+ // When the `resolve-gradle-distribution` feature flag is enabled, the init script
1268
+ // probes each repository for the actual artifact and emits the resolved URL keyed
1269
+ // by `group:name:version`. This is more accurate than guessing the first repository.
1270
+ const resolvedDistributions =
1271
+ parseGradleResolvedDistributions(parallelPropTaskOut);
1272
+
1273
+ for (const p of pkgList) {
1274
+ if (!p.externalReferences) {
1275
+ p.externalReferences = [];
1276
+ }
1277
+ if (!p.externalReferences.find((r) => r.type === "distribution")) {
1278
+ const groupPath = p.group.replace(/\./g, "/");
1279
+ const matchPath = `${groupPath}/${p.name}/${p.version}/`;
1280
+ let matchedUrl = Object.values(fileToUrlMap).find((url) =>
1281
+ url.includes(matchPath),
1282
+ );
1283
+ if (!matchedUrl) {
1284
+ matchedUrl =
1285
+ resolvedDistributions[`${p.group}:${p.name}:${p.version}`];
1286
+ }
1287
+ if (matchedUrl) {
1288
+ p.externalReferences.push({
1289
+ type: "distribution",
1290
+ url: matchedUrl,
1291
+ });
1292
+ }
1293
+ }
1294
+ }
1295
+ if (parentComponent.components?.length) {
1296
+ for (const subProj of parentComponent.components) {
1297
+ pkgList = pkgList.filter(
1298
+ (pkg) =>
1299
+ pkg["bom-ref"] !== subProj["bom-ref"] &&
1300
+ pkg["bom-ref"] !== parentComponent["bom-ref"],
1301
+ );
1302
+ }
1303
+ }
1304
+ thoughtLog(
1305
+ `Obtained ${pkgList.length} components by executing the '${basename(gradleCmd)}' command.`,
1248
1306
  );
1307
+ if (DEBUG_MODE) {
1308
+ console.log("Obtained", pkgList.length, "from this gradle project.");
1309
+ }
1310
+ } else if (hasGradleInvocationFailure()) {
1311
+ // The invocation failure is already ranked, and an empty
1312
+ // configuration is what that failure looks like from here: reporting
1313
+ // both would rank two repairs for one broken toolchain.
1314
+ thoughtLog(
1315
+ "**GRADLE:** No configuration resolved, which the recorded invocation failure already explains.",
1316
+ );
1317
+ } else {
1318
+ recordDegradation("jvm.gradle.unresolved-config", {
1319
+ ecosystem: "java",
1320
+ tool: "gradle",
1321
+ impact: "transitive-deps",
1322
+ command: `${basename(gradleCmd)} ${gradleDepTask}`,
1323
+ detail:
1324
+ "Gradle did not resolve any dependency configuration, so the gradle components carry no resolved dependencies.",
1325
+ });
1326
+ thoughtLog(
1327
+ "**GRADLE:** SBOM is incomplete. I recommend troubleshooting the issue to improve the BOM precision.",
1328
+ );
1329
+ if (!DEBUG_MODE) {
1330
+ console.log(
1331
+ "No packages found. Set the environment variable 'CDXGEN_DEBUG_MODE=debug' to troubleshoot any gradle related errors.",
1332
+ );
1333
+ }
1334
+ deferFailOnError(options, {
1335
+ ecosystem: "java",
1336
+ tool: "gradle",
1337
+ detail: "gradle resolved no dependency configuration",
1338
+ });
1249
1339
  }
1250
- options.failOnError && process.exit(1);
1251
- }
1252
- if (
1253
- (!readEnvironmentVariable("GRADLE_STOP_DAEMON") &&
1254
- (!readEnvironmentVariable("GRADLE_USE_DAEMON") ||
1255
- ["true", "1"].includes(
1256
- readEnvironmentVariable("GRADLE_USE_DAEMON"),
1257
- ))) ||
1258
- ["true", "1"].includes(readEnvironmentVariable("GRADLE_STOP_DAEMON"))
1259
- ) {
1260
- if (DEBUG_MODE) {
1261
- console.log("Stopping gradle daemon...");
1262
- }
1263
- const sresult = safeSpawnSync(gradleCmd, ["--stop"], {
1264
- cwd: gradleRootPath,
1265
- shell: isWin,
1266
- });
1267
- if (sresult.status !== 0 || sresult.error) {
1268
- if (options.failOnError || DEBUG_MODE) {
1269
- console.error(sresult.stdout, sresult.stderr);
1340
+ if (
1341
+ (!readEnvironmentVariable("GRADLE_STOP_DAEMON") &&
1342
+ (!readEnvironmentVariable("GRADLE_USE_DAEMON") ||
1343
+ ["true", "1"].includes(
1344
+ readEnvironmentVariable("GRADLE_USE_DAEMON"),
1345
+ ))) ||
1346
+ ["true", "1"].includes(readEnvironmentVariable("GRADLE_STOP_DAEMON"))
1347
+ ) {
1348
+ if (DEBUG_MODE) {
1349
+ console.log("Stopping gradle daemon...");
1350
+ }
1351
+ const sresult = safeSpawnSync(gradleCmd, ["--stop"], {
1352
+ cwd: gradleRootPath,
1353
+ shell: isWin,
1354
+ });
1355
+ if (sresult.status !== 0 || sresult.error) {
1356
+ if (options.failOnError || DEBUG_MODE) {
1357
+ console.error(sresult.stdout, sresult.stderr);
1358
+ }
1359
+ deferFailOnError(options, {
1360
+ ecosystem: "java",
1361
+ tool: "gradle",
1362
+ exitCode:
1363
+ typeof sresult.status === "number" ? sresult.status : undefined,
1364
+ detail: "stopping the gradle daemon failed",
1365
+ });
1270
1366
  }
1271
- options.failOnError && process.exit(1);
1272
1367
  }
1273
- }
1274
- // Should we attempt to resolve class names
1275
- if (options.resolveClass || options.deep) {
1276
- const tmpjarNSMapping = await collectJarNS(GRADLE_CACHE_DIR);
1277
- if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
1278
- jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
1368
+ // Should we attempt to resolve class names
1369
+ if (options.resolveClass || options.deep) {
1370
+ const tmpjarNSMapping = await collectJarNS(GRADLE_CACHE_DIR);
1371
+ if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
1372
+ jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
1373
+ }
1279
1374
  }
1280
1375
  }
1281
- }
1282
1376
 
1283
- // Bazel
1284
- // Look for the BUILD file only in the root directory
1285
- // NOTE: This can match BUILD files used by perl, so could lead to errors in some projects
1286
- const bazelFiles = getAllFiles(
1287
- path,
1288
- `${options.multiProject ? "**/" : ""}{WORKSPACE{,.bazel},MODULE.bazel}`,
1289
- options,
1290
- );
1291
- if (
1292
- bazelFiles?.length &&
1293
- !hasAnyProjectType(["docker", "oci", "container", "os"], options, false) &&
1294
- isPackageManagerAllowed(
1295
- "bazel",
1296
- ["maven", "gradle", "sbt", "mill"],
1377
+ // Bazel
1378
+ // Look for the BUILD file only in the root directory
1379
+ // NOTE: This can match BUILD files used by perl, so could lead to errors in some projects
1380
+ const bazelFiles = getAllFiles(
1381
+ path,
1382
+ `${options.multiProject ? "**/" : ""}{WORKSPACE{,.bazel},MODULE.bazel}`,
1297
1383
  options,
1298
- )
1299
- ) {
1300
- let BAZEL_CMD = "bazel";
1301
- const bazelHome = readEnvironmentVariable("BAZEL_HOME");
1302
- if (bazelHome) {
1303
- BAZEL_CMD = join(bazelHome, "bin", "bazel");
1304
- }
1305
-
1306
- // bzlmod: parse MODULE.bazel and MODULE.bazel.lock for BCR modules and
1307
- // ecosystem dependencies (Maven, etc.). This works from the manifest and
1308
- // lock alone, so BCR/Maven packages are extracted even when the bazel
1309
- // toolchain is absent. Resolved third-party deps map to their true
1310
- // ecosystem purl (pkg:maven, ...); only BCR modules use pkg:bazel.
1311
- const moduleBazelFiles = bazelFiles.filter(
1312
- (f) => basename(f) === "MODULE.bazel",
1313
1384
  );
1314
- for (const moduleFile of moduleBazelFiles) {
1315
- const baseDir = dirname(moduleFile);
1316
- const moduleResult = parseModuleBazel(moduleFile);
1317
- if (moduleResult.pkgList?.length) {
1318
- pkgList = pkgList.concat(moduleResult.pkgList);
1319
- }
1320
- if (moduleResult.parentComponent?.name) {
1321
- parentComponent = moduleResult.parentComponent;
1385
+ if (
1386
+ bazelFiles?.length &&
1387
+ !hasAnyProjectType(
1388
+ ["docker", "oci", "container", "os"],
1389
+ options,
1390
+ false,
1391
+ ) &&
1392
+ isPackageManagerAllowed(
1393
+ "bazel",
1394
+ ["maven", "gradle", "sbt", "mill"],
1395
+ options,
1396
+ )
1397
+ ) {
1398
+ let BAZEL_CMD = "bazel";
1399
+ const bazelHome = readEnvironmentVariable("BAZEL_HOME");
1400
+ if (bazelHome) {
1401
+ BAZEL_CMD = join(bazelHome, "bin", "bazel");
1322
1402
  }
1323
- const lockFile = join(baseDir, "MODULE.bazel.lock");
1324
- if (safeExistsSync(lockFile)) {
1325
- const lockResult = parseModuleBazelLock(lockFile);
1326
- if (lockResult.pkgList?.length) {
1327
- pkgList = pkgList.concat(lockResult.pkgList);
1403
+
1404
+ // bzlmod: parse MODULE.bazel and MODULE.bazel.lock for BCR modules and
1405
+ // ecosystem dependencies (Maven, etc.). This works from the manifest and
1406
+ // lock alone, so BCR/Maven packages are extracted even when the bazel
1407
+ // toolchain is absent. Resolved third-party deps map to their true
1408
+ // ecosystem purl (pkg:maven, ...); only BCR modules use pkg:bazel.
1409
+ const moduleBazelFiles = bazelFiles.filter(
1410
+ (f) => basename(f) === "MODULE.bazel",
1411
+ );
1412
+ for (const moduleFile of moduleBazelFiles) {
1413
+ const baseDir = dirname(moduleFile);
1414
+ const moduleResult = parseModuleBazel(moduleFile);
1415
+ if (moduleResult.pkgList?.length) {
1416
+ pkgList = pkgList.concat(moduleResult.pkgList);
1328
1417
  }
1329
- if (lockResult.dependencies?.length) {
1330
- dependencies = mergeDependencies(
1331
- dependencies,
1332
- lockResult.dependencies,
1333
- );
1418
+ if (moduleResult.parentComponent?.name) {
1419
+ parentComponent = moduleResult.parentComponent;
1420
+ }
1421
+ const lockFile = join(baseDir, "MODULE.bazel.lock");
1422
+ if (safeExistsSync(lockFile)) {
1423
+ const lockResult = parseModuleBazelLock(lockFile);
1424
+ if (lockResult.pkgList?.length) {
1425
+ pkgList = pkgList.concat(lockResult.pkgList);
1426
+ }
1427
+ if (lockResult.dependencies?.length) {
1428
+ dependencies = mergeDependencies(
1429
+ dependencies,
1430
+ lockResult.dependencies,
1431
+ );
1432
+ }
1334
1433
  }
1335
1434
  }
1336
- }
1337
1435
 
1338
- // Querying bazel means building the project first, which is the most
1339
- // expensive thing cdxgen can do and needs network access. Like every other
1340
- // package-manager invocation it therefore runs only when dependency
1341
- // installation is permitted; `--lifecycle pre-build` and secure mode both
1342
- // turn it off, leaving the manifest and lock as the inventory source.
1343
- for (const f of options.installDeps ? bazelFiles : []) {
1344
- const basePath = dirname(f);
1345
- // Invoke bazel build first
1346
- const bazelTarget = readEnvironmentVariable("BAZEL_TARGET") || "//...";
1347
- let bArgs = [
1348
- ...(readEnvironmentVariable("BAZEL_ARGS")?.split(" ") || []),
1349
- "build",
1350
- bazelTarget,
1351
- ];
1352
- // Automatically load any bazelrc file
1353
- if (
1354
- !readEnvironmentVariable("BAZEL_ARGS") &&
1355
- safeExistsSync(join(basePath, ".bazelrc"))
1356
- ) {
1357
- bArgs = ["--bazelrc=.bazelrc", "build", bazelTarget];
1358
- }
1359
- console.log("Executing", BAZEL_CMD, "in", basePath);
1360
- let result = safeSpawnSync(BAZEL_CMD, bArgs, {
1361
- cwd: basePath,
1362
- shell: isWin,
1363
- });
1364
- if (result.status !== 0 || result.error) {
1365
- if (result.stderr) {
1366
- console.error(result.stdout, result.stderr);
1367
- }
1368
- console.log(
1369
- "1. Check if bazel is installed and available in PATH.\n2. Try building your app with bazel prior to invoking cdxgen",
1370
- );
1371
- options.failOnError && process.exit(1);
1372
- } else {
1373
- const target = readEnvironmentVariable("BAZEL_TARGET") || "//...";
1374
- let query = [
1436
+ // Querying bazel means building the project first, which is the most
1437
+ // expensive thing cdxgen can do and needs network access. Like every other
1438
+ // package-manager invocation it therefore runs only when dependency
1439
+ // installation is permitted; `--lifecycle pre-build` and secure mode both
1440
+ // turn it off, leaving the manifest and lock as the inventory source.
1441
+ for (const f of options.installDeps ? bazelFiles : []) {
1442
+ const basePath = dirname(f);
1443
+ // Invoke bazel build first
1444
+ const bazelTarget = readEnvironmentVariable("BAZEL_TARGET") || "//...";
1445
+ let bArgs = [
1375
1446
  ...(readEnvironmentVariable("BAZEL_ARGS")?.split(" ") || []),
1447
+ "build",
1448
+ bazelTarget,
1376
1449
  ];
1377
- let bazelParser;
1378
1450
  // Automatically load any bazelrc file
1379
1451
  if (
1380
1452
  !readEnvironmentVariable("BAZEL_ARGS") &&
1381
1453
  safeExistsSync(join(basePath, ".bazelrc"))
1382
1454
  ) {
1383
- query = ["--bazelrc=.bazelrc"];
1384
- }
1385
- if (
1386
- ["true", "1"].includes(
1387
- readEnvironmentVariable("BAZEL_USE_ACTION_GRAPH"),
1388
- )
1389
- ) {
1390
- query = query.concat(["query", `deps(${target})`, "--output=label"]);
1391
- bazelParser = parseBazelActionGraph;
1392
- } else {
1393
- query = query.concat([
1394
- "aquery",
1395
- "--output=textproto",
1396
- "--skyframe_state",
1397
- ]);
1398
- bazelParser = parseBazelSkyframe;
1455
+ bArgs = ["--bazelrc=.bazelrc", "build", bazelTarget];
1399
1456
  }
1400
- console.log("Executing", BAZEL_CMD, `${query.join(" ")} in`, basePath);
1401
- result = safeSpawnSync(BAZEL_CMD, query, {
1457
+ console.log("Executing", BAZEL_CMD, "in", basePath);
1458
+ let result = safeSpawnSync(BAZEL_CMD, bArgs, {
1402
1459
  cwd: basePath,
1460
+ shell: isWin,
1403
1461
  });
1404
1462
  if (result.status !== 0 || result.error) {
1405
- console.error(result.stdout, result.stderr);
1406
- options.failOnError && process.exit(1);
1407
- }
1408
- const stdout = result.stdout;
1409
- if (stdout) {
1410
- const cmdOutput = Buffer.from(stdout).toString();
1411
- const dlist = bazelParser(cmdOutput);
1412
- if (dlist?.length) {
1413
- pkgList = pkgList.concat(dlist);
1414
- } else {
1415
- console.log(
1416
- "No packages were detected.\n1. Build your project using bazel build command before running cdxgen\n2. Try running the bazel aquery command manually to see if skyframe state can be retrieved.",
1417
- );
1418
- console.log(
1419
- "If your project requires a different query, please file a bug at cyclonedx/cdxgen repo!",
1420
- );
1421
- options.failOnError && process.exit(1);
1463
+ if (result.stderr) {
1464
+ console.error(result.stdout, result.stderr);
1422
1465
  }
1466
+ console.log(
1467
+ "1. Check if bazel is installed and available in PATH.\n2. Try building your app with bazel prior to invoking cdxgen",
1468
+ );
1469
+ deferFailOnError(options, {
1470
+ ecosystem: "java",
1471
+ tool: "bazel",
1472
+ exitCode:
1473
+ typeof result.status === "number" ? result.status : undefined,
1474
+ detail: "the bazel build failed",
1475
+ });
1423
1476
  } else {
1424
- console.log("Bazel unexpectedly didn't produce any output");
1425
- options.failOnError && process.exit(1);
1477
+ const target = readEnvironmentVariable("BAZEL_TARGET") || "//...";
1478
+ let query = [
1479
+ ...(readEnvironmentVariable("BAZEL_ARGS")?.split(" ") || []),
1480
+ ];
1481
+ let bazelParser;
1482
+ // Automatically load any bazelrc file
1483
+ if (
1484
+ !readEnvironmentVariable("BAZEL_ARGS") &&
1485
+ safeExistsSync(join(basePath, ".bazelrc"))
1486
+ ) {
1487
+ query = ["--bazelrc=.bazelrc"];
1488
+ }
1489
+ if (
1490
+ ["true", "1"].includes(
1491
+ readEnvironmentVariable("BAZEL_USE_ACTION_GRAPH"),
1492
+ )
1493
+ ) {
1494
+ query = query.concat([
1495
+ "query",
1496
+ `deps(${target})`,
1497
+ "--output=label",
1498
+ ]);
1499
+ bazelParser = parseBazelActionGraph;
1500
+ } else {
1501
+ query = query.concat([
1502
+ "aquery",
1503
+ "--output=textproto",
1504
+ "--skyframe_state",
1505
+ ]);
1506
+ bazelParser = parseBazelSkyframe;
1507
+ }
1508
+ console.log(
1509
+ "Executing",
1510
+ BAZEL_CMD,
1511
+ `${query.join(" ")} in`,
1512
+ basePath,
1513
+ );
1514
+ result = safeSpawnSync(BAZEL_CMD, query, {
1515
+ cwd: basePath,
1516
+ });
1517
+ if (result.status !== 0 || result.error) {
1518
+ console.error(result.stdout, result.stderr);
1519
+ deferFailOnError(options, {
1520
+ ecosystem: "java",
1521
+ tool: "bazel",
1522
+ exitCode:
1523
+ typeof result.status === "number" ? result.status : undefined,
1524
+ detail: "the bazel graph query failed",
1525
+ });
1526
+ }
1527
+ const stdout = result.stdout;
1528
+ if (stdout) {
1529
+ const cmdOutput = Buffer.from(stdout).toString();
1530
+ const dlist = bazelParser(cmdOutput);
1531
+ if (dlist?.length) {
1532
+ pkgList = pkgList.concat(dlist);
1533
+ } else {
1534
+ console.log(
1535
+ "No packages were detected.\n1. Build your project using bazel build command before running cdxgen\n2. Try running the bazel aquery command manually to see if skyframe state can be retrieved.",
1536
+ );
1537
+ console.log(
1538
+ "If your project requires a different query, please file a bug at cyclonedx/cdxgen repo!",
1539
+ );
1540
+ deferFailOnError(options, {
1541
+ ecosystem: "java",
1542
+ tool: "bazel",
1543
+ detail: "the bazel graph query produced no packages",
1544
+ });
1545
+ }
1546
+ } else {
1547
+ console.log("Bazel unexpectedly didn't produce any output");
1548
+ deferFailOnError(options, {
1549
+ ecosystem: "java",
1550
+ tool: "bazel",
1551
+ detail: "the bazel graph query unexpectedly produced no output",
1552
+ });
1553
+ }
1426
1554
  }
1427
1555
  }
1428
1556
  }
1429
- }
1430
1557
 
1431
- // scala sbt
1432
- // Identify sbt projects via its `project` directory:
1433
- // - all SBT project _should_ define build.properties file with sbt version info
1434
- // - SBT projects _typically_ have some configs/plugins defined in .sbt files
1435
- // - SBT projects that are still on 0.13.x, can still use the old approach,
1436
- // where configs are defined via Scala files
1437
- // Detecting one of those should be enough to determine an SBT project.
1438
- let sbtProjectFiles = getAllFiles(
1439
- path,
1440
- `${
1441
- options.multiProject ? "**/" : ""
1442
- }project/{build.properties,*.sbt,*.scala}`,
1443
- options,
1444
- );
1445
-
1446
- let sbtProjects = [];
1447
- for (const i in sbtProjectFiles) {
1448
- // parent dir of sbtProjectFile is the `project` directory
1449
- // parent dir of `project` is the sbt root project directory
1450
- const baseDir = dirname(dirname(sbtProjectFiles[i]));
1451
- sbtProjects = sbtProjects.concat(baseDir);
1452
- }
1453
-
1454
- // Fallback in case sbt's project directory is non-existent
1455
- if (!sbtProjects.length) {
1456
- sbtProjectFiles = getAllFiles(
1558
+ // scala sbt
1559
+ // Identify sbt projects via its `project` directory:
1560
+ // - all SBT project _should_ define build.properties file with sbt version info
1561
+ // - SBT projects _typically_ have some configs/plugins defined in .sbt files
1562
+ // - SBT projects that are still on 0.13.x, can still use the old approach,
1563
+ // where configs are defined via Scala files
1564
+ // Detecting one of those should be enough to determine an SBT project.
1565
+ let sbtProjectFiles = getAllFiles(
1457
1566
  path,
1458
- `${options.multiProject ? "**/" : ""}*.sbt`,
1567
+ `${
1568
+ options.multiProject ? "**/" : ""
1569
+ }project/{build.properties,*.sbt,*.scala}`,
1459
1570
  options,
1460
1571
  );
1572
+
1573
+ let sbtProjects = [];
1461
1574
  for (const i in sbtProjectFiles) {
1462
- const baseDir = dirname(sbtProjectFiles[i]);
1575
+ // parent dir of sbtProjectFile is the `project` directory
1576
+ // parent dir of `project` is the sbt root project directory
1577
+ const baseDir = dirname(dirname(sbtProjectFiles[i]));
1463
1578
  sbtProjects = sbtProjects.concat(baseDir);
1464
1579
  }
1465
- }
1466
- // eliminate duplicates and ignore project directories
1467
- sbtProjects = [...new Set(sbtProjects)].filter(
1468
- (p) => !p.endsWith(`${sep}project`) && !p.includes(`target${sep}`),
1469
- );
1470
- const sbtLockFiles = getAllFiles(
1471
- path,
1472
- `${options.multiProject ? "**/" : ""}build.sbt.lock`,
1473
- options,
1474
- );
1475
- const tempCacheDir = safeMkdtempSync(join(getTmpDir(), "sbt-cache-"));
1476
- safeMkdirSync(tempCacheDir, { recursive: true });
1477
- if (
1478
- sbtProjects?.length &&
1479
- isPackageManagerAllowed(
1480
- "sbt",
1481
- ["bazel", "maven", "gradle", "mill"],
1580
+
1581
+ // Fallback in case sbt's project directory is non-existent
1582
+ if (!sbtProjects.length) {
1583
+ sbtProjectFiles = getAllFiles(
1584
+ path,
1585
+ `${options.multiProject ? "**/" : ""}*.sbt`,
1586
+ options,
1587
+ );
1588
+ for (const i in sbtProjectFiles) {
1589
+ const baseDir = dirname(sbtProjectFiles[i]);
1590
+ sbtProjects = sbtProjects.concat(baseDir);
1591
+ }
1592
+ }
1593
+ // eliminate duplicates and ignore project directories
1594
+ sbtProjects = [...new Set(sbtProjects)].filter(
1595
+ (p) => !p.endsWith(`${sep}project`) && !p.includes(`target${sep}`),
1596
+ );
1597
+ const sbtLockFiles = getAllFiles(
1598
+ path,
1599
+ `${options.multiProject ? "**/" : ""}build.sbt.lock`,
1482
1600
  options,
1483
- )
1484
- ) {
1485
- // If the project use sbt lock files
1486
- if (sbtLockFiles?.length) {
1487
- for (const f of sbtLockFiles) {
1488
- const dlist = await parseSbtLock(f);
1489
- if (dlist?.length) {
1490
- pkgList = pkgList.concat(dlist);
1601
+ );
1602
+ const tempCacheDir = safeMkdtempSync(join(getTmpDir(), "sbt-cache-"));
1603
+ safeMkdirSync(tempCacheDir, { recursive: true });
1604
+ if (
1605
+ sbtProjects?.length &&
1606
+ isPackageManagerAllowed(
1607
+ "sbt",
1608
+ ["bazel", "maven", "gradle", "mill"],
1609
+ options,
1610
+ )
1611
+ ) {
1612
+ // If the project use sbt lock files
1613
+ if (sbtLockFiles?.length) {
1614
+ for (const f of sbtLockFiles) {
1615
+ const dlist = await parseSbtLock(f);
1616
+ if (dlist?.length) {
1617
+ pkgList = pkgList.concat(dlist);
1618
+ }
1491
1619
  }
1492
- }
1493
- } else {
1494
- const SBT_CMD = readEnvironmentVariable("SBT_CMD") || "sbt";
1495
- let sbtVersion = determineSbtVersion(path);
1496
- // If can't find sbt version at the root of repository then search in
1497
- // sbt project array too because sometimes the project folder isn't at
1498
- // root of repository
1499
- if (sbtVersion == null) {
1500
- for (const i in sbtProjects) {
1501
- sbtVersion = determineSbtVersion(sbtProjects[i]);
1502
- if (sbtVersion != null) {
1503
- break;
1620
+ } else {
1621
+ const SBT_CMD = readEnvironmentVariable("SBT_CMD") || "sbt";
1622
+ let sbtVersion = determineSbtVersion(path);
1623
+ // If can't find sbt version at the root of repository then search in
1624
+ // sbt project array too because sometimes the project folder isn't at
1625
+ // root of repository
1626
+ if (sbtVersion == null) {
1627
+ for (const i in sbtProjects) {
1628
+ sbtVersion = determineSbtVersion(sbtProjects[i]);
1629
+ if (sbtVersion != null) {
1630
+ break;
1631
+ }
1504
1632
  }
1505
1633
  }
1506
- }
1507
- if (DEBUG_MODE) {
1508
- console.log(`Detected sbt version: ${sbtVersion}`);
1509
- }
1510
- // Introduced in 1.2.0 https://www.scala-sbt.org/1.x/docs/sbt-1.2-Release-Notes.html#addPluginSbtFile+command,
1511
- // however working properly for real only since 1.3.4: https://github.com/sbt/sbt/releases/tag/v1.3.4
1512
- const standalonePluginFile =
1513
- sbtVersion != null &&
1514
- gte(sbtVersion, "1.3.4") &&
1515
- lte(sbtVersion, "1.4.0");
1516
- const useSlashSyntax = !sbtVersion || gte(sbtVersion, "1.5.0");
1517
- const isDependencyTreeBuiltIn =
1518
- sbtVersion != null && gte(sbtVersion, "1.4.0");
1519
- const tempDir = safeMkdtempSync(join(getTmpDir(), "cdxsbt-"));
1520
- const tempSbtgDir = safeMkdtempSync(join(getTmpDir(), "cdxsbtg-"));
1521
- safeMkdirSync(tempSbtgDir, { recursive: true });
1522
- // Create temporary plugins file
1523
- const tempSbtPlugins = join(tempSbtgDir, "dep-plugins.sbt");
1524
-
1525
- // Requires a custom version of `sbt-dependency-graph` that
1526
- // supports `--append` for `toFile` subtask.
1527
- let sbtPluginDefinition = `\naddSbtPlugin("io.shiftleft" % "sbt-dependency-graph" % "0.10.0-append-to-file3")\n`;
1528
- if (isDependencyTreeBuiltIn) {
1529
- sbtPluginDefinition = "\naddDependencyTreePlugin\n";
1530
1634
  if (DEBUG_MODE) {
1531
- console.log("Using addDependencyTreePlugin as the custom plugin");
1635
+ console.log(`Detected sbt version: ${sbtVersion}`);
1532
1636
  }
1533
- }
1534
- safeWriteSync(tempSbtPlugins, sbtPluginDefinition);
1535
- let sbtExtraArgs = "";
1536
- const env = { ...process.env };
1537
- // We need to collect the jars from the cache
1538
- if (options.deep) {
1539
- sbtExtraArgs = " updateClassifiers";
1540
- env["COURSIER_CACHE"] = tempCacheDir;
1541
- env["SBT_IVY_HOME"] = tempCacheDir;
1542
- }
1543
- for (const i in sbtProjects) {
1544
- const basePath = sbtProjects[i];
1545
- const dlFile = join(tempDir, `dl-${i}.tmp`);
1546
- let sbtArgs = [];
1547
- let pluginFile = null;
1548
- const subDlFiles = [];
1549
- if (standalonePluginFile) {
1550
- sbtArgs = [
1551
- `-addPluginSbtFile=${tempSbtPlugins}`,
1552
- `"dependencyList::toFile ${dlFile} --force"`,
1553
- ];
1554
- subDlFiles.push(dlFile);
1555
- } else {
1556
- // write to the existing plugins file
1557
- // Discover the real sbt project ids by asking sbt itself, falling
1558
- // back to the regex-based heuristic if that fails. A per-subproject
1559
- // `dependencyTree` fan-out yields a far more complete tree than a
1560
- // single root resolution (which only captures the root project's own
1561
- // libraryDependencies). See https://github.com/cdxgen/cdxgen/issues/4291
1562
- const subprojects = discoverSbtProjectsFromCmd(
1563
- basePath,
1564
- SBT_CMD,
1565
- env,
1566
- );
1567
- if (subprojects.length > 0 && useSlashSyntax) {
1568
- sbtArgs = [
1569
- `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs}`,
1570
- ];
1571
- for (const sp of subprojects) {
1572
- const subDlFile = join(tempDir, `dl-${i}-${sp}.tmp`);
1573
- subDlFiles.push(subDlFile);
1574
- sbtArgs.push(
1575
- `"${sp}/dependencyTree / toFile ${subDlFile} --force"`,
1576
- );
1577
- }
1578
- } else if (useSlashSyntax) {
1637
+ // Introduced in 1.2.0 https://www.scala-sbt.org/1.x/docs/sbt-1.2-Release-Notes.html#addPluginSbtFile+command,
1638
+ // however working properly for real only since 1.3.4: https://github.com/sbt/sbt/releases/tag/v1.3.4
1639
+ const standalonePluginFile =
1640
+ sbtVersion != null &&
1641
+ gte(sbtVersion, "1.3.4") &&
1642
+ lte(sbtVersion, "1.4.0");
1643
+ const useSlashSyntax = !sbtVersion || gte(sbtVersion, "1.5.0");
1644
+ const isDependencyTreeBuiltIn =
1645
+ sbtVersion != null && gte(sbtVersion, "1.4.0");
1646
+ const tempDir = safeMkdtempSync(join(getTmpDir(), "cdxsbt-"));
1647
+ const tempSbtgDir = safeMkdtempSync(join(getTmpDir(), "cdxsbtg-"));
1648
+ safeMkdirSync(tempSbtgDir, { recursive: true });
1649
+ // Create temporary plugins file
1650
+ const tempSbtPlugins = join(tempSbtgDir, "dep-plugins.sbt");
1651
+
1652
+ // Requires a custom version of `sbt-dependency-graph` that
1653
+ // supports `--append` for `toFile` subtask.
1654
+ let sbtPluginDefinition = `\naddSbtPlugin("io.shiftleft" % "sbt-dependency-graph" % "0.10.0-append-to-file3")\n`;
1655
+ if (isDependencyTreeBuiltIn) {
1656
+ sbtPluginDefinition = "\naddDependencyTreePlugin\n";
1657
+ if (DEBUG_MODE) {
1658
+ console.log("Using addDependencyTreePlugin as the custom plugin");
1659
+ }
1660
+ }
1661
+ safeWriteSync(tempSbtPlugins, sbtPluginDefinition);
1662
+ let sbtExtraArgs = "";
1663
+ const env = { ...process.env };
1664
+ // We need to collect the jars from the cache
1665
+ if (options.deep) {
1666
+ sbtExtraArgs = " updateClassifiers";
1667
+ env["COURSIER_CACHE"] = tempCacheDir;
1668
+ env["SBT_IVY_HOME"] = tempCacheDir;
1669
+ }
1670
+ for (const i in sbtProjects) {
1671
+ const basePath = sbtProjects[i];
1672
+ const dlFile = join(tempDir, `dl-${i}.tmp`);
1673
+ let sbtArgs = [];
1674
+ let pluginFile = null;
1675
+ const subDlFiles = [];
1676
+ if (standalonePluginFile) {
1579
1677
  sbtArgs = [
1580
- `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs} "dependencyTree / toFile ${dlFile} --force"`,
1678
+ `-addPluginSbtFile=${tempSbtPlugins}`,
1679
+ `"dependencyList::toFile ${dlFile} --force"`,
1581
1680
  ];
1582
1681
  subDlFiles.push(dlFile);
1583
1682
  } else {
1584
- sbtArgs = [
1585
- `'set asciiGraphWidth in ThisBuild := 800'${sbtExtraArgs} "dependencyTree::toFile ${dlFile} --force"`,
1586
- ];
1587
- subDlFiles.push(dlFile);
1683
+ // write to the existing plugins file
1684
+ // Discover the real sbt project ids by asking sbt itself, falling
1685
+ // back to the regex-based heuristic if that fails. A per-subproject
1686
+ // `dependencyTree` fan-out yields a far more complete tree than a
1687
+ // single root resolution (which only captures the root project's own
1688
+ // libraryDependencies). See https://github.com/cdxgen/cdxgen/issues/4291
1689
+ const subprojects = discoverSbtProjectsFromCmd(
1690
+ basePath,
1691
+ SBT_CMD,
1692
+ env,
1693
+ );
1694
+ if (subprojects.length > 0 && useSlashSyntax) {
1695
+ sbtArgs = [
1696
+ `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs}`,
1697
+ ];
1698
+ for (const sp of subprojects) {
1699
+ const subDlFile = join(tempDir, `dl-${i}-${sp}.tmp`);
1700
+ subDlFiles.push(subDlFile);
1701
+ sbtArgs.push(
1702
+ `"${sp}/dependencyTree / toFile ${subDlFile} --force"`,
1703
+ );
1704
+ }
1705
+ } else if (useSlashSyntax) {
1706
+ sbtArgs = [
1707
+ `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs} "dependencyTree / toFile ${dlFile} --force"`,
1708
+ ];
1709
+ subDlFiles.push(dlFile);
1710
+ } else {
1711
+ sbtArgs = [
1712
+ `'set asciiGraphWidth in ThisBuild := 800'${sbtExtraArgs} "dependencyTree::toFile ${dlFile} --force"`,
1713
+ ];
1714
+ subDlFiles.push(dlFile);
1715
+ }
1716
+ pluginFile = addPlugin(basePath, sbtPluginDefinition);
1588
1717
  }
1589
- pluginFile = addPlugin(basePath, sbtPluginDefinition);
1590
- }
1591
- // Run sbt in non-interactive batch mode so it never blocks waiting for
1592
- // input on stdin (which can otherwise appear as an indefinite hang) and
1593
- // does not emit ANSI colour codes into the dependency tree output.
1594
- sbtArgs = ["-batch", "-no-colors", ...sbtArgs];
1595
- console.log(
1596
- "Executing",
1597
- SBT_CMD,
1598
- sbtArgs.join(" "),
1599
- "in",
1600
- basePath,
1601
- "using plugins",
1602
- tempSbtgDir,
1603
- );
1604
- // Note that the command has to be invoked with `shell: true` to properly execut sbt
1605
- const result = safeSpawnSync(SBT_CMD, sbtArgs, {
1606
- cwd: basePath,
1607
- shell: true,
1608
- env,
1609
- });
1610
- if (result.status !== 0 || result.error) {
1611
- console.error(result.stdout, result.stderr);
1612
- console.log(
1613
- "1. Check if scala and sbt is installed and available in PATH. Only scala 2.10 + sbt 0.13.6+ and 2.12 + sbt 1.0+ is supported for now.",
1614
- );
1718
+ // Run sbt in non-interactive batch mode so it never blocks waiting for
1719
+ // input on stdin (which can otherwise appear as an indefinite hang) and
1720
+ // does not emit ANSI colour codes into the dependency tree output.
1721
+ sbtArgs = ["-batch", "-no-colors", ...sbtArgs];
1615
1722
  console.log(
1616
- "2. Check if the plugin net.virtual-void:sbt-dependency-graph 0.10.0-RC1 can be used in the environment",
1617
- );
1618
- console.log(
1619
- "3. Consider creating a lockfile using sbt-dependency-lock plugin. See https://github.com/stringbean/sbt-dependency-lock",
1723
+ "Executing",
1724
+ SBT_CMD,
1725
+ sbtArgs.join(" "),
1726
+ "in",
1727
+ basePath,
1728
+ "using plugins",
1729
+ tempSbtgDir,
1620
1730
  );
1621
- options.failOnError && process.exit(1);
1622
- }
1623
- if (!standalonePluginFile) {
1624
- cleanupPlugin(basePath, pluginFile);
1625
- }
1626
- const sbtSubprojectRoots = [];
1627
- for (const subDlFile of subDlFiles) {
1628
- if (safeExistsSync(subDlFile)) {
1629
- const retMap = await parseSbtTree(subDlFile);
1630
- if (retMap.pkgList?.length) {
1631
- const tmpParentComponent = retMap.pkgList.splice(0, 1)[0];
1632
- tmpParentComponent.type = "application";
1633
- pkgList = pkgList.concat(retMap.pkgList);
1634
- pkgList.push(tmpParentComponent);
1635
- sbtSubprojectRoots.push(tmpParentComponent);
1636
- }
1637
- if (retMap.dependenciesList) {
1638
- dependencies = mergeDependencies(
1639
- dependencies,
1640
- retMap.dependenciesList,
1641
- parentComponent,
1642
- );
1643
- }
1644
- } else {
1645
- if (options.failOnError || DEBUG_MODE) {
1646
- console.log(`sbt dependencyList did not yield ${subDlFile}`);
1731
+ // Note that the command has to be invoked with `shell: true` to properly execut sbt
1732
+ const result = safeSpawnSync(SBT_CMD, sbtArgs, {
1733
+ cwd: basePath,
1734
+ shell: true,
1735
+ env,
1736
+ });
1737
+ if (result.status !== 0 || result.error) {
1738
+ console.error(result.stdout, result.stderr);
1739
+ recordDegradation("jvm.sbt.no-lockfile", {
1740
+ ecosystem: "java",
1741
+ tool: "sbt",
1742
+ impact: "versions",
1743
+ command: `${SBT_CMD} ${sbtArgs.join(" ")}`,
1744
+ exitCode:
1745
+ typeof result.status === "number" ? result.status : undefined,
1746
+ detail:
1747
+ "The sbt dependencyTree command did not succeed, so the declared sbt dependencies are the only evidence available.",
1748
+ });
1749
+ console.log(
1750
+ "1. Check if scala and sbt is installed and available in PATH. Only scala 2.10 + sbt 0.13.6+ and 2.12 + sbt 1.0+ is supported for now.",
1751
+ );
1752
+ console.log(
1753
+ "2. Check if the plugin net.virtual-void:sbt-dependency-graph 0.10.0-RC1 can be used in the environment",
1754
+ );
1755
+ console.log(
1756
+ "3. Consider creating a lockfile using sbt-dependency-lock plugin. See https://github.com/stringbean/sbt-dependency-lock",
1757
+ );
1758
+ deferFailOnError(options, {
1759
+ ecosystem: "java",
1760
+ tool: "sbt",
1761
+ exitCode:
1762
+ typeof result.status === "number" ? result.status : undefined,
1763
+ command: `${SBT_CMD} ${sbtArgs.join(" ")}`,
1764
+ detail: "the sbt dependencyTree command failed",
1765
+ });
1766
+ }
1767
+ if (!standalonePluginFile) {
1768
+ cleanupPlugin(basePath, pluginFile);
1769
+ }
1770
+ const sbtSubprojectRoots = [];
1771
+ for (const subDlFile of subDlFiles) {
1772
+ if (safeExistsSync(subDlFile)) {
1773
+ const retMap = await parseSbtTree(subDlFile);
1774
+ if (retMap.pkgList?.length) {
1775
+ const tmpParentComponent = retMap.pkgList.splice(0, 1)[0];
1776
+ tmpParentComponent.type = "application";
1777
+ pkgList = pkgList.concat(retMap.pkgList);
1778
+ pkgList.push(tmpParentComponent);
1779
+ sbtSubprojectRoots.push(tmpParentComponent);
1780
+ }
1781
+ if (retMap.dependenciesList) {
1782
+ dependencies = mergeDependencies(
1783
+ dependencies,
1784
+ retMap.dependenciesList,
1785
+ parentComponent,
1786
+ );
1787
+ }
1788
+ } else {
1789
+ if (options.failOnError || DEBUG_MODE) {
1790
+ console.log(`sbt dependencyList did not yield ${subDlFile}`);
1791
+ }
1792
+ if (options.failOnError) {
1793
+ deferFailOnError(options, {
1794
+ ecosystem: "java",
1795
+ tool: "sbt",
1796
+ detail: "the sbt dependencyList output was missing",
1797
+ });
1798
+ }
1647
1799
  }
1648
- options.failOnError && process.exit(1);
1800
+ }
1801
+ // Construct a synthetic root component from build.sbt name/org/version
1802
+ // that aggregates all subproject roots as its direct dependencies.
1803
+ const sbtRoot = parseSbtRootProject(basePath);
1804
+ if (sbtRoot && sbtSubprojectRoots.length > 0) {
1805
+ const rootPurl = build({
1806
+ type: "maven",
1807
+ namespace: sbtRoot.group || null,
1808
+ name: sbtRoot.name,
1809
+ version: sbtRoot.version || null,
1810
+ qualifiers: { type: "jar" } || null,
1811
+ });
1812
+ const rootComponent = {
1813
+ group: sbtRoot.group,
1814
+ name: sbtRoot.name,
1815
+ version: sbtRoot.version,
1816
+ type: "application",
1817
+ purl: rootPurl,
1818
+ "bom-ref": decodeURIComponent(rootPurl),
1819
+ };
1820
+ parentComponent = rootComponent;
1821
+ dependencies.push({
1822
+ ref: decodeURIComponent(rootPurl),
1823
+ dependsOn: sbtSubprojectRoots
1824
+ .map((c) => c["bom-ref"])
1825
+ .filter(Boolean),
1826
+ });
1827
+ } else if (sbtSubprojectRoots.length > 0 && !parentComponent) {
1828
+ parentComponent = sbtSubprojectRoots[0];
1649
1829
  }
1650
1830
  }
1651
- // Construct a synthetic root component from build.sbt name/org/version
1652
- // that aggregates all subproject roots as its direct dependencies.
1653
- const sbtRoot = parseSbtRootProject(basePath);
1654
- if (sbtRoot && sbtSubprojectRoots.length > 0) {
1655
- const rootPurl = build({
1656
- type: "maven",
1657
- namespace: sbtRoot.group || null,
1658
- name: sbtRoot.name,
1659
- version: sbtRoot.version || null,
1660
- qualifiers: { type: "jar" } || null,
1661
- });
1662
- const rootComponent = {
1663
- group: sbtRoot.group,
1664
- name: sbtRoot.name,
1665
- version: sbtRoot.version,
1666
- type: "application",
1667
- purl: rootPurl,
1668
- "bom-ref": decodeURIComponent(rootPurl),
1669
- };
1670
- parentComponent = rootComponent;
1671
- dependencies.push({
1672
- ref: decodeURIComponent(rootPurl),
1673
- dependsOn: sbtSubprojectRoots
1674
- .map((c) => c["bom-ref"])
1675
- .filter(Boolean),
1676
- });
1677
- } else if (sbtSubprojectRoots.length > 0 && !parentComponent) {
1678
- parentComponent = sbtSubprojectRoots[0];
1679
- }
1680
- }
1681
1831
 
1682
- // Cleanup
1683
- safeUnlinkSync(tempSbtPlugins);
1684
- } // else
1832
+ // Cleanup
1833
+ safeUnlinkSync(tempSbtPlugins);
1834
+ } // else
1685
1835
 
1686
- if (DEBUG_MODE) {
1687
- console.log(`Found ${pkgList.length} packages`);
1688
- }
1689
- // Should we attempt to resolve class names
1690
- if (options.resolveClass || options.deep) {
1691
- const tmpjarNSMapping = await collectJarNS(tempCacheDir);
1692
- if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
1693
- jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
1836
+ if (DEBUG_MODE) {
1837
+ console.log(`Found ${pkgList.length} packages`);
1694
1838
  }
1695
- // sbt can store jars in the target directory
1696
- const jarNSData = await createJarBom(path, options);
1697
- if (jarNSData?.bomJson?.components) {
1698
- pkgList = pkgList.concat(jarNSData?.bomJson?.components);
1699
- const targetJarNSMapping = {};
1700
- for (const p of jarNSData.bomJson.components) {
1701
- if (!p?.purl || !p?.properties?.length) {
1702
- continue;
1703
- }
1704
- const nsProp = p.properties.filter(
1705
- (prop) => prop.name === "internal:Namespaces",
1706
- );
1707
- if (nsProp.length) {
1708
- targetJarNSMapping[p.purl] = nsProp[0].value;
1839
+ // Should we attempt to resolve class names
1840
+ if (options.resolveClass || options.deep) {
1841
+ const tmpjarNSMapping = await collectJarNS(tempCacheDir);
1842
+ if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
1843
+ jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
1844
+ }
1845
+ // sbt can store jars in the target directory
1846
+ const jarNSData = await createJarBom(path, options);
1847
+ if (jarNSData?.bomJson?.components) {
1848
+ pkgList = pkgList.concat(jarNSData?.bomJson?.components);
1849
+ const targetJarNSMapping = {};
1850
+ for (const p of jarNSData.bomJson.components) {
1851
+ if (!p?.purl || !p?.properties?.length) {
1852
+ continue;
1853
+ }
1854
+ const nsProp = p.properties.filter(
1855
+ (prop) => prop.name === "internal:Namespaces",
1856
+ );
1857
+ if (nsProp.length) {
1858
+ targetJarNSMapping[p.purl] = nsProp[0].value;
1859
+ }
1709
1860
  }
1861
+ jarNSMapping = { ...jarNSMapping, ...targetJarNSMapping };
1710
1862
  }
1711
- jarNSMapping = { ...jarNSMapping, ...targetJarNSMapping };
1712
1863
  }
1713
1864
  }
1714
- }
1715
- if (tempCacheDir?.startsWith(getTmpDir())) {
1716
- safeRmSync(tempCacheDir, {
1717
- recursive: true,
1718
- force: true,
1719
- });
1720
- }
1721
-
1722
- if (
1723
- millFiles?.length &&
1724
- isPackageManagerAllowed(
1725
- "mill",
1726
- ["bazel", "sbt", "gradle", "maven"],
1727
- options,
1728
- )
1729
- ) {
1730
- const millRootPath = dirname(millFiles[0]);
1731
- parentComponent = createDefaultParentComponent(
1732
- millRootPath,
1733
- "maven",
1734
- options,
1735
- );
1736
- const millCmd = getMillCommand(millRootPath);
1737
- const millCommonArgs = [
1738
- "--color",
1739
- "false",
1740
- "--disable-callgraph",
1741
- "--disable-prompt",
1742
- "--keep-going",
1743
- "--silent",
1744
- ];
1745
- if (!["true", "1"].includes(readEnvironmentVariable("MILL_USE_SERVER"))) {
1746
- millCommonArgs.unshift("--no-server");
1747
- }
1748
- const millArgs = [...millCommonArgs, "__.ivyDepsTree"];
1749
- if (DEBUG_MODE) {
1750
- console.log("Executing", millCmd, "in", millRootPath);
1751
- }
1752
- let sresult = safeSpawnSync(millCmd, millArgs, {
1753
- cwd: millRootPath,
1754
- shell: isWin,
1755
- maxBuffer: MAX_BUFFER * 10,
1756
- });
1757
- if (sresult.status !== 0 || sresult.error) {
1758
- if (options.failOnError || DEBUG_MODE) {
1759
- console.error(sresult.stdout, sresult.stderr);
1760
- }
1761
- options.failOnError && process.exit(1);
1865
+ if (tempCacheDir?.startsWith(getTmpDir())) {
1866
+ safeRmSync(tempCacheDir, {
1867
+ recursive: true,
1868
+ force: true,
1869
+ });
1762
1870
  }
1763
- const millResolveArgs = [...millCommonArgs, "resolve", "__.ivyDepsTree"];
1764
- if (DEBUG_MODE) {
1765
- console.log(
1766
- "Executing",
1767
- millCmd,
1768
- millResolveArgs.join(" "),
1769
- "in",
1871
+
1872
+ if (
1873
+ millFiles?.length &&
1874
+ isPackageManagerAllowed(
1875
+ "mill",
1876
+ ["bazel", "sbt", "gradle", "maven"],
1877
+ options,
1878
+ )
1879
+ ) {
1880
+ const millRootPath = dirname(millFiles[0]);
1881
+ parentComponent = createDefaultParentComponent(
1770
1882
  millRootPath,
1883
+ "maven",
1884
+ options,
1771
1885
  );
1772
- }
1773
- sresult = safeSpawnSync(millCmd, millResolveArgs, {
1774
- cwd: millRootPath,
1775
- shell: isWin,
1776
- });
1777
- if (sresult.status !== 0 || sresult.error) {
1778
- if (options.failOnError || DEBUG_MODE) {
1779
- console.error(sresult.stdout, sresult.stderr);
1780
- }
1781
- options.failOnError && process.exit(1);
1782
- }
1783
- const sstdout = sresult.stdout;
1784
- if (sstdout) {
1785
- parentComponent.components = [];
1786
- const modules = sstdout
1787
- .trim()
1788
- .split("\n")
1789
- .map((a) => a.substring(0, a.lastIndexOf(".")))
1790
- .filter((a) =>
1791
- ["true", "1"].includes(readEnvironmentVariable("MILL_EXCLUDE_TEST"))
1792
- ? !a.endsWith(".test")
1793
- : true,
1794
- );
1795
- const moduleBomRefs = [];
1796
- const packages = new Map();
1797
- const relations = new Map();
1798
- relations.set(parentComponent["bom-ref"], []);
1799
- for (const module of modules) {
1800
- moduleBomRefs.push(
1801
- parseMillDependency(module, packages, relations, millRootPath),
1802
- );
1886
+ const millCmd = getMillCommand(millRootPath);
1887
+ const millCommonArgs = [
1888
+ "--color",
1889
+ "false",
1890
+ "--disable-callgraph",
1891
+ "--disable-prompt",
1892
+ "--keep-going",
1893
+ "--silent",
1894
+ ];
1895
+ if (!["true", "1"].includes(readEnvironmentVariable("MILL_USE_SERVER"))) {
1896
+ millCommonArgs.unshift("--no-server");
1803
1897
  }
1804
- for (const module of moduleBomRefs) {
1805
- parentComponent.components.push(packages.get(module));
1806
- relations.get(parentComponent["bom-ref"]).push(module);
1807
- packages.delete(module);
1898
+ const millArgs = [...millCommonArgs, "__.ivyDepsTree"];
1899
+ if (DEBUG_MODE) {
1900
+ console.log("Executing", millCmd, "in", millRootPath);
1808
1901
  }
1809
- const newDependencies = [];
1810
- for (const [ref, dependsOn] of relations.entries()) {
1811
- newDependencies.push({
1812
- ref,
1813
- dependsOn,
1902
+ let sresult = safeSpawnSync(millCmd, millArgs, {
1903
+ cwd: millRootPath,
1904
+ shell: isWin,
1905
+ maxBuffer: MAX_BUFFER * 10,
1906
+ });
1907
+ if (sresult.status !== 0 || sresult.error) {
1908
+ if (options.failOnError || DEBUG_MODE) {
1909
+ console.error(sresult.stdout, sresult.stderr);
1910
+ }
1911
+ deferFailOnError(options, {
1912
+ ecosystem: "java",
1913
+ tool: "mill",
1914
+ exitCode:
1915
+ typeof sresult.status === "number" ? sresult.status : undefined,
1916
+ detail: "the mill ivyDepsTree command failed",
1814
1917
  });
1815
1918
  }
1919
+ const millResolveArgs = [...millCommonArgs, "resolve", "__.ivyDepsTree"];
1816
1920
  if (DEBUG_MODE) {
1817
1921
  console.log(
1818
- `Obtained ${packages.size} components and ${relations.size} dependencies from mill.`,
1922
+ "Executing",
1923
+ millCmd,
1924
+ millResolveArgs.join(" "),
1925
+ "in",
1926
+ millRootPath,
1819
1927
  );
1820
1928
  }
1821
- pkgList = pkgList.concat(...packages.values());
1822
- dependencies = mergeDependencies(
1823
- dependencies,
1824
- newDependencies,
1825
- parentComponent,
1826
- );
1827
- }
1828
- if (
1829
- (!readEnvironmentVariable("MILL_SHUTDOWN_SERVER") &&
1830
- ["true", "1"].includes(readEnvironmentVariable("MILL_USE_SERVER"))) ||
1831
- ["true", "1"].includes(readEnvironmentVariable("MILL_SHUTDOWN_SERVER"))
1832
- ) {
1833
- if (DEBUG_MODE) {
1834
- console.log("Shutting down mill server...");
1835
- }
1836
- const sresult = safeSpawnSync(millCmd, ["shutdown"], {
1929
+ sresult = safeSpawnSync(millCmd, millResolveArgs, {
1837
1930
  cwd: millRootPath,
1838
1931
  shell: isWin,
1839
1932
  });
@@ -1841,11 +1934,97 @@ export async function createJavaBom(path, options) {
1841
1934
  if (options.failOnError || DEBUG_MODE) {
1842
1935
  console.error(sresult.stdout, sresult.stderr);
1843
1936
  }
1844
- options.failOnError && process.exit(1);
1937
+ deferFailOnError(options, {
1938
+ ecosystem: "java",
1939
+ tool: "mill",
1940
+ exitCode:
1941
+ typeof sresult.status === "number" ? sresult.status : undefined,
1942
+ detail: "the mill resolve command failed",
1943
+ });
1944
+ }
1945
+ const sstdout = sresult.stdout;
1946
+ if (sstdout) {
1947
+ parentComponent.components = [];
1948
+ const modules = sstdout
1949
+ .trim()
1950
+ .split("\n")
1951
+ .map((a) => a.substring(0, a.lastIndexOf(".")))
1952
+ .filter((a) =>
1953
+ ["true", "1"].includes(readEnvironmentVariable("MILL_EXCLUDE_TEST"))
1954
+ ? !a.endsWith(".test")
1955
+ : true,
1956
+ );
1957
+ const moduleBomRefs = [];
1958
+ const packages = new Map();
1959
+ const relations = new Map();
1960
+ relations.set(parentComponent["bom-ref"], []);
1961
+ for (const module of modules) {
1962
+ moduleBomRefs.push(
1963
+ parseMillDependency(module, packages, relations, millRootPath),
1964
+ );
1965
+ }
1966
+ for (const module of moduleBomRefs) {
1967
+ parentComponent.components.push(packages.get(module));
1968
+ relations.get(parentComponent["bom-ref"]).push(module);
1969
+ packages.delete(module);
1970
+ }
1971
+ const newDependencies = [];
1972
+ for (const [ref, dependsOn] of relations.entries()) {
1973
+ newDependencies.push({
1974
+ ref,
1975
+ dependsOn,
1976
+ });
1977
+ }
1978
+ if (DEBUG_MODE) {
1979
+ console.log(
1980
+ `Obtained ${packages.size} components and ${relations.size} dependencies from mill.`,
1981
+ );
1982
+ }
1983
+ pkgList = pkgList.concat(...packages.values());
1984
+ dependencies = mergeDependencies(
1985
+ dependencies,
1986
+ newDependencies,
1987
+ parentComponent,
1988
+ );
1989
+ }
1990
+ if (
1991
+ (!readEnvironmentVariable("MILL_SHUTDOWN_SERVER") &&
1992
+ ["true", "1"].includes(readEnvironmentVariable("MILL_USE_SERVER"))) ||
1993
+ ["true", "1"].includes(readEnvironmentVariable("MILL_SHUTDOWN_SERVER"))
1994
+ ) {
1995
+ if (DEBUG_MODE) {
1996
+ console.log("Shutting down mill server...");
1997
+ }
1998
+ const sresult = safeSpawnSync(millCmd, ["shutdown"], {
1999
+ cwd: millRootPath,
2000
+ shell: isWin,
2001
+ });
2002
+ if (sresult.status !== 0 || sresult.error) {
2003
+ if (options.failOnError || DEBUG_MODE) {
2004
+ console.error(sresult.stdout, sresult.stderr);
2005
+ }
2006
+ deferFailOnError(options, {
2007
+ ecosystem: "java",
2008
+ tool: "mill",
2009
+ exitCode:
2010
+ typeof sresult.status === "number" ? sresult.status : undefined,
2011
+ detail: "shutting down the mill server failed",
2012
+ });
2013
+ }
1845
2014
  }
1846
2015
  }
2016
+ } catch (err) {
2017
+ if (!isDeferredFailOnError(err)) {
2018
+ throw err;
2019
+ }
2020
+ // The deferred fail-on-error abort kept everything collected so far;
2021
+ // the incomplete-result fallbacks stayed skipped.
2022
+ thoughtLog(
2023
+ "Completing the java BOM with the components collected before the deferred failure.",
2024
+ );
1847
2025
  }
1848
-
2026
+ // Deduplication and metadata belong to whatever was collected, so they run
2027
+ // on the deferred path too.
1849
2028
  pkgList = trimComponents(pkgList);
1850
2029
  pkgList = await getMvnMetadata(pkgList, jarNSMapping, options.deep);
1851
2030
  return buildBomNSData(options, pkgList, "maven", {