Skip to content

Instantly share code, notes, and snippets.

@thoughtpolice
Last active September 8, 2024 22:02
Show Gist options
  • Save thoughtpolice/cff5016514057bde5c1a4cf15ad28661 to your computer and use it in GitHub Desktop.
Save thoughtpolice/cff5016514057bde5c1a4cf15ad28661 to your computer and use it in GitHub Desktop.
Scan Cargo.lock for Rust advisories via https://osv.dev
// SPDX-FileCopyrightText: © 2024 Austin Seipp
// SPDX-License-Identifier: Apache-2.0
// 3p-osv-rust: check buck/third-party/rust/Cargo.lock against https://osv.dev metadata
import { parse as parseTOML } from "jsr:@std/toml";
// ---------------------------------------------------------------------------------------------------------------------
const OSV_API_BASE = "https://api.osv.dev/v1";
const QUERY_BATCH_SIZE = 30;
// This is an entry in a [[package]] section of a Cargo.lock file
type CargoLockPkg = {
name: string;
version: string;
source: string;
checksum: string;
dependencies: string[];
features: string[];
optional: boolean;
platform: string[];
uid: string;
id: string;
yanked: boolean;
};
type OSVRustRequest = {
version: string;
package: {
purl: string;
};
};
// -- MARK: Cargo.lock parsing
const rust_lock_path = Deno.args.length === 0
? "buck/third-party/rust/Cargo.lock"
: Deno.args[0];
const rust_lock = await Deno.readTextFile(rust_lock_path);
console.log(`Examining Rust packages in ${rust_lock_path}...`);
// deno-lint-ignore no-explicit-any
const toml: Record<string, any> = parseTOML(rust_lock);
if (!toml) {
console.error("ERROR: Could not parse Cargo.lock file");
Deno.exit(1);
}
if (toml["version"] !== 3) {
console.error("ERROR: Cargo.lock file is not version 3");
Deno.exit(1);
}
const packages: CargoLockPkg[] = [];
// make sure we understand every top-level key/value pair
for (const key in toml) {
if (key === "version") continue;
if (key === "package") {
packages.push(...toml[key]);
continue;
}
console.error(`ERROR: Unexpected key in Cargo.lock: ${key}`);
Deno.exit(1);
}
// ---------------------------------------------------------------------------------------------------------------------
// -- MARK: Request batching
const num_pkgs = Object.keys(packages).length;
const all_batches: OSVRustRequest[][] = [];
// First, batch sets of Cargo packages into sizes of BATCH_SIZE, because it's
// just not feasible to go through hundreds of packages in one go.
let current_batch: OSVRustRequest[] = [];
for (const pkg in packages) {
const p = packages[pkg];
if (!p.name || !p.version) {
console.error(`ERROR: Package is missing name or version: ${p}`);
Deno.exit(1);
}
const body: OSVRustRequest = {
version: p.version,
package: { purl: `pkg:cargo/${p.name}` },
};
current_batch.push(body);
if (Object.keys(current_batch).length >= QUERY_BATCH_SIZE) {
all_batches.push(current_batch);
current_batch = [];
}
}
if (Object.keys(current_batch).length > 0) {
all_batches.push(current_batch);
current_batch = [];
}
// sanity check: ensure total sum of all batches is equal to the number of packages
const sum = all_batches.reduce(
(acc, batch) => acc + Object.keys(batch).length,
0,
);
if (sum !== num_pkgs) {
console.error(
`ERROR: Sum of all batches (${sum}) does not equal number of packages (${num_pkgs})??? This is a bug!`,
);
Deno.exit(1);
}
console.log(
`Found ${num_pkgs} packages in Cargo.lock; split into ${all_batches.length} batches`,
);
// ---------------------------------------------------------------------------------------------------------------------
// -- MARK: Querying
// now turn the batches into a set of promises
const promises = all_batches.map(async (batch) => {
const resp = await fetch(`${OSV_API_BASE}/querybatch`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ queries: batch }),
});
const json = await resp.json();
return json["results"];
});
// now, use Promises.all to wait for all the batches to complete
console.log("Querying OSV for Rust package vulnerabilities...");
const results = await Promise.all(promises);
// ---------------------------------------------------------------------------------------------------------------------
// -- MARK: Display results
// deno-lint-ignore no-explicit-any
const getVulnDetails = async (vuln: { id: string }): Promise<any> => {
const resp = await fetch(`${OSV_API_BASE}/vulns/${vuln.id}`, {
method: "GET",
headers: {
"Accept": "application/json",
},
});
return await resp.json();
};
let not_vulnerable = 0;
const vuln_crates = [];
for (let i = 0; i < results.length; i++) {
const resp = results[i];
for (let j = 0; j < resp.length; j++) {
const crate = all_batches[i][j];
if (Object.keys(resp[j]).length === 0) {
not_vulnerable++;
} else {
vuln_crates.push([crate, resp[j]]);
}
}
}
console.error(
`Finished: ${not_vulnerable} packages with no known vulnerabilities.`,
);
console.error(`Found ${vuln_crates.length} vulnerable packages.`);
if (vuln_crates.length > 0) {
for (let i = 0; i < vuln_crates.length; i++) {
const [crate, resp] = vuln_crates[i];
const vulns: { id: string }[] = resp["vulns"];
const vulnDetails: {
id: string;
aliases: string[];
summary: string;
}[] = await Promise.all(vulns.map(getVulnDetails));
// remove duplicate vulnerabilities by looking at aliases
let numDupes = 0;
const aliases: string[] = [];
for (let j = 0; j < vulnDetails.length; j++) {
if (aliases.includes(vulnDetails[j].id)) {
numDupes++;
continue;
}
aliases.push(...vulnDetails[j].aliases);
}
// Now report the vulnerabilities
const dupeInfo = numDupes == 0
? ""
: numDupes == 1
? " (1 dupe)"
: ` (${numDupes} dupes)`;
console.error(
` ${crate.package.purl}-${crate.version}: ${vulns.length} advisories${dupeInfo}`,
);
for (let j = 0; j < vulns.length; j++) {
if (aliases.includes(vulns[j].id)) continue;
const details = vulnDetails[j];
console.error(` - ${details.id}: ${details.summary}`);
console.error(
` <https://osv.dev/vulnerability/${details.id}>`,
);
}
}
Deno.exit(1);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment