@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
@@ -4,13 +4,13 @@ import process from "node:process";
4
4
  import { URL } from "node:url";
5
5
 
6
6
  import { build } from "@cdxgen/cdx-purl";
7
- import * as toml from "smol-toml";
8
7
  import { parse as _load } from "yaml";
9
8
 
10
9
  import { DEBUG_MODE } from "../core/activity.js";
11
10
  import { recordDegradation } from "../core/buildLedger.js";
12
11
  import { getAllFiles, safeExistsSync, safeSpawnSync } from "../core/fs.js";
13
12
  import { PYTHON_STD_MODULES } from "../core/state.js";
13
+ import { parseToml } from "../core/toml.js";
14
14
  import { tryBuildPurl } from "../inventory/purl.js";
15
15
  import {
16
16
  createExternalReferenceKey,
@@ -95,8 +95,11 @@ export function parsePyRequiresDist(dist_string) {
95
95
  * Method to parse pipfile.lock data
96
96
  *
97
97
  * @param {Object} lockData JSON data from Pipfile.lock
98
+ * @param {Object[]} [installedMetadata] Components parsed from the installed
99
+ * `*.dist-info/METADATA` files of this machine, which answer before PyPI
100
+ * is asked
98
101
  */
99
- export async function parsePiplockData(lockData) {
102
+ export async function parsePiplockData(lockData, installedMetadata) {
100
103
  const pkgList = [];
101
104
  Object.keys(lockData)
102
105
  .filter((i) => i !== "_meta")
@@ -105,11 +108,16 @@ export async function parsePiplockData(lockData) {
105
108
  Object.keys(depBlock).forEach((p) => {
106
109
  const pkg = depBlock[p];
107
110
  if (Object.hasOwn(pkg, "version")) {
108
- const versionStr = pkg.version.replace("==", "");
111
+ // `===` (arbitrary equality) pins as tightly as `==`, so both are
112
+ // stripped whole rather than leaving a stray `=` behind.
113
+ const versionStr = pkg.version.replace(/^\s*={2,3}\s*/, "");
109
114
  pkgList.push({ name: p, version: versionStr });
110
115
  }
111
116
  });
112
117
  });
118
+ // The installed distributions answer first, so PyPI is asked only about
119
+ // the packages they do not cover.
120
+ enrichFromInstalledMetadata(pkgList, installedMetadata);
113
121
  return await getPyMetadata(pkgList, false);
114
122
  }
115
123
 
@@ -272,8 +280,12 @@ function collectPythonManifestSource(pkg) {
272
280
  { kind: "url", value: pkg?.vcs?.url },
273
281
  { kind: "url", value: pkg?.source?.url },
274
282
  { kind: "path", value: pkg?.source?.path },
283
+ { kind: "path", value: pkg?.source?.directory },
275
284
  { kind: "path", value: pkg?.source?.editable },
276
285
  { kind: "path", value: pkg?.source?.virtual },
286
+ { kind: "path", value: pkg?.directory?.path },
287
+ { kind: "url", value: pkg?.archive?.url },
288
+ { kind: "path", value: pkg?.archive?.path },
277
289
  ];
278
290
  for (const candidate of sourceCandidates) {
279
291
  if (typeof candidate.value !== "string" || !candidate.value.trim()) {
@@ -393,7 +405,7 @@ export function parsePyProjectTomlFile(tomlFile) {
393
405
  const groupDepsKeys = {};
394
406
  const dependencySourceMap = {};
395
407
  try {
396
- tomlData = toml.parse(readFileSync(tomlFile, { encoding: "utf-8" }));
408
+ tomlData = parseToml(readFileSync(tomlFile, { encoding: "utf-8" }));
397
409
  } catch (err) {
398
410
  console.log(`Error while parsing the pyproject file ${tomlFile}.`, err);
399
411
  }
@@ -786,14 +798,114 @@ function collectPythonLockMetadataDistributionReferences(fileEntries) {
786
798
  return distributionReferences;
787
799
  }
788
800
 
801
+ /**
802
+ * Normalise a Python distribution name the way PEP 503 does, so an installed
803
+ * distribution and the lockfile entry it belongs to compare equal whatever
804
+ * separator and case each of them used.
805
+ *
806
+ * @param {string} value Distribution name
807
+ * @returns {string|undefined} The normalised name, or undefined for no name
808
+ */
809
+ export function normalizePep503Name(value) {
810
+ if (typeof value !== "string" || !value.trim()) {
811
+ return undefined;
812
+ }
813
+ return value
814
+ .trim()
815
+ .toLowerCase()
816
+ .replaceAll(/[-_.]+/g, "-");
817
+ }
818
+
819
+ /**
820
+ * The key an installed distribution and a lockfile entry are matched by: the
821
+ * PEP 503 normalised name and the version.
822
+ *
823
+ * @param {Object} p Component with `name` and `version`
824
+ * @returns {string|undefined} The key, or undefined without a name or version
825
+ */
826
+ export function installedMetadataKey(p) {
827
+ const name = normalizePep503Name(p?.name);
828
+ const version = typeof p?.version === "string" ? p.version.trim() : "";
829
+ return name && version ? `${name}@${version}` : undefined;
830
+ }
831
+
832
+ /**
833
+ * Enrich lockfile entries from the installed distributions on this machine,
834
+ * before PyPI is asked.
835
+ *
836
+ * Each entry of `installedMetadata` is a component as
837
+ * {@link parseBdistMetadata} builds it from an installed `*.dist-info/METADATA`
838
+ * file. An entry is matched to the lockfile component with the same PEP 503
839
+ * normalised name and the same version, and fills the fields it lacks, so the
840
+ * registry is left only what the installed distributions do not answer. An
841
+ * installed release of another version describes a different release, so it
842
+ * enriches nothing. The distributions enrich the entries in place; no new
843
+ * components are created.
844
+ *
845
+ * @param {Object[]} pkgList Lockfile components, enriched in place
846
+ * @param {Object[]} installedMetadata Components parsed from installed
847
+ * `*.dist-info/METADATA` files
848
+ * @returns {Set<string>} The keys of the installations that enriched an entry,
849
+ * as {@link installedMetadataKey} builds them, so a caller that also walks
850
+ * the METADATA files can skip those as components
851
+ */
852
+ export function enrichFromInstalledMetadata(pkgList, installedMetadata) {
853
+ const byKey = new Map();
854
+ for (const p of pkgList || []) {
855
+ const key = installedMetadataKey(p);
856
+ if (key && !byKey.has(key)) {
857
+ byKey.set(key, p);
858
+ }
859
+ }
860
+ const enriched = new Set();
861
+ for (const md of installedMetadata || []) {
862
+ const key = installedMetadataKey(md);
863
+ const p = key ? byKey.get(key) : undefined;
864
+ if (!p) {
865
+ continue;
866
+ }
867
+ enriched.add(key);
868
+ if (!p.description?.trim() && md.description?.trim()) {
869
+ p.description = md.description;
870
+ }
871
+ if (!p.license && md.licenses?.length) {
872
+ // The licence field holds the names a lockfile consumer expects; the
873
+ // CycloneDX licence objects travel in `licenses`.
874
+ p.license = md.licenses
875
+ .map((alicense) => alicense.expression || alicense.license?.name)
876
+ .filter((aname) => aname?.length);
877
+ p.licenses = md.licenses;
878
+ }
879
+ if (!p.homepage && md.homepage) {
880
+ p.homepage = md.homepage;
881
+ }
882
+ if (!p.repository && md.repository) {
883
+ p.repository = md.repository;
884
+ }
885
+ if (!p.author && md.author) {
886
+ p.author = md.author;
887
+ }
888
+ }
889
+ return enriched;
890
+ }
891
+
789
892
  /**
790
893
  * Method to parse python lock files such as poetry.lock, pdm.lock, uv.lock, and pylock.toml.
791
894
  *
792
895
  * @param {string} lockData Raw TOML text from poetry.lock, pdm.lock, uv.lock, or pylock.toml
793
896
  * @param {string} lockFile Lock file name for evidence
794
897
  * @param {string} pyProjectFile pyproject.toml file
898
+ * @param {Object} [options] Options
899
+ * @param {Object[]} [options.installedMetadata] Components parsed from the
900
+ * installed `*.dist-info/METADATA` files of this machine, which answer
901
+ * before PyPI is asked
795
902
  */
796
- export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
903
+ export async function parsePyLockData(
904
+ lockData,
905
+ lockFile,
906
+ pyProjectFile,
907
+ options = {},
908
+ ) {
797
909
  let pkgList = [];
798
910
  const rootList = [];
799
911
  const dependenciesList = [];
@@ -940,7 +1052,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
940
1052
  }
941
1053
  let lockTomlObj;
942
1054
  try {
943
- lockTomlObj = toml.parse(lockData);
1055
+ lockTomlObj = parseToml(lockData);
944
1056
  } catch (err) {
945
1057
  if (lockFile) {
946
1058
  // The failing file names the manager whose lock command regenerates
@@ -1416,6 +1528,9 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
1416
1528
  dependsOn: [...dependsOnList].sort(),
1417
1529
  });
1418
1530
  }
1531
+ // The installed distributions answer first, so PyPI is asked only about the
1532
+ // packages they do not cover.
1533
+ enrichFromInstalledMetadata(pkgList, options.installedMetadata);
1419
1534
  pkgList = await getPyMetadata(pkgList, false);
1420
1535
  return {
1421
1536
  parentComponent,
@@ -1427,16 +1542,33 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
1427
1542
  };
1428
1543
  }
1429
1544
 
1545
+ /**
1546
+ * First version clause of a PEP 508 requirement: an operator and the version
1547
+ * that follows it.
1548
+ *
1549
+ * `===` (arbitrary equality) is tried before `==`, whitespace may separate the
1550
+ * operator from the version, and a version may carry a PEP 440 epoch (`1!2.0`)
1551
+ * or a local version label (`2.1.0+cpu`).
1552
+ */
1553
+ const PEP440_VERSION_CLAUSE = /(===|==|!=|<=|>=|~=|<|>)\s*([0-9A-Za-z.*_+!-]*)/;
1554
+
1430
1555
  /**
1431
1556
  * Method to parse requirements.txt file. This must be replaced with atom parsedeps.
1432
1557
  *
1433
1558
  * @param {String} reqFile Requirements.txt file
1434
1559
  * @param {Boolean} fetchDepsInfo Fetch dependencies info from pypi
1560
+ * @param {Object[]} [installedMetadata] Components parsed from the installed
1561
+ * `*.dist-info/METADATA` files of this machine, which answer before PyPI
1562
+ * is asked
1435
1563
  *
1436
1564
  * @returns {Promise[Array<Object>]} List of direct dependencies from the requirements file
1437
1565
  */
1438
- export async function parseReqFile(reqFile, fetchDepsInfo = false) {
1439
- return await parseReqData(reqFile, null, fetchDepsInfo);
1566
+ export async function parseReqFile(
1567
+ reqFile,
1568
+ fetchDepsInfo = false,
1569
+ installedMetadata = undefined,
1570
+ ) {
1571
+ return await parseReqData(reqFile, null, fetchDepsInfo, installedMetadata);
1440
1572
  }
1441
1573
 
1442
1574
  const LICENSE_ID_COMMENTS_PATTERN =
@@ -1465,9 +1597,17 @@ function parseLicenseComment(comment) {
1465
1597
  * @param {String} reqFile Requirements.txt file
1466
1598
  * @param {Object} reqData Requirements.txt data for internal invocations from setup.py file etc.
1467
1599
  * @param {Boolean} fetchDepsInfo Fetch dependencies info from pypi
1600
+ * @param {Object[]} [installedMetadata] Components parsed from the installed
1601
+ * `*.dist-info/METADATA` files of this machine, which answer before PyPI
1602
+ * is asked
1468
1603
  * @returns {Promise<Array<Object>>} List of direct dependencies from the requirements file
1469
1604
  */
1470
- async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
1605
+ async function parseReqData(
1606
+ reqFile,
1607
+ reqData = null,
1608
+ fetchDepsInfo = false,
1609
+ installedMetadata = undefined,
1610
+ ) {
1471
1611
  const pkgList = [];
1472
1612
  let compScope;
1473
1613
  if (!reqFile && !reqData) {
@@ -1513,10 +1653,13 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
1513
1653
  continue;
1514
1654
  }
1515
1655
  let comment = null;
1516
- const commentMatch = l.match(/\s+#(.*)$/);
1517
- if (commentMatch) {
1518
- comment = commentMatch[1].trim();
1519
- l = l.substring(0, commentMatch.index).trim();
1656
+ // Equivalent to /\s+#(.*)$/ but linear: an unanchored `\s+` backtracks
1657
+ // quadratically on long whitespace runs without a '#', which a crafted
1658
+ // requirements.txt used to burn seconds of CPU per line.
1659
+ const hashIndex = l.indexOf("#");
1660
+ if (hashIndex > 0 && /\s/.test(l[hashIndex - 1])) {
1661
+ comment = l.substring(hashIndex + 1).trim();
1662
+ l = l.substring(0, hashIndex).trim();
1520
1663
  }
1521
1664
  const properties = reqFile
1522
1665
  ? [
@@ -1607,9 +1750,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
1607
1750
  if (PYTHON_STD_MODULES.includes(name)) {
1608
1751
  continue;
1609
1752
  }
1610
- const versionMatch = versionSpecifiers.match(
1611
- /(==|!=|<=|>=|<|>|~=)([0-9.a-zA-Z_*\-+]*)/,
1612
- );
1753
+ const versionMatch = versionSpecifiers.match(PEP440_VERSION_CLAUSE);
1613
1754
  let version = null;
1614
1755
  if (versionMatch) {
1615
1756
  version = versionMatch[2].replaceAll("*", "0") || null;
@@ -1636,7 +1777,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
1636
1777
  value: extras.join(","),
1637
1778
  });
1638
1779
  }
1639
- if (versionSpecifiers && !versionSpecifiers.startsWith("==")) {
1780
+ if (versionSpecifiers && !versionSpecifiers.trim().startsWith("==")) {
1640
1781
  properties.push({
1641
1782
  name: "cdx:pypi:versionSpecifiers",
1642
1783
  value: versionSpecifiers.trim(),
@@ -1673,9 +1814,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
1673
1814
  if (PYTHON_STD_MODULES.includes(name)) {
1674
1815
  continue;
1675
1816
  }
1676
- const versionMatch = versionSpecifiers.match(
1677
- /(==|!=|<=|>=|<|>|~=)([0-9.a-zA-Z_*\-+]*)/,
1678
- );
1817
+ const versionMatch = versionSpecifiers.match(PEP440_VERSION_CLAUSE);
1679
1818
  let version = null;
1680
1819
  if (versionMatch) {
1681
1820
  version = versionMatch[2].replaceAll("*", "0") || null;
@@ -1691,7 +1830,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
1691
1830
  if (licenses) {
1692
1831
  apkg.licenses = licenses;
1693
1832
  }
1694
- if (versionSpecifiers && !versionSpecifiers.startsWith("==")) {
1833
+ if (versionSpecifiers && !versionSpecifiers.trim().startsWith("==")) {
1695
1834
  properties.push({
1696
1835
  name: "cdx:pypi:versionSpecifiers",
1697
1836
  value: versionSpecifiers.trim(),
@@ -1721,6 +1860,9 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
1721
1860
  pkgList.push(apkg);
1722
1861
  }
1723
1862
  }
1863
+ // The installed distributions answer first, so PyPI is asked only about
1864
+ // the packages they do not cover.
1865
+ enrichFromInstalledMetadata(pkgList, installedMetadata);
1724
1866
  return await getPyMetadata(pkgList, fetchDepsInfo);
1725
1867
  }
1726
1868
 
@@ -2031,7 +2173,7 @@ export function parsePixiTomlFile(pixiToml) {
2031
2173
  const pixiTomlFile = readFileSync(pixiToml, { encoding: "utf-8" });
2032
2174
  let tomlData;
2033
2175
  try {
2034
- tomlData = toml.parse(pixiTomlFile);
2176
+ tomlData = parseToml(pixiTomlFile);
2035
2177
  } catch (err) {
2036
2178
  console.log(`Error while parsing the pixi file ${pixiToml}.`, err);
2037
2179
  return {};
@@ -2107,7 +2249,7 @@ export function generatePixiLockFile(_path) {
2107
2249
  export function parseMojoProject(mojoProjectFile) {
2108
2250
  let manifest;
2109
2251
  try {
2110
- manifest = toml.parse(readFileSync(mojoProjectFile, "utf-8"));
2252
+ manifest = parseToml(readFileSync(mojoProjectFile, "utf-8"));
2111
2253
  } catch (error) {
2112
2254
  console.warn(`Failed to parse ${mojoProjectFile}: ${error.message}`);
2113
2255
  return { pkgList: [], parentComponent: {} };
@@ -2201,6 +2343,7 @@ function normaliseMojoVersion(spec) {
2201
2343
  const trimmed = spec.trim();
2202
2344
  if (!trimmed) return undefined;
2203
2345
  // Strip common comparison operators and take the first concrete version.
2204
- const match = trimmed.match(/(\d+(?:\.\d+)*(?:[+-][A-Za-z0-9.]+)?)/);
2346
+ // A PEP 440 epoch (`1!2.0`) is part of the version, not a separator.
2347
+ const match = trimmed.match(/((?:\d+!)?\d+(?:\.\d+)*(?:[+-][A-Za-z0-9.]+)?)/);
2205
2348
  return match ? match[1] : undefined;
2206
2349
  }
@@ -3,12 +3,12 @@ import { sep as _sep, basename, dirname, join, resolve } from "node:path";
3
3
 
4
4
  import { build } from "@cdxgen/cdx-purl";
5
5
  import { globSync } from "glob";
6
- import * as toml from "smol-toml";
7
6
 
8
7
  import { DEBUG_MODE } from "../core/activity.js";
9
8
  import { shouldFetchLicense } from "../core/env.js";
10
9
  import { safeExistsSync } from "../core/fs.js";
11
10
  import { traceLog } from "../core/logger.js";
11
+ import { parseToml } from "../core/toml.js";
12
12
  import { getCratesMetadata, normalizeCargoIntegrity } from "./ecosystems.js";
13
13
 
14
14
  /**
@@ -204,7 +204,7 @@ function readCargoTomlData(cargoTomlFile) {
204
204
  return undefined;
205
205
  }
206
206
  try {
207
- return toml.parse(readFileSync(cargoTomlFile, { encoding: "utf-8" }));
207
+ return parseToml(readFileSync(cargoTomlFile, { encoding: "utf-8" }));
208
208
  } catch (error) {
209
209
  traceLog("cargo", {
210
210
  cargoTomlFile,
@@ -1082,7 +1082,7 @@ export async function parseCargoData(
1082
1082
  return pkgList;
1083
1083
  }
1084
1084
 
1085
- const cargoData = toml.parse(
1085
+ const cargoData = parseToml(
1086
1086
  readFileSync(cargoLockFile, { encoding: "utf-8" }),
1087
1087
  );
1088
1088
  if (!cargoData) {
@@ -1290,7 +1290,7 @@ export function parseCargoManifestDependencyData(cargoTomlFile, context = {}) {
1290
1290
  */
1291
1291
  export function parseCargoDependencyData(cargoLockData) {
1292
1292
  const purlFromPackageInfo = (pkg) => decodeURIComponent(buildCargoPurl(pkg));
1293
- const cargoData = toml.parse(cargoLockData);
1293
+ const cargoData = parseToml(cargoLockData);
1294
1294
  const packageNode = cargoData?.package;
1295
1295
  if (!packageNode || !Array.isArray(packageNode)) {
1296
1296
  return [];
@@ -1309,15 +1309,20 @@ export function parseCargoDependencyData(cargoLockData) {
1309
1309
  );
1310
1310
  }
1311
1311
  });
1312
- // Create a map of package names to package objects
1313
- const pkgMap = pkgList.reduce((acc, item) => {
1314
- acc[item.name] = item;
1315
- return acc;
1316
- }, {});
1317
- const result = [];
1318
- // parse dependency version
1319
- Object.values(pkgMap).forEach((pkg) => {
1320
- const dependsOn = new Set();
1312
+ // A lock file can hold several versions of one crate. Cargo then names the
1313
+ // version in every reference to it (`getrandom 0.3.4`), and a bare name is
1314
+ // left only for crates with a single version.
1315
+ const pkgsByName = new Map();
1316
+ for (const pkg of pkgList) {
1317
+ pkgsByName.set(pkg.name, [...(pkgsByName.get(pkg.name) || []), pkg]);
1318
+ }
1319
+ // Every package gets its own entry, so each version keeps its edges. The
1320
+ // same name and version from two sources share a purl; their edges merge.
1321
+ const edgesByRef = new Map();
1322
+ for (const pkg of pkgList) {
1323
+ const ref = purlFromPackageInfo(pkg);
1324
+ const dependsOn = edgesByRef.get(ref) || new Set();
1325
+ edgesByRef.set(ref, dependsOn);
1321
1326
  pkg.dependencies?.forEach((dep) => {
1322
1327
  if (dep.indexOf(" ") !== -1) {
1323
1328
  // fill version in dependency definition like `libc 0.2.79`
@@ -1328,20 +1333,21 @@ export function parseCargoDependencyData(cargoLockData) {
1328
1333
  version: depSplit[1].trim(),
1329
1334
  }),
1330
1335
  );
1331
- } else if (pkgMap[dep]) {
1332
- dependsOn.add(purlFromPackageInfo(pkgMap[dep]));
1336
+ } else if (pkgsByName.get(dep)?.length === 1) {
1337
+ dependsOn.add(purlFromPackageInfo(pkgsByName.get(dep)[0]));
1333
1338
  } else if (DEBUG_MODE) {
1334
1339
  console.warn(
1335
- `The package "${dep.name}" appears as a dependency to "${pkg.name}" but is not itself listed in the Cargo.lock file. The Cargo.lock file is invalid! The produced SBOM will not list ${dep.name} as a dependency.`,
1340
+ pkgsByName.has(dep)
1341
+ ? `The package "${dep}" appears as a dependency to "${pkg.name}" without a version, but the Cargo.lock file lists more than one version of it. The produced SBOM will not list ${dep} as a dependency.`
1342
+ : `The package "${dep}" appears as a dependency to "${pkg.name}" but is not itself listed in the Cargo.lock file. The Cargo.lock file is invalid! The produced SBOM will not list ${dep} as a dependency.`,
1336
1343
  );
1337
1344
  }
1338
1345
  });
1339
- result.push({
1340
- ref: purlFromPackageInfo(pkg),
1341
- dependsOn: [...dependsOn],
1342
- });
1343
- });
1344
- return result;
1346
+ }
1347
+ return [...edgesByRef].map(([ref, dependsOn]) => ({
1348
+ ref,
1349
+ dependsOn: [...dependsOn],
1350
+ }));
1345
1351
  }
1346
1352
 
1347
1353
  /**