@cdxgen/cdxgen 13.1.0 → 13.2.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 (186) hide show
  1. package/README.md +8 -7
  2. package/bin/cdxgen.js +16 -3
  3. package/bin/evinse.js +2 -0
  4. package/data/crypto-oid.json +88 -12
  5. package/data/remediations.json +23 -0
  6. package/data/rules/ai-bom.yaml +78 -0
  7. package/data/rules/cargo-rust.yaml +132 -0
  8. package/data/rules/mcp-servers.yaml +122 -0
  9. package/lib/audit/targets.js +6 -1
  10. package/lib/cli/bomAssembly.js +12 -0
  11. package/lib/cli/index.js +192 -1
  12. package/lib/cli/jsBom.js +59 -2
  13. package/lib/cli/jvmBom.js +22 -0
  14. package/lib/cli/managedBom.js +152 -0
  15. package/lib/cli/nativeBom.js +798 -1
  16. package/lib/core/activity.js +120 -2
  17. package/lib/core/env.js +39 -1
  18. package/lib/core/userAgent.js +151 -0
  19. package/lib/ecosystems/cargoCache.js +364 -0
  20. package/lib/ecosystems/caxa.js +13 -5
  21. package/lib/ecosystems/cppEvidence.js +40 -2
  22. package/lib/ecosystems/ecosystems.js +241 -91
  23. package/lib/ecosystems/golem.js +11 -1
  24. package/lib/ecosystems/gradleutils.js +143 -2
  25. package/lib/ecosystems/kosi.js +1256 -0
  26. package/lib/ecosystems/parsers-crystal.js +179 -0
  27. package/lib/ecosystems/parsers-dotnet.js +69 -39
  28. package/lib/ecosystems/parsers-elm.js +816 -0
  29. package/lib/ecosystems/parsers-gleam.js +1 -1
  30. package/lib/ecosystems/parsers-julia.js +268 -0
  31. package/lib/ecosystems/parsers-lua.js +218 -0
  32. package/lib/ecosystems/parsers-misc.js +172 -0
  33. package/lib/ecosystems/parsers-nim.js +221 -0
  34. package/lib/ecosystems/parsers-opam.js +180 -0
  35. package/lib/ecosystems/parsers-perl.js +277 -0
  36. package/lib/ecosystems/parsers-python.js +1 -1
  37. package/lib/ecosystems/parsers-r.js +148 -0
  38. package/lib/ecosystems/parsers-rebar.js +411 -0
  39. package/lib/ecosystems/parsers-rust.js +932 -17
  40. package/lib/ecosystems/parsers-spack.js +204 -0
  41. package/lib/ecosystems/parsers-stack.js +264 -0
  42. package/lib/ecosystems/parsers-terraform.js +194 -0
  43. package/lib/ecosystems/rusi.js +126 -6
  44. package/lib/evinser/evinser.js +111 -1
  45. package/lib/helpers/agenticToolutils.js +925 -0
  46. package/lib/inventory/agentFormulationParser.js +57 -4
  47. package/lib/inventory/aiCollector.js +504 -10
  48. package/lib/inventory/aiInventory.js +12 -0
  49. package/lib/inventory/analyzer.js +488 -30
  50. package/lib/inventory/astWorker.js +6 -0
  51. package/lib/inventory/auditCategories.js +8 -0
  52. package/lib/inventory/cbomutils.js +30 -0
  53. package/lib/inventory/cdxrs.js +7 -2
  54. package/lib/inventory/communityAiConfigParser.js +215 -2
  55. package/lib/inventory/dosai.js +77 -0
  56. package/lib/inventory/envcontext.js +9 -6
  57. package/lib/inventory/evidenceUtils.js +13 -0
  58. package/lib/inventory/fetchBatch.js +74 -2
  59. package/lib/inventory/fetchRate.js +74 -11
  60. package/lib/inventory/forgeEnricher.js +3 -2
  61. package/lib/inventory/jvmToolEnv.js +6 -5
  62. package/lib/inventory/mcpConfigParser.js +166 -2
  63. package/lib/inventory/mcpDiscovery.js +11 -0
  64. package/lib/inventory/mcpServerManifest.js +389 -0
  65. package/lib/inventory/mcpbBundle.js +419 -0
  66. package/lib/inventory/plugins.js +3 -0
  67. package/lib/inventory/protobom.js +54 -88
  68. package/lib/inventory/protobomLoader.js +4 -0
  69. package/lib/inventory/rustFormulationParser.js +1 -1
  70. package/lib/inventory/source.js +10 -4
  71. package/lib/inventory/stylePackageRefs.js +41 -0
  72. package/lib/inventory/svelteUtils.js +524 -0
  73. package/lib/parsers/huggingfaceUtils.js +15 -7
  74. package/lib/parsers/onnx.js +294 -0
  75. package/lib/parsers/safetensors.js +114 -0
  76. package/lib/stages/postgen/introspection/markers.js +29 -6
  77. package/lib/stages/postgen/postgen.js +169 -1
  78. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  79. package/package.json +25 -25
  80. package/types/lib/audit/targets.d.ts.map +1 -1
  81. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  82. package/types/lib/cli/index.d.ts +2 -2
  83. package/types/lib/cli/index.d.ts.map +1 -1
  84. package/types/lib/cli/jsBom.d.ts +23 -0
  85. package/types/lib/cli/jsBom.d.ts.map +1 -1
  86. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  87. package/types/lib/cli/managedBom.d.ts +33 -0
  88. package/types/lib/cli/managedBom.d.ts.map +1 -1
  89. package/types/lib/cli/nativeBom.d.ts +88 -8
  90. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  91. package/types/lib/core/activity.d.ts.map +1 -1
  92. package/types/lib/core/env.d.ts +21 -0
  93. package/types/lib/core/env.d.ts.map +1 -1
  94. package/types/lib/core/userAgent.d.ts +56 -0
  95. package/types/lib/core/userAgent.d.ts.map +1 -0
  96. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  97. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  98. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  99. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  100. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  101. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  102. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  103. package/types/lib/ecosystems/gradleutils.d.ts +20 -0
  104. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  105. package/types/lib/ecosystems/kosi.d.ts +73 -0
  106. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  107. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  108. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  109. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  110. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  111. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  112. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  113. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  114. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  115. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  116. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  117. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  118. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  120. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  121. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  122. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  123. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  124. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  125. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  126. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  127. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  128. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  129. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  130. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  131. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  132. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  133. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  134. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  135. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  136. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  137. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  138. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  139. package/types/lib/evinser/evinser.d.ts +29 -0
  140. package/types/lib/evinser/evinser.d.ts.map +1 -1
  141. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  142. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  143. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  144. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  145. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  146. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  147. package/types/lib/inventory/auditCategories.d.ts +4 -0
  148. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  149. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  150. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  151. package/types/lib/inventory/dosai.d.ts +23 -0
  152. package/types/lib/inventory/dosai.d.ts.map +1 -1
  153. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  154. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  155. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  156. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  157. package/types/lib/inventory/fetchRate.d.ts +9 -6
  158. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  159. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  160. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  161. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  162. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  163. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  164. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  165. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  166. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  167. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  168. package/types/lib/inventory/plugins.d.ts.map +1 -1
  169. package/types/lib/inventory/protobom.d.ts +18 -13
  170. package/types/lib/inventory/protobom.d.ts.map +1 -1
  171. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  172. package/types/lib/inventory/source.d.ts.map +1 -1
  173. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  174. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  175. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  176. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  177. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  178. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  179. package/types/lib/parsers/onnx.d.ts +28 -0
  180. package/types/lib/parsers/onnx.d.ts.map +1 -0
  181. package/types/lib/parsers/safetensors.d.ts +22 -0
  182. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  183. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  184. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  185. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  186. package/types/lib/stages/postgen/postgen.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)) {
@@ -1267,6 +1344,844 @@ export function parseCargoDependencyData(cargoLockData) {
1267
1344
  return result;
1268
1345
  }
1269
1346
 
1347
+ /**
1348
+ * Normalize a cargo dependency kind to the vocabulary used by the cargo
1349
+ * manifest parsers.
1350
+ *
1351
+ * `cargo metadata` encodes a normal dependency as `null` rather than a string,
1352
+ * so a truthiness test would silently leave runtime dependencies unlabelled.
1353
+ *
1354
+ * @param {string|null|undefined} kind Dependency kind as reported by cargo
1355
+ * @returns {string} One of `runtime`, `build` or `dev`
1356
+ */
1357
+ export function normalizeCargoDepKind(kind) {
1358
+ if (kind === null || kind === undefined || kind === "" || kind === "normal") {
1359
+ return "runtime";
1360
+ }
1361
+ return kind;
1362
+ }
1363
+
1364
+ /**
1365
+ * Parse a cargo package id into its name and version.
1366
+ *
1367
+ * cargo 1.77 and later emit the package id spec form
1368
+ * (`registry+https://...#name@1.0.0`, or `path+file:///p/foo#1.0.0` where the
1369
+ * name is carried by the path); earlier releases emit `name 1.0.0 (source)`.
1370
+ *
1371
+ * @param {string} packageId cargo package id
1372
+ * @returns {{name: string, version: string}|undefined} Parsed identity
1373
+ */
1374
+ export function parseCargoPackageId(packageId) {
1375
+ if (!packageId || typeof packageId !== "string") {
1376
+ return undefined;
1377
+ }
1378
+ const hashIndex = packageId.indexOf("#");
1379
+ if (hashIndex !== -1) {
1380
+ const source = packageId.slice(0, hashIndex);
1381
+ const fragment = packageId.slice(hashIndex + 1);
1382
+ const atIndex = fragment.lastIndexOf("@");
1383
+ if (atIndex > 0) {
1384
+ return {
1385
+ name: fragment.slice(0, atIndex),
1386
+ version: fragment.slice(atIndex + 1),
1387
+ };
1388
+ }
1389
+ // `path+file:///workspace/crates/foo#1.0.0` - the name is the last path segment.
1390
+ const sourcePath = source.replace(/^[a-z+]+\+/, "").replace(/\/+$/, "");
1391
+ const name = basename(sourcePath.split("?")[0]);
1392
+ if (name) {
1393
+ return { name, version: fragment };
1394
+ }
1395
+ return undefined;
1396
+ }
1397
+ const legacyMatch = packageId.match(/^(\S+)\s+(\S+)(\s+\(.*\))?$/);
1398
+ if (legacyMatch) {
1399
+ return { name: legacyMatch[1], version: legacyMatch[2] };
1400
+ }
1401
+ return undefined;
1402
+ }
1403
+
1404
+ /**
1405
+ * Build the `name@version` key used to correlate cargo components across the
1406
+ * lock file, the manifests and `cargo metadata`.
1407
+ *
1408
+ * @param {{name: string, version: string}} pkg Package identity
1409
+ * @returns {string} Correlation key
1410
+ */
1411
+ export function cargoComponentKey(pkg) {
1412
+ if (!pkg?.name) {
1413
+ return "";
1414
+ }
1415
+ return `${pkg.name}@${pkg.version || ""}`;
1416
+ }
1417
+
1418
+ /**
1419
+ * Convert a cargo purl or bom-ref into a `name@version` correlation key.
1420
+ *
1421
+ * @param {string} purl cargo purl
1422
+ * @returns {string} Correlation key
1423
+ */
1424
+ export function cargoPurlToComponentKey(purl) {
1425
+ if (!purl || typeof purl !== "string") {
1426
+ return "";
1427
+ }
1428
+ const withoutPrefix = decodeURIComponent(purl).replace(/^pkg:cargo\//, "");
1429
+ const [identity] = withoutPrefix.split("?");
1430
+ const slashIndex = identity.lastIndexOf("/");
1431
+ return slashIndex === -1 ? identity : identity.slice(slashIndex + 1);
1432
+ }
1433
+
1434
+ /**
1435
+ * Collect the dependency kinds and platform gates declared by a single
1436
+ * `Cargo.toml`, keyed by the crate name that cargo resolves the entry to.
1437
+ *
1438
+ * Manifests declare version requirements rather than resolved versions, so the
1439
+ * returned map is keyed by name alone and is meant to label the edges leaving a
1440
+ * workspace member in the lock file graph.
1441
+ *
1442
+ * @param {string} cargoTomlFile Cargo.toml path
1443
+ * @param {object} [context] Workspace resolution context
1444
+ * @returns {Map<string, {kinds: Set<string>, targets: Set<string>, optional: boolean}>} Declared edges
1445
+ */
1446
+ export function parseCargoManifestDependencyKinds(cargoTomlFile, context = {}) {
1447
+ const declaredEdges = new Map();
1448
+ if (!cargoTomlFile || !safeExistsSync(cargoTomlFile)) {
1449
+ return declaredEdges;
1450
+ }
1451
+ const normalizedCargoTomlFile = resolve(cargoTomlFile);
1452
+ const cargoData = readCargoTomlData(normalizedCargoTomlFile);
1453
+ if (!cargoData) {
1454
+ return declaredEdges;
1455
+ }
1456
+ const workspaceContext = resolveCargoWorkspaceContext(
1457
+ normalizedCargoTomlFile,
1458
+ cargoData,
1459
+ context,
1460
+ );
1461
+ const workspaceDependencies = workspaceContext?.workspaceData?.dependencies;
1462
+ const collect = (dependencyEntries, dependencyKind, targetSelector) => {
1463
+ if (!dependencyEntries || typeof dependencyEntries !== "object") {
1464
+ return;
1465
+ }
1466
+ for (const dependencyName of Object.keys(dependencyEntries)) {
1467
+ const dependencyNode = mergeCargoWorkspaceDependencySpec(
1468
+ dependencyName,
1469
+ dependencyEntries[dependencyName],
1470
+ workspaceDependencies,
1471
+ );
1472
+ // `foo = { package = "bar" }` renames the crate; the lock file graph
1473
+ // carries the real crate name.
1474
+ const resolvedName =
1475
+ (typeof dependencyNode === "object" && dependencyNode?.package) ||
1476
+ dependencyName;
1477
+ const existing = declaredEdges.get(resolvedName) || {
1478
+ kinds: new Set(),
1479
+ targets: new Set(),
1480
+ optional: false,
1481
+ };
1482
+ existing.kinds.add(dependencyKind);
1483
+ if (targetSelector) {
1484
+ existing.targets.add(targetSelector);
1485
+ }
1486
+ if (typeof dependencyNode === "object" && dependencyNode?.optional) {
1487
+ existing.optional = true;
1488
+ }
1489
+ declaredEdges.set(resolvedName, existing);
1490
+ }
1491
+ };
1492
+ collect(cargoData.dependencies, "runtime", undefined);
1493
+ collect(cargoData["build-dependencies"], "build", undefined);
1494
+ collect(cargoData["dev-dependencies"], "dev", undefined);
1495
+ if (cargoData.target && typeof cargoData.target === "object") {
1496
+ for (const targetSelector of Object.keys(cargoData.target)) {
1497
+ const targetBlock = cargoData.target[targetSelector];
1498
+ if (!targetBlock || typeof targetBlock !== "object") {
1499
+ continue;
1500
+ }
1501
+ collect(targetBlock.dependencies, "runtime", targetSelector);
1502
+ collect(targetBlock["build-dependencies"], "build", targetSelector);
1503
+ collect(targetBlock["dev-dependencies"], "dev", targetSelector);
1504
+ }
1505
+ }
1506
+ return declaredEdges;
1507
+ }
1508
+
1509
+ /**
1510
+ * Roll per-edge cargo dependency kinds up to an effective kind per component.
1511
+ *
1512
+ * A crate is only dev-only when *every* path reaching it from a workspace root
1513
+ * traverses a dev edge, so a crate shared between a dev dependency and a normal
1514
+ * one keeps its runtime kind. Cargo never builds the dev dependencies of a
1515
+ * dependency, so dev edges are followed from the workspace roots alone.
1516
+ *
1517
+ * @param {object} graph Dependency graph
1518
+ * @param {string[]} graph.rootKeys Workspace member component keys
1519
+ * @param {Map<string, Array<{to: string, kind: string, target?: string}>>} graph.edges Adjacency map
1520
+ * @returns {Map<string, {kind: string, kinds: Set<string>, targets: Set<string>, targetGated: boolean}>} Effective kinds
1521
+ */
1522
+ export function rollupCargoDependencyKinds({ rootKeys, edges }) {
1523
+ const states = new Map();
1524
+ const queue = [];
1525
+ const visit = (key, kind, gated, targets) => {
1526
+ let state = states.get(key);
1527
+ if (!state) {
1528
+ state = { kinds: new Map(), targets: new Set() };
1529
+ states.set(key, state);
1530
+ }
1531
+ for (const target of targets) {
1532
+ state.targets.add(target);
1533
+ }
1534
+ const previous = state.kinds.get(kind);
1535
+ // An ungated path subsumes a gated one; revisit only when the knowledge improves.
1536
+ if (previous !== undefined && (previous === false || gated === true)) {
1537
+ return;
1538
+ }
1539
+ state.kinds.set(kind, gated);
1540
+ queue.push({ key, kind, gated, targets });
1541
+ };
1542
+ for (const rootKey of rootKeys || []) {
1543
+ if (rootKey) {
1544
+ visit(rootKey, "root", false, new Set());
1545
+ }
1546
+ }
1547
+ while (queue.length) {
1548
+ const current = queue.shift();
1549
+ for (const edge of edges.get(current.key) || []) {
1550
+ const edgeKind = normalizeCargoDepKind(edge.kind);
1551
+ let nextKind;
1552
+ if (current.kind === "root") {
1553
+ nextKind = edgeKind;
1554
+ } else if (edgeKind === "dev") {
1555
+ // The dev dependencies of a dependency are never built.
1556
+ continue;
1557
+ } else if (edgeKind === "build") {
1558
+ nextKind = "build";
1559
+ } else {
1560
+ nextKind = current.kind;
1561
+ }
1562
+ const nextTargets = new Set(current.targets);
1563
+ if (edge.target) {
1564
+ nextTargets.add(edge.target);
1565
+ }
1566
+ visit(edge.to, nextKind, current.gated || !!edge.target, nextTargets);
1567
+ }
1568
+ }
1569
+ const rollup = new Map();
1570
+ for (const [key, state] of states) {
1571
+ const kinds = new Set(state.kinds.keys());
1572
+ let kind;
1573
+ for (const candidate of ["root", "runtime", "build", "dev"]) {
1574
+ if (state.kinds.has(candidate)) {
1575
+ kind = candidate;
1576
+ break;
1577
+ }
1578
+ }
1579
+ if (!kind) {
1580
+ continue;
1581
+ }
1582
+ rollup.set(key, {
1583
+ kind,
1584
+ kinds,
1585
+ targets: state.targets,
1586
+ targetGated: state.kinds.get(kind) === true,
1587
+ });
1588
+ }
1589
+ return rollup;
1590
+ }
1591
+
1592
+ /**
1593
+ * Build a cargo dependency graph from the `resolve` section of
1594
+ * `cargo metadata --format-version 1`.
1595
+ *
1596
+ * @param {object} metadata Parsed `cargo metadata` output
1597
+ * @returns {{rootKeys: string[], edges: Map<string, Array<object>>, packageInfo: Map<string, object>, resolvedKeys: Set<string>}} Graph
1598
+ */
1599
+ export function parseCargoMetadataResolve(metadata) {
1600
+ const edges = new Map();
1601
+ const packageInfo = new Map();
1602
+ const rootKeys = [];
1603
+ // Every package the resolver kept. With `--filter-platform` this is the set
1604
+ // that a build for that triple actually pulls in.
1605
+ const resolvedKeys = new Set();
1606
+ if (!metadata || typeof metadata !== "object") {
1607
+ return { rootKeys, edges, packageInfo, resolvedKeys };
1608
+ }
1609
+ const keyById = new Map();
1610
+ for (const pkg of metadata.packages || []) {
1611
+ if (!pkg?.id) {
1612
+ continue;
1613
+ }
1614
+ const key = cargoComponentKey({ name: pkg.name, version: pkg.version });
1615
+ keyById.set(pkg.id, key);
1616
+ const targetKinds = new Set();
1617
+ for (const cargoTarget of pkg.targets || []) {
1618
+ for (const targetKind of cargoTarget?.kind || []) {
1619
+ targetKinds.add(targetKind);
1620
+ }
1621
+ }
1622
+ packageInfo.set(key, {
1623
+ procMacro: targetKinds.has("proc-macro"),
1624
+ source: pkg.source || "",
1625
+ manifestPath: pkg.manifest_path,
1626
+ // The native library this crate links, as declared by `links` in its
1627
+ // manifest. Only a `-sys` style crate carries one.
1628
+ links: pkg.links || "",
1629
+ version: pkg.version,
1630
+ name: pkg.name,
1631
+ // The feature names the crate declares. A vendored source directory
1632
+ // named after a feature that is not enabled belongs to a variant this
1633
+ // build does not compile.
1634
+ declaredFeatures: Object.keys(pkg.features || {}),
1635
+ });
1636
+ }
1637
+ const resolveIdToKey = (packageId) => {
1638
+ const known = keyById.get(packageId);
1639
+ if (known) {
1640
+ return known;
1641
+ }
1642
+ // A `[patch]`ed or vendored package can appear in `resolve` under an id the
1643
+ // package list spells differently.
1644
+ const parsed = parseCargoPackageId(packageId);
1645
+ return parsed ? cargoComponentKey(parsed) : "";
1646
+ };
1647
+ // `resolve.root` is null for a virtual workspace, so the roots always come
1648
+ // from `workspace_members`.
1649
+ for (const memberId of metadata.workspace_members || []) {
1650
+ const key = resolveIdToKey(memberId);
1651
+ if (key) {
1652
+ rootKeys.push(key);
1653
+ }
1654
+ }
1655
+ if (!rootKeys.length && metadata.resolve?.root) {
1656
+ const key = resolveIdToKey(metadata.resolve.root);
1657
+ if (key) {
1658
+ rootKeys.push(key);
1659
+ }
1660
+ }
1661
+ for (const node of metadata.resolve?.nodes || []) {
1662
+ const fromKey = resolveIdToKey(node?.id);
1663
+ if (!fromKey) {
1664
+ continue;
1665
+ }
1666
+ resolvedKeys.add(fromKey);
1667
+ if (Array.isArray(node.features)) {
1668
+ // The features the resolver actually enabled, as opposed to the ones the
1669
+ // manifest declares. These decide which code is compiled, which optional
1670
+ // dependencies activate, and whether a vulnerable path exists at all.
1671
+ const info = packageInfo.get(fromKey) || {};
1672
+ info.resolvedFeatures = node.features;
1673
+ packageInfo.set(fromKey, info);
1674
+ }
1675
+ const nodeEdges = edges.get(fromKey) || [];
1676
+ for (const dep of node.deps || []) {
1677
+ const toKey = resolveIdToKey(dep?.pkg);
1678
+ if (!toKey) {
1679
+ continue;
1680
+ }
1681
+ // A single edge can carry several kinds, for example a crate used both
1682
+ // as a normal and as a dev dependency by the same package.
1683
+ const depKinds = dep.dep_kinds?.length
1684
+ ? dep.dep_kinds
1685
+ : [{ kind: null, target: null }];
1686
+ for (const depKind of depKinds) {
1687
+ nodeEdges.push({
1688
+ to: toKey,
1689
+ kind: normalizeCargoDepKind(depKind?.kind),
1690
+ target: depKind?.target || undefined,
1691
+ });
1692
+ }
1693
+ }
1694
+ edges.set(fromKey, nodeEdges);
1695
+ }
1696
+ return { rootKeys, edges, packageInfo, resolvedKeys };
1697
+ }
1698
+
1699
+ /**
1700
+ * Build a cargo dependency graph from the lock file relationships, labelling
1701
+ * the edges that leave a workspace member with the kinds declared by its
1702
+ * manifest.
1703
+ *
1704
+ * The lock file is the union of every dependency kind and every target triple,
1705
+ * so only the manifests can distinguish them. The resulting labels are exact at
1706
+ * depth one and inherited below it, which is enough to identify dev-only and
1707
+ * build-only subtrees without invoking cargo.
1708
+ *
1709
+ * @param {object[]} lockDependencies Relationships from {@link parseCargoDependencyData}
1710
+ * @param {Map<string, Map<string, object>>} declaredEdgesByRoot Declared edges keyed by root component key
1711
+ * @returns {{rootKeys: string[], edges: Map<string, Array<object>>}} Graph
1712
+ */
1713
+ export function buildCargoLockDependencyGraph(
1714
+ lockDependencies,
1715
+ declaredEdgesByRoot,
1716
+ ) {
1717
+ const edges = new Map();
1718
+ const rootKeys = [...(declaredEdgesByRoot?.keys() || [])];
1719
+ for (const relationship of lockDependencies || []) {
1720
+ const fromKey = cargoPurlToComponentKey(relationship?.ref);
1721
+ if (!fromKey) {
1722
+ continue;
1723
+ }
1724
+ const declaredEdges = declaredEdgesByRoot?.get(fromKey);
1725
+ const nodeEdges = edges.get(fromKey) || [];
1726
+ for (const dependsOnRef of relationship.dependsOn || []) {
1727
+ const toKey = cargoPurlToComponentKey(dependsOnRef);
1728
+ if (!toKey) {
1729
+ continue;
1730
+ }
1731
+ const declared = declaredEdges?.get(toKey.split("@")[0]);
1732
+ if (!declared) {
1733
+ nodeEdges.push({ to: toKey, kind: "runtime" });
1734
+ continue;
1735
+ }
1736
+ const targets = declared.targets.size ? [...declared.targets] : [null];
1737
+ for (const kind of declared.kinds) {
1738
+ for (const target of targets) {
1739
+ nodeEdges.push({ to: toKey, kind, target: target || undefined });
1740
+ }
1741
+ }
1742
+ }
1743
+ edges.set(fromKey, nodeEdges);
1744
+ }
1745
+ return { rootKeys, edges };
1746
+ }
1747
+
1748
+ /**
1749
+ * Annotate cargo components with their effective dependency kind, platform
1750
+ * gates and scope.
1751
+ *
1752
+ * Components reachable only through dev edges are scoped `excluded` so
1753
+ * `--required-only` drops them. Build dependencies stay required because a
1754
+ * build script is part of producing the artifact; the kind is recorded as a
1755
+ * property so a consumer can filter on it.
1756
+ *
1757
+ * @param {object[]} pkgList Components to annotate
1758
+ * @param {Map<string, object>} rollup Effective kinds from {@link rollupCargoDependencyKinds}
1759
+ * @param {Map<string, object>} [packageInfo] Extra per-package facts
1760
+ * @param {object} [options] Annotation options
1761
+ * @param {boolean} [options.resolveIsComplete] The rollup covers every workspace in the scan, so a
1762
+ * component missing from it is an optional dependency no feature activates
1763
+ * @returns {object[]} The annotated components
1764
+ */
1765
+ export function applyCargoDependencyKindMetadata(
1766
+ pkgList,
1767
+ rollup,
1768
+ packageInfo = new Map(),
1769
+ options = {},
1770
+ ) {
1771
+ for (const pkg of pkgList || []) {
1772
+ const key = cargoComponentKey(pkg);
1773
+ const effective = rollup?.get(key);
1774
+ const info = packageInfo?.get(key);
1775
+ if (info?.resolvedFeatures?.length) {
1776
+ appendCargoProperty(
1777
+ pkg,
1778
+ "cdx:cargo:resolvedFeatures",
1779
+ [...info.resolvedFeatures].sort().join(", "),
1780
+ );
1781
+ }
1782
+ if (info?.links) {
1783
+ appendCargoProperty(pkg, "cdx:cargo:links", info.links);
1784
+ }
1785
+ if (info?.procMacro) {
1786
+ // A proc-macro crate is compiled for the host, never for the target.
1787
+ appendCargoProperty(pkg, "cdx:cargo:procMacro", "true");
1788
+ appendCargoProperty(pkg, "cdx:cargo:hostOnly", "true");
1789
+ }
1790
+ if (
1791
+ options?.filterPlatform &&
1792
+ options?.targetIncludedKeys &&
1793
+ effective?.kind !== "root" &&
1794
+ !options.targetIncludedKeys.has(key)
1795
+ ) {
1796
+ // The resolver for the requested triple did not keep this crate, so no
1797
+ // build for that triple links it.
1798
+ appendCargoProperty(pkg, "cdx:cargo:targetExcluded", "true");
1799
+ appendCargoProperty(
1800
+ pkg,
1801
+ "cdx:cargo:filterPlatform",
1802
+ options.filterPlatform,
1803
+ );
1804
+ pkg.scope = "excluded";
1805
+ }
1806
+ if (!effective) {
1807
+ // The lock file keeps optional dependencies that no enabled feature
1808
+ // pulls in; the resolve graph is what says they are never built.
1809
+ if (options?.resolveIsComplete) {
1810
+ appendCargoProperty(pkg, "cdx:cargo:activated", "false");
1811
+ if (!pkg.scope) {
1812
+ pkg.scope = "optional";
1813
+ }
1814
+ }
1815
+ continue;
1816
+ }
1817
+ if (effective.kind === "root") {
1818
+ continue;
1819
+ }
1820
+ appendCargoProperty(pkg, "cdx:cargo:dependencyKind", effective.kind);
1821
+ appendCargoProperty(pkg, "cdx:cargo:scope", effective.kind);
1822
+ if (effective.kinds.size > 1) {
1823
+ appendCargoProperty(
1824
+ pkg,
1825
+ "cdx:cargo:dependencyKinds",
1826
+ [...effective.kinds].sort().join(", "),
1827
+ );
1828
+ }
1829
+ if (effective.targets.size) {
1830
+ appendCargoProperty(
1831
+ pkg,
1832
+ "cdx:cargo:target",
1833
+ [...effective.targets].sort().join(", "),
1834
+ );
1835
+ }
1836
+ if (effective.targetGated) {
1837
+ appendCargoProperty(pkg, "cdx:cargo:targetGated", "true");
1838
+ }
1839
+ if (effective.kind === "build") {
1840
+ // A build dependency is not part of the assembly; it is consumed while
1841
+ // producing it, which is what the formulation section describes.
1842
+ appendCargoProperty(pkg, "cdx:cargo:hostOnly", "true");
1843
+ }
1844
+ if (effective.kind === "dev" && pkg.scope !== "optional") {
1845
+ pkg.scope = "excluded";
1846
+ }
1847
+ }
1848
+ return pkgList;
1849
+ }
1850
+
1851
+ /**
1852
+ * Well-known `links` values whose library is better known by another name. The
1853
+ * `links` key is a linker-level identifier, so `z` is zlib and `sqlite3` is
1854
+ * SQLite; a consumer matching advisories or license obligations needs the name
1855
+ * the upstream project publishes under. Anything absent from this map keeps its
1856
+ * `links` value, which is still what the crate declared.
1857
+ */
1858
+ const CARGO_NATIVE_LIBRARY_NAMES = Object.freeze({
1859
+ bz2: "bzip2",
1860
+ crypto: "openssl",
1861
+ git2: "libgit2",
1862
+ jpeg: "libjpeg",
1863
+ lzma: "xz",
1864
+ png: "libpng",
1865
+ sqlite3: "sqlite",
1866
+ ssl: "openssl",
1867
+ z: "zlib",
1868
+ zstd: "zstd",
1869
+ });
1870
+
1871
+ /**
1872
+ * Features whose presence means the native library is compiled into the
1873
+ * artifact rather than linked against one the environment provides.
1874
+ */
1875
+ const CARGO_STATIC_LINK_FEATURES = new Set([
1876
+ "bundled",
1877
+ "static",
1878
+ "static-link",
1879
+ "static-linking",
1880
+ "vendored",
1881
+ ]);
1882
+
1883
+ /**
1884
+ * Recover the upstream version of a native library from the version of the
1885
+ * crate that ships it.
1886
+ *
1887
+ * The `-sys` and `-src` crates carry the upstream version as semver build
1888
+ * metadata, because the crate's own version tracks the binding's releases
1889
+ * rather than the library's: `openssl-src@300.6.1+3.6.3` ships OpenSSL 3.6.3
1890
+ * and `curl-sys@0.4.74+curl-8.9.0` ships curl 8.9.0.
1891
+ *
1892
+ * @param {string} crateVersion Version of the providing crate
1893
+ * @returns {string} Upstream version, or an empty string when it is not encoded
1894
+ */
1895
+ export function cargoUpstreamVersionFromCrateVersion(crateVersion) {
1896
+ if (!crateVersion || typeof crateVersion !== "string") {
1897
+ return "";
1898
+ }
1899
+ const buildMetadata = crateVersion.split("+")[1];
1900
+ if (!buildMetadata) {
1901
+ return "";
1902
+ }
1903
+ // `curl-8.9.0` and `zstd.1.5.6` both prefix the version with the library name.
1904
+ const versionMatch = buildMetadata.match(/(\d+(?:\.\d+)*[A-Za-z0-9.-]*)$/);
1905
+ return versionMatch ? versionMatch[1] : "";
1906
+ }
1907
+
1908
+ /**
1909
+ * Recover the version of a vendored native library from the C source the crate
1910
+ * ships.
1911
+ *
1912
+ * A `-sys` crate that bundles its library carries the upstream version in a
1913
+ * header define - `ZLIB_VERSION`, `SQLITE_VERSION`, `OPENSSL_VERSION_TEXT` -
1914
+ * which is the only statement of the version for the crates that do not encode
1915
+ * it in their own version. A crate often ships more than one variant, so a
1916
+ * candidate sitting in a directory named after a feature this build did not
1917
+ * enable is dropped: `libz-sys` ships both `src/zlib` and `src/zlib-ng`, and
1918
+ * only one of them is compiled.
1919
+ *
1920
+ * @param {object} args Resolution inputs
1921
+ * @param {string} args.crateDir Directory holding the crate's unpacked source
1922
+ * @param {string} args.links The crate's `links` value
1923
+ * @param {string} args.libraryName Normalized library name
1924
+ * @param {string[]} [args.declaredFeatures] Features the crate declares
1925
+ * @param {Set<string>} [args.resolvedFeatures] Features the resolver enabled
1926
+ * @returns {{version: string, evidence: string, candidates: string[]}} Version and where it came from
1927
+ */
1928
+ export function resolveCargoVendoredLibraryVersion({
1929
+ crateDir,
1930
+ links,
1931
+ libraryName,
1932
+ declaredFeatures = [],
1933
+ resolvedFeatures = new Set(),
1934
+ }) {
1935
+ const empty = { version: "", evidence: "", candidates: [] };
1936
+ if (!crateDir || !safeExistsSync(crateDir)) {
1937
+ return empty;
1938
+ }
1939
+ const libraryTokens = [...new Set([libraryName, links])].filter(Boolean);
1940
+ const defineNames = new Set();
1941
+ for (const token of libraryTokens) {
1942
+ const upperToken = token.replace(/[^A-Za-z0-9]/g, "_").toUpperCase();
1943
+ defineNames.add(`${upperToken}_VERSION`);
1944
+ defineNames.add(`${upperToken}_VERSION_TEXT`);
1945
+ defineNames.add(`${upperToken}_VERSION_STR`);
1946
+ }
1947
+ let headerFiles;
1948
+ try {
1949
+ headerFiles = globSync("**/*.h", {
1950
+ absolute: true,
1951
+ cwd: crateDir,
1952
+ nodir: true,
1953
+ windowsPathsNoEscape: true,
1954
+ });
1955
+ } catch (_err) {
1956
+ return empty;
1957
+ }
1958
+ // Only a header named after the library states the library's version, and
1959
+ // the filter keeps a large vendored tree from being read in full.
1960
+ const interestingHeaders = headerFiles.filter((headerFile) => {
1961
+ const headerName = basename(headerFile).toLowerCase();
1962
+ return libraryTokens.some((token) =>
1963
+ headerName.includes(token.toLowerCase()),
1964
+ );
1965
+ });
1966
+ // A feature the crate declares but the resolver did not enable names a
1967
+ // variant that is not built.
1968
+ const disabledFeatures = declaredFeatures.filter(
1969
+ (feature) => !resolvedFeatures.has(feature),
1970
+ );
1971
+ const candidates = new Map();
1972
+ for (const headerFile of interestingHeaders.slice(0, 64)) {
1973
+ let headerData;
1974
+ try {
1975
+ headerData = readFileSync(headerFile, { encoding: "utf-8" });
1976
+ } catch (_err) {
1977
+ continue;
1978
+ }
1979
+ for (const match of headerData.matchAll(
1980
+ /^\s*#\s*define\s+([A-Z0-9_]+)\s+"([^"]+)"/gm,
1981
+ )) {
1982
+ if (!defineNames.has(match[1])) {
1983
+ continue;
1984
+ }
1985
+ const version = match[2].trim();
1986
+ if (!/^\d/.test(version)) {
1987
+ continue;
1988
+ }
1989
+ const pathSegments = headerFile.split(/[\\/]/);
1990
+ const isDisabledVariant = disabledFeatures.some((feature) =>
1991
+ pathSegments.includes(feature),
1992
+ );
1993
+ if (isDisabledVariant) {
1994
+ continue;
1995
+ }
1996
+ if (!candidates.has(version)) {
1997
+ candidates.set(version, headerFile);
1998
+ }
1999
+ }
2000
+ }
2001
+ if (candidates.size === 1) {
2002
+ const [version, headerFile] = [...candidates.entries()][0];
2003
+ // The absolute path runs through the user's CARGO_HOME, which does not
2004
+ // belong in a shared document; the crate-relative path is the part that
2005
+ // lets a reviewer find the same define.
2006
+ const registryMatch = headerFile.match(
2007
+ /[\\/]registry[\\/]src[\\/][^\\/]+[\\/](.+)$/,
2008
+ );
2009
+ return {
2010
+ version,
2011
+ evidence: registryMatch ? registryMatch[1] : basename(headerFile),
2012
+ candidates: [version],
2013
+ };
2014
+ }
2015
+ // Several variants remain in play, so naming one of them would be a guess.
2016
+ return { ...empty, candidates: [...candidates.keys()].sort() };
2017
+ }
2018
+
2019
+ /**
2020
+ * Build components for the native libraries that `-sys` crates link.
2021
+ *
2022
+ * A crate declaring `links = "openssl"` puts a C library into the artifact that
2023
+ * the cargo dependency graph cannot describe: its advisories and its license
2024
+ * obligations belong to OpenSSL, not to the Rust binding. When the resolver
2025
+ * enabled a vendoring feature, that library is compiled into the binary, which
2026
+ * makes it a component of the delivered assembly.
2027
+ *
2028
+ * @param {Map<string, object>} packageInfo Per-package facts from {@link parseCargoMetadataResolve}
2029
+ * @param {Map<string, Array<object>>} edges Dependency edges keyed by component key
2030
+ * @param {Map<string, object>} rollup Effective kinds, used to skip crates no build reaches
2031
+ * @returns {{components: object[], dependencies: object[]}} Native library components and their edges
2032
+ */
2033
+ export function buildCargoNativeLibraryComponents(packageInfo, edges, rollup) {
2034
+ const components = [];
2035
+ const dependencies = [];
2036
+ const componentsByRef = new Map();
2037
+ for (const [key, info] of packageInfo || []) {
2038
+ if (!info?.links) {
2039
+ continue;
2040
+ }
2041
+ // A crate no build reaches contributes no native library either.
2042
+ const effective = rollup?.get(key);
2043
+ if (rollup?.size && !effective) {
2044
+ continue;
2045
+ }
2046
+ const resolvedFeatures = new Set(info.resolvedFeatures || []);
2047
+ const linkFeatures = [...resolvedFeatures].filter((feature) =>
2048
+ CARGO_STATIC_LINK_FEATURES.has(feature),
2049
+ );
2050
+ // A `*-src` dependency exists only to compile the library from source.
2051
+ const sourceCrateKeys = (edges?.get(key) || [])
2052
+ .map((edge) => edge.to)
2053
+ .filter((to) => to.split("@")[0].endsWith("-src"));
2054
+ const isStatic = linkFeatures.length > 0 || sourceCrateKeys.length > 0;
2055
+ let upstreamVersion = "";
2056
+ let versionSource = "";
2057
+ for (const sourceCrateKey of sourceCrateKeys) {
2058
+ const sourceInfo = packageInfo.get(sourceCrateKey);
2059
+ upstreamVersion = cargoUpstreamVersionFromCrateVersion(
2060
+ sourceInfo?.version || sourceCrateKey.split("@")[1],
2061
+ );
2062
+ if (upstreamVersion) {
2063
+ versionSource = sourceCrateKey;
2064
+ break;
2065
+ }
2066
+ }
2067
+ if (!upstreamVersion) {
2068
+ upstreamVersion = cargoUpstreamVersionFromCrateVersion(info.version);
2069
+ if (upstreamVersion) {
2070
+ versionSource = key;
2071
+ }
2072
+ }
2073
+ const libraryName = CARGO_NATIVE_LIBRARY_NAMES[info.links] || info.links;
2074
+ let versionCandidates = [];
2075
+ if (!upstreamVersion && isStatic && info.manifestPath) {
2076
+ const vendoredVersion = resolveCargoVendoredLibraryVersion({
2077
+ crateDir: dirname(info.manifestPath),
2078
+ declaredFeatures: info.declaredFeatures,
2079
+ libraryName,
2080
+ links: info.links,
2081
+ resolvedFeatures,
2082
+ });
2083
+ upstreamVersion = vendoredVersion.version;
2084
+ versionCandidates = vendoredVersion.candidates;
2085
+ if (upstreamVersion) {
2086
+ versionSource = vendoredVersion.evidence;
2087
+ }
2088
+ }
2089
+ const purl = build({
2090
+ type: "generic",
2091
+ namespace: null,
2092
+ name: libraryName,
2093
+ version: upstreamVersion || null,
2094
+ });
2095
+ const bomRef = decodeURIComponent(purl);
2096
+ const providerPurl = decodeURIComponent(
2097
+ build({
2098
+ type: "cargo",
2099
+ namespace: null,
2100
+ name: info.name || key.split("@")[0],
2101
+ version: info.version || key.split("@")[1] || null,
2102
+ }),
2103
+ );
2104
+ let component = componentsByRef.get(bomRef);
2105
+ if (!component) {
2106
+ component = {
2107
+ "bom-ref": bomRef,
2108
+ name: libraryName,
2109
+ purl,
2110
+ type: "library",
2111
+ properties: [],
2112
+ evidence: {
2113
+ identity: {
2114
+ field: "purl",
2115
+ // The library is inferred from a manifest declaration rather than
2116
+ // observed in the artifact, and its version, when known at all,
2117
+ // comes from a crate version's build metadata.
2118
+ confidence: upstreamVersion ? 0.5 : 0.3,
2119
+ methods: [
2120
+ {
2121
+ technique: "manifest-analysis",
2122
+ confidence: upstreamVersion ? 0.5 : 0.3,
2123
+ value: info.manifestPath || "",
2124
+ },
2125
+ ],
2126
+ },
2127
+ },
2128
+ };
2129
+ if (upstreamVersion) {
2130
+ component.version = upstreamVersion;
2131
+ }
2132
+ componentsByRef.set(bomRef, component);
2133
+ components.push(component);
2134
+ }
2135
+ appendCargoProperty(component, "cdx:cargo:nativeLibrary", info.links);
2136
+ appendCargoProperty(component, "cdx:cargo:providedBy", providerPurl);
2137
+ appendCargoProperty(
2138
+ component,
2139
+ "cdx:cargo:linkage",
2140
+ isStatic ? "static" : "dynamic",
2141
+ );
2142
+ if (!isStatic) {
2143
+ // A dynamically linked library is not part of the assembly but is
2144
+ // expected from the deployment environment, which is what CycloneDX 1.7
2145
+ // calls an external component.
2146
+ component.isExternal = true;
2147
+ }
2148
+ if (linkFeatures.length) {
2149
+ appendCargoProperty(
2150
+ component,
2151
+ "cdx:cargo:linkageEvidence",
2152
+ linkFeatures.sort().join(", "),
2153
+ );
2154
+ }
2155
+ for (const sourceCrateKey of sourceCrateKeys) {
2156
+ appendCargoProperty(
2157
+ component,
2158
+ "cdx:cargo:linkageEvidence",
2159
+ sourceCrateKey,
2160
+ );
2161
+ }
2162
+ if (versionSource) {
2163
+ appendCargoProperty(
2164
+ component,
2165
+ "cdx:cargo:upstreamVersionSource",
2166
+ versionSource,
2167
+ );
2168
+ }
2169
+ if (!upstreamVersion && versionCandidates.length) {
2170
+ // The crate ships several variants of the library and nothing in the
2171
+ // build says which one is compiled, so the versions are reported as
2172
+ // candidates rather than one of them being chosen.
2173
+ appendCargoProperty(
2174
+ component,
2175
+ "cdx:cargo:upstreamVersionCandidates",
2176
+ versionCandidates.join(", "),
2177
+ );
2178
+ }
2179
+ dependencies.push({ ref: providerPurl, dependsOn: [bomRef] });
2180
+ dependencies.push({ ref: bomRef, dependsOn: [] });
2181
+ }
2182
+ return { components, dependencies };
2183
+ }
2184
+
1270
2185
  /**
1271
2186
  * Parses tab-separated cargo-auditable binary metadata output and returns a list
1272
2187
  * of Rust package components. Optionally fetches crates.io metadata when