@cdxgen/cdxgen 13.5.0 → 13.7.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 (380) hide show
  1. package/README.md +17 -10
  2. package/bin/audit.js +7 -4
  3. package/bin/cdxgen.js +260 -37
  4. package/bin/convert.js +146 -22
  5. package/bin/evinse.js +28 -3
  6. package/bin/repl.js +47 -3
  7. package/bin/sign.js +211 -34
  8. package/bin/tracebom.js +18 -6
  9. package/bin/validate.js +24 -3
  10. package/bin/verify.js +113 -5
  11. package/data/behavior-taxonomy.schema.json +1491 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
  13. package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
  14. package/data/license-aliases.json +24 -2
  15. package/data/pqc-migration.json +239 -0
  16. package/data/remediations.json +176 -0
  17. package/data/rules/ai-bom.yaml +1 -1
  18. package/data/rules/brotli-decoder.yaml +107 -0
  19. package/data/rules/cbom-security.yaml +4 -3
  20. package/data/rules/package-integrity.yaml +28 -0
  21. package/data/rules/vscode-extensions.yaml +75 -0
  22. package/data/spdx-license-list.json +121 -7
  23. package/data/spdx-licenses.json +69 -0
  24. package/data/spdx.schema.json +845 -2
  25. package/lib/audit/index.js +25 -3
  26. package/lib/cli/bomAssembly.js +172 -4
  27. package/lib/cli/cliOptions.js +121 -17
  28. package/lib/cli/index.js +720 -902
  29. package/lib/cli/jsBom.js +156 -64
  30. package/lib/cli/jvmBom.js +602 -86
  31. package/lib/cli/managedBom.js +161 -19
  32. package/lib/cli/nativeBom.js +52 -5
  33. package/lib/core/boundedFile.js +344 -0
  34. package/lib/core/brotliProbe.js +182 -0
  35. package/lib/core/env.js +34 -0
  36. package/lib/core/fs.js +138 -3
  37. package/lib/core/globPatterns.js +218 -0
  38. package/lib/core/httpClient.js +98 -1
  39. package/lib/core/lockPaths.js +38 -0
  40. package/lib/core/paths.js +24 -1
  41. package/lib/core/processTree.js +100 -8
  42. package/lib/core/projectConfig.js +332 -25
  43. package/lib/core/toolRegistry.js +184 -0
  44. package/lib/ecosystems/cppEvidence.js +263 -106
  45. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  46. package/lib/ecosystems/denoutils.js +2 -2
  47. package/lib/ecosystems/ecosystems.js +25 -10
  48. package/lib/ecosystems/gemHostProbe.js +301 -0
  49. package/lib/ecosystems/golem.js +3 -8
  50. package/lib/ecosystems/gradleutils.js +1909 -229
  51. package/lib/ecosystems/jsEvidence.js +90 -38
  52. package/lib/ecosystems/kosi.js +3 -5
  53. package/lib/ecosystems/npmVersionResolution.js +376 -0
  54. package/lib/ecosystems/parsers-bazel.js +107 -6
  55. package/lib/ecosystems/parsers-js.js +505 -22
  56. package/lib/ecosystems/parsers-misc.js +209 -20
  57. package/lib/ecosystems/parsers-python.js +178 -30
  58. package/lib/ecosystems/parsers-rust.js +117 -15
  59. package/lib/ecosystems/pythonutils.js +128 -2
  60. package/lib/ecosystems/remote/huggingface.js +22 -0
  61. package/lib/ecosystems/rubyutils.js +538 -31
  62. package/lib/ecosystems/rusi.js +2 -4
  63. package/lib/ecosystems/scalacliutils.js +2 -0
  64. package/lib/ecosystems/scalasem.js +798 -152
  65. package/lib/ecosystems/vendoredCode.js +237 -3
  66. package/lib/ecosystems/venvProbe.js +360 -0
  67. package/lib/evinser/evinser.js +536 -71
  68. package/lib/evinser/scalasem.js +5 -1
  69. package/lib/helpers/agenticToolutils.js +525 -94
  70. package/lib/helpers/bomSigner.js +43 -9
  71. package/lib/helpers/jssSigner.js +1276 -0
  72. package/lib/helpers/vsixutils.js +1574 -315
  73. package/lib/inventory/agentBlueprint.js +193 -0
  74. package/lib/inventory/agentToolNames.js +145 -0
  75. package/lib/inventory/aiCollector.js +153 -7
  76. package/lib/inventory/aiInventory.js +44 -8
  77. package/lib/inventory/aiProviders.js +324 -0
  78. package/lib/inventory/atomUtils.js +77 -0
  79. package/lib/inventory/bomUtils.js +8 -1
  80. package/lib/inventory/brotliDecoder.js +142 -0
  81. package/lib/inventory/buildToolRateLimit.js +1 -1
  82. package/lib/inventory/cbomutils.js +56 -7
  83. package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
  84. package/lib/inventory/citations.js +31 -2
  85. package/lib/inventory/communityAiConfigParser.js +978 -80
  86. package/lib/inventory/cyclonedx20.js +152 -0
  87. package/lib/inventory/cyclonedx20Common.js +286 -0
  88. package/lib/inventory/cyclonedx20Derived.js +159 -0
  89. package/lib/inventory/cyclonedx20Parties.js +552 -0
  90. package/lib/inventory/cyclonedx20Read.js +768 -0
  91. package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
  92. package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
  93. package/lib/inventory/cyclonedx20ReadContext.js +380 -0
  94. package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
  95. package/lib/inventory/cyclonedx20ReadSections.js +599 -0
  96. package/lib/inventory/cyclonedx20Render.js +766 -0
  97. package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
  98. package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
  99. package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
  100. package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
  101. package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
  102. package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
  103. package/lib/inventory/cyclonedx20RenderSections.js +499 -0
  104. package/lib/inventory/cyclonedx20Tables.js +594 -0
  105. package/lib/inventory/deps.js +141 -13
  106. package/lib/inventory/dosai.js +375 -6
  107. package/lib/inventory/dosaiParsers.js +3 -6
  108. package/lib/inventory/dotnetRuntime.js +367 -0
  109. package/lib/inventory/ecosystemFormulationParsers.js +979 -0
  110. package/lib/inventory/envcontext.js +436 -48
  111. package/lib/inventory/evidenceUtils.js +13 -1
  112. package/lib/inventory/fetchBatch.js +1 -1
  113. package/lib/inventory/formulationParsers.js +124 -14
  114. package/lib/inventory/gtfobins.js +13 -0
  115. package/lib/inventory/hbom.js +45 -6
  116. package/lib/inventory/hbomLoader.js +166 -0
  117. package/lib/inventory/hostRuntimeProbe.js +686 -0
  118. package/lib/inventory/jvmLocalRepos.js +9 -2
  119. package/lib/inventory/jvmToolEnv.js +29 -0
  120. package/lib/inventory/licenseEnhancer.js +12 -0
  121. package/lib/inventory/mavenCentralMirror.js +534 -0
  122. package/lib/inventory/mcpConfigParser.js +5 -0
  123. package/lib/inventory/nodeBuildInfo.js +218 -0
  124. package/lib/inventory/nodeHostProbe.js +202 -0
  125. package/lib/inventory/occurrenceKey.js +90 -0
  126. package/lib/inventory/osPackageResolver.js +182 -0
  127. package/lib/inventory/phpBuildInfo.js +10 -59
  128. package/lib/inventory/phpHostProbe.js +32 -488
  129. package/lib/inventory/pqcMigration.js +426 -0
  130. package/lib/inventory/rubyBuildInfo.js +972 -0
  131. package/lib/inventory/rubyHostProbe.js +218 -0
  132. package/lib/inventory/rustFormulationParser.js +21 -10
  133. package/lib/inventory/scalaCoords.js +4 -3
  134. package/lib/inventory/scalaDedupe.js +440 -0
  135. package/lib/inventory/source.js +114 -8
  136. package/lib/managers/binary.js +337 -3
  137. package/lib/managers/docker.js +79 -0
  138. package/lib/parsers/huggingfaceManifest.js +9 -1
  139. package/lib/parsers/huggingfaceUtils.js +18 -0
  140. package/lib/parsers/kiroPermissions.js +166 -0
  141. package/lib/parsers/largeJson.js +28 -11
  142. package/lib/parsers/vscodeExtensionId.js +183 -0
  143. package/lib/server/server.js +48 -35
  144. package/lib/stages/postgen/postgen.js +41 -1
  145. package/lib/stages/postgen/ruleEngine.js +13 -5
  146. package/lib/stages/postgen/specVersionCompat.js +76 -0
  147. package/lib/stages/pregen/envAudit.js +1 -0
  148. package/lib/stages/pregen/pregen.js +114 -57
  149. package/lib/validator/bomValidator.js +14 -2
  150. package/lib/validator/complianceEngine.js +30 -3
  151. package/lib/validator/complianceRules.js +1416 -4
  152. package/lib/validator/generated/validate-1.6.mjs +1 -1
  153. package/lib/validator/generated/validate-1.7.mjs +1 -1
  154. package/lib/validator/generated/validate-2.0.mjs +57554 -13851
  155. package/lib/validator/index.js +49 -8
  156. package/lib/validator/reporters/json.js +6 -0
  157. package/package.json +11 -10
  158. package/types/bin/repl.d.ts.map +1 -1
  159. package/types/lib/audit/index.d.ts +6 -2
  160. package/types/lib/audit/index.d.ts.map +1 -1
  161. package/types/lib/cli/bomAssembly.d.ts +29 -1
  162. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  163. package/types/lib/cli/cliOptions.d.ts +6 -0
  164. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  165. package/types/lib/cli/index.d.ts.map +1 -1
  166. package/types/lib/cli/jsBom.d.ts +3 -18
  167. package/types/lib/cli/jsBom.d.ts.map +1 -1
  168. package/types/lib/cli/jvmBom.d.ts +15 -0
  169. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  170. package/types/lib/cli/managedBom.d.ts.map +1 -1
  171. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  172. package/types/lib/core/boundedFile.d.ts +100 -0
  173. package/types/lib/core/boundedFile.d.ts.map +1 -0
  174. package/types/lib/core/brotliProbe.d.ts +37 -0
  175. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  176. package/types/lib/core/env.d.ts +14 -0
  177. package/types/lib/core/env.d.ts.map +1 -1
  178. package/types/lib/core/fs.d.ts +4 -0
  179. package/types/lib/core/fs.d.ts.map +1 -1
  180. package/types/lib/core/globPatterns.d.ts +70 -0
  181. package/types/lib/core/globPatterns.d.ts.map +1 -0
  182. package/types/lib/core/httpClient.d.ts +24 -0
  183. package/types/lib/core/httpClient.d.ts.map +1 -1
  184. package/types/lib/core/lockPaths.d.ts +19 -0
  185. package/types/lib/core/lockPaths.d.ts.map +1 -0
  186. package/types/lib/core/paths.d.ts +9 -0
  187. package/types/lib/core/paths.d.ts.map +1 -1
  188. package/types/lib/core/processTree.d.ts +17 -4
  189. package/types/lib/core/processTree.d.ts.map +1 -1
  190. package/types/lib/core/projectConfig.d.ts +47 -8
  191. package/types/lib/core/projectConfig.d.ts.map +1 -1
  192. package/types/lib/core/toolRegistry.d.ts +36 -0
  193. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  194. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  195. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  196. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  197. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  198. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  199. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  200. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  201. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  202. package/types/lib/ecosystems/gradleutils.d.ts +294 -20
  203. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
  205. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  206. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  207. package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
  208. package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
  209. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  210. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  211. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  212. package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
  213. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  214. package/types/lib/ecosystems/parsers-python.d.ts +10 -0
  215. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  216. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  217. package/types/lib/ecosystems/pythonutils.d.ts +19 -1
  218. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  219. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  220. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  221. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  222. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  223. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  224. package/types/lib/ecosystems/scalasem.d.ts +58 -16
  225. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  226. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  227. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  228. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  229. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  230. package/types/lib/evinser/evinser.d.ts +120 -13
  231. package/types/lib/evinser/evinser.d.ts.map +1 -1
  232. package/types/lib/evinser/scalasem.d.ts.map +1 -1
  233. package/types/lib/helpers/agenticToolutils.d.ts +53 -19
  234. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
  235. package/types/lib/helpers/bomSigner.d.ts +20 -3
  236. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  237. package/types/lib/helpers/jssSigner.d.ts +288 -0
  238. package/types/lib/helpers/jssSigner.d.ts.map +1 -0
  239. package/types/lib/helpers/vsixutils.d.ts +303 -19
  240. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  241. package/types/lib/inventory/agentBlueprint.d.ts +70 -0
  242. package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
  243. package/types/lib/inventory/agentToolNames.d.ts +51 -0
  244. package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
  245. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  246. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  247. package/types/lib/inventory/aiProviders.d.ts +85 -0
  248. package/types/lib/inventory/aiProviders.d.ts.map +1 -0
  249. package/types/lib/inventory/atomUtils.d.ts +19 -0
  250. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  251. package/types/lib/inventory/bomUtils.d.ts.map +1 -1
  252. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  253. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  254. package/types/lib/inventory/cbomutils.d.ts +17 -0
  255. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  256. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
  257. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
  258. package/types/lib/inventory/citations.d.ts +5 -0
  259. package/types/lib/inventory/citations.d.ts.map +1 -1
  260. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  261. package/types/lib/inventory/cyclonedx20.d.ts +99 -0
  262. package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
  263. package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
  264. package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
  265. package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
  266. package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
  267. package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
  268. package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
  269. package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
  270. package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
  271. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
  272. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
  273. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
  274. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
  275. package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
  276. package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
  277. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
  278. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
  279. package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
  280. package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
  281. package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
  282. package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
  283. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
  284. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
  285. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
  286. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
  287. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
  288. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
  289. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
  290. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
  291. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
  292. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
  293. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
  294. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
  295. package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
  296. package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
  297. package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
  298. package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
  299. package/types/lib/inventory/deps.d.ts +35 -2
  300. package/types/lib/inventory/deps.d.ts.map +1 -1
  301. package/types/lib/inventory/dosai.d.ts +36 -1
  302. package/types/lib/inventory/dosai.d.ts.map +1 -1
  303. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  304. package/types/lib/inventory/dotnetRuntime.d.ts +91 -0
  305. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  306. package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
  307. package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
  308. package/types/lib/inventory/envcontext.d.ts +74 -12
  309. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  310. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  311. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  312. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  313. package/types/lib/inventory/hbom.d.ts +11 -0
  314. package/types/lib/inventory/hbom.d.ts.map +1 -1
  315. package/types/lib/inventory/hbomLoader.d.ts +54 -0
  316. package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
  317. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  318. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  319. package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
  320. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
  321. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  322. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  323. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  324. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  325. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  326. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  327. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  328. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  329. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  330. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  331. package/types/lib/inventory/occurrenceKey.d.ts +52 -0
  332. package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
  333. package/types/lib/inventory/osPackageResolver.d.ts +38 -0
  334. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  335. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
  336. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
  337. package/types/lib/inventory/pqcMigration.d.ts +84 -0
  338. package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
  339. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  340. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  341. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  342. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  343. package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
  344. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  345. package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
  346. package/types/lib/inventory/scalaDedupe.d.ts +43 -0
  347. package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
  348. package/types/lib/inventory/source.d.ts +25 -0
  349. package/types/lib/inventory/source.d.ts.map +1 -1
  350. package/types/lib/managers/binary.d.ts.map +1 -1
  351. package/types/lib/managers/docker.d.ts +12 -0
  352. package/types/lib/managers/docker.d.ts.map +1 -1
  353. package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
  354. package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
  355. package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
  356. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  357. package/types/lib/parsers/kiroPermissions.d.ts +62 -0
  358. package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
  359. package/types/lib/parsers/largeJson.d.ts +4 -3
  360. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  361. package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
  362. package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
  363. package/types/lib/server/server.d.ts.map +1 -1
  364. package/types/lib/stages/postgen/postgen.d.ts +7 -0
  365. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  366. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  367. package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
  368. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  369. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  370. package/types/lib/stages/pregen/pregen.d.ts +12 -8
  371. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  372. package/types/lib/validator/bomValidator.d.ts +5 -1
  373. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  374. package/types/lib/validator/complianceEngine.d.ts +4 -4
  375. package/types/lib/validator/complianceEngine.d.ts.map +1 -1
  376. package/types/lib/validator/complianceRules.d.ts +14 -2
  377. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  378. package/types/lib/validator/index.d.ts +10 -0
  379. package/types/lib/validator/index.d.ts.map +1 -1
  380. package/types/lib/validator/reporters/json.d.ts.map +1 -1
