@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
@@ -53,15 +53,30 @@ export function purlTypeIsRegisteredString(purlString) {
53
53
  }
54
54
 
55
55
  /**
56
- * Encode a string for safe inclusion in a PackageURL, percent-encoding special characters
57
- * while preserving already-encoded `%40` sequences and keeping `:` and `/` unencoded.
56
+ * Encode a string for inclusion in a hand-written PackageURL string,
57
+ * percent-encoding every character the purl grammar does not allow literally,
58
+ * while preserving already-encoded `%40` sequences and keeping `:` and `/`
59
+ * unencoded.
60
+ *
61
+ * `encodeURIComponent` alone leaves `!`, `'`, `(`, `)` and `*` literal, and the
62
+ * purl grammar requires them escaped, so they are escaped here as well.
63
+ *
64
+ * Only for comparing against or assembling purl text. Never pass the result to
65
+ * `build()`, `new Purl()` or one of the helpers in this module: they escape
66
+ * their inputs themselves, and escaping twice turns `+` into `%252B`.
58
67
  *
59
68
  * @param {string} s String to encode
60
69
  * @returns {string} Encoded string suitable for use in a PackageURL component
61
70
  */
62
71
  export function encodeForPurl(s) {
63
72
  return s && !s.includes("%40")
64
- ? encodeURIComponent(s).replace(/%3A/g, ":").replace(/%2F/g, "/")
73
+ ? encodeURIComponent(s)
74
+ .replace(
75
+ /[!'()*]/g,
76
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
77
+ )
78
+ .replace(/%3A/g, ":")
79
+ .replace(/%2F/g, "/")
65
80
  : s;
66
81
  }
67
82
 
@@ -328,6 +343,140 @@ export function isValidPurl(candidate) {
328
343
  }
329
344
  }
330
345
 
