@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
@@ -390,6 +390,27 @@ function targetSlug(target) {
390
390
  return `${target.type}-${normalized || "package"}-${version}-${digest}`;
391
391
  }
392
392
 
393
+ /**
394
+ * Build a deterministic directory name for a repository checkout, keyed by
395
+ * the repository URL and the ref it is cloned at.
396
+ *
397
+ * Keying by the coordinates rather than the package means the packages of
398
+ * one monorepo share a single checkout instead of each cloning the whole
399
+ * repository, while a different ref, which is a different snapshot of the
400
+ * tree, never shares one.
401
+ *
402
+ * @param {string} repoUrl Repository URL
403
+ * @param {string | undefined} gitRef Ref the checkout is cloned at
404
+ * @returns {string} slug string
405
+ */
406
+ function repositoryCheckoutSlug(repoUrl, gitRef) {
407
+ const digest = createHash("sha256")
408
+ .update(`${repoUrl}\n${gitRef || ""}`)
409
+ .digest("hex")
410
+ .slice(0, 16);
411
+ return `repo-${digest}`;
412
+ }
413
+
393
414
  /**
394
415
  * Ensure a parent directory exists before writing a file.
395
416
  *
@@ -1378,7 +1399,12 @@ async function ensureCheckout(target, resolution, workspaceDir, gitRef) {
1378
1399
  if (!safeExistsSync(resolvedWorkspaceDir)) {
1379
1400
  safeMkdirSync(resolvedWorkspaceDir, { recursive: true });
1380
1401
  }
1381
- const cloneDir = join(resolvedWorkspaceDir, targetSlug(target));
1402
+ // One checkout per repository and ref, so packages of the same monorepo
1403
+ // share it; each package still keeps its own cache and report directory.
1404
+ const cloneDir = join(
1405
+ resolvedWorkspaceDir,
1406
+ repositoryCheckoutSlug(resolution.repoUrl, gitRef),
1407
+ );
1382
1408
  if (safeExistsSync(join(cloneDir, ".git"))) {
1383
1409
  return {
1384
1410
  cleanup: false,
@@ -2635,11 +2661,17 @@ export async function runAuditFromBoms(inputBoms, options) {
2635
2661
  total: extractedTargets.targets.length,
2636
2662
  type: "run:start",
2637
2663
  });
2638
- // Every target's repository lookup is known here, before the first clone,
2639
- // so they are resolved in one round rather than one per iteration below.
2640
- await prefetchGitUrlSources(
2641
- extractedTargets.targets.map((target) => target.purl),
2642
- );
2664
+ // Every target's repository lookup is known here, before the first
2665
+ // clone, so they are resolved in one round rather than one per iteration
2666
+ // below. A target whose cached child BOM already answers it is resolved
2667
+ // from the workspace alone, so only the targets that will be cloned take
2668
+ // part in the round.
2669
+ const purlsToResolve = extractedTargets.targets
2670
+ .filter((target) => !loadCachedChildBom(options.workspaceDir, target))
2671
+ .map((target) => target.purl);
2672
+ if (purlsToResolve.length) {
2673
+ await prefetchGitUrlSources(purlsToResolve);
2674
+ }
2643
2675
  }
2644
2676
  for (const [index, target] of extractedTargets.targets.entries()) {
2645
2677
  const targetIndex = index + 1;
@@ -25,7 +25,10 @@ import {
25
25
  CDXGEN_VERSION,
26
26
  frameworksList,
27
27
  } from "../core/state.js";
28
- import { isPkgTypeOnlyImport } from "../ecosystems/npmutils.js";
28
+ import {
29
+ isPkgTypeOnlyImport,
30
+ setNpmCleanedVersionProperty,
31
+ } from "../ecosystems/npmutils.js";
29
32
  import { parsePackageJsonName } from "../ecosystems/parsers-js.js";
30
33
  import {
31
34
  AI_INSTRUCTION_FILE_KINDS,
@@ -50,8 +53,7 @@ import {
50
53
  import { enrichComponentWithMcpMetadata } from "../inventory/mcp.js";
51
54
  import {
52
55
  applyPurl,
53
- encodeForPurl,
54
- isValidPurl,
56
+ canonicalPurlFromLooseString,
55
57
  npmPurl,
56
58
  tryBuildPurl,
57
59
  } from "../inventory/purl.js";
@@ -182,6 +184,31 @@ export const determineParentComponent = (options) => {
182
184
  return parentComponent;
183
185
  };
184
186
 
187
+ /**
188
+ * Tells whether a `--project-name`/`--project-version` override names the very
189
+ * project the generator detected, rather than a larger project containing it.
190
+ *
191
+ * The override is typed `application` because it can name anything. When its
192
+ * name, group, and version match the detected parent, the two describe the same
193
+ * project, and the detected parent is the better description of it: it carries
194
+ * the ecosystem purl and the manifest metadata. Treating it as a subproject
195
+ * would list the project as a dependency of itself under a second purl.
196
+ *
197
+ * @param {object|undefined} overrideParent Parent built from the options
198
+ * @param {object|undefined} detectedParent Parent the generator derived
199
+ * @returns {boolean} true when both describe the same project
200
+ */
201
+ const overrideNamesDetectedParent = (overrideParent, detectedParent) => {
202
+ if (!overrideParent || !detectedParent?.purl) {
203
+ return false;
204
+ }
205
+ return (
206
+ overrideParent.name === detectedParent.name &&
207
+ (overrideParent.group || "") === (detectedParent.group || "") &&
208
+ `${overrideParent.version}` === `${detectedParent.version}`
209
+ );
210
+ };
211
+
185
212
  /**
186
213
  * Re-anchor a dependency graph onto a caller-supplied parent component.
187
214
  *
@@ -338,7 +365,8 @@ export const componentToSimpleFullName = (comp) => {
338
365
  /**
339
366
  * Strips transient keys (`evidence`, `_integrity`, `license`, `qualifiers`,
340
367
  * `repository`, `homepage`) from a parent component while preserving licenses
341
- * and external references (bug #1519).
368
+ * and external references (bug #1519). External references whose URL is not a
369
+ * valid IRI reference are dropped.
342
370
  *
343
371
  * @param {object} comp Parent component to clean in place
344
372
  * @returns {object} The cleaned parent component
@@ -356,10 +384,15 @@ export const cleanParentComponent = (comp) => {
356
384
  }
357
385
  delete comp.license;
358
386
  delete comp.qualifiers;
359
- if (comp.repository || comp.homepage) {
387
+ // Components get their references validated in listComponents. The parent
388
+ // skips that conversion, so validate here too: an unusable URL taken from a
389
+ // manifest must not make the whole document invalid.
390
+ if (comp.repository || comp.homepage || comp.externalReferences?.length) {
360
391
  const externalReferences = addExternalReferences(comp);
361
392
  if (externalReferences?.length) {
362
393
  comp.externalReferences = externalReferences;
394
+ } else {
395
+ delete comp.externalReferences;
363
396
  }
364
397
  }
365
398
  delete comp.repository;
@@ -454,12 +487,18 @@ export function addMetadata(parentComponent = {}, options = {}, context = {}) {
454
487
  // name-based bom-ref, and blindly copying that here manufactured invalid
455
488
  // purls such as `"purl": "swift-smoke"`. CycloneDX requires `purl` to be
456
489
  // a valid Package URL when present, so absent is the only correct answer.
490
+ // A bom-ref holds the decoded purl (`pkg:npm/app@1.0.0+build`), so it is
491
+ // split on its separators and rebuilt rather than escaped wholesale,
492
+ // which would also escape the `@` before the version.
457
493
  if (!parentComponent["purl"] && parentComponent["bom-ref"]) {
458
- const candidate = encodeForPurl(parentComponent["bom-ref"]);
459
- if (isValidPurl(candidate)) {
494
+ const candidate = canonicalPurlFromLooseString(
495
+ parentComponent["bom-ref"],
496
+ );
497
+ if (candidate) {
460
498
  parentComponent["purl"] = candidate;
461
499
  }
462
500
  }
501
+ setNpmCleanedVersionProperty(parentComponent);
463
502
  }
464
503
  if (parentComponent?.components) {
465
504
  parentComponent.components = listComponents(
@@ -841,6 +880,7 @@ export function addExternalReferences(opkg) {
841
880
  }
842
881
  }
843
882
  return externalReferences
883
+ .filter((reference) => typeof reference?.url === "string")
844
884
  .map((reference) => ({ ...reference, url: reference.url.trim() }))
845
885
  .filter((reference) => isValidIriReference(reference.url));
846
886
  }
@@ -1014,13 +1054,16 @@ export function addComponent(
1014
1054
  let purl = pkg.purl;
1015
1055
  if (!purl && ptype) {
1016
1056
  try {
1057
+ // Raw values: cdx-purl escapes each component itself, so escaping
1058
+ // them here first turned `@scope` into `%2540scope` and
1059
+ // `libstdc++6` into `libstdc%252B%252B6`.
1017
1060
  purl = new Purl({
1018
1061
  type: ptype,
1019
- namespace: encodeForPurl(group) || null,
1020
- name: encodeForPurl(name),
1062
+ namespace: group || null,
1063
+ name,
1021
1064
  version: version || null,
1022
1065
  qualifiers: pkg.qualifiers || null,
1023
- subpath: encodeForPurl(pkg.subpath) || null,
1066
+ subpath: pkg.subpath || null,
1024
1067
  });
1025
1068
  } catch {
1026
1069
  // purl construction may fail for types requiring a namespace (maven,
@@ -1071,9 +1114,10 @@ export function addComponent(
1071
1114
  } else {
1072
1115
  compScope = "required";
1073
1116
  }
1074
- } else if (impPkgs.length && compScope !== "excluded") {
1075
- compScope = "optional";
1076
1117
  }
1118
+ // A package missing from the imports keeps its scope. Usage analysis
1119
+ // only promotes: the ecosystem passes have already scoped the package
1120
+ // list, and a missing import is not evidence that a package is optional.
1077
1121
  }
1078
1122
  let component = {
1079
1123
  author,
@@ -1089,6 +1133,11 @@ export function addComponent(
1089
1133
  purl: purlString,
1090
1134
  externalReferences: addExternalReferences(pkg),
1091
1135
  };
1136
+ // A CPE from the collector (caxa records one for the bundled Node.js
1137
+ // runtime) is how NVD-based scanners match the component.
1138
+ if (typeof pkg.cpe === "string" && pkg.cpe.length) {
1139
+ component.cpe = pkg.cpe;
1140
+ }
1092
1141
  if (options.specVersion >= 1.5) {
1093
1142
  component.pedigree = pkg.pedigree || undefined;
1094
1143
  }
@@ -1102,6 +1151,13 @@ export function addComponent(
1102
1151
  // than bundled with the product. The schema defaults it to false, so it
1103
1152
  // is only worth emitting when true.
1104
1153
  component.isExternal = pkg.isExternal ? true : undefined;
1154
+ // A range describes an external dependency named without a resolved
1155
+ // version, such as the Ruby a gem requires. The schema allows it only
1156
+ // on an external component, and never next to a version.
1157
+ component.versionRange =
1158
+ pkg.isExternal && !pkg.version
1159
+ ? pkg.versionRange || undefined
1160
+ : undefined;
1105
1161
  }
1106
1162
  component["type"] = determinePackageType(pkg);
1107
1163
  // `version` is optional in every spec version cdxgen emits. An empty string
@@ -1131,9 +1187,16 @@ export function addComponent(
1131
1187
  if (options.specVersion >= 1.6 && component.author) {
1132
1188
  const authorsList = [];
1133
1189
  for (const aauthor of component.author.split(",")) {
1134
- authorsList.push({ name: aauthor });
1190
+ // Registries hand out comma separated lists without a consistent
1191
+ // spacing, so `A, B` and `A,B` must produce the same names
1192
+ const name = aauthor.trim();
1193
+ if (name.length) {
1194
+ authorsList.push({ name });
1195
+ }
1196
+ }
1197
+ if (authorsList.length) {
1198
+ component.authors = authorsList;
1135
1199
  }
1136
- component.authors = authorsList;
1137
1200
  delete component.author;
1138
1201
  }
1139
1202
  // Downgrade authors section for < 1.5 :(
@@ -1169,6 +1232,7 @@ export function addComponent(
1169
1232
  if (pkg.properties?.length) {
1170
1233
  component.properties = pkg.properties;
1171
1234
  }
1235
+ setNpmCleanedVersionProperty(component);
1172
1236
  if (isTypeOnlyPkg) {
1173
1237
  component.properties = component.properties || [];
1174
1238
  if (
@@ -1285,6 +1349,25 @@ export function determinePackageType(pkg) {
1285
1349
  ) {
1286
1350
  return "library";
1287
1351
  }
1352
+ // A gem is a framework only when the curated list names that very gem:
1353
+ // the prefix matches below took rails-html-sanitizer and sinatra-contrib
1354
+ // for their frameworks, and the bare words meant for other ecosystems
1355
+ // turned childprocess ("chi") and jquery-rails ("jquery") into
1356
+ // frameworks. Any other gem is a library, unless it ships executables,
1357
+ // which makes it an application (discussions 4405 and 4408). A
1358
+ // description or keyword that merely mentions a framework is no evidence
1359
+ // of one: every gem whose summary says "tracing framework" would
1360
+ // otherwise become a framework component.
1361
+ if (purl.type === "gem") {
1362
+ if (frameworksList.all.includes(`pkg:gem/${purl.name}`)) {
1363
+ return "framework";
1364
+ }
1365
+ const hasExecutables = pkg.properties?.some(
1366
+ (prop) =>
1367
+ prop.name === "cdx:gem:executables" && prop.value?.trim().length,
1368
+ );
1369
+ return hasExecutables ? "application" : "library";
1370
+ }
1288
1371
  for (const cf of frameworksList.all) {
1289
1372
  if (
1290
1373
  pkg.purl.startsWith(cf) ||
@@ -1466,7 +1549,14 @@ export const buildBomNSData = (options, pkgInfo, ptype, context) => {
1466
1549
  }
1467
1550
  const nsMapping = context.nsMapping || {};
1468
1551
  const services = context.services || [];
1469
- const overrideParent = determineParentComponent(options);
1552
+ let overrideParent = determineParentComponent(options);
1553
+ // A caller-supplied parentComponent is always honoured as given
1554
+ if (
1555
+ overrideParent !== options.parentComponent &&
1556
+ overrideNamesDetectedParent(overrideParent, context.parentComponent)
1557
+ ) {
1558
+ overrideParent = undefined;
1559
+ }
1470
1560
  const parentComponent = overrideParent || context.parentComponent;
1471
1561
  const metadata = addMetadata(parentComponent, options, context);
1472
1562
  const components = listComponents(options, allImports, pkgInfo, ptype);
@@ -15,6 +15,10 @@ import { join, resolve } from "node:path";
15
15
 
16
16
  import { isSecureMode, readEnvironmentVariable } from "../core/activity.js";
17
17
  import { PROJECT_TYPE_ALIASES } from "../core/env.js";
18
+ import {
19
+ METADATA_PROPERTY_CONFIG_KEYS,
20
+ parseMetadataProperties,
21
+ } from "../core/metadataProperties.js";
18
22
  import {
19
23
  DEFAULT_CDX_SPEC_VERSION,
20
24
  normalizeCycloneDxComponentTypeFilter,
@@ -120,6 +124,80 @@ export function isUserProvided(value, yaDefault) {
120
124
  return value !== undefined && value !== yaDefault;
121
125
  }
122
126
 
127
+ /**
128
+ * Combine the custom metadata properties of every source (discussion 4391):
129
+ * the `--metadata-property` flag, the CDXGEN_METADATA_PROPERTY and
130
+ * CDXGEN_METADATA_PROPERTIES environment variables, and a config file's
131
+ * `metadata-property` or `metadataProperties` entry.
132
+ *
133
+ * yargs would let the highest-precedence source replace the others outright,
134
+ * so each is read on its own and they are merged instead. A name set by more
135
+ * than one source takes its values from the one that wins in the usual order
136
+ * — the command line, then the environment, then the config file — while a
137
+ * source may repeat a name for a multi-valued property.
138
+ *
139
+ * @param {object} args Parsed yargs argv
140
+ * @param {object} [context]
141
+ * @param {string[]} [context.argv] Raw command-line arguments, which tell a
142
+ * flag on the command line from one yargs filled in from elsewhere
143
+ * @param {object} [context.projectConfig] The sanitized config file contents.
144
+ * Without it, a programmatic caller's `args` are taken as the config.
145
+ * @returns {{name: string, value: string}[]} The merged properties
146
+ */
147
+ export function resolveMetadataProperties(
148
+ args,
149
+ { argv = process.argv, projectConfig } = {},
150
+ ) {
151
+ const flagGiven = argv.some((argument) =>
152
+ ["--metadata-property", "--metadataProperty"].includes(
153
+ `${argument}`.split("=")[0],
154
+ ),
155
+ );
156
+ const fromConfig = projectConfig
157
+ ? METADATA_PROPERTY_CONFIG_KEYS.flatMap((key) =>
158
+ parseMetadataProperties(projectConfig[key]),
159
+ )
160
+ : flagGiven
161
+ ? []
162
+ : [
163
+ ...parseMetadataProperties(args?.metadataProperty),
164
+ ...parseMetadataProperties(args?.metadataProperties),
165
+ ];
166
+ const sources = [
167
+ flagGiven ? parseMetadataProperties(args?.metadataProperty) : [],
168
+ [
169
+ ...parseMetadataProperties(
170
+ readEnvironmentVariable("CDXGEN_METADATA_PROPERTY"),
171
+ ),
172
+ ...parseMetadataProperties(
173
+ readEnvironmentVariable("CDXGEN_METADATA_PROPERTIES"),
174
+ ),
175
+ ],
176
+ fromConfig,
177
+ ];
178
+ const properties = [];
179
+ const claimedNames = new Set();
180
+ for (const source of sources) {
181
+ const sourceNames = new Set();
182
+ for (const aprop of source) {
183
+ if (
184
+ claimedNames.has(aprop.name) ||
185
+ properties.some(
186
+ (prop) => prop.name === aprop.name && prop.value === aprop.value,
187
+ )
188
+ ) {
189
+ continue;
190
+ }
191
+ properties.push(aprop);
192
+ sourceNames.add(aprop.name);
193
+ }
194
+ for (const name of sourceNames) {
195
+ claimedNames.add(name);
196
+ }
197
+ }
198
+ return properties;
199
+ }
200
+
123
201
  /**
124
202
  * Apply command-name-based defaults to the raw parsed args. When cdxgen is
125
203
  * invoked via a symlink or alias (e.g. `obom`, `cbom`, `spdxgen`, `aibom`),
@@ -162,6 +240,8 @@ export function applyCommandNameDefaults(args, invokedCommandName) {
162
240
  * @param {object} context
163
241
  * @param {string} context.filePath Resolved source path
164
242
  * @param {boolean} context.isRemoteOrPurl True when source is a URL or purl
243
+ * @param {string[]} [context.argv] Raw command-line arguments
244
+ * @param {object} [context.projectConfig] Sanitized config file contents
165
245
  * @returns {object} The initial options object
166
246
  */
