Harden dependency and supply-chain policy (#100)
Some checks failed
Native code generation / deterministic (push) Failing after 2m6s
Imaging and meshing gate / native (push) Failing after 2m48s
JPEG 2000 feature / linux (push) Successful in 2m43s
Release platform and feature matrix / audit (push) Successful in 35s
Native Rust workspace compile / compile (push) Failing after 57s
Skia feature / linux (push) Successful in 31m0s
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled

This commit is contained in:
2026-08-11 22:57:13 +00:00
parent 9779e50ce9
commit 9e3b532a7e
21 changed files with 2086 additions and 78 deletions

View File

@@ -0,0 +1,375 @@
use super::{MatrixError, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
use std::fs::{self, OpenOptions};
use std::io::Write as _;
use std::path::Path;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
const POLICY_PATH: &str = "ci/dependency-policy.json";
const REWRITE_PATH: &str = "RUSTREWRITE.md";
#[derive(Debug, Deserialize)]
struct DependencyPolicy {
schema: u32,
msrv: String,
current: String,
reviewed_on: String,
review_by: String,
direct: Vec<DirectPolicy>,
approved_duplicates: Vec<DuplicatePolicy>,
}
#[derive(Debug, Deserialize)]
struct DirectPolicy {
name: String,
versions: Vec<String>,
purpose: String,
maintenance: String,
transitive_cost: String,
native: bool,
rewrite_anchor: String,
}
#[derive(Debug, Deserialize)]
struct DuplicatePolicy {
name: String,
versions: Vec<String>,
reason: String,
}
#[derive(Debug, Default, Serialize)]
struct ObservedDirect {
versions: BTreeSet<String>,
consumers: BTreeSet<String>,
scopes: BTreeSet<String>,
}
struct ObservedGraph {
direct: BTreeMap<String, ObservedDirect>,
resolved_external_packages: usize,
duplicates: BTreeMap<String, BTreeSet<String>>,
}
#[derive(Debug, Serialize)]
struct DependencyEvidence<'a> {
schema: u32,
msrv: &'a str,
current: &'a str,
reviewed_on: &'a str,
review_by: &'a str,
recorded_unix_seconds: u64,
direct_dependencies: &'a BTreeMap<String, ObservedDirect>,
direct_dependency_count: usize,
resolved_external_package_count: usize,
approved_duplicates: &'a BTreeMap<String, BTreeSet<String>>,
native_dependencies: Vec<&'a str>,
status: &'static str,
}
/// Audits the complete resolved dependency graph against its reviewed policy.
///
/// # Errors
///
/// Returns an error for unreviewed direct crates or versions, stale duplicate
/// approvals, wildcard or non-registry dependencies, incomplete rationale, or
/// a metadata/evidence I/O failure.
pub fn audit_dependencies(root: &Path, evidence: &Path) -> Result<()> {
if evidence.exists() {
return Err(MatrixError::new(format!(
"{} already exists; preserve or remove it before rerunning the audit",
evidence.display()
)));
}
let policy: DependencyPolicy = serde_json::from_slice(&fs::read(root.join(POLICY_PATH))?)?;
validate_policy(&policy, &fs::read_to_string(root.join(REWRITE_PATH))?)?;
let metadata = cargo_metadata(root)?;
let observed = observe(&metadata)?;
compare_direct(&policy.direct, &observed.direct)?;
compare_duplicates(&policy.approved_duplicates, &observed.duplicates)?;
let native_dependencies = policy
.direct
.iter()
.filter(|dependency| dependency.native)
.map(|dependency| dependency.name.as_str())
.collect();
let evidence_record = DependencyEvidence {
schema: 1,
msrv: &policy.msrv,
current: &policy.current,
reviewed_on: &policy.reviewed_on,
review_by: &policy.review_by,
recorded_unix_seconds: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| MatrixError::new("system clock predates Unix epoch"))?
.as_secs(),
direct_dependencies: &observed.direct,
direct_dependency_count: observed.direct.len(),
resolved_external_package_count: observed.resolved_external_packages,
approved_duplicates: &observed.duplicates,
native_dependencies,
status: "ok",
};
if let Some(parent) = evidence.parent() {
fs::create_dir_all(parent)?;
}
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(evidence)?;
serde_json::to_writer_pretty(&mut file, &evidence_record)?;
file.write_all(b"\n")?;
file.sync_all()?;
Ok(())
}
fn validate_policy(policy: &DependencyPolicy, rewrite: &str) -> Result<()> {
if policy.schema != 1 || policy.msrv != "1.96.0" || policy.current != "stable" {
return Err(MatrixError::new(
"dependency policy must use schema 1, MSRV 1.96.0, and current stable",
));
}
if !date_shape(&policy.reviewed_on) || !date_shape(&policy.review_by) {
return Err(MatrixError::new(
"dependency review dates must use YYYY-MM-DD",
));
}
let mut names = BTreeSet::new();
for dependency in &policy.direct {
if dependency.name.trim().is_empty()
|| !names.insert(dependency.name.as_str())
|| dependency.versions.is_empty()
|| dependency.purpose.trim().is_empty()
|| dependency.rewrite_anchor.trim().is_empty()
|| !rewrite.contains(&dependency.rewrite_anchor)
|| !matches!(
dependency.maintenance.as_str(),
"active" | "stable" | "monitored-native"
)
|| !matches!(
dependency.transitive_cost.as_str(),
"low" | "medium" | "high"
)
{
return Err(MatrixError::new(format!(
"dependency {} has incomplete or invalid review metadata",
dependency.name
)));
}
unique_versions(&dependency.name, &dependency.versions)?;
}
let mut duplicate_names = BTreeSet::new();
for duplicate in &policy.approved_duplicates {
if duplicate.name.trim().is_empty()
|| !duplicate_names.insert(duplicate.name.as_str())
|| duplicate.versions.len() < 2
|| duplicate.reason.trim().len() < 20
{
return Err(MatrixError::new(format!(
"duplicate {} needs unique versions and a substantive reason",
duplicate.name
)));
}
unique_versions(&duplicate.name, &duplicate.versions)?;
}
Ok(())
}
fn date_shape(value: &str) -> bool {
value.len() == 10
&& value.as_bytes()[4] == b'-'
&& value.as_bytes()[7] == b'-'
&& value
.bytes()
.enumerate()
.all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit())
}
fn unique_versions(name: &str, versions: &[String]) -> Result<()> {
let unique = versions.iter().map(String::as_str).collect::<BTreeSet<_>>();
if unique.len() != versions.len() || unique.iter().any(|version| version.trim().is_empty()) {
return Err(MatrixError::new(format!(
"dependency {name} versions must be nonempty and unique"
)));
}
Ok(())
}
fn cargo_metadata(root: &Path) -> Result<Value> {
let output = Command::new(super::cargo_program())
.args([
"metadata",
"--locked",
"--all-features",
"--format-version",
"1",
])
.current_dir(root)
.output()?;
if !output.status.success() {
return Err(MatrixError::new(format!(
"cargo metadata failed during dependency audit: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(serde_json::from_slice(&output.stdout)?)
}
fn observe(metadata: &Value) -> Result<ObservedGraph> {
let packages = metadata["packages"]
.as_array()
.ok_or_else(|| MatrixError::new("cargo metadata has no package array"))?;
let mut package_by_id = BTreeMap::new();
let mut all_external = BTreeMap::<String, BTreeSet<String>>::new();
for package in packages {
let id = string(package, "id")?;
let name = string(package, "name")?;
let version = string(package, "version")?;
let source = package["source"].as_str();
if let Some(source) = source {
if !source.starts_with("registry+https://github.com/rust-lang/crates.io-index") {
return Err(MatrixError::new(format!(
"package {name} {version} uses unapproved source {source}"
)));
}
all_external
.entry(name.to_owned())
.or_default()
.insert(version.to_owned());
}
if package["dependencies"]
.as_array()
.is_some_and(|dependencies| {
dependencies.iter().any(|dependency| {
dependency["source"].is_string() && dependency["req"].as_str() == Some("*")
})
})
{
return Err(MatrixError::new(format!(
"package {name} contains a wildcard dependency"
)));
}
package_by_id.insert(
id.to_owned(),
(name.to_owned(), version.to_owned(), source.is_some()),
);
}
let nodes = metadata["resolve"]["nodes"]
.as_array()
.ok_or_else(|| MatrixError::new("cargo metadata has no resolve nodes"))?;
let mut direct = BTreeMap::<String, ObservedDirect>::new();
for node in nodes {
let id = string(node, "id")?;
let Some((consumer, _, false)) = package_by_id.get(id) else {
continue;
};
let dependencies = node["deps"]
.as_array()
.ok_or_else(|| MatrixError::new("cargo metadata node has no dependencies"))?;
for dependency in dependencies {
let package_id = string(dependency, "pkg")?;
let Some((name, version, true)) = package_by_id.get(package_id) else {
continue;
};
let observed = direct.entry(name.clone()).or_default();
observed.versions.insert(version.clone());
observed.consumers.insert(consumer.clone());
for kind in dependency["dep_kinds"].as_array().into_iter().flatten() {
observed
.scopes
.insert(kind["kind"].as_str().unwrap_or("normal").to_owned());
}
}
}
let duplicates = all_external
.iter()
.filter(|(_, versions)| versions.len() > 1)
.map(|(name, versions)| (name.clone(), versions.clone()))
.collect();
Ok(ObservedGraph {
direct,
resolved_external_packages: all_external.values().map(BTreeSet::len).sum(),
duplicates,
})
}
fn string<'a>(value: &'a Value, key: &str) -> Result<&'a str> {
value[key]
.as_str()
.ok_or_else(|| MatrixError::new(format!("cargo metadata field {key} is not a string")))
}
fn compare_direct(
policy: &[DirectPolicy],
observed: &BTreeMap<String, ObservedDirect>,
) -> Result<()> {
let expected = policy
.iter()
.map(|dependency| {
(
dependency.name.as_str(),
dependency.versions.iter().cloned().collect::<BTreeSet<_>>(),
)
})
.collect::<BTreeMap<_, _>>();
let expected_names = expected.keys().copied().collect::<BTreeSet<_>>();
let observed_names = observed.keys().map(String::as_str).collect::<BTreeSet<_>>();
if expected_names != observed_names {
return Err(MatrixError::new(format!(
"direct dependency policy mismatch; expected {expected_names:?}, observed {observed_names:?}"
)));
}
for (name, versions) in expected {
if observed[name].versions != versions {
return Err(MatrixError::new(format!(
"direct dependency {name} version mismatch; expected {versions:?}, observed {:?}",
observed[name].versions
)));
}
}
Ok(())
}
fn compare_duplicates(
policy: &[DuplicatePolicy],
observed: &BTreeMap<String, BTreeSet<String>>,
) -> Result<()> {
let expected = policy
.iter()
.map(|duplicate| {
(
duplicate.name.clone(),
duplicate.versions.iter().cloned().collect::<BTreeSet<_>>(),
)
})
.collect::<BTreeMap<_, _>>();
if expected != *observed {
return Err(MatrixError::new(format!(
"resolved duplicate policy mismatch; expected {expected:?}, observed {observed:?}"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checked_in_dependency_policy_matches_the_locked_graph() {
let root = super::super::workspace_root(Path::new(env!("CARGO_MANIFEST_DIR"))).unwrap();
let evidence = std::env::temp_dir().join(format!(
"metacrate-dependency-audit-{}.json",
std::process::id()
));
let _ = fs::remove_file(&evidence);
audit_dependencies(&root, &evidence).unwrap();
let contents = fs::read_to_string(&evidence).unwrap();
assert!(contents.contains("\"status\": \"ok\""));
fs::remove_file(evidence).unwrap();
}
}

View File

@@ -11,6 +11,10 @@ use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus, Stdio};
use std::time::{SystemTime, UNIX_EPOCH};
mod dependency;
pub use dependency::audit_dependencies;
pub const MATRIX_PATH: &str = "ci/release-matrix.json";
const WORKFLOW_PATH: &str = ".gitea/workflows/release-matrix.yml";
const REQUIRED_PROFILES: [&str; 7] = [

View File

@@ -1,4 +1,4 @@
use metacrate_ci_matrix::{audit, load, run, workspace_root};
use metacrate_ci_matrix::{audit, audit_dependencies, load, run, workspace_root};
use std::path::{Path, PathBuf};
fn main() {
@@ -32,7 +32,26 @@ fn execute() -> Result<(), Box<dyn std::error::Error>> {
run(&root, &matrix, &profile, &evidence)?;
println!("release CI profile {profile}: ok ({})", evidence.display());
}
_ => return Err("usage: ci-matrix audit | run PROFILE --evidence FILE".into()),
Some("dependency-audit") => {
let flag = arguments
.next()
.ok_or("dependency-audit requires --evidence FILE")?;
let evidence = arguments
.next()
.ok_or("dependency-audit requires --evidence FILE")?;
if flag != "--evidence" || arguments.next().is_some() {
return Err("usage: ci-matrix dependency-audit --evidence FILE".into());
}
let evidence = absolute_or_rooted(&root, &evidence);
audit_dependencies(&root, &evidence)?;
println!("dependency policy: ok ({})", evidence.display());
}
_ => {
return Err(
"usage: ci-matrix audit | run PROFILE --evidence FILE | dependency-audit --evidence FILE"
.into(),
);
}
}
Ok(())
}