@cdxgen/cdxgen 13.1.0 → 13.3.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 (283) hide show
  1. package/README.md +32 -15
  2. package/bin/cdxgen.js +243 -122
  3. package/bin/evinse.js +7 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/crypto-oid.json +88 -12
  9. package/data/remediations.json +47 -0
  10. package/data/rules/ai-bom.yaml +78 -0
  11. package/data/rules/cargo-rust.yaml +132 -0
  12. package/data/rules/mcp-servers.yaml +122 -0
  13. package/lib/audit/targets.js +6 -1
  14. package/lib/cli/bomAssembly.js +117 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +235 -5
  17. package/lib/cli/jsBom.js +108 -26
  18. package/lib/cli/jvmBom.js +68 -12
  19. package/lib/cli/managedBom.js +264 -41
  20. package/lib/cli/nativeBom.js +923 -15
  21. package/lib/core/activity.js +120 -2
  22. package/lib/core/env.js +44 -7
  23. package/lib/core/fs.js +174 -6
  24. package/lib/core/metadataProperties.js +97 -0
  25. package/lib/core/projectConfig.js +31 -4
  26. package/lib/core/userAgent.js +151 -0
  27. package/lib/ecosystems/bunutils.js +269 -101
  28. package/lib/ecosystems/cargoCache.js +364 -0
  29. package/lib/ecosystems/caxa.js +447 -7
  30. package/lib/ecosystems/cmakeResolver.js +66 -0
  31. package/lib/ecosystems/cppEvidence.js +104 -33
  32. package/lib/ecosystems/ecosystems.js +260 -99
  33. package/lib/ecosystems/golem.js +11 -1
  34. package/lib/ecosystems/gradleutils.js +185 -9
  35. package/lib/ecosystems/jsEvidence.js +19 -20
  36. package/lib/ecosystems/kosi.js +1263 -0
  37. package/lib/ecosystems/npmutils.js +110 -9
  38. package/lib/ecosystems/parsers-crystal.js +179 -0
  39. package/lib/ecosystems/parsers-dotnet.js +120 -49
  40. package/lib/ecosystems/parsers-elm.js +816 -0
  41. package/lib/ecosystems/parsers-gleam.js +1 -1
  42. package/lib/ecosystems/parsers-go.js +47 -9
  43. package/lib/ecosystems/parsers-js.js +171 -50
  44. package/lib/ecosystems/parsers-julia.js +268 -0
  45. package/lib/ecosystems/parsers-lua.js +218 -0
  46. package/lib/ecosystems/parsers-misc.js +919 -161
  47. package/lib/ecosystems/parsers-nim.js +221 -0
  48. package/lib/ecosystems/parsers-opam.js +180 -0
  49. package/lib/ecosystems/parsers-perl.js +277 -0
  50. package/lib/ecosystems/parsers-python.js +27 -15
  51. package/lib/ecosystems/parsers-r.js +148 -0
  52. package/lib/ecosystems/parsers-rebar.js +411 -0
  53. package/lib/ecosystems/parsers-rust.js +954 -33
  54. package/lib/ecosystems/parsers-spack.js +204 -0
  55. package/lib/ecosystems/parsers-stack.js +264 -0
  56. package/lib/ecosystems/parsers-terraform.js +194 -0
  57. package/lib/ecosystems/rubyutils.js +1426 -166
  58. package/lib/ecosystems/rusi.js +158 -7
  59. package/lib/evinser/evinser.js +547 -129
  60. package/lib/evinser/swiftsem.js +1914 -302
  61. package/lib/helpers/agenticToolutils.js +925 -0
  62. package/lib/helpers/bomSigner.js +652 -221
  63. package/lib/inventory/agentFormulationParser.js +57 -4
  64. package/lib/inventory/aiCollector.js +504 -10
  65. package/lib/inventory/aiInventory.js +12 -0
  66. package/lib/inventory/analyzer.js +488 -30
  67. package/lib/inventory/astWorker.js +6 -0
  68. package/lib/inventory/atomUtils.js +568 -31
  69. package/lib/inventory/auditCategories.js +8 -0
  70. package/lib/inventory/cbomutils.js +30 -0
  71. package/lib/inventory/cdxrs.js +110 -14
  72. package/lib/inventory/communityAiConfigParser.js +215 -2
  73. package/lib/inventory/depsUtils.js +183 -11
  74. package/lib/inventory/display.js +4 -5
  75. package/lib/inventory/dosai.js +627 -13
  76. package/lib/inventory/dynamic.js +39 -1
  77. package/lib/inventory/envcontext.js +9 -6
  78. package/lib/inventory/evidenceUtils.js +26 -5
  79. package/lib/inventory/fetchBatch.js +190 -19
  80. package/lib/inventory/fetchRate.js +74 -11
  81. package/lib/inventory/forgeEnricher.js +3 -2
  82. package/lib/inventory/jvmToolEnv.js +6 -5
  83. package/lib/inventory/mcpConfigParser.js +166 -2
  84. package/lib/inventory/mcpDiscovery.js +11 -0
  85. package/lib/inventory/mcpServerManifest.js +389 -0
  86. package/lib/inventory/mcpbBundle.js +419 -0
  87. package/lib/inventory/plugins.js +3 -0
  88. package/lib/inventory/protobom.js +62 -89
  89. package/lib/inventory/protobomLoader.js +4 -0
  90. package/lib/inventory/purl.js +152 -3
  91. package/lib/inventory/rustFormulationParser.js +1 -1
  92. package/lib/inventory/sbomDocument.js +2 -70
  93. package/lib/inventory/source.js +47 -23
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/traceRunner.js +210 -21
  97. package/lib/managers/binary.js +115 -24
  98. package/lib/managers/docker.js +440 -20
  99. package/lib/managers/oci.js +39 -1
  100. package/lib/parsers/cmakeLists.js +452 -0
  101. package/lib/parsers/huggingfaceUtils.js +15 -7
  102. package/lib/parsers/largeJson.js +454 -0
  103. package/lib/parsers/npmrc.js +19 -1
  104. package/lib/parsers/onnx.js +294 -0
  105. package/lib/parsers/safetensors.js +114 -0
  106. package/lib/stages/postgen/introspection/markers.js +29 -6
  107. package/lib/stages/postgen/postgen.js +237 -16
  108. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  109. package/lib/validator/bomValidator.js +6 -16
  110. package/lib/validator/index.js +48 -10
  111. package/lib/validator/reporters/console.js +3 -0
  112. package/lib/validator/reporters/json.js +1 -0
  113. package/lib/validator/reporters/sarif.js +7 -0
  114. package/package.json +27 -27
  115. package/types/lib/audit/targets.d.ts.map +1 -1
  116. package/types/lib/cli/bomAssembly.d.ts +2 -1
  117. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  118. package/types/lib/cli/cliOptions.d.ts +33 -1
  119. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  120. package/types/lib/cli/index.d.ts +2 -2
  121. package/types/lib/cli/index.d.ts.map +1 -1
  122. package/types/lib/cli/jsBom.d.ts +23 -0
  123. package/types/lib/cli/jsBom.d.ts.map +1 -1
  124. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  125. package/types/lib/cli/managedBom.d.ts +33 -0
  126. package/types/lib/cli/managedBom.d.ts.map +1 -1
  127. package/types/lib/cli/nativeBom.d.ts +88 -8
  128. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  129. package/types/lib/core/activity.d.ts.map +1 -1
  130. package/types/lib/core/env.d.ts +21 -0
  131. package/types/lib/core/env.d.ts.map +1 -1
  132. package/types/lib/core/fs.d.ts +55 -0
  133. package/types/lib/core/fs.d.ts.map +1 -1
  134. package/types/lib/core/metadataProperties.d.ts +39 -0
  135. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  136. package/types/lib/core/projectConfig.d.ts +6 -2
  137. package/types/lib/core/projectConfig.d.ts.map +1 -1
  138. package/types/lib/core/userAgent.d.ts +56 -0
  139. package/types/lib/core/userAgent.d.ts.map +1 -0
  140. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  141. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  143. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  144. package/types/lib/ecosystems/caxa.d.ts +14 -0
  145. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  146. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  147. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  148. package/types/lib/ecosystems/cppEvidence.d.ts +2 -2
  149. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  150. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  151. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  152. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  153. package/types/lib/ecosystems/gradleutils.d.ts +40 -4
  154. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  155. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  156. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  157. package/types/lib/ecosystems/kosi.d.ts +86 -0
  158. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  159. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  160. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  161. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  162. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  163. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  164. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  165. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  166. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  167. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  168. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  169. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  170. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  171. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  172. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  173. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  174. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-misc.d.ts +90 -3
  176. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  177. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  178. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  179. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  180. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  181. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  182. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  183. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  184. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  185. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  187. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  188. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  189. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  191. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  192. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  193. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  194. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  195. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  196. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  197. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  198. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  199. package/types/lib/ecosystems/rusi.d.ts +13 -0
  200. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  201. package/types/lib/evinser/evinser.d.ts +81 -2
  202. package/types/lib/evinser/evinser.d.ts.map +1 -1
  203. package/types/lib/evinser/swiftsem.d.ts +280 -15
  204. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  205. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  206. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  207. package/types/lib/helpers/bomSigner.d.ts +106 -13
  208. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  209. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  210. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  211. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  212. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  213. package/types/lib/inventory/atomUtils.d.ts +145 -3
  214. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +4 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/cdxrs.d.ts +8 -3
  219. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  220. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  221. package/types/lib/inventory/depsUtils.d.ts +15 -0
  222. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/display.d.ts.map +1 -1
  224. package/types/lib/inventory/dosai.d.ts +87 -1
  225. package/types/lib/inventory/dosai.d.ts.map +1 -1
  226. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  227. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  228. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  229. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  230. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  231. package/types/lib/inventory/fetchRate.d.ts +9 -6
  232. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  233. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  234. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  235. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  236. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  237. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  238. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  239. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  240. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  241. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  242. package/types/lib/inventory/plugins.d.ts.map +1 -1
  243. package/types/lib/inventory/protobom.d.ts +18 -13
  244. package/types/lib/inventory/protobom.d.ts.map +1 -1
  245. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  246. package/types/lib/inventory/purl.d.ts +71 -2
  247. package/types/lib/inventory/purl.d.ts.map +1 -1
  248. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  249. package/types/lib/inventory/source.d.ts.map +1 -1
  250. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  251. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  252. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  253. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  254. package/types/lib/inventory/traceRunner.d.ts +47 -1
  255. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  256. package/types/lib/managers/binary.d.ts +30 -1
  257. package/types/lib/managers/binary.d.ts.map +1 -1
  258. package/types/lib/managers/docker.d.ts +29 -0
  259. package/types/lib/managers/docker.d.ts.map +1 -1
  260. package/types/lib/managers/oci.d.ts +14 -0
  261. package/types/lib/managers/oci.d.ts.map +1 -1
  262. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  263. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  264. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  265. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  266. package/types/lib/parsers/largeJson.d.ts +52 -0
  267. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  268. package/types/lib/parsers/npmrc.d.ts +5 -0
  269. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  270. package/types/lib/parsers/onnx.d.ts +28 -0
  271. package/types/lib/parsers/onnx.d.ts.map +1 -0
  272. package/types/lib/parsers/safetensors.d.ts +22 -0
  273. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  275. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  276. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  277. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  278. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  279. package/types/lib/validator/index.d.ts +10 -1
  280. package/types/lib/validator/index.d.ts.map +1 -1
  281. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  282. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  283. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
 
