@cdxgen/cdxgen 13.5.0 → 13.6.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 (182) hide show
  1. package/README.md +6 -6
  2. package/bin/cdxgen.js +4 -0
  3. package/bin/evinse.js +3 -0
  4. package/bin/validate.js +6 -0
  5. package/data/license-aliases.json +24 -2
  6. package/data/remediations.json +124 -0
  7. package/data/rules/ai-bom.yaml +1 -1
  8. package/data/rules/brotli-decoder.yaml +107 -0
  9. package/data/rules/vscode-extensions.yaml +75 -0
  10. package/data/spdx-license-list.json +121 -7
  11. package/data/spdx-licenses.json +69 -0
  12. package/data/spdx.schema.json +845 -2
  13. package/lib/cli/bomAssembly.js +92 -3
  14. package/lib/cli/index.js +108 -1
  15. package/lib/cli/jsBom.js +153 -64
  16. package/lib/cli/jvmBom.js +237 -46
  17. package/lib/cli/managedBom.js +142 -16
  18. package/lib/cli/nativeBom.js +42 -5
  19. package/lib/core/boundedFile.js +344 -0
  20. package/lib/core/brotliProbe.js +182 -0
  21. package/lib/core/env.js +8 -0
  22. package/lib/core/fs.js +12 -0
  23. package/lib/core/httpClient.js +98 -1
  24. package/lib/core/lockPaths.js +38 -0
  25. package/lib/core/paths.js +24 -1
  26. package/lib/core/processTree.js +5 -0
  27. package/lib/core/toolRegistry.js +170 -0
  28. package/lib/ecosystems/cppEvidence.js +263 -106
  29. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  30. package/lib/ecosystems/ecosystems.js +25 -10
  31. package/lib/ecosystems/gemHostProbe.js +301 -0
  32. package/lib/ecosystems/gradleutils.js +828 -224
  33. package/lib/ecosystems/parsers-bazel.js +107 -6
  34. package/lib/ecosystems/parsers-js.js +505 -22
  35. package/lib/ecosystems/parsers-misc.js +209 -20
  36. package/lib/ecosystems/parsers-python.js +89 -2
  37. package/lib/ecosystems/pythonutils.js +128 -2
  38. package/lib/ecosystems/rubyutils.js +538 -31
  39. package/lib/ecosystems/scalacliutils.js +2 -0
  40. package/lib/ecosystems/scalasem.js +796 -151
  41. package/lib/ecosystems/vendoredCode.js +237 -3
  42. package/lib/ecosystems/venvProbe.js +360 -0
  43. package/lib/evinser/evinser.js +346 -43
  44. package/lib/helpers/agenticToolutils.js +413 -93
  45. package/lib/helpers/vsixutils.js +1574 -315
  46. package/lib/inventory/atomUtils.js +77 -0
  47. package/lib/inventory/brotliDecoder.js +142 -0
  48. package/lib/inventory/buildToolRateLimit.js +1 -1
  49. package/lib/inventory/cbomutils.js +56 -7
  50. package/lib/inventory/deps.js +35 -0
  51. package/lib/inventory/dosai.js +139 -5
  52. package/lib/inventory/dotnetRuntime.js +217 -0
  53. package/lib/inventory/envcontext.js +307 -13
  54. package/lib/inventory/evidenceUtils.js +11 -0
  55. package/lib/inventory/fetchBatch.js +1 -1
  56. package/lib/inventory/formulationParsers.js +31 -3
  57. package/lib/inventory/gtfobins.js +13 -0
  58. package/lib/inventory/hostRuntimeProbe.js +686 -0
  59. package/lib/inventory/jvmLocalRepos.js +9 -2
  60. package/lib/inventory/jvmToolEnv.js +29 -0
  61. package/lib/inventory/licenseEnhancer.js +12 -0
  62. package/lib/inventory/mavenCentralMirror.js +534 -0
  63. package/lib/inventory/nodeBuildInfo.js +218 -0
  64. package/lib/inventory/nodeHostProbe.js +202 -0
  65. package/lib/inventory/osPackageResolver.js +182 -0
  66. package/lib/inventory/phpBuildInfo.js +10 -59
  67. package/lib/inventory/phpHostProbe.js +32 -488
  68. package/lib/inventory/rubyBuildInfo.js +972 -0
  69. package/lib/inventory/rubyHostProbe.js +218 -0
  70. package/lib/inventory/scalaCoords.js +4 -3
  71. package/lib/inventory/scalaDedupe.js +440 -0
  72. package/lib/inventory/source.js +49 -2
  73. package/lib/managers/binary.js +337 -3
  74. package/lib/managers/docker.js +79 -0
  75. package/lib/parsers/largeJson.js +28 -11
  76. package/lib/parsers/vscodeExtensionId.js +183 -0
  77. package/lib/server/server.js +12 -1
  78. package/lib/stages/postgen/ruleEngine.js +13 -5
  79. package/lib/stages/pregen/envAudit.js +1 -0
  80. package/lib/stages/pregen/pregen.js +114 -57
  81. package/lib/validator/bomValidator.js +6 -1
  82. package/lib/validator/generated/validate-1.6.mjs +1 -1
  83. package/lib/validator/generated/validate-1.7.mjs +1 -1
  84. package/lib/validator/generated/validate-2.0.mjs +1 -1
  85. package/lib/validator/index.js +1 -1
  86. package/package.json +3 -2
  87. package/types/lib/cli/bomAssembly.d.ts +13 -1
  88. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/cli/jsBom.d.ts +1 -17
  91. package/types/lib/cli/jsBom.d.ts.map +1 -1
  92. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  93. package/types/lib/cli/managedBom.d.ts.map +1 -1
  94. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  95. package/types/lib/core/boundedFile.d.ts +70 -0
  96. package/types/lib/core/boundedFile.d.ts.map +1 -0
  97. package/types/lib/core/brotliProbe.d.ts +37 -0
  98. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  99. package/types/lib/core/env.d.ts +4 -0
  100. package/types/lib/core/env.d.ts.map +1 -1
  101. package/types/lib/core/fs.d.ts.map +1 -1
  102. package/types/lib/core/httpClient.d.ts +24 -0
  103. package/types/lib/core/httpClient.d.ts.map +1 -1
  104. package/types/lib/core/lockPaths.d.ts +19 -0
  105. package/types/lib/core/lockPaths.d.ts.map +1 -0
  106. package/types/lib/core/processTree.d.ts +2 -0
  107. package/types/lib/core/processTree.d.ts.map +1 -1
  108. package/types/lib/core/toolRegistry.d.ts +36 -0
  109. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  110. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  111. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  112. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  113. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  114. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  115. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  116. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  117. package/types/lib/ecosystems/gradleutils.d.ts +47 -13
  118. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  121. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
  123. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-python.d.ts +10 -0
  125. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  126. package/types/lib/ecosystems/pythonutils.d.ts +20 -2
  127. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  128. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  129. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  131. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  132. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  133. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  134. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  135. package/types/lib/evinser/evinser.d.ts +63 -8
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/inventory/atomUtils.d.ts +19 -0
  138. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  139. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  140. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  141. package/types/lib/inventory/cbomutils.d.ts +17 -0
  142. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  143. package/types/lib/inventory/deps.d.ts.map +1 -1
  144. package/types/lib/inventory/dosai.d.ts +17 -1
  145. package/types/lib/inventory/dosai.d.ts.map +1 -1
  146. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  147. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  148. package/types/lib/inventory/envcontext.d.ts +52 -6
  149. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  150. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  151. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  152. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  153. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  154. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  155. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  156. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  157. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  158. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  159. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  160. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  161. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  162. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  163. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  164. package/types/lib/inventory/osPackageResolver.d.ts +38 -0
  165. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  166. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
  167. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
  168. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  169. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  170. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  171. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  172. package/types/lib/inventory/source.d.ts +10 -0
  173. package/types/lib/inventory/source.d.ts.map +1 -1
  174. package/types/lib/managers/binary.d.ts.map +1 -1
  175. package/types/lib/managers/docker.d.ts +12 -0
  176. package/types/lib/managers/docker.d.ts.map +1 -1
  177. package/types/lib/parsers/largeJson.d.ts +4 -3
  178. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  179. package/types/lib/server/server.d.ts.map +1 -1
  180. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/pregen.d.ts +12 -8
  182. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
