aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-23 22:10:16 +0300
committerValentin Popov <valentin@popov.link>2026-06-23 22:10:16 +0300
commit0e76c2ed7c870cd0312b4762de344a0830324489 (patch)
tree7772e313dc348b8a934dcc7bcebd417245ae8435
parent4c1edef21b55929355c42341d294d3d3c6a3be96 (diff)
downloadfparkan-0e76c2ed7c870cd0312b4762de344a0830324489.tar.xz
fparkan-0e76c2ed7c870cd0312b4762de344a0830324489.zip
ci: add built-in supply chain policy fallback
-rw-r--r--fixtures/acceptance/coverage.tsv1
-rw-r--r--fixtures/acceptance/stage_0_2_roadmap.md1
-rw-r--r--xtask/src/main.rs121
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"));