@cdxgen/cdxgen 13.2.0 → 13.4.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 (311) hide show
  1. package/README.md +33 -14
  2. package/bin/cdxgen.js +310 -138
  3. package/bin/evinse.js +22 -0
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +119 -15
  6. package/bin/tracebom.js +69 -5
  7. package/bin/validate.js +107 -19
  8. package/bin/verify.js +172 -63
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +85 -0
  11. package/data/rules/dependency-sources.yaml +67 -0
  12. package/data/rules/scala-evidence.yaml +115 -0
  13. package/lib/audit/index.js +38 -6
  14. package/lib/cli/bomAssembly.js +105 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +156 -58
  17. package/lib/cli/jsBom.js +99 -53
  18. package/lib/cli/jvmBom.js +708 -147
  19. package/lib/cli/managedBom.js +415 -224
  20. package/lib/cli/nativeBom.js +264 -44
  21. package/lib/core/activity.js +12 -0
  22. package/lib/core/env.js +22 -15
  23. package/lib/core/fs.js +176 -7
  24. package/lib/core/httpClient.js +159 -24
  25. package/lib/core/metadataProperties.js +97 -0
  26. package/lib/core/pluginRun.js +184 -0
  27. package/lib/core/processTree.js +100 -0
  28. package/lib/core/projectConfig.js +31 -4
  29. package/lib/core/runState.js +39 -0
  30. package/lib/core/toml.js +72 -0
  31. package/lib/ecosystems/asarutils.js +4 -2
  32. package/lib/ecosystems/bunutils.js +274 -108
  33. package/lib/ecosystems/cargoCache.js +231 -8
  34. package/lib/ecosystems/caxa.js +434 -2
  35. package/lib/ecosystems/cmakeResolver.js +76 -21
  36. package/lib/ecosystems/cppBuildContext.js +631 -0
  37. package/lib/ecosystems/cppEvidence.js +399 -51
  38. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  39. package/lib/ecosystems/denoutils.js +93 -66
  40. package/lib/ecosystems/ecosystems.js +1983 -793
  41. package/lib/ecosystems/golem.js +28 -12
  42. package/lib/ecosystems/gradleutils.js +356 -37
  43. package/lib/ecosystems/jsEvidence.js +104 -44
  44. package/lib/ecosystems/kosi.js +31 -7
  45. package/lib/ecosystems/npmutils.js +174 -9
  46. package/lib/ecosystems/parsers-dotnet.js +616 -159
  47. package/lib/ecosystems/parsers-elm.js +46 -28
  48. package/lib/ecosystems/parsers-gleam.js +3 -4
  49. package/lib/ecosystems/parsers-go.js +371 -70
  50. package/lib/ecosystems/parsers-js.js +263 -76
  51. package/lib/ecosystems/parsers-julia.js +2 -3
  52. package/lib/ecosystems/parsers-jvm.js +204 -12
  53. package/lib/ecosystems/parsers-misc.js +929 -216
  54. package/lib/ecosystems/parsers-python.js +167 -24
  55. package/lib/ecosystems/parsers-rust.js +28 -22
  56. package/lib/ecosystems/parsers-terraform.js +1573 -19
  57. package/lib/ecosystems/pythonutils.js +102 -6
  58. package/lib/ecosystems/remote/huggingface.js +3 -0
  59. package/lib/ecosystems/rubyutils.js +1492 -182
  60. package/lib/ecosystems/rusi.js +54 -10
  61. package/lib/ecosystems/sbtutils.js +243 -187
  62. package/lib/ecosystems/scalacliutils.js +342 -0
  63. package/lib/ecosystems/scalasem.js +2618 -0
  64. package/lib/ecosystems/terraformRegistry.js +288 -0
  65. package/lib/ecosystems/utils.js +2 -0
  66. package/lib/ecosystems/vendoredCode.js +156 -0
  67. package/lib/evinser/evinser.js +1008 -191
  68. package/lib/evinser/swiftsem.js +1914 -302
  69. package/lib/helpers/agenticToolutils.js +2 -2
  70. package/lib/helpers/bomSigner.js +1101 -222
  71. package/lib/inventory/aiCollector.js +4 -1
  72. package/lib/inventory/analyzer.js +110 -127
  73. package/lib/inventory/atomUtils.js +703 -43
  74. package/lib/inventory/auditCategories.js +7 -0
  75. package/lib/inventory/buildToolRateLimit.js +144 -0
  76. package/lib/inventory/cacheCommands.js +2 -1
  77. package/lib/inventory/cdxrs.js +103 -12
  78. package/lib/inventory/cmakeBuildDirs.js +254 -0
  79. package/lib/inventory/deps.js +501 -124
  80. package/lib/inventory/depsUtils.js +183 -11
  81. package/lib/inventory/display.js +4 -5
  82. package/lib/inventory/dosai.js +798 -57
  83. package/lib/inventory/dosaiParsers.js +310 -15
  84. package/lib/inventory/dynamic.js +90 -6
  85. package/lib/inventory/evidenceUtils.js +166 -113
  86. package/lib/inventory/fetchBatch.js +690 -55
  87. package/lib/inventory/fetchDiskCache.js +597 -0
  88. package/lib/inventory/forgeEnricher.js +14 -5
  89. package/lib/inventory/jvmLocalRepos.js +917 -0
  90. package/lib/inventory/mcpConfigParser.js +2 -3
  91. package/lib/inventory/parentSelection.js +146 -0
  92. package/lib/inventory/protobom.js +8 -1
  93. package/lib/inventory/purl.js +152 -3
  94. package/lib/inventory/rustFormulationParser.js +3 -4
  95. package/lib/inventory/sbomDocument.js +2 -70
  96. package/lib/inventory/scalaCoords.js +153 -0
  97. package/lib/inventory/source.js +45 -27
  98. package/lib/inventory/spdx.js +101 -0
  99. package/lib/inventory/traceRunner.js +295 -25
  100. package/lib/inventory/tracedCrypto.js +294 -0
  101. package/lib/managers/binary.js +115 -24
  102. package/lib/managers/docker.js +467 -46
  103. package/lib/managers/oci.js +63 -3
  104. package/lib/parsers/cmakeCache.js +69 -0
  105. package/lib/parsers/cmakeLists.js +466 -0
  106. package/lib/parsers/cmakePresets.js +400 -0
  107. package/lib/parsers/compileCommands.js +557 -0
  108. package/lib/parsers/largeJson.js +454 -0
  109. package/lib/parsers/npmrc.js +19 -1
  110. package/lib/parsers/strictJson.js +322 -0
  111. package/lib/parsers/terraformConfig.js +1073 -0
  112. package/lib/parsers/terraformSource.js +652 -0
  113. package/lib/server/server.js +23 -0
  114. package/lib/stages/postgen/introspection/markers.js +1 -1
  115. package/lib/stages/postgen/postgen.js +83 -16
  116. package/lib/stages/postgen/specVersionCompat.js +32 -0
  117. package/lib/validator/bomValidator.js +6 -16
  118. package/lib/validator/index.js +64 -11
  119. package/lib/validator/reporters/console.js +3 -0
  120. package/lib/validator/reporters/json.js +2 -0
  121. package/lib/validator/reporters/sarif.js +7 -0
  122. package/package.json +23 -23
  123. package/types/lib/audit/index.d.ts.map +1 -1
  124. package/types/lib/cli/bomAssembly.d.ts +2 -1
  125. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  126. package/types/lib/cli/cliOptions.d.ts +33 -1
  127. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  128. package/types/lib/cli/index.d.ts.map +1 -1
  129. package/types/lib/cli/jsBom.d.ts.map +1 -1
  130. package/types/lib/cli/jvmBom.d.ts +24 -1
  131. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  132. package/types/lib/cli/managedBom.d.ts +7 -4
  133. package/types/lib/cli/managedBom.d.ts.map +1 -1
  134. package/types/lib/cli/nativeBom.d.ts +3 -1
  135. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  136. package/types/lib/core/activity.d.ts.map +1 -1
  137. package/types/lib/core/env.d.ts +9 -0
  138. package/types/lib/core/env.d.ts.map +1 -1
  139. package/types/lib/core/fs.d.ts +55 -0
  140. package/types/lib/core/fs.d.ts.map +1 -1
  141. package/types/lib/core/httpClient.d.ts +13 -8
  142. package/types/lib/core/httpClient.d.ts.map +1 -1
  143. package/types/lib/core/metadataProperties.d.ts +39 -0
  144. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  145. package/types/lib/core/pluginRun.d.ts +97 -0
  146. package/types/lib/core/pluginRun.d.ts.map +1 -0
  147. package/types/lib/core/processTree.d.ts +36 -0
  148. package/types/lib/core/processTree.d.ts.map +1 -0
  149. package/types/lib/core/projectConfig.d.ts +6 -2
  150. package/types/lib/core/projectConfig.d.ts.map +1 -1
  151. package/types/lib/core/runState.d.ts +29 -0
  152. package/types/lib/core/runState.d.ts.map +1 -0
  153. package/types/lib/core/toml.d.ts +20 -0
  154. package/types/lib/core/toml.d.ts.map +1 -0
  155. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  156. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  157. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  159. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  160. package/types/lib/ecosystems/caxa.d.ts +14 -0
  161. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  162. package/types/lib/ecosystems/cmakeResolver.d.ts +28 -3
  163. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  164. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  165. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  166. package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
  167. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  168. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  169. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  170. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  171. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  172. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  173. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  174. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  175. package/types/lib/ecosystems/gradleutils.d.ts +70 -4
  176. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  177. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  178. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  179. package/types/lib/ecosystems/kosi.d.ts +13 -0
  180. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  181. package/types/lib/ecosystems/npmutils.d.ts +57 -0
  182. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  183. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  184. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-go.d.ts +54 -2
  188. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  189. package/types/lib/ecosystems/parsers-js.d.ts +16 -5
  190. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  191. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  199. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  200. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  201. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rubyutils.d.ts +164 -3
  205. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  206. package/types/lib/ecosystems/rusi.d.ts +13 -0
  207. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  208. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  209. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  210. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  211. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  212. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  213. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  214. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  215. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  216. package/types/lib/ecosystems/utils.d.ts +2 -2
  217. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  218. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  219. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  220. package/types/lib/evinser/evinser.d.ts +132 -15
  221. package/types/lib/evinser/evinser.d.ts.map +1 -1
  222. package/types/lib/evinser/swiftsem.d.ts +280 -15
  223. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  224. package/types/lib/helpers/bomSigner.d.ts +182 -13
  225. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  226. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  227. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  228. package/types/lib/inventory/atomUtils.d.ts +219 -5
  229. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  230. package/types/lib/inventory/auditCategories.d.ts +3 -0
  231. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  232. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  233. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  234. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  235. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  236. package/types/lib/inventory/cdxrs.d.ts +8 -3
  237. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  238. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  239. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  240. package/types/lib/inventory/deps.d.ts +59 -1
  241. package/types/lib/inventory/deps.d.ts.map +1 -1
  242. package/types/lib/inventory/depsUtils.d.ts +15 -0
  243. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  244. package/types/lib/inventory/display.d.ts.map +1 -1
  245. package/types/lib/inventory/dosai.d.ts +121 -9
  246. package/types/lib/inventory/dosai.d.ts.map +1 -1
  247. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  248. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  249. package/types/lib/inventory/dynamic.d.ts +10 -0
  250. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  251. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  252. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  253. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  254. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  255. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  256. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  257. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  258. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  259. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  260. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  261. package/types/lib/inventory/parentSelection.d.ts +34 -0
  262. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  263. package/types/lib/inventory/plugins.d.ts.map +1 -1
  264. package/types/lib/inventory/protobom.d.ts.map +1 -1
  265. package/types/lib/inventory/purl.d.ts +71 -2
  266. package/types/lib/inventory/purl.d.ts.map +1 -1
  267. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  268. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  269. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  270. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  271. package/types/lib/inventory/source.d.ts.map +1 -1
  272. package/types/lib/inventory/spdx.d.ts +9 -0
  273. package/types/lib/inventory/spdx.d.ts.map +1 -1
  274. package/types/lib/inventory/traceRunner.d.ts +59 -1
  275. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  276. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  277. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  278. package/types/lib/managers/binary.d.ts +30 -1
  279. package/types/lib/managers/binary.d.ts.map +1 -1
  280. package/types/lib/managers/docker.d.ts +29 -0
  281. package/types/lib/managers/docker.d.ts.map +1 -1
  282. package/types/lib/managers/oci.d.ts +17 -1
  283. package/types/lib/managers/oci.d.ts.map +1 -1
  284. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  285. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  286. package/types/lib/parsers/cmakeLists.d.ts +77 -0
  287. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  288. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  289. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  290. package/types/lib/parsers/compileCommands.d.ts +115 -0
  291. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  292. package/types/lib/parsers/largeJson.d.ts +52 -0
  293. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  294. package/types/lib/parsers/npmrc.d.ts +5 -0
  295. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  296. package/types/lib/parsers/strictJson.d.ts +46 -0
  297. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  298. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  299. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  300. package/types/lib/parsers/terraformSource.d.ts +66 -0
  301. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  302. package/types/lib/server/server.d.ts.map +1 -1
  303. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  304. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  306. package/types/lib/validator/index.d.ts +18 -1
  307. package/types/lib/validator/index.d.ts.map +1 -1
  308. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  309. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  310. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
  311. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -1,12 +1,11 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { basename } from "node:path";
