@cdxgen/cdxgen 13.3.0 → 13.5.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 (372) hide show
  1. package/README.md +12 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +115 -31
  4. package/bin/evinse.js +23 -1
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +115 -0
  7. package/bin/sign.js +45 -15
  8. package/bin/tracebom.js +5 -1
  9. package/bin/validate.js +84 -18
  10. package/bin/verify.js +155 -86
  11. package/data/cbomosdb-queries.json +13 -1
  12. package/data/component-tags.json +8 -0
  13. package/data/helpers/init.gradle +46 -21
  14. package/data/queries-darwin.json +25 -1
  15. package/data/queries-win.json +19 -1
  16. package/data/queries.json +41 -0
  17. package/data/remediations.json +344 -1
  18. package/data/rules/cbom-security.yaml +235 -4
  19. package/data/rules/dependency-sources.yaml +67 -0
  20. package/data/rules/go-binaries.yaml +46 -0
  21. package/data/rules/golem-go.yaml +80 -0
  22. package/data/rules/hbom-compliance.yaml +40 -22
  23. package/data/rules/hbom-performance.yaml +7 -1
  24. package/data/rules/host-topology.yaml +1 -1
  25. package/data/rules/obom-runtime.yaml +31 -2
  26. package/data/rules/php-runtime.yaml +143 -0
  27. package/data/rules/scala-evidence.yaml +115 -0
  28. package/lib/audit/index.js +52 -6
  29. package/lib/cli/bomAssembly.js +28 -3
  30. package/lib/cli/cliOptions.js +110 -3
  31. package/lib/cli/index.js +186 -58
  32. package/lib/cli/jsBom.js +50 -29
  33. package/lib/cli/jvmBom.js +779 -146
  34. package/lib/cli/managedBom.js +1021 -261
  35. package/lib/cli/nativeBom.js +364 -58
  36. package/lib/core/activity.js +12 -0
  37. package/lib/core/env.js +47 -17
  38. package/lib/core/fs.js +31 -21
  39. package/lib/core/httpClient.js +233 -69
  40. package/lib/core/pluginRun.js +184 -0
  41. package/lib/core/processTree.js +100 -0
  42. package/lib/core/projectConfig.js +1 -0
  43. package/lib/core/projectTooling.js +97 -0
  44. package/lib/core/propertySanitizer.js +72 -0
  45. package/lib/core/runState.js +39 -0
  46. package/lib/core/toml.js +72 -0
  47. package/lib/ecosystems/asarutils.js +4 -2
  48. package/lib/ecosystems/bunutils.js +5 -7
  49. package/lib/ecosystems/cargoCache.js +231 -8
  50. package/lib/ecosystems/cmakeResolver.js +10 -21
  51. package/lib/ecosystems/cppBuildContext.js +631 -0
  52. package/lib/ecosystems/cppEvidence.js +337 -22
  53. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  54. package/lib/ecosystems/denoutils.js +93 -66
  55. package/lib/ecosystems/ecosystems.js +1967 -788
  56. package/lib/ecosystems/golem.js +578 -18
  57. package/lib/ecosystems/gradleutils.js +340 -46
  58. package/lib/ecosystems/jsEvidence.js +85 -24
  59. package/lib/ecosystems/kosi.js +21 -4
  60. package/lib/ecosystems/npmutils.js +64 -0
  61. package/lib/ecosystems/parsers-dotnet.js +668 -153
  62. package/lib/ecosystems/parsers-elm.js +46 -28
  63. package/lib/ecosystems/parsers-gleam.js +3 -4
  64. package/lib/ecosystems/parsers-go.js +1283 -297
  65. package/lib/ecosystems/parsers-js.js +92 -26
  66. package/lib/ecosystems/parsers-julia.js +2 -3
  67. package/lib/ecosystems/parsers-jvm.js +463 -29
  68. package/lib/ecosystems/parsers-misc.js +405 -93
  69. package/lib/ecosystems/parsers-python.js +152 -20
  70. package/lib/ecosystems/parsers-rust.js +4 -4
  71. package/lib/ecosystems/parsers-terraform.js +1573 -19
  72. package/lib/ecosystems/pythonutils.js +102 -6
  73. package/lib/ecosystems/remote/huggingface.js +3 -0
  74. package/lib/ecosystems/rubyutils.js +73 -20
  75. package/lib/ecosystems/rusi.js +32 -9
  76. package/lib/ecosystems/sbtutils.js +243 -187
  77. package/lib/ecosystems/scalacliutils.js +343 -0
  78. package/lib/ecosystems/scalasem.js +2620 -0
  79. package/lib/ecosystems/terraformRegistry.js +288 -0
  80. package/lib/ecosystems/utils.js +2 -0
  81. package/lib/ecosystems/vendoredCode.js +156 -0
  82. package/lib/evinser/evinser.js +695 -78
  83. package/lib/evinser/swiftsem.js +68 -5
  84. package/lib/helpers/agenticToolutils.js +2 -2
  85. package/lib/helpers/bomSigner.js +576 -128
  86. package/lib/inventory/agentSkillSpec.js +17 -0
  87. package/lib/inventory/aiCollector.js +4 -1
  88. package/lib/inventory/analyzer.js +110 -127
  89. package/lib/inventory/apkInstalledDb.js +243 -0
  90. package/lib/inventory/atomUtils.js +204 -48
  91. package/lib/inventory/auditCategories.js +10 -3
  92. package/lib/inventory/buildToolRateLimit.js +144 -0
  93. package/lib/inventory/cacheCommands.js +2 -1
  94. package/lib/inventory/cacheDir.js +52 -9
  95. package/lib/inventory/certificates.js +2371 -0
  96. package/lib/inventory/cmakeBuildDirs.js +254 -0
  97. package/lib/inventory/communityAiConfigParser.js +4 -9
  98. package/lib/inventory/deps.js +507 -128
  99. package/lib/inventory/display.js +200 -3
  100. package/lib/inventory/dosai.js +747 -101
  101. package/lib/inventory/dosaiParsers.js +310 -15
  102. package/lib/inventory/dynamic.js +96 -8
  103. package/lib/inventory/envcontext.js +168 -5
  104. package/lib/inventory/evidenceUtils.js +590 -132
  105. package/lib/inventory/fetchBatch.js +574 -38
  106. package/lib/inventory/fetchDiskCache.js +597 -0
  107. package/lib/inventory/forgeEnricher.js +14 -5
  108. package/lib/inventory/formulationParsers.js +3 -2
  109. package/lib/inventory/goBuildInfo.js +140 -0
  110. package/lib/inventory/hbom.js +67 -7
  111. package/lib/inventory/hbomAnalysis.js +16 -0
  112. package/lib/inventory/hostTopology.js +21 -0
  113. package/lib/inventory/jvmLocalRepos.js +917 -0
  114. package/lib/inventory/mcpConfigParser.js +2 -3
  115. package/lib/inventory/osPackagePurl.js +244 -0
  116. package/lib/inventory/osPackageResolver.js +89 -87
  117. package/lib/inventory/osinfo.js +79 -31
  118. package/lib/inventory/osqueryTransform.js +114 -5
  119. package/lib/inventory/parentSelection.js +146 -0
  120. package/lib/inventory/phpBuildInfo.js +487 -0
  121. package/lib/inventory/phpHostProbe.js +641 -0
  122. package/lib/inventory/rootfsCerts.js +426 -0
  123. package/lib/inventory/rustFormulationParser.js +3 -4
  124. package/lib/inventory/scalaCoords.js +153 -0
  125. package/lib/inventory/source.js +215 -11
  126. package/lib/inventory/spdx.js +245 -0
  127. package/lib/inventory/traceRunner.js +93 -8
  128. package/lib/inventory/tracedCrypto.js +423 -0
  129. package/lib/managers/binary.js +529 -183
  130. package/lib/managers/docker.js +199 -44
  131. package/lib/managers/oci.js +24 -2
  132. package/lib/parsers/cmakeCache.js +69 -0
  133. package/lib/parsers/cmakeLists.js +16 -2
  134. package/lib/parsers/cmakePresets.js +400 -0
  135. package/lib/parsers/compileCommands.js +557 -0
  136. package/lib/parsers/email.js +277 -0
  137. package/lib/parsers/largeJson.js +34 -2
  138. package/lib/parsers/strictJson.js +322 -0
  139. package/lib/parsers/terraformConfig.js +1073 -0
  140. package/lib/parsers/terraformSource.js +652 -0
  141. package/lib/server/server.js +209 -1
  142. package/lib/stages/postgen/annotator.js +17 -1
  143. package/lib/stages/postgen/introspection/markers.js +1 -1
  144. package/lib/stages/postgen/introspection/reflect.js +4 -1
  145. package/lib/stages/postgen/postgen.js +20 -6
  146. package/lib/stages/postgen/ruleEngine.js +58 -1
  147. package/lib/stages/postgen/specVersionCompat.js +62 -0
  148. package/lib/stages/pregen/pregen.js +117 -5
  149. package/lib/validator/bomValidator.js +26 -5
  150. package/lib/validator/generated/runtime.mjs +13 -0
  151. package/lib/validator/generated/validate-1.6.mjs +26 -7
  152. package/lib/validator/generated/validate-1.7.mjs +31 -12
  153. package/lib/validator/generated/validate-2.0.mjs +30 -11
  154. package/lib/validator/index.js +25 -10
  155. package/lib/validator/reporters/json.js +1 -0
  156. package/package.json +23 -23
  157. package/types/bin/cdxgen-secure.d.ts +3 -0
  158. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  159. package/types/bin/repl.d.ts.map +1 -1
  160. package/types/lib/audit/index.d.ts.map +1 -1
  161. package/types/lib/cli/bomAssembly.d.ts +9 -2
  162. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  163. package/types/lib/cli/cliOptions.d.ts +43 -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.map +1 -1
  167. package/types/lib/cli/jvmBom.d.ts +44 -1
  168. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  169. package/types/lib/cli/managedBom.d.ts +7 -4
  170. package/types/lib/cli/managedBom.d.ts.map +1 -1
  171. package/types/lib/cli/nativeBom.d.ts +3 -1
  172. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  173. package/types/lib/core/activity.d.ts.map +1 -1
  174. package/types/lib/core/env.d.ts +27 -2
  175. package/types/lib/core/env.d.ts.map +1 -1
  176. package/types/lib/core/fs.d.ts.map +1 -1
  177. package/types/lib/core/httpClient.d.ts +13 -8
  178. package/types/lib/core/httpClient.d.ts.map +1 -1
  179. package/types/lib/core/pluginRun.d.ts +97 -0
  180. package/types/lib/core/pluginRun.d.ts.map +1 -0
  181. package/types/lib/core/processTree.d.ts +36 -0
  182. package/types/lib/core/processTree.d.ts.map +1 -0
  183. package/types/lib/core/projectConfig.d.ts.map +1 -1
  184. package/types/lib/core/projectTooling.d.ts +41 -0
  185. package/types/lib/core/projectTooling.d.ts.map +1 -0
  186. package/types/lib/core/propertySanitizer.d.ts +26 -0
  187. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  188. package/types/lib/core/runState.d.ts +29 -0
  189. package/types/lib/core/runState.d.ts.map +1 -0
  190. package/types/lib/core/toml.d.ts +20 -0
  191. package/types/lib/core/toml.d.ts.map +1 -0
  192. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  193. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  194. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  195. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  196. package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
  197. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  198. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  199. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  200. package/types/lib/ecosystems/cppEvidence.d.ts +19 -3
  201. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  202. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  203. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  204. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  205. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  206. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  207. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  208. package/types/lib/ecosystems/golem.d.ts +81 -1
  209. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  210. package/types/lib/ecosystems/gradleutils.d.ts +59 -7
  211. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  212. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  213. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  214. package/types/lib/ecosystems/npmutils.d.ts +15 -0
  215. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  216. package/types/lib/ecosystems/parsers-dotnet.d.ts +155 -5
  217. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  218. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  219. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  220. package/types/lib/ecosystems/parsers-go.d.ts +119 -5
  221. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  222. package/types/lib/ecosystems/parsers-js.d.ts +6 -1
  223. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  224. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  225. package/types/lib/ecosystems/parsers-jvm.d.ts +74 -3
  226. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  227. package/types/lib/ecosystems/parsers-misc.d.ts +21 -59
  228. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  229. package/types/lib/ecosystems/parsers-python.d.ts +57 -6
  230. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  231. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  232. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  233. package/types/lib/ecosystems/pythonutils.d.ts +26 -1
  234. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  235. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  236. package/types/lib/ecosystems/rubyutils.d.ts +21 -1
  237. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  238. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  239. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  240. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  241. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  242. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  243. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  244. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  245. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  246. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  247. package/types/lib/ecosystems/utils.d.ts +2 -2
  248. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  249. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  250. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  251. package/types/lib/evinser/evinser.d.ts +100 -8
  252. package/types/lib/evinser/evinser.d.ts.map +1 -1
  253. package/types/lib/evinser/swiftsem.d.ts +10 -0
  254. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  255. package/types/lib/helpers/bomSigner.d.ts +80 -4
  256. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  257. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  258. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  259. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  260. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  261. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  262. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  263. package/types/lib/inventory/atomUtils.d.ts +96 -10
  264. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  265. package/types/lib/inventory/auditCategories.d.ts +3 -0
  266. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  267. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  268. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  269. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  270. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  271. package/types/lib/inventory/cacheDir.d.ts +12 -4
  272. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  273. package/types/lib/inventory/certificates.d.ts +205 -0
  274. package/types/lib/inventory/certificates.d.ts.map +1 -0
  275. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  276. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  277. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  278. package/types/lib/inventory/deps.d.ts +61 -2
  279. package/types/lib/inventory/deps.d.ts.map +1 -1
  280. package/types/lib/inventory/display.d.ts +18 -0
  281. package/types/lib/inventory/display.d.ts.map +1 -1
  282. package/types/lib/inventory/dosai.d.ts +202 -14
  283. package/types/lib/inventory/dosai.d.ts.map +1 -1
  284. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  285. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  286. package/types/lib/inventory/dynamic.d.ts +16 -1
  287. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  288. package/types/lib/inventory/envcontext.d.ts +40 -2
  289. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  290. package/types/lib/inventory/evidenceUtils.d.ts +48 -14
  291. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  292. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  293. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  294. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  295. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  296. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  297. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  298. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  299. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  300. package/types/lib/inventory/hbom.d.ts.map +1 -1
  301. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  302. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  303. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  304. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  305. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  306. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  307. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  308. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  309. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  310. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  311. package/types/lib/inventory/osinfo.d.ts +31 -6
  312. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  313. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  314. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  315. package/types/lib/inventory/parentSelection.d.ts +34 -0
  316. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  317. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  318. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  319. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  320. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  321. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  322. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  323. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  324. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  325. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  326. package/types/lib/inventory/source.d.ts +28 -0
  327. package/types/lib/inventory/source.d.ts.map +1 -1
  328. package/types/lib/inventory/spdx.d.ts +24 -0
  329. package/types/lib/inventory/spdx.d.ts.map +1 -1
  330. package/types/lib/inventory/traceRunner.d.ts +16 -1
  331. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  332. package/types/lib/inventory/tracedCrypto.d.ts +64 -0
  333. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  334. package/types/lib/managers/binary.d.ts +10 -0
  335. package/types/lib/managers/binary.d.ts.map +1 -1
  336. package/types/lib/managers/docker.d.ts +12 -0
  337. package/types/lib/managers/docker.d.ts.map +1 -1
  338. package/types/lib/managers/oci.d.ts +3 -1
  339. package/types/lib/managers/oci.d.ts.map +1 -1
  340. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  341. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  342. package/types/lib/parsers/cmakeLists.d.ts +2 -1
  343. package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
  344. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  345. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  346. package/types/lib/parsers/compileCommands.d.ts +115 -0
  347. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  348. package/types/lib/parsers/email.d.ts +19 -0
  349. package/types/lib/parsers/email.d.ts.map +1 -0
  350. package/types/lib/parsers/largeJson.d.ts +13 -0
  351. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  352. package/types/lib/parsers/strictJson.d.ts +46 -0
  353. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  354. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  355. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  356. package/types/lib/parsers/terraformSource.d.ts +66 -0
  357. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  358. package/types/lib/server/server.d.ts +8 -0
  359. package/types/lib/server/server.d.ts.map +1 -1
  360. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  361. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  362. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  363. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  364. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  365. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  366. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  367. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  368. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  369. package/types/lib/validator/index.d.ts +8 -0
  370. package/types/lib/validator/index.d.ts.map +1 -1
  371. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  372. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -0,0 +1,2620 @@