@@ -31,6 +31,7 @@ import {
31
31
  } from "../inventory/buildToolRateLimit.js";
32
32
  import { collectJarNS } from "../inventory/deps.js";
33
33
  import { minimumGradleVersionForJava } from "../inventory/jvmToolEnv.js";
34
+ import { gradleMirrorInitScriptArgs } from "../inventory/mavenCentralMirror.js";
34
35
  import { applyPurl, tryBuildPurl } from "../inventory/purl.js";
35
36
  import { parsePkgJson } from "./parsers-js.js";
36
37
 
@@ -218,6 +219,50 @@ function gradleEvaluationOverrides(gradleArguments) {
218
219
  return overrides;
219
220
  }
220
221
 
222
+ /**
223
+ * Keep Gradle's task headers in the output. cdxgen splits the `properties`
224
+ * and `dependencies` reports by the `> Task :path` headers, which Gradle
225
+ * prints at the lifecycle log level, so a project that sets
226
+ * `org.gradle.logging.level=quiet` or `warn` in `gradle.properties` would
227
+ * leave nothing to split. A command-line `-D` property beats
228
+ * `gradle.properties`, and a log-level option such as `--info` beats the
229
+ * property. A log level the user chose in `GRADLE_ARGS` is left alone.
230
+ *
231
+ * @param {string[]} gradleArguments User-supplied general gradle arguments
232
+ * @returns {string[]} The `-D` flag to inject, or nothing
233
+ */
234
+ function gradleLogLevelOverride(gradleArguments) {
235
+ const logLevelOptions = [
236
+ "-q",
237
+ "--quiet",
238
+ "-w",
239
+ "--warn",
240
+ "-i",
241
+ "--info",
242
+ "-d",
243
+ "--debug",
244
+ ];
245
+ const userChose = gradleArguments.some(
246
+ (arg) =>
247
+ logLevelOptions.includes(arg) ||
248
+ arg.startsWith("-Dorg.gradle.logging.level="),
249
+ );
250
+ return userChose ? [] : ["-Dorg.gradle.logging.level=lifecycle"];
251
+ }
252
+
253
+ /**
254
+ * Read a whitespace-separated list of Gradle arguments from an environment
255
+ * variable such as `GRADLE_ARGS`. Repeated, leading or trailing whitespace
256
+ * adds no empty argument, which Gradle would read as a task path.
257
+ *
258
+ * @param {string} name Name of the environment variable
259
+ * @returns {string[]} The arguments, empty when the variable is unset
260
+ */
261
+ export function gradleArgsFromEnv(name) {
262
+ const value = readEnvironmentVariable(name);
263
+ return value ? value.split(/\s+/).filter(Boolean) : [];
264
+ }
265
+
221
266
  /**
222
267
  * Method to combine the general gradle arguments, the sub-commands and the sub-commands' arguments in the correct way
223
268
  *
@@ -242,6 +287,8 @@ export function buildGradleCommandArguments(
242
287
  ]
243
288
  .concat(getGradleDaemonParameter())
244
289
  .concat(gradleEvaluationOverrides(gradleArguments))
290
+ .concat(gradleLogLevelOverride(gradleArguments))
291
+ .concat(gradleMirrorInitScriptArgs())
245
292
  .concat(gradleArguments);
246
293
  const maxCliArgsLength = isWin
247
294
  ? 7500 - gradleCommandLength - mainGradleArguments.join(" ").length - 2
@@ -257,7 +304,13 @@ export function buildGradleCommandArguments(
257
304
  for (const gradleSubCommand of gradleSubCommands) {
258
305
  const subCommandLength =
259
306
  [gradleSubCommand, ...gradleSubCommandArguments].join(" ").length + 1;
260
- if (maxCliArgsLength !== -1 && remainingLength - subCommandLength < 0) {
307
+ // An invocation always carries at least one task: without one Gradle
308
+ // runs the project's default tasks.
309
+ if (
310
+ maxCliArgsLength !== -1 &&
311
+ remainingLength - subCommandLength < 0 &&
312
+ allGradleArguments.length > mainGradleArguments.length
313
+ ) {
261
314
  splitArgs.push(allGradleArguments);
262
315
  allGradleArguments = [].concat(mainGradleArguments);
263
316
  remainingLength = maxCliArgsLength;
@@ -297,16 +350,52 @@ function parseGradleTaskHeader(line) {
297
350
  if (!line.startsWith("> Task :")) {
298
351
  return undefined;
299
352
  }
300
- // The path is the first token: a status such as UP-TO-DATE or FAILED may
301
- // follow it, and a CRLF line keeps its carriage return.
302
- const taskPath = line.slice("> Task ".length).trim().split(/\s+/)[0];
353
+ // A project path may contain spaces (`:my lib`), and a status such as
354
+ // UP-TO-DATE or FAILED may follow the task name, so the task name is the
355
+ // first token after the last colon. A status never contains a colon, and a
356
+ // CRLF line keeps its carriage return.
357
+ const taskPath = line.slice("> Task ".length).trim();
303
358
  const lastColon = taskPath.lastIndexOf(":");
304
359
  return {
305
360
  projectPath: taskPath.slice(0, lastColon),
306
- taskName: taskPath.slice(lastColon + 1),
361
+ taskName: taskPath.slice(lastColon + 1).split(/\s+/)[0],
307
362
  };
308
363
  }
309
364
 
365
+ /**
366
+ * List the dependency tasks to run for the root project and every module read
367
+ * from `gradle properties`.
368
+ *
369
+ * The root project is recognised by identity, never by purl. A module named
370
+ * after the root project that inherits its group and version has the root's
371
+ * purl (issue 4695), and it still needs its own `:module:dependencies` task.
372
+ * `root` in `modulesToSkip` drops the root task; any other entry is a module
373
+ * path.
374
+ *
375
+ * @param {Map<string, Object>} gradleModules Modules keyed by project path, and the root project by its name
376
+ * @param {Object} rootComponent The component built for the root project
377
+ * @param {string} gradleDepTask Name of the dependency task, usually `dependencies`
378
+ * @param {string[]} modulesToSkip Module paths, or `root`, to leave out
379
+ * @returns {string[]} Task paths in the order Gradle should run them
380
+ */
381
+ export function gradleDependencyTasks(
382
+ gradleModules,
383
+ rootComponent,
384
+ gradleDepTask,
385
+ modulesToSkip = [],
386
+ ) {
387
+ const tasks = [];
388
+ if (!modulesToSkip.includes("root")) {
389
+ tasks.push(gradleDepTask);
390
+ }
391
+ for (const [key, module] of gradleModules) {
392
+ if (module !== rootComponent && !modulesToSkip.includes(key)) {
393
+ tasks.push(`${key}:${gradleDepTask}`);
394
+ }
395
+ }
396
+ return tasks;
397
+ }
398
+
310
399
  /**
311
400
  * Method to split the output produced by Gradle using parallel processing by project
312
401
  *
@@ -398,27 +487,19 @@ export function parseGradleProjects(rawOutput) {
398
487
  }
399
488
  }
400
489
  }
401
- } else if (l.includes("--- project ")) {
402
- const tmpB = l.split("--- project ");
403
- if (tmpB && tmpB.length > 1) {
404
- const projName = tmpB[1];
405
- if (projName.startsWith(":")) {
406
- const tmpName = projName.split(" ")[0];
407
- if (tmpName.length > 1) {
408
- projects.add(tmpName);
409
- }
490
+ } else {
491
+ for (const marker of ["--- project ", "-> project "]) {
492
+ const markerIndex = l.indexOf(marker);
493
+ if (markerIndex === -1) {
494
+ continue;
410
495
  }
411
- }
412
- } else if (l.includes("-> project ")) {
413
- const tmpB = l.split("-> project ");
414
- if (tmpB && tmpB.length > 1) {
415
- const projName = tmpB[1];
416
- if (projName.startsWith(":")) {
417
- const tmpName = projName.split(" ")[0];
418
- if (tmpName.length > 1) {
419
- projects.add(tmpName);
420
- }
496
+ const projName = gradleProjectPath(
497
+ l.slice(markerIndex + marker.length),
498
+ );
499
+ if (projName?.startsWith(":") && projName.length > 1) {
500
+ projects.add(projName);
421
501
  }
502
+ break;
422
503
  }
423
504
  }
424
505
  });
@@ -432,11 +513,17 @@ export function parseGradleProjects(rawOutput) {
432
513
  /**
433
514
  * Parse gradle properties output
434
515
  *
516
+ * `gradlePlugin` is true when the project has the `gradlePlugin` extension,
517
+ * which `java-gradle-plugin`, `kotlin-dsl` and `groovy-gradle-plugin` add: the
518
+ * project builds Gradle plugins, as a `build-logic` build does.
519
+ *
435
520
  * @param {string} rawOutput Raw string output
436
521
  * @param {string} gradleModuleName The name (or 'path') of the module as seen from the root of the project
522
+ * @returns {{rootProject: string, projects: string[], metadata: Object, gradlePlugin: boolean}} The parsed properties
437
523
  */