346
+ /**
347
+ * Split a purl into its components without requiring it to be well-formed.
348
+ *
349
+ * `Purl.parse` rejects a purl whose parts are not already escaped, which is the
350
+ * case this exists to handle: a `bom-ref` (cdxgen writes the decoded form of the
351
+ * purl there), a `go mod graph` token such as `github.com/Azure/go-autorest@v14.2.0+incompatible`,
352
+ * or a purl from a third-party document that left a `+` or a `://` unescaped.
353
+ * The separators (`:`, `/`, `@`, `?`, `#`, `&`, `=`) are structural and are read
354
+ * positionally; everything between them is decoded and handed back raw, ready
355
+ * for `build` to escape correctly.
356
+ *
357
+ * The version is taken after the last `@` of the path, so a scoped npm name
358
+ * (`@scope/name@1.0.0`) and a composer branch version (`dev-feature/x`) both
359
+ * split correctly.
360
+ *
361
+ * @param {string} purl Purl to split
362
+ * @returns {{type: string, namespace: (string|null), name: string, version: (string|null), qualifiers: (Object|null), subpath: (string|null)}|null} Parts, or null when the purl has no type and name
363
+ */
364
+ export function splitPurlLoose(purl) {
365
+ if (typeof purl !== "string" || !purl.startsWith("pkg:")) {
366
+ return null;
367
+ }
368
+ const decode = (value) => {
369
+ try {
370
+ return decodeURIComponent(value);
371
+ } catch {
372
+ return value;
373
+ }
374
+ };
375
+ let rest = purl.slice("pkg:".length).replace(/^\/+/, "");
376
+ let subpath = null;
377
+ const hash = rest.indexOf("#");
378
+ if (hash !== -1) {
379
+ subpath = decode(rest.slice(hash + 1)) || null;
380
+ rest = rest.slice(0, hash);
381
+ }
382
+ let qualifiers = null;
383
+ const query = rest.indexOf("?");
384
+ if (query !== -1) {
385
+ qualifiers = {};
386
+ for (const pair of rest.slice(query + 1).split("&")) {
387
+ const equals = pair.indexOf("=");
388
+ if (equals <= 0) {
389
+ continue;
390
+ }
391
+ qualifiers[pair.slice(0, equals)] = decode(pair.slice(equals + 1));
392
+ }
393
+ if (!Object.keys(qualifiers).length) {
394
+ qualifiers = null;
395
+ }
396
+ rest = rest.slice(0, query);
397
+ }
398
+ const slash = rest.indexOf("/");
399
+ if (slash <= 0) {
400
+ return null;
401
+ }
402
+ const type = rest.slice(0, slash);
403
+ let path = rest.slice(slash + 1);
404
+ let version = null;
405
+ const at = path.lastIndexOf("@");
406
+ if (at > 0 && path[at - 1] !== "/") {
407
+ version = decode(path.slice(at + 1)) || null;
408
+ path = path.slice(0, at);
409
+ }
410
+ const segments = path.split("/").filter(Boolean).map(decode);
411
+ const name = segments.pop();
412
+ if (!name) {
413
+ return null;
414
+ }
415
+ return {
416
+ type,
417
+ namespace: segments.length ? segments.join("/") : null,
418
+ name,
419
+ version,
420
+ qualifiers,
421
+ subpath,
422
+ };
423
+ }
424
+
425
+ /**
426
+ * Parse a purl string that may carry unescaped reserved characters.
427
+ *
428
+ * A well-formed purl is parsed strictly, so its components come back exactly as
429
+ * cdx-purl reads them. Anything else is split structurally by
430
+ * {@link splitPurlLoose}. Use this where the input is known to be a decoded or
431
+ * hand-written purl (a `bom-ref`, a workspace reference, a package manager's
432
+ * `name@version` token), never to accept an invalid purl into a `purl` field:
433
+ * pass the parts to `build()` instead, which escapes them.
434
+ *
435
+ * @param {string} purlString Purl or decoded purl
436
+ * @returns {{type: string, namespace: (string|null), name: string, version: (string|null), qualifiers: (Object|null), subpath: (string|null)}|null} Raw components, or null when nothing purl-shaped remains
437
+ */
438
+ export function parsePurlLoose(purlString) {
439
+ if (typeof purlString !== "string" || !purlString.startsWith("pkg:")) {
440
+ return null;
441
+ }
442
+ try {
443
+ const parsed = Purl.parse(purlString);
444
+ return {
445
+ type: parsed.type,
446
+ namespace: parsed.namespace || null,
447
+ name: parsed.name,
448
+ version: parsed.version || null,
449
+ qualifiers: parsed.qualifiers || null,
450
+ subpath: parsed.subpath || null,
451
+ };
452
+ } catch (err) {
453
+ if (!err?.code?.startsWith("E_")) {
454
+ throw err;
455
+ }
456
+ }
457
+ return splitPurlLoose(purlString);
458
+ }
459
+
460
+ /**
461
+ * Turn a decoded or hand-written purl into a canonical, correctly escaped one.
462
+ *
463
+ * `bom-ref` values are the decoded form of a purl
464
+ * (`pkg:npm/foo@1.0.0+build`), which cdx-purl rightly rejects as a purl. This
465
+ * recovers the purl the ref was made from by splitting it structurally and
466
+ * rebuilding it with `build()`, so every reserved character is escaped exactly
467
+ * once.
468
+ *
469
+ * @param {string} purlString Purl or decoded purl
470
+ * @returns {string|null} Canonical purl string, or `null` when no valid purl can be built
471
+ */
472
+ export function canonicalPurlFromLooseString(purlString) {
473
+ const parts = parsePurlLoose(purlString);
474
+ if (!parts) {
475
+ return null;
476
+ }
477
+ return tryBuildPurl(parts);
478
+ }
479
+
331
480
  /**
332
481
  * Parse a purl string, returning `null` instead of throwing when it is invalid.
333
482
  *
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
3
 
4
- import toml from "@iarna/toml";
4
+ import * as toml from "smol-toml";
5
5
 
6
6
  import { safeExistsSync } from "../core/fs.js";
7
7
 
@@ -14,6 +14,7 @@
14
14
  import { build, Purl } from "@cdxgen/cdx-purl";
15
15
 
16
16
  import { isSpdxJsonLd } from "./bomUtils.js";
17
+ import { splitPurlLoose } from "./purl.js";
17
18
  import { toCycloneDxLikeBom } from "./spdxUtils.js";
18
19
 
19
20
  // A document larger than this is not a plausible SBOM for a single
@@ -150,75 +151,6 @@ function encodeVersionBuildMetadata(purl) {
150
151
  return `${head.slice(0, at + 1)}${head.slice(at + 1).replaceAll("+", "%2B")}${tail}`;
151
152
  }
152
153
 
153
- /**
154
- * Split a purl into its components without requiring it to be well-formed.
155
- *
156
- * `Purl.parse` rejects a purl whose parts are not already escaped, which is the
157
- * case this exists to handle. The separators (`:`, `/`, `@`, `?`, `#`, `&`,
158
- * `=`) are structural and are read positionally; everything between them is
159
- * decoded and handed back raw, ready for `build` to escape correctly.
160
- *
161
- * @param {string} purl Purl to split
162
- * @returns {{type: string, namespace: (string|null), name: string, version: (string|null), qualifiers: (Object|null), subpath: (string|null)}|null} Parts, or null when the purl has no type and name
163
- */
164
- function splitPurl(purl) {
165
- const decode = (value) => {
166
- try {
167
- return decodeURIComponent(value);
168
- } catch {
169
- return value;
170
- }
171
- };
172
- let rest = purl.slice("pkg:".length).replace(/^\/+/, "");
173
- let subpath = null;
174
- const hash = rest.indexOf("#");
175
- if (hash !== -1) {
176
- subpath = decode(rest.slice(hash + 1)) || null;
177
- rest = rest.slice(0, hash);
178
- }
179
- let qualifiers = null;
180
- const query = rest.indexOf("?");
181
- if (query !== -1) {
182
- qualifiers = {};
183
- for (const pair of rest.slice(query + 1).split("&")) {
184
- const equals = pair.indexOf("=");
185
- if (equals <= 0) {
186
- continue;
187
- }
188
- qualifiers[pair.slice(0, equals)] = decode(pair.slice(equals + 1));
189
- }
190
- if (!Object.keys(qualifiers).length) {
191
- qualifiers = null;
192
- }
193
- rest = rest.slice(0, query);
194
- }
195
- const slash = rest.indexOf("/");
196
- if (slash <= 0) {
197
- return null;
198
- }
199
- const type = rest.slice(0, slash);
200
- let path = rest.slice(slash + 1);
201
- let version = null;
202
- const at = path.lastIndexOf("@");
203
- if (at > 0) {
204
- version = decode(path.slice(at + 1)) || null;
205
- path = path.slice(0, at);
206
- }
207
- const segments = path.split("/").filter(Boolean).map(decode);
208
- const name = segments.pop();
209
- if (!name) {
210
- return null;
211
- }
212
- return {
213
- type,
214
- namespace: segments.length ? segments.join("/") : null,
215
- name,
216
- version,
217
- qualifiers,
218
- subpath,
219
- };
220
- }
221
-
222
154
  /**
223
155
  * Repair the purl of a component read from a third-party document.
224
156
  *
@@ -247,7 +179,7 @@ export function repairPurl(component) {
247
179
  } catch {
248
180
  // Fall through to the structural rebuild below.
249
181
  }
250
- const parts = splitPurl(repaired);
182
+ const parts = splitPurlLoose(repaired);
251
183
  if (!parts) {
252
184
  return;
253
185
  }
@@ -28,7 +28,9 @@ import {
28
28
  prefetchEnabled,
29
29
  prefetchedResponse,
30
30
  prefetchJson,
31
+ withHostRateLimit,
31
32
  } from "./fetchBatch.js";
33
+ import { canonicalPurlFromLooseString } from "./purl.js";
32
34
 
33
35
  /** Raised when a caller omits a registry helper that the purl type requires. */