@@ -1,7 +1,7 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import { chmodSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { homedir, platform } from "node:os";
4
- import { basename, join, resolve } from "node:path";
4
+ import { basename, dirname, join, relative, resolve } from "node:path";
5
5
  import process from "node:process";
6
6
 
7
7
  import { build } from "@cdxgen/cdx-purl";
@@ -12,6 +12,7 @@ import {
12
12
  readEnvironmentVariable,
13
13
  recordDecisionActivity,
14
14
  } from "../core/activity.js";
15
+ import { readRegularTextFile } from "../core/boundedFile.js";
15
16
  import {
16
17
  commandOutputText,
17
18
  isLedgerEnabled,
@@ -30,9 +31,13 @@ import {
30
31
  noteBuildToolRateLimit,
31
32
  } from "../inventory/buildToolRateLimit.js";
32
33
  import { collectJarNS } from "../inventory/deps.js";
34
+ import { splitArtifactFileName } from "../inventory/jvmLocalRepos.js";
33
35
  import { minimumGradleVersionForJava } from "../inventory/jvmToolEnv.js";
34
- import { applyPurl, tryBuildPurl } from "../inventory/purl.js";
36
+ import { gradleMirrorInitScriptArgs } from "../inventory/mavenCentralMirror.js";
37
+ import { applyPurl, mavenPurl, tryBuildPurl } from "../inventory/purl.js";
38
+ import { xml2js } from "../parsers/xml.js";
35
39
  import { parsePkgJson } from "./parsers-js.js";
40
+ import { isInVendoredDir } from "./vendoredCode.js";
36
41
 
37
42
  /**
38
43
  * True when the pre-generation stage pinned an exact JVM build tool version
@@ -218,6 +223,50 @@ function gradleEvaluationOverrides(gradleArguments) {
218
223
  return overrides;
219
224
  }
220
225
 
226
+ /**
227
+ * Keep Gradle's task headers in the output. cdxgen splits the `properties`
228
+ * and `dependencies` reports by the `> Task :path` headers, which Gradle
229
+ * prints at the lifecycle log level, so a project that sets
230
+ * `org.gradle.logging.level=quiet` or `warn` in `gradle.properties` would
231
+ * leave nothing to split. A command-line `-D` property beats
232
+ * `gradle.properties`, and a log-level option such as `--info` beats the
233
+ * property. A log level the user chose in `GRADLE_ARGS` is left alone.
234
+ *
235
+ * @param {string[]} gradleArguments User-supplied general gradle arguments
236
+ * @returns {string[]} The `-D` flag to inject, or nothing
237
+ */
238
+ function gradleLogLevelOverride(gradleArguments) {
239
+ const logLevelOptions = [
240
+ "-q",
241
+ "--quiet",
242
+ "-w",
243
+ "--warn",
244
+ "-i",
245
+ "--info",
246
+ "-d",
247
+ "--debug",
248
+ ];
249
+ const userChose = gradleArguments.some(
250
+ (arg) =>
251
+ logLevelOptions.includes(arg) ||
252
+ arg.startsWith("-Dorg.gradle.logging.level="),
253
+ );
254
+ return userChose ? [] : ["-Dorg.gradle.logging.level=lifecycle"];
255
+ }
256
+
257
+ /**
258
+ * Read a whitespace-separated list of Gradle arguments from an environment
259
+ * variable such as `GRADLE_ARGS`. Repeated, leading or trailing whitespace
260
+ * adds no empty argument, which Gradle would read as a task path.
261
+ *
262
+ * @param {string} name Name of the environment variable
263
+ * @returns {string[]} The arguments, empty when the variable is unset
264
+ */
265
+ export function gradleArgsFromEnv(name) {
266
+ const value = readEnvironmentVariable(name);
267
+ return value ? value.split(/\s+/).filter(Boolean) : [];
268
+ }
269
+
221
270
  /**
222
271
  * Method to combine the general gradle arguments, the sub-commands and the sub-commands' arguments in the correct way
223
272
  *
@@ -242,6 +291,8 @@ export function buildGradleCommandArguments(
242
291
  ]
243
292
  .concat(getGradleDaemonParameter())
244
293
  .concat(gradleEvaluationOverrides(gradleArguments))
294
+ .concat(gradleLogLevelOverride(gradleArguments))
295
+ .concat(gradleMirrorInitScriptArgs())
245
296
  .concat(gradleArguments);
246
297
  const maxCliArgsLength = isWin
247
298
  ? 7500 - gradleCommandLength - mainGradleArguments.join(" ").length - 2
@@ -257,7 +308,13 @@ export function buildGradleCommandArguments(
257
308
  for (const gradleSubCommand of gradleSubCommands) {
258
309
  const subCommandLength =
259
310
  [gradleSubCommand, ...gradleSubCommandArguments].join(" ").length + 1;
260
- if (maxCliArgsLength !== -1 && remainingLength - subCommandLength < 0) {
311
+ // An invocation always carries at least one task: without one Gradle
312
+ // runs the project's default tasks.
313
+ if (
314
+ maxCliArgsLength !== -1 &&
315
+ remainingLength - subCommandLength < 0 &&
316
+ allGradleArguments.length > mainGradleArguments.length
317
+ ) {
261
318
  splitArgs.push(allGradleArguments);
262
319
  allGradleArguments = [].concat(mainGradleArguments);
263
320
  remainingLength = maxCliArgsLength;
@@ -297,16 +354,52 @@ function parseGradleTaskHeader(line) {
297
354
  if (!line.startsWith("> Task :")) {
298
355
  return undefined;
299
356
  }
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];
357
+ // A project path may contain spaces (`:my lib`), and a status such as
358
+ // UP-TO-DATE or FAILED may follow the task name, so the task name is the
359
+ // first token after the last colon. A status never contains a colon, and a
360
+ // CRLF line keeps its carriage return.
361
+ const taskPath = line.slice("> Task ".length).trim();
303
362
  const lastColon = taskPath.lastIndexOf(":");
304
363
  return {
305
364
  projectPath: taskPath.slice(0, lastColon),
306
- taskName: taskPath.slice(lastColon + 1),
365
+ taskName: taskPath.slice(lastColon + 1).split(/\s+/)[0],
307
366
  };
308
367
  }
309
368
 
369
+ /**
370
+ * List the dependency tasks to run for the root project and every module read
371
+ * from `gradle properties`.
372
+ *
373
+ * The root project is recognised by identity, never by purl. A module named
374
+ * after the root project that inherits its group and version has the root's
375
+ * purl (issue 4695), and it still needs its own `:module:dependencies` task.
376
+ * `root` in `modulesToSkip` drops the root task; any other entry is a module
377
+ * path.
378
+ *
379
+ * @param {Map<string, Object>} gradleModules Modules keyed by project path, and the root project by its name
380
+ * @param {Object} rootComponent The component built for the root project
381
+ * @param {string} gradleDepTask Name of the dependency task, usually `dependencies`
382
+ * @param {string[]} modulesToSkip Module paths, or `root`, to leave out
383
+ * @returns {string[]} Task paths in the order Gradle should run them
384
+ */
385
+ export function gradleDependencyTasks(
386
+ gradleModules,
387
+ rootComponent,
388
+ gradleDepTask,
389
+ modulesToSkip = [],
390
+ ) {
391
+ const tasks = [];
392
+ if (!modulesToSkip.includes("root")) {
393
+ tasks.push(gradleDepTask);
394
+ }
395
+ for (const [key, module] of gradleModules) {
396
+ if (module !== rootComponent && !modulesToSkip.includes(key)) {
397
+ tasks.push(`${key}:${gradleDepTask}`);
398
+ }
399
+ }
400
+ return tasks;
401
+ }
402
+
310
403
  /**
311
404
  * Method to split the output produced by Gradle using parallel processing by project
312
405
  *
@@ -398,27 +491,19 @@ export function parseGradleProjects(rawOutput) {
398
491
  }
399
492
  }
400
493
  }
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
- }
494
+ } else {
495
+ for (const marker of ["--- project ", "-> project "]) {
496
+ const markerIndex = l.indexOf(marker);
497
+ if (markerIndex === -1) {
498
+ continue;
410
499
  }
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
- }
500
+ const projName = gradleProjectPath(
501
+ l.slice(markerIndex + marker.length),
502
+ );
503
+ if (projName?.startsWith(":") && projName.length > 1) {
504
+ projects.add(projName);
421
505
  }
506
+ break;
422
507
  }
423
508
  }
424
509
  });
@@ -432,11 +517,17 @@ export function parseGradleProjects(rawOutput) {
432
517
  /**
433
518
  * Parse gradle properties output
434
519
  *
520
+ * `gradlePlugin` is true when the project has the `gradlePlugin` extension,
521
+ * which `java-gradle-plugin`, `kotlin-dsl` and `groovy-gradle-plugin` add: the
522
+ * project builds Gradle plugins, as a `build-logic` build does.
523
+ *
435
524
  * @param {string} rawOutput Raw string output
436
525
  * @param {string} gradleModuleName The name (or 'path') of the module as seen from the root of the project
526
+ * @returns {{rootProject: string, projects: string[], metadata: Object, gradlePlugin: boolean}} The parsed properties
437
527
  */
438
528
  export function parseGradleProperties(rawOutput, gradleModuleName = null) {
439
529
  let rootProject = "root";
530
+ let gradlePlugin = false;
440
531
  const projects = new Set();
441
532
  const metadata = { group: "", version: "latest", properties: [] };
442
533
  if (gradleModuleName) {
@@ -472,6 +563,8 @@ export function parseGradleProperties(rawOutput, gradleModuleName = null) {
472
563
  metadata[tmpB[0]] = tmpB[1].trim().replace("unspecified", "latest");
473
564
  } else if (["buildFile", "projectDir", "rootDir"].includes(tmpB[0])) {
474
565
  metadata.properties.push({ name: tmpB[0], value: tmpB[1].trim() });
566
+ } else if (tmpB[0] === "gradlePlugin") {
567
+ gradlePlugin = true;
475
568
  } else if (tmpB[0] === "subprojects") {
476
569
  const spStrs = tmpB[1].replace(/[[\]']/g, "").split(", ");
477
570
  const tmpprojects = spStrs
@@ -486,6 +579,7 @@ export function parseGradleProperties(rawOutput, gradleModuleName = null) {
486
579
  rootProject,
487
580
  projects: Array.from(projects),
488
581
  metadata,
582
+ gradlePlugin,
489
583
  };
490
584
  }
491
585
 
@@ -511,17 +605,11 @@ export function executeParallelGradleProperties(
511
605
  }
512
606
  const gradleCmd = getGradleCommand(dir, null, options);
513
607
  const gradleArgs = buildGradleCommandArguments(
514
- extraArgs.concat(
515
- readEnvironmentVariable("GRADLE_ARGS")
516
- ? readEnvironmentVariable("GRADLE_ARGS").split(" ")
517
- : [],
518
- ),
608
+ extraArgs.concat(gradleArgsFromEnv("GRADLE_ARGS")),
519
609
  allProjectsStr.map((project) =>
520
610
  project ? `${project}:properties` : "properties",
521
611
  ),
522
- readEnvironmentVariable("GRADLE_ARGS_PROPERTIES")
523
- ? readEnvironmentVariable("GRADLE_ARGS_PROPERTIES").split(" ")
524
- : [],
612
+ gradleArgsFromEnv("GRADLE_ARGS_PROPERTIES"),
525
613
  gradleCmd.length,
526
614
  );
527
615
  const allOutputs = [];
@@ -880,19 +968,539 @@ export function recordGradleInvocationFailure(result, context = {}) {
880
968
  });
881
969
  }
882
970
 
971
+ /**
972
+ * Configurations whose dependencies never reach the packaged application:
973
+ * code-quality tools, coverage agents and Spring Boot's development-only
974
+ * dependencies.
975
+ */
976
+ const GRADLE_TOOL_CONFIGURATIONS = new Set([
977
+ "checkstyle",
978
+ "codenarc",
979
+ "detekt",
980
+ "developmentOnly",
981
+ "errorprone",
982
+ "jacocoAgent",
983
+ "jacocoAnt",
984
+ "ktlint",
985
+ "lintChecks",
986
+ "lintClassPath",
987
+ "pmd",
988
+ "spotbugs",
989
+ "spotbugsPlugins",
990
+ ]);
991
+
992
+ /**
993
+ * Parts of a configuration name that mark it as feeding the compiler or a
994
+ * build tool, such as `annotationProcessor`, Android's
995
+ * `debugAnnotationProcessorClasspath` or `kotlinCompilerPluginClasspathMain`.
996
+ */
997
+ const GRADLE_TOOL_CONFIGURATION_PARTS = [
998
+ "annotationprocessor",
999
+ "buildtoolsapiclasspath",
1000
+ "commonizer",
1001
+ "compilerclasspath",
1002
+ "compilerpluginclasspath",
1003
+ ];
1004
+
1005
+ /**
1006
+ * Properties that describe a dependency Gradle could not resolve.
1007
+ */
1008
+ const GRADLE_FAILURE_PROPERTIES = [
1009
+ "cdx:gradle:resolutionFailed",
1010
+ "cdx:gradle:requestedVersion",
1011
+ ];
1012
+
1013
+ /**
1014
+ * Markers Gradle appends to a dependency tree line, with the flag each sets.
1015
+ */
1016
+ const GRADLE_TREE_LINE_MARKERS = [
1017
+ ["(*)", "omitted"],
1018
+ ["(c)", "constraint"],
1019
+ ["(n)", "unresolvable"],
1020
+ ["FAILED", "failed"],
1021
+ ];
1022
+
1023
+ /**
1024
+ * Read the name of the configuration a `gradle dependencies` header line
1025
+ * introduces, such as `runtimeClasspath - Runtime classpath of source set
1026
+ * 'main'.`, `api (n)`, a bare `classpath`, a Groovy name such as
1027
+ * `integration-test` or an internal one such as
1028
+ * `_internal-unified-test-platform-android-device-provider-ddmlib`.
1029
+ *
1030
+ * @param {string} line One line of the report
1031
+ * @returns {string|undefined} The configuration name, or undefined when the
1032
+ * line is not a configuration header
1033
+ */
1034
+ function parseGradleConfigurationHeader(line) {
1035
+ let name = line.split(" - ")[0];
1036
+ if (name.endsWith(" (n)")) {
1037
+ name = name.slice(0, -" (n)".length);
1038
+ }
1039
+ return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(name) ? name : undefined;
1040
+ }
1041
+
1042
+ /**
1043
+ * Configurations, other than runtime classpaths, whose dependencies ship
1044
+ * with the application: `compile` and `default` in Gradle 6 and older, which
1045
+ * the runtime classpath extended, and the Shadow plugin's `shadow`, whose
1046
+ * dependencies the published artifact declares as runtime dependencies.
1047
+ */
1048
+ const GRADLE_SHIPPED_CONFIGURATIONS = new Set(["compile", "default", "shadow"]);
1049
+
1050
+ /**
1051
+ * Map a Gradle configuration to the CycloneDX scope of the dependencies it
1052
+ * resolves, so that only what ships with the application reads as required.
1053
+ *
1054
+ * Configurations whose name starts with `_`, such as the Android Gradle
1055
+ * plugin's `_internal-unified-test-platform-*` ones, are the plugin's own
1056
+ * tooling and `excluded`. Test configurations are `optional`. Runtime
1057
+ * classpaths, including Android
1058
+ * variants such as `debugRuntimeClasspath` and Kotlin targets such as
1059
+ * `jvmRuntimeClasspath`, are `required`, and so are Kotlin/Native klibraries,
1060
+ * which are linked into the binary, and the configurations in
1061
+ * `GRADLE_SHIPPED_CONFIGURATIONS`. Everything else is `excluded`: annotation
1062
+ * processors, compiler, code-quality and other build-tool classpaths, and
1063
+ * compile classpaths, whose dependencies a runtime classpath also lists unless
1064
+ * they are compile-only, as Maven's `provided` scope is. A dependency seen in
1065
+ * several configurations keeps the strongest scope among them. Test and
1066
+ * runtime are matched as whole words of the camel-case name, so
1067
+ * `latestRuntimeClasspath` is a runtime classpath, not a test one.
1068
+ *
1069
+ * @param {string} configurationName Name of the configuration
1070
+ * @returns {string} The CycloneDX scope
1071
+ */
1072
+ function gradleConfigurationScope(configurationName) {
1073
+ const lowerName = configurationName.toLowerCase();
1074
+ const words = configurationName
1075
+ .split(/[-_.]|(?=[A-Z])/)
1076
+ .map((word) => word.toLowerCase());
1077
+ if (
1078
+ GRADLE_TOOL_CONFIGURATIONS.has(configurationName) ||
1079
+ configurationName.startsWith("_") ||
1080
+ ["kapt", "ksp"].includes(words[0]) ||
1081
+ GRADLE_TOOL_CONFIGURATION_PARTS.some((part) => lowerName.includes(part))
1082
+ ) {
1083
+ return "excluded";
1084
+ }
1085
+ if (words.includes("test") || words.includes("tests")) {
1086
+ return "optional";
1087
+ }
1088
+ if (
1089
+ words.includes("runtime") ||
1090
+ words.includes("klibraries") ||
1091
+ GRADLE_SHIPPED_CONFIGURATIONS.has(configurationName)
1092
+ ) {
1093
+ return "required";
1094
+ }
1095
+ return "excluded";
1096
+ }
1097
+
1098
+ /**
1099
+ * Rank a scope so that a dependency seen in several configurations keeps the
1100
+ * strongest one: `required` or no scope (which CycloneDX reads as required),
1101
+ * then `optional`, then `excluded`.
1102
+ *
1103
+ * @param {string|undefined} scope CycloneDX scope
1104
+ * @returns {number} The rank, higher being stronger
1105
+ */
1106
+ function gradleScopeRank(scope) {
1107
+ if (scope === "required" || scope === undefined) {
1108
+ return 2;
1109
+ }
1110
+ return scope === "optional" ? 1 : 0;
1111
+ }
1112
+
1113
+ /**
1114
+ * Read a project path from the text that follows `project ` in a dependency
1115
+ * tree line. Gradle 9 quotes the path (`project ':lib'`), Gradle 8 does not
1116
+ * (`project :lib`).
1117
+ *
1118
+ * @param {string} text The text after `project `
1119
+ * @returns {string|undefined} The project path, such as `:lib`
1120
+ */
1121
+ function gradleProjectPath(text) {
1122
+ const trimmed = text.trim();
1123
+ if (trimmed.startsWith("'")) {
1124
+ const end = trimmed.indexOf("'", 1);
1125
+ return end > 1 ? trimmed.slice(1, end) : undefined;
1126
+ }
1127
+ return trimmed.split(" ")[0] || undefined;
1128
+ }
1129
+
1130
+ /**
1131
+ * Find the module a project path in a dependency tree names. Subprojects are
1132
+ * keyed by their path (`:lib`) and the root project by its name, so the root
1133
+ * path `:` resolves to the report's own project when that is the root, or else
1134
+ * to the one module keyed by a name.
1135
+ *
1136
+ * @param {Map} gradleModules Modules keyed by project path or root name
1137
+ * @param {string} projectPath Project path from the tree, such as `:lib` or `:`
1138
+ * @param {string} reportProjectName Key of the project the report belongs to
1139
+ * @returns {Object|undefined} The module component
1140
+ */
1141
+ function gradleModuleForPath(gradleModules, projectPath, reportProjectName) {
1142
+ if (projectPath !== ":") {
1143
+ return gradleModules.get(projectPath);
1144
+ }
1145
+ if (!reportProjectName.startsWith(":")) {
1146
+ return gradleModules.get(reportProjectName);
1147
+ }
1148
+ const rootKeys = [...gradleModules.keys()].filter((k) => !k.startsWith(":"));
1149
+ return rootKeys.length === 1 ? gradleModules.get(rootKeys[0]) : undefined;
1150
+ }
1151
+
1152
+ /**
1153
+ * Pick a concrete version out of the version Gradle printed as requested. A
1154
+ * rich version such as `{strictly 1.0}`, `{require 2.2; reject 2.0}` or
1155
+ * `{prefer 2.0.9}` gives the version of its first strictly, require or prefer
1156
+ * clause. A range or dynamic version gives none.
1157
+ *
1158
+ * @param {string|undefined} versionText The requested version
1159
+ * @returns {string|undefined} The concrete version
1160
+ */
1161
+ function gradleRequestedVersion(versionText) {
1162
+ if (!versionText) {
1163
+ return undefined;
1164
+ }
1165
+ // `1.+` and `latest.release` are dynamic and a bracketed range is a
1166
+ // range, but `0.92.2+1.20.1` is a concrete version with build metadata.
1167
+ const isConcrete = (v) =>
1168
+ v &&
1169
+ !v.endsWith("+") &&
1170
+ !v.startsWith("latest.") &&
1171
+ ![..."[](),"].some((ch) => v.includes(ch));
1172
+ if (!versionText.startsWith("{")) {
1173
+ return isConcrete(versionText) ? versionText : undefined;
1174
+ }
1175
+ const body = versionText.slice(1, versionText.lastIndexOf("}"));
1176
+ for (const clause of body.split(";")) {
1177
+ const [keyword, ...rest] = clause.trim().split(" ");
1178
+ const value = rest.join(" ").trim();
1179
+ if (
1180
+ ["strictly", "require", "prefer"].includes(keyword) &&
1181
+ isConcrete(value)
1182
+ ) {
1183
+ return value;
1184
+ }
1185
+ }
1186
+ return undefined;
1187
+ }
1188
+
1189
+ /**
1190
+ * Split one line of a `gradle dependencies` tree into its depth and the
1191
+ * dependency it names. The selected side of `->` wins over the requested
1192
+ * side, whether it is a version, other coordinates or a project.
1193
+ *
1194
+ * @param {string} line One line of the report, without its line ending
1195
+ * @returns {Object|undefined} `{ level, group, name, version,
1196
+ * requestedVersion, projectPath, omitted, constraint, unresolvable, failed }`,
1197
+ * `{ level, unknown: true }` for a tree line whose dependency cannot be
1198
+ * read, or undefined when the line is not a tree line
1199
+ */
1200
+ function parseGradleTreeLine(line) {
1201
+ const markerIndex = line.indexOf("--- ");
1202
+ if (markerIndex < 1) {
1203
+ return undefined;
1204
+ }
1205
+ for (const ch of line.slice(0, markerIndex)) {
1206
+ if (!"|+\\ ".includes(ch)) {
1207
+ return undefined;
1208
+ }
1209
+ }
1210
+ const parsed = {
1211
+ level: Math.floor(markerIndex / 5) + 1,
1212
+ omitted: false,
1213
+ constraint: false,
1214
+ unresolvable: false,
1215
+ failed: false,
1216
+ };
1217
+ let text = line.slice(markerIndex + "--- ".length).trim();
1218
+ let stripped = true;
1219
+ while (stripped) {
1220
+ stripped = false;
1221
+ for (const [marker, flag] of GRADLE_TREE_LINE_MARKERS) {
1222
+ if (text.endsWith(` ${marker}`)) {
1223
+ parsed[flag] = true;
1224
+ text = text.slice(0, -marker.length - 1).trimEnd();
1225
+ stripped = true;
1226
+ }
1227
+ }
1228
+ }
1229
+ const [requested, selected] = text.split(" -> ");
1230
+ // Gradle 9.6 and later print the root project as `root project '<name>'`
1231
+ // where earlier versions print `project :`. Either way it is the root
1232
+ // project, whose path is `:`.
1233
+ if (requested.startsWith("root project ")) {
1234
+ parsed.projectPath = ":";
1235
+ } else if (requested.startsWith("project ")) {
1236
+ parsed.projectPath = gradleProjectPath(requested.slice("project ".length));
1237
+ } else {
1238
+ const [group, name, ...versionParts] = requested.split(":");
1239
+ if (!group || !name || group.includes(" ") || name.includes(" ")) {
1240
+ return { level: parsed.level, unknown: true };
1241
+ }
1242
+ parsed.group = group;
1243
+ parsed.name = name;
1244
+ parsed.requestedVersion = versionParts.join(":") || undefined;
1245
+ parsed.version = gradleRequestedVersion(parsed.requestedVersion);
1246
+ }
1247
+ if (selected?.startsWith("root project ")) {
1248
+ // A module substituted by the root project, as with
1249
+ // `substitute module('org.ex:main') using project(':')`.
1250
+ parsed.projectPath = ":";
1251
+ } else if (selected?.startsWith("project ")) {
1252
+ parsed.projectPath = gradleProjectPath(selected.slice("project ".length));
1253
+ } else if (selected?.includes(":")) {
1254
+ // The selected module replaces the requested one, version and all.
1255
+ const [group, name, version] = selected.split(":");
1256
+ parsed.group = group;
1257
+ parsed.name = name;
1258
+ parsed.version = version;
1259
+ parsed.requestedVersion = undefined;
1260
+ } else if (selected && !/\s/u.test(selected)) {
1261
+ // A selected version has no whitespace. Anything else is a form of the
1262
+ // report this parser does not know, and must not become a version.
1263
+ parsed.version = selected;
1264
+ } else if (selected) {
1265
+ return { level: parsed.level, unknown: true };
1266
+ }
1267
+ if (parsed.projectPath === undefined && !parsed.group) {
1268
+ return { level: parsed.level, unknown: true };
1269
+ }
1270
+ return parsed;
1271
+ }
1272
+
1273
+ /**
1274
+ * Read the properties of every project the dependency reports name that
1275
+ * `gradleModules` does not have yet, such as a project of an included build,
1276
+ * and add a module for each one whose properties task printed a report. One
1277
+ * Gradle invocation covers all of them.
1278
+ *
1279
+ * @param {Iterable<string>} reports Text of the `gradle dependencies` reports
1280
+ * @param {Map} gradleModules Modules keyed by project path, updated in place
1281
+ * @param {string} gradleRootPath Directory to run Gradle in
1282
+ * @param {Object} options CLI options
1283
+ * @returns {Promise<string[]>} Paths of the projects added
1284
+ */
1285
+ export async function loadGradleModulesForReports(
1286
+ reports,
1287
+ gradleModules,
1288
+ gradleRootPath,
1289
+ options = {},
1290
+ ) {
1291
+ const modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
1292
+ ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
1293
+ : [];
1294
+ const missing = new Set();
1295
+ for (const report of reports) {
1296
+ for (const projectPath of parseGradleProjects(report).projects) {
1297
+ if (
1298
+ !gradleModules.has(projectPath) &&
1299
+ !modulesToSkip.includes(projectPath)
1300
+ ) {
1301
+ missing.add(projectPath);
1302
+ }
1303
+ }
1304
+ }
1305
+ if (!missing.size || !isProjectToolingAllowed(options)) {
1306
+ return [];
1307
+ }
1308
+ const splitPropTaskOut = splitOutputByGradleProjects(
1309
+ executeParallelGradleProperties(gradleRootPath, [...missing], [], options),
1310
+ ["properties"],
1311
+ );
1312
+ const added = [];
1313
+ for (const projectPath of missing) {
1314
+ // A project whose properties task printed nothing has no metadata to
1315
+ // build a module from.
1316
+ if (!splitPropTaskOut.has(projectPath)) {
1317
+ continue;
1318
+ }
1319
+ const propMap = parseGradleProperties(
1320
+ splitPropTaskOut.get(projectPath),
1321
+ projectPath,
1322
+ );
1323
+ gradleModules.set(
1324
+ projectPath,
1325
+ await buildObjectForGradleModule(
1326
+ propMap.rootProject === "root"
1327
+ ? projectPath.split(":").pop()
1328
+ : propMap.rootProject,
1329
+ propMap.metadata,
1330
+ ),
1331
+ );
1332
+ added.push(projectPath);
1333
+ }
1334
+ return added;
1335
+ }
1336
+
1337
+ /**
1338
+ * Give the components of a build-tooling report, such as that of an included
1339
+ * build that builds Gradle plugins, the `excluded` scope that build-tool
1340
+ * configurations get. `reconcileGradleComponents` later keeps a stronger
1341
+ * scope that another project's report gives the same component.
1342
+ *
1343
+ * @param {Object[]} pkgList Components from one project's report, updated in place
1344
+ * @param {Map<string, Object>} gradleModules Modules, which keep their own scope
1345
+ * @returns {Object[]} The same list
1346
+ */
1347
+ export function excludeGradleBuildTooling(pkgList, gradleModules) {
1348
+ const modules = new Set(gradleModules.values());
1349
+ for (const pkg of pkgList) {
1350
+ if (!modules.has(pkg)) {
1351
+ pkg.scope = "excluded";
1352
+ }
1353
+ }
1354
+ return pkgList;
1355
+ }
1356
+
1357
+ /**
1358
+ * Reconcile the copies of a component that several Gradle projects report.
1359
+ * Every copy gets the strongest scope any project gave it, so a library one
1360
+ * module only tests with stays required when another module compiles against
1361
+ * it, and a dependency one project failed to resolve loses its failure
1362
+ * properties when another project resolved it.
1363
+ *
1364
+ * @param {Object[]} pkgList Components from all the projects, updated in place
1365
+ * @returns {Object[]} The same list
1366
+ */
1367
+ export function reconcileGradleComponents(pkgList) {
1368
+ const strongest = new Map();
1369
+ const resolvedSomewhere = new Set();
1370
+ const isFailed = (pkg) =>
1371
+ pkg.properties?.some((p) => p.name === "cdx:gradle:resolutionFailed");
1372
+ for (const pkg of pkgList) {
1373
+ const key = pkg?.["bom-ref"];
1374
+ if (!key) {
1375
+ continue;
1376
+ }
1377
+ if (
1378
+ !strongest.has(key) ||
1379
+ gradleScopeRank(pkg.scope) > gradleScopeRank(strongest.get(key))
1380
+ ) {
1381
+ strongest.set(key, pkg.scope);
1382
+ }
1383
+ if (!isFailed(pkg)) {
1384
+ resolvedSomewhere.add(key);
1385
+ }
1386
+ }
1387
+ for (const pkg of pkgList) {
1388
+ const key = pkg?.["bom-ref"];
1389
+ if (!key) {
1390
+ continue;
1391
+ }
1392
+ if (strongest.get(key) === undefined) {
1393
+ delete pkg.scope;
1394
+ } else {
1395
+ pkg.scope = strongest.get(key);
1396
+ }
1397
+ if (resolvedSomewhere.has(key) && isFailed(pkg)) {
1398
+ pkg.properties = pkg.properties.filter(
1399
+ (p) => !GRADLE_FAILURE_PROPERTIES.includes(p.name),
1400
+ );
1401
+ if (!pkg.properties.length) {
1402
+ delete pkg.properties;
1403
+ }
1404
+ }
1405
+ }
1406
+ return pkgList;
1407
+ }
1408
+
1409
+ /**
1410
+ * Whether a module name is one that platforms and BOMs use, such as
1411
+ * `jackson-bom`, `spring-boot-dependencies`, `micronaut-platform` or the AWS
1412
+ * SDK's `bom`.
1413
+ *
1414
+ * @param {string} name Module name
1415
+ * @returns {boolean} True for a platform-style name
1416
+ */
1417
+ function isGradlePlatformName(name) {
1418
+ return (
1419
+ name === "bom" ||
1420
+ ["-bom", "-dependencies", "-platform"].some((suffix) =>
1421
+ name.endsWith(suffix),
1422
+ )
1423
+ );
1424
+ }
1425
+
1426
+ /**
1427
+ * Give the platforms and BOMs in one report a `pom` type and the `excluded`
1428
+ * scope. Gradle prints a platform like a library, with only constraints
1429
+ * (`(c)`) under it, but a platform is a pom that never ships. A module counts
1430
+ * as a platform when its name is one platforms use and the report gives it no
1431
+ * dependency of its own, so a jar such as `jna-platform`, which depends on
1432
+ * `jna`, stays a jar.
1433
+ *
1434
+ * @param {Map<string, {adep: Object, isModule: boolean}>} depsByRef Components of the report by bom-ref, updated in place
1435
+ * @param {Object<string, string[]>} levelTrees Edges of the report by bom-ref
1436
+ * @returns {Map<string, string>} The new bom-ref of each renamed component, keyed by its old one
1437
+ */
1438
+ function markGradlePlatforms(depsByRef, levelTrees) {
1439
+ const renamed = new Map();
1440
+ for (const [bomRef, { adep, isModule }] of depsByRef) {
1441
+ if (
1442
+ isModule ||
1443
+ adep.qualifiers?.type !== "jar" ||
1444
+ !isGradlePlatformName(adep.name || "") ||
1445
+ levelTrees[bomRef]?.length
1446
+ ) {
1447
+ continue;
1448
+ }
1449
+ const purl = tryBuildPurl({
1450
+ type: "maven",
1451
+ namespace: adep.group || null,
1452
+ name: adep.name,
1453
+ version: adep.version || null,
1454
+ qualifiers: { type: "pom" },
1455
+ });
1456
+ if (!purl) {
1457
+ continue;
1458
+ }
1459
+ adep.qualifiers = { type: "pom" };
1460
+ adep.scope = "excluded";
1461
+ applyPurl(adep, purl);
1462
+ renamed.set(bomRef, adep["bom-ref"]);
1463
+ }
1464
+ return renamed;
1465
+ }
1466
+
883
1467
  /**
884
1468
  * Method to resolve dependencies from a gradle output
885
1469
  *
1470
+ * A constraint line (`(c)`) adds neither a component nor an edge. Gradle
1471
+ * prints a nested constraint under the platform or module that declares it,
1472
+ * and only when the dependency it affects is resolved elsewhere in the tree,
1473
+ * where that dependency gets its component and edges. A top-level
1474
+ * `{strictly …}` constraint from consistent resolution can name a module the
1475
+ * configuration never resolves. A dependency that failed to resolve carries
1476
+ * `cdx:gradle:resolutionFailed`, keeps the version it was requested at when
1477
+ * that is a concrete version, and otherwise records the request in
1478
+ * `cdx:gradle:requestedVersion`. A dependency seen in several configurations
1479
+ * keeps the strongest scope among them. Platforms and BOMs get a `pom` type
1480
+ * and the `excluded` scope.
1481
+ *
1482
+ * The parser runs no Gradle task. A project the tree names but `gradleModules`
1483
+ * lacks gets a component built from the root project's group and version; use
1484
+ * `loadGradleModulesForReports` first to read such projects' properties.
1485
+ *
1486
+ * The lines Gradle prints under another project, such as `project ':lib'` in
1487
+ * the report of `:app`, show that project's dependencies as `:app` resolved
1488
+ * them, after conflict resolution. When `reportedProjects` names that project,
1489
+ * its own report supplies its edges, so these lines add no edge from it.
1490
+ *
886
1491
  * @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
1492
+ * @param {string} rootProjectName Key in `gradleModules` of the project the report belongs to
1493
+ * @param {map} gradleModules Gradle modules keyed by project path, and the root project by its name
1494
+ * @param {string} gradleRootPath Directory Gradle ran in, recorded as `cdx:gradle:GradleRootPath`
1495
+ * @param {Set<string>} [reportedProjects] Keys in `gradleModules` of the projects whose own reports are parsed
1496
+ * @returns {{pkgList: Object[], dependenciesList: Object[]}|{}} The components and dependency graph
890
1497
  */
891
1498
  export async function parseGradleDep(
892
1499
  rawOutput,
893
1500
  rootProjectName = "root",
894
1501
  gradleModules = new Map(),
895
1502
  gradleRootPath = "",
1503
+ reportedProjects = new Set(),
896
1504
  ) {
897
1505
  if (typeof rawOutput === "string") {
898
1506
  // Bug: 249. Get any sub-projects refered here
@@ -907,225 +1515,238 @@ export async function parseGradleDep(
907
1515
  ) {
908
1516
  rootProjectName = retMap.rootProject;
909
1517
  }
910
- let match = "";
911
1518
  // To render dependency tree we need a root project
912
1519
  const rootProject = gradleModules.get(rootProjectName);
1520
+ if (!rootProject) {
1521
+ thoughtLog(
1522
+ `The Gradle report for ${rootProjectName} belongs to no module read from 'gradle properties', so it has no parent to hang its dependency tree from.`,
1523
+ );
1524
+ return {};
1525
+ }
913
1526
  const deps = [];
914
1527
  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;
1528
+ const depsByRef = new Map();
1529
+ const firstBomRef = rootProject["bom-ref"];
921
1530
  const level_trees = {};
922
- level_trees[last_bomref] = [];
1531
+ level_trees[firstBomRef] = [];
1532
+ // Modules whose own report supplies their edges.
1533
+ const reportedModuleRefs = new Set();
1534
+ for (const key of reportedProjects) {
1535
+ const reportedRef = gradleModules.get(key)?.["bom-ref"];
1536
+ if (reportedRef && reportedRef !== firstBomRef) {
1537
+ reportedModuleRefs.add(reportedRef);
1538
+ }
1539
+ }
923
1540
  let scope;
924
1541
  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
- }
1542
+ // ancestors[level] is the bom-ref of the line most recently seen at that
1543
+ // depth; ancestors[0] is the project the report belongs to.
1544
+ const ancestors = [firstBomRef];
1545
+ const lines = rawOutput.split("\n").map((l) => l.replaceAll("\r", ""));
1546
+ // Modules some configuration resolved to a version, so that a failed,
1547
+ // versionless request for one of them adds nothing.
1548
+ const resolvedModules = new Set();
1549
+ for (const line of lines) {
1550
+ const parsed = parseGradleTreeLine(line);
1551
+ if (
1552
+ parsed?.group &&
1553
+ parsed.version !== undefined &&
1554
+ !parsed.failed &&
1555
+ !parsed.constraint &&
1556
+ !parsed.unresolvable
1557
+ ) {
1558
+ resolvedModules.add(`${parsed.group}:${parsed.name}`);
962
1559
  }
963
- level_trees[last_bomref] = subDependsOn;
964
1560
  }
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) {
1561
+ for (const rline of lines) {
1562
+ const configurationName = parseGradleConfigurationHeader(rline);
1563
+ if (configurationName) {
1564
+ ancestors.length = 1;
1565
+ profileName = configurationName;
1566
+ scope = gradleConfigurationScope(configurationName);
1567
+ }
1568
+ if (rline.trim() === "") {
1569
+ ancestors.length = 1;
970
1570
  continue;
971
1571
  }
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
- ) {
1572
+ const parsed = parseGradleTreeLine(rline);
1573
+ if (!parsed) {
1574
+ continue;
1575
+ }
1576
+ // A line that names nothing usable still takes its place in the tree,
1577
+ // so the lines under it get no parent rather than a wrong one.
1578
+ if (parsed.unknown || parsed.unresolvable) {
1579
+ ancestors.length = Math.min(ancestors.length, parsed.level);
980
1580
  continue;
981
1581
  }
1582
+ const { level } = parsed;
1583
+ const isProject = parsed.projectPath !== undefined;
1584
+ if (parsed.constraint) {
1585
+ ancestors.length = Math.min(ancestors.length, level);
1586
+ continue;
1587
+ }
1588
+ // Only a dependency that failed to resolve can lack a version: Gradle
1589
+ // prints every resolved one with the version it selected.
982
1590
  if (
983
- trimmedLine === "" ||
984
- rline.startsWith("+--- ") ||
985
- rline.startsWith("\\--- ")
1591
+ !isProject &&
1592
+ parsed.version === undefined &&
1593
+ (!parsed.failed ||
1594
+ resolvedModules.has(`${parsed.group}:${parsed.name}`))
986
1595
  ) {
987
- last_level = 1;
988
- last_project_bomref = first_bomref;
989
- last_bomref = last_project_bomref;
990
- stack = [first_bomref];
1596
+ ancestors.length = Math.min(ancestors.length, level);
1597
+ continue;
991
1598
  }
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;
1599
+ // A project the modules lack is named as Gradle publishes it by
1600
+ // default: the last segment of its path, with the root project's group
1601
+ // and version.
1602
+ const name = isProject
1603
+ ? parsed.projectPath.split(":").pop()
1604
+ : parsed.name;
1605
+ const group = isProject ? rootProject.group : parsed.group;
1606
+ const version = isProject ? rootProject.version : parsed.version;
1607
+ const gradleModule = isProject
1608
+ ? gradleModuleForPath(
1609
+ gradleModules,
1610
+ parsed.projectPath,
1611
+ rootProjectName,
1612
+ )
1613
+ : undefined;
1614
+ if (isProject && !gradleModule && name === "") {
1615
+ ancestors.length = Math.min(ancestors.length, level);
1616
+ continue;
1617
+ }
1618
+ let purl;
1619
+ let bomRef;
1620
+ if (gradleModule) {
1621
+ purl = gradleModule["purl"];
1622
+ bomRef = gradleModule["bom-ref"];
1623
+ } else {
1624
+ try {
1625
+ purl = build({
1626
+ type: "maven",
1627
+ namespace: group || null,
1628
+ name,
1629
+ version: version || null,
1630
+ qualifiers: { type: "jar" },
1631
+ });
1632
+ bomRef = decodeURIComponent(purl);
1633
+ } catch {
1634
+ // A purl that cannot be built keeps a name-only bom-ref so the
1635
+ // component still lands in the graph.
1636
+ purl = undefined;
1637
+ bomRef = name;
1000
1638
  }
1001
1639
  }
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
- }
1640
+ const existing = depsByRef.get(bomRef);
1641
+ // The project the report belongs to is the parent, not a component.
1642
+ if (!existing && bomRef !== firstBomRef) {
1643
+ let adep;
1644
+ if (gradleModule) {
1645
+ adep = gradleModule;
1646
+ } else {
1647
+ adep = {
1648
+ group,
1649
+ name: name,
1650
+ version,
1651
+ qualifiers: { type: "jar" },
1652
+ };
1653
+ if (version === undefined) {
1654
+ delete adep.version;
1027
1655
  }
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
- }
1656
+ adep["purl"] = purl;
1657
+ adep["bom-ref"] = bomRef;
1658
+ if (scope) {
1659
+ adep["scope"] = scope;
1051
1660
  }
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);
1661
+ adep.properties = [];
1662
+ if (profileName) {
1663
+ adep.properties.push({
1664
+ name: "internal:GradleProfileName",
1665
+ value: profileName,
1666
+ });
1089
1667
  }
1090
- if (!level_trees[bomRef]) {
1091
- level_trees[bomRef] = [];
1668
+ if (gradleRootPath && gradleRootPath !== ".") {
1669
+ adep.properties.push({
1670
+ name: "cdx:gradle:GradleRootPath",
1671
+ value: gradleRootPath,
1672
+ });
1092
1673
  }
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);
1674
+ if (parsed.failed) {
1675
+ adep.properties.push({
1676
+ name: "cdx:gradle:resolutionFailed",
1677
+ value: "true",
1678
+ });
1679
+ if (
1680
+ parsed.requestedVersion &&
1681
+ parsed.requestedVersion !== version
1682
+ ) {
1683
+ adep.properties.push({
1684
+ name: "cdx:gradle:requestedVersion",
1685
+ value: parsed.requestedVersion.slice(0, 256),
1686
+ });
1106
1687
  }
1688
+ }
1689
+ }
1690
+ if (adep?.properties?.length === 0) {
1691
+ delete adep.properties;
1692
+ }
1693
+ depsByRef.set(bomRef, { adep, isModule: !!gradleModule });
1694
+ deps.push(adep);
1695
+ } else if (existing && !existing.isModule) {
1696
+ const { adep } = existing;
1697
+ if (gradleScopeRank(scope) > gradleScopeRank(adep.scope)) {
1698
+ if (scope) {
1699
+ adep.scope = scope;
1107
1700
  } 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);
1701
+ delete adep.scope;
1702
+ }
1703
+ const profileProp = adep.properties?.find(
1704
+ (p) => p.name === "internal:GradleProfileName",
1705
+ );
1706
+ if (profileProp && profileName) {
1707
+ profileProp.value = profileName;
1708
+ }
1709
+ }
1710
+ // A dependency one configuration failed to resolve and another
1711
+ // resolved is not a failed dependency.
1712
+ if (!parsed.failed && adep.properties) {
1713
+ adep.properties = adep.properties.filter(
1714
+ (p) => !GRADLE_FAILURE_PROPERTIES.includes(p.name),
1715
+ );
1716
+ if (!adep.properties.length) {
1717
+ delete adep.properties;
1119
1718
  }
1120
- last_level = level;
1121
- last_bomref = bomRef;
1122
1719
  }
1123
1720
  }
1721
+ if (!level_trees[bomRef]) {
1722
+ level_trees[bomRef] = [];
1723
+ }
1724
+ // A line whose parent was skipped has no known parent and gets no edge.
1725
+ const parentRef =
1726
+ ancestors.length >= level ? ancestors[level - 1] : undefined;
1727
+ if (
1728
+ parentRef &&
1729
+ parentRef !== bomRef &&
1730
+ !reportedModuleRefs.has(parentRef)
1731
+ ) {
1732
+ const cnodes = level_trees[parentRef] || [];
1733
+ if (!cnodes.includes(bomRef)) {
1734
+ cnodes.push(bomRef);
1735
+ }
1736
+ level_trees[parentRef] = cnodes;
1737
+ }
1738
+ ancestors.length = Math.min(ancestors.length, level);
1739
+ if (ancestors.length === level) {
1740
+ ancestors.push(bomRef);
1741
+ }
1124
1742
  }
