@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.
- package/README.md +8 -7
- package/bin/cdxgen.js +16 -3
- package/bin/evinse.js +2 -0
- package/data/crypto-oid.json +88 -12
- package/data/remediations.json +23 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +12 -0
- package/lib/cli/index.js +192 -1
- package/lib/cli/jsBom.js +59 -2
- package/lib/cli/jvmBom.js +22 -0
- package/lib/cli/managedBom.js +152 -0
- package/lib/cli/nativeBom.js +798 -1
- package/lib/core/activity.js +120 -2
- package/lib/core/env.js +39 -1
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +13 -5
- package/lib/ecosystems/cppEvidence.js +40 -2
- package/lib/ecosystems/ecosystems.js +241 -91
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +143 -2
- package/lib/ecosystems/kosi.js +1256 -0
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +69 -39
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +172 -0
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +1 -1
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +932 -17
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/rusi.js +126 -6
- package/lib/evinser/evinser.js +111 -1
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/auditCategories.js +8 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +7 -2
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/dosai.js +77 -0
- package/lib/inventory/envcontext.js +9 -6
- package/lib/inventory/evidenceUtils.js +13 -0
- package/lib/inventory/fetchBatch.js +74 -2
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +6 -5
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +54 -88
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/source.js +10 -4
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/stages/postgen/introspection/markers.js +29 -6
- package/lib/stages/postgen/postgen.js +169 -1
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/package.json +25 -25
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +2 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +21 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +20 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +73 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +29 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +4 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +23 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +34 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +33 -0
- 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 =
|
|
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
|