167
247
  /**
@@ -221,8 +301,12 @@ function resolveIntrospectOption(args, argv) {
221
301
 
222
302
  export function buildInitialOptions(
223
303
  args,
224
- { filePath, isRemoteOrPurl, argv = process.argv },
304
+ { filePath, isRemoteOrPurl, argv = process.argv, projectConfig },
225
305
  ) {
306
+ const metadataProperties = resolveMetadataProperties(args, {
307
+ argv,
308
+ projectConfig,
309
+ });
226
310
  const res = Object.assign({}, args, {
227
311
  projectType: args.type,
228
312
  multiProject: args.recurse,
@@ -235,6 +319,9 @@ export function buildInitialOptions(
235
319
  include: args.include || args.includeRegex,
236
320
  noIgnore: args.noIgnore,
237
321
  gitAiNotesRef: args.gitAiNotesRef || "refs/notes/ai",
322
+ metadataProperties: metadataProperties.length
323
+ ? metadataProperties
324
+ : undefined,
238
325
  // Introspection: the flag or the documented environment opt-in. An
239
326
  // explicit --no-introspect stays `false` and beats both, while an
240
327
  // unmentioned flag stays undefined so a profile can still enable it.
@@ -598,6 +685,9 @@ export function applyAdvancedOptions(options, context = {}) {
598
685
  * passed `--spec-version`
599
686
  * @param {boolean} [context.isDryRun=false] Dry-run mode
600
687
  * @param {boolean} [context.isSecureMode] Secure mode
688
+ * @param {object} [context.projectConfig] Sanitized config file contents,
689
+ * whose metadata properties merge
690
+ * with the other sources'
601
691
  * @returns {{ options: object, warnings: OptionsWarning[] }}
602
692
  */
603
693
  export function buildOptionsFromArgs(args, context) {
@@ -607,12 +697,14 @@ export function buildOptionsFromArgs(args, context) {
607
697
  isRemoteOrPurl,
608
698
  userSetSpecVersion,
609
699
  isDryRun = false,
700
+ projectConfig,
610
701
  } = context;
611
702
 
612
703
  const expandedArgs = applyCommandNameDefaults(args, invokedCommandName);
613
704
  const options = buildInitialOptions(expandedArgs, {
614
705
  filePath,
615
706
  isRemoteOrPurl,
707
+ projectConfig,
616
708
  });
617
709
  const phase3Warnings = applyPostConstructionOverrides(options, {
618
710
  invokedCommandName,