@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,5 +1,13 @@
1
- import { lstatSync, readFileSync, statSync } from "node:fs";
2
- import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
1
+ import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import {
3
+ basename,
4
+ dirname,
5
+ isAbsolute,
6
+ join,
7
+ relative,
8
+ resolve,
9
+ sep,
10
+ } from "node:path";
3
11
  import process from "node:process";
4
12
 
5
13
  import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
@@ -8,12 +16,12 @@ import { PROJECT_TYPE_ALIASES } from "../core/env.js";
8
16
  import {
9
17
  getAllFiles,
10
18
  getTmpDir,
11
- safeCopyFileSync,
12
19
  safeExistsSync,
13
20
  safeMkdirSync,
14
21
  safeMkdtempSync,
15
22
  safeRmSync,
16
23
  safeSpawnSync,
24
+ safeWriteSync,
17
25
  } from "../core/fs.js";
18
26
  import { dirNameStr } from "../core/paths.js";
19
27
  import { resolvePluginSourceDir } from "../core/pluginRun.js";
@@ -22,6 +30,7 @@ import { isProjectToolingAllowed } from "../core/projectTooling.js";
22
30
  import { parsetoolsFile } from "../inventory/atomUtils.js";
23
31
  import { applyAlgorithmProperties } from "../inventory/cryptoAlgorithmFamily.js";
24
32
  import { getJarClasses } from "../inventory/deps.js";