3
3
 
4
- import * as toml from "smol-toml";
5
-
6
4
  import {
7
5
  sanitizeBomPropertyValue,
8
6
  sanitizeBomUrl,
9
7
  } from "../core/propertySanitizer.js";
8
+ import { parseToml } from "../core/toml.js";
10
9
  import { parseJsonLike } from "./jsonLike.js";
11
10
  import { classifyMcpReference } from "./mcp.js";
12
11
  import {
@@ -813,7 +812,7 @@ export const mcpConfigParser = {
813
812
  let configJson;
814
813
  try {
815
814
  configJson = filePath.toLowerCase().endsWith(".toml")
816
- ? toml.parse(raw)
815
+ ? parseToml(raw)
817
816
  : parseJsonLike(raw);
818
817
  } catch {
819
818
  continue;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Which detected project becomes the parent (`metadata.component`) when one
3
+ * scan covers several ecosystems and no parent was given.
4
+ *
5
+ * Each ecosystem detects its own project from its own manifest. The project
6
+ * whose manifest sits at the scan root describes the repository; one found
7
+ * deeper is a part of it (a web client in `tools/ui/`, a helper script's
8
+ * `pyproject.toml`). So candidates are ordered by how deep their ecosystem's
9
+ * manifests sit, the scan root first, and ties keep the ecosystem order the
10
+ * scan ran in.
11
+ */
12
+
13
+ import { readdirSync } from "node:fs";
14
+ import { dirname, relative, resolve, sep } from "node:path";
15
+
16
+ import { getAllFiles } from "../core/fs.js";
17
+
18
+ /**
19
+ * Manifest file names (or `*.ext` suffixes) per purl type: the files a
20
+ * project of that ecosystem is described by.
21
+ */
22
+ const MANIFESTS_BY_PURL_TYPE = {
23
+ npm: ["package.json"],
24
+ pypi: [
25
+ "pyproject.toml",
26
+ "setup.py",
27
+ "setup.cfg",
28
+ "Pipfile",
29
+ "requirements.txt",
30
+ "uv.lock",
31
+ "poetry.lock",
32
+ "pixi.toml",
33
+ ],
34
+ generic: [
35
+ "CMakeLists.txt",
36
+ "meson.build",
37
+ "conanfile.txt",
38
+ "conanfile.py",
39
+ "vcpkg.json",
40
+ "xmake.lua",
41
+ ],
42
+ maven: ["pom.xml", "build.gradle", "build.gradle.kts", "build.sbt"],
43
+ golang: ["go.mod"],
44
+ cargo: ["Cargo.toml"],
45
+ composer: ["composer.json"],
46
+ gem: ["Gemfile", "*.gemspec"],
47
+ nuget: ["*.csproj", "*.fsproj", "*.vbproj", "*.sln", "packages.config"],
48
+ pub: ["pubspec.yaml"],
49
+ hex: ["mix.exs"],
50
+ hackage: ["*.cabal", "stack.yaml"],
51
+ swift: ["Package.swift"],
52
+ cocoapods: ["Podfile"],
53
+ clojars: ["deps.edn", "project.clj"],
54
+ };
55
+
56
+ /** The purl type of a component, or `undefined`. */
57
+ function purlTypeOf(component) {
58
+ const purl = `${component?.purl || ""}`;
59
+ if (!purl.startsWith("pkg:")) {
60
+ return undefined;
61
+ }
62
+ const rest = purl.slice(4);
63
+ const end = rest.search(/[/@?#]/);
64
+ return (end < 0 ? rest : rest.slice(0, end)).toLowerCase();
65
+ }
66
+
67
+ function matchesManifest(name, patterns) {
68
+ return patterns.some((p) =>
69
+ p.startsWith("*.") ? name.endsWith(p.slice(1)) : name === p,
70
+ );
71
+ }
72
+
73
+ /**
74
+ * How deep the shallowest manifest of an ecosystem sits under a scan root:
75
+ * 0 for one at the root itself, `Infinity` when there is none (or the
76
+ * ecosystem is not one this module knows).
77
+ *
78
+ * @param {string} root Scan root
79
+ * @param {string} purlType Purl type of the ecosystem
80
+ * @param {Object} options CLI options (exclusions)
81
+ * @returns {number}
82
+ */
83
+ export function manifestDepth(root, purlType, options = {}) {
84
+ const patterns = MANIFESTS_BY_PURL_TYPE[purlType];
85
+ if (!patterns || !root) {
86
+ return Number.POSITIVE_INFINITY;
87
+ }
88
+ const absRoot = resolve(root);
89
+ try {
90
+ if (readdirSync(absRoot).some((name) => matchesManifest(name, patterns))) {
91
+ return 0;
92
+ }
93
+ } catch (_err) {
94
+ return Number.POSITIVE_INFINITY;
95
+ }
96
+ const glob =
97
+ patterns.length === 1 ? `**/${patterns[0]}` : `**/{${patterns.join(",")}}`;
98
+ const files = getAllFiles(absRoot, glob, options);
99
+ let depth = Number.POSITIVE_INFINITY;
100
+ for (const f of files) {
101
+ const rel = relative(absRoot, dirname(resolve(f)));
102
+ if (!rel || rel.startsWith("..")) {
103
+ continue;
104
+ }
105
+ depth = Math.min(depth, rel.split(sep).length);
106
+ }
107
+ return depth;
108
+ }
109
+
110
+ /**
111
+ * Order parent candidates by the depth of their ecosystem's manifests under
112
+ * the scan roots (the shallowest first). The sort is stable, so candidates of
113
+ * equal depth keep the order the ecosystems were scanned in.
114
+ *
115
+ * @param {Object[]} candidates Detected parent components, in scan order
116
+ * @param {string[]} roots Scan roots
117
+ * @param {Object} options CLI options
118
+ * @returns {Object[]} The candidates, reordered
119
+ */
120
+ export function orderParentCandidates(candidates, roots, options = {}) {
121
+ const depthByType = new Map();
122
+ const depthOf = (component) => {
123
+ const type = purlTypeOf(component);
124
+ if (!type) {
125
+ return Number.POSITIVE_INFINITY;
126
+ }
127
+ if (!depthByType.has(type)) {
128
+ depthByType.set(
129
+ type,
130
+ Math.min(
131
+ Number.POSITIVE_INFINITY,
132
+ ...(roots || []).map((r) => manifestDepth(r, type, options)),
133
+ ),
134
+ );
135
+ }
136
+ return depthByType.get(type);
137
+ };
138
+ return candidates
139
+ .map((component, index) => ({
140
+ component,
141
+ index,
142
+ depth: depthOf(component),
143
+ }))
144
+ .sort((a, b) => a.depth - b.depth || a.index - b.index)
145
+ .map((c) => c.component);
146
+ }
@@ -143,7 +143,14 @@ export const readBinary = (binFile, asJson, specVersion) => {
143
143
  ? decodeBomBinary(specVersion, binaryData, BINARY_READ_OPTIONS)
144
144
  : parseBomBinary(binaryData, BINARY_READ_OPTIONS);
145
145
  if (asJson) {
146
- return encodeBomJson(bomObject);
146
+ const bomJson = encodeBomJson(bomObject);
147
+ // Protobuf omits empty repeated fields, so a round-trip drops empty
148
+ // dependsOn lists. Restore them so JSON and protobuf inputs yield
149
+ // identical documents.
150
+ for (const adep of bomJson?.dependencies || []) {
151
+ adep.dependsOn ??= [];
152
+ }
153
+ return bomJson;
147
154
  }
148
155
  return bomObject;
149
156
  };
@@ -53,15 +53,30 @@ export function purlTypeIsRegisteredString(purlString) {
53
53
  }
54
54
 
55
55
  /**
56
- * Encode a string for safe inclusion in a PackageURL, percent-encoding special characters
57
- * while preserving already-encoded `%40` sequences and keeping `:` and `/` unencoded.
56
+ * Encode a string for inclusion in a hand-written PackageURL string,
57
+ * percent-encoding every character the purl grammar does not allow literally,
58
+ * while preserving already-encoded `%40` sequences and keeping `:` and `/`
59
+ * unencoded.
60
+ *
61
+ * `encodeURIComponent` alone leaves `!`, `'`, `(`, `)` and `*` literal, and the
62
+ * purl grammar requires them escaped, so they are escaped here as well.
63
+ *
64
+ * Only for comparing against or assembling purl text. Never pass the result to
65
+ * `build()`, `new Purl()` or one of the helpers in this module: they escape
66
+ * their inputs themselves, and escaping twice turns `+` into `%252B`.
58
67
  *
59
68
  * @param {string} s String to encode
60
69
  * @returns {string} Encoded string suitable for use in a PackageURL component
61
70
  */
62
71
  export function encodeForPurl(s) {
63
72
  return s && !s.includes("%40")
64
- ? encodeURIComponent(s).replace(/%3A/g, ":").replace(/%2F/g, "/")
73
+ ? encodeURIComponent(s)
74
+ .replace(
75
+ /[!'()*]/g,
76
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
77
+ )
78
+ .replace(/%3A/g, ":")
79
+ .replace(/%2F/g, "/")
65
80
  : s;
66
81
  }
67
82
 
@@ -328,6 +343,140 @@ export function isValidPurl(candidate) {
328
343
  }
329
344
  }
330
345
 
346
+ /**
347
+ * Split a purl into its components without requiring it to be well-formed.
348
+ *
349
+ * `Purl.parse` rejects a purl whose parts are not already escaped, which is the
350
+ * case this exists to handle: a `bom-ref` (cdxgen writes the decoded form of the
351
+ * purl there), a `go mod graph` token such as `github.com/Azure/go-autorest@v14.2.0+incompatible`,
352
+ * or a purl from a third-party document that left a `+` or a `://` unescaped.
353
+ * The separators (`:`, `/`, `@`, `?`, `#`, `&`, `=`) are structural and are read
354
+ * positionally; everything between them is decoded and handed back raw, ready
355
+ * for `build` to escape correctly.
356
+ *
357
+ * The version is taken after the last `@` of the path, so a scoped npm name
358
+ * (`@scope/name@1.0.0`) and a composer branch version (`dev-feature/x`) both
359
+ * split correctly.
360
+ *
361
+ * @param {string} purl Purl to split
362
+ * @returns {{type: string, namespace: (string|null), name: string, version: (string|null), qualifiers: (Object|null), subpath: (string|null)}|null} Parts, or null when the purl has no type and name
363
+ */
364
+ export function splitPurlLoose(purl) {
365
+ if (typeof purl !== "string" || !purl.startsWith("pkg:")) {
366
+ return null;
367
+ }
368
+ const decode = (value) => {
369
+ try {
370
+ return decodeURIComponent(value);
371
+ } catch {
372
+ return value;
373
+ }
374
+ };
375
+ let rest = purl.slice("pkg:".length).replace(/^\/+/, "");
376
+ let subpath = null;
377
+ const hash = rest.indexOf("#");
378
+ if (hash !== -1) {
379
+ subpath = decode(rest.slice(hash + 1)) || null;
380
+ rest = rest.slice(0, hash);
381
+ }
382
+ let qualifiers = null;
383
+ const query = rest.indexOf("?");
384
+ if (query !== -1) {
385
+ qualifiers = {};
386
+ for (const pair of rest.slice(query + 1).split("&")) {
387
+ const equals = pair.indexOf("=");
388
+ if (equals <= 0) {
389
+ continue;
390
+ }
391
+ qualifiers[pair.slice(0, equals)] = decode(pair.slice(equals + 1));
392
+ }
393
+ if (!Object.keys(qualifiers).length) {
394
+ qualifiers = null;
395
+ }
396
+ rest = rest.slice(0, query);
397
+ }
398
+ const slash = rest.indexOf("/");
399
+ if (slash <= 0) {
400
+ return null;
401
+ }
402
+ const type = rest.slice(0, slash);
403
+ let path = rest.slice(slash + 1);
404
+ let version = null;
405
+ const at = path.lastIndexOf("@");
406
+ if (at > 0 && path[at - 1] !== "/") {
407
+ version = decode(path.slice(at + 1)) || null;
408
+ path = path.slice(0, at);
409
+ }
410
+ const segments = path.split("/").filter(Boolean).map(decode);
411
+ const name = segments.pop();
412
+ if (!name) {
413
+ return null;
414
+ }
415
+ return {
416
+ type,
417
+ namespace: segments.length ? segments.join("/") : null,
418
+ name,
419
+ version,
420
+ qualifiers,
421
+ subpath,
422
+ };
423
+ }
424
+
425
+ /**
426
+ * Parse a purl string that may carry unescaped reserved characters.
427
+ *
428
+ * A well-formed purl is parsed strictly, so its components come back exactly as
429
+ * cdx-purl reads them. Anything else is split structurally by
430
+ * {@link splitPurlLoose}. Use this where the input is known to be a decoded or
431
+ * hand-written purl (a `bom-ref`, a workspace reference, a package manager's
432
+ * `name@version` token), never to accept an invalid purl into a `purl` field:
433
+ * pass the parts to `build()` instead, which escapes them.
434
+ *
435
+ * @param {string} purlString Purl or decoded purl
436
+ * @returns {{type: string, namespace: (string|null), name: string, version: (string|null), qualifiers: (Object|null), subpath: (string|null)}|null} Raw components, or null when nothing purl-shaped remains
437
+ */
438
+ export function parsePurlLoose(purlString) {
439
+ if (typeof purlString !== "string" || !purlString.startsWith("pkg:")) {
440
+ return null;
441
+ }
442
+ try {
443
+ const parsed = Purl.parse(purlString);
444
+ return {
445
+ type: parsed.type,
446
+ namespace: parsed.namespace || null,
447
+ name: parsed.name,
448
+ version: parsed.version || null,
449
+ qualifiers: parsed.qualifiers || null,
450
+ subpath: parsed.subpath || null,
451
+ };
452
+ } catch (err) {
453
+ if (!err?.code?.startsWith("E_")) {
454
+ throw err;
455
+ }
456
+ }
457
+ return splitPurlLoose(purlString);
458
+ }
459
+
460
+ /**
461
+ * Turn a decoded or hand-written purl into a canonical, correctly escaped one.
462
+ *
463
+ * `bom-ref` values are the decoded form of a purl
464
+ * (`pkg:npm/foo@1.0.0+build`), which cdx-purl rightly rejects as a purl. This
465
+ * recovers the purl the ref was made from by splitting it structurally and
466
+ * rebuilding it with `build()`, so every reserved character is escaped exactly
467
+ * once.
468
+ *
469
+ * @param {string} purlString Purl or decoded purl
470
+ * @returns {string|null} Canonical purl string, or `null` when no valid purl can be built
471
+ */
472
+ export function canonicalPurlFromLooseString(purlString) {
473
+ const parts = parsePurlLoose(purlString);
474
+ if (!parts) {
475
+ return null;
476
+ }
477
+ return tryBuildPurl(parts);
478
+ }
479
+
331
480
  /**
332
481
  * Parse a purl string, returning `null` instead of throwing when it is invalid.
333
482
  *
@@ -1,9 +1,8 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
3
 
4
- import * as toml from "smol-toml";
5
-
6
4
  import { safeExistsSync } from "../core/fs.js";
5
+ import { parseToml } from "../core/toml.js";
7
6
 
8
7
  // Keep this list conservative and high-signal: it is meant to surface common
9
8
  // Rust native build helpers for audit triage, not to exhaustively model every
@@ -67,7 +66,7 @@ function createFormulationComponent(name, filePath, toolName) {
67
66
  function parseCargoManifest(filePath) {
68
67
  let cargoData;
69
68
  try {
70
- cargoData = toml.parse(readFileSync(filePath, { encoding: "utf-8" }));
69
+ cargoData = parseToml(readFileSync(filePath, { encoding: "utf-8" }));
71
70
  } catch {
72
71
  return undefined;
73
72
  }
@@ -252,7 +251,7 @@ function parseCargoManifest(filePath) {
252
251
  function parsePyProject(filePath) {
253
252
  let pyprojectData;
254
253
  try {
255
- pyprojectData = toml.parse(readFileSync(filePath, { encoding: "utf-8" }));
254
+ pyprojectData = parseToml(readFileSync(filePath, { encoding: "utf-8" }));
256
255
  } catch {
257
256
  return undefined;
258
257
  }
@@ -14,6 +14,7 @@
14
14
  import { build, Purl } from "@cdxgen/cdx-purl";
15
15
 
16
16
  import { isSpdxJsonLd } from "./bomUtils.js";
17
+ import { splitPurlLoose } from "./purl.js";
17
18
  import { toCycloneDxLikeBom } from "./spdxUtils.js";
18
19
 
19
20
  // A document larger than this is not a plausible SBOM for a single
@@ -150,75 +151,6 @@ function encodeVersionBuildMetadata(purl) {
150
151
  return `${head.slice(0, at + 1)}${head.slice(at + 1).replaceAll("+", "%2B")}${tail}`;
151
152
  }
152
153
 
153
- /**
154
- * Split a purl into its components without requiring it to be well-formed.
155
- *
156
- * `Purl.parse` rejects a purl whose parts are not already escaped, which is the
157
- * case this exists to handle. The separators (`:`, `/`, `@`, `?`, `#`, `&`,
158
- * `=`) are structural and are read positionally; everything between them is
159
- * decoded and handed back raw, ready for `build` to escape correctly.
160
- *
161
- * @param {string} purl Purl to split
162
- * @returns {{type: string, namespace: (string|null), name: string, version: (string|null), qualifiers: (Object|null), subpath: (string|null)}|null} Parts, or null when the purl has no type and name
163
- */
164
- function splitPurl(purl) {
165
- const decode = (value) => {
166
- try {
167
- return decodeURIComponent(value);
168
- } catch {
169
- return value;
170
- }
171
- };
172
- let rest = purl.slice("pkg:".length).replace(/^\/+/, "");
173
- let subpath = null;
174
- const hash = rest.indexOf("#");
175
- if (hash !== -1) {
176
- subpath = decode(rest.slice(hash + 1)) || null;
177
- rest = rest.slice(0, hash);
178
- }
179
- let qualifiers = null;
180
- const query = rest.indexOf("?");
181
- if (query !== -1) {
182
- qualifiers = {};
183
- for (const pair of rest.slice(query + 1).split("&")) {
184
- const equals = pair.indexOf("=");
185
- if (equals <= 0) {
186
- continue;
187
- }
188
- qualifiers[pair.slice(0, equals)] = decode(pair.slice(equals + 1));
189
- }
190
- if (!Object.keys(qualifiers).length) {
191
- qualifiers = null;
192
- }
193
- rest = rest.slice(0, query);
194
- }
195
- const slash = rest.indexOf("/");
196
- if (slash <= 0) {
197
- return null;
198
- }
199
- const type = rest.slice(0, slash);
200
- let path = rest.slice(slash + 1);
201
- let version = null;
202
- const at = path.lastIndexOf("@");
203
- if (at > 0) {
204
- version = decode(path.slice(at + 1)) || null;
205
- path = path.slice(0, at);
206
- }
207
- const segments = path.split("/").filter(Boolean).map(decode);
208
- const name = segments.pop();
209
- if (!name) {
210
- return null;
211
- }
212
- return {
213
- type,
214
- namespace: segments.length ? segments.join("/") : null,
215
- name,
216
- version,
217
- qualifiers,
218
- subpath,
219
- };
220
- }
221
-
222
154
  /**
223
155
  * Repair the purl of a component read from a third-party document.
224
156
  *
@@ -247,7 +179,7 @@ export function repairPurl(component) {
247
179
  } catch {
248
180
  // Fall through to the structural rebuild below.
249
181
  }
250
- const parts = splitPurl(repaired);
182
+ const parts = splitPurlLoose(repaired);
251
183
  if (!parts) {
252
184
  return;
253
185
  }
@@ -0,0 +1,153 @@
1
+ import { Purl } from "@cdxgen/cdx-purl";
2
+
3
+ // Scala libraries publish their Maven artifacts with a trailing Scala binary
4
+ // version suffix (`jwt-core_3`, `coursier_2.13`), optionally preceded by a
5
+ // platform suffix for Scala.js and Scala Native builds (`upickle_sjs1_3`,
6
+ // `upickle_native0.5_3`). sbt components strip the binary suffix and keep the
7
+ // platform suffix (`jwt-core`, `upickle_sjs1`), so the same library is named
8
+ // differently depending on the build tool that reported it.
9
+ const SCALA_BINARY_SUFFIX_REGEX = /_(2\.\d+|3)$/;
10
+ const SCALA_PLATFORM_SUFFIX_REGEX = /_(sjs1|sjs0\.6|native0\.\d+)$/;
11
+
12
+ /**
13
+ * Parse a Scala-published Maven artifact name into its coordinate parts.
14
+ *
15
+ * @param {string} name Maven artifactId, for example `upickle_sjs1_3`
16
+ * @returns {{ originalName: string, artifactBase: string, binaryVersion: string|null,
17
+ * platformSuffix: string, purlName: string }} The artifact name without any
18
+ * Scala suffix (`upickle`), the Scala binary version from the suffix (`3`),
19
+ * the platform suffix including the underscore (`_sjs1`) or an empty string,
20
+ * and the artifact name sbt-style purls emit (binary suffix dropped, platform
21
+ * suffix kept).
22
+ */
23
+ export function parseScalaArtifact(name) {
24
+ const result = {
25
+ originalName: name,
26
+ artifactBase: name,
27
+ binaryVersion: null,
28
+ platformSuffix: "",
29
+ purlName: name,
30
+ };
31
+ if (!name) {
32
+ return result;
33
+ }
34
+ let rest = name;
35
+ const binaryMatch = rest.match(SCALA_BINARY_SUFFIX_REGEX);
36
+ if (binaryMatch) {
37
+ result.binaryVersion = binaryMatch[1];
38
+ rest = rest.slice(0, rest.length - binaryMatch[0].length);
39
+ }
40
+ const platformMatch = rest.match(SCALA_PLATFORM_SUFFIX_REGEX);
41
+ if (platformMatch) {
42
+ result.platformSuffix = platformMatch[0];
43
+ rest = rest.slice(0, rest.length - platformMatch[0].length);
44
+ }
45
+ result.artifactBase = rest;
46
+ result.purlName = binaryMatch ? `${rest}${result.platformSuffix}` : name;
47
+ return result;
48
+ }
49
+
50
+ /**
51
+ * Build a qualifier-free coordinate key for a Scala-published artifact.
52
+ *
53
+ * The key ignores purl qualifiers such as `repository_url` and `type`, so the
54
+ * POM-coordinate keys of the jar namespace mapping and the sbt-style purls of
55
+ * the components resolve to the same entry.
56
+ *
57
+ * @param {Object} parts Coordinate parts
58
+ * @param {string} [parts.type] Purl type, defaults to `maven`
59
+ * @param {string} [parts.group] Maven groupId
60
+ * @param {string} parts.artifactBase Artifact name without any Scala suffix
61
+ * @param {string} [parts.platformSuffix] `_sjs1`, `_native0.5` or empty
62
+ * @param {string} [parts.version] Package version
63
+ * @returns {string} The coordinate key
64
+ */
65
+ export function scalaCoordinateKey({
66
+ type = "maven",
67
+ group = "",
68
+ artifactBase = "",
69
+ platformSuffix = "",
70
+ version = "",
71
+ } = {}) {
72
+ return [type, group, artifactBase, platformSuffix, version].join(":");
73
+ }
74
+
75
+ /**
76
+ * Build the qualifier-free coordinate key of a purl string.
77
+ *
78
+ * The artifact name is normalized with {@link parseScalaArtifact}, so
79
+ * `pkg:maven/g/foo_3@1.0` and the sbt-style `pkg:maven/g/foo@1.0` share a key,
80
+ * while the Scala.js build `foo_sjs1_3` keeps its own key (`foo` plus the
81
+ * `_sjs1` platform suffix) and never joins the plain `foo` component.
82
+ *
83
+ * @param {string} purlString Package URL to normalize
84
+ * @returns {string|undefined} The coordinate key, or undefined when the string
85
+ * is not a parseable purl
86
+ */
87
+ export function coordinateKeyFromPurl(purlString) {
88
+ return scalaCoordinateOfPurl(purlString)?.key;
89
+ }
90
+
91
+ /**
92
+ * The coordinate key of a purl together with the Scala binary version its
93
+ * artifact name carries, if any.
94
+ *
95
+ * @param {string} purlString Package URL to normalize
96
+ * @returns {{ key: string, binaryVersion: string|null }|undefined} The key
97
+ * and binary version, or undefined when the string is not a parseable purl
98
+ */
99
+ export function scalaCoordinateOfPurl(purlString) {
100
+ if (!purlString?.startsWith("pkg:")) {
101
+ return undefined;
102
+ }
103
+ let purlObj;
104
+ try {
105
+ purlObj = Purl.parse(purlString);
106
+ } catch (_err) {
107
+ return undefined;
108
+ }
109
+ if (!purlObj?.name) {
110
+ return undefined;
111
+ }
112
+ const scalaInfo = parseScalaArtifact(purlObj.name);
113
+ return {
114
+ key: scalaCoordinateKey({
115
+ type: purlObj.type,
116
+ group: purlObj.namespace || "",
117
+ artifactBase: scalaInfo.artifactBase,
118
+ platformSuffix: scalaInfo.platformSuffix,
119
+ version: purlObj.version || "",
120
+ }),
121
+ binaryVersion: scalaInfo.binaryVersion,
122
+ };
123
+ }
124
+
125
+ // An artifact name that already ends in a full Scala version, as scala-cli
126
+ // `:::` and sbt `CrossVersion.full` artifacts do (`_3.3.7`, `_2.13.16`). A
127
+ // binary suffix such as `_3` left on a name is part of the name: the
128
+ // component producers strip exactly one, into the compiler version.
129
+ const SCALA_FULL_VERSION_SUFFIX_REGEX = /_\d+\.\d+\.\d+(?:-[A-Za-z0-9.]+)?$/;
130
+
131
+ /**
132
+ * The artifactId the library is published under, for cache and repository
133
+ * lookups only. Components a Scala build tool reports carry the name with the
134
+ * Scala binary suffix stripped (the platform suffix kept) together with
135
+ * `cdx:scala:compilerVersion`; on disk and in the repository the artifact is
136
+ * `cats-core_2.13` or `upickle_sjs1_3`. Names that end in a full Scala version
137
+ * are used as they are.
138
+ *
139
+ * The component's own `name`, `purl` and `bom-ref` never change.
140
+ *
141
+ * @param {Object} pkg Package with `name` and `properties`.
142
+ * @returns {string} The artifactId to look up.
143
+ */
144
+ export function publishedArtifactId(pkg) {
145
+ const name = pkg?.name;
146
+ if (!name || SCALA_FULL_VERSION_SUFFIX_REGEX.test(name)) {
147
+ return name;
148
+ }
149
+ const binaryVersion = (pkg.properties || []).find(
150
+ (property) => property.name === "cdx:scala:compilerVersion",
151
+ )?.value;
152
+ return binaryVersion ? `${name}_${binaryVersion}` : name;
153
+ }