34
36
  class MissingRegistryHelperError extends Error {}
@@ -953,6 +955,36 @@ function packageNameForLookup(purlObj) {
953
955
  return `${namespace}/${purlObj.name}`;
954
956
  }
955
957
 
958
+ /**
959
+ * Read a purl supplied as a scan source.
960
+ *
961
+ * People type purls the way their package manager prints versions and URLs:
962
+ * `pkg:cargo/wasi@0.11.0+wasi-snapshot-preview1` or
963
+ * `pkg:npm/foo?vcs_url=git+https://...`. cdx-purl requires those `+` to be
964
+ * escaped, so a purl that does not parse strictly is split on its separators
965
+ * and rebuilt with every component escaped once. The rebuilt purl is then
966
+ * parsed strictly, so validation always inspects cdx-purl's reading of it.
967
+ *
968
+ * @param {string} purlString Package URL as supplied
969
+ * @returns {Purl|undefined} Parsed purl, or undefined when none can be recovered
970
+ */
971
+ function parsePurlSourceInput(purlString) {
972
+ try {
973
+ return Purl.parse(purlString);
974
+ } catch {
975
+ // Fall through to the structural rebuild.
976
+ }
977
+ const canonical = canonicalPurlFromLooseString(purlString);
978
+ if (!canonical) {
979
+ return undefined;
980
+ }
981
+ try {
982
+ return Purl.parse(canonical);
983
+ } catch {
984
+ return undefined;
985
+ }
986
+ }
987
+
956
988
  /**
957
989
  * Validate package URL source input and return an error object when invalid.
958
990
  *
@@ -963,10 +995,8 @@ export function validatePurlSource(purlString) {
963
995
  if (!maybePurlSource(purlString)) {
964
996
  return null;
965
997
  }
966
- let purlObj;
967
- try {
968
- purlObj = Purl.parse(purlString);
969
- } catch (_err) {
998
+ const purlObj = parsePurlSourceInput(purlString);
999
+ if (!purlObj) {
970
1000
  return {
971
1001
  status: 400,
972
1002
  error: "Invalid purl source",
@@ -1183,12 +1213,17 @@ export async function prefetchGitUrlSources(purlStrings) {
1183
1213
  * @returns {Promise<Object>} A response-shaped object with a `body`.
1184
1214
  */
