@cdxgen/cdxgen 13.4.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 (305) hide show
  1. package/README.md +9 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +29 -5
  4. package/bin/evinse.js +8 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/bin/validate.js +6 -0
  8. package/data/cbomosdb-queries.json +13 -1
  9. package/data/component-tags.json +8 -0
  10. package/data/license-aliases.json +24 -2
  11. package/data/queries-darwin.json +25 -1
  12. package/data/queries-win.json +19 -1
  13. package/data/queries.json +41 -0
  14. package/data/remediations.json +407 -1
  15. package/data/rules/ai-bom.yaml +1 -1
  16. package/data/rules/brotli-decoder.yaml +107 -0
  17. package/data/rules/cbom-security.yaml +235 -4
  18. package/data/rules/go-binaries.yaml +46 -0
  19. package/data/rules/golem-go.yaml +80 -0
  20. package/data/rules/hbom-compliance.yaml +40 -22
  21. package/data/rules/hbom-performance.yaml +7 -1
  22. package/data/rules/host-topology.yaml +1 -1
  23. package/data/rules/obom-runtime.yaml +31 -2
  24. package/data/rules/php-runtime.yaml +143 -0
  25. package/data/rules/vscode-extensions.yaml +75 -0
  26. package/data/spdx-license-list.json +121 -7
  27. package/data/spdx-licenses.json +69 -0
  28. package/data/spdx.schema.json +845 -2
  29. package/lib/audit/index.js +14 -0
  30. package/lib/cli/bomAssembly.js +120 -6
  31. package/lib/cli/cliOptions.js +110 -3
  32. package/lib/cli/index.js +182 -6
  33. package/lib/cli/jsBom.js +153 -64
  34. package/lib/cli/jvmBom.js +363 -66
  35. package/lib/cli/managedBom.js +853 -87
  36. package/lib/cli/nativeBom.js +267 -33
  37. package/lib/core/boundedFile.js +344 -0
  38. package/lib/core/brotliProbe.js +182 -0
  39. package/lib/core/env.js +38 -8
  40. package/lib/core/fs.js +40 -19
  41. package/lib/core/httpClient.js +172 -46
  42. package/lib/core/lockPaths.js +38 -0
  43. package/lib/core/paths.js +24 -1
  44. package/lib/core/processTree.js +5 -0
  45. package/lib/core/projectConfig.js +1 -0
  46. package/lib/core/projectTooling.js +97 -0
  47. package/lib/core/propertySanitizer.js +72 -0
  48. package/lib/core/toolRegistry.js +170 -0
  49. package/lib/ecosystems/cppEvidence.js +263 -106
  50. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  51. package/lib/ecosystems/ecosystems.js +25 -10
  52. package/lib/ecosystems/gemHostProbe.js +301 -0
  53. package/lib/ecosystems/golem.js +550 -6
  54. package/lib/ecosystems/gradleutils.js +858 -244
  55. package/lib/ecosystems/parsers-bazel.js +107 -6
  56. package/lib/ecosystems/parsers-dotnet.js +117 -18
  57. package/lib/ecosystems/parsers-go.js +1090 -367
  58. package/lib/ecosystems/parsers-js.js +505 -22
  59. package/lib/ecosystems/parsers-jvm.js +259 -17
  60. package/lib/ecosystems/parsers-misc.js +438 -64
  61. package/lib/ecosystems/parsers-python.js +100 -12
  62. package/lib/ecosystems/pythonutils.js +128 -2
  63. package/lib/ecosystems/rubyutils.js +544 -34
  64. package/lib/ecosystems/rusi.js +10 -0
  65. package/lib/ecosystems/scalacliutils.js +4 -1
  66. package/lib/ecosystems/scalasem.js +798 -151
  67. package/lib/ecosystems/vendoredCode.js +237 -3
  68. package/lib/ecosystems/venvProbe.js +360 -0
  69. package/lib/evinser/evinser.js +472 -61
  70. package/lib/evinser/swiftsem.js +68 -5
  71. package/lib/helpers/agenticToolutils.js +413 -93
  72. package/lib/helpers/vsixutils.js +1574 -315
  73. package/lib/inventory/agentSkillSpec.js +17 -0
  74. package/lib/inventory/apkInstalledDb.js +243 -0
  75. package/lib/inventory/atomUtils.js +110 -0
  76. package/lib/inventory/auditCategories.js +3 -3
  77. package/lib/inventory/brotliDecoder.js +142 -0
  78. package/lib/inventory/buildToolRateLimit.js +1 -1
  79. package/lib/inventory/cacheDir.js +52 -9
  80. package/lib/inventory/cbomutils.js +56 -7
  81. package/lib/inventory/certificates.js +2371 -0
  82. package/lib/inventory/communityAiConfigParser.js +4 -9
  83. package/lib/inventory/deps.js +41 -4
  84. package/lib/inventory/display.js +200 -3
  85. package/lib/inventory/dosai.js +639 -63
  86. package/lib/inventory/dotnetRuntime.js +217 -0
  87. package/lib/inventory/dynamic.js +45 -3
  88. package/lib/inventory/envcontext.js +475 -18
  89. package/lib/inventory/evidenceUtils.js +447 -23
  90. package/lib/inventory/fetchBatch.js +1 -1
  91. package/lib/inventory/formulationParsers.js +32 -3
  92. package/lib/inventory/goBuildInfo.js +140 -0
  93. package/lib/inventory/gtfobins.js +13 -0
  94. package/lib/inventory/hbom.js +67 -7
  95. package/lib/inventory/hbomAnalysis.js +16 -0
  96. package/lib/inventory/hostRuntimeProbe.js +686 -0
  97. package/lib/inventory/hostTopology.js +21 -0
  98. package/lib/inventory/jvmLocalRepos.js +9 -2
  99. package/lib/inventory/jvmToolEnv.js +29 -0
  100. package/lib/inventory/licenseEnhancer.js +12 -0
  101. package/lib/inventory/mavenCentralMirror.js +534 -0
  102. package/lib/inventory/nodeBuildInfo.js +218 -0
  103. package/lib/inventory/nodeHostProbe.js +202 -0
  104. package/lib/inventory/osPackagePurl.js +244 -0
  105. package/lib/inventory/osPackageResolver.js +271 -87
  106. package/lib/inventory/osinfo.js +79 -31
  107. package/lib/inventory/osqueryTransform.js +114 -5
  108. package/lib/inventory/phpBuildInfo.js +438 -0
  109. package/lib/inventory/phpHostProbe.js +185 -0
  110. package/lib/inventory/rootfsCerts.js +426 -0
  111. package/lib/inventory/rubyBuildInfo.js +972 -0
  112. package/lib/inventory/rubyHostProbe.js +218 -0
  113. package/lib/inventory/scalaCoords.js +4 -3
  114. package/lib/inventory/scalaDedupe.js +440 -0
  115. package/lib/inventory/source.js +256 -5
  116. package/lib/inventory/spdx.js +144 -0
  117. package/lib/inventory/traceRunner.js +5 -1
  118. package/lib/inventory/tracedCrypto.js +129 -0
  119. package/lib/managers/binary.js +866 -186
  120. package/lib/managers/docker.js +245 -12
  121. package/lib/parsers/email.js +277 -0
  122. package/lib/parsers/largeJson.js +62 -13
  123. package/lib/parsers/vscodeExtensionId.js +183 -0
  124. package/lib/server/server.js +198 -2
  125. package/lib/stages/postgen/annotator.js +17 -1
  126. package/lib/stages/postgen/introspection/reflect.js +4 -1
  127. package/lib/stages/postgen/postgen.js +5 -5
  128. package/lib/stages/postgen/ruleEngine.js +71 -6
  129. package/lib/stages/postgen/specVersionCompat.js +30 -0
  130. package/lib/stages/pregen/envAudit.js +1 -0
  131. package/lib/stages/pregen/pregen.js +231 -62
  132. package/lib/validator/bomValidator.js +32 -6
  133. package/lib/validator/generated/runtime.mjs +13 -0
  134. package/lib/validator/generated/validate-1.6.mjs +27 -8
  135. package/lib/validator/generated/validate-1.7.mjs +32 -13
  136. package/lib/validator/generated/validate-2.0.mjs +31 -12
  137. package/lib/validator/index.js +1 -1
  138. package/package.json +19 -18
  139. package/types/bin/cdxgen-secure.d.ts +3 -0
  140. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  141. package/types/bin/repl.d.ts.map +1 -1
  142. package/types/lib/audit/index.d.ts.map +1 -1
  143. package/types/lib/cli/bomAssembly.d.ts +22 -3
  144. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  145. package/types/lib/cli/cliOptions.d.ts +43 -0
  146. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  147. package/types/lib/cli/index.d.ts.map +1 -1
  148. package/types/lib/cli/jsBom.d.ts +1 -17
  149. package/types/lib/cli/jsBom.d.ts.map +1 -1
  150. package/types/lib/cli/jvmBom.d.ts +20 -0
  151. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  152. package/types/lib/cli/managedBom.d.ts.map +1 -1
  153. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  154. package/types/lib/core/boundedFile.d.ts +70 -0
  155. package/types/lib/core/boundedFile.d.ts.map +1 -0
  156. package/types/lib/core/brotliProbe.d.ts +37 -0
  157. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  158. package/types/lib/core/env.d.ts +22 -2
  159. package/types/lib/core/env.d.ts.map +1 -1
  160. package/types/lib/core/fs.d.ts.map +1 -1
  161. package/types/lib/core/httpClient.d.ts +24 -0
  162. package/types/lib/core/httpClient.d.ts.map +1 -1
  163. package/types/lib/core/lockPaths.d.ts +19 -0
  164. package/types/lib/core/lockPaths.d.ts.map +1 -0
  165. package/types/lib/core/processTree.d.ts +2 -0
  166. package/types/lib/core/processTree.d.ts.map +1 -1
  167. package/types/lib/core/projectConfig.d.ts.map +1 -1
  168. package/types/lib/core/projectTooling.d.ts +41 -0
  169. package/types/lib/core/projectTooling.d.ts.map +1 -0
  170. package/types/lib/core/propertySanitizer.d.ts +26 -0
  171. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  172. package/types/lib/core/toolRegistry.d.ts +36 -0
  173. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  174. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  175. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  176. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  177. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  178. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  179. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  180. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  181. package/types/lib/ecosystems/golem.d.ts +81 -1
  182. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  183. package/types/lib/ecosystems/gradleutils.d.ts +57 -21
  184. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  187. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  188. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  189. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  191. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +14 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/pythonutils.d.ts +19 -1
  199. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  200. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  201. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  202. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  203. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  205. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  206. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  207. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  208. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  209. package/types/lib/evinser/evinser.d.ts +88 -8
  210. package/types/lib/evinser/evinser.d.ts.map +1 -1
  211. package/types/lib/evinser/swiftsem.d.ts +10 -0
  212. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  213. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  214. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  215. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  216. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  217. package/types/lib/inventory/atomUtils.d.ts +33 -0
  218. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  219. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  220. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  221. package/types/lib/inventory/cacheDir.d.ts +12 -4
  222. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  223. package/types/lib/inventory/cbomutils.d.ts +17 -0
  224. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  225. package/types/lib/inventory/certificates.d.ts +205 -0
  226. package/types/lib/inventory/certificates.d.ts.map +1 -0
  227. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  228. package/types/lib/inventory/deps.d.ts +2 -1
  229. package/types/lib/inventory/deps.d.ts.map +1 -1
  230. package/types/lib/inventory/display.d.ts +18 -0
  231. package/types/lib/inventory/display.d.ts.map +1 -1
  232. package/types/lib/inventory/dosai.d.ts +162 -7
  233. package/types/lib/inventory/dosai.d.ts.map +1 -1
  234. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  235. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  236. package/types/lib/inventory/dynamic.d.ts +6 -1
  237. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  238. package/types/lib/inventory/envcontext.d.ts +92 -8
  239. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  240. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  241. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  242. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  243. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  244. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  245. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  246. package/types/lib/inventory/hbom.d.ts.map +1 -1
  247. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  248. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  249. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  250. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  251. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  252. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  253. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  254. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  255. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  256. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  257. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  258. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  259. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  260. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  261. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  262. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  263. package/types/lib/inventory/osPackageResolver.d.ts +44 -2
  264. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  265. package/types/lib/inventory/osinfo.d.ts +31 -6
  266. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  267. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  268. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  269. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  270. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  271. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  272. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  273. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  274. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  275. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  276. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  277. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  278. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  279. package/types/lib/inventory/source.d.ts +38 -0
  280. package/types/lib/inventory/source.d.ts.map +1 -1
  281. package/types/lib/inventory/spdx.d.ts +15 -0
  282. package/types/lib/inventory/spdx.d.ts.map +1 -1
  283. package/types/lib/inventory/traceRunner.d.ts +4 -1
  284. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  285. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  286. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  287. package/types/lib/managers/binary.d.ts +10 -0
  288. package/types/lib/managers/binary.d.ts.map +1 -1
  289. package/types/lib/managers/docker.d.ts +24 -0
  290. package/types/lib/managers/docker.d.ts.map +1 -1
  291. package/types/lib/parsers/email.d.ts +19 -0
  292. package/types/lib/parsers/email.d.ts.map +1 -0
  293. package/types/lib/parsers/largeJson.d.ts +17 -3
  294. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  295. package/types/lib/server/server.d.ts +8 -0
  296. package/types/lib/server/server.d.ts.map +1 -1
  297. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  298. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  299. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  300. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  301. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  302. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  303. package/types/lib/stages/pregen/pregen.d.ts +26 -9
  304. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -23,6 +23,7 @@ import {
23
23
  import { getAllFiles, safeExistsSync, safeSpawnSync } from "../core/fs.js";
24
24
  import { thoughtLog } from "../core/logger.js";
25
25
  import { isWin } from "../core/paths.js";
26
+ import { isProjectToolingAllowed } from "../core/projectTooling.js";
26
27
  import { parseToml } from "../core/toml.js";
27
28
  import {
28
29
  isBuildToolRateLimited,
@@ -30,6 +31,7 @@ import {
30
31
  } from "../inventory/buildToolRateLimit.js";
31
32
  import { collectJarNS } from "../inventory/deps.js";
32
33
  import { minimumGradleVersionForJava } from "../inventory/jvmToolEnv.js";
34
+ import { gradleMirrorInitScriptArgs } from "../inventory/mavenCentralMirror.js";
33
35
  import { applyPurl, tryBuildPurl } from "../inventory/purl.js";
34
36
  import { parsePkgJson } from "./parsers-js.js";
35
37
 
@@ -44,13 +46,16 @@ function isJvmToolPinned() {
44
46
  );
45
47
  }
46
48
 
49
+ // The gate lives in lib/core so that every layer can reach it; it stays
50
+ // exported here for existing callers.
51
+ export { isProjectToolingAllowed };
52
+
47
53
  /**
48
54
  * Method to return the gradle command to use.
49
55
  *
50
56
  * Project-local wrapper scripts (`gradlew`) are attacker-controlled files in
51
- * the scanned project, so they are only used when dependency tooling is
52
- * allowed (`options.installDeps !== false`); otherwise the system Gradle is
53
- * used.
57
+ * the scanned project, so they are only used when the project's own tooling
58
+ * may run (see isProjectToolingAllowed); otherwise the system Gradle is used.
54
59
  *
55
60
  * @param {string} srcPath Path to look for gradlew wrapper
56
61
  * @param {string|null} rootPath Root directory to look for gradlew wrapper
@@ -59,10 +64,7 @@ function isJvmToolPinned() {
59
64
  export function getGradleCommand(srcPath, rootPath, options = {}) {
60
65
  let gradleCmd = "gradle";
61
66
  const pinnedToolSelected = isJvmToolPinned();
62
- // Introspection mode exists to observe the project's own toolchain, so it
63
- // keeps using wrappers even when dependency installation is disabled.
64
- const allowProjectWrappers =
65
- options.installDeps !== false || options.introspect === true;
67
+ const allowProjectWrappers = isProjectToolingAllowed(options);
66
68
 
67
69
  let findGradleFile = "gradlew";
68
70
  if (platform() === "win32") {
@@ -217,6 +219,50 @@ function gradleEvaluationOverrides(gradleArguments) {
217
219
  return overrides;
218
220
  }
219
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
+
220
266
  /**
221
267
  * Method to combine the general gradle arguments, the sub-commands and the sub-commands' arguments in the correct way
222
268
  *
@@ -241,6 +287,8 @@ export function buildGradleCommandArguments(
241
287
  ]
242
288
  .concat(getGradleDaemonParameter())
243
289
  .concat(gradleEvaluationOverrides(gradleArguments))
290
+ .concat(gradleLogLevelOverride(gradleArguments))
291
+ .concat(gradleMirrorInitScriptArgs())
244
292
  .concat(gradleArguments);
245
293
  const maxCliArgsLength = isWin
246
294
  ? 7500 - gradleCommandLength - mainGradleArguments.join(" ").length - 2
@@ -256,7 +304,13 @@ export function buildGradleCommandArguments(
256
304
  for (const gradleSubCommand of gradleSubCommands) {
257
305
  const subCommandLength =
258
306
  [gradleSubCommand, ...gradleSubCommandArguments].join(" ").length + 1;
259
- 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
+ ) {
260
314
  splitArgs.push(allGradleArguments);
261
315
  allGradleArguments = [].concat(mainGradleArguments);
262
316
  remainingLength = maxCliArgsLength;
@@ -296,16 +350,52 @@ function parseGradleTaskHeader(line) {
296
350
  if (!line.startsWith("> Task :")) {
297
351
  return undefined;
298
352
  }
299
- // The path is the first token: a status such as UP-TO-DATE or FAILED may
300
- // follow it, and a CRLF line keeps its carriage return.
301
- 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();
302
358
  const lastColon = taskPath.lastIndexOf(":");
303
359
  return {
304
360
  projectPath: taskPath.slice(0, lastColon),
305
- taskName: taskPath.slice(lastColon + 1),
361
+ taskName: taskPath.slice(lastColon + 1).split(/\s+/)[0],
306
362
  };
307
363
  }
308
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
+
309
399
  /**
310
400
  * Method to split the output produced by Gradle using parallel processing by project
311
401
  *
@@ -397,27 +487,19 @@ export function parseGradleProjects(rawOutput) {
397
487
  }
398
488
  }
399
489
  }
400
- } else if (l.includes("--- project ")) {
401
- const tmpB = l.split("--- project ");
402
- if (tmpB && tmpB.length > 1) {
403
- const projName = tmpB[1];
404
- if (projName.startsWith(":")) {
405
- const tmpName = projName.split(" ")[0];
406
- if (tmpName.length > 1) {
407
- projects.add(tmpName);
408
- }
490
+ } else {
491
+ for (const marker of ["--- project ", "-> project "]) {
492
+ const markerIndex = l.indexOf(marker);
493
+ if (markerIndex === -1) {
494
+ continue;
409
495
  }
410
- }
411
- } else if (l.includes("-> project ")) {
412
- const tmpB = l.split("-> project ");
413
- if (tmpB && tmpB.length > 1) {
414
- const projName = tmpB[1];
415
- if (projName.startsWith(":")) {
416
- const tmpName = projName.split(" ")[0];
417
- if (tmpName.length > 1) {
418
- projects.add(tmpName);
419
- }
496
+ const projName = gradleProjectPath(
497
+ l.slice(markerIndex + marker.length),
498
+ );
499
+ if (projName?.startsWith(":") && projName.length > 1) {
500
+ projects.add(projName);
420
501
  }
502
+ break;
421
503
  }
422
504
  }
423
505
  });
@@ -431,11 +513,17 @@ export function parseGradleProjects(rawOutput) {
431
513
  /**
432
514
  * Parse gradle properties output
433
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
+ *
434
520
  * @param {string} rawOutput Raw string output
435
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
436
523
  */
437
524
  export function parseGradleProperties(rawOutput, gradleModuleName = null) {
438
525
  let rootProject = "root";
526
+ let gradlePlugin = false;
439
527
  const projects = new Set();
440
528
  const metadata = { group: "", version: "latest", properties: [] };
441
529
  if (gradleModuleName) {
@@ -471,6 +559,8 @@ export function parseGradleProperties(rawOutput, gradleModuleName = null) {
471
559
  metadata[tmpB[0]] = tmpB[1].trim().replace("unspecified", "latest");
472
560
  } else if (["buildFile", "projectDir", "rootDir"].includes(tmpB[0])) {
473
561
  metadata.properties.push({ name: tmpB[0], value: tmpB[1].trim() });
562
+ } else if (tmpB[0] === "gradlePlugin") {
563
+ gradlePlugin = true;
474
564
  } else if (tmpB[0] === "subprojects") {
475
565
  const spStrs = tmpB[1].replace(/[[\]']/g, "").split(", ");
476
566
  const tmpprojects = spStrs
@@ -485,6 +575,7 @@ export function parseGradleProperties(rawOutput, gradleModuleName = null) {
485
575
  rootProject,
486
576
  projects: Array.from(projects),
487
577
  metadata,
578
+ gradlePlugin,
488
579
  };
489
580
  }
490
581
 
@@ -510,17 +601,11 @@ export function executeParallelGradleProperties(
510
601
  }
511
602
  const gradleCmd = getGradleCommand(dir, null, options);
512
603
  const gradleArgs = buildGradleCommandArguments(
513
- extraArgs.concat(
514
- readEnvironmentVariable("GRADLE_ARGS")
515
- ? readEnvironmentVariable("GRADLE_ARGS").split(" ")
516
- : [],
517
- ),
604
+ extraArgs.concat(gradleArgsFromEnv("GRADLE_ARGS")),
518
605
  allProjectsStr.map((project) =>
519
606
  project ? `${project}:properties` : "properties",
520
607
  ),
521
- readEnvironmentVariable("GRADLE_ARGS_PROPERTIES")
522
- ? readEnvironmentVariable("GRADLE_ARGS_PROPERTIES").split(" ")
523
- : [],
608
+ gradleArgsFromEnv("GRADLE_ARGS_PROPERTIES"),
524
609
  gradleCmd.length,
525
610
  );
526
611
  const allOutputs = [];
@@ -879,19 +964,526 @@ export function recordGradleInvocationFailure(result, context = {}) {
879
964
  });
880
965
  }
881
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
+
882
1450
  /**
883
1451
  * Method to resolve dependencies from a gradle output
884
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
+ *
885
1474
  * @param {string} rawOutput Text output from gradle dependencies task
886
- * @param {string} rootProjectName Name of the root project
887
- * @param {map} gradleModules Cache with all gradle modules that have already been read
888
- * @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
889
1480
  */
890
1481
  export async function parseGradleDep(
891
1482
  rawOutput,
892
1483
  rootProjectName = "root",
893
1484
  gradleModules = new Map(),
894
1485
  gradleRootPath = "",
1486
+ reportedProjects = new Set(),
895
1487
  ) {
896
1488
  if (typeof rawOutput === "string") {
897
1489
  // Bug: 249. Get any sub-projects refered here
@@ -906,225 +1498,238 @@ export async function parseGradleDep(
906
1498
  ) {
907
1499
  rootProjectName = retMap.rootProject;
908
1500
  }
909
- let match = "";
910
1501
  // To render dependency tree we need a root project
911
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
+ }
912
1509
  const deps = [];
913
1510
  const dependenciesList = [];
914
- const keys_cache = {};
915
- const deps_keys_cache = {};
916
- let last_level = 0;
917
- let last_bomref = rootProject["bom-ref"];
918
- const first_bomref = last_bomref;
919
- let last_project_bomref = first_bomref;
1511
+ const depsByRef = new Map();
1512
+ const firstBomRef = rootProject["bom-ref"];
920
1513
  const level_trees = {};
921
- 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
+ }
922
1523
  let scope;
923
1524
  let profileName;
924
- if (retMap?.projects) {
925
- const modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
926
- ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
927
- : [];
928
- const modulesToScan = retMap.projects.filter(
929
- (module) => !gradleModules.has(module),
930
- );
931
- if (modulesToScan.length > 0) {
932
- const parallelPropTaskOut = executeParallelGradleProperties(
933
- gradleRootPath,
934
- modulesToScan.filter((module) => !modulesToSkip.includes(module)),
935
- );
936
- const splitPropTaskOut = splitOutputByGradleProjects(
937
- parallelPropTaskOut,
938
- ["properties"],
939
- );
940
-
941
- for (const module of modulesToScan) {
942
- const propMap = parseGradleProperties(
943
- splitPropTaskOut.get(module),
944
- module,
945
- );
946
- const rootSubProject = propMap.rootProject;
947
- if (rootSubProject) {
948
- const rootSubProjectObj = await buildObjectForGradleModule(
949
- rootSubProject === "root" ? module : rootSubProject,
950
- propMap.metadata,
951
- );
952
- gradleModules.set(module, rootSubProjectObj);
953
- }
954
- }
955
- }
956
- const subDependsOn = [];
957
- for (const sd of retMap.projects) {
958
- if (gradleModules.has(sd)) {
959
- subDependsOn.push(gradleModules.get(sd)["bom-ref"]);
960
- }
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}`);
961
1542
  }
962
- level_trees[last_bomref] = subDependsOn;
963
1543
  }
964
- let stack = [last_bomref];
965
- const depRegex =
966
- /^.*?--- +(?<groupspecified>[^\s:]+) ?:(?<namespecified>[^\s:]+)(?::(?:{strictly [[]?)?(?<versionspecified>[^,\s:}]+))?(?:})?(?:[^->]* +-> +(?:(?<groupoverride>[^\s:]+):(?<nameoverride>[^\s:]+):)?(?<versionoverride>[^\s:]+))?/gm;
967
- for (let rline of rawOutput.split("\n")) {
968
- 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;
969
1553
  continue;
970
1554
  }
971
- rline = rline.replaceAll("\r", "");
972
- const trimmedLine = rline.trim();
973
- if (
974
- trimmedLine.endsWith("(n)") ||
975
- ((rline.startsWith("+--- ") || rline.startsWith("\\--- ")) &&
976
- rline.includes("{strictly") &&
977
- rline.includes("(c)"))
978
- ) {
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);
979
1563
  continue;
980
1564
  }
1565
+ const { level } = parsed;
1566
+ const isProject = parsed.projectPath !== undefined;
1567
+ if (parsed.constraint) {
1568
+ ancestors.length = Math.min(ancestors.length, level);
1569
+ continue;
1570
+ }
1571
+ // Only a dependency that failed to resolve can lack a version: Gradle
1572
+ // prints every resolved one with the version it selected.
981
1573
  if (
982
- trimmedLine === "" ||
983
- rline.startsWith("+--- ") ||
984
- rline.startsWith("\\--- ")
1574
+ !isProject &&
1575
+ parsed.version === undefined &&
1576
+ (!parsed.failed ||
1577
+ resolvedModules.has(`${parsed.group}:${parsed.name}`))
985
1578
  ) {
986
- last_level = 1;
987
- last_project_bomref = first_bomref;
988
- last_bomref = last_project_bomref;
989
- stack = [first_bomref];
1579
+ ancestors.length = Math.min(ancestors.length, level);
1580
+ continue;
990
1581
  }
991
- if (rline.includes(" - ") && !rline.startsWith("Project ':")) {
992
- profileName = rline.split(" - ")[0];
993
- if (profileName.toLowerCase().includes("test")) {
994
- scope = "optional";
995
- } else if (profileName.toLowerCase().includes("runtime")) {
996
- scope = "required";
997
- } else {
998
- 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;
999
1621
  }
1000
1622
  }
1001
- while ((match = depRegex.exec(rline))) {
1002
- const [
1003
- _line,
1004
- groupspecified,
1005
- namespecified,
1006
- versionspecified,
1007
- groupoverride,
1008
- nameoverride,
1009
- versionoverride,
1010
- ] = match;
1011
- let group = groupoverride || groupspecified;
1012
- let name = nameoverride || namespecified;
1013
- let version = versionoverride || versionspecified;
1014
- const prefix = rline.split("---")[0];
1015
- const level = Math.floor(prefix.length / 5) + 1;
1016
- if (version !== undefined || group === "project") {
1017
- // Project line has no version
1018
- // For multi sub-module projects such as :module:dummy:starter the regex is producing incorrect values
1019
- if (rline.includes("project ")) {
1020
- const tmpA = rline.split("project ");
1021
- if (tmpA && tmpA.length > 1) {
1022
- group = rootProject.group;
1023
- name = tmpA[1].split(" ")[0];
1024
- version = undefined;
1025
- }
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;
1026
1638
  }
1027
- let purl;
1028
- let bomRef;
1029
- if (gradleModules.has(name)) {
1030
- purl = gradleModules.get(name)["purl"];
1031
- bomRef = gradleModules.get(name)["bom-ref"];
1032
- } else {
1033
- try {
1034
- purl = build({
1035
- type: "maven",
1036
- namespace:
1037
- group !== "project" ? group : rootProject.group || null,
1038
- name: name.replace(/^:/, ""),
1039
- version:
1040
- version !== undefined ? version : rootProject.version || null,
1041
- qualifiers: { type: "jar" },
1042
- });
1043
- bomRef = decodeURIComponent(purl);
1044
- } catch {
1045
- // A purl that cannot be built keeps a name-only bom-ref so the
1046
- // component still lands in the graph.
1047
- purl = undefined;
1048
- bomRef = name.replace(/^:/, "");
1049
- }
1639
+ adep["purl"] = purl;
1640
+ adep["bom-ref"] = bomRef;
1641
+ if (scope) {
1642
+ adep["scope"] = scope;
1050
1643
  }
1051
- keys_cache[`${bomRef}_${last_bomref}`] = true;
1052
- // Filter duplicates
1053
- if (!deps_keys_cache[bomRef]) {
1054
- deps_keys_cache[bomRef] = true;
1055
- let adep;
1056
- if (gradleModules.has(name)) {
1057
- adep = gradleModules.get(name);
1058
- } else {
1059
- adep = {
1060
- group: group !== "project" ? group : rootProject.group,
1061
- name: name,
1062
- version: version !== undefined ? version : rootProject.version,
1063
- qualifiers: { type: "jar" },
1064
- };
1065
- adep["purl"] = purl;
1066
- adep["bom-ref"] = bomRef;
1067
- if (scope) {
1068
- adep["scope"] = scope;
1069
- }
1070
- adep.properties = [];
1071
- if (profileName) {
1072
- adep.properties.push({
1073
- name: "internal:GradleProfileName",
1074
- value: profileName,
1075
- });
1076
- }
1077
- if (gradleRootPath && gradleRootPath !== ".") {
1078
- adep.properties.push({
1079
- name: "cdx:gradle:GradleRootPath",
1080
- value: gradleRootPath,
1081
- });
1082
- }
1083
- }
1084
- if (adep?.properties?.length === 0) {
1085
- delete adep.properties;
1086
- }
1087
- deps.push(adep);
1644
+ adep.properties = [];
1645
+ if (profileName) {
1646
+ adep.properties.push({
1647
+ name: "internal:GradleProfileName",
1648
+ value: profileName,
1649
+ });
1088
1650
  }
1089
- if (!level_trees[bomRef]) {
1090
- level_trees[bomRef] = [];
1651
+ if (gradleRootPath && gradleRootPath !== ".") {
1652
+ adep.properties.push({
1653
+ name: "cdx:gradle:GradleRootPath",
1654
+ value: gradleRootPath,
1655
+ });
1091
1656
  }
1092
- if (level === 0) {
1093
- stack = [first_bomref];
1094
- stack.push(bomRef);
1095
- } else if (last_bomref === "") {
1096
- stack.push(bomRef);
1097
- } else if (level > last_level) {
1098
- const cnodes = level_trees[last_bomref] || [];
1099
- if (!cnodes.includes(bomRef)) {
1100
- cnodes.push(bomRef);
1101
- }
1102
- level_trees[last_bomref] = cnodes;
1103
- if (stack[stack.length - 1] !== bomRef) {
1104
- 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
+ });
1105
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;
1106
1683
  } else {
1107
- for (let i = level; i <= last_level; i++) {
1108
- stack.pop();
1109
- }
1110
- const last_stack =
1111
- stack.length > 0 ? stack[stack.length - 1] : last_project_bomref;
1112
- const cnodes = level_trees[last_stack] || [];
1113
- if (!cnodes.includes(bomRef)) {
1114
- cnodes.push(bomRef);
1115
- }
1116
- level_trees[last_stack] = cnodes;
1117
- 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;
1691
+ }
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;
1118
1701
  }
1119
- last_level = level;
1120
- last_bomref = bomRef;
1121
1702
  }
1122
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);
1724
+ }
1123
1725
  }
1726
+ const renamedRefs = markGradlePlatforms(depsByRef, level_trees);
1124
1727
  for (const lk of Object.keys(level_trees)) {
1125
1728
  dependenciesList.push({
1126
- ref: lk,
1127
- 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(),
1128
1733
  });
1129
1734
  }
1130
1735
  return {
@@ -1349,15 +1954,18 @@ export async function collectGradleDependencies(
1349
1954
  * Method to return the mill command to use.
1350
1955
  *
1351
1956
  * The project-local `mill` script is an attacker-controlled file in the
1352
- * scanned project, so it is only used when dependency tooling is allowed
1353
- * (`options.installDeps !== false`).
1957
+ * scanned project, so it is only used when the project's own tooling may run
1958
+ * (see isProjectToolingAllowed).
1354
1959
  *
1355
1960
  * @param {string} srcPath Path to look for mill wrapper
1356
1961
  * @param {Object} [options] CLI options (`installDeps` gates wrapper use)
1357
1962
  */
1358
1963
  export function getMillCommand(srcPath, options = {}) {
1359
1964
  let millCmd = `mill${platform() === "win32" ? ".bat" : ""}`;
1360
- if (options.installDeps !== false && safeExistsSync(join(srcPath, millCmd))) {
1965
+ if (
1966
+ isProjectToolingAllowed(options) &&
1967
+ safeExistsSync(join(srcPath, millCmd))
1968
+ ) {
1361
1969
  // Use local mill wrapper if available
1362
1970
  // Enable execute permission
1363
1971
  try {
@@ -1510,16 +2118,25 @@ function isProcessAlive(pid) {
1510
2118
  * @param {string} rootPath Scanned tree
1511
2119
  * @param {Map<string, Set<number>>} [runningBefore] Result of
1512
2120
  * {@link findMillDaemons} taken before the scan
2121
+ * @param {Object} [options] CLI options
1513
2122
  */
1514
- export function stopMillDaemons(rootPath, runningBefore = new Map()) {
2123
+ export function stopMillDaemons(
2124
+ rootPath,
2125
+ runningBefore = new Map(),
2126
+ options = {},
2127
+ ) {
2128
+ // The daemon PID files live in the scanned tree. When the scan was not
2129
+ // allowed to run Mill it started no daemon, and running the build's own
2130
+ // launcher here would execute a script from the scanned project.
2131
+ if (!isProjectToolingAllowed(options)) {
2132
+ return;
2133
+ }
1515
2134
  for (const [buildRoot, pids] of findMillDaemons(rootPath)) {
1516
2135
  const before = runningBefore.get(buildRoot) || new Set();
1517
2136
  if ([...pids].every((pid) => before.has(pid))) {
1518
2137
  continue;
1519
2138
  }
1520
- const launcher = safeExistsSync(join(buildRoot, "mill"))
1521
- ? join(buildRoot, "mill")
1522
- : "mill";
2139
+ const launcher = getMillCommand(buildRoot, options);
1523
2140
  console.log("Stopping the Mill daemon the scan started in", buildRoot);
1524
2141
  safeSpawnSync(launcher, ["shutdown"], {
1525
2142
  cwd: buildRoot,
@@ -1532,8 +2149,8 @@ export function stopMillDaemons(rootPath, runningBefore = new Map()) {
1532
2149
  * Method to return the maven command to use.
1533
2150
  *
1534
2151
  * Project-local wrapper scripts (`mvnw`) are attacker-controlled files in the
1535
- * scanned project, so they are only used when dependency tooling is allowed
1536
- * (`options.installDeps !== false`); otherwise the installed Maven is used.
2152
+ * scanned project, so they are only used when the project's own tooling may
2153
+ * run (see isProjectToolingAllowed); otherwise the installed Maven is used.
1537
2154
  *
1538
2155
  * @param {string} srcPath Path to look for maven wrapper
1539
2156
  * @param {string} rootPath Root directory to look for maven wrapper
@@ -1545,10 +2162,7 @@ export function getMavenCommand(srcPath, rootPath, options = {}) {
1545
2162
  let isWrapperReady = false;
1546
2163
  let isWrapperFound = false;
1547
2164
  const pinnedToolSelected = isJvmToolPinned();
1548
- // Introspection mode exists to observe the project's own toolchain, so it
1549
- // keeps using wrappers even when dependency installation is disabled.
1550
- const allowProjectWrappers =
1551
- options.installDeps !== false || options.introspect === true;
2165
+ const allowProjectWrappers = isProjectToolingAllowed(options);
1552
2166
  let findMavenFile = "mvnw";
1553
2167
  let mavenWrapperCmd = null;
1554
2168
  if (platform() === "win32") {