ci: add built-in supply chain policy fallback
This commit is contained in:
@@ -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-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-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-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-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-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
|
S0-CORPUS-001 covered cargo test -p fparkan-corpus --offline deterministic_traversal_is_creation_order_independent
|
||||||
|
|||||||
|
@@ -11,6 +11,7 @@
|
|||||||
`S0-ARCH-004`
|
`S0-ARCH-004`
|
||||||
`S0-ARCH-005`
|
`S0-ARCH-005`
|
||||||
`S0-ARCH-006`
|
`S0-ARCH-006`
|
||||||
|
`S0-ARCH-007`
|
||||||
`S0-DIAG-001`
|
`S0-DIAG-001`
|
||||||
`S0-DIAG-002`
|
`S0-DIAG-002`
|
||||||
`S0-CORPUS-001`
|
`S0-CORPUS-001`
|
||||||
|
|||||||
+118
-3
@@ -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_ROADMAP: &str = "fixtures/acceptance/stage_0_2_roadmap.md";
|
||||||
const CI_ACCEPTANCE_COVERAGE: &str = "fixtures/acceptance/coverage.tsv";
|
const CI_ACCEPTANCE_COVERAGE: &str = "fixtures/acceptance/coverage.tsv";
|
||||||
const CI_ACCEPTANCE_REPORT: &str = "target/fparkan/acceptance/stage-0-2-audit.json";
|
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() {
|
fn main() {
|
||||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
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> {
|
fn run_cargo_deny() -> Result<(), String> {
|
||||||
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
|
let cargo_deny = std::env::var_os("CARGO_DENY").unwrap_or_else(|| "cargo-deny".into());
|
||||||
let status = Command::new(cargo)
|
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([
|
.args([
|
||||||
"deny",
|
|
||||||
"check",
|
"check",
|
||||||
"--workspace",
|
"--workspace",
|
||||||
"--all-features",
|
"--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> {
|
fn run_cargo_doc() -> Result<(), String> {
|
||||||
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
|
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
|
||||||
let status = Command::new(cargo)
|
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> {
|
fn validate_workspace_license(root: &Path, failures: &mut Vec<String>) -> Result<(), String> {
|
||||||
let manifest = root.join("Cargo.toml");
|
let manifest = root.join("Cargo.toml");
|
||||||
let license = fs::read_to_string(root.join("LICENSE.txt"))
|
let license = fs::read_to_string(root.join("LICENSE.txt"))
|
||||||
@@ -1950,6 +2020,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]
|
#[test]
|
||||||
fn detects_forbidden_domain_dependencies() {
|
fn detects_forbidden_domain_dependencies() {
|
||||||
assert!(!is_forbidden_domain_dependency("fparkan-render-vulkan"));
|
assert!(!is_forbidden_domain_dependency("fparkan-render-vulkan"));
|
||||||
|
|||||||
Reference in New Issue
Block a user