438
524
  export function parseGradleProperties(rawOutput, gradleModuleName = null) {
439
525
  let rootProject = "root";
526
+ let gradlePlugin = false;
440
527
  const projects = new Set();
441
528
  const metadata = { group: "", version: "latest", properties: [] };
442
529
  if (gradleModuleName) {
@@ -472,6 +559,8 @@ export function parseGradleProperties(rawOutput, gradleModuleName = null) {
472
559
  metadata[tmpB[0]] = tmpB[1].trim().replace("unspecified", "latest");
473
560
  } else if (["buildFile", "projectDir", "rootDir"].includes(tmpB[0])) {
474
561
  metadata.properties.push({ name: tmpB[0], value: tmpB[1].trim() });
562
+ } else if (tmpB[0] === "gradlePlugin") {
563
+ gradlePlugin = true;
475
564
  } else if (tmpB[0] === "subprojects") {
476
565
  const spStrs = tmpB[1].replace(/[[\]']/g, "").split(", ");
477
566
  const tmpprojects = spStrs
@@ -486,6 +575,7 @@ export function parseGradleProperties(rawOutput, gradleModuleName = null) {
486
575
  rootProject,
487
576
  projects: Array.from(projects),
488
577
  metadata,
578
+ gradlePlugin,
489
579
  };
490
580
  }
491
581
 
@@ -511,17 +601,11 @@ export function executeParallelGradleProperties(
511
601
  }
512
602
  const gradleCmd = getGradleCommand(dir, null, options);
513
603
  const gradleArgs = buildGradleCommandArguments(
514
- extraArgs.concat(
515
- readEnvironmentVariable("GRADLE_ARGS")
516
- ? readEnvironmentVariable("GRADLE_ARGS").split(" ")
517
- : [],
518
- ),
604
+ extraArgs.concat(gradleArgsFromEnv("GRADLE_ARGS")),
519
605
  allProjectsStr.map((project) =>
520
606
  project ? `${project}:properties` : "properties",
521
607
  ),
522
- readEnvironmentVariable("GRADLE_ARGS_PROPERTIES")
523
- ? readEnvironmentVariable("GRADLE_ARGS_PROPERTIES").split(" ")
524
- : [],
608
+ gradleArgsFromEnv("GRADLE_ARGS_PROPERTIES"),
525
609
  gradleCmd.length,
526
610
  );
527
611
  const allOutputs = [];
@@ -880,19 +964,526 @@ export function recordGradleInvocationFailure(result, context = {}) {
880
964
  });
881
965
  }
882
966
 
967
+ /**
968
+ * Configurations whose dependencies never reach the packaged application:
969
+ * code-quality tools, coverage agents and Spring Boot's development-only
970
+ * dependencies.
971
+ */
972
+ const GRADLE_TOOL_CONFIGURATIONS = new Set([
973
+ "checkstyle",
974
+ "codenarc",
975
+ "detekt",
976
+ "developmentOnly",
977
+ "errorprone",
978
+ "jacocoAgent",
979
+ "jacocoAnt",
980
+ "ktlint",
981
+ "lintChecks",
982
+ "lintClassPath",
983
+ "pmd",
984
+ "spotbugs",
985
+ "spotbugsPlugins",
986
+ ]);
987
+
988
+ /**
989
+ * Parts of a configuration name that mark it as feeding the compiler or a
990
+ * build tool, such as `annotationProcessor`, Android's
991
+ * `debugAnnotationProcessorClasspath` or `kotlinCompilerPluginClasspathMain`.
992
+ */
993
+ const GRADLE_TOOL_CONFIGURATION_PARTS = [
994
+ "annotationprocessor",
995
+ "buildtoolsapiclasspath",
996
+ "commonizer",
997
+ "compilerclasspath",
998
+ "compilerpluginclasspath",
999
+ ];
1000
+
1001
+ /**
1002
+ * Properties that describe a dependency Gradle could not resolve.
1003
+ */
1004
+ const GRADLE_FAILURE_PROPERTIES = [
1005
+ "cdx:gradle:resolutionFailed",
1006
+ "cdx:gradle:requestedVersion",
1007
+ ];
1008
+
1009
+ /**
1010
+ * Markers Gradle appends to a dependency tree line, with the flag each sets.
1011
+ */
1012
+ const GRADLE_TREE_LINE_MARKERS = [
1013
+ ["(*)", "omitted"],
1014
+ ["(c)", "constraint"],
1015
+ ["(n)", "unresolvable"],
1016
+ ["FAILED", "failed"],
1017
+ ];
1018
+
1019
+ /**
1020
+ * Read the name of the configuration a `gradle dependencies` header line
1021
+ * introduces, such as `runtimeClasspath - Runtime classpath of source set
1022
+ * 'main'.`, `api (n)`, a bare `classpath`, a Groovy name such as
1023
+ * `integration-test` or an internal one such as
1024
+ * `_internal-unified-test-platform-android-device-provider-ddmlib`.
1025
+ *
1026
+ * @param {string} line One line of the report
1027
+ * @returns {string|undefined} The configuration name, or undefined when the
1028
+ * line is not a configuration header
1029
+ */
1030
+ function parseGradleConfigurationHeader(line) {
1031
+ let name = line.split(" - ")[0];
1032
+ if (name.endsWith(" (n)")) {
1033
+ name = name.slice(0, -" (n)".length);
1034
+ }
1035
+ return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(name) ? name : undefined;
1036
+ }
1037
+
1038
+ /**
1039
+ * Configurations, other than runtime classpaths, whose dependencies ship
1040
+ * with the application: `compile` and `default` in Gradle 6 and older, which
1041
+ * the runtime classpath extended, and the Shadow plugin's `shadow`, whose
1042
+ * dependencies the published artifact declares as runtime dependencies.
1043
+ */
1044
+ const GRADLE_SHIPPED_CONFIGURATIONS = new Set(["compile", "default", "shadow"]);
1045
+
1046
+ /**
1047
+ * Map a Gradle configuration to the CycloneDX scope of the dependencies it
1048
+ * resolves, so that only what ships with the application reads as required.
1049
+ *
1050
+ * Configurations whose name starts with `_`, such as the Android Gradle
1051
+ * plugin's `_internal-unified-test-platform-*` ones, are the plugin's own
1052
+ * tooling and `excluded`. Test configurations are `optional`. Runtime
1053
+ * classpaths, including Android
1054
+ * variants such as `debugRuntimeClasspath` and Kotlin targets such as
1055
+ * `jvmRuntimeClasspath`, are `required`, and so are Kotlin/Native klibraries,
1056
+ * which are linked into the binary, and the configurations in
1057
+ * `GRADLE_SHIPPED_CONFIGURATIONS`. Everything else is `excluded`: annotation
1058
+ * processors, compiler, code-quality and other build-tool classpaths, and
1059
+ * compile classpaths, whose dependencies a runtime classpath also lists unless
1060
+ * they are compile-only, as Maven's `provided` scope is. A dependency seen in
1061
+ * several configurations keeps the strongest scope among them. Test and
1062
+ * runtime are matched as whole words of the camel-case name, so
1063
+ * `latestRuntimeClasspath` is a runtime classpath, not a test one.
1064
+ *
1065
+ * @param {string} configurationName Name of the configuration
1066
+ * @returns {string} The CycloneDX scope
1067
+ */
1068
+ function gradleConfigurationScope(configurationName) {
1069
+ const lowerName = configurationName.toLowerCase();
1070
+ const words = configurationName
1071
+ .split(/[-_.]|(?=[A-Z])/)
1072
+ .map((word) => word.toLowerCase());
1073
+ if (
1074
+ GRADLE_TOOL_CONFIGURATIONS.has(configurationName) ||
1075
+ configurationName.startsWith("_") ||
1076
+ ["kapt", "ksp"].includes(words[0]) ||
1077
+ GRADLE_TOOL_CONFIGURATION_PARTS.some((part) => lowerName.includes(part))
1078
+ ) {
1079
+ return "excluded";
1080
+ }
1081
+ if (words.includes("test") || words.includes("tests")) {
1082
+ return "optional";
1083
+ }
1084
+ if (
1085
+ words.includes("runtime") ||
1086
+ words.includes("klibraries") ||
1087
+ GRADLE_SHIPPED_CONFIGURATIONS.has(configurationName)
1088
+ ) {
1089
+ return "required";
1090
+ }
1091
+ return "excluded";
1092
+ }
1093
+
1094
+ /**
1095
+ * Rank a scope so that a dependency seen in several configurations keeps the
1096
+ * strongest one: `required` or no scope (which CycloneDX reads as required),
1097
+ * then `optional`, then `excluded`.
1098
+ *
1099
+ * @param {string|undefined} scope CycloneDX scope
1100
+ * @returns {number} The rank, higher being stronger
1101
+ */
1102
+ function gradleScopeRank(scope) {
1103
+ if (scope === "required" || scope === undefined) {
1104
+ return 2;
1105
+ }
1106
+ return scope === "optional" ? 1 : 0;
1107
+ }
1108
+
1109
+ /**
1110
+ * Read a project path from the text that follows `project ` in a dependency
1111
+ * tree line. Gradle 9 quotes the path (`project ':lib'`), Gradle 8 does not
1112
+ * (`project :lib`).
1113
+ *
1114
+ * @param {string} text The text after `project `
1115
+ * @returns {string|undefined} The project path, such as `:lib`
1116
+ */
1117
+ function gradleProjectPath(text) {
1118
+ const trimmed = text.trim();
1119
+ if (trimmed.startsWith("'")) {
1120
+ const end = trimmed.indexOf("'", 1);
1121
+ return end > 1 ? trimmed.slice(1, end) : undefined;
1122
+ }
1123
+ return trimmed.split(" ")[0] || undefined;
1124
+ }
1125
+
1126
+ /**
1127
+ * Find the module a project path in a dependency tree names. Subprojects are
1128
+ * keyed by their path (`:lib`) and the root project by its name, so the root
1129
+ * path `:` resolves to the report's own project when that is the root, or else
1130
+ * to the one module keyed by a name.
1131
+ *
1132
+ * @param {Map} gradleModules Modules keyed by project path or root name
1133
+ * @param {string} projectPath Project path from the tree, such as `:lib` or `:`
1134
+ * @param {string} reportProjectName Key of the project the report belongs to
1135
+ * @returns {Object|undefined} The module component
1136
+ */
1137
+ function gradleModuleForPath(gradleModules, projectPath, reportProjectName) {
1138
+ if (projectPath !== ":") {
1139
+ return gradleModules.get(projectPath);
1140
+ }
1141
+ if (!reportProjectName.startsWith(":")) {
1142
+ return gradleModules.get(reportProjectName);
1143
+ }
1144
+ const rootKeys = [...gradleModules.keys()].filter((k) => !k.startsWith(":"));
1145
+ return rootKeys.length === 1 ? gradleModules.get(rootKeys[0]) : undefined;
1146
+ }
1147
+
1148
+ /**
1149
+ * Pick a concrete version out of the version Gradle printed as requested. A
1150
+ * rich version such as `{strictly 1.0}`, `{require 2.2; reject 2.0}` or
1151
+ * `{prefer 2.0.9}` gives the version of its first strictly, require or prefer
1152
+ * clause. A range or dynamic version gives none.
1153
+ *
1154
+ * @param {string|undefined} versionText The requested version
1155
+ * @returns {string|undefined} The concrete version
1156
+ */
1157
+ function gradleRequestedVersion(versionText) {
1158
+ if (!versionText) {
1159
+ return undefined;
1160
+ }
1161
+ // `1.+` and `latest.release` are dynamic and a bracketed range is a
1162
+ // range, but `0.92.2+1.20.1` is a concrete version with build metadata.
1163
+ const isConcrete = (v) =>
1164
+ v &&
1165
+ !v.endsWith("+") &&
1166
+ !v.startsWith("latest.") &&
1167
+ ![..."[](),"].some((ch) => v.includes(ch));
1168
+ if (!versionText.startsWith("{")) {
1169
+ return isConcrete(versionText) ? versionText : undefined;
1170
+ }
1171
+ const body = versionText.slice(1, versionText.lastIndexOf("}"));
1172
+ for (const clause of body.split(";")) {
1173
+ const [keyword, ...rest] = clause.trim().split(" ");
1174
+ const value = rest.join(" ").trim();
1175
+ if (
1176
+ ["strictly", "require", "prefer"].includes(keyword) &&
1177
+ isConcrete(value)
1178
+ ) {
1179
+ return value;
1180
+ }
1181
+ }
1182
+ return undefined;
1183
+ }
1184
+
1185
+ /**
1186
+ * Split one line of a `gradle dependencies` tree into its depth and the
1187
+ * dependency it names. The selected side of `->` wins over the requested
1188
+ * side, whether it is a version, other coordinates or a project.
1189
+ *
1190
+ * @param {string} line One line of the report, without its line ending
1191
+ * @returns {Object|undefined} `{ level, group, name, version,
1192
+ * requestedVersion, projectPath, omitted, constraint, unresolvable, failed }`,
1193
+ * `{ level, unknown: true }` for a tree line whose dependency cannot be
1194
+ * read, or undefined when the line is not a tree line
1195
+ */
1196
+ function parseGradleTreeLine(line) {
1197
+ const markerIndex = line.indexOf("--- ");
1198
+ if (markerIndex < 1) {
1199
+ return undefined;
1200
+ }
1201
+ for (const ch of line.slice(0, markerIndex)) {
1202
+ if (!"|+\\ ".includes(ch)) {
1203
+ return undefined;
1204
+ }
1205
+ }
1206
+ const parsed = {
1207
+ level: Math.floor(markerIndex / 5) + 1,
1208
+ omitted: false,
1209
+ constraint: false,
1210
+ unresolvable: false,
1211
+ failed: false,
1212
+ };
1213
+ let text = line.slice(markerIndex + "--- ".length).trim();
1214
+ let stripped = true;
1215
+ while (stripped) {
1216
+ stripped = false;
1217
+ for (const [marker, flag] of GRADLE_TREE_LINE_MARKERS) {
1218
+ if (text.endsWith(` ${marker}`)) {
1219
+ parsed[flag] = true;
1220
+ text = text.slice(0, -marker.length - 1).trimEnd();
1221
+ stripped = true;
1222
+ }
1223
+ }
1224
+ }
1225
+ const [requested, selected] = text.split(" -> ");
1226
+ if (requested.startsWith("project ")) {
1227
+ parsed.projectPath = gradleProjectPath(requested.slice("project ".length));
1228
+ } else {
1229
+ const [group, name, ...versionParts] = requested.split(":");
1230
+ if (!group || !name || group.includes(" ") || name.includes(" ")) {
1231
+ return { level: parsed.level, unknown: true };
1232
+ }
1233
+ parsed.group = group;
1234
+ parsed.name = name;
1235
+ parsed.requestedVersion = versionParts.join(":") || undefined;
1236
+ parsed.version = gradleRequestedVersion(parsed.requestedVersion);
1237
+ }
1238
+ if (selected?.startsWith("project ")) {
1239
+ parsed.projectPath = gradleProjectPath(selected.slice("project ".length));
1240
+ } else if (selected?.includes(":")) {
1241
+ // The selected module replaces the requested one, version and all.
1242
+ const [group, name, version] = selected.split(":");
1243
+ parsed.group = group;
1244
+ parsed.name = name;
1245
+ parsed.version = version;
1246
+ parsed.requestedVersion = undefined;
1247
+ } else if (selected) {
1248
+ parsed.version = selected;
1249
+ }
1250
+ if (parsed.projectPath === undefined && !parsed.group) {
1251
+ return { level: parsed.level, unknown: true };
1252
+ }
1253
+ return parsed;
1254
+ }
1255
+
1256
+ /**
1257
+ * Read the properties of every project the dependency reports name that
1258
+ * `gradleModules` does not have yet, such as a project of an included build,
1259
+ * and add a module for each one whose properties task printed a report. One
1260
+ * Gradle invocation covers all of them.
1261
+ *
1262
+ * @param {Iterable<string>} reports Text of the `gradle dependencies` reports
1263
+ * @param {Map} gradleModules Modules keyed by project path, updated in place
1264
+ * @param {string} gradleRootPath Directory to run Gradle in
1265
+ * @param {Object} options CLI options
1266
+ * @returns {Promise<string[]>} Paths of the projects added
1267
+ */
1268
+ export async function loadGradleModulesForReports(
1269
+ reports,
1270
+ gradleModules,
1271
+ gradleRootPath,
1272
+ options = {},
1273
+ ) {
1274
+ const modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
1275
+ ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
1276
+ : [];
1277
+ const missing = new Set();
1278
+ for (const report of reports) {
1279
+ for (const projectPath of parseGradleProjects(report).projects) {
1280
+ if (
1281
+ !gradleModules.has(projectPath) &&
1282
+ !modulesToSkip.includes(projectPath)
1283
+ ) {
1284
+ missing.add(projectPath);
1285
+ }
1286
+ }
1287
+ }
1288
+ if (!missing.size || !isProjectToolingAllowed(options)) {
1289
+ return [];
1290
+ }
1291
+ const splitPropTaskOut = splitOutputByGradleProjects(
1292
+ executeParallelGradleProperties(gradleRootPath, [...missing], [], options),
1293
+ ["properties"],
1294
+ );
1295
+ const added = [];
1296
+ for (const projectPath of missing) {
1297
+ // A project whose properties task printed nothing has no metadata to
1298
+ // build a module from.
1299
+ if (!splitPropTaskOut.has(projectPath)) {
1300
+ continue;
1301
+ }
1302
+ const propMap = parseGradleProperties(
1303
+ splitPropTaskOut.get(projectPath),
1304
+ projectPath,
1305
+ );
1306
+ gradleModules.set(
1307
+ projectPath,
1308
+ await buildObjectForGradleModule(
1309
+ propMap.rootProject === "root"
1310
+ ? projectPath.split(":").pop()
1311
+ : propMap.rootProject,
1312
+ propMap.metadata,
1313
+ ),
1314
+ );
1315
+ added.push(projectPath);
1316
+ }
1317
+ return added;
1318
+ }
1319
+
1320
+ /**
1321
+ * Give the components of a build-tooling report, such as that of an included
1322
+ * build that builds Gradle plugins, the `excluded` scope that build-tool
1323
+ * configurations get. `reconcileGradleComponents` later keeps a stronger
1324
+ * scope that another project's report gives the same component.
1325
+ *
1326
+ * @param {Object[]} pkgList Components from one project's report, updated in place
1327
+ * @param {Map<string, Object>} gradleModules Modules, which keep their own scope
1328
+ * @returns {Object[]} The same list
1329
+ */
1330
+ export function excludeGradleBuildTooling(pkgList, gradleModules) {
1331
+ const modules = new Set(gradleModules.values());
1332
+ for (const pkg of pkgList) {
1333
+ if (!modules.has(pkg)) {
1334
+ pkg.scope = "excluded";
1335
+ }
1336
+ }
1337
+ return pkgList;
1338
+ }
1339
+
1340
+ /**
1341
+ * Reconcile the copies of a component that several Gradle projects report.
1342
+ * Every copy gets the strongest scope any project gave it, so a library one
1343
+ * module only tests with stays required when another module compiles against
1344
+ * it, and a dependency one project failed to resolve loses its failure
1345
+ * properties when another project resolved it.
1346
+ *
1347
+ * @param {Object[]} pkgList Components from all the projects, updated in place
1348
+ * @returns {Object[]} The same list
1349
+ */
1350
+ export function reconcileGradleComponents(pkgList) {
1351
+ const strongest = new Map();
1352
+ const resolvedSomewhere = new Set();
1353
+ const isFailed = (pkg) =>
1354
+ pkg.properties?.some((p) => p.name === "cdx:gradle:resolutionFailed");
1355
+ for (const pkg of pkgList) {
1356
+ const key = pkg?.["bom-ref"];
1357
+ if (!key) {
1358
+ continue;
1359
+ }
1360
+ if (
1361
+ !strongest.has(key) ||
1362
+ gradleScopeRank(pkg.scope) > gradleScopeRank(strongest.get(key))
1363
+ ) {
1364
+ strongest.set(key, pkg.scope);
1365
+ }
1366
+ if (!isFailed(pkg)) {
1367
+ resolvedSomewhere.add(key);
1368
+ }
1369
+ }
1370
+ for (const pkg of pkgList) {
1371
+ const key = pkg?.["bom-ref"];
1372
+ if (!key) {
1373
+ continue;
1374
+ }
1375
+ if (strongest.get(key) === undefined) {
1376
+ delete pkg.scope;
1377
+ } else {
1378
+ pkg.scope = strongest.get(key);
1379
+ }
1380
+ if (resolvedSomewhere.has(key) && isFailed(pkg)) {
1381
+ pkg.properties = pkg.properties.filter(
1382
+ (p) => !GRADLE_FAILURE_PROPERTIES.includes(p.name),
1383
+ );
1384
+ if (!pkg.properties.length) {
1385
+ delete pkg.properties;
1386
+ }
1387
+ }
1388
+ }
1389
+ return pkgList;
1390
+ }
1391
+
1392
+ /**
1393
+ * Whether a module name is one that platforms and BOMs use, such as
1394
+ * `jackson-bom`, `spring-boot-dependencies`, `micronaut-platform` or the AWS
1395
+ * SDK's `bom`.
1396
+ *
1397
+ * @param {string} name Module name
1398
+ * @returns {boolean} True for a platform-style name
1399
+ */
1400
+ function isGradlePlatformName(name) {
1401
+ return (
1402
+ name === "bom" ||
1403
+ ["-bom", "-dependencies", "-platform"].some((suffix) =>
1404
+ name.endsWith(suffix),
1405
+ )
1406
+ );
1407
+ }
1408
+
1409
+ /**
1410
+ * Give the platforms and BOMs in one report a `pom` type and the `excluded`
1411
+ * scope. Gradle prints a platform like a library, with only constraints
1412
+ * (`(c)`) under it, but a platform is a pom that never ships. A module counts
1413
+ * as a platform when its name is one platforms use and the report gives it no
1414
+ * dependency of its own, so a jar such as `jna-platform`, which depends on
1415
+ * `jna`, stays a jar.
1416
+ *
1417
+ * @param {Map<string, {adep: Object, isModule: boolean}>} depsByRef Components of the report by bom-ref, updated in place
1418
+ * @param {Object<string, string[]>} levelTrees Edges of the report by bom-ref
1419
+ * @returns {Map<string, string>} The new bom-ref of each renamed component, keyed by its old one
1420
+ */
1421
+ function markGradlePlatforms(depsByRef, levelTrees) {
1422
+ const renamed = new Map();
1423
+ for (const [bomRef, { adep, isModule }] of depsByRef) {
1424
+ if (
1425
+ isModule ||
1426
+ adep.qualifiers?.type !== "jar" ||
1427
+ !isGradlePlatformName(adep.name || "") ||
1428
+ levelTrees[bomRef]?.length
1429
+ ) {
1430
+ continue;
1431
+ }
1432
+ const purl = tryBuildPurl({
1433
+ type: "maven",
1434
+ namespace: adep.group || null,
1435
+ name: adep.name,
1436
+ version: adep.version || null,
1437
+ qualifiers: { type: "pom" },
1438
+ });
1439
+ if (!purl) {
1440
+ continue;
1441
+ }
1442
+ adep.qualifiers = { type: "pom" };
1443
+ adep.scope = "excluded";
1444
+ applyPurl(adep, purl);
1445
+ renamed.set(bomRef, adep["bom-ref"]);
1446
+ }
1447
+ return renamed;
1448
+ }
1449
+
883
1450
  /**
884
1451
  * Method to resolve dependencies from a gradle output
885
1452
  *
1453
+ * A constraint line (`(c)`) adds neither a component nor an edge. Gradle
1454
+ * prints a nested constraint under the platform or module that declares it,
1455
+ * and only when the dependency it affects is resolved elsewhere in the tree,
1456
+ * where that dependency gets its component and edges. A top-level
1457
+ * `{strictly …}` constraint from consistent resolution can name a module the
1458
+ * configuration never resolves. A dependency that failed to resolve carries
1459
+ * `cdx:gradle:resolutionFailed`, keeps the version it was requested at when
1460
+ * that is a concrete version, and otherwise records the request in
1461
+ * `cdx:gradle:requestedVersion`. A dependency seen in several configurations
1462
+ * keeps the strongest scope among them. Platforms and BOMs get a `pom` type
1463
+ * and the `excluded` scope.
1464
+ *
1465
+ * The parser runs no Gradle task. A project the tree names but `gradleModules`
1466
+ * lacks gets a component built from the root project's group and version; use
1467
+ * `loadGradleModulesForReports` first to read such projects' properties.
1468
+ *
1469
+ * The lines Gradle prints under another project, such as `project ':lib'` in
1470
+ * the report of `:app`, show that project's dependencies as `:app` resolved
1471
+ * them, after conflict resolution. When `reportedProjects` names that project,
1472
+ * its own report supplies its edges, so these lines add no edge from it.
1473
+ *
886
1474
  * @param {string} rawOutput Text output from gradle dependencies task
887
- * @param {string} rootProjectName Name of the root project
888
- * @param {map} gradleModules Cache with all gradle modules that have already been read
889
- * @param {string} gradleRootPath Root path where Gradle is to be run when getting module information
1475
+ * @param {string} rootProjectName Key in `gradleModules` of the project the report belongs to
1476
+ * @param {map} gradleModules Gradle modules keyed by project path, and the root project by its name
1477
+ * @param {string} gradleRootPath Directory Gradle ran in, recorded as `cdx:gradle:GradleRootPath`
1478
+ * @param {Set<string>} [reportedProjects] Keys in `gradleModules` of the projects whose own reports are parsed
1479
+ * @returns {{pkgList: Object[], dependenciesList: Object[]}|{}} The components and dependency graph
890
1480
  */
891
1481
  export async function parseGradleDep(
892
1482
  rawOutput,
893
1483
  rootProjectName = "root",
894
1484
  gradleModules = new Map(),
895
1485
  gradleRootPath = "",
1486
+ reportedProjects = new Set(),
896
1487
  ) {
897
1488
  if (typeof rawOutput === "string") {
898
1489
  // Bug: 249. Get any sub-projects refered here
@@ -907,225 +1498,238 @@ export async function parseGradleDep(
907
1498
  ) {
908
1499
  rootProjectName = retMap.rootProject;
909
1500
  }
910
- let match = "";
911
1501
  // To render dependency tree we need a root project
912
1502
  const rootProject = gradleModules.get(rootProjectName);
1503
+ if (!rootProject) {
1504
+ thoughtLog(
1505
+ `The Gradle report for ${rootProjectName} belongs to no module read from 'gradle properties', so it has no parent to hang its dependency tree from.`,
1506
+ );
1507
+ return {};
1508
+ }
913
1509
  const deps = [];
914
1510
  const dependenciesList = [];
915
- const keys_cache = {};
916
- const deps_keys_cache = {};
917
- let last_level = 0;
918
- let last_bomref = rootProject["bom-ref"];
919
- const first_bomref = last_bomref;
920
- let last_project_bomref = first_bomref;
1511
+ const depsByRef = new Map();
1512
+ const firstBomRef = rootProject["bom-ref"];
921
1513
  const level_trees = {};
922
- level_trees[last_bomref] = [];
1514
+ level_trees[firstBomRef] = [];
1515
+ // Modules whose own report supplies their edges.
1516
+ const reportedModuleRefs = new Set();
1517
+ for (const key of reportedProjects) {
1518
+ const reportedRef = gradleModules.get(key)?.["bom-ref"];
1519
+ if (reportedRef && reportedRef !== firstBomRef) {
1520
+ reportedModuleRefs.add(reportedRef);
1521
+ }
1522
+ }
923
1523
  let scope;
924
1524
  let profileName;
925
- if (retMap?.projects) {
926
- const modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
927
- ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
928
- : [];
929
- const modulesToScan = retMap.projects.filter(
930
- (module) => !gradleModules.has(module),
931
- );
932
- if (modulesToScan.length > 0) {
933
- const parallelPropTaskOut = executeParallelGradleProperties(
934
- gradleRootPath,
935
- modulesToScan.filter((module) => !modulesToSkip.includes(module)),
936
- );
937
- const splitPropTaskOut = splitOutputByGradleProjects(
938
- parallelPropTaskOut,
939
- ["properties"],
940
- );
941
-
942
- for (const module of modulesToScan) {
943
- const propMap = parseGradleProperties(
944
- splitPropTaskOut.get(module),
945
- module,
946
- );
947
- const rootSubProject = propMap.rootProject;
948
- if (rootSubProject) {
949
- const rootSubProjectObj = await buildObjectForGradleModule(
950
- rootSubProject === "root" ? module : rootSubProject,
951
- propMap.metadata,
952
- );
953
- gradleModules.set(module, rootSubProjectObj);
954
- }
955
- }
956
- }
957
- const subDependsOn = [];
958
- for (const sd of retMap.projects) {
959
- if (gradleModules.has(sd)) {
960
- subDependsOn.push(gradleModules.get(sd)["bom-ref"]);
961
- }
1525
+ // ancestors[level] is the bom-ref of the line most recently seen at that
1526
+ // depth; ancestors[0] is the project the report belongs to.
1527
+ const ancestors = [firstBomRef];
1528
+ const lines = rawOutput.split("\n").map((l) => l.replaceAll("\r", ""));
1529
+ // Modules some configuration resolved to a version, so that a failed,
1530
+ // versionless request for one of them adds nothing.
1531
+ const resolvedModules = new Set();
1532
+ for (const line of lines) {
1533
+ const parsed = parseGradleTreeLine(line);
1534
+ if (
1535
+ parsed?.group &&
1536
+ parsed.version !== undefined &&
1537
+ !parsed.failed &&
1538
+ !parsed.constraint &&
1539
+ !parsed.unresolvable
1540
+ ) {
1541
+ resolvedModules.add(`${parsed.group}:${parsed.name}`);
962
1542
  }
963
- level_trees[last_bomref] = subDependsOn;
964
1543
  }
965
- let stack = [last_bomref];
966
- const depRegex =
967
- /^.*?--- +(?<groupspecified>[^\s:]+) ?:(?<namespecified>[^\s:]+)(?::(?:{strictly [[]?)?(?<versionspecified>[^,\s:}]+))?(?:})?(?:[^->]* +-> +(?:(?<groupoverride>[^\s:]+):(?<nameoverride>[^\s:]+):)?(?<versionoverride>[^\s:]+))?/gm;
968
- for (let rline of rawOutput.split("\n")) {
969
- if (!rline) {
1544
+ for (const rline of lines) {
1545
+ const configurationName = parseGradleConfigurationHeader(rline);
1546
+ if (configurationName) {
1547
+ ancestors.length = 1;
1548
+ profileName = configurationName;
1549
+ scope = gradleConfigurationScope(configurationName);
1550
+ }
1551
+ if (rline.trim() === "") {
1552
+ ancestors.length = 1;
970
1553
  continue;
971
1554
  }
972
- rline = rline.replaceAll("\r", "");
973
- const trimmedLine = rline.trim();
974
- if (
975
- trimmedLine.endsWith("(n)") ||
976
- ((rline.startsWith("+--- ") || rline.startsWith("\\--- ")) &&
977
- rline.includes("{strictly") &&
978
- rline.includes("(c)"))
979
- ) {
1555
+ const parsed = parseGradleTreeLine(rline);
1556
+ if (!parsed) {
1557
+ continue;
1558
+ }
1559
+ // A line that names nothing usable still takes its place in the tree,
1560
+ // so the lines under it get no parent rather than a wrong one.
1561
+ if (parsed.unknown || parsed.unresolvable) {
1562
+ ancestors.length = Math.min(ancestors.length, parsed.level);
1563
+ continue;
1564
+ }
1565
+ const { level } = parsed;
1566
+ const isProject = parsed.projectPath !== undefined;
1567
+ if (parsed.constraint) {
1568
+ ancestors.length = Math.min(ancestors.length, level);
980
1569
  continue;
981
1570
  }
1571
+ // Only a dependency that failed to resolve can lack a version: Gradle
1572
+ // prints every resolved one with the version it selected.
982
1573
  if (
983
- trimmedLine === "" ||
984
- rline.startsWith("+--- ") ||
985
- rline.startsWith("\\--- ")
1574
+ !isProject &&
1575
+ parsed.version === undefined &&
1576
+ (!parsed.failed ||
1577
+ resolvedModules.has(`${parsed.group}:${parsed.name}`))
986
1578
  ) {
987
- last_level = 1;
988
- last_project_bomref = first_bomref;
989
- last_bomref = last_project_bomref;
990
- stack = [first_bomref];
1579
+ ancestors.length = Math.min(ancestors.length, level);
1580
+ continue;
991
1581
  }
992
- if (rline.includes(" - ") && !rline.startsWith("Project ':")) {
993
- profileName = rline.split(" - ")[0];
994
- if (profileName.toLowerCase().includes("test")) {
995
- scope = "optional";
996
- } else if (profileName.toLowerCase().includes("runtime")) {
997
- scope = "required";
998
- } else {
999
- scope = undefined;
1582
+ // A project the modules lack is named as Gradle publishes it by
1583
+ // default: the last segment of its path, with the root project's group
1584
+ // and version.
1585
+ const name = isProject
1586
+ ? parsed.projectPath.split(":").pop()
1587
+ : parsed.name;
1588
+ const group = isProject ? rootProject.group : parsed.group;
1589
+ const version = isProject ? rootProject.version : parsed.version;
1590
+ const gradleModule = isProject
1591
+ ? gradleModuleForPath(
1592
+ gradleModules,
1593
+ parsed.projectPath,
1594
+ rootProjectName,
1595
+ )
1596
+ : undefined;
1597
+ if (isProject && !gradleModule && name === "") {
1598
+ ancestors.length = Math.min(ancestors.length, level);
1599
+ continue;
1600
+ }
1601
+ let purl;
1602
+ let bomRef;
1603
+ if (gradleModule) {
1604
+ purl = gradleModule["purl"];
1605
+ bomRef = gradleModule["bom-ref"];
1606
+ } else {
1607
+ try {
1608
+ purl = build({
1609
+ type: "maven",
1610
+ namespace: group || null,
1611
+ name,
1612
+ version: version || null,
1613
+ qualifiers: { type: "jar" },
1614
+ });
1615
+ bomRef = decodeURIComponent(purl);
1616
+ } catch {
1617
+ // A purl that cannot be built keeps a name-only bom-ref so the
1618
+ // component still lands in the graph.
1619
+ purl = undefined;
1620
+ bomRef = name;
1000
1621
  }
1001
1622
  }
1002
- while ((match = depRegex.exec(rline))) {
1003
- const [
1004
- _line,
1005
- groupspecified,
1006
- namespecified,
1007
- versionspecified,
1008
- groupoverride,
1009
- nameoverride,
1010
- versionoverride,
1011
- ] = match;
1012
- let group = groupoverride || groupspecified;
1013
- let name = nameoverride || namespecified;
1014
- let version = versionoverride || versionspecified;
1015
- const prefix = rline.split("---")[0];
1016
- const level = Math.floor(prefix.length / 5) + 1;
1017
- if (version !== undefined || group === "project") {
1018
- // Project line has no version
1019
- // For multi sub-module projects such as :module:dummy:starter the regex is producing incorrect values
1020
- if (rline.includes("project ")) {
1021
- const tmpA = rline.split("project ");
1022
- if (tmpA && tmpA.length > 1) {
1023
- group = rootProject.group;
1024
- name = tmpA[1].split(" ")[0];
1025
- version = undefined;
1026
- }
1623
+ const existing = depsByRef.get(bomRef);
1624
+ // The project the report belongs to is the parent, not a component.
1625
+ if (!existing && bomRef !== firstBomRef) {
1626
+ let adep;
1627
+ if (gradleModule) {
1628
+ adep = gradleModule;
1629
+ } else {
1630
+ adep = {
1631
+ group,
1632
+ name: name,
1633
+ version,
1634
+ qualifiers: { type: "jar" },
1635
+ };
1636
+ if (version === undefined) {
1637
+ delete adep.version;
1027
1638
  }
1028
- let purl;
1029
- let bomRef;
1030
- if (gradleModules.has(name)) {
1031
- purl = gradleModules.get(name)["purl"];
1032
- bomRef = gradleModules.get(name)["bom-ref"];
1033
- } else {
1034
- try {
1035
- purl = build({
1036
- type: "maven",
1037
- namespace:
1038
- group !== "project" ? group : rootProject.group || null,
1039
- name: name.replace(/^:/, ""),
1040
- version:
1041
- version !== undefined ? version : rootProject.version || null,
1042
- qualifiers: { type: "jar" },
1043
- });
1044
- bomRef = decodeURIComponent(purl);
1045
- } catch {
1046
- // A purl that cannot be built keeps a name-only bom-ref so the
1047
- // component still lands in the graph.
1048
- purl = undefined;
1049
- bomRef = name.replace(/^:/, "");
1050
- }
1639
+ adep["purl"] = purl;
1640
+ adep["bom-ref"] = bomRef;
1641
+ if (scope) {
1642
+ adep["scope"] = scope;
1051
1643
  }
1052
- keys_cache[`${bomRef}_${last_bomref}`] = true;
1053
- // Filter duplicates
1054
- if (!deps_keys_cache[bomRef]) {
1055
- deps_keys_cache[bomRef] = true;
1056
- let adep;
1057
- if (gradleModules.has(name)) {
1058
- adep = gradleModules.get(name);
1059
- } else {
1060
- adep = {
1061
- group: group !== "project" ? group : rootProject.group,
1062
- name: name,
1063
- version: version !== undefined ? version : rootProject.version,
1064
- qualifiers: { type: "jar" },
1065
- };
1066
- adep["purl"] = purl;
1067
- adep["bom-ref"] = bomRef;
1068
- if (scope) {
1069
- adep["scope"] = scope;
1070
- }
1071
- adep.properties = [];
1072
- if (profileName) {
1073
- adep.properties.push({
1074
- name: "internal:GradleProfileName",
1075
- value: profileName,
1076
- });
1077
- }
1078
- if (gradleRootPath && gradleRootPath !== ".") {
1079
- adep.properties.push({
1080
- name: "cdx:gradle:GradleRootPath",
1081
- value: gradleRootPath,
1082
- });
1083
- }
1084
- }
1085
- if (adep?.properties?.length === 0) {
1086
- delete adep.properties;
1087
- }
1088
- deps.push(adep);
1644
+ adep.properties = [];
1645
+ if (profileName) {
1646
+ adep.properties.push({
1647
+ name: "internal:GradleProfileName",
1648
+ value: profileName,
1649
+ });
1089
1650
  }
1090
- if (!level_trees[bomRef]) {
1091
- level_trees[bomRef] = [];
1651
+ if (gradleRootPath && gradleRootPath !== ".") {
1652
+ adep.properties.push({
1653
+ name: "cdx:gradle:GradleRootPath",
1654
+ value: gradleRootPath,
1655
+ });
1092
1656
  }
1093
- if (level === 0) {
1094
- stack = [first_bomref];
1095
- stack.push(bomRef);
1096
- } else if (last_bomref === "") {
1097
- stack.push(bomRef);
1098
- } else if (level > last_level) {
1099
- const cnodes = level_trees[last_bomref] || [];
1100
- if (!cnodes.includes(bomRef)) {
1101
- cnodes.push(bomRef);
1102
- }
1103
- level_trees[last_bomref] = cnodes;
1104
- if (stack[stack.length - 1] !== bomRef) {
1105
- stack.push(bomRef);
1657
+ if (parsed.failed) {
1658
+ adep.properties.push({
1659
+ name: "cdx:gradle:resolutionFailed",
1660
+ value: "true",
1661
+ });
1662
+ if (
1663
+ parsed.requestedVersion &&
1664
+ parsed.requestedVersion !== version
1665
+ ) {
1666
+ adep.properties.push({
1667
+ name: "cdx:gradle:requestedVersion",
1668
+ value: parsed.requestedVersion.slice(0, 256),
1669
+ });
1106
1670
  }
1671
+ }
1672
+ }
1673
+ if (adep?.properties?.length === 0) {
1674
+ delete adep.properties;
1675
+ }
1676
+ depsByRef.set(bomRef, { adep, isModule: !!gradleModule });
1677
+ deps.push(adep);
1678
+ } else if (existing && !existing.isModule) {
1679
+ const { adep } = existing;
1680
+ if (gradleScopeRank(scope) > gradleScopeRank(adep.scope)) {
1681
+ if (scope) {
1682
+ adep.scope = scope;
1107
1683
  } else {
1108
- for (let i = level; i <= last_level; i++) {
1109
- stack.pop();
1110
- }
1111
- const last_stack =
1112
- stack.length > 0 ? stack[stack.length - 1] : last_project_bomref;
1113
- const cnodes = level_trees[last_stack] || [];
1114
- if (!cnodes.includes(bomRef)) {
1115
- cnodes.push(bomRef);
1116
- }
1117
- level_trees[last_stack] = cnodes;
1118
- stack.push(bomRef);
1684
+ delete adep.scope;
1685
+ }
1686
+ const profileProp = adep.properties?.find(
1687
+ (p) => p.name === "internal:GradleProfileName",
1688
+ );
1689
+ if (profileProp && profileName) {
1690
+ profileProp.value = profileName;
1119
1691
  }
1120
- last_level = level;
1121
- last_bomref = bomRef;
1122
1692
  }
1693
+ // A dependency one configuration failed to resolve and another
1694
+ // resolved is not a failed dependency.
1695
+ if (!parsed.failed && adep.properties) {
1696
+ adep.properties = adep.properties.filter(
1697
+ (p) => !GRADLE_FAILURE_PROPERTIES.includes(p.name),
1698
+ );
1699
+ if (!adep.properties.length) {
1700
+ delete adep.properties;
1701
+ }
1702
+ }
1703
+ }
1704
+ if (!level_trees[bomRef]) {
1705
+ level_trees[bomRef] = [];
1706
+ }
1707
+ // A line whose parent was skipped has no known parent and gets no edge.
1708
+ const parentRef =
1709
+ ancestors.length >= level ? ancestors[level - 1] : undefined;
1710
+ if (
1711
+ parentRef &&
1712
+ parentRef !== bomRef &&
1713
+ !reportedModuleRefs.has(parentRef)
1714
+ ) {
1715
+ const cnodes = level_trees[parentRef] || [];
1716
+ if (!cnodes.includes(bomRef)) {
1717
+ cnodes.push(bomRef);
1718
+ }
1719
+ level_trees[parentRef] = cnodes;
1720
+ }
1721
+ ancestors.length = Math.min(ancestors.length, level);
1722
+ if (ancestors.length === level) {
1723
+ ancestors.push(bomRef);
1123
1724
  }
1124
1725
  }
1726
+ const renamedRefs = markGradlePlatforms(depsByRef, level_trees);
1125
1727
  for (const lk of Object.keys(level_trees)) {
1126
1728
  dependenciesList.push({
1127
- ref: lk,
1128
- dependsOn: [...new Set(level_trees[lk])].sort(),
1729
+ ref: renamedRefs.get(lk) || lk,
1730
+ dependsOn: [
1731
+ ...new Set(level_trees[lk].map((ref) => renamedRefs.get(ref) || ref)),
1732
+ ].sort(),
1129
1733
  });
1130
1734
  }
1131
1735
  return {