1743
+ const renamedRefs = markGradlePlatforms(depsByRef, level_trees);
1125
1744
  for (const lk of Object.keys(level_trees)) {
1126
1745
  dependenciesList.push({
1127
- ref: lk,
1128
- dependsOn: [...new Set(level_trees[lk])].sort(),
1746
+ ref: renamedRefs.get(lk) || lk,
1747
+ dependsOn: [
1748
+ ...new Set(level_trees[lk].map((ref) => renamedRefs.get(ref) || ref)),
1749
+ ].sort(),
1129
1750
  });
1130
1751
  }
1131
1752
  return {
@@ -1830,8 +2451,16 @@ function catalogCoordinate(entry) {
1830
2451
  }
1831
2452
 
1832
2453
  /**
1833
- * Resolve the version of one catalog entry.
1834
- *
2454
+ * Gradle rich-version syntax that names no single version: a bracketed range,
2455
+ * an interval, a `!!` or `{strictly}` marker, a `->` transition, or a `..`
2456
+ * separator. A version text matching this is a declaration about versions, not
2457
+ * one version.
2458
+ */
2459
+ const GRADLE_RICH_VERSION_PATTERN = /[[\](),!]|->|\.\./u;
2460
+
2461
+ /**
2462
+ * Resolve the version of one catalog entry.
2463
+ *
1835
2464
  * An entry may pin `version = "1.2.3"` directly or reference the versions
1836
2465
  * table with `version.ref = "alias"`. Version references that are missing, or
1837
2466
  * that carry Gradle rich-version syntax (ranges, `!!`, separators), resolve
@@ -1851,8 +2480,1059 @@ function resolveCatalogVersion(entry, versions) {
1851
2480
  if (typeof raw !== "string" || !raw) {
1852
2481
  return undefined;
1853
2482
  }
1854
- if (/[[\](),!]|->|\.\./u.test(raw)) {
2483
+ if (GRADLE_RICH_VERSION_PATTERN.test(raw)) {
1855
2484
  return undefined;
1856
2485
  }
1857
2486
  return raw;
1858
2487
  }
2488
+
2489
+ /**
2490
+ * The largest Gradle lockfile read. Real lockfiles of large multi-project
2491
+ * builds stay well under a megabyte; the file is untrusted input, so a larger
2492
+ * one is skipped whole rather than read in part.
2493
+ */
2494
+ const GRADLE_LOCKFILE_MAX_BYTES = 8 * 1024 * 1024;
2495
+
2496
+ /**
2497
+ * The most lines read from one Gradle lockfile. Lines past the cap are
2498
+ * ignored.
2499
+ */
2500
+ const GRADLE_LOCKFILE_MAX_LINES = 100000;
2501
+
2502
+ /**
2503
+ * Gradle 6+ lockfiles that lock a buildscript classpath rather than the
2504
+ * project's own configurations.
2505
+ */
2506
+ const GRADLE_BUILD_TOOLING_LOCKFILES = new Set([
2507
+ "buildscript-gradle.lockfile",
2508
+ "settings-gradle.lockfile",
2509
+ ]);
2510
+
2511
+ const GRADLE_LOCKFILE_SUFFIX = ".lockfile";
2512
+
2513
+ /**
2514
+ * Work out which kind of Gradle lockfile a path names.
2515
+ *
2516
+ * Gradle 6 and later write one `gradle.lockfile` per project, plus
2517
+ * `buildscript-gradle.lockfile` and `settings-gradle.lockfile` for the
2518
+ * buildscript classpaths. Gradle 4.8 to 5.x wrote one
2519
+ * `gradle/dependency-locks/<configuration>.lockfile` per configuration, with
2520
+ * the buildscript configurations prefixed `buildscript-`.
2521
+ *
2522
+ * @param {string} lockFile Path to the lockfile
2523
+ * @returns {{legacy: boolean, buildTooling: boolean, configuration: (string|undefined)}|undefined}
2524
+ * The kind, or undefined when the name is not a lockfile name
2525
+ */
2526
+ function gradleLockfileKind(lockFile) {
2527
+ const fileName = basename(lockFile);
2528
+ if (!fileName.endsWith(GRADLE_LOCKFILE_SUFFIX)) {
2529
+ return undefined;
2530
+ }
2531
+ const parentDir = dirname(lockFile);
2532
+ if (
2533
+ basename(parentDir) === "dependency-locks" &&
2534
+ basename(dirname(parentDir)) === "gradle"
2535
+ ) {
2536
+ const configuration = fileName.slice(0, -GRADLE_LOCKFILE_SUFFIX.length);
2537
+ if (!configuration) {
2538
+ return undefined;
2539
+ }
2540
+ return {
2541
+ legacy: true,
2542
+ buildTooling: configuration.startsWith("buildscript-"),
2543
+ configuration,
2544
+ };
2545
+ }
2546
+ return {
2547
+ legacy: false,
2548
+ buildTooling: GRADLE_BUILD_TOOLING_LOCKFILES.has(fileName),
2549
+ configuration: undefined,
2550
+ };
2551
+ }
2552
+
2553
+ /**
2554
+ * Read a `group:name:version` coordinate from a lockfile entry.
2555
+ *
2556
+ * Gradle writes only resolved versions to a lockfile, so a version that is
2557
+ * blank, a range, a dynamic selector (`1.+`, `latest.release`) or any other
2558
+ * rich-version syntax means the line was not written by Gradle and is
2559
+ * skipped.
2560
+ *
2561
+ * @param {string} text Coordinate text, already trimmed
2562
+ * @returns {{group: string, name: string, version: string}|undefined}
2563
+ * The coordinate, or undefined for anything else
2564
+ */
2565
+ function parseGradleLockCoordinate(text) {
2566
+ if (!text || /[\s=]/u.test(text)) {
2567
+ return undefined;
2568
+ }
2569
+ const parts = text.split(":");
2570
+ if (parts.length !== 3 || parts.some((part) => !part)) {
2571
+ return undefined;
2572
+ }
2573
+ const [group, name, version] = parts;
2574
+ // Gradle writes only resolved versions to a lockfile, so a rich version or
2575
+ // a dynamic selector (`1.+`, `2.*`, `latest.release`) means the line was
2576
+ // not written by Gradle. The rich-version rejection is the one
2577
+ // `resolveCatalogVersion` applies; a trailing `+` and a `*` are the
2578
+ // dynamic selectors.
2579
+ if (
2580
+ version.startsWith("latest.") ||
2581
+ version.endsWith("+") ||
2582
+ version.includes("*") ||
2583
+ GRADLE_RICH_VERSION_PATTERN.test(version)
2584
+ ) {
2585
+ return undefined;
2586
+ }
2587
+ return { group, name, version };
2588
+ }
2589
+
2590
+ /**
2591
+ * Parse one Gradle dependency lockfile.
2592
+ *
2593
+ * Both layouts are read. A Gradle 6+ `gradle.lockfile` has one
2594
+ * `group:name:version=configuration,configuration` line per module and an
2595
+ * `empty=` line naming the configurations that resolved nothing. A legacy
2596
+ * `gradle/dependency-locks/<configuration>.lockfile` has one
2597
+ * `group:name:version` line per module, and the file name is the
2598
+ * configuration.
2599
+ *
2600
+ * The scope of a module is the strongest that `gradleConfigurationScope`
2601
+ * gives any of its configurations, so a module locked for both
2602
+ * `testRuntimeClasspath` and `runtimeClasspath` is `required`. Everything in
2603
+ * a buildscript or settings lockfile is build tooling and `excluded`.
2604
+ *
2605
+ * A lockfile records resolved versions, so components carry a purl identity
2606
+ * with confidence 1 and the lockfile path as the evidence value. The purl and
2607
+ * bom-ref match what `parseGradleDep` builds for the same coordinate, so a
2608
+ * component reads the same whether Gradle ran or not. A lockfile is flat: it
2609
+ * says nothing about which module depends on which, so no dependency graph is
2610
+ * returned.
2611
+ *
2612
+ * The file is untrusted input. It is read only when it is a regular file no
2613
+ * larger than 8 MiB, at most 100000 lines are read, and a malformed line
2614
+ * costs only itself. Line content never reaches a property.
2615
+ *
2616
+ * @param {string} lockFile Path to the lockfile
2617
+ * @param {Object} [options] Parse options
2618
+ * @param {string} [options.rootDir] Directory the lockfile must resolve inside
2619
+ * @returns {Object[]} Components, one per locked module
2620
+ */
2621
+ export function parseGradleLockfile(lockFile, options = {}) {
2622
+ const kind = gradleLockfileKind(lockFile);
2623
+ if (!kind) {
2624
+ return [];
2625
+ }
2626
+ const text = readRegularTextFile(
2627
+ lockFile,
2628
+ GRADLE_LOCKFILE_MAX_BYTES,
2629
+ options?.rootDir,
2630
+ );
2631
+ if (text === undefined) {
2632
+ if (safeExistsSync(lockFile)) {
2633
+ console.warn(
2634
+ `Skipping ${lockFile}: it is not a regular file inside the project, or is larger than ${GRADLE_LOCKFILE_MAX_BYTES} bytes.`,
2635
+ );
2636
+ }
2637
+ return [];
2638
+ }
2639
+ const body = text.startsWith("\uFEFF") ? text.slice(1) : text;
2640
+ const pkgsByRef = new Map();
2641
+ let skippedLines = 0;
2642
+ let lineStart = 0;
2643
+ let lineCount = 0;
2644
+ while (lineStart <= body.length) {
2645
+ if (lineCount === GRADLE_LOCKFILE_MAX_LINES) {
2646
+ if (lineStart < body.length) {
2647
+ thoughtLog(
2648
+ `${lockFile} has more than ${GRADLE_LOCKFILE_MAX_LINES} lines; only the first ${GRADLE_LOCKFILE_MAX_LINES} are read.`,
2649
+ );
2650
+ }
2651
+ break;
2652
+ }
2653
+ let lineEnd = body.indexOf("\n", lineStart);
2654
+ if (lineEnd === -1) {
2655
+ lineEnd = body.length;
2656
+ }
2657
+ const line = body.slice(lineStart, lineEnd).trim();
2658
+ lineStart = lineEnd + 1;
2659
+ lineCount++;
2660
+ if (!line || line.startsWith("#")) {
2661
+ continue;
2662
+ }
2663
+ let coordinateText = line;
2664
+ let configurations = [kind.configuration];
2665
+ if (!kind.legacy) {
2666
+ const separator = line.indexOf("=");
2667
+ if (separator === -1) {
2668
+ skippedLines++;
2669
+ continue;
2670
+ }
2671
+ coordinateText = line.slice(0, separator).trim();
2672
+ if (coordinateText === "empty") {
2673
+ continue;
2674
+ }
2675
+ configurations = line
2676
+ .slice(separator + 1)
2677
+ .split(",")
2678
+ .map((configuration) => configuration.trim())
2679
+ .filter(Boolean);
2680
+ }
2681
+ const coordinate = parseGradleLockCoordinate(coordinateText);
2682
+ if (!coordinate || !configurations.length) {
2683
+ skippedLines++;
2684
+ continue;
2685
+ }
2686
+ const { group, name, version } = coordinate;
2687
+ const purl = mavenPurl(group, name, version, { type: "jar" });
2688
+ if (!purl) {
2689
+ skippedLines++;
2690
+ continue;
2691
+ }
2692
+ let scope = "excluded";
2693
+ if (!kind.buildTooling) {
2694
+ for (const configuration of configurations) {
2695
+ const configurationScope = gradleConfigurationScope(configuration);
2696
+ if (gradleScopeRank(configurationScope) > gradleScopeRank(scope)) {
2697
+ scope = configurationScope;
2698
+ }
2699
+ }
2700
+ }
2701
+ const bomRef = decodeURIComponent(purl);
2702
+ const existing = pkgsByRef.get(bomRef);
2703
+ if (existing) {
2704
+ if (gradleScopeRank(scope) > gradleScopeRank(existing.scope)) {
2705
+ existing.scope = scope;
2706
+ }
2707
+ continue;
2708
+ }
2709
+ pkgsByRef.set(bomRef, {
2710
+ group,
2711
+ name,
2712
+ version,
2713
+ qualifiers: { type: "jar" },
2714
+ type: "library",
2715
+ scope,
2716
+ purl,
2717
+ "bom-ref": bomRef,
2718
+ properties: [{ name: "internal:SrcFile", value: lockFile }],
2719
+ evidence: {
2720
+ identity: {
2721
+ field: "purl",
2722
+ confidence: 1,
2723
+ concludedValue: purl,
2724
+ methods: [
2725
+ {
2726
+ technique: "manifest-analysis",
2727
+ confidence: 1,
2728
+ value: lockFile,
2729
+ },
2730
+ ],
2731
+ },
2732
+ },
2733
+ });
2734
+ }
2735
+ if (skippedLines) {
2736
+ thoughtLog(
2737
+ `Skipped ${skippedLines} malformed or unresolved line(s) in ${lockFile}.`,
2738
+ );
2739
+ }
2740
+ return [...pkgsByRef.values()];
2741
+ }
2742
+
2743
+ /**
2744
+ * Parse several Gradle lockfiles of one build and merge the result.
2745
+ *
2746
+ * Every project of a multi-project build has its own lockfile, so the same
2747
+ * module often appears in several of them with different configurations. The
2748
+ * copies collapse into one component with the strongest scope any lockfile
2749
+ * gave it (see `reconcileGradleComponents`), and its evidence lists every
2750
+ * lockfile that locked it. The first lockfile that locked a module stays its
2751
+ * `internal:SrcFile`.
2752
+ *
2753
+ * @param {string[]} lockFiles Paths to the lockfiles
2754
+ * @param {Object} [options] Parse options, as for `parseGradleLockfile`
2755
+ * @returns {Object[]} Components, one per locked module
2756
+ */
2757
+ export function parseGradleLockfiles(lockFiles, options = {}) {
2758
+ const allPkgs = [];
2759
+ for (const lockFile of lockFiles || []) {
2760
+ allPkgs.push(...parseGradleLockfile(lockFile, options));
2761
+ }
2762
+ reconcileGradleComponents(allPkgs);
2763
+ const pkgsByRef = new Map();
2764
+ for (const pkg of allPkgs) {
2765
+ const existing = pkgsByRef.get(pkg["bom-ref"]);
2766
+ if (!existing) {
2767
+ pkgsByRef.set(pkg["bom-ref"], pkg);
2768
+ continue;
2769
+ }
2770
+ const methods = existing.evidence.identity.methods;
2771
+ for (const method of pkg.evidence.identity.methods) {
2772
+ if (!methods.some((m) => m.value === method.value)) {
2773
+ methods.push(method);
2774
+ }
2775
+ }
2776
+ }
2777
+ return [...pkgsByRef.values()];
2778
+ }
2779
+
2780
+ /**
2781
+ * The build files that mark a directory as a Gradle project of its own. A
2782
+ * lockfile is accepted only beside one of these, so a lockfile copied into a
2783
+ * notes folder or a build's output is not read as an inventory source.
2784
+ */
2785
+ const GRADLE_PROJECT_MARKERS = [
2786
+ "build.gradle",
2787
+ "build.gradle.kts",
2788
+ "settings.gradle",
2789
+ "settings.gradle.kts",
2790
+ ];
2791
+
2792
+ /**
2793
+ * Find the Gradle dependency lockfiles of one build.
2794
+ *
2795
+ * Both layouts are searched below `rootPath`, recursively even when
2796
+ * `options.multiProject` is off: every subproject of a Gradle build locks its
2797
+ * own configurations, and the subprojects belong to the same build. The
2798
+ * Gradle 6+ names are `gradle.lockfile`, `buildscript-gradle.lockfile` and
2799
+ * `settings-gradle.lockfile`; the Gradle 4.8 to 5.x layout is
2800
+ * `gradle/dependency-locks/<configuration>.lockfile`.
2801
+ *
2802
+ * A lockfile is accepted only when the project directory that owns it holds a
2803
+ * Gradle build file, and never when it sits under `build/`, `.gradle/`,
2804
+ * `node_modules/` or a vendored directory. `getAllFiles` already leaves
2805
+ * `.git`, `.hg`, `.chen` and `node_modules` out; `build/` and `.gradle/` are
2806
+ * Gradle's own output and cache directories, which a lockfile in them cannot
2807
+ * belong to the project's source of truth.
2808
+ *
2809
+ * Custom `dependencyLocking.lockFile` paths, the Nebula `dependencies.lock`
2810
+ * and Palantir `versions.lock` formats are not looked for.
2811
+ *
2812
+ * @param {string} rootPath Directory to search
2813
+ * @param {Object} [options] CLI options
2814
+ * @returns {string[]} Lockfile paths, in a stable order
2815
+ */
2816
+ export function findGradleLockfiles(rootPath, options = {}) {
2817
+ const lockFiles = new Set();
2818
+ for (const pattern of [
2819
+ "**/{gradle,buildscript-gradle,settings-gradle}.lockfile",
2820
+ "**/gradle/dependency-locks/*.lockfile",
2821
+ ]) {
2822
+ for (const lockFile of getAllFiles(rootPath, pattern, options)) {
2823
+ // Only the segments below the scan root count: a scan root that sits
2824
+ // under a directory named `build` still has lockfiles of its own.
2825
+ const segments = relative(rootPath, lockFile).split(/[\\/]/);
2826
+ if (
2827
+ segments.some((segment) =>
2828
+ ["build", ".gradle", "node_modules"].includes(segment),
2829
+ )
2830
+ ) {
2831
+ continue;
2832
+ }
2833
+ if (isInVendoredDir(rootPath, lockFile)) {
2834
+ continue;
2835
+ }
2836
+ const kind = gradleLockfileKind(lockFile);
2837
+ if (!kind) {
2838
+ continue;
2839
+ }
2840
+ // The project directory is the lockfile's own directory, and the
2841
+ // directory above `gradle` for a legacy dependency-locks file.
2842
+ const projectDir = kind.legacy
2843
+ ? dirname(dirname(dirname(lockFile)))
2844
+ : dirname(lockFile);
2845
+ if (
2846
+ !GRADLE_PROJECT_MARKERS.some((marker) =>
2847
+ safeExistsSync(join(projectDir, marker)),
2848
+ )
2849
+ ) {
2850
+ continue;
2851
+ }
2852
+ lockFiles.add(lockFile);
2853
+ }
2854
+ }
2855
+ return [...lockFiles].sort();
2856
+ }
2857
+
2858
+ /**
2859
+ * The most bytes read from a settings script when looking for the root
2860
+ * project name.
2861
+ */
2862
+ const GRADLE_SETTINGS_MAX_BYTES = 1024 * 1024;
2863
+
2864
+ /**
2865
+ * Read the root project name a Gradle settings script assigns with
2866
+ * `rootProject.name = '<name>'` or `rootProject.name = "<name>"`, without
2867
+ * running Gradle.
2868
+ *
2869
+ * `settings.gradle` is read before `settings.gradle.kts`, and the last
2870
+ * assignment in the file wins, as it does in Gradle. Only a plain string
2871
+ * literal is accepted: a value built from a variable, a template
2872
+ * (`"${x}"`) or a method call is not something a static read can resolve.
2873
+ *
2874
+ * @param {string} gradleRootPath Root directory of the Gradle build
2875
+ * @returns {string|undefined} The root project name, or undefined when no
2876
+ * settings script assigns one as a plain literal
2877
+ */
2878
+ export function readGradleRootProjectName(gradleRootPath) {
2879
+ for (const settingsName of ["settings.gradle", "settings.gradle.kts"]) {
2880
+ const settingsFile = join(gradleRootPath, settingsName);
2881
+ if (!safeExistsSync(settingsFile)) {
2882
+ continue;
2883
+ }
2884
+ const text = readRegularTextFile(
2885
+ settingsFile,
2886
+ GRADLE_SETTINGS_MAX_BYTES,
2887
+ gradleRootPath,
2888
+ );
2889
+ if (!text) {
2890
+ continue;
2891
+ }
2892
+ let rootName;
2893
+ for (const rawLine of text.split("\n")) {
2894
+ const line = rawLine.trim();
2895
+ if (!line.startsWith("rootProject.name")) {
2896
+ continue;
2897
+ }
2898
+ let rest = line.slice("rootProject.name".length).trim();
2899
+ if (!rest.startsWith("=")) {
2900
+ continue;
2901
+ }
2902
+ rest = rest.slice(1).trim();
2903
+ const commentIndex = rest.indexOf("//");
2904
+ if (commentIndex !== -1) {
2905
+ rest = rest.slice(0, commentIndex).trim();
2906
+ }
2907
+ if (rest.endsWith(";")) {
2908
+ rest = rest.slice(0, -1).trim();
2909
+ }
2910
+ const quote = rest.charAt(0);
2911
+ if (
2912
+ rest.length < 3 ||
2913
+ !["'", '"'].includes(quote) ||
2914
+ !rest.endsWith(quote)
2915
+ ) {
2916
+ continue;
2917
+ }
2918
+ const value = rest.slice(1, -1).trim();
2919
+ if (
2920
+ !value ||
2921
+ [..."'\"$\\"].some((ch) => value.includes(ch)) ||
2922
+ value.includes("/")
2923
+ ) {
2924
+ continue;
2925
+ }
2926
+ rootName = value;
2927
+ }
2928
+ if (rootName) {
2929
+ return rootName;
2930
+ }
2931
+ }
2932
+ return undefined;
2933
+ }
2934
+
2935
+ /**
2936
+ * The largest gradle/verification-metadata.xml read. Real files of large
2937
+ * builds stay around half a megabyte; the file is untrusted input, so a
2938
+ * larger one is skipped whole rather than read in part.
2939
+ */
2940
+ const GRADLE_VERIFICATION_METADATA_MAX_BYTES = 32 * 1024 * 1024;
2941
+
2942
+ /**
2943
+ * The most components read from one verification-metadata.xml. Components
2944
+ * past the cap are ignored.
2945
+ */
2946
+ const GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS = 200000;
2947
+
2948
+ /**
2949
+ * The most artifacts read for one component. A real entry names the jar, the
2950
+ * pom and the module metadata; a coordinate listing many more artifacts was
2951
+ * not written by Gradle.
2952
+ */
2953
+ const GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS = 64;
2954
+
2955
+ /**
2956
+ * Checksum element of the verification schema, the CycloneDX hash algorithm
2957
+ * it maps to, and the exact hex digest length that algorithm produces. A
2958
+ * digest of any other length was not written by Gradle.
2959
+ */
2960
+ const VERIFICATION_HASH_ALGORITHMS = [
2961
+ { element: "sha512", alg: "SHA-512", length: 128 },
2962
+ { element: "sha256", alg: "SHA-256", length: 64 },
2963
+ { element: "sha1", alg: "SHA-1", length: 40 },
2964
+ { element: "md5", alg: "MD5", length: 32 },
2965
+ ];
2966
+
2967
+ const HEX_DIGEST_PATTERN = /^[0-9a-f]+$/iu;
2968
+
2969
+ /**
2970
+ * A `pgp value` is either a 16-character long key id or a 40-character v4
2971
+ * fingerprint. Anything else is not a key id.
2972
+ */
2973
+ const PGP_KEY_ID_LENGTHS = new Set([16, 40]);
2974
+
2975
+ /**
2976
+ * The build stamp a timestamped SNAPSHOT file carries in place of
2977
+ * `-SNAPSHOT`, for example `-20240401.123456-1`.
2978
+ */
2979
+ const SNAPSHOT_STAMP_PATTERN = /^-\d{8}\.\d{6}-\d+/u;
2980
+
2981
+ /**
2982
+ * Children of one element in the compact XML form. A child that occurs once
2983
+ * parses as an object and a repeated child as an array; this accepts both.
2984
+ *
2985
+ * @param {object|object[]|undefined} value Compact-form child or children
2986
+ * @returns {object[]} The children as a list
2987
+ */
2988
+ function verificationElementList(value) {
2989
+ if (value === undefined || value === null) {
2990
+ return [];
2991
+ }
2992
+ return Array.isArray(value) ? value : [value];
2993
+ }
2994
+
2995
+ /**
2996
+ * One named child of an element in the compact XML form.
2997
+ *
2998
+ * A document whose root element carries a namespace prefix keeps that prefix
2999
+ * on the name of every element below it, so the child name is looked up with
3000
+ * the root's prefix in front of it. Attributes are never prefixed: the
3001
+ * schema's attributes are unqualified even when the elements are not.
3002
+ *
3003
+ * @param {object|undefined} element Compact-form element
3004
+ * @param {string} name Child element name without a prefix
3005
+ * @param {string} [prefix] Namespace prefix of the root element, with its
3006
+ * colon
3007
+ * @returns {object[]} The children as a list
3008
+ */
3009
+ function verificationChildList(element, name, prefix = "") {
3010
+ return verificationElementList(element?.[`${prefix}${name}`]);
3011
+ }
3012
+
3013
+ /**
3014
+ * Read the `true` or `false` text of a configuration flag. A flag that is
3015
+ * absent, or whose text is neither, reads as undefined so the caller cannot
3016
+ * tell a written `false` from an unreadable one.
3017
+ *
3018
+ * @param {object|undefined} element Compact-form flag element
3019
+ * @returns {boolean|undefined} The flag value, or undefined
3020
+ */
3021
+ function verificationBoolean(element) {
3022
+ const value = typeof element?._ === "string" ? element._.trim() : "";
3023
+ if (value === "true") {
3024
+ return true;
3025
+ }
3026
+ if (value === "false") {
3027
+ return false;
3028
+ }
3029
+ return undefined;
3030
+ }
3031
+
3032
+ /**
3033
+ * Split an artifact file name into its classifier and extension, accepting
3034
+ * the timestamped file names Gradle writes for SNAPSHOT versions, for which
3035
+ * the build stamp replaces `-SNAPSHOT` in the file name only.
3036
+ *
3037
+ * @param {string} fileName File name from the verification metadata
3038
+ * @param {string} name Artifact ID of the coordinate
3039
+ * @param {string} version Version of the coordinate
3040
+ * @returns {{classifier: string, extension: string}|undefined} Undefined when
3041
+ * the name does not belong to the coordinate
3042
+ */
3043
+ function splitVerificationArtifactName(fileName, name, version) {
3044
+ const split = splitArtifactFileName(fileName, name, version);
3045
+ if (split) {
3046
+ return split;
3047
+ }
3048
+ if (!version.endsWith("-SNAPSHOT")) {
3049
+ return undefined;
3050
+ }
3051
+ const base = version.slice(0, -"-SNAPSHOT".length);
3052
+ if (!fileName.startsWith(`${name}-${base}`)) {
3053
+ return undefined;
3054
+ }
3055
+ const stamp = SNAPSHOT_STAMP_PATTERN.exec(
3056
+ fileName.substring(`${name}-${base}`.length),
3057
+ );
3058
+ if (!stamp) {
3059
+ return undefined;
3060
+ }
3061
+ return splitArtifactFileName(fileName, name, `${base}${stamp[0]}`);
3062
+ }
3063
+
3064
+ /**
3065
+ * Keep a checksum value only when it is a hex digest of the exact length its
3066
+ * algorithm produces. The `origin` and `reason` attributes, and the
3067
+ * `also-trust` alternates, are never read.
3068
+ *
3069
+ * @param {string} alg CycloneDX hash algorithm name
3070
+ * @param {number} length Expected hex digest length
3071
+ * @param {object|undefined} element Compact-form checksum element
3072
+ * @returns {{alg: string, content: string}|undefined} The hash, or undefined
3073
+ */
3074
+ function verificationHash(alg, length, element) {
3075
+ const value = element?.$?.value;
3076
+ if (typeof value !== "string" || value.length !== length) {
3077
+ return undefined;
3078
+ }
3079
+ if (!HEX_DIGEST_PATTERN.test(value)) {
3080
+ return undefined;
3081
+ }
3082
+ return { alg, content: value.toLowerCase() };
3083
+ }
3084
+
3085
+ /**
3086
+ * Keep a `pgp value` only when it is a long key id or a v4 fingerprint, in
3087
+ * the upper-case form the verification schema itself uses.
3088
+ *
3089
+ * @param {object|undefined} element Compact-form pgp element
3090
+ * @returns {string|undefined} The key id, or undefined
3091
+ */
3092
+ function verificationPgpKeyId(element) {
3093
+ const value = element?.$?.value;
3094
+ if (typeof value !== "string") {
3095
+ return undefined;
3096
+ }
3097
+ const keyId = value.trim();
3098
+ if (
3099
+ !PGP_KEY_ID_LENGTHS.has(keyId.length) ||
3100
+ !HEX_DIGEST_PATTERN.test(keyId)
3101
+ ) {
3102
+ return undefined;
3103
+ }
3104
+ return keyId.toUpperCase();
3105
+ }
3106
+
3107
+ /**
3108
+ * Parse a `gradle/verification-metadata.xml` file.
3109
+ *
3110
+ * The file is untrusted input. It records what Gradle verified when
3111
+ * `gradle --write-verification-metadata` last ran, which does not prove that
3112
+ * the artifacts in this BOM match: the file travels with the source, and
3113
+ * nothing here re-checks the digests. Trust rules (`trusted-artifacts`,
3114
+ * `trusted-keys`, `ignored-keys`) may be regular expressions, so they are
3115
+ * only counted, never evaluated, and no free text of the file (an `origin`, a
3116
+ * `reason`, a key-server URI) reaches the result. Only checksum digests,
3117
+ * PGP key ids and the configuration flags survive.
3118
+ *
3119
+ * The read is bounded: the file must be a regular file no larger than
3120
+ * 32 MiB inside `options.rootDir`, at most 200000 components and 64
3121
+ * artifacts per component are read, and a malformed file yields undefined
3122
+ * rather than an error.
3123
+ *
3124
+ * @param {string} metadataFile Path to verification-metadata.xml
3125
+ * @param {Object} [options] Parse options
3126
+ * @param {string} [options.rootDir] Directory the file must resolve inside
3127
+ * @returns {{configuration: {verifyMetadata: (boolean|undefined), verifySignatures: (boolean|undefined), trustedKeyCount: number, trustedArtifactRuleCount: number, ignoredKeyCount: number, keyServerCount: number}, components: Map<string, {group: string, name: string, version: string, artifacts: {fileName: string, classifier: string, extension: string, hashes: {alg: string, content: string}[], pgpKeyIds: string[]}[]}>}|undefined}
3128
+ * The parsed metadata, or undefined when the file could not be read
3129
+ */
3130
+ export function parseGradleVerificationMetadata(metadataFile, options = {}) {
3131
+ const text = readRegularTextFile(
3132
+ metadataFile,
3133
+ GRADLE_VERIFICATION_METADATA_MAX_BYTES,
3134
+ options?.rootDir,
3135
+ );
3136
+ if (text === undefined) {
3137
+ if (safeExistsSync(metadataFile)) {
3138
+ console.warn(
3139
+ `Skipping ${metadataFile}: it is not a regular file inside the project, or is larger than ${GRADLE_VERIFICATION_METADATA_MAX_BYTES} bytes.`,
3140
+ );
3141
+ }
3142
+ return undefined;
3143
+ }
3144
+ const body = text.startsWith("\uFEFF") ? text.slice(1) : text;
3145
+ let parsed;
3146
+ try {
3147
+ parsed = xml2js(body, {
3148
+ compact: true,
3149
+ ignoreComment: true,
3150
+ ignoreDoctype: true,
3151
+ ignoreDeclaration: true,
3152
+ textKey: "_",
3153
+ attributesKey: "$",
3154
+ });
3155
+ } catch (err) {
3156
+ thoughtLog(
3157
+ `${metadataFile} is not well-formed XML, so it was skipped: ${
3158
+ err?.message || err
3159
+ }`,
3160
+ );
3161
+ return undefined;
3162
+ }
3163
+ // Gradle writes the root element in its own namespace, which a default
3164
+ // namespace leaves off element names and a prefix keeps; both are accepted.
3165
+ // A prefixed document keeps the prefix on the name of every element below
3166
+ // the root, so the prefix is carried into every child lookup below.
3167
+ const rootKey = Object.keys(parsed).find(
3168
+ (key) =>
3169
+ key === "verification-metadata" || key.endsWith(":verification-metadata"),
3170
+ );
3171
+ const root = verificationElementList(
3172
+ rootKey ? parsed[rootKey] : undefined,
3173
+ )[0];
3174
+ if (!root) {
3175
+ thoughtLog(
3176
+ `${metadataFile} has no verification-metadata root element, so it was skipped.`,
3177
+ );
3178
+ return undefined;
3179
+ }
3180
+ const prefix = rootKey?.includes(":")
3181
+ ? rootKey.slice(0, rootKey.indexOf(":") + 1)
3182
+ : "";
3183
+ const configurationElement = verificationChildList(
3184
+ root,
3185
+ "configuration",
3186
+ prefix,
3187
+ )[0];
3188
+ const componentsElement = verificationChildList(
3189
+ root,
3190
+ "components",
3191
+ prefix,
3192
+ )[0];
3193
+ const trustedKeysElement = verificationChildList(
3194
+ configurationElement,
3195
+ "trusted-keys",
3196
+ prefix,
3197
+ )[0];
3198
+ const trustedArtifactsElement = verificationChildList(
3199
+ configurationElement,
3200
+ "trusted-artifacts",
3201
+ prefix,
3202
+ )[0];
3203
+ const ignoredKeysElement = verificationChildList(
3204
+ configurationElement,
3205
+ "ignored-keys",
3206
+ prefix,
3207
+ )[0];
3208
+ const keyServersElement = verificationChildList(
3209
+ configurationElement,
3210
+ "key-servers",
3211
+ prefix,
3212
+ )[0];
3213
+ const components = new Map();
3214
+ let skippedArtifacts = 0;
3215
+ const componentList = verificationChildList(
3216
+ componentsElement,
3217
+ "component",
3218
+ prefix,
3219
+ );
3220
+ for (const componentElement of componentList) {
3221
+ if (components.size === GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS) {
3222
+ thoughtLog(
3223
+ `${metadataFile} lists more than ${GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS} components; only the first ${GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS} are read.`,
3224
+ );
3225
+ break;
3226
+ }
3227
+ const attributes = componentElement?.$ || {};
3228
+ const group =
3229
+ typeof attributes.group === "string" ? attributes.group.trim() : "";
3230
+ const name =
3231
+ typeof attributes.name === "string" ? attributes.name.trim() : "";
3232
+ const version =
3233
+ typeof attributes.version === "string" ? attributes.version.trim() : "";
3234
+ // A coordinate without all three parts, or one no valid purl can be
3235
+ // built for, can never match a component of this BOM.
3236
+ if (!group || !name || !version) {
3237
+ continue;
3238
+ }
3239
+ if (!mavenPurl(group, name, version)) {
3240
+ continue;
3241
+ }
3242
+ const artifacts = [];
3243
+ const artifactList = verificationChildList(
3244
+ componentElement,
3245
+ "artifact",
3246
+ prefix,
3247
+ );
3248
+ if (artifactList.length > GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS) {
3249
+ skippedArtifacts +=
3250
+ artifactList.length - GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS;
3251
+ }
3252
+ for (const artifactElement of artifactList.slice(
3253
+ 0,
3254
+ GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS,
3255
+ )) {
3256
+ const fileName =
3257
+ typeof artifactElement?.$?.name === "string"
3258
+ ? artifactElement.$.name.trim()
3259
+ : "";
3260
+ const split = fileName
3261
+ ? splitVerificationArtifactName(fileName, name, version)
3262
+ : undefined;
3263
+ if (!split) {
3264
+ skippedArtifacts++;
3265
+ continue;
3266
+ }
3267
+ const hashes = [];
3268
+ for (const { element, alg, length } of VERIFICATION_HASH_ALGORITHMS) {
3269
+ const hash = verificationHash(
3270
+ alg,
3271
+ length,
3272
+ verificationChildList(artifactElement, element, prefix)[0],
3273
+ );
3274
+ if (hash) {
3275
+ hashes.push(hash);
3276
+ }
3277
+ }
3278
+ const pgpKeyIds = [];
3279
+ for (const pgpElement of verificationChildList(
3280
+ artifactElement,
3281
+ "pgp",
3282
+ prefix,
3283
+ )) {
3284
+ const keyId = verificationPgpKeyId(pgpElement);
3285
+ if (keyId && !pgpKeyIds.includes(keyId)) {
3286
+ pgpKeyIds.push(keyId);
3287
+ }
3288
+ }
3289
+ pgpKeyIds.sort();
3290
+ artifacts.push({
3291
+ fileName,
3292
+ classifier: split.classifier,
3293
+ extension: split.extension,
3294
+ hashes,
3295
+ pgpKeyIds,
3296
+ });
3297
+ }
3298
+ const key = `${group}:${name}:${version}`;
3299
+ if (!components.has(key)) {
3300
+ components.set(key, { group, name, version, artifacts });
3301
+ }
3302
+ }
3303
+ if (skippedArtifacts) {
3304
+ thoughtLog(
3305
+ `Skipped ${skippedArtifacts} artifact element(s) whose file name does not belong to their coordinate in ${metadataFile}.`,
3306
+ );
3307
+ }
3308
+ return {
3309
+ configuration: {
3310
+ verifyMetadata: verificationBoolean(
3311
+ verificationChildList(
3312
+ configurationElement,
3313
+ "verify-metadata",
3314
+ prefix,
3315
+ )[0],
3316
+ ),
3317
+ verifySignatures: verificationBoolean(
3318
+ verificationChildList(
3319
+ configurationElement,
3320
+ "verify-signatures",
3321
+ prefix,
3322
+ )[0],
3323
+ ),
3324
+ trustedKeyCount: verificationChildList(
3325
+ trustedKeysElement,
3326
+ "trusted-key",
3327
+ prefix,
3328
+ ).length,
3329
+ trustedArtifactRuleCount: verificationChildList(
3330
+ trustedArtifactsElement,
3331
+ "trust",
3332
+ prefix,
3333
+ ).length,
3334
+ ignoredKeyCount: verificationChildList(
3335
+ ignoredKeysElement,
3336
+ "ignored-key",
3337
+ prefix,
3338
+ ).length,
3339
+ keyServerCount: verificationChildList(
3340
+ keyServersElement,
3341
+ "key-server",
3342
+ prefix,
3343
+ ).length,
3344
+ },
3345
+ components,
3346
+ };
3347
+ }
3348
+
3349
+ /**
3350
+ * Find the gradle/verification-metadata.xml of one Gradle build.
3351
+ *
3352
+ * Gradle reads one verification file per build, from the root build's
3353
+ * `gradle` directory; included builds' copies are ignored, so nothing is
3354
+ * searched below the root. The file is accepted only when the directory is a
3355
+ * Gradle project of its own, so a file copied into a nested folder is not
3356
+ * read as an inventory source.
3357
+ *
3358
+ * @param {string} gradleRootPath Root directory of the Gradle build
3359
+ * @returns {string|undefined} The metadata file path, or undefined
3360
+ */
3361
+ export function findGradleVerificationMetadata(gradleRootPath) {
3362
+ const metadataFile = join(
3363
+ gradleRootPath,
3364
+ "gradle",
3365
+ "verification-metadata.xml",
3366
+ );
3367
+ if (!safeExistsSync(metadataFile)) {
3368
+ return undefined;
3369
+ }
3370
+ if (
3371
+ !GRADLE_PROJECT_MARKERS.some((marker) =>
3372
+ safeExistsSync(join(gradleRootPath, marker)),
3373
+ )
3374
+ ) {
3375
+ return undefined;
3376
+ }
3377
+ return metadataFile;
3378
+ }
3379
+
3380
+ /**
3381
+ * The artifact of a coordinate whose hashes describe the component itself:
3382
+ * the `.pom` artifact for a platform or BOM component, else the main jar,
3383
+ * else the main aar. Sources, javadoc and other classifiers, and the
3384
+ * `.module` and `.pom` hashes of a jar component, are never used.
3385
+ *
3386
+ * @param {object} component Parsed verification metadata component
3387
+ * @param {boolean} wantsPom True for a component with purl qualifier type=pom
3388
+ * @returns {object|undefined} The artifact, or undefined
3389
+ */
3390
+ export function gradleVerificationMainArtifact(component, wantsPom) {
3391
+ const wanted = wantsPom ? "pom" : "jar";
3392
+ const main = component.artifacts.find(
3393
+ (artifact) => !artifact.classifier && artifact.extension === wanted,
3394
+ );
3395
+ if (main || wantsPom) {
3396
+ return main;
3397
+ }
3398
+ return component.artifacts.find(
3399
+ (artifact) => !artifact.classifier && artifact.extension === "aar",
3400
+ );
3401
+ }
3402
+
3403
+ /**
3404
+ * Merge the checksums and PGP key ids a verification-metadata.xml records
3405
+ * into the components of a package list.
3406
+ *
3407
+ * A digest of the artifact itself, computed from the jar in the local cache
3408
+ * or found by another collector, is the stronger claim, so the recorded
3409
+ * digests only fill the algorithms a component still lacks. Where the file
3410
+ * records a different digest for an algorithm the component already has,
3411
+ * the recorded digest never wins: the component keeps its own, and the
3412
+ * component gains the cdx:gradle:verificationMismatch=true property so the
3413
+ * disagreement is visible. A released Maven version does not change after
3414
+ * publishing, so a stale file cannot explain a mismatch: a stale file names
3415
+ * old versions, which match nothing. The likely causes are an artifact
3416
+ * republished on a mirror or internal repository, or a corrupted or
3417
+ * tampered cache. SNAPSHOT versions and digests that are not hex are not
3418
+ * compared.
3419
+ *
3420
+ * Every matched component also gains two properties naming the verification
3421
+ * methods and the PGP key ids of its artifact. The evidence of the
3422
+ * component is left alone: a component found by a Gradle run or a lockfile
3423
+ * keeps the identity evidence of that source, and only the components the
3424
+ * verification-metadata fallback built carry the file as their evidence.
3425
+ *
3426
+ * @param {Object[]} pkgList Components to enrich in place
3427
+ * @param {Object} metadata Parsed metadata, as parseGradleVerificationMetadata
3428
+ * returns
3429
+ * @param {string} metadataFile Path to the verification-metadata.xml, named
3430
+ * in the debug reasoning
3431
+ * @returns {number} The number of components this call changed
3432
+ */
3433
+ export function applyGradleVerificationMetadata(
3434
+ pkgList,
3435
+ metadata,
3436
+ metadataFile,
3437
+ ) {
3438
+ if (!metadata?.components?.size) {
3439
+ return 0;
3440
+ }
3441
+ let changedCount = 0;
3442
+ for (const pkg of pkgList || []) {
3443
+ if (!pkg?.purl?.startsWith("pkg:maven/")) {
3444
+ continue;
3445
+ }
3446
+ const component = metadata.components.get(
3447
+ `${pkg.group}:${pkg.name}:${pkg.version}`,
3448
+ );
3449
+ if (!component) {
3450
+ continue;
3451
+ }
3452
+ const artifact = gradleVerificationMainArtifact(
3453
+ component,
3454
+ pkg.qualifiers?.type === "pom",
3455
+ );
3456
+ if (!artifact) {
3457
+ continue;
3458
+ }
3459
+ const hashesBefore = pkg.hashes;
3460
+ const propertiesBefore = pkg.properties;
3461
+ // A SNAPSHOT version names a moving artifact: the file can record
3462
+ // several timestamped builds of it while the cache holds the newest, so
3463
+ // a differing digest there says nothing about tampering.
3464
+ const comparable = !String(pkg.version || "").endsWith("-SNAPSHOT");
3465
+ // The merged list keeps the order the component's own hashes came in
3466
+ // and appends the recorded ones in the schema's algorithm order.
3467
+ const hashes = Array.isArray(pkg.hashes) ? [...pkg.hashes] : [];
3468
+ let mismatch = false;
3469
+ for (const recorded of artifact.hashes) {
3470
+ const existing = hashes.find((hash) => hash.alg === recorded.alg);
3471
+ if (existing) {
3472
+ // Other collectors may carry an upper-case or base64 digest; only a
3473
+ // hex digest can be compared with the recorded one.
3474
+ const existingContent =
3475
+ typeof existing.content === "string" ? existing.content : "";
3476
+ if (
3477
+ comparable &&
3478
+ HEX_DIGEST_PATTERN.test(existingContent) &&
3479
+ existingContent.toLowerCase() !== recorded.content
3480
+ ) {
3481
+ mismatch = true;
3482
+ thoughtLog(
3483
+ `${metadataFile} records a different ${recorded.alg} digest for ${pkg.group}:${pkg.name}:${pkg.version} than the artifact carries; the recorded digest is flagged, not applied.`,
3484
+ );
3485
+ }
3486
+ continue;
3487
+ }
3488
+ hashes.push({ alg: recorded.alg, content: recorded.content });
3489
+ }
3490
+ if (hashes.length && hashes.length !== (pkg.hashes?.length || 0)) {
3491
+ pkg.hashes = hashes;
3492
+ }
3493
+ const methods = new Set();
3494
+ for (const { element, alg } of VERIFICATION_HASH_ALGORITHMS) {
3495
+ if (artifact.hashes.some((hash) => hash.alg === alg)) {
3496
+ methods.add(element);
3497
+ }
3498
+ }
3499
+ if (artifact.pgpKeyIds.length) {
3500
+ methods.add("pgp");
3501
+ }
3502
+ const properties = pkg.properties || [];
3503
+ if (
3504
+ methods.size &&
3505
+ !properties.some((p) => p.name === "cdx:gradle:verificationMethods")
3506
+ ) {
3507
+ properties.push({
3508
+ name: "cdx:gradle:verificationMethods",
3509
+ value: [...methods].sort().join(","),
3510
+ });
3511
+ }
3512
+ if (
3513
+ artifact.pgpKeyIds.length &&
3514
+ !properties.some((p) => p.name === "cdx:gradle:pgpKeyIds")
3515
+ ) {
3516
+ properties.push({
3517
+ name: "cdx:gradle:pgpKeyIds",
3518
+ value: [...new Set(artifact.pgpKeyIds)].sort().slice(0, 10).join(","),
3519
+ });
3520
+ }
3521
+ if (
3522
+ mismatch &&
3523
+ !properties.some((p) => p.name === "cdx:gradle:verificationMismatch")
3524
+ ) {
3525
+ properties.push({
3526
+ name: "cdx:gradle:verificationMismatch",
3527
+ value: "true",
3528
+ });
3529
+ }
3530
+ if (properties.length !== (pkg.properties?.length || 0)) {
3531
+ pkg.properties = properties;
3532
+ }
3533
+ if (pkg.hashes !== hashesBefore || pkg.properties !== propertiesBefore) {
3534
+ changedCount++;
3535
+ }
3536
+ }
3537
+ return changedCount;
3538
+ }