From 34bda0438bc2532d903a19a5a4b63a08da1d84da Mon Sep 17 00:00:00 2001 From: Constantin <74049135+constantin-jais@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:59:06 +0200 Subject: [PATCH] feat(supply-chain): execute the dependency policy with cargo-deny (Y43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align deny.toml on the constellation base policy (licence allowlist of ADR-0042 §4 and §11, crates.io as the only registry, git trust bounded to the libre-ai organisation, allow-wildcard-paths for the private workspace crates) and execute it from check:rust, so the required check now covers advisories, bans, licenses and sources of the 99 locked packages. Until now the file was versioned and executed by nothing. cargo-deny 0.19.5 is fetched by the gate and verified against the archive sha256 pinned by the fleet template, because the composition runner does not install it. Owner-arbitration: 2026-10-09 Claude-Session: https://claude.ai/code/session_01Qw4AExV42cXJ67Sj9dqLVf Signed-off-by: Constantin <74049135+constantin-jais@users.noreply.github.com> --- deny.toml | 19 +- package.json | 3 +- tools/quality/check-dependency-policy.test.ts | 88 +++++++ tools/quality/check-dependency-policy.ts | 246 ++++++++++++++++++ 4 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 tools/quality/check-dependency-policy.test.ts create mode 100644 tools/quality/check-dependency-policy.ts diff --git a/deny.toml b/deny.toml index 49bc1a2..4945131 100644 --- a/deny.toml +++ b/deny.toml @@ -1,3 +1,15 @@ +# Base dependency policy of the constellation (decision Y43, 2026-10-09). +# Every Rust repository of libre-ai carries this file unchanged and executes it +# with cargo-deny from its required check. The licence allowlist is the inbound +# policy of ADR-0042 §4 and §11; GPL (linked), AGPL and SSPL are refused by +# omission. +# +# `[sources.allow-org] github = ["libre-ai"]` bounds git-dep trust to the +# organisation (ADR-0020 §2.5). `allow-wildcard-paths` admits the version-less +# `path` (and organisation `git`) dependencies between private workspace +# crates; a registry dependency declared as `*` stays refused by +# `wildcards = "deny"`, and so does any path dependency of a publishable crate. + [graph] all-features = true @@ -7,8 +19,10 @@ yanked = "deny" [licenses] confidence-threshold = 0.8 unused-allowed-license = "allow" -# Outbound workspace licences are governed by LICENSING.md; this allowlist is -# intentionally restricted to inbound dependencies. +# The allowlist governs inbound dependencies only: the workspace's own private +# crates carry the outbound licences of the repository (REUSE.toml), which +# `private.ignore` keeps out of this check. LGPL is not admitted: Cargo links +# statically, and ADR-0042 §4 admits LGPL only where dynamic linking applies. private = { ignore = true } allow = [ "MIT", @@ -26,6 +40,7 @@ allow = [ [bans] multiple-versions = "warn" wildcards = "deny" +allow-wildcard-paths = true [sources] unknown-registry = "deny" diff --git a/package.json b/package.json index 6914dff..4d20a64 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "test": "cargo test --locked", "check": "bun run check:bun && bun run check:toolchain && bun run check:schemas && bun run check:secret-scan && bun run check:personal-data && bun run lint && bun run typecheck && bun run check:rust", "pretest": "bun run check:bun", - "check:rust": "bun run check:bun:runtime && cargo fmt --all --check && cargo clippy --locked --all-targets --all-features -- -D warnings && cargo test --locked --all-features" + "check:dependency-policy": "bun run check:bun:runtime && bun test tools/quality/check-dependency-policy.test.ts && bun tools/quality/check-dependency-policy.ts", + "check:rust": "bun run check:bun:runtime && bun run check:dependency-policy && cargo fmt --all --check && cargo clippy --locked --all-targets --all-features -- -D warnings && cargo test --locked --all-features" } } diff --git a/tools/quality/check-dependency-policy.test.ts b/tools/quality/check-dependency-policy.test.ts new file mode 100644 index 0000000..2bd40e2 --- /dev/null +++ b/tools/quality/check-dependency-policy.test.ts @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2026 Libre AI contributors +// SPDX-License-Identifier: EUPL-1.2 + +import { describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { + archiveFor, + CARGO_DENY_VERSION, + countLockedPackages, + main, + parseArguments, + sha256Of, + verifyDigest, +} from "./check-dependency-policy"; + +describe("archiveFor", () => { + test("pins the CI runner archive to the fleet template digest", () => { + const archive = archiveFor("linux", "x64"); + expect(archive.triple).toBe("x86_64-unknown-linux-musl"); + expect(archive.sha256).toBe("5ea64ae09959b5fe1072d898f95caaa89b374678ba6728d5e9ed1366745479b0"); + expect(archive.url).toBe( + `https://github.com/EmbarkStudios/cargo-deny/releases/download/${CARGO_DENY_VERSION}/cargo-deny-${CARGO_DENY_VERSION}-x86_64-unknown-linux-musl.tar.gz`, + ); + }); + + test("refuses a platform without a pinned digest", () => { + expect(() => archiveFor("win32", "x64")).toThrow("No pinned cargo-deny"); + }); +}); + +describe("verifyDigest", () => { + test("accepts matching bytes and rejects altered bytes", () => { + const bytes = new TextEncoder().encode("archive"); + expect(() => verifyDigest(bytes, sha256Of(bytes))).not.toThrow(); + expect(() => verifyDigest(new TextEncoder().encode("archivf"), sha256Of(bytes))).toThrow( + "digest mismatch", + ); + }); +}); + +describe("countLockedPackages", () => { + test("counts every [[package]] entry", () => { + const lock = 'version = 4\n\n[[package]]\nname = "a"\n\n[[package]]\nname = "b"\n'; + expect(countLockedPackages(lock)).toBe(2); + expect(countLockedPackages("version = 4\n")).toBe(0); + }); +}); + +describe("parseArguments", () => { + test("defaults to the root manifest", () => { + expect(parseArguments([], "/repo")).toEqual({ root: "/repo", manifests: ["Cargo.toml"] }); + }); + + test("refuses a manifest outside the repository", () => { + expect(() => parseArguments(["--manifest-path=../x/Cargo.toml"], "/repo")).toThrow( + "repository-relative", + ); + expect(() => parseArguments(["--manifest-path=/x/Cargo.toml"], "/repo")).toThrow( + "repository-relative", + ); + expect(() => parseArguments(["--unknown"], "/repo")).toThrow("Unknown argument"); + }); +}); + +describe("main", () => { + test("fails on a missing deny.toml instead of passing an empty graph", async () => { + const root = mkdtempSync(join(tmpdir(), "dependency-policy-")); + try { + await expect(main([`--root=${root}`])).rejects.toThrow("UNREADABLE deny.toml"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + test("fails on a missing Cargo.lock", async () => { + const root = mkdtempSync(join(tmpdir(), "dependency-policy-")); + try { + writeFileSync(join(root, "deny.toml"), "[licenses]\n"); + writeFileSync(join(root, "Cargo.toml"), "[workspace]\n"); + await expect(main([`--root=${root}`])).rejects.toThrow("UNREADABLE ./Cargo.lock"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/tools/quality/check-dependency-policy.ts b/tools/quality/check-dependency-policy.ts new file mode 100644 index 0000000..9deaf60 --- /dev/null +++ b/tools/quality/check-dependency-policy.ts @@ -0,0 +1,246 @@ +// SPDX-FileCopyrightText: 2026 Libre AI contributors +// SPDX-License-Identifier: EUPL-1.2 +// +// Dependency policy gate (decision Y43, 2026-10-09): executes this +// repository's own `deny.toml` against its committed Cargo graph(s) with +// cargo-deny — advisories, bans, licenses and sources — from the required +// check. +// +// Why the binary is fetched here rather than taken from PATH: the required +// check runs inside the source composition, whose runner installs Bun, Node +// and Rust but no cargo-deny, and the composition workflow is not this +// repository's to change. Why it is pinned by archive sha256 and not resolved +// with `cargo install`: `cargo install` resolves against a mutable registry +// and would silently change the enforcing binary between two runs of the same +// commit (same pin and same digests as the fleet template +// `reusable-dependency-policy.yml`). The cached archive is re-hashed on every +// run, so the cache is never trusted beyond its digest. +// +// Why advisories block here although the fleet template only reports them: +// Y43 makes advisories part of the required verdict. The verdict therefore +// depends on the RustSec database of the day; an accepted risk is recorded as +// a dated waiver in `deny.toml` under docs/security/ADVISORY-WAIVER-POLICY.md +// (project-governance), never as a silent exception. + +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { homedir, tmpdir } from "node:os"; +import { dirname, isAbsolute, join, resolve } from "node:path"; + +export const CARGO_DENY_VERSION = "0.19.5"; + +export interface CargoDenyArchive { + readonly triple: string; + readonly url: string; + readonly sha256: string; +} + +// Digests measured on the release archives and cross-checked against the +// `.sha256` files published with release 0.19.5. +const ARCHIVE_DIGESTS: Readonly> = { + "darwin-arm64": { + triple: "aarch64-apple-darwin", + sha256: "0cf28e019edb3708ba9755b8c822864ee6d6175d6fc167956972e78ea9ff0be3", + }, + "darwin-x64": { + triple: "x86_64-apple-darwin", + sha256: "36102b6ab83a546036ada57526227317a7827e1788ce39ac6df1c9102b86fa10", + }, + "linux-arm64": { + triple: "aarch64-unknown-linux-musl", + sha256: "f23d2b4e343a54af3d925b557294c8c9d00dacb7bb98663f995a4427efebe1db", + }, + "linux-x64": { + triple: "x86_64-unknown-linux-musl", + sha256: "5ea64ae09959b5fe1072d898f95caaa89b374678ba6728d5e9ed1366745479b0", + }, +}; + +const ARCHIVE_BYTE_LIMIT = 64 * 1024 * 1024; + +export function archiveFor(platform: string, arch: string): CargoDenyArchive { + const entry = ARCHIVE_DIGESTS[`${platform}-${arch}`]; + if (entry === undefined) { + throw new Error(`No pinned cargo-deny ${CARGO_DENY_VERSION} archive for ${platform}-${arch}`); + } + return { + triple: entry.triple, + sha256: entry.sha256, + url: `https://github.com/EmbarkStudios/cargo-deny/releases/download/${CARGO_DENY_VERSION}/cargo-deny-${CARGO_DENY_VERSION}-${entry.triple}.tar.gz`, + }; +} + +export function sha256Of(bytes: Uint8Array): string { + return createHash("sha256").update(bytes).digest("hex"); +} + +export function verifyDigest(bytes: Uint8Array, expected: string): void { + const actual = sha256Of(bytes); + if (actual !== expected) { + throw new Error(`cargo-deny archive digest mismatch: expected ${expected}, got ${actual}`); + } +} + +/** Number of `[[package]]` entries in a Cargo.lock — the volume the gate examined. */ +export function countLockedPackages(lockText: string): number { + return lockText.split("\n").filter((line) => line.trim() === "[[package]]").length; +} + +export interface Arguments { + readonly root: string; + readonly manifests: readonly string[]; +} + +export function parseArguments(argv: readonly string[], defaultRoot: string): Arguments { + let root = defaultRoot; + const manifests: string[] = []; + for (const argument of argv) { + if (argument.startsWith("--root=")) { + root = resolve(argument.slice("--root=".length)); + } else if (argument.startsWith("--manifest-path=")) { + const manifest = argument.slice("--manifest-path=".length); + if (manifest === "" || isAbsolute(manifest) || manifest.split(/[\\/]/).includes("..")) { + throw new Error(`--manifest-path must be repository-relative: ${manifest}`); + } + manifests.push(manifest); + } else { + throw new Error(`Unknown argument: ${argument}`); + } + } + return { root, manifests: manifests.length === 0 ? ["Cargo.toml"] : manifests }; +} + +function readRequired(path: string, label: string): string { + try { + return readFileSync(path, "utf8"); + } catch (error) { + // An unreadable declared source is a failure, never an empty pass. + throw new Error(`UNREADABLE ${label} (${path}): ${(error as Error).message}`); + } +} + +function cacheDirectory(): string { + const base = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"); + return join(base, "libre-ai", "cargo-deny", CARGO_DENY_VERSION); +} + +async function obtainArchive(archive: CargoDenyArchive): Promise { + const cached = join( + cacheDirectory(), + `cargo-deny-${CARGO_DENY_VERSION}-${archive.triple}.tar.gz`, + ); + if (existsSync(cached)) { + const bytes = new Uint8Array(readFileSync(cached)); + if (sha256Of(bytes) === archive.sha256) return bytes; + rmSync(cached, { force: true }); + } + const response = await fetch(archive.url, { redirect: "follow" }); + if (!response.ok) { + throw new Error(`Cannot download ${archive.url}: HTTP ${response.status}`); + } + const bytes = new Uint8Array(await response.arrayBuffer()); + if (bytes.byteLength > ARCHIVE_BYTE_LIMIT) { + throw new Error(`cargo-deny archive exceeds ${ARCHIVE_BYTE_LIMIT} bytes`); + } + verifyDigest(bytes, archive.sha256); + mkdirSync(dirname(cached), { recursive: true }); + const partial = `${cached}.${process.pid}.partial`; + writeFileSync(partial, bytes); + renameSync(partial, cached); + return bytes; +} + +function run(command: string, args: readonly string[], cwd: string): number { + const result = spawnSync(command, args, { cwd, stdio: "inherit" }); + if (result.error !== undefined) throw result.error; + return result.status ?? 1; +} + +export async function main(argv: readonly string[]): Promise { + const { root, manifests } = parseArguments(argv, resolve(import.meta.dir, "..", "..")); + const config = join(root, "deny.toml"); + readRequired(config, "deny.toml"); + let lockedPackages = 0; + for (const manifest of manifests) { + const manifestPath = join(root, manifest); + readRequired(manifestPath, manifest); + const lock = join(dirname(manifestPath), "Cargo.lock"); + lockedPackages += countLockedPackages(readRequired(lock, `${dirname(manifest)}/Cargo.lock`)); + } + + const archive = archiveFor(process.platform, process.arch); + const bytes = await obtainArchive(archive); + const scratch = mkdtempSync(join(tmpdir(), "cargo-deny-")); + try { + const tarball = join(scratch, "cargo-deny.tar.gz"); + writeFileSync(tarball, bytes); + if (run("tar", ["-xzf", tarball, "-C", scratch], scratch) !== 0) { + throw new Error("Cannot extract the cargo-deny archive"); + } + const binary = join( + scratch, + `cargo-deny-${CARGO_DENY_VERSION}-${archive.triple}`, + "cargo-deny", + ); + const version = spawnSync(binary, ["--version"], { encoding: "utf8" }); + if (version.status !== 0 || version.stdout.trim() !== `cargo-deny ${CARGO_DENY_VERSION}`) { + throw new Error( + `Unexpected cargo-deny binary: ${version.stdout ?? ""}${version.stderr ?? ""}`, + ); + } + + for (const manifest of manifests) { + console.log(`cargo-deny ${CARGO_DENY_VERSION} check: ${manifest} with deny.toml`); + const status = run( + binary, + [ + "--locked", + "--color", + "never", + "--manifest-path", + join(root, manifest), + "--workspace", + "check", + "--config", + config, + "--show-stats", + "advisories", + "bans", + "licenses", + "sources", + ], + root, + ); + if (status !== 0) { + console.error(`Dependency policy FAILED for ${manifest} (cargo-deny exit ${status}).`); + return 1; + } + } + } finally { + rmSync(scratch, { recursive: true, force: true }); + } + console.log( + `Dependency policy verified: advisories, bans, licenses and sources hold for ` + + `${manifests.length} manifest(s), ${lockedPackages} locked package(s) inspected ` + + `(cargo-deny ${CARGO_DENY_VERSION}, archive sha256 ${archive.sha256}).`, + ); + return 0; +} + +if (import.meta.main) { + try { + process.exitCode = await main(process.argv.slice(2)); + } catch (error) { + console.error((error as Error).message); + process.exitCode = 1; + } +}