1185
1215
  async function fetchRegistryDocument(lookup) {
1216
+ // A prefetch miss issues the request here instead, so it goes through the
1217
+ // same per-host gate the batch pool uses. That matters most for crates.io,
1218
+ // whose policy is one request per second however the request was reached.
1186
1219
  return (
1187
1220
  prefetchedResponse(gitUrlSourcePrefetch, lookup.url) ||
1188
- (await cdxgenAgent.get(lookup.url, {
1189
- ...(lookup.accept ? { headers: { Accept: lookup.accept } } : {}),
1190
- responseType: "json",
1191
- }))
1221
+ (await withHostRateLimit(lookup.url, () =>
1222
+ cdxgenAgent.get(lookup.url, {
1223
+ ...(lookup.accept ? { headers: { Accept: lookup.accept } } : {}),
1224
+ responseType: "json",
1225
+ }),
1226
+ ))
1192
1227
  );
1193
1228
  }
1194
1229
 
@@ -1203,24 +1238,13 @@ function parsePurlForSourceResolution(purlString) {
1203
1238
  if (!maybePurlSource(purlString) || validatePurlSource(purlString)) {
1204
1239
  return undefined;
1205
1240
  }
1206
- let purlObj;
1207
- try {
1208
- // Some purls in the wild use unencoded + in qualifier values (e.g.
1209
- // git+https:// in vcs_url). cdx-purl requires percent-encoding; normalize
1210
- // the qualifier section before parsing so real-world purls resolve.
1211
- const qIdx = purlString.indexOf("?");
1212
- const safePurlString =
1213
- qIdx >= 0
1214
- ? purlString.slice(0, qIdx + 1) +
1215
- purlString.slice(qIdx + 1).replaceAll("+", "%2B")
1216
- : purlString;
1217
- purlObj = Purl.parse(safePurlString);
1218
- } catch (err) {
1241
+ // Purls in the wild leave `+` unescaped in versions and in qualifier values
1242
+ // such as `vcs_url=git+https://`; the shared reader rebuilds them.
1243
+ const purlObj = parsePurlSourceInput(purlString);
1244
+ if (!purlObj) {
1219
1245
  if (DEBUG_MODE) {
1220
- const errorMessage = err?.message || String(err);
1221
1246
  thoughtLog("Unable to resolve repository URL for purl.", {
1222
1247
  purlString,
1223
- errorMessage,
1224
1248
  });
1225
1249
  }
1226
1250
  return undefined;
@@ -0,0 +1,41 @@
1
+ // Shared CSS package-reference extraction for stylesheet content.
2
+ //
3
+ // Angular style files, Vue and Svelte single-file-component `<style>` blocks,
4
+ // and `additionalData` strings injected by vite/vue configs all refer to npm
5
+ // packages through the same small set of CSS rules (`@use`, `@import`,
6
+ // `@forward`, and `url()` references). The patterns and the `url()` package
7
+ // guard live here so every collector applies one dialect instead of drifting
8
+ // per-framework copies.
9
+ //
10
+ // Both patterns are global and therefore stateful (`lastIndex`): call sites
11
+ // that reuse them across files must reset `lastIndex = 0` first, the way
12
+ // `matchAll` callers in this codebase already do.
13
+
14
+ /**
15
+ * Patterns matching a package reference inside CSS/SCSS/Less/Stylus content.
16
+ * Capture group 1 holds the reference (for example `bulma/sass/utilities`).
17
+ *
18
+ * @type {RegExp[]}
19
+ */
20
+ export const STYLE_PACKAGE_REFERENCE_PATTERNS = [
21
+ /@(?:import|use|forward)\s+(?:url\(\s*)?['"]([^'"\n)]+)['"]/g,
22
+ /url\(\s*['"](~?[^'"\n)]+)['"]\s*\)/g,
23
+ ];
24
+
25
+ /**
26
+ * True when a `url()` reference points at a package rather than a local file.
27
+ * Only the `~package/...` and `node_modules/...` spellings qualify; plain
28
+ * relative URLs stay local.
29
+ *
30
+ * @param {string} reference Value captured from a `url()` rule.
31
+ * @returns {boolean}
32
+ */
33
+ export const isPackageStyleUrlReference = (reference) => {
34
+ const normalizedReference = String(reference || "")
35
+ .trim()
36
+ .replaceAll("\\", "/");
37
+ return (
38
+ normalizedReference.startsWith("~") ||
39
+ normalizedReference.startsWith("node_modules/")
40
+ );
41
+ };