3
- import toml from "@iarna/toml";
3
+ import * as toml from "smol-toml";
4
4
 
5
5
  import { tryBuildPurl } from "../inventory/purl.js";
6
6
 
@@ -7,7 +7,12 @@ import { Purl } from "@cdxgen/cdx-purl";
7
7
  import { DEBUG_MODE } from "../core/activity.js";
8
8
  import { shouldFetchLicense, shouldFetchVCS } from "../core/env.js";
9
9
  import { safeExistsSync } from "../core/fs.js";
10
- import { applyPurl, tryBuildPurl, tryParsePurl } from "../inventory/purl.js";
10
+ import {
11
+ applyPurl,
12
+ canonicalPurlFromLooseString,
13
+ tryBuildPurl,
14
+ tryParsePurl,
15
+ } from "../inventory/purl.js";
11
16
  import {
12
17
  _clearMetadataCache,
13
18
  getGoPkgLicense,
@@ -443,6 +448,23 @@ function _addGoComponentEvidence(component, goModFile, confidence = 0.8) {
443
448
  return component;
444
449
  }
445
450
 
451
+ /**
452
+ * Build the purl of one `module@version` token from `go mod graph` output.
453
+ *
454
+ * The version is written the way Go prints it, so `+incompatible` and the build
455
+ * metadata of a pseudo-version are unescaped. Reading the token strictly would
456
+ * reject it and leave a reference that keeps the module path's upper-case
457
+ * letters, which then never matches the lower-cased component and drops the
458
+ * dependency edge.
459
+ *
460
+ * @param {string} token Module path, optionally followed by `@version`
461
+ * @returns {Purl|null} Parsed purl, or null when the token forms no valid golang purl
462
+ */
463
+ function goModGraphTokenPurl(token) {
464
+ const canonical = canonicalPurlFromLooseString(`pkg:golang/${token}`);
465
+ return canonical ? Purl.parse(canonical) : null;
466
+ }
467
+
446
468
  /**
447
469
  * Parse go mod graph
448
470
  *
@@ -512,22 +534,20 @@ export async function parseGoModGraph(
512
534
  // cdx-purl requires a namespace. Use the raw ref when parsing fails.
513
535
  let sourceRefString;
514
536
  let sourceName = tmpA[0].split("@")[0];
515
- let sourcePurl = null;
516
- try {
517
- sourcePurl = Purl.parse(`pkg:golang/${tmpA[0]}`);
537
+ const sourcePurl = goModGraphTokenPurl(tmpA[0]);
538
+ if (sourcePurl) {
518
539
  sourceRefString = decodeURIComponent(sourcePurl.toString());
519
540
  sourceName = `${sourcePurl.namespace ? `${sourcePurl.namespace}/` : ""}${sourcePurl.name}`;
520
- } catch {
541
+ } else {
521
542
  sourceRefString = `pkg:golang/${tmpA[0]}`;
522
543
  }
523
544
  let dependsRefString;
524
545
  let dependsName = tmpA[1].split("@")[0];
525
- let dependsPurl = null;
526
- try {
527
- dependsPurl = Purl.parse(`pkg:golang/${tmpA[1]}`);
546
+ const dependsPurl = goModGraphTokenPurl(tmpA[1]);
547
+ if (dependsPurl) {
528
548
  dependsRefString = decodeURIComponent(dependsPurl.toString());
529
549
  dependsName = `${dependsPurl.namespace ? `${dependsPurl.namespace}/` : ""}${dependsPurl.name}`;
530
- } catch {
550
+ } else {
531
551
  dependsRefString = `pkg:golang/${tmpA[1]}`;
532
552
  }
533
553
  // Since go mod graph over-reports direct dependencies we use the existing list
@@ -683,6 +703,24 @@ export function parseGoModWhy(rawOutput) {
683
703
  return undefined;
684
704
  }
685
705
 
706
+ /**
707
+ * Reports whether `go mod why -m` output says the main module does not need
708
+ * the module. go prints `(main module does not need module X)`, or
709
+ * `(main module does not need to vendor module X)` with `-vendor`, and exits
710
+ * with status 0 either way.
711
+ *
712
+ * @param {string} rawOutput Output from go mod why
713
+ * @returns {boolean} True when go reports the module as not needed
714
+ */
715
+ export function isGoModWhyNotNeeded(rawOutput) {
716
+ if (typeof rawOutput !== "string") {
717
+ return false;
718
+ }
719
+ return rawOutput
720
+ .split("\n")
721
+ .some((line) => line.trim().startsWith("(main module does not need "));
722
+ }
723
+
686
724
  /**
687
725
  * Parse go sum data
688
726
  * @param {string} gosumData Content of go.sum
@@ -18,7 +18,7 @@ import { recordDegradation } from "../core/buildLedger.js";
18
18
  import { shouldFetchPackageMetadata } from "../core/env.js";
19
19
  import { safeExistsSync } from "../core/fs.js";
20
20
  import { analyzeSuspiciousJsFile } from "../inventory/analyzer.js";
21
- import { encodeForPurl, npmPurl } from "../inventory/purl.js";
21
+ import { npmPurl, parsePurlLoose } from "../inventory/purl.js";
22
22
  import Arborist from "../third-party/arborist/lib/index.js";
23
23
  import { getNpmMetadata } from "./ecosystems.js";
24
24
  import {
@@ -914,11 +914,16 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
914
914
  // A package.json need not declare a version, and npm installs without
915
915
  // one, so the root of a lockfile can reach here with none. A purl carries
916
916
  // no version rather than an empty one.
917
- purlString = Purl.parse(
918
- `pkg:npm/${projectGroup ? `${encodeURIComponent(projectGroup).replace(/%2F/g, "/")}/` : ""}${encodeURIComponent(projectName).replace(/%2F/g, "/")}${projectVersion ? `@${projectVersion}` : ""}`,
919
- )
920
- .toString()
921
- .replace(/%2F/g, "/");
917
+ purlString = (
918
+ projectGroup
919
+ ? build({
920
+ type: "npm",
921
+ namespace: projectGroup,
922
+ name: projectName,
923
+ version: projectVersion || null,
924
+ })
925
+ : npmPurl(projectName, projectVersion)
926
+ ).replace(/%2F/g, "/");
922
927
  pkg = {
923
928
  author: authorString,
924
929
  group: options.projectGroup || "",
@@ -1778,17 +1783,15 @@ function _resolveYarnDependencyBomRef(
1778
1783
  packageNameToUse,
1779
1784
  );
1780
1785
  if (matchingWorkspace) {
1781
- const versionMatch = matchingWorkspace.match(/@([^@]+)$/);
1782
- if (versionMatch) {
1783
- resolvedVersion = versionMatch[1];
1784
- }
1786
+ // The workspace reference may be an escaped purl, so its version is read
1787
+ // through the purl parser rather than cut out of the string.
1788
+ resolvedVersion =
1789
+ parsePurlLoose(matchingWorkspace)?.version || resolvedVersion;
1785
1790
  }
1786
1791
  }
1787
- let purlPart = `pkg:npm/${encodeURIComponent(packageNameToUse).replace(/%2F/g, "/")}`;
1788
- if (resolvedVersion?.length) {
1789
- purlPart = `${purlPart}@${resolvedVersion}`;
1790
- }
1791
- return decodeURIComponent(Purl.parse(purlPart).toString());
1792
+ return decodeURIComponent(
1793
+ npmPurl(packageNameToUse, resolvedVersion?.length ? resolvedVersion : null),
1794
+ );
1792
1795
  }
1793
1796
 
1794
1797
  function _collectYarnRootDependencyRefs(
@@ -1919,7 +1922,7 @@ function _markYarnDependencyScopeClosures(
1919
1922
  * @param {Object} parentComponent parent component
1920
1923
  * @param {Array[String]} workspacePackages Workspace packages
1921
1924
  * @param {Object} workspaceSrcFiles Workspace package.json files
1922
- * @param {Object} _workspaceDirectDeps Direct dependencies of each workspace
1925
+ * @param {Object} workspaceDirectDeps Declared dependencies (name to range) of each workspace package
1923
1926
  * @param {Object} depsWorkspaceRefs Workspace references for each dependency
1924
1927
  */
1925
1928
  export async function parseYarnLock(
@@ -1927,12 +1930,14 @@ export async function parseYarnLock(
1927
1930
  parentComponent = null,
1928
1931
  workspacePackages = [],
1929
1932
  workspaceSrcFiles = {},
1930
- _workspaceDirectDeps = {},
1933
+ workspaceDirectDeps = {},
1931
1934
  depsWorkspaceRefs = {},
1932
1935
  ) {
1933
1936
  let pkgList = [];
1934
1937
  const dependenciesList = [];
1935
1938
  const depKeys = {};
1939
+ // The lock keys and the resolved version required to solve the dependency tree
1940
+ let identMap = {};
1936
1941
  let yarnRootRefs = _createYarnRootDependencyRefs();
1937
1942
  const yarnOptionalDependencyRefs = new Set();
1938
1943
  const npmrcConfig = loadNpmrcConfig(dirname(yarnLockFile));
@@ -1951,8 +1956,7 @@ export async function parseYarnLock(
1951
1956
  const pkgAddedMap = {};
1952
1957
  // Map to track workspace package PURL replacements from 0.0.0-use.local to actual versions
1953
1958
  const workspacePurlMap = {};
1954
- // This would have the keys and the resolved version required to solve the dependency tree
1955
- const identMap = yarnLockToIdentMap(lockData);
1959
+ identMap = yarnLockToIdentMap(lockData);
1956
1960
  yarnRootRefs = _collectYarnRootDependencyRefs(
1957
1961
  yarnLockFile,
1958
1962
  identMap,
@@ -2020,8 +2024,8 @@ export async function parseYarnLock(
2020
2024
  const actualWorkspacePurl = workspacePackages.find(
2021
2025
  (wp) =>
2022
2026
  wp.startsWith(expectedPurlStart) ||
2023
- wp.includes(
2024
- `pkg:npm/${encodeForPurl(group ? `${group}/${name}` : name)}@`,
2027
+ wp.startsWith(
2028
+ `${npmPurl(group ? `${group}/${name}` : name)}@`,
2025
2029
  ),
2026
2030
  );
2027
2031
  // Also check if this package already exists in pkgList with a real version
@@ -2286,23 +2290,21 @@ export async function parseYarnLock(
2286
2290
  if (DEBUG_MODE) {
2287
2291
  console.log(`Processing workspace package: "${workspacePkg}"`);
2288
2292
  }
2289
- let purlObj;
2290
- // For workspace packages that already have versions in the PURL format
2291
- if (
2292
- workspacePkg.includes("@") &&
2293
- workspacePkg.lastIndexOf("@") > workspacePkg.indexOf("/")
2294
- ) {
2295
- // This is a scoped package with version like "npm:@swc/helpers@0.4.14"
2296
- const lastAtIndex = workspacePkg.lastIndexOf("@");
2297
- const purlWithoutVersion = workspacePkg.substring(0, lastAtIndex);
2298
- const version = workspacePkg.substring(lastAtIndex + 1);
2299
- // Parse the PURL without version first, then set the version.
2300
- purlObj = Purl.parse(purlWithoutVersion);
2301
- purlObj.version = version;
2302
- } else {
2303
- // Parse the PURL as-is (no version or simple package)
2304
- purlObj = Purl.parse(workspacePkg);
2293
+ // Workspace references arrive both escaped (built by cdx-purl) and in
2294
+ // the decoded bom-ref form, so they are split structurally and
2295
+ // rebuilt. Copying the version text across forms is what turned
2296
+ // `3.0.0+ws` into `3.0.0%252Bws`.
2297
+ const workspaceParts = parsePurlLoose(workspacePkg);
2298
+ if (!workspaceParts) {
2299
+ continue;
2305
2300
  }
2301
+ const purlObj = new Purl({
2302
+ type: "npm",
2303
+ namespace: workspaceParts.namespace,
2304
+ name: workspaceParts.name,
2305
+ version: workspaceParts.version,
2306
+ subpath: workspaceParts.subpath,
2307
+ });
2306
2308
 
2307
2309
  // Create the workspace component using the properly parsed PURL
2308
2310
  const workspaceComponent = {
@@ -2405,6 +2407,41 @@ export async function parseYarnLock(
2405
2407
  }
2406
2408
  }
2407
2409
  }
2410
+ // A yarn v1 lock file has no entries for workspace packages, so a member
2411
+ // would otherwise have no dependency entry and nothing would reach it.
2412
+ // Each member gets one built from the dependencies its own package.json
2413
+ // declares; the root's entry, built from every dependency entry, then
2414
+ // includes the member too. A lock file that already recorded the member
2415
+ // (yarn berry) keeps the entry it produced.
2416
+ const recordedRefs = new Set(dependenciesList.map((d) => d.ref));
2417
+ for (const workspacePkg of workspacePackages) {
2418
+ const workspaceRef = decodeURIComponent(workspacePkg);
2419
+ if (
2420
+ recordedRefs.has(workspaceRef) ||
2421
+ !pkgList.some((pkg) => pkg["bom-ref"] === workspaceRef)
2422
+ ) {
2423
+ continue;
2424
+ }
2425
+ const dependsOn = new Set();
2426
+ for (const [depName, depRange] of Object.entries(
2427
+ workspaceDirectDeps?.[workspacePkg] || {},
2428
+ )) {
2429
+ const depRef = _resolveYarnDependencyBomRef(
2430
+ depName,
2431
+ String(depRange),
2432
+ identMap,
2433
+ workspacePackages,
2434
+ );
2435
+ if (depRef && depRef !== workspaceRef) {
2436
+ dependsOn.add(depRef);
2437
+ }
2438
+ }
2439
+ dependenciesList.push({
2440
+ ref: workspaceRef,
2441
+ dependsOn: [...dependsOn].sort(),
2442
+ });
2443
+ recordedRefs.add(workspaceRef);
2444
+ }
2408
2445
  }
2409
2446
 
2410
2447
  _markYarnDependencyScopeClosures(
@@ -2646,7 +2683,10 @@ export function stripPnpmPeerSuffix(version) {
2646
2683
  * {@link stripPnpmPeerSuffix}.
2647
2684
  *
2648
2685
  * @param {string|object} depPkg Dependency version string or package object.
2649
- * @param {string} relativePath Base path for resolving link/file references.
2686
+ * @param {string} relativePath Base path for resolving link/file references:
2687
+ * the directory of the importer that declares the dependency. Without it the
2688
+ * path is tried against the working directory, which only works when cdxgen
2689
+ * runs from the lock file's directory.
2650
2690
  * @returns {Promise<string|undefined>} The resolved published version.
2651
2691
  */
2652
2692
  export async function getVersionNumPnpm(depPkg, relativePath) {
@@ -2685,6 +2725,25 @@ export async function getVersionNumPnpm(depPkg, relativePath) {
2685
2725
  return version;
2686
2726
  }
2687
2727
 
2728
+ /**
2729
+ * Split the package name of a pnpm `file:` lock key into scope and name.
2730
+ *
2731
+ * @param {string} fullName Package name as written before `@file:`, eg `@scope/pkg`
2732
+ * @returns {{group: string, name: string}} Scope (with its `@`) and bare name
2733
+ */
2734
+ function splitPnpmFilePackageName(fullName) {
2735
+ if (fullName.startsWith("@")) {
2736
+ const slash = fullName.indexOf("/");
2737
+ if (slash > 0) {
2738
+ return {
2739
+ group: fullName.slice(0, slash),
2740
+ name: fullName.slice(slash + 1),
2741
+ };
2742
+ }
2743
+ }
2744
+ return { group: "", name: fullName };
2745
+ }
2746
+
2688
2747
  /**
2689
2748
  * Resolve a pnpm dependency to its PURL string.
2690
2749
  *
@@ -2723,11 +2782,16 @@ export async function getPnpmDepPurl(
2723
2782
  }
2724
2783
  gitPkgRefs[versionObj.version] = { group, name, version };
2725
2784
  } else {
2785
+ const rawSpec =
2786
+ typeof versionObj?.version === "string" ? versionObj.version : "";
2726
2787
  version = await getVersionNumPnpm(depPkg, relativePath);
2727
2788
  const gitEntry =
2728
2789
  gitPkgRefs[packageName] ||
2729
2790
  gitPkgRefs[version] ||
2730
- gitPkgRefs[normalizePnpmLockKey(`${packageName}@${version}`)];
2791
+ gitPkgRefs[normalizePnpmLockKey(`${packageName}@${version}`)] ||
2792
+ (rawSpec.startsWith("file:")
2793
+ ? gitPkgRefs[normalizePnpmLockKey(`${packageName}@${rawSpec}`)]
2794
+ : undefined);
2731
2795
  if (gitEntry) {
2732
2796
  group = gitEntry.group;
2733
2797
  name = gitEntry.name;
@@ -2980,7 +3044,8 @@ export async function pnpmMetadata(pkgList, lockFilePath) {
2980
3044
  // Silently ignore parsing errors for individual packages
2981
3045
  if (DEBUG_MODE) {
2982
3046
  console.log(
2983
- `Failed to parse package.json at ${packageJsonPath}:`,
3047
+ "Failed to parse package.json at %s:",
3048
+ packageJsonPath,
2984
3049
  error.message,
2985
3050
  );
2986
3051
  }
@@ -3148,6 +3213,33 @@ export async function parsePnpmLock(
3148
3213
  npmrcConfig,
3149
3214
  ),
3150
3215
  );
3216
+ // A `file:` package is keyed by its path (`localdep@file:localdep.tgz`) and
3217
+ // pnpm copies the manifest version into its `packages` entry. Registering
3218
+ // it lets an importer that names the path resolve to the same ref the
3219
+ // package loop below builds, instead of a version made from the path.
3220
+ if (lockfileVersion >= 9) {
3221
+ for (const [lockKey, pkgEntry] of Object.entries(
3222
+ yamlObj.packages || {},
3223
+ )) {
3224
+ const fileAt = lockKey.indexOf("@file:", 1);
3225
+ if (fileAt < 1 || !pkgEntry?.version) {
3226
+ continue;
3227
+ }
3228
+ const normalizedKey = normalizePnpmLockKey(lockKey);
3229
+ if (gitPkgRefs[normalizedKey]) {
3230
+ continue;
3231
+ }
3232
+ const { group, name } = splitPnpmFilePackageName(
3233
+ lockKey.slice(0, fileAt),
3234
+ );
3235
+ gitPkgRefs[normalizedKey] = {
3236
+ group,
3237
+ name,
3238
+ version: String(pkgEntry.version),
3239
+ vcsUrl: lockKey.slice(fileAt + 1),
3240
+ };
3241
+ }
3242
+ }
3151
3243
  // This logic matches the pnpm list command to include only direct dependencies
3152
3244
  if (ppurl !== "" && yamlObj?.importers) {
3153
3245
  // In lock file version 6, direct dependencies is under importers
@@ -3184,7 +3276,7 @@ export async function parsePnpmLock(
3184
3276
  rootDevDeps[rdk],
3185
3277
  rdk,
3186
3278
  gitPkgRefs,
3187
- undefined,
3279
+ pnpmLockDir,
3188
3280
  githubServerHost,
3189
3281
  npmrcConfig,
3190
3282
  );
@@ -3206,7 +3298,7 @@ export async function parsePnpmLock(
3206
3298
  rootOptionalDeps[rdk] || rootPeerDeps[rdk],
3207
3299
  rdk,
3208
3300
  gitPkgRefs,
3209
- undefined,
3301
+ pnpmLockDir,
3210
3302
  githubServerHost,
3211
3303
  npmrcConfig,
3212
3304
  );
@@ -3229,7 +3321,7 @@ export async function parsePnpmLock(
3229
3321
  rootDirectDeps[dk],
3230
3322
  dk,
3231
3323
  gitPkgRefs,
3232
- undefined,
3324
+ pnpmLockDir,
3233
3325
  githubServerHost,
3234
3326
  npmrcConfig,
3235
3327
  );
@@ -3340,7 +3432,7 @@ export async function parsePnpmLock(
3340
3432
  versionObj,
3341
3433
  cdk,
3342
3434
  gitPkgRefs,
3343
- importedComponentName,
3435
+ join(pnpmLockDir, importedComponentName),
3344
3436
  githubServerHost,
3345
3437
  npmrcConfig,
3346
3438
  );
@@ -3357,7 +3449,10 @@ export async function parsePnpmLock(
3357
3449
  }
3358
3450
  }
3359
3451
  for (const cdk of Object.keys(componentDevDeps)) {
3360
- const version = await getVersionNumPnpm(componentDevDeps[cdk]);
3452
+ const version = await getVersionNumPnpm(
3453
+ componentDevDeps[cdk],
3454
+ join(pnpmLockDir, importedComponentName),
3455
+ );
3361
3456
  const dpurl = build({
3362
3457
  type: "npm",
3363
3458
  namespace: "" || null,
@@ -3375,6 +3470,7 @@ export async function parsePnpmLock(
3375
3470
  })) {
3376
3471
  const version = await getVersionNumPnpm(
3377
3472
  componentOptionalDeps[cdk] || componentPeerDeps[cdk],
3473
+ join(pnpmLockDir, importedComponentName),
3378
3474
  );
3379
3475
  const dpurl = build({
3380
3476
  type: "npm",
@@ -3399,6 +3495,11 @@ export async function parsePnpmLock(
3399
3495
  // snapshots is a new key under lockfile version 9
3400
3496
  const snapshots = yamlObj.snapshots || {};
3401
3497
  const pkgKeys = { ...Object.keys(packages), ...Object.keys(snapshots) };
3498
+ // A lock entry named `__proto__` is not a real package: emitting it as a
3499
+ // component produces SBOM data that breaks naive JSON consumers, and the
3500
+ // name can never come from the npm registry. Legitimate names such as
3501
+ // `constructor` or `prototype` are unaffected.
3502
+ const protoLockKey = /(^|\/)__proto__(@|$)/;
3402
3503
  for (const k in pkgKeys) {
3403
3504
  // Eg: @babel/code-frame/7.10.1
3404
3505
  // In lockfileVersion 6, /@babel/code-frame@7.18.6
@@ -3407,6 +3508,9 @@ export async function parsePnpmLock(
3407
3508
  if (lockfileVersion >= 6 && fullName.includes("(")) {
3408
3509
  fullName = fullName.split("(")[0];
3409
3510
  }
3511
+ if (protoLockKey.test(pkgKeys[k]) || protoLockKey.test(fullName)) {
3512
+ continue;
3513
+ }
3410
3514
  const parts = fullName.split("/");
3411
3515
  const packageNode =
3412
3516
  packages[pkgKeys[k]] ||
@@ -3465,9 +3569,17 @@ export async function parsePnpmLock(
3465
3569
  }
3466
3570
  let opurlString;
3467
3571
  if (vers.includes("@")) {
3468
- opurlString = Purl.parse(
3469
- `pkg:npm/${vers.replace(/^@/g, "%40")}`,
3470
- ).toString();
3572
+ // An aliased optional dependency records `name@version`, and the
3573
+ // version may carry semver build metadata that must be escaped.
3574
+ const aliasParts = parsePurlLoose(`pkg:npm/${vers}`);
3575
+ opurlString = aliasParts
3576
+ ? npmPurl(
3577
+ aliasParts.namespace
3578
+ ? `${aliasParts.namespace}/${aliasParts.name}`
3579
+ : aliasParts.name,
3580
+ aliasParts.version,
3581
+ )
3582
+ : npmPurl(opkgName, vers);
3471
3583
  } else {
3472
3584
  opurlString = npmPurl(opkgName, vers);
3473
3585
  }
@@ -3536,8 +3648,17 @@ export async function parsePnpmLock(
3536
3648
  possibleHttpParts[possibleHttpParts.length - 1].startsWith("file")
3537
3649
  ) {
3538
3650
  srcUrl = possibleHttpParts[possibleHttpParts.length - 1];
3539
- name = fullName.replace(`@${srcUrl}`, "");
3540
- version = "";
3651
+ ({ group, name } = splitPnpmFilePackageName(
3652
+ fullName.replace(`@${srcUrl}`, ""),
3653
+ ));
3654
+ // The key names the path, not a version. pnpm copies the version
3655
+ // from the package's own manifest into its `packages` entry, and
3656
+ // that may carry semver build metadata (`2.0.0+local`).
3657
+ version = String(
3658
+ packages[pkgKeys[k]]?.version ||
3659
+ packages[fullName]?.version ||
3660
+ "",
3661
+ );
3541
3662
  } else {
3542
3663
  group = parts.length > 1 ? parts[0] : "";
3543
3664
  const tmpA = parts[parts.length - 1].split("@");