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, current: String, reviewed_on: String, review_by: String, direct: Vec, approved_duplicates: Vec, } #[derive(Debug, Deserialize)] struct DirectPolicy { name: String, versions: Vec, purpose: String, maintenance: String, transitive_cost: String, native: bool, rewrite_anchor: String, } #[derive(Debug, Deserialize)] struct DuplicatePolicy { name: String, versions: Vec, reason: String, } #[derive(Debug, Default, Serialize)] struct ObservedDirect { versions: BTreeSet, consumers: BTreeSet, scopes: BTreeSet, } struct ObservedGraph { direct: BTreeMap, resolved_external_packages: usize, duplicates: BTreeMap>, } #[derive(Debug, Serialize)] struct DependencyEvidence<'a> { schema: u32, current: &'a str, reviewed_on: &'a str, review_by: &'a str, recorded_unix_seconds: u64, direct_dependencies: &'a BTreeMap, direct_dependency_count: usize, resolved_external_package_count: usize, approved_duplicates: &'a BTreeMap>, 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, 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.current != "stable" { return Err(MatrixError::new( "dependency policy must use schema 1 and current stable Rust", )); } 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::>(); 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 { 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 { 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::>::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::::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, ) -> Result<()> { let expected = policy .iter() .map(|dependency| { ( dependency.name.as_str(), dependency.versions.iter().cloned().collect::>(), ) }) .collect::>(); let expected_names = expected.keys().copied().collect::>(); let observed_names = observed.keys().map(String::as_str).collect::>(); 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>, ) -> Result<()> { let expected = policy .iter() .map(|duplicate| { ( duplicate.name.clone(), duplicate.versions.iter().cloned().collect::>(), ) }) .collect::>(); 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(); } }