diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-23 22:10:16 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-23 22:10:16 +0300 |
| commit | 0e76c2ed7c870cd0312b4762de344a0830324489 (patch) | |
| tree | 7772e313dc348b8a934dcc7bcebd417245ae8435 | |
| parent | 4c1edef21b55929355c42341d294d3d3c6a3be96 (diff) | |
| download | fparkan-0e76c2ed7c870cd0312b4762de344a0830324489.tar.xz fparkan-0e76c2ed7c870cd0312b4762de344a0830324489.zip | |
ci: add built-in supply chain policy fallback
| -rw-r--r-- | fixtures/acceptance/coverage.tsv | 1 | ||||
| -rw-r--r-- | fixtures/acceptance/stage_0_2_roadmap.md | 1 | ||||
| -rw-r--r-- | xtask/src/main.rs | 121 |
3 files changed, 120 insertions, 3 deletions
diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv index 17a3145..141520b 100644 --- a/fixtures/acceptance/coverage.tsv +++ b/fixtures/acceptance/coverage.tsv @@ -11,6 +11,7 @@ S0-ARCH-003 covered cargo xtask policy rejects platform/render adapter dependenc S0-ARCH-004 covered cargo xtask policy scans workspace-owned Rust/TOML for unsafe constructs and workspace lints forbid unsafe_code S0-ARCH-005 covered cargo xtask policy rejects Python source files, Python shebangs, and Python CI workflow steps while allowing docs requirements.txt S0-ARCH-006 covered cargo xtask policy rejects non-fparkan package directories under crates/ +S0-ARCH-007 covered cargo xtask ci runs fmt, policy, workspace test, clippy, rustdoc warnings, cargo-deny or built-in supply-chain fallback, and strict acceptance audit S0-DIAG-001 covered cargo test -p fparkan-diagnostics --offline diagnostic_chain_preserves_context S0-DIAG-002 covered cargo test -p fparkan-diagnostics --offline json_is_stable S0-CORPUS-001 covered cargo test -p fparkan-corpus --offline deterministic_traversal_is_creation_order_independent diff --git a/fixtures/acceptance/stage_0_2_roadmap.md b/fixtures/acceptance/stage_0_2_roadmap.md index 89e4afc..c365485 100644 --- a/fixtures/acceptance/stage_0_2_roadmap.md +++ b/fixtures/acceptance/stage_0_2_roadmap.md @@ -11,6 +11,7 @@ `S0-ARCH-004` `S0-ARCH-005` `S0-ARCH-006` +`S0-ARCH-007` `S0-DIAG-001` `S0-DIAG-002` `S0-CORPUS-001` diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 1a90b30..07118db 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -37,6 +37,8 @@ const PART2_ROOT_ENV: &str = "FPARKAN_CORPUS_PART2_ROOT"; const CI_ACCEPTANCE_ROADMAP: &str = "fixtures/acceptance/stage_0_2_roadmap.md"; const CI_ACCEPTANCE_COVERAGE: &str = "fixtures/acceptance/coverage.tsv"; const CI_ACCEPTANCE_REPORT: &str = "target/fparkan/acceptance/stage-0-2-audit.json"; +const APPROVED_REGISTRY_SOURCE: &str = "registry+https://github.com/rust-lang/crates.io-index"; +const SUPPLY_CHAIN_BANNED_PACKAGES: &[&str] = &["native-tls", "openssl", "openssl-sys"]; fn main() { let args = std::env::args().skip(1).collect::<Vec<_>>(); @@ -170,10 +172,18 @@ fn run_cargo_fmt_check() -> Result<(), String> { } fn run_cargo_deny() -> Result<(), String> { - let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - let status = Command::new(cargo) + let cargo_deny = std::env::var_os("CARGO_DENY").unwrap_or_else(|| "cargo-deny".into()); + let available = Command::new(&cargo_deny).arg("--version").status(); + match available { + Ok(status) if status.success() => {} + Ok(_) | Err(_) => { + eprintln!("cargo-deny is unavailable; running built-in supply-chain policy fallback"); + return run_builtin_supply_chain_policy(Path::new(".")); + } + } + + let status = Command::new(cargo_deny) .args([ - "deny", "check", "--workspace", "--all-features", @@ -191,6 +201,20 @@ fn run_cargo_deny() -> Result<(), String> { } } +fn run_builtin_supply_chain_policy(root: &Path) -> Result<(), String> { + let mut failures = Vec::new(); + validate_workspace_license(root, &mut failures)?; + validate_lockfile_supply_chain(root, &mut failures)?; + if failures.is_empty() { + Ok(()) + } else { + Err(format!( + "built-in supply-chain policy failed:\n{}", + failures.join("\n") + )) + } +} + fn run_cargo_doc() -> Result<(), String> { let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); let status = Command::new(cargo) @@ -530,6 +554,52 @@ fn validate_lockfile(root: &Path, failures: &mut Vec<String>) { } } +fn validate_lockfile_supply_chain(root: &Path, failures: &mut Vec<String>) -> Result<(), String> { + let lockfile = root.join("Cargo.lock"); + let packages = read_lockfile_packages(&lockfile)?; + for package in packages { + if let Some(source) = package.source.as_deref() { + if source != APPROVED_REGISTRY_SOURCE { + failures.push(format!( + "{}: package {} {} uses unapproved source {source}", + lockfile.display(), + package.name, + package.version + )); + } + } + if SUPPLY_CHAIN_BANNED_PACKAGES.contains(&package.name.as_str()) { + failures.push(format!( + "{}: package {} {} is banned by built-in supply-chain policy", + lockfile.display(), + package.name, + package.version + )); + } + } + Ok(()) +} + +fn read_lockfile_packages(lockfile: &Path) -> Result<Vec<CargoLockPackage>, String> { + let text = + fs::read_to_string(lockfile).map_err(|err| format!("{}: {err}", lockfile.display()))?; + let parsed = toml::from_str::<CargoLock>(&text) + .map_err(|err| format!("{}: invalid Cargo.lock TOML: {err}", lockfile.display()))?; + Ok(parsed.package) +} + +#[derive(Debug, Deserialize)] +struct CargoLock { + package: Vec<CargoLockPackage>, +} + +#[derive(Debug, Deserialize)] +struct CargoLockPackage { + name: String, + version: String, + source: Option<String>, +} + fn validate_workspace_license(root: &Path, failures: &mut Vec<String>) -> Result<(), String> { let manifest = root.join("Cargo.toml"); let license = fs::read_to_string(root.join("LICENSE.txt")) @@ -1951,6 +2021,51 @@ fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" } } #[test] + fn lockfile_supply_chain_rejects_unapproved_sources() -> Result<(), String> { + let root = temp_dir("lockfile-source"); + fs::create_dir_all(&root).map_err(|err| err.to_string())?; + fs::write( + root.join("Cargo.lock"), + r#" +[[package]] +name = "external" +version = "1.0.0" +source = "git+https://example.invalid/repo" +"#, + ) + .map_err(|err| err.to_string())?; + + let mut failures = Vec::new(); + validate_lockfile_supply_chain(&root, &mut failures)?; + + assert_eq!(failures.len(), 1); + assert!(failures[0].contains("uses unapproved source")); + fs::remove_dir_all(root).map_err(|err| err.to_string())?; + Ok(()) + } + + #[test] + fn lockfile_supply_chain_rejects_banned_packages() -> Result<(), String> { + let root = temp_dir("lockfile-ban"); + fs::create_dir_all(&root).map_err(|err| err.to_string())?; + fs::write( + root.join("Cargo.lock"), + format!( + "[[package]]\nname = \"openssl\"\nversion = \"0.10.0\"\nsource = \"{APPROVED_REGISTRY_SOURCE}\"\n" + ), + ) + .map_err(|err| err.to_string())?; + + let mut failures = Vec::new(); + validate_lockfile_supply_chain(&root, &mut failures)?; + + assert_eq!(failures.len(), 1); + assert!(failures[0].contains("is banned")); + fs::remove_dir_all(root).map_err(|err| err.to_string())?; + Ok(()) + } + + #[test] fn detects_forbidden_domain_dependencies() { assert!(!is_forbidden_domain_dependency("fparkan-render-vulkan")); assert!(is_forbidden_domain_dependency("sdl2")); |