33
+ import { occurrenceLocationKey } from "../inventory/occurrenceKey.js";
25
34
  import {
26
35
  parseScalaArtifact,
27
36
  scalaCoordinateKey,
@@ -147,22 +156,25 @@ function scalasemTimeoutMs(options = {}) {
147
156
  const TIMEOUT_GRACE_MS = 30_000;
148
157
 
149
158
  // The build files that identify a Scala project, for a source argument that
150
- // names the working directory relative to itself.
159
+ // names the working directory relative to itself, and that a persisted report
160
+ // must be newer than. sbt 0.13 builds may define everything under project/.
151
161
  const SCALA_BUILD_MARKERS = [
152
162
  "build.sbt",
153
163
  "build.sc",
154
164
  "build.mill",
155
165
  "pom.xml",
156
166
  "project.scala",
167
+ join("project", "build.properties"),
157
168
  ];
158
169
 
159
170
  /**
160
171
  * Where the scalasem report is persisted. An absolute --semantics-slices-file
161
172
  * keeps its directory; anything else lands in the evinse output directory, the
162
- * same rule the atom slice writer follows.
173
+ * same rule the atom slice writer follows. Without either there is nowhere to
174
+ * keep it, and the report lives only as long as the run.
163
175
  *
164
176
  * @param {Object} options CLI options carrying `output` and `semanticsSlicesFile`.
165
- * @returns {string} Absolute report path.
177
+ * @returns {string|undefined} Absolute report path.
166
178
  */
167
179
  export function scalasemOutputFile(options = {}) {
168
180
  const requested = options.semanticsSlicesFile || "semantics.slices.json";
@@ -171,21 +183,18 @@ export function scalasemOutputFile(options = {}) {
171
183
  if (isAbsolute(requested)) {
172
184
  return requested;
173
185
  }
174
- let outputDir;
175
- if (options.output) {
176
- const resolvedOutput = resolve(options.output);
177
- let isDir = false;
178
- try {
179
- isDir =
180
- safeExistsSync(resolvedOutput) &&
181
- lstatSync(resolvedOutput).isDirectory();
182
- } catch (_err) {
183
- isDir = false;
184
- }
185
- outputDir = isDir ? resolvedOutput : dirname(resolvedOutput);
186
- } else {
187
- outputDir = safeMkdtempSync(join(getTmpDir(), "scalasem-"));
186
+ if (!options.output) {
187
+ return undefined;
188
+ }
189
+ const resolvedOutput = resolve(options.output);
190
+ let isDir = false;
191
+ try {
192
+ isDir =
193
+ safeExistsSync(resolvedOutput) && lstatSync(resolvedOutput).isDirectory();
194
+ } catch (_err) {
195
+ isDir = false;
188
196
  }
197
+ const outputDir = isDir ? resolvedOutput : dirname(resolvedOutput);
189
198
  return join(outputDir, basename(requested));
190
199
  }
191
200
 
@@ -224,6 +233,18 @@ export function isV1SemanticsSlice(parsed) {
224
233
  return Object.keys(parsed).some((key) => key.endsWith(".scala"));
225
234
  }
226
235
 
236
+ // Whether a path lies below a directory. A prefix test fails for a root
237
+ // directory (`/`, `C:\\`), whose path already ends in the separator.
238
+ function isInside(directory, path) {
239
+ const rel = relative(directory, path);
240
+ return (
241
+ Boolean(rel) &&
242
+ rel !== ".." &&
243
+ !rel.startsWith(`..${sep}`) &&
244
+ !isAbsolute(rel)
245
+ );
246
+ }
247
+
227
248
  /**
228
249
  * Whether a version 1 slice describes this project: it names no project, so
229
250
  * every source file it lists must exist under the project directory.
@@ -234,15 +255,104 @@ function isV1SliceOfProject(slice, projectDir) {
234
255
  files.length > 0 &&
235
256
  files.every((file) => {
236
257
  const path = resolve(projectDir, file);
237
- return path.startsWith(`${projectDir}${sep}`) && safeExistsSync(path);
258
+ return isInside(projectDir, path) && safeExistsSync(path);
238
259
  })
239
260
  );
240
261
  }
241
262
 
263
+ // The run settings a report depends on. cdxgen records them in the report it
264
+ // keeps, so a later run reuses that report only when it would ask for the
265
+ // same: tests included or not, the build run or not.
266
+ const RUN_SETTINGS_KEY = "cdxgenRunSettings";
267
+
268
+ function scalasemRunSettings(options) {
269
+ return {
270
+ build: isProjectToolingAllowed(options),
271
+ includeTests: Boolean(
272
+ options.scalasemIncludeTests && !options.requiredOnly,
273
+ ),
274
+ };
275
+ }
276
+
277
+ function canonicalPath(path) {
278
+ try {
279
+ return realpathSync(path);
280
+ } catch (_err) {
281
+ return resolve(path);
282
+ }
283
+ }
284
+
285
+ // Whether two paths name one directory, through symbolic links such as
286
+ // /var and /private/var, and in any case on Windows.
287
+ function isSamePath(left, right) {
288
+ const a = canonicalPath(left);
289
+ const b = canonicalPath(right);
290
+ return process.platform === "win32"
291
+ ? a.toLowerCase() === b.toLowerCase()
292
+ : a === b;
293
+ }
294
+
295
+ function modifiedAt(file) {
296
+ try {
297
+ return statSync(file).mtimeMs;
298
+ } catch (_err) {
299
+ return undefined;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Why a persisted report cannot stand in for a new run: it describes another
305
+ * project, it predates the input BOM, cdxgen kept it from a run with other
306
+ * settings, or a build file or a source it describes changed or went away
307
+ * since it was written.
308
+ *
309
+ * @returns {string|undefined} The reason, or undefined when it can be reused.
310
+ */
311
+ function reportReuseProblem(report, reportFile, analysisDir, options) {
312
+ const projectPath = report._meta?.projectPath;
313
+ if (!projectPath || !isSamePath(projectPath, analysisDir)) {
314
+ return "it describes another project";
315
+ }
316
+ if (!isNewerThan(reportFile, options.input)) {
317
+ return "it is older than the input BOM";
318
+ }
319
+ const recorded = report._meta[RUN_SETTINGS_KEY];
320
+ const wanted = scalasemRunSettings(options);
321
+ if (
322
+ recorded &&
323
+ (recorded.build !== wanted.build ||
324
+ recorded.includeTests !== wanted.includeTests)
325
+ ) {
326
+ return "it was made with other settings";
327
+ }
328
+ const writtenAt = modifiedAt(reportFile);
329
+ if (writtenAt === undefined) {
330
+ return "its modification time is unknown";
331
+ }
332
+ for (const marker of SCALA_BUILD_MARKERS) {
333
+ const changedAt = modifiedAt(join(analysisDir, marker));
334
+ if (changedAt !== undefined && changedAt > writtenAt) {
335
+ return `${marker} changed since`;
336
+ }
337
+ }
338
+ for (const key of Object.keys(report)) {
339
+ const file = isReportFileKey(key) ? resolve(analysisDir, key) : undefined;
340
+ if (!file || !isInside(analysisDir, file)) {
341
+ continue;
342
+ }
343
+ const changedAt = modifiedAt(file);
344
+ if (changedAt === undefined || changedAt > writtenAt) {
345
+ return `${key} changed since`;
346
+ }
347
+ }
348
+ return undefined;
349
+ }
350
+
242
351
  /**
243
352
  * Run scalasem over a Scala project and return its report, reusing a
244
- * persisted version 2 report that belongs to this project and is newer than
245
- * the input BOM.
353
+ * persisted version 2 report that belongs to this project, is newer than the
354
+ * input BOM and than the build files and sources it describes, and, when
355
+ * cdxgen kept it, was made with the settings this run asks for.
246
356
  *
247
357
  * Failures are visible rather than silent: the reason is printed once, the
248
358
  * run's diagnostics reach the caller as `cdx:scalasem:diagnostic` properties,
@@ -280,10 +390,18 @@ export function analyzeScalaProject(src, options = {}) {
280
390
  for (const candidate of candidates) {
281
391
  const existing = readScalasemReport(candidate);
282
392
  if (existing?._meta?.schemaVersion === "scalasem/2") {
283
- if (
284
- resolve(existing._meta.projectPath || "") === analysisDir &&
285
- isNewerThan(candidate, options.input)
286
- ) {
393
+ const problem = reportReuseProblem(
394
+ existing,
395
+ candidate,
396
+ analysisDir,
397
+ options,
398
+ );
399
+ if (problem && DEBUG_MODE) {
400
+ console.log(
401
+ `Not reusing the scalasem report "${candidate}": ${problem}.`,
402
+ );
403
+ }
404
+ if (!problem) {
287
405
  if (DEBUG_MODE) {
288
406
  console.log(`Reusing the scalasem report "${candidate}".`);
289
407
  }
@@ -340,13 +458,38 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
340
458
  // earlier run (or another project's run) had left there, and a report
341
459
  // written within the millisecond the run started passed for fresh.
342
460
  const runDir = safeMkdtempSync(join(getTmpDir(), "scalasem-"));
461
+ try {
462
+ return runScalasemIn(
463
+ runDir,
464
+ scalasem,
465
+ analysisDir,
466
+ options,
467
+ requestedFile,
468
+ v1Slice,
469
+ v1File,
470
+ );
471
+ } finally {
472
+ safeRmSync(runDir, { recursive: true, force: true });
473
+ }
474
+ }
475
+
476
+ function runScalasemIn(
477
+ runDir,
478
+ scalasem,
479
+ analysisDir,
480
+ options,
481
+ requestedFile,
482
+ v1Slice,
483
+ v1File,
484
+ ) {
343
485
  const runReportFile = join(runDir, "scalasem.slices.json");
344
486
  const args = [scalasem, analysisDir, runReportFile];
487
+ const settings = scalasemRunSettings(options);
345
488
  // The build runs the project's sbt definitions
346
- if (!isProjectToolingAllowed(options)) {
489
+ if (!settings.build) {
347
490
  args.push("--no-build");
348
491
  }
349
- if (options.scalasemIncludeTests && !options.requiredOnly) {
492
+ if (settings.includeTests) {
350
493
  args.push("--include-tests");
351
494
  }
352
495
  if (DEBUG_MODE) {
@@ -357,17 +500,24 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
357
500
  // scalasem enforces the limit itself, so the builds it started stop with
358
501
  // it; cdxgen's own timeout is the backstop. Naming cdxgen as the supervisor
359
502
  // stops scalasem when cdxgen is killed.
360
- const result = safeSpawnSync(process.execPath, args, {
361
- cwd: analysisDir,
362
- shell: false,
363
- killSignal: "SIGKILL",
364
- timeout: timeoutMs + Math.min(TIMEOUT_GRACE_MS, timeoutMs),
365
- env: {
366
- ...process.env,
367
- ATOM_PARENT_PID: String(process.pid),
368
- SCALASEM_TIMEOUT: String(timeoutMs),
369
- },
370
- });
503
+ // A spawn that throws, such as a command the allow list refuses, degrades
504
+ // the evidence like any other run that could not start.
505
+ let result;
506
+ try {
507
+ result = safeSpawnSync(process.execPath, args, {
508
+ cwd: analysisDir,
509
+ shell: false,
510
+ killSignal: "SIGKILL",
511
+ timeout: timeoutMs + Math.min(TIMEOUT_GRACE_MS, timeoutMs),
512
+ env: {
513
+ ...process.env,
514
+ ATOM_PARENT_PID: String(process.pid),
515
+ SCALASEM_TIMEOUT: String(timeoutMs),
516
+ },
517
+ });
518
+ } catch (err) {
519
+ result = { error: err };
520
+ }
371
521
  const elapsedMs = Date.now() - startedAt;
372
522
  const wroteReport = safeExistsSync(runReportFile);
373
523
  const written = wroteReport ? readScalasemReport(runReportFile) : undefined;
@@ -431,7 +581,9 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
431
581
  ),
432
582
  ];
433
583
  }
434
- const summary = summarizeDiagnostics(report._meta.diagnostics);
584
+ const summary = summarizeDiagnostics(
585
+ degradingDiagnostics(report._meta.diagnostics),
586
+ );
435
587
  if (summary) {
436
588
  console.warn(
437
589
  `scalasem: the report is degraded: ${summary}. The cdx:scalasem:diagnostic:* metadata properties of the BOM say where.`,
@@ -443,19 +595,26 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
443
595
  }
444
596
  // Only a usable report replaces the one at the requested path, and a
445
597
  // version 1 slice there stays as the user left it.
598
+ // The kept copy records the settings of this run, so a later run reuses it
599
+ // only when it asks for the same.
446
600
  let reportFile;
447
- if (usable && v1File !== requestedFile) {
601
+ if (usable && requestedFile && v1File !== requestedFile) {
448
602
  try {
449
603
  safeMkdirSync(dirname(requestedFile), { recursive: true });
450
- safeCopyFileSync(runReportFile, requestedFile);
451
- reportFile = requestedFile;
604
+ safeWriteSync(
605
+ requestedFile,
606
+ JSON.stringify({
607
+ ...report,
608
+ _meta: { ...report._meta, [RUN_SETTINGS_KEY]: settings },
609
+ }),
610
+ );
611
+ reportFile = safeExistsSync(requestedFile) ? requestedFile : undefined;
452
612
  } catch (err) {
453
613
  console.warn(
454
614
  `scalasem: the report could not be kept at ${requestedFile} (${err.message}); this run still uses it.`,
455
615
  );
456
616
  }
457
617
  }
458
- safeRmSync(runDir, { recursive: true, force: true });
459
618
  return {
460
619
  report,
461
620
  reportFile,
@@ -464,6 +623,21 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
464
623
  };
465
624
  }
466
625
 
626
+ // Diagnostics scalasem counts on a healthy run: symbols its semantic pass
627
+ // could not resolve, and stale build outputs it correctly skipped. They say
628
+ // the analysis is incomplete, not that it is degraded, so they stay out of
629
+ // the BOM and out of the degraded flag.
630
+ const INFORMATIONAL_DIAGNOSTIC_CODES = new Set([
631
+ "unresolved-symbols",
632
+ "stale-output",
633
+ ]);
634
+
635
+ function degradingDiagnostics(diagnostics = []) {
636
+ return (diagnostics || []).filter(
637
+ (diagnostic) => !INFORMATIONAL_DIAGNOSTIC_CODES.has(diagnostic?.code),
638
+ );
639
+ }
640
+
467
641
  function summarizeDiagnostics(diagnostics = []) {
468
642
  const byCode = new Map();
469
643
  for (const diagnostic of diagnostics || []) {
@@ -490,8 +664,9 @@ function summarizeDiagnostics(diagnostics = []) {
490
664
  */
491
665
  export function scalasemMetadataProperties(
492
666
  report,
493
- diagnostics = report?._meta?.diagnostics || [],
667
+ allDiagnostics = report?._meta?.diagnostics || [],
494
668
  ) {
669
+ const diagnostics = degradingDiagnostics(allDiagnostics);
495
670
  const properties = [];
496
671
  const meta = report?._meta || {};
497
672
  appendUniqueProperty(
@@ -553,20 +728,23 @@ export function sortedCsv(values) {
553
728
 
554
729
  /**
555
730
  * The platform a purl's artifact name targets: `_sjs1` is Scala.js,
556
- * `_native0.x` is Scala Native, and neither is the JVM.
731
+ * `_native0.x` is Scala Native, and neither is the JVM. The platform suffix
732
+ * counts whether the Scala binary suffix follows it (`upickle_sjs1_3`, the
733
+ * published name a jar scan keeps) or not (`upickle_sjs1`, the sbt name).
557
734
  *
558
735
  * @param {string} purl Package URL.
559
736
  * @returns {"jvm"|"js"|"native"} Platform key.
560
737
  */
561
738
  export function scalaPurlPlatform(purl) {
562
- const name = purlPurlName(purl);
563
- if (!name) {
564
- return "jvm";
565
- }
566
- if (/_(sjs\d+(?:\.\d+)?)$/.test(name)) {
739
+ return scalaNamePlatform(purlPurlName(purl));
740
+ }
741
+
742
+ function scalaNamePlatform(name) {
743
+ const { platformSuffix } = parseScalaArtifact(name);
744
+ if (platformSuffix.startsWith("_sjs")) {
567
745
  return "js";
568
746
  }
569
- if (/_(native\d+(?:\.\d+)*)$/.test(name)) {
747
+ if (platformSuffix.startsWith("_native")) {
570
748
  return "native";
571
749
  }
572
750
  return "jvm";
@@ -584,6 +762,20 @@ function purlPurlName(purl) {
584
762
  }
585
763
  }
586
764
 
765
+ // A compiler-generated class: a `$`-separated part that is empty past the
766
+ // top-level name (`$$`) or numeric, or one of the anonymous and lambda markers.
767
+ function isSyntheticClassName(symbol, dollar) {
768
+ return symbol
769
+ .slice(dollar + 1)
770
+ .split("$")
771
+ .some(
772
+ (part, index, parts) =>
773
+ (part === "" && index < parts.length - 1) ||
774
+ /^\d+$/.test(part) ||
775
+ ["anon", "anonfun", "Lambda", "lambda"].includes(part),
776
+ );
777
+ }
778
+
587
779
  /**
588
780
  * The class and package index that joins scalasem symbols to component purls.
589
781
  *
@@ -633,11 +825,22 @@ export class ScalaJoinIndex {
633
825
  if (!symbol || symbol.includes(" ") || symbol.includes("/")) {
634
826
  continue;
635
827
  }
636
- indexed.push(symbol);
637
- const dollar = symbol.indexOf("$");
828
+ // A `$` inside the simple name separates nested classes; one that starts
829
+ // it belongs to an encoded operator name (`$colon$colon` for `::`).
830
+ const simpleName = symbol.lastIndexOf(".") + 1;
831
+ const dollar = symbol.startsWith("$", simpleName)
832
+ ? -1
833
+ : symbol.indexOf("$", simpleName);
638
834
  const topLevel = dollar > 0 ? symbol.slice(0, dollar) : symbol;
835
+ // Anonymous classes and lambdas (`Foo$$anon$1`, `Foo$$anonfun$bar$1`,
836
+ // `Foo$$Lambda$12`) are no names a source can reference; their outer
837
+ // class speaks for them.
838
+ const synthetic = dollar > 0 && isSyntheticClassName(symbol, dollar);
839
+ if (!synthetic) {
840
+ indexed.push(symbol);
841
+ }
639
842
  this.addSymbol(this.classPurls, topLevel, purl, source);
640
- if (dollar > 0) {
843
+ if (dollar > 0 && !synthetic) {
641
844
  const nested = symbol.replaceAll("$", ".").replace(/\.+$/, "");
642
845
  if (nested !== topLevel) {
643
846
  this.addSymbol(this.classPurls, nested, purl, source);
@@ -734,15 +937,25 @@ export class ScalaJoinIndex {
734
937
  * sits in. A package the project also fills is ambiguous and joins nothing,
735
938
  * and `javax.` names join only through an exact class, since the JDK ships
736
939
  * most of them.
940
+ *
941
+ * The package is the part of the symbol before its first type name, which
942
+ * starts with a capital letter (`io.circe` of `io.circe.Json.fromString`). A
943
+ * symbol without one names a package (`io.circe`) or a member of one, such
944
+ * as a package object member (`io.circe.syntax.asJson`). Parent packages
945
+ * never answer: a missing `io.circe.generic` is not circe-core.
737
946
  */
738
947
  packageOwners(symbol) {
739
948
  if (symbol.startsWith(JDK_EXTENSION_PREFIX)) {
740
949
  return undefined;
741
950
  }
742
951
  const parts = symbol.replace(/\$+$/, "").split(".");
743
- for (let i = parts.length; i > 0; i--) {
744
- const name = parts.slice(0, i).join(".");
745
- const owners = this.packagePurls.get(name);
952
+ const firstType = parts.findIndex((part) => /^[A-Z]/.test(part));
953
+ const packages =
954
+ firstType >= 0
955
+ ? [parts.slice(0, firstType).join(".")]
956
+ : [parts.join("."), parts.slice(0, -1).join(".")];
957
+ for (const name of packages) {
958
+ const owners = name ? this.packagePurls.get(name) : undefined;
746
959
  if (owners?.size) {
747
960
  return this.projectPackages.has(name) ? undefined : owners;
748
961
  }
@@ -875,41 +1088,83 @@ function componentBinaryVersions(component, coordinate) {
875
1088
  );
876
1089
  }
877
1090
 
1091
+ // Which of two components standing for the same library is the one evidence
1092
+ // attaches to: the plain purl over one with qualifiers, then the shorter, then
1093
+ // the lexicographically first, so the choice never depends on BOM order.
1094
+ function preferredComponent(current, candidate) {
1095
+ const rank = (component) => [
1096
+ component.purl.includes("?") ? 1 : 0,
1097
+ component.purl.length,
1098
+ ];
1099
+ const [currentQualified, currentLength] = rank(current);
1100
+ const [candidateQualified, candidateLength] = rank(candidate);
1101
+ if (currentQualified !== candidateQualified) {
1102
+ return candidateQualified < currentQualified ? candidate : current;
1103
+ }
1104
+ if (currentLength !== candidateLength) {
1105
+ return candidateLength < currentLength ? candidate : current;
1106
+ }
1107
+ return candidate.purl < current.purl ? candidate : current;
1108
+ }
1109
+
878
1110
  /**
879
1111
  * Index the BOM components by their normalized Scala coordinates, both with
880
1112
  * and without the version, so a classpath jar, a namespace property and a map
881
1113
  * entry of the same library meet on one component. Each entry keeps the Scala
882
- * binary version the component was built for, so the `_2.13` and `_3`
883
- * artifacts of one library in a Maven build stay apart.
1114
+ * binary versions the component was built for, so the `_2.13` and `_3`
1115
+ * artifacts of one library in a Maven build stay apart. Components that share
1116
+ * a normalized coordinate and were built for an overlapping set of Scala
1117
+ * binary versions are one library listed more than once (a repository_url
1118
+ * qualifier, a `_3` suffix beside the sbt name built for 2.13 and 3, a stale
1119
+ * duplicate), so they share one entry, which the preferred component of the
1120
+ * group stands for.
884
1121
  */
885
1122
  export function componentCoordinateIndex(components = []) {
886
- const byVersion = new Map();
887
- const withoutVersion = new Map();
1123
+ const groupsByKey = new Map();
888
1124
  for (const component of components) {
889
1125
  const coordinate = scalaCoordinateOfPurl(component?.purl);
890
1126
  if (!coordinate) {
891
1127
  continue;
892
1128
  }
893
- const [type, group, artifactBase, platformSuffix] =
894
- coordinate.key.split(":");
895
- const entry = {
1129
+ const binaryVersions = componentBinaryVersions(component, coordinate);
1130
+ const groups = groupsByKey.get(coordinate.key) || [];
1131
+ groupsByKey.set(coordinate.key, groups);
1132
+ // Every group this component overlaps becomes one with it.
1133
+ const overlapping = groups.filter((group) =>
1134
+ binaryVersions.size
1135
+ ? [...binaryVersions].some((version) =>
1136
+ group.binaryVersions.has(version),
1137
+ )
1138
+ : !group.binaryVersions.size,
1139
+ );
1140
+ const merged = {
896
1141
  component,
897
- binaryVersions: componentBinaryVersions(component, coordinate),
1142
+ binaryVersions: new Set(binaryVersions),
898
1143
  };
899
- if (!byVersion.has(coordinate.key)) {
900
- byVersion.set(coordinate.key, []);
1144
+ for (const group of overlapping) {
1145
+ merged.component = preferredComponent(merged.component, group.component);
1146
+ for (const version of group.binaryVersions) {
1147
+ merged.binaryVersions.add(version);
1148
+ }
1149
+ groups.splice(groups.indexOf(group), 1);
901
1150
  }
902
- byVersion.get(coordinate.key).push(entry);
903
- const key = scalaCoordinateKey({
1151
+ groups.push(merged);
1152
+ }
1153
+ const byVersion = new Map();
1154
+ const withoutVersion = new Map();
1155
+ for (const [key, groups] of groupsByKey) {
1156
+ const [type, group, artifactBase, platformSuffix] = key.split(":");
1157
+ byVersion.set(key, groups);
1158
+ const versionless = scalaCoordinateKey({
904
1159
  type,
905
1160
  group,
906
1161
  artifactBase,
907
1162
  platformSuffix,
908
1163
  });
909
- if (!withoutVersion.has(key)) {
910
- withoutVersion.set(key, []);
911
- }
912
- withoutVersion.get(key).push(entry);
1164
+ withoutVersion.set(versionless, [
1165
+ ...(withoutVersion.get(versionless) || []),
1166
+ ...groups,
1167
+ ]);
913
1168
  }
914
1169
  return { byVersion, withoutVersion };
915
1170
  }
@@ -944,11 +1199,126 @@ function pickByBinaryVersion(entries, binaryVersion) {
944
1199
  * @returns {{component: Object, exact: boolean}|undefined} The component, and
945
1200
  * whether its version is the one on the classpath.
946
1201
  */
947
- export function matchClasspathEntry(
948
- entry,
949
- { byVersion, withoutVersion },
950
- platform,
951
- ) {
1202
+ export function matchClasspathEntry(entry, coordinateIndex, platform) {
1203
+ const direct =
1204
+ entry?.group && entry?.artifact
1205
+ ? matchCoordinates(entry, coordinateIndex, platform)
1206
+ : undefined;
1207
+ if (direct?.component || direct?.ambiguous) {
1208
+ return direct.component ? direct : undefined;
1209
+ }
1210
+ // The report reads coordinates from a Maven repository layout and can take
1211
+ // directories above the group for part of it (`maven.io.circe` for jars
1212
+ // under `<repository>/maven/io/circe/`), and reads none from the Gradle and
1213
+ // Ivy cache layouts. The coordinates the jar's own path spells are tried
1214
+ // then, and must still meet a component on artifact and version.
1215
+ for (const coordinates of jarPathCoordinates(entry)) {
1216
+ const byPath = matchCoordinates(
1217
+ { ...entry, ...coordinates },
1218
+ coordinateIndex,
1219
+ platform,
1220
+ );
1221
+ if (byPath?.exact) {
1222
+ return byPath;
1223
+ }
1224
+ }
1225
+ return undefined;
1226
+ }
1227
+
1228
+ /**
1229
+ * The coordinates a classpath jar's path spells, for the layouts the report
1230
+ * does not read itself, most specific first:
1231
+ *
1232
+ * - a Maven layout `<group dirs>/<artifact>/<version>/<file>` whose group is
1233
+ * a dotted tail of the group the report recorded, so a group is only ever
1234
+ * shortened by the directories the report took for it, never guessed
1235
+ * - the Gradle cache `files-2.1/<group>/<artifact>/<version>/<sha1>/<file>`
1236
+ * - an Ivy cache `<org>/<artifact>/jars/<artifact>-<version>.jar`, as sbt 0.13
1237
+ * and sbt 1.0 to 1.2 keep it, and an Ivy local repository
1238
+ * `<org>/<artifact>/<version>/jars/<artifact>.jar`
1239
+ *
1240
+ * @param {Object} entry Classpath entry with `path` and, when the report read
1241
+ * them, `group`, `artifact` and `version`.
1242
+ * @returns {{group: string, artifact: string, version: string}[]}
1243
+ */
1244
+ export function jarPathCoordinates(entry) {
1245
+ if (!entry?.path) {
1246
+ return [];
1247
+ }
1248
+ const segments = String(entry.path).split(/[\\/]/).filter(Boolean);
1249
+ const total = segments.length;
1250
+ const file = segments[total - 1] || "";
1251
+ if (total < 4 || !file.toLowerCase().endsWith(".jar")) {
1252
+ return [];
1253
+ }
1254
+ const found = [];
1255
+ if (entry.group && entry.artifact && entry.version) {
1256
+ const artifactIndex = total - 3;
1257
+ if (
1258
+ segments[artifactIndex] === entry.artifact &&
1259
+ segments[artifactIndex + 1] === entry.version
1260
+ ) {
1261
+ const groupParts = entry.group.split(".");
1262
+ for (let start = 1; start < groupParts.length; start++) {
1263
+ const tail = groupParts.slice(start);
1264
+ const directories = segments.slice(
1265
+ artifactIndex - tail.length,
1266
+ artifactIndex,
1267
+ );
1268
+ if (
1269
+ artifactIndex - tail.length >= 0 &&
1270
+ directories.join("/") === tail.join("/")
1271
+ ) {
1272
+ found.push({
1273
+ group: tail.join("."),
1274
+ artifact: entry.artifact,
1275
+ version: entry.version,
1276
+ });
1277
+ }
1278
+ }
1279
+ }
1280
+ }
1281
+ const gradle = segments.lastIndexOf("files-2.1");
1282
+ if (gradle >= 0 && gradle + 5 === total - 1) {
1283
+ found.push({
1284
+ group: segments[gradle + 1],
1285
+ artifact: segments[gradle + 2],
1286
+ version: segments[gradle + 3],
1287
+ });
1288
+ }
1289
+ const stem = file.slice(0, -".jar".length);
1290
+ if (["jars", "bundles"].includes(segments[total - 2])) {
1291
+ // Ivy local: <org>/<artifact>/<version>/jars/<artifact>.jar
1292
+ if (total >= 5 && stem === segments[total - 4]) {
1293
+ found.push({
1294
+ group: segments[total - 5],
1295
+ artifact: segments[total - 4],
1296
+ version: segments[total - 3],
1297
+ });
1298
+ } else if (stem.startsWith(`${segments[total - 3]}-`)) {
1299
+ // Ivy cache: <org>/<artifact>/jars/<artifact>-<version>.jar
1300
+ found.push({
1301
+ group: segments[total - 4],
1302
+ artifact: segments[total - 3],
1303
+ version: stem.slice(segments[total - 3].length + 1),
1304
+ });
1305
+ }
1306
+ }
1307
+ return found.filter(
1308
+ (coordinates) =>
1309
+ coordinates.group &&
1310
+ coordinates.artifact &&
1311
+ coordinates.version &&
1312
+ (coordinates.group !== entry.group ||
1313
+ coordinates.artifact !== entry.artifact ||
1314
+ coordinates.version !== entry.version),
1315
+ );
1316
+ }
1317
+
1318
+ // The component a set of coordinates names: `{component, exact}` for a single
1319
+ // owner, `{ambiguous: true}` when several components answer, undefined when
1320
+ // none does.
1321
+ function matchCoordinates(entry, { byVersion, withoutVersion }, platform) {
952
1322
  const parsed = parseScalaArtifact(entry?.artifact);
953
1323
  const parts = {
954
1324
  group: entry?.group,
@@ -963,7 +1333,7 @@ export function matchClasspathEntry(
963
1333
  return { component: exact[0].component, exact: true };
964
1334
  }
965
1335
  if (exact.length > 1) {
966
- return undefined;
1336
+ return { ambiguous: true };
967
1337
  }
968
1338
  const candidates = pickByBinaryVersion(
969
1339
  withoutVersion.get(scalaCoordinateKey(parts)),
@@ -973,9 +1343,10 @@ export function matchClasspathEntry(
973
1343
  (candidate) => scalaPurlPlatform(candidate.component.purl) === platform,
974
1344
  );
975
1345
  const pool = matching.length ? matching : candidates;
976
- return pool.length === 1
977
- ? { component: pool[0].component, exact: false }
978
- : undefined;
1346
+ if (pool.length === 1) {
1347
+ return { component: pool[0].component, exact: false };
1348
+ }
1349
+ return pool.length > 1 ? { ambiguous: true } : undefined;
979
1350
  }
980
1351
 
981
1352
  /**
@@ -1043,8 +1414,14 @@ export async function buildScalaJoinIndex(report, components, jarNSMapping) {
1043
1414
  }
1044
1415
  }
1045
1416
  for (const mapPurl of Object.keys(jarNSMapping || {})) {
1046
- const entry = jarNSMapping[mapPurl] || {};
1047
- if (!Array.isArray(entry.namespaces) || !entry.namespaces.length) {
1417
+ // Maps written before every producer used `{ namespaces }` hold the
1418
+ // newline-joined namespaces of a jar-scanned component as a string.
1419
+ const value = jarNSMapping[mapPurl];
1420
+ const namespaces =
1421
+ typeof value === "string"
1422
+ ? value.split("\n").filter(Boolean)
1423
+ : value?.namespaces;
1424
+ if (!Array.isArray(namespaces) || !namespaces.length) {
1048
1425
  continue;
1049
1426
  }
1050
1427
  const segments = /^pkg:[^/]+\/(?:([^/@?]+)\/)?([^@?]+)@([^?#]*)/.exec(
@@ -1063,7 +1440,7 @@ export async function buildScalaJoinIndex(report, components, jarNSMapping) {
1063
1440
  scalaPurlPlatform(mapPurl),
1064
1441
  );
1065
1442
  if (matched) {
1066
- index.addNamespaces(matched.component.purl, entry.namespaces, 3);
1443
+ index.addNamespaces(matched.component.purl, namespaces, 3);
1067
1444
  }
1068
1445
  }
1069
1446
  return index;
@@ -1310,10 +1687,11 @@ export function scalasemCryptoOidKey(name, keySize, mode, curve, padding) {
1310
1687
  return undefined;
1311
1688
  }
1312
1689
  if (SCALASEM_AES_NAMES.has(name)) {
1690
+ const upperMode = mode?.toUpperCase();
1313
1691
  const modeKey =
1314
- SCALASEM_AES_MODE_KEYS[mode] ||
1692
+ SCALASEM_AES_MODE_KEYS[upperMode] ||
1315
1693
  SCALASEM_AES_MODE_KEYS[name.split("-")[1]];
1316
- const wrap = name === "AES-KW" || mode === "KW";
1694
+ const wrap = name === "AES-KW" || upperMode === "KW";
1317
1695
  if (keySize && (modeKey !== undefined || wrap)) {
1318
1696
  const sizedKey = wrap ? `aes${keySize}-wrap` : `aes${keySize}-${modeKey}`;
1319
1697
  const hit = SCALASEM_OID_KEYS.get(sizedKey.toLowerCase());
@@ -1456,6 +1834,7 @@ export async function collectScalasemEvidence(
1456
1834
  componentPropertiesMap,
1457
1835
  metadataProperties,
1458
1836
  options,
1837
+ allowedPurlsByModule,
1459
1838
  );
1460
1839
  addJsModuleEvidence(
1461
1840
  report,
@@ -1606,7 +1985,7 @@ function addUsageEvidence(
1606
1985
  let added = false;
1607
1986
  for (const [file, lines] of files) {
1608
1987
  if (depth < lines.length && locations.size < cap) {
1609
- locations.add(`${file}#${lines[depth]}`);
1988
+ locations.add(occurrenceLocationKey(file, lines[depth]));
1610
1989
  added = true;
1611
1990
  }
1612
1991
  }
@@ -1788,7 +2167,17 @@ function stackTargetPurls(sink, fileEntry, joinIndex, platform, allowed) {
1788
2167
  return [...purls];
1789
2168
  }
1790
2169
 
2170
+ // The package of a class the report names. A class nested in an object reads
2171
+ // `Outer$.Inner`, so the package ends before the first part that is a type:
2172
+ // one that carries a `$` or starts with a capital letter.
1791
2173
  function packageOf(className) {
2174
+ const parts = String(className || "").split(".");
2175
+ const firstType = parts.findIndex(
2176
+ (part) => part.includes("$") || /^[A-Z]/.test(part),
2177
+ );
2178
+ if (firstType > 0) {
2179
+ return parts.slice(0, firstType).join(".");
2180
+ }
1792
2181
  const text = String(className || "").replace(/\$+$/, "");
1793
2182
  const dot = text.lastIndexOf(".");
1794
2183
  return dot > 0 ? text.slice(0, dot) : "";
@@ -1848,9 +2237,22 @@ function providerAliases(purl, components = []) {
1848
2237
  /**
1849
2238
  * The component purl of the library a crypto finding names as its provider.
1850
2239
  * Maven coordinates match on group and base artifact; the short aliases cover
1851
- * the providers the rules emit for libraries without a coordinate.
2240
+ * the providers the rules emit for libraries without a coordinate. Of several
2241
+ * matching components, one on the referencing module's classpath wins, then
2242
+ * the artifact whose name extends the provider's least (`bcprov-jdk18on`
2243
+ * over `bcprov-ext-jdk18on`), then the first purl, so the choice never
2244
+ * depends on the order of the BOM.
2245
+ *
2246
+ * @param {string} provider Provider the finding names.
2247
+ * @param {Object[]} components BOM components.
2248
+ * @param {Set<string>} [classpathPurls] Purls on the module's classpath.
2249
+ * @returns {string|undefined} Component purl.
1852
2250
  */
1853
- export function scalasemProviderPurl(provider, components = []) {
2251
+ export function scalasemProviderPurl(
2252
+ provider,
2253
+ components = [],
2254
+ classpathPurls = undefined,
2255
+ ) {
1854
2256
  if (!provider) {
1855
2257
  return undefined;
1856
2258
  }
@@ -1865,6 +2267,7 @@ export function scalasemProviderPurl(provider, components = []) {
1865
2267
  return undefined;
1866
2268
  }
1867
2269
  const base = parseScalaArtifact(artifactPrefix).artifactBase;
2270
+ const matches = [];
1868
2271
  for (const component of components) {
1869
2272
  if (!component?.purl) {
1870
2273
  continue;
@@ -1877,10 +2280,23 @@ export function scalasemProviderPurl(provider, components = []) {
1877
2280
  }
1878
2281
  const name = parseScalaArtifact(decodeSafe(segments[2])).artifactBase;
1879
2282
  if (name === base || name.startsWith(`${base}-`)) {
1880
- return component.purl;
2283
+ matches.push({ purl: component.purl, name });
1881
2284
  }
1882
2285
  }
1883
- return undefined;
2286
+ const rank = (match) => [
2287
+ classpathPurls?.has(match.purl) ? 0 : 1,
2288
+ match.name.length,
2289
+ ];
2290
+ matches.sort((left, right) => {
2291
+ const [leftClasspath, leftLength] = rank(left);
2292
+ const [rightClasspath, rightLength] = rank(right);
2293
+ return (
2294
+ leftClasspath - rightClasspath ||
2295
+ leftLength - rightLength ||
2296
+ (left.purl < right.purl ? -1 : left.purl > right.purl ? 1 : 0)
2297
+ );
2298
+ });
2299
+ return matches[0]?.purl;
1884
2300
  }
1885
2301
 
1886
2302
  // Primitives a mode of operation applies to. RSA spells `RSA/ECB/...` in JCA,
@@ -1965,6 +2381,7 @@ function addCryptoEvidence(
1965
2381
  componentPropertiesMap,
1966
2382
  metadataProperties,
1967
2383
  options = {},
2384
+ allowedPurlsByModule = new Map(),
1968
2385
  ) {
1969
2386
  const includeTests = options.scalasemIncludeTests === true;
1970
2387
  const nativeLibraryBySymbol = new Map();
@@ -1979,11 +2396,17 @@ function addCryptoEvidence(
1979
2396
  if (scope === "test" && !includeTests) {
1980
2397
  continue;
1981
2398
  }
1982
- const location = `${finding.file || ""}#${finding.line || 0}`;
2399
+ const location = finding.line
2400
+ ? `${finding.file || ""}#${finding.line}`
2401
+ : finding.file || "";
1983
2402
  const nativeLibrary = finding.kind?.startsWith("native")
1984
2403
  ? nativeLibraryBySymbol.get(finding.name)
1985
2404
  : undefined;
1986
- const providerPurl = scalasemProviderPurl(finding.provider, components);
2405
+ const providerPurl = scalasemProviderPurl(
2406
+ finding.provider,
2407
+ components,
2408
+ allowedPurlsFor(report[finding.file], allowedPurlsByModule),
2409
+ );
1987
2410
  const asset =
1988
2411
  finding.resolution === "unresolved"
1989
2412
  ? undefined
@@ -2054,12 +2477,18 @@ function addCryptoEvidence(
2054
2477
  }
2055
2478
  }
2056
2479
 
2480
+ // `file#line` as an occurrence. A location without a line, or whose `#` is
2481
+ // part of the file name, is the file alone.
2057
2482
  function parseLocationString(location) {
2058
2483
  const separator = location.lastIndexOf("#");
2059
- const line = Number.parseInt(location.slice(separator + 1), 10);
2484
+ const suffix = separator >= 0 ? location.slice(separator + 1) : "";
2485
+ if (!/^\d+$/.test(suffix)) {
2486
+ return { location };
2487
+ }
2488
+ const line = Number.parseInt(suffix, 10);
2060
2489
  return {
2061
2490
  location: location.slice(0, separator),
2062
- ...(Number.isFinite(line) && line > 0 ? { line } : {}),
2491
+ ...(line > 0 ? { line } : {}),
2063
2492
  };
2064
2493
  }
2065
2494
 
@@ -2187,6 +2616,191 @@ function cryptoAssetProperties(finding, nativeLibrary) {
2187
2616
  return properties;
2188
2617
  }
2189
2618
 
2619
+ // The characters that end a host list: a path, URL parameters, and the
2620
+ // `;key=value` properties some JDBC drivers take.
2621
+ const ADDRESS_END = ["/", "?", "#", ";"];
2622
+
2623
+ const HOST_PATTERN = /^(?:\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9._%-]+)(?::\d+)?$/;
2624
+
2625
+ function indexOfAny(text, characters, from = 0) {
2626
+ let found = -1;
2627
+ for (const character of characters) {
2628
+ const index = text.indexOf(character, from);
2629
+ if (index >= 0 && (found < 0 || index < found)) {
2630
+ found = index;
2631
+ }
2632
+ }
2633
+ return found;
2634
+ }
2635
+
2636
+ function cutAt(text, characters) {
2637
+ const index = indexOfAny(text, characters);
2638
+ return index >= 0 ? text.slice(0, index) : text;
2639
+ }
2640
+
2641
+ // `host`, `host:port`, `[ipv6]:port`, or a comma separated list of those.
2642
+ function isHostList(text) {
2643
+ return (
2644
+ Boolean(text) && text.split(",").every((host) => HOST_PATTERN.test(host))
2645
+ );
2646
+ }
2647
+
2648
+ /**
2649
+ * Where the userinfo in front of a host ends, or -1 when there is none. A
2650
+ * password may hold any of `/ ? # ; @ =`, so an `@` ends the userinfo unless a
2651
+ * well-formed host list is followed by parameters that hold it
2652
+ * (`host:1433;user=a@b`). Of several `@`, the last one before the first
2653
+ * parameter ends it, and the last one of all when that leaves none: a host the
2654
+ * split gets wrong costs a service name, a split that keeps a password leaks it.
2655
+ *
2656
+ * @param {string} rest The text after `//`, or after a JDBC subprotocol.
2657
+ * @returns {number} Index of the `@` that ends the userinfo.
2658
+ */
2659
+ function userinfoEnd(rest) {
2660
+ const firstAt = rest.indexOf("@");
2661
+ if (firstAt < 0) {
2662
+ return -1;
2663
+ }
2664
+ const end = indexOfAny(rest, ADDRESS_END);
2665
+ if (
2666
+ end >= 0 &&
2667
+ end < firstAt &&
2668
+ isHostList(rest.slice(0, end)) &&
2669
+ rest.slice(end + 1, firstAt).includes("=")
2670
+ ) {
2671
+ return -1;
2672
+ }
2673
+ const firstEquals = rest.indexOf("=");
2674
+ const beforeParameters =
2675
+ firstEquals >= 0 ? rest.lastIndexOf("@", firstEquals) : -1;
2676
+ return beforeParameters >= 0 ? beforeParameters : rest.lastIndexOf("@");
2677
+ }
2678
+
2679
+ /**
2680
+ * The `host[:port]` list of a parenthesized address: MySQL
2681
+ * `address=(host=h)(port=3306)` and `(host=h,port=3306,user=u)`, Oracle
2682
+ * `(DESCRIPTION=(ADDRESS=(HOST=h)(PORT=1521)))`. Only host and port values are
2683
+ * read, so a user or password beside them never leaves.
2684
+ */
2685
+ function parenthesizedHosts(text) {
2686
+ const hosts = [];
2687
+ const ports = [];
2688
+ for (const group of text.split(/[()]/)) {
2689
+ for (const pair of group.split(",")) {
2690
+ const equals = pair.indexOf("=");
2691
+ if (equals <= 0) {
2692
+ continue;
2693
+ }
2694
+ const key = pair.slice(0, equals).trim().toLowerCase();
2695
+ const value = pair.slice(equals + 1).trim();
2696
+ if (key === "host" && HOST_PATTERN.test(value)) {
2697
+ hosts.push(value);
2698
+ } else if (key === "port" && /^\d+$/.test(value)) {
2699
+ ports[hosts.length ? hosts.length - 1 : 0] ??= value;
2700
+ }
2701
+ }
2702
+ }
2703
+ return hosts.map((host, index) =>
2704
+ ports[index] && !host.includes(":") ? `${host}:${ports[index]}` : host,
2705
+ );
2706
+ }
2707
+
2708
+ // The hosts and database path of an authority, after its userinfo is gone.
2709
+ function parseAuthority(text) {
2710
+ const parenthesized = text.startsWith("(") || /^[A-Za-z]+=\(/.test(text);
2711
+ if (parenthesized) {
2712
+ const close = text.lastIndexOf(")");
2713
+ const after = close >= 0 ? text.slice(close + 1) : "";
2714
+ return {
2715
+ hosts: parenthesizedHosts(text.slice(0, close + 1)),
2716
+ path: after.startsWith("/") ? cutAt(after, ["?", "#", ";", ":"]) : "",
2717
+ };
2718
+ }
2719
+ const end = indexOfAny(text, ADDRESS_END);
2720
+ const hostPart = end >= 0 ? text.slice(0, end) : text;
2721
+ const hosts = isHostList(hostPart) ? hostPart.split(",") : [];
2722
+ // DB2 and Informix append `:key=value;` properties to the database name.
2723
+ const path =
2724
+ hosts.length && end >= 0 && text[end] === "/"
2725
+ ? cutAt(text.slice(end), ["?", "#", ";", ":"])
2726
+ : "";
2727
+ return { hosts, path };
2728
+ }
2729
+
2730
+ /**
2731
+ * Split a JDBC URL into its subprotocol, hosts and database path, dropping
2732
+ * anything that may carry credentials: userinfo, the `user/password@` prefix
2733
+ * Oracle takes, URL parameters, and the `;key=value` and `:key=value`
2734
+ * properties some drivers take. Host values that do not look like hosts are
2735
+ * dropped too, rather than passed on.
2736
+ *
2737
+ * @param {string} value JDBC URL.
2738
+ * @returns {{scheme: string, hosts: string[], address: string}|undefined}
2739
+ * The first subprotocol, the hosts, and the address rebuilt from those
2740
+ * parts; undefined for anything but a JDBC URL.
2741
+ */
2742
+ function parseJdbcUrl(value) {
2743
+ const text = String(value || "").trim();
2744
+ if (!/^jdbc:/i.test(text)) {
2745
+ return undefined;
2746
+ }
2747
+ let rest = text.slice(5);
2748
+ const tokens = [];
2749
+ for (
2750
+ let token = /^[A-Za-z][\w+.-]*:/.exec(rest);
2751
+ token && !rest.startsWith("//");
2752
+ token = /^[A-Za-z][\w+.-]*:/.exec(rest)
2753
+ ) {
2754
+ tokens.push(token[0]);
2755
+ rest = rest.slice(token[0].length);
2756
+ }
2757
+ const scheme = (tokens[0] || "jdbc:").slice(0, -1).toLowerCase();
2758
+ if (rest.startsWith("//")) {
2759
+ const authority = rest.slice(2);
2760
+ const { hosts, path } = parseAuthority(
2761
+ authority.slice(userinfoEnd(authority) + 1),
2762
+ );
2763
+ return {
2764
+ scheme,
2765
+ hosts,
2766
+ address: `jdbc:${tokens.join("")}//${hosts.join(",")}${path}`,
2767
+ };
2768
+ }
2769
+ const at = userinfoEnd(rest);
2770
+ if (at < 0) {
2771
+ return {
2772
+ scheme,
2773
+ hosts: [],
2774
+ address: `jdbc:${tokens.join("")}${cutAt(rest, ["?", "#", ";"])}`,
2775
+ };
2776
+ }
2777
+ // `user/password@host:port:sid`: a word before the credentials can read as
2778
+ // a subprotocol (`jdbc:db:user:password@host`), so only the driver keeps
2779
+ // its tokens, and Oracle its driver type.
2780
+ const kept = tokens.slice(0, scheme === "oracle" ? 2 : 1).join("");
2781
+ const target = rest.slice(at + 1);
2782
+ if (target.startsWith("//")) {
2783
+ const { hosts, path } = parseAuthority(target.slice(2));
2784
+ return {
2785
+ scheme,
2786
+ hosts,
2787
+ address: `jdbc:${kept}@//${hosts.join(",")}${path}`,
2788
+ };
2789
+ }
2790
+ if (target.startsWith("(")) {
2791
+ const hosts = parenthesizedHosts(target);
2792
+ return { scheme, hosts, address: `jdbc:${kept}@${hosts.join(",")}` };
2793
+ }
2794
+ const address = cutAt(target, ["?", "#", ";", "/"]);
2795
+ const host = address.split(":")[0];
2796
+ const hosts = HOST_PATTERN.test(host) ? [host] : [];
2797
+ return {
2798
+ scheme,
2799
+ hosts,
2800
+ address: `jdbc:${kept}@${hosts.length ? address : ""}`,
2801
+ };
2802
+ }
2803
+
2190
2804
  /**
2191
2805
  * A JDBC address without what may carry credentials: userinfo, the Oracle
2192
2806
  * `user/password@` prefix, URL parameters and the `;key=value` properties
@@ -2197,49 +2811,58 @@ function cryptoAssetProperties(finding, nativeLibrary) {
2197
2811
  * @returns {string} The address part.
2198
2812
  */
2199
2813
  export function sanitizeJdbcUrl(value) {
2200
- let text = String(value || "").trim();
2201
- if (!/^jdbc:/i.test(text)) {
2202
- return text;
2203
- }
2204
- text = `jdbc:${text.slice(5)}`;
2205
- for (const separator of ["?", "#", ";"]) {
2206
- const index = text.indexOf(separator);
2207
- if (index >= 0) {
2208
- text = text.slice(0, index);
2209
- }
2210
- }
2211
- const slashes = text.indexOf("//");
2212
- if (slashes >= 0) {
2213
- const authorityEnd = text.indexOf("/", slashes + 2);
2214
- const authority = text.slice(
2215
- slashes + 2,
2216
- authorityEnd >= 0 ? authorityEnd : undefined,
2217
- );
2218
- const at = authority.lastIndexOf("@");
2219
- if (at >= 0) {
2220
- text =
2221
- text.slice(0, slashes + 2) +
2222
- authority.slice(at + 1) +
2223
- (authorityEnd >= 0 ? text.slice(authorityEnd) : "");
2224
- }
2225
- }
2226
- // Oracle thin: jdbc:oracle:thin:user/password@host:port:sid
2227
- const thin = /^(jdbc:oracle:thin:)[^@]*@/i.exec(text);
2228
- if (thin) {
2229
- text = `${thin[1]}@${text.slice(thin[0].length)}`;
2814
+ const parsed = parseJdbcUrl(value);
2815
+ return parsed ? parsed.address : String(value || "").trim();
2816
+ }
2817
+
2818
+ // The host name of `host`, `host:port` or `[ipv6]:port`.
2819
+ function hostName(host) {
2820
+ if (host.startsWith("[")) {
2821
+ const close = host.indexOf("]");
2822
+ return close > 0 ? host.slice(1, close) : undefined;
2230
2823
  }
2231
- return text;
2824
+ return host.split(":")[0] || undefined;
2232
2825
  }
2233
2826
 
2234
2827
  function jdbcServiceName(value) {
2235
- const address = sanitizeJdbcUrl(value);
2236
- const scheme = address.slice(5).split(":", 1)[0].toLowerCase() || "jdbc";
2237
- let host = /\/\/([^/:]+)/.exec(address)?.[1];
2238
- if (!host) {
2239
- // Oracle thin and similar: the host follows the @.
2240
- host = /@(?:\/\/)?([^/:]+)/.exec(address)?.[1];
2828
+ const { scheme, hosts } = parseJdbcUrl(value);
2829
+ return [scheme, hosts.length ? hostName(hosts[0]) : undefined]
2830
+ .filter(Boolean)
2831
+ .join("-");
2832
+ }
2833
+
2834
+ // A host value as `host[:port]`, without a scheme, userinfo, path or
2835
+ // parameters, or undefined when what is left is not a host.
2836
+ function bareHost(value) {
2837
+ let text = String(value || "").trim();
2838
+ const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.exec(text);
2839
+ if (scheme) {
2840
+ text = text.slice(scheme[0].length);
2841
+ }
2842
+ text = cutAt(text.slice(userinfoEnd(text) + 1), ADDRESS_END);
2843
+ return HOST_PATTERN.test(text) ? text : undefined;
2844
+ }
2845
+
2846
+ // The `host[:port]` a URL names. The userinfo is dropped before the URL is
2847
+ // parsed: a password holding `/` would otherwise end the authority early and
2848
+ // leave the user name to read as the host.
2849
+ function urlHost(value) {
2850
+ const separator = value.indexOf("://");
2851
+ if (separator <= 0) {
2852
+ return undefined;
2853
+ }
2854
+ const rest = value.slice(separator + 3);
2855
+ const withoutUserinfo = rest.slice(userinfoEnd(rest) + 1);
2856
+ try {
2857
+ const host = new URL(`${value.slice(0, separator)}://${withoutUserinfo}`)
2858
+ .host;
2859
+ if (host) {
2860
+ return host;
2861
+ }
2862
+ } catch (_err) {
2863
+ // Not a URL the WHATWG parser takes; the host is read by hand.
2241
2864
  }
2242
- return [scheme, host].filter(Boolean).join("-");
2865
+ return bareHost(withoutUserinfo);
2243
2866
  }
2244
2867
 
2245
2868
  /**
@@ -2259,13 +2882,11 @@ export function scalasemServiceName(service) {
2259
2882
  return jdbcServiceName(value);
2260
2883
  }
2261
2884
  if (service?.host) {
2262
- return service.host.split(":")[0];
2263
- }
2264
- try {
2265
- return new URL(value).hostname || undefined;
2266
- } catch (_err) {
2267
- return undefined;
2885
+ const host = bareHost(service.host);
2886
+ return host ? hostName(host) : undefined;
2268
2887
  }
2888
+ const host = urlHost(value);
2889
+ return host ? hostName(host) : undefined;
2269
2890
  }
2270
2891
 
2271
2892
  /**
@@ -2287,15 +2908,12 @@ export function scalasemServiceEndpoint(service) {
2287
2908
  return sanitizeJdbcUrl(value);
2288
2909
  }
2289
2910
  if (service?.host) {
2290
- return service.host;
2291
- }
2292
- try {
2293
- const parsed = new URL(value);
2294
- return `${parsed.protocol}//${parsed.host}`;
2295
- } catch (_err) {
2296
- const hostMatch = /:\/\/([^/?#@]+@)?([^/?#]+)/.exec(value);
2297
- return hostMatch ? `${value.split("://")[0]}://${hostMatch[2]}` : undefined;
2911
+ return bareHost(service.host);
2298
2912
  }
2913
+ const host = urlHost(value);
2914
+ return host
2915
+ ? `${value.slice(0, value.indexOf("://")).toLowerCase()}://${host}`
2916
+ : undefined;
2299
2917
  }
2300
2918
 
2301
2919
  function serviceLocation(service) {
@@ -2459,7 +3077,10 @@ export async function collectScalaJsNpmComponents(
2459
3077
  ) {
2460
3078
  const isScalaJs = pkgList.some((pkg) => {
2461
3079
  const name = pkg?.name || purlPurlName(pkg?.purl) || "";
2462
- return /_(sjs\d+(?:\.\d+)?)$/.test(name) || name === "scalajs-library";
3080
+ return (
3081
+ scalaNamePlatform(name) === "js" ||
3082
+ parseScalaArtifact(name).artifactBase === "scalajs-library"
3083
+ );
2463
3084
  });
2464
3085
  const empty = { components: [], roots: [], dependencies: [] };
2465
3086
  if (!isScalaJs) {
@@ -2548,6 +3169,24 @@ function npmNameFromPurl(purl) {
2548
3169
  }
2549
3170
  }
2550
3171
 
3172
+ function nodeModulesDepth(component) {
3173
+ const location = (component.properties || []).find(
3174
+ (property) => property.name === "internal:LocalNodeModulesPath",
3175
+ )?.value;
3176
+ return location
3177
+ ? location.split("node_modules/").length - 1
3178
+ : Number.POSITIVE_INFINITY;
3179
+ }
3180
+
3181
+ function isPreferredNpmCopy(candidate, known) {
3182
+ const candidateDepth = nodeModulesDepth(candidate);
3183
+ const knownDepth = nodeModulesDepth(known);
3184
+ if (candidateDepth !== knownDepth) {
3185
+ return candidateDepth < knownDepth;
3186
+ }
3187
+ return candidate.purl < known.purl;
3188
+ }
3189
+
2551
3190
  /**
2552
3191
  * Attribute the report's JavaScript module imports to the npm components the
2553
3192
  * build bundles. A module no component owns is reported on the project
@@ -2564,10 +3203,17 @@ function addJsModuleEvidence(
2564
3203
  if (!jsModules.length) {
2565
3204
  return;
2566
3205
  }
3206
+ // Of several versions of one package, the one a bare import resolves to is
3207
+ // the hoisted copy, the shallowest under node_modules; the first purl
3208
+ // breaks a tie, so the choice never depends on the order of the BOM.
2567
3209
  const npmByName = new Map();
2568
3210
  for (const component of components) {
2569
3211
  const name = npmNameFromPurl(component?.purl);
2570
- if (name) {
3212
+ if (!name) {
3213
+ continue;
3214
+ }
3215
+ const known = npmByName.get(name);
3216
+ if (!known || isPreferredNpmCopy(component, known)) {
2571
3217
  npmByName.set(name, component);
2572
3218
  }
2573
3219
  }