1
+ import { lstatSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
3
+ import process from "node:process";
4
+
5
+ import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
6
+ import { deferFailOnError } from "../core/deferredExit.js";
7
+ import { PROJECT_TYPE_ALIASES } from "../core/env.js";
8
+ import {
9
+ getAllFiles,
10
+ getTmpDir,
11
+ safeCopyFileSync,
12
+ safeExistsSync,
13
+ safeMkdirSync,
14
+ safeMkdtempSync,
15
+ safeRmSync,
16
+ safeSpawnSync,
17
+ } from "../core/fs.js";
18
+ import { dirNameStr } from "../core/paths.js";
19
+ import { resolvePluginSourceDir } from "../core/pluginRun.js";
20
+ import { formatDuration } from "../core/processTree.js";
21
+ import { isProjectToolingAllowed } from "../core/projectTooling.js";
22
+ import { parsetoolsFile } from "../inventory/atomUtils.js";
23
+ import { applyAlgorithmProperties } from "../inventory/cryptoAlgorithmFamily.js";
24
+ import { getJarClasses } from "../inventory/deps.js";
25
+ import {
26
+ parseScalaArtifact,
27
+ scalaCoordinateKey,
28
+ scalaCoordinateOfPurl,
29
+ } from "../inventory/scalaCoords.js";
30
+ import { parsePkgLock } from "./parsers-js.js";
31
+
32
+ /**
33
+ * scalasem is the Scala semantic analyzer shipped inside atom-parsetools. This
34
+ * module is the only place that spawns it, and it owns the projection of the
35
+ * scalasem report (schema scalasem/2) onto CycloneDX evidence. It follows the
36
+ * golem and kosi discipline: a failed run may degrade the Scala evidence but
37
+ * never aborts SBOM generation, and it always says why.
38
+ */
39
+
40
+ /** Longest wait for a scalasem run. Large repositories degrade, not hang. */
41
+ const DEFAULT_TIMEOUT_MS = 20 * 60_000;
42
+
43
+ /** Occurrences kept per component unless CDXGEN_SCALASEM_MAX_OCCURRENCES says otherwise. */
44
+ const DEFAULT_MAX_OCCURRENCES = 200;
45
+
46
+ // The JDK ships these packages and no component stands for it. `javax.` is
47
+ // split between the JDK and libraries (`javax.servlet`, `javax.inject`), so a
48
+ // `javax.` symbol joins a library only through a class the library ships.
49
+ export const JDK_OWNER_PREFIXES = ["java.", "jdk.", "sun."];
50
+ const JDK_EXTENSION_PREFIX = "javax.";
51
+
52
+ // The language runtimes every module carries, on each platform. Their
53
+ // occurrences are capped at one per file: they say a file is Scala, not that
54
+ // it uses a library.
55
+ export const SCALA_STDLIB_ARTIFACTS = new Set([
56
+ "auxlib",
57
+ "javalib",
58
+ "nativelib",
59
+ "scala-library",
60
+ "scala3-library",
61
+ "scalajs-library",
62
+ "scalalib",
63
+ ]);
64
+
65
+ export function appendUniqueProperty(properties, name, value) {
66
+ if (value === undefined || value === null || value === "") {
67
+ return;
68
+ }
69
+ const propertyValue = String(value);
70
+ if (
71
+ !properties.some(
72
+ (property) => property.name === name && property.value === propertyValue,
73
+ )
74
+ ) {
75
+ properties.push({ name, value: propertyValue });
76
+ }
77
+ }
78
+
79
+ export function addPropertyValue(map, key, name, value) {
80
+ if (!key || value === undefined || value === null || value === "") {
81
+ return;
82
+ }
83
+ map[key] ??= [];
84
+ appendUniqueProperty(map[key], name, value);
85
+ }
86
+
87
+ export function addSetValue(map, key, value) {
88
+ if (!key || !value) {
89
+ return;
90
+ }
91
+ map[key] ??= new Set();
92
+ map[key].add(value);
93
+ }
94
+
95
+ /**
96
+ * True for every project type the Scala path serves, matching the alias list
97
+ * the BOM generators dispatch on.
98
+ *
99
+ * @param {string} language Project language or type.
100
+ * @returns {boolean}
101
+ */
102
+ export function isScalasemLanguage(language) {
103
+ return PROJECT_TYPE_ALIASES.scala.includes(
104
+ String(language || "").toLowerCase(),
105
+ );
106
+ }
107
+
108
+ /**
109
+ * True when scalasem is turned off, through CDXGEN_SCALASEM_DISABLE or the
110
+ * --no-scalasem flag, which yargs carries as `scalasem: false`. Library code never reads
111
+ * process.argv for this.
112
+ *
113
+ * @param {Object} options CLI options.
114
+ * @returns {boolean}
115
+ */
116
+ export function scalasemDisabled(options = {}) {
117
+ const raw = (
118
+ readEnvironmentVariable("CDXGEN_SCALASEM_DISABLE") || ""
119
+ ).toLowerCase();
120
+ return (
121
+ raw === "1" || raw === "true" || raw === "all" || options.scalasem === false
122
+ );
123
+ }
124
+
125
+ /**
126
+ * The scalasem entry point: SCALASEM_CMD wins, then the scalasem.js inside
127
+ * the installed atom-parsetools package.
128
+ *
129
+ * @returns {string|undefined}
130
+ */
131
+ export function resolveScalasemCommand() {
132
+ return (
133
+ readEnvironmentVariable("SCALASEM_CMD") || parsetoolsFile("scalasem.js")
134
+ );
135
+ }
136
+
137
+ function scalasemTimeoutMs(options = {}) {
138
+ const raw =
139
+ options.scalasemTimeoutMs ??
140
+ readEnvironmentVariable("CDXGEN_SCALASEM_TIMEOUT");
141
+ const parsed = Number.parseInt(String(raw ?? ""), 10);
142
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
143
+ }
144
+
145
+ // Time scalasem gets to stop its builds and itself after its own limit,
146
+ // before cdxgen stops it.
147
+ const TIMEOUT_GRACE_MS = 30_000;
148
+
149
+ // The build files that identify a Scala project, for a source argument that
150
+ // names the working directory relative to itself.
151
+ const SCALA_BUILD_MARKERS = [
152
+ "build.sbt",
153
+ "build.sc",
154
+ "build.mill",
155
+ "pom.xml",
156
+ "project.scala",
157
+ ];
158
+
159
+ /**
160
+ * Where the scalasem report is persisted. An absolute --semantics-slices-file
161
+ * keeps its directory; anything else lands in the evinse output directory, the
162
+ * same rule the atom slice writer follows.
163
+ *
164
+ * @param {Object} options CLI options carrying `output` and `semanticsSlicesFile`.
165
+ * @returns {string} Absolute report path.
166
+ */
167
+ export function scalasemOutputFile(options = {}) {
168
+ const requested = options.semanticsSlicesFile || "semantics.slices.json";
169
+ // Comparing with resolve() missed Windows paths written with forward slashes
170
+ // or without a drive.
171
+ if (isAbsolute(requested)) {
172
+ return requested;
173
+ }
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-"));
188
+ }
189
+ return join(outputDir, basename(requested));
190
+ }
191
+
192
+ function readScalasemReport(reportFile) {
193
+ if (!reportFile || !safeExistsSync(reportFile)) {
194
+ return undefined;
195
+ }
196
+ try {
197
+ const parsed = JSON.parse(readFileSync(reportFile, "utf-8"));
198
+ return parsed && typeof parsed === "object" ? parsed : undefined;
199
+ } catch (_err) {
200
+ return undefined;
201
+ }
202
+ }
203
+
204
+ function isNewerThan(file, otherFile) {
205
+ try {
206
+ return statSync(file).mtimeMs >= statSync(otherFile).mtimeMs;
207
+ } catch (_err) {
208
+ // Without both timestamps the project checks alone decide.
209
+ return true;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * A version 1 semantics slice (the shape atom's scalasem wrote before schema
215
+ * version 2): keyed by .scala files with `usedTypes`, no `_meta.schemaVersion`.
216
+ *
217
+ * @param {Object} parsed Candidate slice.
218
+ * @returns {boolean}
219
+ */
220
+ export function isV1SemanticsSlice(parsed) {
221
+ if (!parsed || parsed._meta?.schemaVersion) {
222
+ return false;
223
+ }
224
+ return Object.keys(parsed).some((key) => key.endsWith(".scala"));
225
+ }
226
+
227
+ /**
228
+ * Whether a version 1 slice describes this project: it names no project, so
229
+ * every source file it lists must exist under the project directory.
230
+ */
231
+ function isV1SliceOfProject(slice, projectDir) {
232
+ const files = Object.keys(slice).filter((key) => key.endsWith(".scala"));
233
+ return (
234
+ files.length > 0 &&
235
+ files.every((file) => {
236
+ const path = resolve(projectDir, file);
237
+ return path.startsWith(`${projectDir}${sep}`) && safeExistsSync(path);
238
+ })
239
+ );
240
+ }
241
+
242
+ /**
243
+ * 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.
246
+ *
247
+ * Failures are visible rather than silent: the reason is printed once, the
248
+ * run's diagnostics reach the caller as `cdx:scalasem:diagnostic` properties,
249
+ * and --fail-on-error claims the exit status. A version 1 slice of this
250
+ * project, at the path the user named or at the report path, is returned
251
+ * separately and never overwritten; the fresh report then goes to a
252
+ * temporary file.
253
+ *
254
+ * @param {string} src Project directory.
255
+ * @param {Object} options CLI options.
256
+ * @returns {Object|undefined} `{ report, reportFile, v1Slice, metadataProperties }`,
257
+ * or undefined when the analyzer is disabled.
258
+ */
259
+ export function analyzeScalaProject(src, options = {}) {
260
+ if (scalasemDisabled(options)) {
261
+ console.log(
262
+ "scalasem: disabled; skipping the Scala semantic evidence. Unset CDXGEN_SCALASEM_DISABLE or drop --no-scalasem to re-enable it.",
263
+ );
264
+ return undefined;
265
+ }
266
+ const analysisDir = resolvePluginSourceDir(src, SCALA_BUILD_MARKERS);
267
+ const reportFile = scalasemOutputFile(options);
268
+ const candidates = [
269
+ ...new Set(
270
+ [
271
+ options.semanticsSlicesFile
272
+ ? resolve(options.semanticsSlicesFile)
273
+ : undefined,
274
+ reportFile,
275
+ ].filter(Boolean),
276
+ ),
277
+ ];
278
+ let v1Slice;
279
+ let v1File;
280
+ for (const candidate of candidates) {
281
+ const existing = readScalasemReport(candidate);
282
+ if (existing?._meta?.schemaVersion === "scalasem/2") {
283
+ if (
284
+ resolve(existing._meta.projectPath || "") === analysisDir &&
285
+ isNewerThan(candidate, options.input)
286
+ ) {
287
+ if (DEBUG_MODE) {
288
+ console.log(`Reusing the scalasem report "${candidate}".`);
289
+ }
290
+ return {
291
+ report: existing,
292
+ reportFile: candidate,
293
+ v1Slice: undefined,
294
+ metadataProperties: scalasemMetadataProperties(existing),
295
+ };
296
+ }
297
+ } else if (
298
+ !v1Slice &&
299
+ isV1SemanticsSlice(existing) &&
300
+ isV1SliceOfProject(existing, analysisDir)
301
+ ) {
302
+ v1Slice = existing;
303
+ v1File = candidate;
304
+ }
305
+ }
306
+ return runScalasem(analysisDir, options, reportFile, v1Slice, v1File);
307
+ }
308
+
309
+ function scalasemRunFailure(code, message, options, args, status) {
310
+ console.warn(`scalasem: ${message}`);
311
+ deferFailOnError(options, {
312
+ ecosystem: "scala",
313
+ tool: "scalasem",
314
+ detail: message,
315
+ exitCode: typeof status === "number" ? status : undefined,
316
+ command: `${process.execPath} ${args.join(" ")}`,
317
+ });
318
+ return [{ code }];
319
+ }
320
+
321
+ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
322
+ const scalasem = options.scalasemCommand || resolveScalasemCommand();
323
+ if (!scalasem) {
324
+ const diagnostics = scalasemRunFailure(
325
+ "scalasem-missing",
326
+ "analyzer not found, so the BOM carries no Scala semantic evidence. Install @appthreat/atom-parsetools or set SCALASEM_CMD.",
327
+ options,
328
+ [],
329
+ );
330
+ return {
331
+ report: undefined,
332
+ reportFile: undefined,
333
+ v1Slice,
334
+ metadataProperties: scalasemMetadataProperties(undefined, diagnostics),
335
+ };
336
+ }
337
+ // scalasem writes into a directory of this run's own, so a report found
338
+ // there afterwards is this run's. Writing to the requested path directly
339
+ // left only the file's modification time to tell a fresh report from one an
340
+ // earlier run (or another project's run) had left there, and a report
341
+ // written within the millisecond the run started passed for fresh.
342
+ const runDir = safeMkdtempSync(join(getTmpDir(), "scalasem-"));
343
+ const runReportFile = join(runDir, "scalasem.slices.json");
344
+ const args = [scalasem, analysisDir, runReportFile];
345
+ // The build runs the project's sbt definitions
346
+ if (!isProjectToolingAllowed(options)) {
347
+ args.push("--no-build");
348
+ }
349
+ if (options.scalasemIncludeTests && !options.requiredOnly) {
350
+ args.push("--include-tests");
351
+ }
352
+ if (DEBUG_MODE) {
353
+ console.log("Executing", process.execPath, args.join(" "));
354
+ }
355
+ const timeoutMs = scalasemTimeoutMs(options);
356
+ const startedAt = Date.now();
357
+ // scalasem enforces the limit itself, so the builds it started stop with
358
+ // it; cdxgen's own timeout is the backstop. Naming cdxgen as the supervisor
359
+ // 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
+ });
371
+ const elapsedMs = Date.now() - startedAt;
372
+ const wroteReport = safeExistsSync(runReportFile);
373
+ const written = wroteReport ? readScalasemReport(runReportFile) : undefined;
374
+ const usable = written?._meta?.schemaVersion === "scalasem/2";
375
+ const report = usable ? written : undefined;
376
+ // scalasem stops itself at the limit and exits with an error and no
377
+ // report; cdxgen's own timeout is only the backstop. A run that ends without
378
+ // a usable report once the limit has passed overran it, however it exited.
379
+ const timedOut =
380
+ result?.error?.code === "ETIMEDOUT" || (!usable && elapsedMs >= timeoutMs);
381
+ let diagnostics;
382
+ if (timedOut) {
383
+ diagnostics = scalasemRunFailure(
384
+ "scalasem-timeout",
385
+ `did not finish within ${formatDuration(timeoutMs)} and was stopped, so the BOM carries no Scala semantic evidence. Allow more time with CDXGEN_SCALASEM_TIMEOUT (milliseconds).`,
386
+ options,
387
+ args,
388
+ );
389
+ } else if (result?.error) {
390
+ diagnostics = scalasemRunFailure(
391
+ "scalasem-not-runnable",
392
+ `could not be started (${result.error.message}). Check SCALASEM_CMD or the atom-parsetools install.`,
393
+ options,
394
+ args,
395
+ );
396
+ } else if (!wroteReport) {
397
+ diagnostics = scalasemRunFailure(
398
+ "scalasem-no-report",
399
+ `wrote no report for ${analysisDir} (exit status ${result?.status ?? "unknown"}); run with CDXGEN_DEBUG_MODE=debug to see its output.`,
400
+ options,
401
+ args,
402
+ result?.status,
403
+ );
404
+ } else if (!written) {
405
+ diagnostics = scalasemRunFailure(
406
+ "scalasem-invalid-report",
407
+ `wrote a report for ${analysisDir} that is not valid JSON; run with CDXGEN_DEBUG_MODE=debug to see its output.`,
408
+ options,
409
+ args,
410
+ result?.status,
411
+ );
412
+ } else if (!usable) {
413
+ diagnostics = scalasemRunFailure(
414
+ "scalasem-old-report",
415
+ "wrote a version 1 report. Scala evidence needs atom-parsetools 1.10.0 or later.",
416
+ options,
417
+ args,
418
+ result?.status,
419
+ );
420
+ } else {
421
+ diagnostics = report._meta.diagnostics || [];
422
+ if (result?.status !== 0) {
423
+ diagnostics = [
424
+ ...diagnostics,
425
+ ...scalasemRunFailure(
426
+ "scalasem-exit-status",
427
+ `exited with status ${result?.status}; its report is used, and may be incomplete.`,
428
+ options,
429
+ args,
430
+ result?.status,
431
+ ),
432
+ ];
433
+ }
434
+ const summary = summarizeDiagnostics(report._meta.diagnostics);
435
+ if (summary) {
436
+ console.warn(
437
+ `scalasem: the report is degraded: ${summary}. The cdx:scalasem:diagnostic:* metadata properties of the BOM say where.`,
438
+ );
439
+ }
440
+ }
441
+ if (DEBUG_MODE && !usable && (result?.stdout || result?.stderr)) {
442
+ console.log(result.stdout, result.stderr);
443
+ }
444
+ // Only a usable report replaces the one at the requested path, and a
445
+ // version 1 slice there stays as the user left it.
446
+ let reportFile;
447
+ if (usable && v1File !== requestedFile) {
448
+ try {
449
+ safeMkdirSync(dirname(requestedFile), { recursive: true });
450
+ safeCopyFileSync(runReportFile, requestedFile);
451
+ reportFile = requestedFile;
452
+ } catch (err) {
453
+ console.warn(
454
+ `scalasem: the report could not be kept at ${requestedFile} (${err.message}); this run still uses it.`,
455
+ );
456
+ }
457
+ }
458
+ safeRmSync(runDir, { recursive: true, force: true });
459
+ return {
460
+ report,
461
+ reportFile,
462
+ v1Slice,
463
+ metadataProperties: scalasemMetadataProperties(report, diagnostics),
464
+ };
465
+ }
466
+
467
+ function summarizeDiagnostics(diagnostics = []) {
468
+ const byCode = new Map();
469
+ for (const diagnostic of diagnostics || []) {
470
+ if (diagnostic?.code) {
471
+ byCode.set(
472
+ diagnostic.code,
473
+ (byCode.get(diagnostic.code) || 0) + (diagnostic.count ?? 1),
474
+ );
475
+ }
476
+ }
477
+ return [...byCode.entries()]
478
+ .sort(([left], [right]) => left.localeCompare(right))
479
+ .map(([code, count]) => `${code} (${count})`)
480
+ .join(", ");
481
+ }
482
+
483
+ /**
484
+ * The `cdx:scalasem:*` metadata properties of one run: what produced the
485
+ * report, and what limited it.
486
+ *
487
+ * @param {Object|undefined} report Parsed scalasem report, when one exists.
488
+ * @param {Object[]} diagnostics Run diagnostics, from the report or the spawn.
489
+ * @returns {Object[]} Metadata properties.
490
+ */
491
+ export function scalasemMetadataProperties(
492
+ report,
493
+ diagnostics = report?._meta?.diagnostics || [],
494
+ ) {
495
+ const properties = [];
496
+ const meta = report?._meta || {};
497
+ appendUniqueProperty(
498
+ properties,
499
+ "cdx:scalasem:schemaVersion",
500
+ meta.schemaVersion,
501
+ );
502
+ appendUniqueProperty(
503
+ properties,
504
+ "cdx:scalasem:factsSource",
505
+ sortedCsv(meta.generatedFrom),
506
+ );
507
+ appendUniqueProperty(
508
+ properties,
509
+ "cdx:scalasem:compilerSource",
510
+ sortedCsv((meta.compilers || []).map((compiler) => compiler.source)),
511
+ );
512
+ appendUniqueProperty(
513
+ properties,
514
+ "cdx:scalasem:scalaVersions",
515
+ sortedCsv((meta.compilers || []).map((compiler) => compiler.version)),
516
+ );
517
+ appendUniqueProperty(
518
+ properties,
519
+ "cdx:scalasem:platforms",
520
+ sortedCsv(meta.platforms),
521
+ );
522
+ appendUniqueProperty(
523
+ properties,
524
+ "cdx:scalasem:filesAnalyzed",
525
+ meta.counts?.files,
526
+ );
527
+ if (meta.truncated || diagnostics.length) {
528
+ appendUniqueProperty(properties, "cdx:scalasem:degraded", "true");
529
+ }
530
+ const byCode = {};
531
+ for (const diagnostic of diagnostics) {
532
+ if (!diagnostic?.code) {
533
+ continue;
534
+ }
535
+ byCode[diagnostic.code] =
536
+ (byCode[diagnostic.code] || 0) + (diagnostic.count ?? 1);
537
+ }
538
+ for (const code of Object.keys(byCode).sort()) {
539
+ appendUniqueProperty(
540
+ properties,
541
+ `cdx:scalasem:diagnostic:${code}`,
542
+ byCode[code],
543
+ );
544
+ }
545
+ return properties;
546
+ }
547
+
548
+ export function sortedCsv(values) {
549
+ const items = values instanceof Set ? [...values] : values || [];
550
+ const filtered = [...new Set(items.filter(Boolean))].sort();
551
+ return filtered.length ? filtered.join(",") : undefined;
552
+ }
553
+
554
+ /**
555
+ * The platform a purl's artifact name targets: `_sjs1` is Scala.js,
556
+ * `_native0.x` is Scala Native, and neither is the JVM.
557
+ *
558
+ * @param {string} purl Package URL.
559
+ * @returns {"jvm"|"js"|"native"} Platform key.
560
+ */
561
+ export function scalaPurlPlatform(purl) {
562
+ const name = purlPurlName(purl);
563
+ if (!name) {
564
+ return "jvm";
565
+ }
566
+ if (/_(sjs\d+(?:\.\d+)?)$/.test(name)) {
567
+ return "js";
568
+ }
569
+ if (/_(native\d+(?:\.\d+)*)$/.test(name)) {
570
+ return "native";
571
+ }
572
+ return "jvm";
573
+ }
574
+
575
+ function purlPurlName(purl) {
576
+ const match = /pkg:[^/]+\/(?:[^/@?]+\/)?([^/@?]+)/.exec(purl || "");
577
+ if (!match) {
578
+ return undefined;
579
+ }
580
+ try {
581
+ return decodeURIComponent(match[1]);
582
+ } catch (_err) {
583
+ return match[1];
584
+ }
585
+ }
586
+
587
+ /**
588
+ * The class and package index that joins scalasem symbols to component purls.
589
+ *
590
+ * Symbols are indexed from three sources in priority order: the classpath the
591
+ * report recorded (1), the `internal:Namespaces` properties of the components
592
+ * (2), and the jar namespace map cdxgen writes beside the BOM (3). A symbol an
593
+ * earlier source already attributed is never re-attributed by a later one.
594
+ *
595
+ * A jar answers for the classes it ships and for the packages it ships classes
596
+ * in directly, never for the parents of those packages: a library with one
597
+ * class under `scala.collection.compat` owns neither `scala.collection` nor
598
+ * `scala`. The JDK has no component, and neither do the project's own classes.
599
+ */
600
+ export class ScalaJoinIndex {
601
+ constructor() {
602
+ this.classPurls = new Map();
603
+ this.packagePurls = new Map();
604
+ this.sourceBySymbol = new Map();
605
+ this.purlsBySource = new Map();
606
+ this.namespacesByPurl = new Map();
607
+ this.projectClasses = new Set();
608
+ this.projectPackages = new Set();
609
+ }
610
+
611
+ hasNamespaces(purl) {
612
+ return (this.purlsBySource.get(purl) || 0) > 0;
613
+ }
614
+
615
+ /**
616
+ * Index the classes of one library: each class under its dotted name (a
617
+ * nested `Outer$Inner` also as `Outer.Inner`, an object `Foo$` as `Foo`),
618
+ * and the package each one sits in.
619
+ *
620
+ * @param {string} purl The owning component purl.
621
+ * @param {string[]} names Class names from the jar or the namespace property.
622
+ * @param {number} source The source priority, 1 to 3.
623
+ */
624
+ addNamespaces(purl, names, source) {
625
+ if (!purl || this.hasNamespaces(purl)) {
626
+ return;
627
+ }
628
+ this.purlsBySource.set(purl, source);
629
+ const indexed = [];
630
+ this.namespacesByPurl.set(purl, indexed);
631
+ for (const name of names || []) {
632
+ const symbol = String(name || "").trim();
633
+ if (!symbol || symbol.includes(" ") || symbol.includes("/")) {
634
+ continue;
635
+ }
636
+ indexed.push(symbol);
637
+ const dollar = symbol.indexOf("$");
638
+ const topLevel = dollar > 0 ? symbol.slice(0, dollar) : symbol;
639
+ this.addSymbol(this.classPurls, topLevel, purl, source);
640
+ if (dollar > 0) {
641
+ const nested = symbol.replaceAll("$", ".").replace(/\.+$/, "");
642
+ if (nested !== topLevel) {
643
+ this.addSymbol(this.classPurls, nested, purl, source);
644
+ }
645
+ }
646
+ const lastDot = topLevel.lastIndexOf(".");
647
+ if (lastDot > 0) {
648
+ this.addSymbol(
649
+ this.packagePurls,
650
+ topLevel.slice(0, lastDot),
651
+ purl,
652
+ source,
653
+ );
654
+ }
655
+ }
656
+ }
657
+
658
+ addSymbol(map, symbol, purl, source) {
659
+ const key = `${map === this.classPurls ? "c" : "p"}:${symbol}`;
660
+ const existing = this.sourceBySymbol.get(key);
661
+ if (existing !== undefined && existing < source) {
662
+ // A higher-priority source (lower number) already attributed it.
663
+ return;
664
+ }
665
+ if (existing !== undefined && existing > source) {
666
+ map.set(symbol, new Set());
667
+ }
668
+ this.sourceBySymbol.set(key, source);
669
+ if (!map.has(symbol)) {
670
+ map.set(symbol, new Set());
671
+ }
672
+ map.get(symbol).add(purl);
673
+ }
674
+
675
+ /**
676
+ * Record the classes the project defines, so that references to them, and
677
+ * to packages only the project fills, never join a library.
678
+ *
679
+ * @param {Object} report Parsed scalasem report.
680
+ */
681
+ addProjectDefinitions(report) {
682
+ for (const [fileKey, fileEntry] of Object.entries(report || {})) {
683
+ if (!isReportFileKey(fileKey) || !Array.isArray(fileEntry?.definitions)) {
684
+ continue;
685
+ }
686
+ for (const definition of fileEntry.definitions) {
687
+ if (
688
+ !["class", "object", "trait", "enum"].includes(definition?.kind) ||
689
+ !definition.owner ||
690
+ !definition.name
691
+ ) {
692
+ continue;
693
+ }
694
+ this.projectClasses.add(`${definition.owner}.${definition.name}`);
695
+ this.projectPackages.add(definition.owner);
696
+ }
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Whether a symbol belongs to code the BOM has no component for: the JDK,
702
+ * or the project itself.
703
+ */
704
+ isUnowned(symbol) {
705
+ if (JDK_OWNER_PREFIXES.some((prefix) => symbol.startsWith(prefix))) {
706
+ return true;
707
+ }
708
+ const parts = symbol.replace(/\$+$/, "").split(".");
709
+ for (let i = parts.length; i > 1; i--) {
710
+ if (this.projectClasses.has(parts.slice(0, i).join("."))) {
711
+ return true;
712
+ }
713
+ }
714
+ return false;
715
+ }
716
+
717
+ /**
718
+ * The libraries that ship the class a symbol names: the longest dotted
719
+ * prefix of the symbol that is a class name, for a member or a nested type.
720
+ */
721
+ classOwners(symbol) {
722
+ const parts = symbol.replace(/\$+$/, "").split(".");
723
+ for (let i = parts.length; i > 0; i--) {
724
+ const owners = this.classPurls.get(parts.slice(0, i).join("."));
725
+ if (owners?.size) {
726
+ return owners;
727
+ }
728
+ }
729
+ return undefined;
730
+ }
731
+
732
+ /**
733
+ * The libraries that ship classes directly in the package a symbol names or
734
+ * sits in. A package the project also fills is ambiguous and joins nothing,
735
+ * and `javax.` names join only through an exact class, since the JDK ships
736
+ * most of them.
737
+ */
738
+ packageOwners(symbol) {
739
+ if (symbol.startsWith(JDK_EXTENSION_PREFIX)) {
740
+ return undefined;
741
+ }
742
+ 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);
746
+ if (owners?.size) {
747
+ return this.projectPackages.has(name) ? undefined : owners;
748
+ }
749
+ }
750
+ return undefined;
751
+ }
752
+
753
+ /**
754
+ * The purl owning a symbol: the class first, then the package it sits in,
755
+ * each narrowed to the module's own classpath and the file's platform, and
756
+ * kept only when a single owner remains.
757
+ *
758
+ * @param {string} symbol Fully qualified symbol from the report.
759
+ * @param {string|undefined} platform Platform of the referencing file.
760
+ * @param {Set<string>|undefined} allowedPurls Purls on the module's classpath.
761
+ * @returns {string|undefined} Component purl.
762
+ */
763
+ lookup(symbol, platform, allowedPurls) {
764
+ if (!symbol || this.isUnowned(symbol)) {
765
+ return undefined;
766
+ }
767
+ const byClass = this.narrow(
768
+ this.classOwners(symbol),
769
+ platform,
770
+ allowedPurls,
771
+ );
772
+ if (byClass.length) {
773
+ return byClass.length === 1 ? byClass[0] : undefined;
774
+ }
775
+ const byPackage = this.narrow(
776
+ this.packageOwners(symbol),
777
+ platform,
778
+ allowedPurls,
779
+ );
780
+ return byPackage.length === 1 ? byPackage[0] : undefined;
781
+ }
782
+
783
+ /**
784
+ * The owners a stack is evidence for: every jar that ships the class, since
785
+ * which one served the call cannot be told from the source, or the single
786
+ * package owner when no jar lists the class.
787
+ */
788
+ lookupAll(symbol, platform, allowedPurls) {
789
+ if (!symbol || this.isUnowned(symbol)) {
790
+ return [];
791
+ }
792
+ const byClass = this.narrow(
793
+ this.classOwners(symbol),
794
+ platform,
795
+ allowedPurls,
796
+ );
797
+ if (byClass.length) {
798
+ return byClass;
799
+ }
800
+ const single = this.lookup(symbol, platform, allowedPurls);
801
+ return single ? [single] : [];
802
+ }
803
+
804
+ /**
805
+ * Narrow candidate owners to what the referencing file can use. A module
806
+ * compiles against its own classpath: a library on it wins, and a library
807
+ * only other modules compile against does not qualify. A library on no
808
+ * module's classpath stays a candidate, since the report cannot list every
809
+ * jar (a locally published snapshot, say). The file's platform then picks
810
+ * among the JVM, Scala.js and Scala Native variants of one library.
811
+ */
812
+ narrow(candidates, platform, allowedPurls) {
813
+ let pool = [...(candidates || [])];
814
+ if (!pool.length) {
815
+ return pool;
816
+ }
817
+ let onClasspath = false;
818
+ if (allowedPurls?.size) {
819
+ const inModule = pool.filter((purl) => allowedPurls.has(purl));
820
+ onClasspath = inModule.length > 0;
821
+ pool = onClasspath
822
+ ? inModule
823
+ : pool.filter((purl) => !this.classpathPurls?.has(purl));
824
+ }
825
+ if (platform && pool.length) {
826
+ const samePlatform = pool.filter(
827
+ (purl) => scalaPurlPlatform(purl) === platform,
828
+ );
829
+ if (samePlatform.length || !onClasspath) {
830
+ pool = samePlatform;
831
+ }
832
+ } else if (pool.length > 1) {
833
+ // Without a platform the JVM speaks for a library built for several.
834
+ const jvm = pool.filter((purl) => scalaPurlPlatform(purl) === "jvm");
835
+ if (jvm.length) {
836
+ pool = jvm;
837
+ }
838
+ }
839
+ return pool;
840
+ }
841
+ }
842
+
843
+ /**
844
+ * Whether a report key names a source file entry. The schema keys file
845
+ * entries by their relative path, for Scala sources, scripts and the Java
846
+ * sources of a mixed module alike.
847
+ */
848
+ export function isReportFileKey(key) {
849
+ return /\.(scala|sc|java)$/.test(key || "");
850
+ }
851
+
852
+ function decodeSafe(value) {
853
+ if (!value) {
854
+ return "";
855
+ }
856
+ try {
857
+ return decodeURIComponent(value);
858
+ } catch (_err) {
859
+ return value;
860
+ }
861
+ }
862
+
863
+ // The Scala binary versions a component was built for: the suffix its purl
864
+ // keeps, or every cdx:scala:compilerVersion it carries, since one component can
865
+ // stand for the same library in builds of several Scala versions.
866
+ function componentBinaryVersions(component, coordinate) {
867
+ if (coordinate.binaryVersion) {
868
+ return new Set([coordinate.binaryVersion]);
869
+ }
870
+ return new Set(
871
+ (component.properties || [])
872
+ .filter((property) => property.name === "cdx:scala:compilerVersion")
873
+ .map((property) => property.value)
874
+ .filter(Boolean),
875
+ );
876
+ }
877
+
878
+ /**
879
+ * Index the BOM components by their normalized Scala coordinates, both with
880
+ * and without the version, so a classpath jar, a namespace property and a map
881
+ * 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.
884
+ */
885
+ export function componentCoordinateIndex(components = []) {
886
+ const byVersion = new Map();
887
+ const withoutVersion = new Map();
888
+ for (const component of components) {
889
+ const coordinate = scalaCoordinateOfPurl(component?.purl);
890
+ if (!coordinate) {
891
+ continue;
892
+ }
893
+ const [type, group, artifactBase, platformSuffix] =
894
+ coordinate.key.split(":");
895
+ const entry = {
896
+ component,
897
+ binaryVersions: componentBinaryVersions(component, coordinate),
898
+ };
899
+ if (!byVersion.has(coordinate.key)) {
900
+ byVersion.set(coordinate.key, []);
901
+ }
902
+ byVersion.get(coordinate.key).push(entry);
903
+ const key = scalaCoordinateKey({
904
+ type,
905
+ group,
906
+ artifactBase,
907
+ platformSuffix,
908
+ });
909
+ if (!withoutVersion.has(key)) {
910
+ withoutVersion.set(key, []);
911
+ }
912
+ withoutVersion.get(key).push(entry);
913
+ }
914
+ return { byVersion, withoutVersion };
915
+ }
916
+
917
+ function pickByBinaryVersion(entries, binaryVersion) {
918
+ if (!entries?.length) {
919
+ return [];
920
+ }
921
+ if (binaryVersion) {
922
+ const same = entries.filter((entry) =>
923
+ entry.binaryVersions.has(binaryVersion),
924
+ );
925
+ if (same.length) {
926
+ return same;
927
+ }
928
+ const unknown = entries.filter((entry) => !entry.binaryVersions.size);
929
+ if (unknown.length) {
930
+ return unknown;
931
+ }
932
+ return [];
933
+ }
934
+ return entries;
935
+ }
936
+
937
+ /**
938
+ * The component a report classpath entry belongs to. Coordinates are
939
+ * normalized (`upickle_sjs1_3` and the purl name `upickle_sjs1` share a key),
940
+ * the entry's version and Scala binary version win, and a versionless match
941
+ * must have a single owner. A jar the BOM does not hold matches nothing: no
942
+ * component is invented.
943
+ *
944
+ * @returns {{component: Object, exact: boolean}|undefined} The component, and
945
+ * whether its version is the one on the classpath.
946
+ */
947
+ export function matchClasspathEntry(
948
+ entry,
949
+ { byVersion, withoutVersion },
950
+ platform,
951
+ ) {
952
+ const parsed = parseScalaArtifact(entry?.artifact);
953
+ const parts = {
954
+ group: entry?.group,
955
+ artifactBase: parsed.artifactBase,
956
+ platformSuffix: parsed.platformSuffix,
957
+ };
958
+ const exact = pickByBinaryVersion(
959
+ byVersion.get(scalaCoordinateKey({ ...parts, version: entry?.version })),
960
+ parsed.binaryVersion,
961
+ );
962
+ if (exact.length === 1) {
963
+ return { component: exact[0].component, exact: true };
964
+ }
965
+ if (exact.length > 1) {
966
+ return undefined;
967
+ }
968
+ const candidates = pickByBinaryVersion(
969
+ withoutVersion.get(scalaCoordinateKey(parts)),
970
+ parsed.binaryVersion,
971
+ );
972
+ const matching = candidates.filter(
973
+ (candidate) => scalaPurlPlatform(candidate.component.purl) === platform,
974
+ );
975
+ const pool = matching.length ? matching : candidates;
976
+ return pool.length === 1
977
+ ? { component: pool[0].component, exact: false }
978
+ : undefined;
979
+ }
980
+
981
+ /**
982
+ * Build the symbol index from the three sources, reading each classpath jar
983
+ * once, and only for components that carry no namespaces yet.
984
+ *
985
+ * @param {Object} report Parsed scalasem report.
986
+ * @param {Object[]} components BOM components.
987
+ * @param {Object} jarNSMapping The jar namespace map cdxgen writes beside the BOM.
988
+ * @returns {Promise<ScalaJoinIndex>}
989
+ */
990
+ export async function buildScalaJoinIndex(report, components, jarNSMapping) {
991
+ const index = new ScalaJoinIndex();
992
+ index.addProjectDefinitions(report);
993
+ index.exactPurls = new Set();
994
+ const coordinateIndex = componentCoordinateIndex(components);
995
+ const purlsWithNamespaces = new Set(
996
+ components
997
+ .filter((component) =>
998
+ (component.properties || []).some(
999
+ (property) => property.name === "internal:Namespaces",
1000
+ ),
1001
+ )
1002
+ .map((component) => component.purl),
1003
+ );
1004
+ const readJars = new Set();
1005
+ for (const module of report.modules || []) {
1006
+ for (const entry of module.classpath || []) {
1007
+ const matched = matchClasspathEntry(
1008
+ entry,
1009
+ coordinateIndex,
1010
+ module.platform,
1011
+ );
1012
+ const purl = matched?.component?.purl;
1013
+ if (!purl || purlsWithNamespaces.has(purl) || index.hasNamespaces(purl)) {
1014
+ continue;
1015
+ }
1016
+ if (!entry.path || readJars.has(entry.path)) {
1017
+ continue;
1018
+ }
1019
+ readJars.add(entry.path);
1020
+ if (!safeExistsSync(entry.path)) {
1021
+ continue;
1022
+ }
1023
+ let classes = [];
1024
+ try {
1025
+ classes = await getJarClasses(entry.path);
1026
+ } catch (_err) {
1027
+ classes = [];
1028
+ }
1029
+ if (classes.length) {
1030
+ index.addNamespaces(purl, classes, 1);
1031
+ if (matched.exact) {
1032
+ index.exactPurls.add(purl);
1033
+ }
1034
+ }
1035
+ }
1036
+ }
1037
+ for (const component of components) {
1038
+ if (purlsWithNamespaces.has(component.purl)) {
1039
+ const namespaces = (component.properties || [])
1040
+ .filter((property) => property.name === "internal:Namespaces")
1041
+ .flatMap((property) => (property.value || "").split("\n"));
1042
+ index.addNamespaces(component.purl, namespaces, 2);
1043
+ }
1044
+ }
1045
+ for (const mapPurl of Object.keys(jarNSMapping || {})) {
1046
+ const entry = jarNSMapping[mapPurl] || {};
1047
+ if (!Array.isArray(entry.namespaces) || !entry.namespaces.length) {
1048
+ continue;
1049
+ }
1050
+ const segments = /^pkg:[^/]+\/(?:([^/@?]+)\/)?([^@?]+)@([^?#]*)/.exec(
1051
+ mapPurl,
1052
+ );
1053
+ if (!segments) {
1054
+ continue;
1055
+ }
1056
+ const matched = matchClasspathEntry(
1057
+ {
1058
+ group: decodeSafe(segments[1]),
1059
+ artifact: decodeSafe(segments[2]),
1060
+ version: decodeSafe(segments[3]),
1061
+ },
1062
+ coordinateIndex,
1063
+ scalaPurlPlatform(mapPurl),
1064
+ );
1065
+ if (matched) {
1066
+ index.addNamespaces(matched.component.purl, entry.namespaces, 3);
1067
+ }
1068
+ }
1069
+ return index;
1070
+ }
1071
+
1072
+ // The registry of known algorithm OIDs, shared with the golem converter.
1073
+ const SCALASEM_CRYPTO_OIDS = JSON.parse(
1074
+ readFileSync(join(dirNameStr, "data", "crypto-oid.json"), "utf-8"),
1075
+ );
1076
+ const SCALASEM_OID_KEYS = new Map(
1077
+ Object.keys(SCALASEM_CRYPTO_OIDS).map((key) => [key.toLowerCase(), key]),
1078
+ );
1079
+
1080
+ // Canonical scalasem algorithm spellings that have no key of their own in the
1081
+ // OID registry, mapped to the key that names the same algorithm. Everything
1082
+ // the rules can emit is covered by this table, the key-size dependent AES
1083
+ // family below, or one of the explicit no-OID sets.
1084
+ const SCALASEM_OID_ALIASES = {
1085
+ HS256: "hmacWithSHA256",
1086
+ HS384: "hmacWithSHA384",
1087
+ HS512: "hmacWithSHA512",
1088
+ HmacSHA1: "hmacWithSHA1",
1089
+ HmacSHA224: "hmacWithSHA224",
1090
+ HmacSHA256: "hmacWithSHA256",
1091
+ HmacSHA384: "hmacWithSHA384",
1092
+ HmacSHA512: "hmacWithSHA512",
1093
+ ES256: "ecdsaWithSHA256",
1094
+ ES384: "ecdsaWithSHA384",
1095
+ ES512: "ecdsaWithSHA512",
1096
+ RS256: "sha256WithRSAEncryption",
1097
+ RS384: "sha384WithRSAEncryption",
1098
+ RS512: "sha512WithRSAEncryption",
1099
+ PS256: "rsassa-pss",
1100
+ PS384: "rsassa-pss",
1101
+ PS512: "rsassa-pss",
1102
+ SHA1withDSA: "dsaWithSha1",
1103
+ SHA1withRSA: "sha1-with-rsa-signature",
1104
+ SHA224withDSA: "dsaWithSha224",
1105
+ SHA256withDSA: "dsaWithSha256",
1106
+ SHA1withECDSA: "ecdsaWithSHA1",
1107
+ SHA224withECDSA: "ecdsaWithSHA224",
1108
+ SHA256withECDSA: "ecdsaWithSHA256",
1109
+ SHA384withECDSA: "ecdsaWithSHA384",
1110
+ SHA512withECDSA: "ecdsaWithSHA512",
1111
+ SHA224withRSA: "sha224WithRSAEncryption",
1112
+ SHA256withRSA: "sha256WithRSAEncryption",
1113
+ SHA384withRSA: "sha384WithRSAEncryption",
1114
+ SHA512withRSA: "sha512WithRSAEncryption",
1115
+ MD2withRSA: "md2WithRSAEncryption",
1116
+ MD5withRSA: "md5WithRSAEncryption",
1117
+ "RSA-OAEP": "id-RSAES-OAEP",
1118
+ "RSASSA-PKCS1-v1_5": "rsaSignature",
1119
+ "SHA256withRSA/PSS": "rsassa-pss",
1120
+ "ChaCha20-Poly1305": "chaCha20Poly1305",
1121
+ X448: "curveX448",
1122
+ PBKDF2WithHmacSHA1: "PBKDF2",
1123
+ PBKDF2WithHmacSHA256: "PBKDF2",
1124
+ PBKDF2WithHmacSHA384: "PBKDF2",
1125
+ PBKDF2WithHmacSHA512: "PBKDF2",
1126
+ PBEWithMD5AndDES: "pbeWithMD5AndDES-CBC",
1127
+ };
1128
+
1129
+ // AES spellings resolve through the key size and mode when both are known,
1130
+ // and to the generic `aes` key otherwise.
1131
+ const SCALASEM_AES_MODE_KEYS = {
1132
+ ECB: "ECB",
1133
+ CBC: "CBC",
1134
+ CFB: "CFB",
1135
+ OFB: "OFB",
1136
+ CTR: "CTR",
1137
+ GCM: "GCM",
1138
+ CCM: "CCM",
1139
+ GMAC: "GMAC",
1140
+ };
1141
+ const SCALASEM_AES_NAMES = new Set([
1142
+ "AES",
1143
+ "AES-CBC",
1144
+ "AES-CTR",
1145
+ "AES-GCM",
1146
+ "AES-KW",
1147
+ ]);
1148
+
1149
+ // Names that never resolve to an OID. Protocols, keystores and random
1150
+ // generators are emitted as their own asset kinds without an OID; the
1151
+ // algorithms listed here have no registry key and become
1152
+ // `cdx:scalasem:crypto:*` properties instead of assets. Ciphers whose key
1153
+ // depends on the mode (Blowfish, DESede, RC2) and the curve algorithms
1154
+ // resolve only when the finding names the mode or the curve, and are
1155
+ // properties otherwise.
1156
+ export const SCALASEM_PROTOCOL_NAMES = new Set([
1157
+ "SSL",
1158
+ "SSLv3",
1159
+ "TLS",
1160
+ "TLSv1",
1161
+ "TLSv1.1",
1162
+ "TLSv1.2",
1163
+ "TLSv1.3",
1164
+ "DTLSv1.2",
1165
+ ]);
1166
+ export const SCALASEM_KEYSTORE_NAMES = new Set([
1167
+ "JKS",
1168
+ "JCEKS",
1169
+ "PKCS12",
1170
+ "BCFKS",
1171
+ ]);
1172
+ // Algorithms that resolve to an OID only together with what the finding
1173
+ // says about them: the mode of a mode-keyed cipher, or the curve.
1174
+ export const SCALASEM_QUALIFIED_ALGORITHMS = new Set([
1175
+ "Blowfish",
1176
+ "DESede",
1177
+ "EC",
1178
+ "ECDH",
1179
+ "ECDSA",
1180
+ "ECIES",
1181
+ "EdDSA",
1182
+ "RC2",
1183
+ "XDH",
1184
+ ]);
1185
+ export const SCALASEM_NO_OID_ALGORITHMS = new Set([
1186
+ "Argon2",
1187
+ "Argon2d",
1188
+ "Argon2i",
1189
+ "Argon2id",
1190
+ "BLAKE2b-512",
1191
+ "BLAKE2s-256",
1192
+ "CMAC",
1193
+ "Camellia",
1194
+ "ChaCha20",
1195
+ "DH",
1196
+ "DRBG",
1197
+ "HKDF",
1198
+ "HMAC",
1199
+ "HmacSHA3-256",
1200
+ "HmacSHA3-512",
1201
+ "MD2withDSA",
1202
+ "MD2withECDSA",
1203
+ "MD5withDSA",
1204
+ "MD5withECDSA",
1205
+ "NativePRNG",
1206
+ "NONEwithECDSA",
1207
+ "NONEwithRSA",
1208
+ "PBEWithHmacSHA256AndAES_128",
1209
+ "PBEWithHmacSHA256AndAES_256",
1210
+ "PBEWithMD5AndTripleDES",
1211
+ "PBEWithSHA1AndDESede",
1212
+ "PBEWithSHA1AndRC4_128",
1213
+ "Poly1305",
1214
+ "SHA-512/224",
1215
+ "SHA-512/256",
1216
+ "SHA1PRNG",
1217
+ "SHA3-256withECDSA",
1218
+ "SHA384withDSA",
1219
+ "SHA512withDSA",
1220
+ "SM3",
1221
+ "SM4",
1222
+ "Whirlpool",
1223
+ "Windows-PRNG",
1224
+ "bcrypt",
1225
+ "none",
1226
+ "scrypt",
1227
+ ]);
1228
+
1229
+ // Ciphers whose registry key names the mode: `blowfishCBC`, `rc2ECB`.
1230
+ const SCALASEM_MODE_KEYED_CIPHERS = {
1231
+ Blowfish: (mode) => `blowfish${mode}`,
1232
+ DESede: (mode) => (mode === "CBC" ? "des-EDE3-CBC" : undefined),
1233
+ RC2: (mode) => `rc2${mode}`,
1234
+ };
1235
+
1236
+ // NIST names of the prime curves, as the registry spells them.
1237
+ const SCALASEM_CURVE_ALIASES = {
1238
+ "p-256": "secp256r1",
1239
+ "p-384": "secp384r1",
1240
+ "p-521": "secp521r1",
1241
+ };
1242
+ const SCALASEM_WEIERSTRASS_ALGORITHMS = new Set([
1243
+ "EC",
1244
+ "ECDH",
1245
+ "ECDSA",
1246
+ "ECIES",
1247
+ ]);
1248
+
1249
+ /**
1250
+ * The registry key of a curve algorithm on a named curve. A key pair or an
1251
+ * ECDSA or ECDH use on a prime curve is identified by that curve. The
1252
+ * Montgomery curves serve key agreement only and the Edwards curves
1253
+ * signatures only, so a curve the algorithm cannot use resolves to nothing.
1254
+ */
1255
+ function curveOidKey(name, curve) {
1256
+ const normalized = String(curve || "").toLowerCase();
1257
+ if (!normalized) {
1258
+ return undefined;
1259
+ }
1260
+ const montgomery = /^(x|curve)(25519|448)$/.exec(normalized);
1261
+ const edwards = /^ed(25519|448)$/.exec(normalized);
1262
+ if (montgomery) {
1263
+ return ["XDH", "ECDH"].includes(name)
1264
+ ? SCALASEM_OID_KEYS.get(
1265
+ montgomery[2] === "25519" ? "x25519" : "curvex448",
1266
+ )
1267
+ : undefined;
1268
+ }
1269
+ if (edwards) {
1270
+ return name === "EdDSA"
1271
+ ? SCALASEM_OID_KEYS.get(`ed${edwards[1]}`)
1272
+ : undefined;
1273
+ }
1274
+ if (!SCALASEM_WEIERSTRASS_ALGORITHMS.has(name)) {
1275
+ return undefined;
1276
+ }
1277
+ return SCALASEM_OID_KEYS.get(
1278
+ SCALASEM_CURVE_ALIASES[normalized] || normalized,
1279
+ );
1280
+ }
1281
+
1282
+ /**
1283
+ * The CycloneDX padding enum value of a JCA padding name.
1284
+ */
1285
+ function paddingOf(padding) {
1286
+ const normalized = String(padding || "").toLowerCase();
1287
+ if (!normalized) {
1288
+ return undefined;
1289
+ }
1290
+ if (normalized.startsWith("oaep")) {
1291
+ return "oaep";
1292
+ }
1293
+ return SCALASEM_PADDING[normalized] || "other";
1294
+ }
1295
+
1296
+ /**
1297
+ * The OID registry key for a canonical scalasem algorithm name, with the key
1298
+ * size, mode, curve or padding when the name needs one. Names that have no
1299
+ * key return undefined.
1300
+ *
1301
+ * @param {string} name Canonical algorithm name.
1302
+ * @param {number} [keySize] Key size in bits, when the report carries one.
1303
+ * @param {string} [mode] Block cipher mode, such as `GCM`.
1304
+ * @param {string} [curve] Elliptic curve name.
1305
+ * @param {string} [padding] JCA padding name.
1306
+ * @returns {string|undefined} Registry key such as `aes256-GCM`.
1307
+ */
1308
+ export function scalasemCryptoOidKey(name, keySize, mode, curve, padding) {
1309
+ if (!name) {
1310
+ return undefined;
1311
+ }
1312
+ if (SCALASEM_AES_NAMES.has(name)) {
1313
+ const modeKey =
1314
+ SCALASEM_AES_MODE_KEYS[mode] ||
1315
+ SCALASEM_AES_MODE_KEYS[name.split("-")[1]];
1316
+ const wrap = name === "AES-KW" || mode === "KW";
1317
+ if (keySize && (modeKey !== undefined || wrap)) {
1318
+ const sizedKey = wrap ? `aes${keySize}-wrap` : `aes${keySize}-${modeKey}`;
1319
+ const hit = SCALASEM_OID_KEYS.get(sizedKey.toLowerCase());
1320
+ if (hit) {
1321
+ return hit;
1322
+ }
1323
+ }
1324
+ return SCALASEM_OID_KEYS.get("aes");
1325
+ }
1326
+ if (SCALASEM_MODE_KEYED_CIPHERS[name]) {
1327
+ const key = mode && SCALASEM_MODE_KEYED_CIPHERS[name](mode.toUpperCase());
1328
+ return key ? SCALASEM_OID_KEYS.get(key.toLowerCase()) : undefined;
1329
+ }
1330
+ if (name === "RSA") {
1331
+ const rsaPadding = paddingOf(padding);
1332
+ if (rsaPadding === "oaep") {
1333
+ return SCALASEM_OID_KEYS.get("id-rsaes-oaep");
1334
+ }
1335
+ if (rsaPadding === "pkcs1v15") {
1336
+ return SCALASEM_OID_KEYS.get("rsaencryption");
1337
+ }
1338
+ }
1339
+ const aliased = SCALASEM_OID_ALIASES[name];
1340
+ if (aliased && SCALASEM_OID_KEYS.has(aliased.toLowerCase())) {
1341
+ return SCALASEM_OID_KEYS.get(aliased.toLowerCase());
1342
+ }
1343
+ if (
1344
+ SCALASEM_WEIERSTRASS_ALGORITHMS.has(name) ||
1345
+ name === "XDH" ||
1346
+ name === "EdDSA"
1347
+ ) {
1348
+ return curveOidKey(name, curve);
1349
+ }
1350
+ return SCALASEM_OID_KEYS.get(name.toLowerCase());
1351
+ }
1352
+
1353
+ function maxOccurrences() {
1354
+ const parsed = Number.parseInt(
1355
+ String(readEnvironmentVariable("CDXGEN_SCALASEM_MAX_OCCURRENCES") ?? ""),
1356
+ 10,
1357
+ );
1358
+ return Number.isFinite(parsed) && parsed > 0
1359
+ ? parsed
1360
+ : DEFAULT_MAX_OCCURRENCES;
1361
+ }
1362
+
1363
+ function isStdlibPurl(purl) {
1364
+ return SCALA_STDLIB_ARTIFACTS.has(
1365
+ parseScalaArtifact(purlPurlName(purl) || "").artifactBase,
1366
+ );
1367
+ }
1368
+
1369
+ /**
1370
+ * The component purls each module's own classpath names, so a file compiled
1371
+ * against one version of a library attributes its uses to that version.
1372
+ */
1373
+ function classpathPurlsByModule(report, coordinateIndex) {
1374
+ const byModule = new Map();
1375
+ for (const module of report.modules || []) {
1376
+ const purls = new Set();
1377
+ for (const entry of module.classpath || []) {
1378
+ const matched = matchClasspathEntry(
1379
+ entry,
1380
+ coordinateIndex,
1381
+ module.platform,
1382
+ );
1383
+ if (matched) {
1384
+ purls.add(matched.component.purl);
1385
+ }
1386
+ }
1387
+ byModule.set(module.id, purls);
1388
+ }
1389
+ return byModule;
1390
+ }
1391
+
1392
+ function allowedPurlsFor(fileEntry, allowedPurlsByModule) {
1393
+ if (!fileEntry?.module) {
1394
+ return undefined;
1395
+ }
1396
+ return allowedPurlsByModule.get(fileEntry.module);
1397
+ }
1398
+
1399
+ /**
1400
+ * Project a scalasem report onto CycloneDX evidence: occurrences for the
1401
+ * joined components, call stacks for the library sinks, namespaces for the
1402
+ * components the classpath join read, and the run's metadata properties.
1403
+ *
1404
+ * @param {Object} report Parsed scalasem report.
1405
+ * @param {Object[]} components BOM components.
1406
+ * @param {Object} options CLI options; `scalaNamespaceMap` carries the jar
1407
+ * namespace map cdxgen wrote beside the BOM.
1408
+ * @returns {Promise<Object>} The evidence maps `createEvinseFile` consumes.
1409
+ */
1410
+ export async function collectScalasemEvidence(
1411
+ report = {},
1412
+ components = [],
1413
+ options = {},
1414
+ ) {
1415
+ const joinIndex = await buildScalaJoinIndex(
1416
+ report,
1417
+ components,
1418
+ options.scalaNamespaceMap || {},
1419
+ );
1420
+ const purlLocationMap = {};
1421
+ const componentPropertiesMap = {};
1422
+ const dataFlowFrames = {};
1423
+ const metadataProperties = scalasemMetadataProperties(report);
1424
+ const cryptoComponentsByRef = new Map();
1425
+ const cryptoGeneratePurls = {};
1426
+ if (options.deep || options.scalasemNamespaces) {
1427
+ attachJoinedNamespaces(joinIndex, components, componentPropertiesMap);
1428
+ }
1429
+ const allowedPurlsByModule = classpathPurlsByModule(
1430
+ report,
1431
+ componentCoordinateIndex(components),
1432
+ );
1433
+ joinIndex.classpathPurls = new Set(
1434
+ [...allowedPurlsByModule.values()].flatMap((purls) => [...purls]),
1435
+ );
1436
+ addUsageEvidence(
1437
+ report,
1438
+ joinIndex,
1439
+ purlLocationMap,
1440
+ componentPropertiesMap,
1441
+ options,
1442
+ allowedPurlsByModule,
1443
+ );
1444
+ addCallStackEvidence(
1445
+ report,
1446
+ joinIndex,
1447
+ dataFlowFrames,
1448
+ allowedPurlsByModule,
1449
+ options,
1450
+ );
1451
+ addCryptoEvidence(
1452
+ report,
1453
+ components,
1454
+ cryptoComponentsByRef,
1455
+ cryptoGeneratePurls,
1456
+ componentPropertiesMap,
1457
+ metadataProperties,
1458
+ options,
1459
+ );
1460
+ addJsModuleEvidence(
1461
+ report,
1462
+ components,
1463
+ purlLocationMap,
1464
+ componentPropertiesMap,
1465
+ metadataProperties,
1466
+ );
1467
+ return {
1468
+ componentPropertiesMap,
1469
+ cryptoComponents: Array.from(cryptoComponentsByRef.values()).sort(
1470
+ (left, right) =>
1471
+ `${left.name}:${left["bom-ref"]}`.localeCompare(
1472
+ `${right.name}:${right["bom-ref"]}`,
1473
+ ),
1474
+ ),
1475
+ cryptoGeneratePurls,
1476
+ dataFlowFrames,
1477
+ joinIndex,
1478
+ metadataProperties,
1479
+ purlLocationMap,
1480
+ };
1481
+ }
1482
+
1483
+ /**
1484
+ * With --deep, publish the namespaces the classpath join read as
1485
+ * `internal:Namespaces` properties, the property the deep Maven flow
1486
+ * attaches, so components from builds that report no namespaces gain them.
1487
+ * Only a jar of the component's own version speaks for its classes.
1488
+ */
1489
+ function attachJoinedNamespaces(joinIndex, components, componentPropertiesMap) {
1490
+ const known = new Set(
1491
+ components
1492
+ .filter((component) =>
1493
+ (component.properties || []).some(
1494
+ (property) => property.name === "internal:Namespaces",
1495
+ ),
1496
+ )
1497
+ .map((component) => component.purl),
1498
+ );
1499
+ for (const [purl, source] of joinIndex.purlsBySource.entries()) {
1500
+ if (source !== 1 || known.has(purl) || !joinIndex.exactPurls?.has(purl)) {
1501
+ continue;
1502
+ }
1503
+ const namespaces = joinIndex.namespacesByPurl.get(purl) || [];
1504
+ if (namespaces.length) {
1505
+ componentPropertiesMap[purl] ??= [];
1506
+ appendUniqueProperty(
1507
+ componentPropertiesMap[purl],
1508
+ "internal:Namespaces",
1509
+ namespaces.join("\n"),
1510
+ );
1511
+ }
1512
+ }
1513
+ }
1514
+
1515
+ /**
1516
+ * Occurrences from the report's references and calls: every symbol and call
1517
+ * owner joined to a component records its source location. Test sources count
1518
+ * only when they were asked for, the Scala runtime is capped at one
1519
+ * occurrence per file, and each component keeps at most a bounded number of
1520
+ * locations, every file it is used in first and further lines after. Call
1521
+ * sites are counted in full, whatever the cap kept.
1522
+ */
1523
+ function addUsageEvidence(
1524
+ report,
1525
+ joinIndex,
1526
+ purlLocationMap,
1527
+ componentPropertiesMap,
1528
+ options,
1529
+ allowedPurlsByModule,
1530
+ ) {
1531
+ const includeTests = options.scalasemIncludeTests === true;
1532
+ const cap = maxOccurrences();
1533
+ const scopesByPurl = {};
1534
+ const platformsByPurl = {};
1535
+ const callSitesByPurl = {};
1536
+ const stdlibFilesByPurl = {};
1537
+ const linesByPurl = new Map();
1538
+ const addOccurrence = (purl, file, line, kind, scope, platform) => {
1539
+ if (!purl || !line) {
1540
+ return;
1541
+ }
1542
+ addSetValue(scopesByPurl, purl, scope);
1543
+ addSetValue(platformsByPurl, purl, platform);
1544
+ if (kind === "call") {
1545
+ callSitesByPurl[purl] = (callSitesByPurl[purl] || 0) + 1;
1546
+ }
1547
+ if (isStdlibPurl(purl)) {
1548
+ stdlibFilesByPurl[purl] ??= new Set();
1549
+ if (stdlibFilesByPurl[purl].has(file)) {
1550
+ return;
1551
+ }
1552
+ stdlibFilesByPurl[purl].add(file);
1553
+ }
1554
+ if (!linesByPurl.has(purl)) {
1555
+ linesByPurl.set(purl, new Map());
1556
+ }
1557
+ const byFile = linesByPurl.get(purl);
1558
+ if (!byFile.has(file)) {
1559
+ byFile.set(file, new Set());
1560
+ }
1561
+ byFile.get(file).add(line);
1562
+ };
1563
+ for (const [fileKey, fileEntry] of Object.entries(report)) {
1564
+ if (
1565
+ !isReportFileKey(fileKey) ||
1566
+ !fileEntry ||
1567
+ typeof fileEntry !== "object"
1568
+ ) {
1569
+ continue;
1570
+ }
1571
+ if (fileEntry.scope === "test" && !includeTests) {
1572
+ continue;
1573
+ }
1574
+ const platform = fileEntry.platform || (fileEntry.platforms || [])[0];
1575
+ const allowed = allowedPurlsFor(fileEntry, allowedPurlsByModule);
1576
+ const scope = fileEntry.scope || "main";
1577
+ for (const reference of fileEntry.references || []) {
1578
+ addOccurrence(
1579
+ joinIndex.lookup(reference.symbol, platform, allowed),
1580
+ fileKey,
1581
+ reference.line,
1582
+ "reference",
1583
+ scope,
1584
+ platform,
1585
+ );
1586
+ }
1587
+ for (const call of fileEntry.calls || []) {
1588
+ addOccurrence(
1589
+ joinIndex.lookup(call.owner, platform, allowed),
1590
+ fileKey,
1591
+ call.line,
1592
+ "call",
1593
+ scope,
1594
+ platform,
1595
+ );
1596
+ }
1597
+ }
1598
+ for (const [purl, byFile] of linesByPurl) {
1599
+ const locations = (purlLocationMap[purl] ??= new Set());
1600
+ const files = [...byFile.entries()].map(([file, lines]) => [
1601
+ file,
1602
+ [...lines].sort((left, right) => left - right),
1603
+ ]);
1604
+ // Every file first, then the further lines of each file in turn.
1605
+ for (let depth = 0; locations.size < cap; depth++) {
1606
+ let added = false;
1607
+ for (const [file, lines] of files) {
1608
+ if (depth < lines.length && locations.size < cap) {
1609
+ locations.add(`${file}#${lines[depth]}`);
1610
+ added = true;
1611
+ }
1612
+ }
1613
+ if (!added) {
1614
+ break;
1615
+ }
1616
+ }
1617
+ }
1618
+ for (const purl of Object.keys(scopesByPurl)) {
1619
+ addPropertyValue(
1620
+ componentPropertiesMap,
1621
+ purl,
1622
+ "cdx:scalasem:usageScopes",
1623
+ sortedCsv(scopesByPurl[purl]),
1624
+ );
1625
+ addPropertyValue(
1626
+ componentPropertiesMap,
1627
+ purl,
1628
+ "cdx:scalasem:platforms",
1629
+ sortedCsv(platformsByPurl[purl]),
1630
+ );
1631
+ if (callSitesByPurl[purl]) {
1632
+ addPropertyValue(
1633
+ componentPropertiesMap,
1634
+ purl,
1635
+ "cdx:scalasem:callSites",
1636
+ callSitesByPurl[purl],
1637
+ );
1638
+ }
1639
+ }
1640
+ }
1641
+
1642
+ /**
1643
+ * Call stacks: each stack the report found becomes one frame list on the
1644
+ * components of the library it ends in, with the entry point first and the
1645
+ * library call last. CycloneDX holds a single call stack per component, and
1646
+ * `framePicker` chooses which of the stacks a component collected to publish.
1647
+ * A library called on the sink's line is reached by the stack too, but a
1648
+ * stack that ends in a call to the component itself is the better evidence:
1649
+ * such a component is offered only its own stacks. Stacks that differ only in
1650
+ * which call of one method into the library they end at are one path; the
1651
+ * method's last call stands for it.
1652
+ */
1653
+ function addCallStackEvidence(
1654
+ report,
1655
+ joinIndex,
1656
+ dataFlowFrames,
1657
+ allowedPurlsByModule,
1658
+ options = {},
1659
+ ) {
1660
+ const includeTests = options.scalasemIncludeTests === true;
1661
+ const ownStacks = {};
1662
+ const reachedStacks = {};
1663
+ for (const stack of report.callStacks || []) {
1664
+ const sink = stack.sink;
1665
+ if (!sink?.owner) {
1666
+ continue;
1667
+ }
1668
+ const fileEntry = report[sink.file];
1669
+ if (fileEntry?.scope === "test" && !includeTests) {
1670
+ continue;
1671
+ }
1672
+ const platform = fileEntry?.platform || (fileEntry?.platforms || [])[0];
1673
+ const allowed = allowedPurlsFor(fileEntry, allowedPurlsByModule);
1674
+ const sinkPurls = new Set(
1675
+ joinIndex.lookupAll(sink.owner, platform, allowed),
1676
+ );
1677
+ const purls = stackTargetPurls(
1678
+ sink,
1679
+ fileEntry,
1680
+ joinIndex,
1681
+ platform,
1682
+ allowed,
1683
+ );
1684
+ if (!purls.length) {
1685
+ continue;
1686
+ }
1687
+ const frames = (stack.frames || [])
1688
+ .filter((frame) => frame?.file)
1689
+ .map((frame) => stackFrame(report, frame));
1690
+ const sinkCovered = frames.some(
1691
+ (frame) => frame.fullFilename === sink.file && frame.line === sink.line,
1692
+ );
1693
+ if (!sinkCovered) {
1694
+ frames.push({
1695
+ package: packageOf(sink.owner),
1696
+ module: sink.owner,
1697
+ function: sink.name || "",
1698
+ line: sink.line,
1699
+ column: undefined,
1700
+ fullFilename: sink.file,
1701
+ });
1702
+ }
1703
+ for (const purl of purls) {
1704
+ const target = sinkPurls.has(purl) ? ownStacks : reachedStacks;
1705
+ target[purl] ??= [];
1706
+ target[purl].push(frames);
1707
+ }
1708
+ }
1709
+ for (const purl of new Set([
1710
+ ...Object.keys(ownStacks),
1711
+ ...Object.keys(reachedStacks),
1712
+ ])) {
1713
+ dataFlowFrames[purl] = onePerPath(ownStacks[purl] || reachedStacks[purl]);
1714
+ }
1715
+ }
1716
+
1717
+ function onePerPath(stacks) {
1718
+ const byPath = new Map();
1719
+ for (const frames of stacks) {
1720
+ const last = frames[frames.length - 1];
1721
+ const key = [
1722
+ ...frames
1723
+ .slice(0, -1)
1724
+ .map((frame) => `${frame.fullFilename}#${frame.line}`),
1725
+ `${last.fullFilename}#${last.module}.${last.function}`,
1726
+ ].join(">");
1727
+ const known = byPath.get(key);
1728
+ if (!known || (known[known.length - 1].line || 0) < (last.line || 0)) {
1729
+ byPath.set(key, frames);
1730
+ }
1731
+ }
1732
+ return [...byPath.values()];
1733
+ }
1734
+
1735
+ /**
1736
+ * One call stack frame in the shape `framePicker` reads. The report names the
1737
+ * method; the definition that encloses the frame's line names its class.
1738
+ */
1739
+ function stackFrame(report, frame) {
1740
+ const owner = enclosingOwner(report[frame.file], frame);
1741
+ return {
1742
+ package: owner ? packageOf(owner) : "",
1743
+ module: owner || "",
1744
+ function: frame.function || "",
1745
+ line: frame.line || undefined,
1746
+ column: frame.column || undefined,
1747
+ fullFilename: frame.file,
1748
+ };
1749
+ }
1750
+
1751
+ function enclosingOwner(fileEntry, frame) {
1752
+ let best;
1753
+ for (const definition of fileEntry?.definitions || []) {
1754
+ if (
1755
+ definition?.kind !== "def" ||
1756
+ definition.name !== frame.function ||
1757
+ !definition.owner ||
1758
+ definition.line > frame.line ||
1759
+ (definition.endLine || definition.line) < frame.line
1760
+ ) {
1761
+ continue;
1762
+ }
1763
+ if (!best || definition.line > best.line) {
1764
+ best = definition;
1765
+ }
1766
+ }
1767
+ return best?.owner;
1768
+ }
1769
+
1770
+ /**
1771
+ * The components a stack is evidence for: the jars that ship the sink class,
1772
+ * plus the libraries called on the sink's own line, which is the terminal hop
1773
+ * the sink call came through. The Scala runtime is ambient and never a
1774
+ * target.
1775
+ */
1776
+ function stackTargetPurls(sink, fileEntry, joinIndex, platform, allowed) {
1777
+ const purls = new Set(joinIndex.lookupAll(sink.owner, platform, allowed));
1778
+ for (const call of fileEntry?.calls || []) {
1779
+ if (call.line !== sink.line || !call.owner) {
1780
+ continue;
1781
+ }
1782
+ for (const purl of joinIndex.lookupAll(call.owner, platform, allowed)) {
1783
+ if (!isStdlibPurl(purl)) {
1784
+ purls.add(purl);
1785
+ }
1786
+ }
1787
+ }
1788
+ return [...purls];
1789
+ }
1790
+
1791
+ function packageOf(className) {
1792
+ const text = String(className || "").replace(/\$+$/, "");
1793
+ const dot = text.lastIndexOf(".");
1794
+ return dot > 0 ? text.slice(0, dot) : "";
1795
+ }
1796
+
1797
+ const SCALASEM_CRYPTO_PRIMITIVES = new Set([
1798
+ "drbg",
1799
+ "mac",
1800
+ "block-cipher",
1801
+ "stream-cipher",
1802
+ "signature",
1803
+ "hash",
1804
+ "pke",
1805
+ "xof",
1806
+ "kdf",
1807
+ "key-agree",
1808
+ "kem",
1809
+ "ae",
1810
+ "combiner",
1811
+ "key-wrap",
1812
+ "other",
1813
+ "unknown",
1814
+ ]);
1815
+
1816
+ const SCALASEM_MODES = new Set([
1817
+ "cbc",
1818
+ "ecb",
1819
+ "ccm",
1820
+ "gcm",
1821
+ "cfb",
1822
+ "ofb",
1823
+ "ctr",
1824
+ ]);
1825
+
1826
+ const SCALASEM_PADDING = {
1827
+ nopadding: "raw",
1828
+ pkcs5padding: "pkcs5",
1829
+ pkcs7padding: "pkcs7",
1830
+ pkcs1padding: "pkcs1v15",
1831
+ oaepwithsha1andmgf1: "oaep",
1832
+ ssl3padding: "other",
1833
+ iso10126padding: "other",
1834
+ };
1835
+
1836
+ // The spellings one purl answers to: sbt components carry the encoded purl and
1837
+ // the decoded bom-ref, and dependency refs use either.
1838
+ function providerAliases(purl, components = []) {
1839
+ if (!purl) {
1840
+ return [];
1841
+ }
1842
+ const component = components.find((candidate) => candidate.purl === purl);
1843
+ return component?.["bom-ref"] && component["bom-ref"] !== purl
1844
+ ? [component["bom-ref"]]
1845
+ : [];
1846
+ }
1847
+
1848
+ /**
1849
+ * The component purl of the library a crypto finding names as its provider.
1850
+ * Maven coordinates match on group and base artifact; the short aliases cover
1851
+ * the providers the rules emit for libraries without a coordinate.
1852
+ */
1853
+ export function scalasemProviderPurl(provider, components = []) {
1854
+ if (!provider) {
1855
+ return undefined;
1856
+ }
1857
+ let group;
1858
+ let artifactPrefix;
1859
+ if (provider === "bcprov") {
1860
+ group = "org.bouncycastle";
1861
+ artifactPrefix = "bcprov";
1862
+ } else if (provider.includes(":")) {
1863
+ [group, artifactPrefix] = provider.split(":");
1864
+ } else {
1865
+ return undefined;
1866
+ }
1867
+ const base = parseScalaArtifact(artifactPrefix).artifactBase;
1868
+ for (const component of components) {
1869
+ if (!component?.purl) {
1870
+ continue;
1871
+ }
1872
+ const segments = /^pkg:[^/]+\/(?:([^/@?]+)\/)?([^@?]+)/.exec(
1873
+ component.purl,
1874
+ );
1875
+ if (!segments || decodeSafe(segments[1]) !== group) {
1876
+ continue;
1877
+ }
1878
+ const name = parseScalaArtifact(decodeSafe(segments[2])).artifactBase;
1879
+ if (name === base || name.startsWith(`${base}-`)) {
1880
+ return component.purl;
1881
+ }
1882
+ }
1883
+ return undefined;
1884
+ }
1885
+
1886
+ // Primitives a mode of operation applies to. RSA spells `RSA/ECB/...` in JCA,
1887
+ // but a public key cipher has no mode, and calling it ECB would read as the
1888
+ // weak block cipher mode.
1889
+ const SCALASEM_MODE_PRIMITIVES = new Set(["block-cipher", "ae"]);
1890
+
1891
+ function cryptoModeOf(finding) {
1892
+ const mode = String(finding.mode || "").toLowerCase();
1893
+ return mode &&
1894
+ SCALASEM_MODES.has(mode) &&
1895
+ SCALASEM_MODE_PRIMITIVES.has(finding.primitive)
1896
+ ? mode
1897
+ : undefined;
1898
+ }
1899
+
1900
+ /**
1901
+ * The asset name: the algorithm with its key size, mode and padding, so one
1902
+ * cipher used two ways is two assets, and an AES name that carries its mode
1903
+ * (`AES-GCM`) reads the same as AES with that mode.
1904
+ */
1905
+ function cryptoAssetDisplayName(finding) {
1906
+ let base = finding.algorithm;
1907
+ let mode = cryptoModeOf(finding)?.toUpperCase();
1908
+ if (SCALASEM_AES_NAMES.has(base) && base.includes("-")) {
1909
+ [base] = base.split("-");
1910
+ mode ||= finding.algorithm.split("-")[1];
1911
+ }
1912
+ const parts = [base];
1913
+ if (finding.keySize) {
1914
+ parts.push(String(finding.keySize));
1915
+ }
1916
+ if (mode) {
1917
+ parts.push(mode);
1918
+ }
1919
+ const padding = paddingOf(finding.padding);
1920
+ if (finding.primitive === "pke" && padding && padding !== "raw") {
1921
+ parts.push(padding.toUpperCase());
1922
+ }
1923
+ return parts.join("-");
1924
+ }
1925
+
1926
+ function cryptoAlgorithmProperties(finding) {
1927
+ const properties = {};
1928
+ if (finding.primitive) {
1929
+ const primitive = finding.primitive === "rng" ? "drbg" : finding.primitive;
1930
+ properties.primitive = SCALASEM_CRYPTO_PRIMITIVES.has(primitive)
1931
+ ? primitive
1932
+ : "other";
1933
+ }
1934
+ const mode = cryptoModeOf(finding);
1935
+ if (mode) {
1936
+ properties.mode = mode;
1937
+ }
1938
+ const padding = paddingOf(finding.padding);
1939
+ if (padding) {
1940
+ properties.padding = padding;
1941
+ }
1942
+ if (finding.keySize) {
1943
+ properties.parameterSetIdentifier = String(finding.keySize);
1944
+ }
1945
+ return properties;
1946
+ }
1947
+
1948
+ function cryptoBomRef(kind, name, identifier) {
1949
+ return `crypto/${kind}/${encodeURIComponent(name)}@${encodeURIComponent(identifier || name)}`;
1950
+ }
1951
+
1952
+ /**
1953
+ * Crypto findings become CycloneDX crypto assets. Algorithms resolve through
1954
+ * the OID alias table and are skipped without one, protocols and keystores
1955
+ * are their own asset kinds, and everything the registry cannot name becomes
1956
+ * a `cdx:scalasem:crypto:*` property on the providing component instead of an
1957
+ * asset with an invented identifier. A finding the report could not resolve
1958
+ * never becomes an asset.
1959
+ */
1960
+ function addCryptoEvidence(
1961
+ report,
1962
+ components,
1963
+ cryptoComponentsByRef,
1964
+ cryptoGeneratePurls,
1965
+ componentPropertiesMap,
1966
+ metadataProperties,
1967
+ options = {},
1968
+ ) {
1969
+ const includeTests = options.scalasemIncludeTests === true;
1970
+ const nativeLibraryBySymbol = new Map();
1971
+ for (const binding of report.nativeBindings || []) {
1972
+ if (binding.library && binding.symbol) {
1973
+ nativeLibraryBySymbol.set(binding.symbol, binding.library);
1974
+ }
1975
+ }
1976
+ const assetOccurrencesByRef = new Map();
1977
+ for (const finding of report.crypto || []) {
1978
+ const scope = report[finding.file]?.scope || "main";
1979
+ if (scope === "test" && !includeTests) {
1980
+ continue;
1981
+ }
1982
+ const location = `${finding.file || ""}#${finding.line || 0}`;
1983
+ const nativeLibrary = finding.kind?.startsWith("native")
1984
+ ? nativeLibraryBySymbol.get(finding.name)
1985
+ : undefined;
1986
+ const providerPurl = scalasemProviderPurl(finding.provider, components);
1987
+ const asset =
1988
+ finding.resolution === "unresolved"
1989
+ ? undefined
1990
+ : cryptoAssetFor(finding, options);
1991
+ if (asset) {
1992
+ const existing = cryptoComponentsByRef.get(asset["bom-ref"]);
1993
+ const target = existing || asset;
1994
+ if (!existing) {
1995
+ cryptoComponentsByRef.set(asset["bom-ref"], asset);
1996
+ }
1997
+ for (const property of cryptoAssetProperties(finding, nativeLibrary)) {
1998
+ appendUniqueProperty(target.properties, property.name, property.value);
1999
+ }
2000
+ appendUniqueProperty(
2001
+ target.properties,
2002
+ "cdx:scalasem:usageScopes",
2003
+ scope,
2004
+ );
2005
+ const occurrences = assetOccurrencesByRef.get(asset["bom-ref"]) || [];
2006
+ if (!occurrences.includes(location)) {
2007
+ occurrences.push(location);
2008
+ }
2009
+ assetOccurrencesByRef.set(asset["bom-ref"], occurrences);
2010
+ if (providerPurl) {
2011
+ cryptoGeneratePurls[providerPurl] ??= new Set();
2012
+ cryptoGeneratePurls[providerPurl].add(asset["bom-ref"]);
2013
+ addPropertyValue(
2014
+ componentPropertiesMap,
2015
+ providerPurl,
2016
+ "cdx:scalasem:cryptoAlgorithms",
2017
+ finding.algorithm,
2018
+ );
2019
+ }
2020
+ providerAliases(providerPurl, components).forEach((alias) => {
2021
+ cryptoGeneratePurls[alias] ??= new Set();
2022
+ cryptoGeneratePurls[alias].add(asset["bom-ref"]);
2023
+ });
2024
+ } else if (finding.algorithm || finding.name) {
2025
+ // No registry OID: the finding stays visible as a property, and a weak
2026
+ // one says so, since no asset carries the verdict.
2027
+ const name = finding.algorithm || finding.name;
2028
+ const value = `${name}@${location}`;
2029
+ const record = (propertyName) => {
2030
+ if (providerPurl) {
2031
+ addPropertyValue(
2032
+ componentPropertiesMap,
2033
+ providerPurl,
2034
+ propertyName,
2035
+ value,
2036
+ );
2037
+ } else {
2038
+ appendUniqueProperty(metadataProperties, propertyName, value);
2039
+ }
2040
+ };
2041
+ record(`cdx:scalasem:crypto:${finding.kind}`);
2042
+ if (finding.weak) {
2043
+ record("cdx:scalasem:crypto:weakFinding");
2044
+ }
2045
+ }
2046
+ }
2047
+ for (const [ref, asset] of cryptoComponentsByRef.entries()) {
2048
+ asset.evidence = {
2049
+ occurrences: assetOccurrencesByRef
2050
+ .get(ref)
2051
+ .sort()
2052
+ .map((location) => parseLocationString(location)),
2053
+ };
2054
+ }
2055
+ }
2056
+
2057
+ function parseLocationString(location) {
2058
+ const separator = location.lastIndexOf("#");
2059
+ const line = Number.parseInt(location.slice(separator + 1), 10);
2060
+ return {
2061
+ location: location.slice(0, separator),
2062
+ ...(Number.isFinite(line) && line > 0 ? { line } : {}),
2063
+ };
2064
+ }
2065
+
2066
+ // Protocol names as CycloneDX types and versions. SSL predates TLS and has
2067
+ // no type of its own, and the bare names carry no version.
2068
+ const SCALASEM_PROTOCOLS = {
2069
+ SSL: { type: "other" },
2070
+ SSLv3: { type: "other", version: "3.0" },
2071
+ TLS: { type: "tls" },
2072
+ TLSv1: { type: "tls", version: "1.0" },
2073
+ "TLSv1.1": { type: "tls", version: "1.1" },
2074
+ "TLSv1.2": { type: "tls", version: "1.2" },
2075
+ "TLSv1.3": { type: "tls", version: "1.3" },
2076
+ "DTLSv1.2": { type: "dtls", version: "1.2" },
2077
+ };
2078
+
2079
+ function cryptoAssetFor(finding) {
2080
+ const name = finding.algorithm || finding.name;
2081
+ if (!name) {
2082
+ return undefined;
2083
+ }
2084
+ if (SCALASEM_PROTOCOL_NAMES.has(name) || finding.kind === "protocol") {
2085
+ const protocol = SCALASEM_PROTOCOLS[name] || { type: "other" };
2086
+ return {
2087
+ type: "cryptographic-asset",
2088
+ name,
2089
+ "bom-ref": cryptoBomRef("protocol", name),
2090
+ description:
2091
+ "Cryptographic protocol detected by scalasem source analysis",
2092
+ cryptoProperties: {
2093
+ assetType: "protocol",
2094
+ protocolProperties: { ...protocol },
2095
+ },
2096
+ properties: [],
2097
+ };
2098
+ }
2099
+ if (SCALASEM_KEYSTORE_NAMES.has(name) || finding.kind === "keystore") {
2100
+ return {
2101
+ type: "cryptographic-asset",
2102
+ name,
2103
+ "bom-ref": cryptoBomRef("keystore", name),
2104
+ description: "Key store detected by scalasem source analysis",
2105
+ cryptoProperties: {
2106
+ assetType: "related-crypto-material",
2107
+ relatedCryptoMaterialProperties: { type: "other" },
2108
+ },
2109
+ properties: [],
2110
+ };
2111
+ }
2112
+ const oidKey = scalasemCryptoOidKey(
2113
+ name,
2114
+ finding.keySize,
2115
+ finding.mode,
2116
+ finding.curve,
2117
+ finding.padding,
2118
+ );
2119
+ const oid = oidKey ? SCALASEM_CRYPTO_OIDS[oidKey]?.oid : undefined;
2120
+ if (!oid) {
2121
+ return undefined;
2122
+ }
2123
+ const algorithmProperties = cryptoAlgorithmProperties(finding);
2124
+ const displayName = cryptoAssetDisplayName(finding);
2125
+ // A native finding's bom-ref carries the C function it resolves to, so the
2126
+ // asset stays distinct per binding and findable by the function name.
2127
+ const nativeSuffix =
2128
+ finding.kind?.startsWith("native") && finding.name
2129
+ ? `#${finding.name}`
2130
+ : "";
2131
+ const component = {
2132
+ type: "cryptographic-asset",
2133
+ name: displayName,
2134
+ "bom-ref": `${cryptoBomRef("algorithm", displayName, oid)}${nativeSuffix}`,
2135
+ description: "Cryptographic algorithm detected by scalasem source analysis",
2136
+ cryptoProperties: {
2137
+ assetType: "algorithm",
2138
+ oid,
2139
+ algorithmProperties,
2140
+ },
2141
+ properties: [],
2142
+ };
2143
+ // The family and the curve in the schema's own spellings (`secg/secp256r1`);
2144
+ // spec compatibility keeps them for 1.7 and later only.
2145
+ applyAlgorithmProperties(component, {
2146
+ name: SCALASEM_AES_NAMES.has(name) ? "AES" : name,
2147
+ curve: finding.curve,
2148
+ });
2149
+ return component;
2150
+ }
2151
+
2152
+ function cryptoAssetProperties(finding, nativeLibrary) {
2153
+ const properties = [];
2154
+ appendUniqueProperty(
2155
+ properties,
2156
+ "cdx:scalasem:crypto:resolution",
2157
+ finding.resolution,
2158
+ );
2159
+ appendUniqueProperty(properties, "cdx:scalasem:crypto:api", finding.api);
2160
+ appendUniqueProperty(
2161
+ properties,
2162
+ "cdx:scalasem:crypto:provider",
2163
+ finding.provider,
2164
+ );
2165
+ if (finding.weak) {
2166
+ appendUniqueProperty(properties, "cdx:scalasem:crypto:weak", "true");
2167
+ }
2168
+ if (finding.name && finding.kind?.startsWith("native")) {
2169
+ appendUniqueProperty(
2170
+ properties,
2171
+ "cdx:scalasem:crypto:nativeFunction",
2172
+ finding.name,
2173
+ );
2174
+ appendUniqueProperty(
2175
+ properties,
2176
+ "cdx:scalasem:crypto:nativeLibrary",
2177
+ nativeLibrary,
2178
+ );
2179
+ }
2180
+ if (finding.bits) {
2181
+ appendUniqueProperty(
2182
+ properties,
2183
+ "cdx:scalasem:crypto:gcmTagBits",
2184
+ finding.bits,
2185
+ );
2186
+ }
2187
+ return properties;
2188
+ }
2189
+
2190
+ /**
2191
+ * A JDBC address without what may carry credentials: userinfo, the Oracle
2192
+ * `user/password@` prefix, URL parameters and the `;key=value` properties
2193
+ * some drivers take. The report is sanitized already; this keeps the BOM
2194
+ * safe from a report written by anything else.
2195
+ *
2196
+ * @param {string} value JDBC URL.
2197
+ * @returns {string} The address part.
2198
+ */
2199
+ 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)}`;
2230
+ }
2231
+ return text;
2232
+ }
2233
+
2234
+ 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];
2241
+ }
2242
+ return [scheme, host].filter(Boolean).join("-");
2243
+ }
2244
+
2245
+ /**
2246
+ * The host a service URL names, or the topic and JDBC scheme equivalents, so
2247
+ * every outbound service is named after where it talks to rather than the
2248
+ * client library it uses.
2249
+ */
2250
+ export function scalasemServiceName(service) {
2251
+ if (service?.topic) {
2252
+ return scalasemServiceEndpoint(service);
2253
+ }
2254
+ const value = service?.url || service?.host;
2255
+ if (!value) {
2256
+ return undefined;
2257
+ }
2258
+ if (/^jdbc:/i.test(value)) {
2259
+ return jdbcServiceName(value);
2260
+ }
2261
+ 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;
2268
+ }
2269
+ }
2270
+
2271
+ /**
2272
+ * The endpoint a service row carries, normalized: a URL as its scheme and
2273
+ * host, a host as it is, a topic as `client:topic`, a JDBC URL with its
2274
+ * credentials and parameters dropped.
2275
+ */
2276
+ export function scalasemServiceEndpoint(service) {
2277
+ if (service?.topic) {
2278
+ const scheme = service.client || "messaging";
2279
+ return `${scheme}:${service.topic}`;
2280
+ }
2281
+ const value = service?.url || service?.host;
2282
+ if (!value) {
2283
+ return undefined;
2284
+ }
2285
+ if (/^jdbc:/i.test(value)) {
2286
+ // The database name in the path is part of the address.
2287
+ return sanitizeJdbcUrl(value);
2288
+ }
2289
+ 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;
2298
+ }
2299
+ }
2300
+
2301
+ function serviceLocation(service) {
2302
+ if (!service?.file) {
2303
+ return undefined;
2304
+ }
2305
+ return service.line ? `${service.file}#${service.line}` : service.file;
2306
+ }
2307
+
2308
+ /**
2309
+ * Outbound services: HTTP and websocket clients, data stores, messaging
2310
+ * topics and cloud clients. Locations ride in
2311
+ * `cdx:scalasem:service:location` properties because CycloneDX services
2312
+ * carry no evidence field before 2.0; spec compatibility keeps the
2313
+ * occurrences below that only at 2.0.
2314
+ *
2315
+ * @param {Object} report Parsed scalasem report.
2316
+ * @param {Object} servicesMap Map populated with service definitions.
2317
+ * @returns {Object} The mutated services map.
2318
+ */
2319
+ export function collectScalasemServices(report = {}, servicesMap = {}) {
2320
+ for (const service of report.services || []) {
2321
+ const name = scalasemServiceName(service);
2322
+ if (!name) {
2323
+ continue;
2324
+ }
2325
+ const definition = (servicesMap[name] ??= {
2326
+ name,
2327
+ endpoints: new Set(),
2328
+ authenticated: undefined,
2329
+ xTrustBoundary: true,
2330
+ properties: [],
2331
+ });
2332
+ definition.endpoints ??= new Set();
2333
+ definition.properties ??= [];
2334
+ const value = scalasemServiceEndpoint(service);
2335
+ if (value) {
2336
+ definition.endpoints.add(value);
2337
+ }
2338
+ const location = serviceLocation(service);
2339
+ if (location) {
2340
+ appendUniqueProperty(
2341
+ definition.properties,
2342
+ "cdx:scalasem:service:location",
2343
+ location,
2344
+ );
2345
+ definition.evidence ??= { occurrences: [] };
2346
+ const known = definition.evidence.occurrences;
2347
+ if (!known.some((occurrence) => occurrence.location === location)) {
2348
+ known.push(parseLocationString(location));
2349
+ }
2350
+ }
2351
+ appendUniqueProperty(
2352
+ definition.properties,
2353
+ "cdx:scalasem:service:kind",
2354
+ service.kind,
2355
+ );
2356
+ appendUniqueProperty(
2357
+ definition.properties,
2358
+ "cdx:scalasem:service:client",
2359
+ service.client,
2360
+ );
2361
+ appendUniqueProperty(
2362
+ definition.properties,
2363
+ "cdx:scalasem:service:resolution",
2364
+ service.resolution,
2365
+ );
2366
+ }
2367
+ return servicesMap;
2368
+ }
2369
+
2370
+ /**
2371
+ * Inbound endpoints, one service per route, named the way the OpenAPI reader
2372
+ * names its own so the two converge on one entry for the same route.
2373
+ *
2374
+ * @param {Object} report Parsed scalasem report.
2375
+ * @param {Object} servicesMap Map populated with service definitions.
2376
+ * @returns {Object} The mutated services map.
2377
+ */
2378
+ export function collectScalasemApiEndpoints(report = {}, servicesMap = {}) {
2379
+ for (const endpoint of report.endpoints || []) {
2380
+ const path = endpoint?.path;
2381
+ const method = String(endpoint.method || "").toUpperCase();
2382
+ if (!path || !method) {
2383
+ continue;
2384
+ }
2385
+ const serviceName = `service-${path.replaceAll("/", "")}-${method.toLowerCase()}`;
2386
+ const definition = (servicesMap[serviceName] ??= {
2387
+ name: serviceName,
2388
+ endpoints: new Set(),
2389
+ authenticated: undefined,
2390
+ xTrustBoundary: undefined,
2391
+ properties: [],
2392
+ });
2393
+ definition.endpoints ??= new Set();
2394
+ definition.properties ??= [];
2395
+ definition.endpoints.add(path);
2396
+ if (endpoint.authenticated === true) {
2397
+ definition.authenticated = true;
2398
+ definition.xTrustBoundary = true;
2399
+ }
2400
+ if (endpoint.handler) {
2401
+ appendUniqueProperty(
2402
+ definition.properties,
2403
+ "internal:operationId",
2404
+ endpoint.handler,
2405
+ );
2406
+ appendUniqueProperty(
2407
+ definition.properties,
2408
+ "cdx:scalasem:endpoint:handler",
2409
+ endpoint.handler,
2410
+ );
2411
+ }
2412
+ appendUniqueProperty(
2413
+ definition.properties,
2414
+ "cdx:service:httpMethod",
2415
+ method,
2416
+ );
2417
+ appendUniqueProperty(
2418
+ definition.properties,
2419
+ "cdx:scalasem:endpoint:framework",
2420
+ endpoint.framework,
2421
+ );
2422
+ const location = serviceLocation(endpoint);
2423
+ if (location) {
2424
+ appendUniqueProperty(
2425
+ definition.properties,
2426
+ "cdx:scalasem:service:location",
2427
+ location,
2428
+ );
2429
+ definition.evidence ??= { occurrences: [] };
2430
+ const known = definition.evidence.occurrences;
2431
+ if (!known.some((occurrence) => occurrence.location === location)) {
2432
+ known.push(parseLocationString(location));
2433
+ }
2434
+ }
2435
+ }
2436
+ return servicesMap;
2437
+ }
2438
+
2439
+ /**
2440
+ * The npm packages a Scala.js build bundles. scalajs-bundler installs them
2441
+ * with a lock file under `target`, and a bundler workspace such as a Vite
2442
+ * client keeps its own manifest and lock file beside the build. Their npm
2443
+ * components join the BOM so the report's JavaScript module imports can be
2444
+ * attributed to them. Only lock files name the installed versions, and a
2445
+ * workspace counts only when its manifest uses Scala.js, so a documentation
2446
+ * site or a tool's own `package.json` adds nothing.
2447
+ *
2448
+ * @param {string} src Project directory.
2449
+ * @param {Object[]} pkgList Components the build tools reported.
2450
+ * @param {Object} [options] CLI options; `exclude` is honoured.
2451
+ * @returns {Promise<{components: Object[], roots: string[], dependencies: Object[]}>}
2452
+ * Npm components, the bom-refs the workspaces depend on directly, and the
2453
+ * lock files' dependency edges between the npm components.
2454
+ */
2455
+ export async function collectScalaJsNpmComponents(
2456
+ src,
2457
+ pkgList = [],
2458
+ options = {},
2459
+ ) {
2460
+ const isScalaJs = pkgList.some((pkg) => {
2461
+ const name = pkg?.name || purlPurlName(pkg?.purl) || "";
2462
+ return /_(sjs\d+(?:\.\d+)?)$/.test(name) || name === "scalajs-library";
2463
+ });
2464
+ const empty = { components: [], roots: [], dependencies: [] };
2465
+ if (!isScalaJs) {
2466
+ return empty;
2467
+ }
2468
+ const projectDir = resolve(src);
2469
+ const searchOptions = {
2470
+ includeNodeModulesDir: false,
2471
+ exclude: options.exclude,
2472
+ };
2473
+ const manifestDirs = new Set();
2474
+ for (const file of getAllFiles(
2475
+ projectDir,
2476
+ "**/scalajs-bundler/**/package.json",
2477
+ searchOptions,
2478
+ )) {
2479
+ manifestDirs.add(dirname(file));
2480
+ }
2481
+ // A workspace beside the build: the project's own manifests, not the ones
2482
+ // node_modules holds. Two levels cover the usual client/server split.
2483
+ for (const pattern of ["package.json", "*/package.json"]) {
2484
+ for (const file of getAllFiles(projectDir, pattern, searchOptions)) {
2485
+ if (manifestUsesScalaJs(file)) {
2486
+ manifestDirs.add(dirname(file));
2487
+ }
2488
+ }
2489
+ }
2490
+ const components = [];
2491
+ const roots = [];
2492
+ const dependencies = [];
2493
+ for (const manifestDir of manifestDirs) {
2494
+ const lockFile = ["package-lock.json", "npm-shrinkwrap.json"]
2495
+ .map((name) => join(manifestDir, name))
2496
+ .find((file) => safeExistsSync(file));
2497
+ if (!lockFile) {
2498
+ continue;
2499
+ }
2500
+ const lockData = await parsePkgLock(lockFile, options);
2501
+ // The lock file's own package is the workspace, which is part of the
2502
+ // project rather than a dependency of it.
2503
+ const workspaceRefs = new Set(
2504
+ (lockData?.pkgList || [])
2505
+ .filter((pkg) => pkg.type === "application")
2506
+ .map((pkg) => pkg["bom-ref"]),
2507
+ );
2508
+ components.push(
2509
+ ...(lockData?.pkgList || []).filter((pkg) => pkg.type !== "application"),
2510
+ );
2511
+ for (const dependency of lockData?.dependenciesList || []) {
2512
+ if (workspaceRefs.has(dependency.ref)) {
2513
+ roots.push(...(dependency.dependsOn || []));
2514
+ } else {
2515
+ dependencies.push(dependency);
2516
+ }
2517
+ }
2518
+ }
2519
+ return { components, roots: [...new Set(roots)], dependencies };
2520
+ }
2521
+
2522
+ function manifestUsesScalaJs(manifestFile) {
2523
+ try {
2524
+ const manifest = JSON.parse(readFileSync(manifestFile, "utf-8"));
2525
+ const names = Object.keys({
2526
+ ...(manifest?.dependencies || {}),
2527
+ ...(manifest?.devDependencies || {}),
2528
+ });
2529
+ return names.some((name) => /scala-?js/i.test(name));
2530
+ } catch (_err) {
2531
+ return false;
2532
+ }
2533
+ }
2534
+
2535
+ // The package name an npm purl carries, with the scope decoded back to
2536
+ // `@scope/name`, which is how JavaScript names its modules.
2537
+ function npmNameFromPurl(purl) {
2538
+ const rest = /^pkg:npm\/(.+)$/.exec(purl || "")?.[1];
2539
+ if (!rest) {
2540
+ return undefined;
2541
+ }
2542
+ const at = rest.lastIndexOf("@");
2543
+ const name = at > 0 ? rest.slice(0, at) : rest;
2544
+ try {
2545
+ return decodeURIComponent(name);
2546
+ } catch (_err) {
2547
+ return name;
2548
+ }
2549
+ }
2550
+
2551
+ /**
2552
+ * Attribute the report's JavaScript module imports to the npm components the
2553
+ * build bundles. A module no component owns is reported on the project
2554
+ * component instead, so the import stays visible either way.
2555
+ */
2556
+ function addJsModuleEvidence(
2557
+ report,
2558
+ components,
2559
+ purlLocationMap,
2560
+ componentPropertiesMap,
2561
+ metadataProperties,
2562
+ ) {
2563
+ const jsModules = report.jsModules || [];
2564
+ if (!jsModules.length) {
2565
+ return;
2566
+ }
2567
+ const npmByName = new Map();
2568
+ for (const component of components) {
2569
+ const name = npmNameFromPurl(component?.purl);
2570
+ if (name) {
2571
+ npmByName.set(name, component);
2572
+ }
2573
+ }
2574
+ const moduleMatchesComponent = (moduleName) => {
2575
+ if (npmByName.has(moduleName)) {
2576
+ return npmByName.get(moduleName);
2577
+ }
2578
+ // `pkg/subpath` and `@scope/pkg/subpath` belong to the package named by
2579
+ // their leading segments.
2580
+ const segments = moduleName.split("/");
2581
+ const scoped =
2582
+ moduleName.startsWith("@") && segments.length > 2
2583
+ ? `${segments[0]}/${segments[1]}`
2584
+ : segments[0];
2585
+ return npmByName.get(scoped);
2586
+ };
2587
+ const unmatched = new Set();
2588
+ const seenLocations = new Set();
2589
+ for (const jsModule of jsModules) {
2590
+ const moduleName = jsModule.module;
2591
+ if (!moduleName || moduleName.startsWith(".") || moduleName.includes("*")) {
2592
+ continue;
2593
+ }
2594
+ const component = moduleMatchesComponent(moduleName);
2595
+ const location = jsModule.line
2596
+ ? `${jsModule.file}#${jsModule.line}`
2597
+ : jsModule.file;
2598
+ if (!component) {
2599
+ unmatched.add(moduleName);
2600
+ continue;
2601
+ }
2602
+ if (!seenLocations.has(`${component.purl}#${location}`)) {
2603
+ seenLocations.add(`${component.purl}#${location}`);
2604
+ addSetValue(purlLocationMap, component.purl, location);
2605
+ }
2606
+ addPropertyValue(
2607
+ componentPropertiesMap,
2608
+ component.purl,
2609
+ "cdx:scalasem:jsModule",
2610
+ moduleName,
2611
+ );
2612
+ }
2613
+ if (unmatched.size) {
2614
+ appendUniqueProperty(
2615
+ metadataProperties,
2616
+ "cdx:scalasem:jsModules",
2617
+ sortedCsv(unmatched),
2618
+ );
2619
+ }
2620
+ }