@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
@@ -2,8 +2,8 @@ import { readFileSync } from "node:fs";
2
2
  import { sep as _sep, basename, dirname, join, resolve } from "node:path";
3
3
 
4
4
  import { build } from "@cdxgen/cdx-purl";
5
- import toml from "@iarna/toml";
6
5
  import { globSync } from "glob";
6
+ import * as toml from "smol-toml";
7
7
 
8
8
  import { DEBUG_MODE } from "../core/activity.js";
9
9
  import { shouldFetchLicense } from "../core/env.js";
@@ -108,9 +108,83 @@ function parseCargoDependencyFromPackageNode(packageNode) {
108
108
  if (!isExtendFromWorkspace(pkgDependencies) && pkgDependencies) {
109
109
  pkg.dependencies = pkgDependencies;
110
110
  }
111
+ const pkgSource = packageNode["source"];
112
+ if (!isExtendFromWorkspace(pkgSource) && typeof pkgSource === "string") {
113
+ pkg.source = pkgSource;
114
+ }
111
115
  return pkg;
112
116
  }
113
117
 
118
+ /** The crates.io index, which a cargo purl identifies without a qualifier. */
119
+ const CRATES_IO_INDEX_URLS = new Set([
120
+ "https://github.com/rust-lang/crates.io-index",
121
+ "sparse+https://index.crates.io/",
122
+ "sparse+https://index.crates.io",
123
+ ]);
124
+
125
+ /**
126
+ * Describe a cargo source string, as a lock file spells it, so a component can
127
+ * say where it came from and whether that is the public registry.
128
+ *
129
+ * A crate resolved from an alternative or private registry shares its name and
130
+ * version with whatever crates.io publishes under them, so the registry has to
131
+ * take part in the component's identity.
132
+ *
133
+ * @param {string} source Lock file `source` value
134
+ * @returns {{kind: string, url: string, isAlternateRegistry: boolean}|undefined} Source facts
135
+ */
136
+ function cargoSourceInfo(source) {
137
+ if (!source || typeof source !== "string") {
138
+ return undefined;
139
+ }
140
+ const separatorIndex = source.indexOf("+");
141
+ if (separatorIndex === -1) {
142
+ return { kind: source, url: "", isAlternateRegistry: false };
143
+ }
144
+ const kind = source.slice(0, separatorIndex);
145
+ let url = source.slice(separatorIndex + 1);
146
+ if (kind === "sparse") {
147
+ // `sparse+https://…` is a registry protocol, not a source kind.
148
+ return {
149
+ kind: "registry",
150
+ url,
151
+ isAlternateRegistry: !CRATES_IO_INDEX_URLS.has(source),
152
+ };
153
+ }
154
+ if (kind === "git") {
155
+ // The fragment is the resolved commit, which the purl carries separately.
156
+ url = url.split("#")[0];
157
+ }
158
+ return {
159
+ kind,
160
+ url,
161
+ isAlternateRegistry:
162
+ kind === "registry" && !CRATES_IO_INDEX_URLS.has(url.replace(/\/$/, "")),
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Build the purl for a cargo package, qualifying it with the registry when the
168
+ * crate does not come from crates.io.
169
+ *
170
+ * @param {{name: string, group?: string, version?: string, source?: string}} pkg Package identity
171
+ * @returns {string} cargo purl
172
+ */
173
+ function buildCargoPurl(pkg) {
174
+ const sourceInfo = cargoSourceInfo(pkg?.source);
175
+ const qualifiers =
176
+ sourceInfo?.isAlternateRegistry && sourceInfo.url
177
+ ? { repository_url: sourceInfo.url }
178
+ : undefined;
179
+ return build({
180
+ type: "cargo",
181
+ namespace: pkg?.group || null,
182
+ name: pkg?.name,
183
+ version: pkg?.version || null,
184
+ qualifiers,
185
+ });
186
+ }
187
+
114
188
  function cargoIntegrityToComponentHash(integrity) {
115
189
  const normalizedIntegrity = normalizeCargoIntegrity(integrity);
116
190
  if (!normalizedIntegrity) {
@@ -935,16 +1009,11 @@ export async function parseCargoData(
935
1009
  return;
936
1010
  }
937
1011
 
938
- const purl = build({
939
- type: "cargo",
940
- namespace: "" || null,
941
- name: newPackage.name,
942
- version: newPackage.version || null,
943
- });
1012
+ const purl = buildCargoPurl(newPackage);
944
1013
  const component = {
945
1014
  type: "library",
946
1015
  group: newPackage.group,
947
- "bom-ref": purl,
1016
+ "bom-ref": decodeURIComponent(purl),
948
1017
  purl: purl,
949
1018
  name: newPackage.name,
950
1019
  version: newPackage.version,
@@ -984,6 +1053,22 @@ export async function parseCargoData(
984
1053
  value: cargoLockFile,
985
1054
  },
986
1055
  ];
1056
+ const sourceInfo = cargoSourceInfo(newPackage.source);
1057
+ // The public registry is the norm, and saying so on every component
1058
+ // would bury the crates whose source is worth a second look. Only a
1059
+ // source that is not plain crates.io is recorded.
1060
+ if (sourceInfo && sourceInfo.kind !== "registry") {
1061
+ appendCargoProperty(component, "cdx:cargo:sourceKind", sourceInfo.kind);
1062
+ appendCargoProperty(component, "cdx:cargo:sourceUrl", sourceInfo.url);
1063
+ } else if (sourceInfo?.isAlternateRegistry) {
1064
+ appendCargoProperty(component, "cdx:cargo:sourceKind", "registry");
1065
+ appendCargoProperty(component, "cdx:cargo:sourceUrl", sourceInfo.url);
1066
+ appendCargoProperty(component, "cdx:cargo:alternateRegistry", "true");
1067
+ } else if (!sourceInfo && newPackage.name) {
1068
+ // A package with no source is local to the workspace: a path
1069
+ // dependency, a workspace member, or a `[patch]`/`[replace]` override.
1070
+ appendCargoProperty(component, "cdx:cargo:sourceKind", "local");
1071
+ }
987
1072
  if (pkgFilesMap?.[newPackage.name]) {
988
1073
  component.components = fileListToComponents(
989
1074
  pkgFilesMap[component.name],
@@ -1204,15 +1289,7 @@ export function parseCargoManifestDependencyData(cargoTomlFile, context = {}) {
1204
1289
  * @returns {Object[]} Array of dependency relationship objects with ref and dependsOn fields
1205
1290
  */
1206
1291
  export function parseCargoDependencyData(cargoLockData) {
1207
- const purlFromPackageInfo = (pkg) =>
1208
- decodeURIComponent(
1209
- build({
1210
- type: "cargo",
1211
- namespace: "" || null,
1212
- name: pkg.name,
1213
- version: pkg.version || null,
1214
- }),
1215
- );
1292
+ const purlFromPackageInfo = (pkg) => decodeURIComponent(buildCargoPurl(pkg));
1216
1293
  const cargoData = toml.parse(cargoLockData);
1217
1294
  const packageNode = cargoData?.package;
1218
1295
  if (!packageNode || !Array.isArray(packageNode)) {
@@ -1232,15 +1309,20 @@ export function parseCargoDependencyData(cargoLockData) {
1232
1309
  );
1233
1310
  }
1234
1311
  });
1235
- // Create a map of package names to package objects
1236
- const pkgMap = pkgList.reduce((acc, item) => {
1237
- acc[item.name] = item;
1238
- return acc;
1239
- }, {});
1240
- const result = [];
1241
- // parse dependency version
1242
- Object.values(pkgMap).forEach((pkg) => {
1243
- const dependsOn = new Set();
1312
+ // A lock file can hold several versions of one crate. Cargo then names the
1313
+ // version in every reference to it (`getrandom 0.3.4`), and a bare name is
1314
+ // left only for crates with a single version.
1315
+ const pkgsByName = new Map();
1316
+ for (const pkg of pkgList) {
1317
+ pkgsByName.set(pkg.name, [...(pkgsByName.get(pkg.name) || []), pkg]);
1318
+ }
1319
+ // Every package gets its own entry, so each version keeps its edges. The
1320
+ // same name and version from two sources share a purl; their edges merge.
1321
+ const edgesByRef = new Map();
1322
+ for (const pkg of pkgList) {
1323
+ const ref = purlFromPackageInfo(pkg);
1324
+ const dependsOn = edgesByRef.get(ref) || new Set();
1325
+ edgesByRef.set(ref, dependsOn);
1244
1326
  pkg.dependencies?.forEach((dep) => {
1245
1327
  if (dep.indexOf(" ") !== -1) {
1246
1328
  // fill version in dependency definition like `libc 0.2.79`
@@ -1251,20 +1333,859 @@ export function parseCargoDependencyData(cargoLockData) {
1251
1333
  version: depSplit[1].trim(),
1252
1334
  }),
1253
1335
  );
1254
- } else if (pkgMap[dep]) {
1255
- dependsOn.add(purlFromPackageInfo(pkgMap[dep]));
1336
+ } else if (pkgsByName.get(dep)?.length === 1) {
1337
+ dependsOn.add(purlFromPackageInfo(pkgsByName.get(dep)[0]));
1256
1338
  } else if (DEBUG_MODE) {
1257
1339
  console.warn(
1258
- `The package "${dep.name}" appears as a dependency to "${pkg.name}" but is not itself listed in the Cargo.lock file. The Cargo.lock file is invalid! The produced SBOM will not list ${dep.name} as a dependency.`,
1340
+ pkgsByName.has(dep)
1341
+ ? `The package "${dep}" appears as a dependency to "${pkg.name}" without a version, but the Cargo.lock file lists more than one version of it. The produced SBOM will not list ${dep} as a dependency.`
1342
+ : `The package "${dep}" appears as a dependency to "${pkg.name}" but is not itself listed in the Cargo.lock file. The Cargo.lock file is invalid! The produced SBOM will not list ${dep} as a dependency.`,
1259
1343
  );
1260
1344
  }
1261
1345
  });
1262
- result.push({
1263
- ref: purlFromPackageInfo(pkg),
1264
- dependsOn: [...dependsOn],
1346
+ }
1347
+ return [...edgesByRef].map(([ref, dependsOn]) => ({
1348
+ ref,
1349
+ dependsOn: [...dependsOn],
1350
+ }));
1351
+ }
1352
+
1353
+ /**
1354
+ * Normalize a cargo dependency kind to the vocabulary used by the cargo
1355
+ * manifest parsers.
1356
+ *
1357
+ * `cargo metadata` encodes a normal dependency as `null` rather than a string,
1358
+ * so a truthiness test would silently leave runtime dependencies unlabelled.
1359
+ *
1360
+ * @param {string|null|undefined} kind Dependency kind as reported by cargo
1361
+ * @returns {string} One of `runtime`, `build` or `dev`
1362
+ */
1363
+ export function normalizeCargoDepKind(kind) {
1364
+ if (kind === null || kind === undefined || kind === "" || kind === "normal") {
1365
+ return "runtime";
1366
+ }
1367
+ return kind;
1368
+ }
1369
+
1370
+ /**
1371
+ * Parse a cargo package id into its name and version.
1372
+ *
1373
+ * cargo 1.77 and later emit the package id spec form
1374
+ * (`registry+https://...#name@1.0.0`, or `path+file:///p/foo#1.0.0` where the
1375
+ * name is carried by the path); earlier releases emit `name 1.0.0 (source)`.
1376
+ *
1377
+ * @param {string} packageId cargo package id
1378
+ * @returns {{name: string, version: string}|undefined} Parsed identity
1379
+ */
1380
+ export function parseCargoPackageId(packageId) {
1381
+ if (!packageId || typeof packageId !== "string") {
1382
+ return undefined;
1383
+ }
1384
+ const hashIndex = packageId.indexOf("#");
1385
+ if (hashIndex !== -1) {
1386
+ const source = packageId.slice(0, hashIndex);
1387
+ const fragment = packageId.slice(hashIndex + 1);
1388
+ const atIndex = fragment.lastIndexOf("@");
1389
+ if (atIndex > 0) {
1390
+ return {
1391
+ name: fragment.slice(0, atIndex),
1392
+ version: fragment.slice(atIndex + 1),
1393
+ };
1394
+ }
1395
+ // `path+file:///workspace/crates/foo#1.0.0` - the name is the last path segment.
1396
+ const sourcePath = source.replace(/^[a-z+]+\+/, "").replace(/\/+$/, "");
1397
+ const name = basename(sourcePath.split("?")[0]);
1398
+ if (name) {
1399
+ return { name, version: fragment };
1400
+ }
1401
+ return undefined;
1402
+ }
1403
+ const legacyMatch = packageId.match(/^(\S+)\s+(\S+)(\s+\(.*\))?$/);
1404
+ if (legacyMatch) {
1405
+ return { name: legacyMatch[1], version: legacyMatch[2] };
1406
+ }
1407
+ return undefined;
1408
+ }
1409
+
1410
+ /**
1411
+ * Build the `name@version` key used to correlate cargo components across the
1412
+ * lock file, the manifests and `cargo metadata`.
1413
+ *
1414
+ * @param {{name: string, version: string}} pkg Package identity
1415
+ * @returns {string} Correlation key
1416
+ */
1417
+ export function cargoComponentKey(pkg) {
1418
+ if (!pkg?.name) {
1419
+ return "";
1420
+ }
1421
+ return `${pkg.name}@${pkg.version || ""}`;
1422
+ }
1423
+
1424
+ /**
1425
+ * Convert a cargo purl or bom-ref into a `name@version` correlation key.
1426
+ *
1427
+ * @param {string} purl cargo purl
1428
+ * @returns {string} Correlation key
1429
+ */
1430
+ export function cargoPurlToComponentKey(purl) {
1431
+ if (!purl || typeof purl !== "string") {
1432
+ return "";
1433
+ }
1434
+ const withoutPrefix = decodeURIComponent(purl).replace(/^pkg:cargo\//, "");
1435
+ const [identity] = withoutPrefix.split("?");
1436
+ const slashIndex = identity.lastIndexOf("/");
1437
+ return slashIndex === -1 ? identity : identity.slice(slashIndex + 1);
1438
+ }
1439
+
1440
+ /**
1441
+ * Collect the dependency kinds and platform gates declared by a single
1442
+ * `Cargo.toml`, keyed by the crate name that cargo resolves the entry to.
1443
+ *
1444
+ * Manifests declare version requirements rather than resolved versions, so the
1445
+ * returned map is keyed by name alone and is meant to label the edges leaving a
1446
+ * workspace member in the lock file graph.
1447
+ *
1448
+ * @param {string} cargoTomlFile Cargo.toml path
1449
+ * @param {object} [context] Workspace resolution context
1450
+ * @returns {Map<string, {kinds: Set<string>, targets: Set<string>, optional: boolean}>} Declared edges
1451
+ */
1452
+ export function parseCargoManifestDependencyKinds(cargoTomlFile, context = {}) {
1453
+ const declaredEdges = new Map();
1454
+ if (!cargoTomlFile || !safeExistsSync(cargoTomlFile)) {
1455
+ return declaredEdges;
1456
+ }
1457
+ const normalizedCargoTomlFile = resolve(cargoTomlFile);
1458
+ const cargoData = readCargoTomlData(normalizedCargoTomlFile);
1459
+ if (!cargoData) {
1460
+ return declaredEdges;
1461
+ }
1462
+ const workspaceContext = resolveCargoWorkspaceContext(
1463
+ normalizedCargoTomlFile,
1464
+ cargoData,
1465
+ context,
1466
+ );
1467
+ const workspaceDependencies = workspaceContext?.workspaceData?.dependencies;
1468
+ const collect = (dependencyEntries, dependencyKind, targetSelector) => {
1469
+ if (!dependencyEntries || typeof dependencyEntries !== "object") {
1470
+ return;
1471
+ }
1472
+ for (const dependencyName of Object.keys(dependencyEntries)) {
1473
+ const dependencyNode = mergeCargoWorkspaceDependencySpec(
1474
+ dependencyName,
1475
+ dependencyEntries[dependencyName],
1476
+ workspaceDependencies,
1477
+ );
1478
+ // `foo = { package = "bar" }` renames the crate; the lock file graph
1479
+ // carries the real crate name.
1480
+ const resolvedName =
1481
+ (typeof dependencyNode === "object" && dependencyNode?.package) ||
1482
+ dependencyName;
1483
+ const existing = declaredEdges.get(resolvedName) || {
1484
+ kinds: new Set(),
1485
+ targets: new Set(),
1486
+ optional: false,
1487
+ };
1488
+ existing.kinds.add(dependencyKind);
1489
+ if (targetSelector) {
1490
+ existing.targets.add(targetSelector);
1491
+ }
1492
+ if (typeof dependencyNode === "object" && dependencyNode?.optional) {
1493
+ existing.optional = true;
1494
+ }
1495
+ declaredEdges.set(resolvedName, existing);
1496
+ }
1497
+ };
1498
+ collect(cargoData.dependencies, "runtime", undefined);
1499
+ collect(cargoData["build-dependencies"], "build", undefined);
1500
+ collect(cargoData["dev-dependencies"], "dev", undefined);
1501
+ if (cargoData.target && typeof cargoData.target === "object") {
1502
+ for (const targetSelector of Object.keys(cargoData.target)) {
1503
+ const targetBlock = cargoData.target[targetSelector];
1504
+ if (!targetBlock || typeof targetBlock !== "object") {
1505
+ continue;
1506
+ }
1507
+ collect(targetBlock.dependencies, "runtime", targetSelector);
1508
+ collect(targetBlock["build-dependencies"], "build", targetSelector);
1509
+ collect(targetBlock["dev-dependencies"], "dev", targetSelector);
1510
+ }
1511
+ }
1512
+ return declaredEdges;
1513
+ }
1514
+
1515
+ /**
1516
+ * Roll per-edge cargo dependency kinds up to an effective kind per component.
1517
+ *
1518
+ * A crate is only dev-only when *every* path reaching it from a workspace root
1519
+ * traverses a dev edge, so a crate shared between a dev dependency and a normal
1520
+ * one keeps its runtime kind. Cargo never builds the dev dependencies of a
1521
+ * dependency, so dev edges are followed from the workspace roots alone.
1522
+ *
1523
+ * @param {object} graph Dependency graph
1524
+ * @param {string[]} graph.rootKeys Workspace member component keys
1525
+ * @param {Map<string, Array<{to: string, kind: string, target?: string}>>} graph.edges Adjacency map
1526
+ * @returns {Map<string, {kind: string, kinds: Set<string>, targets: Set<string>, targetGated: boolean}>} Effective kinds
1527
+ */
1528
+ export function rollupCargoDependencyKinds({ rootKeys, edges }) {
1529
+ const states = new Map();
1530
+ const queue = [];
1531
+ const visit = (key, kind, gated, targets) => {
1532
+ let state = states.get(key);
1533
+ if (!state) {
1534
+ state = { kinds: new Map(), targets: new Set() };
1535
+ states.set(key, state);
1536
+ }
1537
+ for (const target of targets) {
1538
+ state.targets.add(target);
1539
+ }
1540
+ const previous = state.kinds.get(kind);
1541
+ // An ungated path subsumes a gated one; revisit only when the knowledge improves.
1542
+ if (previous !== undefined && (previous === false || gated === true)) {
1543
+ return;
1544
+ }
1545
+ state.kinds.set(kind, gated);
1546
+ queue.push({ key, kind, gated, targets });
1547
+ };
1548
+ for (const rootKey of rootKeys || []) {
1549
+ if (rootKey) {
1550
+ visit(rootKey, "root", false, new Set());
1551
+ }
1552
+ }
1553
+ while (queue.length) {
1554
+ const current = queue.shift();
1555
+ for (const edge of edges.get(current.key) || []) {
1556
+ const edgeKind = normalizeCargoDepKind(edge.kind);
1557
+ let nextKind;
1558
+ if (current.kind === "root") {
1559
+ nextKind = edgeKind;
1560
+ } else if (edgeKind === "dev") {
1561
+ // The dev dependencies of a dependency are never built.
1562
+ continue;
1563
+ } else if (edgeKind === "build") {
1564
+ nextKind = "build";
1565
+ } else {
1566
+ nextKind = current.kind;
1567
+ }
1568
+ const nextTargets = new Set(current.targets);
1569
+ if (edge.target) {
1570
+ nextTargets.add(edge.target);
1571
+ }
1572
+ visit(edge.to, nextKind, current.gated || !!edge.target, nextTargets);
1573
+ }
1574
+ }
1575
+ const rollup = new Map();
1576
+ for (const [key, state] of states) {
1577
+ const kinds = new Set(state.kinds.keys());
1578
+ let kind;
1579
+ for (const candidate of ["root", "runtime", "build", "dev"]) {
1580
+ if (state.kinds.has(candidate)) {
1581
+ kind = candidate;
1582
+ break;
1583
+ }
1584
+ }
1585
+ if (!kind) {
1586
+ continue;
1587
+ }
1588
+ rollup.set(key, {
1589
+ kind,
1590
+ kinds,
1591
+ targets: state.targets,
1592
+ targetGated: state.kinds.get(kind) === true,
1593
+ });
1594
+ }
1595
+ return rollup;
1596
+ }
1597
+
1598
+ /**
1599
+ * Build a cargo dependency graph from the `resolve` section of
1600
+ * `cargo metadata --format-version 1`.
1601
+ *
1602
+ * @param {object} metadata Parsed `cargo metadata` output
1603
+ * @returns {{rootKeys: string[], edges: Map<string, Array<object>>, packageInfo: Map<string, object>, resolvedKeys: Set<string>}} Graph
1604
+ */
1605
+ export function parseCargoMetadataResolve(metadata) {
1606
+ const edges = new Map();
1607
+ const packageInfo = new Map();
1608
+ const rootKeys = [];
1609
+ // Every package the resolver kept. With `--filter-platform` this is the set
1610
+ // that a build for that triple actually pulls in.
1611
+ const resolvedKeys = new Set();
1612
+ if (!metadata || typeof metadata !== "object") {
1613
+ return { rootKeys, edges, packageInfo, resolvedKeys };
1614
+ }
1615
+ const keyById = new Map();
1616
+ for (const pkg of metadata.packages || []) {
1617
+ if (!pkg?.id) {
1618
+ continue;
1619
+ }
1620
+ const key = cargoComponentKey({ name: pkg.name, version: pkg.version });
1621
+ keyById.set(pkg.id, key);
1622
+ const targetKinds = new Set();
1623
+ for (const cargoTarget of pkg.targets || []) {
1624
+ for (const targetKind of cargoTarget?.kind || []) {
1625
+ targetKinds.add(targetKind);
1626
+ }
1627
+ }
1628
+ packageInfo.set(key, {
1629
+ procMacro: targetKinds.has("proc-macro"),
1630
+ source: pkg.source || "",
1631
+ manifestPath: pkg.manifest_path,
1632
+ // The native library this crate links, as declared by `links` in its
1633
+ // manifest. Only a `-sys` style crate carries one.
1634
+ links: pkg.links || "",
1635
+ version: pkg.version,
1636
+ name: pkg.name,
1637
+ // The feature names the crate declares. A vendored source directory
1638
+ // named after a feature that is not enabled belongs to a variant this
1639
+ // build does not compile.
1640
+ declaredFeatures: Object.keys(pkg.features || {}),
1641
+ });
1642
+ }
1643
+ const resolveIdToKey = (packageId) => {
1644
+ const known = keyById.get(packageId);
1645
+ if (known) {
1646
+ return known;
1647
+ }
1648
+ // A `[patch]`ed or vendored package can appear in `resolve` under an id the
1649
+ // package list spells differently.
1650
+ const parsed = parseCargoPackageId(packageId);
1651
+ return parsed ? cargoComponentKey(parsed) : "";
1652
+ };
1653
+ // `resolve.root` is null for a virtual workspace, so the roots always come
1654
+ // from `workspace_members`.
1655
+ for (const memberId of metadata.workspace_members || []) {
1656
+ const key = resolveIdToKey(memberId);
1657
+ if (key) {
1658
+ rootKeys.push(key);
1659
+ }
1660
+ }
1661
+ if (!rootKeys.length && metadata.resolve?.root) {
1662
+ const key = resolveIdToKey(metadata.resolve.root);
1663
+ if (key) {
1664
+ rootKeys.push(key);
1665
+ }
1666
+ }
1667
+ for (const node of metadata.resolve?.nodes || []) {
1668
+ const fromKey = resolveIdToKey(node?.id);
1669
+ if (!fromKey) {
1670
+ continue;
1671
+ }
1672
+ resolvedKeys.add(fromKey);
1673
+ if (Array.isArray(node.features)) {
1674
+ // The features the resolver actually enabled, as opposed to the ones the
1675
+ // manifest declares. These decide which code is compiled, which optional
1676
+ // dependencies activate, and whether a vulnerable path exists at all.
1677
+ const info = packageInfo.get(fromKey) || {};
1678
+ info.resolvedFeatures = node.features;
1679
+ packageInfo.set(fromKey, info);
1680
+ }
1681
+ const nodeEdges = edges.get(fromKey) || [];
1682
+ for (const dep of node.deps || []) {
1683
+ const toKey = resolveIdToKey(dep?.pkg);
1684
+ if (!toKey) {
1685
+ continue;
1686
+ }
1687
+ // A single edge can carry several kinds, for example a crate used both
1688
+ // as a normal and as a dev dependency by the same package.
1689
+ const depKinds = dep.dep_kinds?.length
1690
+ ? dep.dep_kinds
1691
+ : [{ kind: null, target: null }];
1692
+ for (const depKind of depKinds) {
1693
+ nodeEdges.push({
1694
+ to: toKey,
1695
+ kind: normalizeCargoDepKind(depKind?.kind),
1696
+ target: depKind?.target || undefined,
1697
+ });
1698
+ }
1699
+ }
1700
+ edges.set(fromKey, nodeEdges);
1701
+ }
1702
+ return { rootKeys, edges, packageInfo, resolvedKeys };
1703
+ }
1704
+
1705
+ /**
1706
+ * Build a cargo dependency graph from the lock file relationships, labelling
1707
+ * the edges that leave a workspace member with the kinds declared by its
1708
+ * manifest.
1709
+ *
1710
+ * The lock file is the union of every dependency kind and every target triple,
1711
+ * so only the manifests can distinguish them. The resulting labels are exact at
1712
+ * depth one and inherited below it, which is enough to identify dev-only and
1713
+ * build-only subtrees without invoking cargo.
1714
+ *
1715
+ * @param {object[]} lockDependencies Relationships from {@link parseCargoDependencyData}
1716
+ * @param {Map<string, Map<string, object>>} declaredEdgesByRoot Declared edges keyed by root component key
1717
+ * @returns {{rootKeys: string[], edges: Map<string, Array<object>>}} Graph
1718
+ */
1719
+ export function buildCargoLockDependencyGraph(
1720
+ lockDependencies,
1721
+ declaredEdgesByRoot,
1722
+ ) {
1723
+ const edges = new Map();
1724
+ const rootKeys = [...(declaredEdgesByRoot?.keys() || [])];
1725
+ for (const relationship of lockDependencies || []) {
1726
+ const fromKey = cargoPurlToComponentKey(relationship?.ref);
1727
+ if (!fromKey) {
1728
+ continue;
1729
+ }
1730
+ const declaredEdges = declaredEdgesByRoot?.get(fromKey);
1731
+ const nodeEdges = edges.get(fromKey) || [];
1732
+ for (const dependsOnRef of relationship.dependsOn || []) {
1733
+ const toKey = cargoPurlToComponentKey(dependsOnRef);
1734
+ if (!toKey) {
1735
+ continue;
1736
+ }
1737
+ const declared = declaredEdges?.get(toKey.split("@")[0]);
1738
+ if (!declared) {
1739
+ nodeEdges.push({ to: toKey, kind: "runtime" });
1740
+ continue;
1741
+ }
1742
+ const targets = declared.targets.size ? [...declared.targets] : [null];
1743
+ for (const kind of declared.kinds) {
1744
+ for (const target of targets) {
1745
+ nodeEdges.push({ to: toKey, kind, target: target || undefined });
1746
+ }
1747
+ }
1748
+ }
1749
+ edges.set(fromKey, nodeEdges);
1750
+ }
1751
+ return { rootKeys, edges };
1752
+ }
1753
+
1754
+ /**
1755
+ * Annotate cargo components with their effective dependency kind, platform
1756
+ * gates and scope.
1757
+ *
1758
+ * Components reachable only through dev edges are scoped `excluded` so
1759
+ * `--required-only` drops them. Build dependencies stay required because a
1760
+ * build script is part of producing the artifact; the kind is recorded as a
1761
+ * property so a consumer can filter on it.
1762
+ *
1763
+ * @param {object[]} pkgList Components to annotate
1764
+ * @param {Map<string, object>} rollup Effective kinds from {@link rollupCargoDependencyKinds}
1765
+ * @param {Map<string, object>} [packageInfo] Extra per-package facts
1766
+ * @param {object} [options] Annotation options
1767
+ * @param {boolean} [options.resolveIsComplete] The rollup covers every workspace in the scan, so a
1768
+ * component missing from it is an optional dependency no feature activates
1769
+ * @returns {object[]} The annotated components
1770
+ */
1771
+ export function applyCargoDependencyKindMetadata(
1772
+ pkgList,
1773
+ rollup,
1774
+ packageInfo = new Map(),
1775
+ options = {},
1776
+ ) {
1777
+ for (const pkg of pkgList || []) {
1778
+ const key = cargoComponentKey(pkg);
1779
+ const effective = rollup?.get(key);
1780
+ const info = packageInfo?.get(key);
1781
+ if (info?.resolvedFeatures?.length) {
1782
+ appendCargoProperty(
1783
+ pkg,
1784
+ "cdx:cargo:resolvedFeatures",
1785
+ [...info.resolvedFeatures].sort().join(", "),
1786
+ );
1787
+ }
1788
+ if (info?.links) {
1789
+ appendCargoProperty(pkg, "cdx:cargo:links", info.links);
1790
+ }
1791
+ if (info?.procMacro) {
1792
+ // A proc-macro crate is compiled for the host, never for the target.
1793
+ appendCargoProperty(pkg, "cdx:cargo:procMacro", "true");
1794
+ appendCargoProperty(pkg, "cdx:cargo:hostOnly", "true");
1795
+ }
1796
+ if (
1797
+ options?.filterPlatform &&
1798
+ options?.targetIncludedKeys &&
1799
+ effective?.kind !== "root" &&
1800
+ !options.targetIncludedKeys.has(key)
1801
+ ) {
1802
+ // The resolver for the requested triple did not keep this crate, so no
1803
+ // build for that triple links it.
1804
+ appendCargoProperty(pkg, "cdx:cargo:targetExcluded", "true");
1805
+ appendCargoProperty(
1806
+ pkg,
1807
+ "cdx:cargo:filterPlatform",
1808
+ options.filterPlatform,
1809
+ );
1810
+ pkg.scope = "excluded";
1811
+ }
1812
+ if (!effective) {
1813
+ // The lock file keeps optional dependencies that no enabled feature
1814
+ // pulls in; the resolve graph is what says they are never built.
1815
+ if (options?.resolveIsComplete) {
1816
+ appendCargoProperty(pkg, "cdx:cargo:activated", "false");
1817
+ if (!pkg.scope) {
1818
+ pkg.scope = "optional";
1819
+ }
1820
+ }
1821
+ continue;
1822
+ }
1823
+ if (effective.kind === "root") {
1824
+ continue;
1825
+ }
1826
+ appendCargoProperty(pkg, "cdx:cargo:dependencyKind", effective.kind);
1827
+ appendCargoProperty(pkg, "cdx:cargo:scope", effective.kind);
1828
+ if (effective.kinds.size > 1) {
1829
+ appendCargoProperty(
1830
+ pkg,
1831
+ "cdx:cargo:dependencyKinds",
1832
+ [...effective.kinds].sort().join(", "),
1833
+ );
1834
+ }
1835
+ if (effective.targets.size) {
1836
+ appendCargoProperty(
1837
+ pkg,
1838
+ "cdx:cargo:target",
1839
+ [...effective.targets].sort().join(", "),
1840
+ );
1841
+ }
1842
+ if (effective.targetGated) {
1843
+ appendCargoProperty(pkg, "cdx:cargo:targetGated", "true");
1844
+ }
1845
+ if (effective.kind === "build") {
1846
+ // A build dependency is not part of the assembly; it is consumed while
1847
+ // producing it, which is what the formulation section describes.
1848
+ appendCargoProperty(pkg, "cdx:cargo:hostOnly", "true");
1849
+ }
1850
+ if (effective.kind === "dev" && pkg.scope !== "optional") {
1851
+ pkg.scope = "excluded";
1852
+ }
1853
+ }
1854
+ return pkgList;
1855
+ }
1856
+
1857
+ /**
1858
+ * Well-known `links` values whose library is better known by another name. The
1859
+ * `links` key is a linker-level identifier, so `z` is zlib and `sqlite3` is
1860
+ * SQLite; a consumer matching advisories or license obligations needs the name
1861
+ * the upstream project publishes under. Anything absent from this map keeps its
1862
+ * `links` value, which is still what the crate declared.
1863
+ */
1864
+ const CARGO_NATIVE_LIBRARY_NAMES = Object.freeze({
1865
+ bz2: "bzip2",
1866
+ crypto: "openssl",
1867
+ git2: "libgit2",
1868
+ jpeg: "libjpeg",
1869
+ lzma: "xz",
1870
+ png: "libpng",
1871
+ sqlite3: "sqlite",
1872
+ ssl: "openssl",
1873
+ z: "zlib",
1874
+ zstd: "zstd",
1875
+ });
1876
+
1877
+ /**
1878
+ * Features whose presence means the native library is compiled into the
1879
+ * artifact rather than linked against one the environment provides.
1880
+ */
1881
+ const CARGO_STATIC_LINK_FEATURES = new Set([
1882
+ "bundled",
1883
+ "static",
1884
+ "static-link",
1885
+ "static-linking",
1886
+ "vendored",
1887
+ ]);
1888
+
1889
+ /**
1890
+ * Recover the upstream version of a native library from the version of the
1891
+ * crate that ships it.
1892
+ *
1893
+ * The `-sys` and `-src` crates carry the upstream version as semver build
1894
+ * metadata, because the crate's own version tracks the binding's releases
1895
+ * rather than the library's: `openssl-src@300.6.1+3.6.3` ships OpenSSL 3.6.3
1896
+ * and `curl-sys@0.4.74+curl-8.9.0` ships curl 8.9.0.
1897
+ *
1898
+ * @param {string} crateVersion Version of the providing crate
1899
+ * @returns {string} Upstream version, or an empty string when it is not encoded
1900
+ */
1901
+ export function cargoUpstreamVersionFromCrateVersion(crateVersion) {
1902
+ if (!crateVersion || typeof crateVersion !== "string") {
1903
+ return "";
1904
+ }
1905
+ const buildMetadata = crateVersion.split("+")[1];
1906
+ if (!buildMetadata) {
1907
+ return "";
1908
+ }
1909
+ // `curl-8.9.0` and `zstd.1.5.6` both prefix the version with the library name.
1910
+ const versionMatch = buildMetadata.match(/(\d+(?:\.\d+)*[A-Za-z0-9.-]*)$/);
1911
+ return versionMatch ? versionMatch[1] : "";
1912
+ }
1913
+
1914
+ /**
1915
+ * Recover the version of a vendored native library from the C source the crate
1916
+ * ships.
1917
+ *
1918
+ * A `-sys` crate that bundles its library carries the upstream version in a
1919
+ * header define - `ZLIB_VERSION`, `SQLITE_VERSION`, `OPENSSL_VERSION_TEXT` -
1920
+ * which is the only statement of the version for the crates that do not encode
1921
+ * it in their own version. A crate often ships more than one variant, so a
1922
+ * candidate sitting in a directory named after a feature this build did not
1923
+ * enable is dropped: `libz-sys` ships both `src/zlib` and `src/zlib-ng`, and
1924
+ * only one of them is compiled.
1925
+ *
1926
+ * @param {object} args Resolution inputs
1927
+ * @param {string} args.crateDir Directory holding the crate's unpacked source
1928
+ * @param {string} args.links The crate's `links` value
1929
+ * @param {string} args.libraryName Normalized library name
1930
+ * @param {string[]} [args.declaredFeatures] Features the crate declares
1931
+ * @param {Set<string>} [args.resolvedFeatures] Features the resolver enabled
1932
+ * @returns {{version: string, evidence: string, candidates: string[]}} Version and where it came from
1933
+ */
1934
+ export function resolveCargoVendoredLibraryVersion({
1935
+ crateDir,
1936
+ links,
1937
+ libraryName,
1938
+ declaredFeatures = [],
1939
+ resolvedFeatures = new Set(),
1940
+ }) {
1941
+ const empty = { version: "", evidence: "", candidates: [] };
1942
+ if (!crateDir || !safeExistsSync(crateDir)) {
1943
+ return empty;
1944
+ }
1945
+ const libraryTokens = [...new Set([libraryName, links])].filter(Boolean);
1946
+ const defineNames = new Set();
1947
+ for (const token of libraryTokens) {
1948
+ const upperToken = token.replace(/[^A-Za-z0-9]/g, "_").toUpperCase();
1949
+ defineNames.add(`${upperToken}_VERSION`);
1950
+ defineNames.add(`${upperToken}_VERSION_TEXT`);
1951
+ defineNames.add(`${upperToken}_VERSION_STR`);
1952
+ }
1953
+ let headerFiles;
1954
+ try {
1955
+ headerFiles = globSync("**/*.h", {
1956
+ absolute: true,
1957
+ cwd: crateDir,
1958
+ nodir: true,
1959
+ windowsPathsNoEscape: true,
1265
1960
  });
1961
+ } catch (_err) {
1962
+ return empty;
1963
+ }
1964
+ // Only a header named after the library states the library's version, and
1965
+ // the filter keeps a large vendored tree from being read in full.
1966
+ const interestingHeaders = headerFiles.filter((headerFile) => {
1967
+ const headerName = basename(headerFile).toLowerCase();
1968
+ return libraryTokens.some((token) =>
1969
+ headerName.includes(token.toLowerCase()),
1970
+ );
1266
1971
  });
1267
- return result;
1972
+ // A feature the crate declares but the resolver did not enable names a
1973
+ // variant that is not built.
1974
+ const disabledFeatures = declaredFeatures.filter(
1975
+ (feature) => !resolvedFeatures.has(feature),
1976
+ );
1977
+ const candidates = new Map();
1978
+ for (const headerFile of interestingHeaders.slice(0, 64)) {
1979
+ let headerData;
1980
+ try {
1981
+ headerData = readFileSync(headerFile, { encoding: "utf-8" });
1982
+ } catch (_err) {
1983
+ continue;
1984
+ }
1985
+ for (const match of headerData.matchAll(
1986
+ /^\s*#\s*define\s+([A-Z0-9_]+)\s+"([^"]+)"/gm,
1987
+ )) {
1988
+ if (!defineNames.has(match[1])) {
1989
+ continue;
1990
+ }
1991
+ const version = match[2].trim();
1992
+ if (!/^\d/.test(version)) {
1993
+ continue;
1994
+ }
1995
+ const pathSegments = headerFile.split(/[\\/]/);
1996
+ const isDisabledVariant = disabledFeatures.some((feature) =>
1997
+ pathSegments.includes(feature),
1998
+ );
1999
+ if (isDisabledVariant) {
2000
+ continue;
2001
+ }
2002
+ if (!candidates.has(version)) {
2003
+ candidates.set(version, headerFile);
2004
+ }
2005
+ }
2006
+ }
2007
+ if (candidates.size === 1) {
2008
+ const [version, headerFile] = [...candidates.entries()][0];
2009
+ // The absolute path runs through the user's CARGO_HOME, which does not
2010
+ // belong in a shared document; the crate-relative path is the part that
2011
+ // lets a reviewer find the same define.
2012
+ const registryMatch = headerFile.match(
2013
+ /[\\/]registry[\\/]src[\\/][^\\/]+[\\/](.+)$/,
2014
+ );
2015
+ return {
2016
+ version,
2017
+ evidence: registryMatch ? registryMatch[1] : basename(headerFile),
2018
+ candidates: [version],
2019
+ };
2020
+ }
2021
+ // Several variants remain in play, so naming one of them would be a guess.
2022
+ return { ...empty, candidates: [...candidates.keys()].sort() };
2023
+ }
2024
+
2025
+ /**
2026
+ * Build components for the native libraries that `-sys` crates link.
2027
+ *
2028
+ * A crate declaring `links = "openssl"` puts a C library into the artifact that
2029
+ * the cargo dependency graph cannot describe: its advisories and its license
2030
+ * obligations belong to OpenSSL, not to the Rust binding. When the resolver
2031
+ * enabled a vendoring feature, that library is compiled into the binary, which
2032
+ * makes it a component of the delivered assembly.
2033
+ *
2034
+ * @param {Map<string, object>} packageInfo Per-package facts from {@link parseCargoMetadataResolve}
2035
+ * @param {Map<string, Array<object>>} edges Dependency edges keyed by component key
2036
+ * @param {Map<string, object>} rollup Effective kinds, used to skip crates no build reaches
2037
+ * @returns {{components: object[], dependencies: object[]}} Native library components and their edges
2038
+ */
2039
+ export function buildCargoNativeLibraryComponents(packageInfo, edges, rollup) {
2040
+ const components = [];
2041
+ const dependencies = [];
2042
+ const componentsByRef = new Map();
2043
+ for (const [key, info] of packageInfo || []) {
2044
+ if (!info?.links) {
2045
+ continue;
2046
+ }
2047
+ // A crate no build reaches contributes no native library either.
2048
+ const effective = rollup?.get(key);
2049
+ if (rollup?.size && !effective) {
2050
+ continue;
2051
+ }
2052
+ const resolvedFeatures = new Set(info.resolvedFeatures || []);
2053
+ const linkFeatures = [...resolvedFeatures].filter((feature) =>
2054
+ CARGO_STATIC_LINK_FEATURES.has(feature),
2055
+ );
2056
+ // A `*-src` dependency exists only to compile the library from source.
2057
+ const sourceCrateKeys = (edges?.get(key) || [])
2058
+ .map((edge) => edge.to)
2059
+ .filter((to) => to.split("@")[0].endsWith("-src"));
2060
+ const isStatic = linkFeatures.length > 0 || sourceCrateKeys.length > 0;
2061
+ let upstreamVersion = "";
2062
+ let versionSource = "";
2063
+ for (const sourceCrateKey of sourceCrateKeys) {
2064
+ const sourceInfo = packageInfo.get(sourceCrateKey);
2065
+ upstreamVersion = cargoUpstreamVersionFromCrateVersion(
2066
+ sourceInfo?.version || sourceCrateKey.split("@")[1],
2067
+ );
2068
+ if (upstreamVersion) {
2069
+ versionSource = sourceCrateKey;
2070
+ break;
2071
+ }
2072
+ }
2073
+ if (!upstreamVersion) {
2074
+ upstreamVersion = cargoUpstreamVersionFromCrateVersion(info.version);
2075
+ if (upstreamVersion) {
2076
+ versionSource = key;
2077
+ }
2078
+ }
2079
+ const libraryName = CARGO_NATIVE_LIBRARY_NAMES[info.links] || info.links;
2080
+ let versionCandidates = [];
2081
+ if (!upstreamVersion && isStatic && info.manifestPath) {
2082
+ const vendoredVersion = resolveCargoVendoredLibraryVersion({
2083
+ crateDir: dirname(info.manifestPath),
2084
+ declaredFeatures: info.declaredFeatures,
2085
+ libraryName,
2086
+ links: info.links,
2087
+ resolvedFeatures,
2088
+ });
2089
+ upstreamVersion = vendoredVersion.version;
2090
+ versionCandidates = vendoredVersion.candidates;
2091
+ if (upstreamVersion) {
2092
+ versionSource = vendoredVersion.evidence;
2093
+ }
2094
+ }
2095
+ const purl = build({
2096
+ type: "generic",
2097
+ namespace: null,
2098
+ name: libraryName,
2099
+ version: upstreamVersion || null,
2100
+ });
2101
+ const bomRef = decodeURIComponent(purl);
2102
+ const providerPurl = decodeURIComponent(
2103
+ build({
2104
+ type: "cargo",
2105
+ namespace: null,
2106
+ name: info.name || key.split("@")[0],
2107
+ version: info.version || key.split("@")[1] || null,
2108
+ }),
2109
+ );
2110
+ let component = componentsByRef.get(bomRef);
2111
+ if (!component) {
2112
+ component = {
2113
+ "bom-ref": bomRef,
2114
+ name: libraryName,
2115
+ purl,
2116
+ type: "library",
2117
+ properties: [],
2118
+ evidence: {
2119
+ identity: {
2120
+ field: "purl",
2121
+ // The library is inferred from a manifest declaration rather than
2122
+ // observed in the artifact, and its version, when known at all,
2123
+ // comes from a crate version's build metadata.
2124
+ confidence: upstreamVersion ? 0.5 : 0.3,
2125
+ methods: [
2126
+ {
2127
+ technique: "manifest-analysis",
2128
+ confidence: upstreamVersion ? 0.5 : 0.3,
2129
+ value: info.manifestPath || "",
2130
+ },
2131
+ ],
2132
+ },
2133
+ },
2134
+ };
2135
+ if (upstreamVersion) {
2136
+ component.version = upstreamVersion;
2137
+ }
2138
+ componentsByRef.set(bomRef, component);
2139
+ components.push(component);
2140
+ }
2141
+ appendCargoProperty(component, "cdx:cargo:nativeLibrary", info.links);
2142
+ appendCargoProperty(component, "cdx:cargo:providedBy", providerPurl);
2143
+ appendCargoProperty(
2144
+ component,
2145
+ "cdx:cargo:linkage",
2146
+ isStatic ? "static" : "dynamic",
2147
+ );
2148
+ if (!isStatic) {
2149
+ // A dynamically linked library is not part of the assembly but is
2150
+ // expected from the deployment environment, which is what CycloneDX 1.7
2151
+ // calls an external component.
2152
+ component.isExternal = true;
2153
+ }
2154
+ if (linkFeatures.length) {
2155
+ appendCargoProperty(
2156
+ component,
2157
+ "cdx:cargo:linkageEvidence",
2158
+ linkFeatures.sort().join(", "),
2159
+ );
2160
+ }
2161
+ for (const sourceCrateKey of sourceCrateKeys) {
2162
+ appendCargoProperty(
2163
+ component,
2164
+ "cdx:cargo:linkageEvidence",
2165
+ sourceCrateKey,
2166
+ );
2167
+ }
2168
+ if (versionSource) {
2169
+ appendCargoProperty(
2170
+ component,
2171
+ "cdx:cargo:upstreamVersionSource",
2172
+ versionSource,
2173
+ );
2174
+ }
2175
+ if (!upstreamVersion && versionCandidates.length) {
2176
+ // The crate ships several variants of the library and nothing in the
2177
+ // build says which one is compiled, so the versions are reported as
2178
+ // candidates rather than one of them being chosen.
2179
+ appendCargoProperty(
2180
+ component,
2181
+ "cdx:cargo:upstreamVersionCandidates",
2182
+ versionCandidates.join(", "),
2183
+ );
2184
+ }
2185
+ dependencies.push({ ref: providerPurl, dependsOn: [bomRef] });
2186
+ dependencies.push({ ref: bomRef, dependsOn: [] });
2187
+ }
2188
+ return { components, dependencies };
1268
2189
  }
1269
2190
 
1270
2191
  /**