diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c7ecf5ea..5f2436f4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,19 @@ version: 2 updates: + - package-ecosystem: rust-toolchain + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 1 - package-ecosystem: cargo directory: / schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: / schedule: interval: weekly + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3096352..463ac413 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,12 +17,19 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + - name: Read pinned Rust toolchain + id: pinned-toolchain + run: | + version=$(sed -n 's/^channel = "\(.*\)"$/\1/p' rust-toolchain.toml) + test -n "$version" + echo "version=$version" >> "$GITHUB_OUTPUT" + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # pinned with: - toolchain: stable + toolchain: ${{ steps.pinned-toolchain.outputs.version }} + components: llvm-tools-preview, rustfmt, clippy - name: Check formatting run: cargo fmt --check - name: Test run: cargo test --locked --workspace - name: Clippy - run: cargo clippy --locked --workspace --all-targets -- -D warnings + run: cargo clippy --locked --workspace --all-targets -- -D warnings \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 742e3096..df6ec949 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,9 @@ cargo +nightly fuzz run fuzz_score_request -- -max_total_time=60 ## Toolchain -`rust-toolchain.toml` pins the `stable` channel with `llvm-tools-preview` (needed by `cargo llvm-cov`), `rustfmt`, and `clippy`. Both workspace crates use `edition = "2024"`. Fuzzing is the one exception that needs nightly. +`rust-toolchain.toml` pins Rust `1.98.0` with `llvm-tools-preview` (needed by `cargo llvm-cov`), `rustfmt`, and `clippy`. CI reads the compiler version from that file so automated toolchain bumps update one source of truth. Both workspace crates use `edition = "2024"`. Fuzzing is the one exception that needs nightly. + +This pin is a supply-chain control, not just a convenience. Lamb and Zacchiroli (2021) describe reproducible builds as the path that lets independent builders verify that source and released binaries match, and Malka et al. (2026) show that Docker alone does not guarantee reproducibility because build instructions and environment details still matter. Wardnet therefore keeps the reviewed compiler version in `rust-toolchain.toml`, has CI derive the toolchain from that file, and avoids a second drifting Rust version source in the container build. Sources: Lamb, C., & Zacchiroli, S. (2021). *Reproducible Builds: Increasing the Integrity of Software Supply Chains* [Preprint]. arXiv. https://arxiv.org/abs/2104.06020 ; Malka, J., Zacchiroli, S., & Zimmermann, T. (2026). *Docker Does Not Guarantee Reproducibility* [Preprint]. arXiv. https://arxiv.org/abs/2601.12811. Local PDFs: `docs/papers/reproducible-builds-software-supply-chains-arxiv-2104.06020.pdf`, `docs/papers/docker-does-not-guarantee-reproducibility-arxiv-2601.12811.pdf`. ## Workspace Layout diff --git a/Dockerfile b/Dockerfile index f92c557f..3014c144 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,8 @@ -FROM rust:1.88-bookworm@sha256:af306cfa71d987911a781c37b59d7d67d934f49684058f96cf72079c3626bfe0 AS build +FROM rust:bookworm@sha256:af306cfa71d987911a781c37b59d7d67d934f49684058f96cf72079c3626bfe0 AS build WORKDIR /app COPY Cargo.toml Cargo.lock ./ +COPY rust-toolchain.toml ./ COPY src ./src COPY crates ./crates RUN cargo build --locked --release diff --git a/docs/papers/docker-does-not-guarantee-reproducibility-arxiv-2601.12811.pdf b/docs/papers/docker-does-not-guarantee-reproducibility-arxiv-2601.12811.pdf new file mode 100644 index 00000000..e8a5e5f9 Binary files /dev/null and b/docs/papers/docker-does-not-guarantee-reproducibility-arxiv-2601.12811.pdf differ diff --git a/docs/papers/reproducible-builds-software-supply-chains-arxiv-2104.06020.pdf b/docs/papers/reproducible-builds-software-supply-chains-arxiv-2104.06020.pdf new file mode 100644 index 00000000..ecc5331c Binary files /dev/null and b/docs/papers/reproducible-builds-software-supply-chains-arxiv-2104.06020.pdf differ diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b164a86d..2d9b78cc 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -4,5 +4,6 @@ # harness fails with "failed to find llvm-tools-preview" before it can measure # the workspace. Declaring it here lets rustup install it automatically in any # environment that runs cargo from this workspace (org coverage harness + CI). -channel = "stable" +channel = "1.98.0" +profile = "minimal" components = ["llvm-tools-preview", "rustfmt", "clippy"] diff --git a/tests/deployment_manifest.rs b/tests/deployment_manifest.rs index e169913d..0e309341 100644 --- a/tests/deployment_manifest.rs +++ b/tests/deployment_manifest.rs @@ -208,80 +208,82 @@ fn named_list_item_block<'a>( .collect() } -/// Locate `ADMIN_TOKEN` on the `waf-ids-ai-soc` gateway container only. +/// Locate `ADMIN_TOKEN` on the one canonical `waf-ids-ai-soc` Deployment. /// -/// Duplicate entries, literal fallback values, and `secretKeyRef.optional: true` -/// are treated as absent (fail closed). +/// Duplicate target Deployments, duplicate token entries, literal fallback values, +/// and `secretKeyRef.optional: true` are treated as absent (fail closed). fn external_admin_secret_ref(manifest: &str) -> Option> { - manifest.split("\n---\n").find_map(|document| { + let mut target_documents = manifest.split("\n---\n").filter(|document| { let lines = document.lines().collect::>(); - if !lines.iter().any(|line| line.trim() == "kind: Deployment") { - return None; - } - - if mapping_value(&lines, "metadata:", 0, "name:") != Some("waf-ids-ai-soc") { - return None; - } + lines.iter().any(|line| line.trim() == "kind: Deployment") + && mapping_value(&lines, "metadata:", 0, "name:") == Some("waf-ids-ai-soc") + && mapping_value(&lines, "metadata:", 0, "namespace:") == Some("waf-ids-ai-soc") + }); + + let document = target_documents.next()?; + if target_documents.next().is_some() { + return None; + } - let namespace = mapping_value(&lines, "metadata:", 0, "namespace:")?; - let workload_spec = nested_block(&lines, "spec:", 0); - let pod_template = nested_block(&workload_spec, "template:", 2); - let pod_spec = nested_block(&pod_template, "spec:", 4); - let containers = nested_block(&pod_spec, "containers:", 6); - let gateway = named_list_item_block(&containers, "gateway", 8); - let env = nested_block(&gateway, "env:", 10); - let admin_token_entries = env - .iter() - .filter(|line| yaml_named_entry_matches(line, 12, "ADMIN_TOKEN")) - .count(); - if admin_token_entries != 1 { - return None; - } + let lines = document.lines().collect::>(); + let namespace = mapping_value(&lines, "metadata:", 0, "namespace:")?; + let workload_spec = nested_block(&lines, "spec:", 0); + let pod_template = nested_block(&workload_spec, "template:", 2); + let pod_spec = nested_block(&pod_template, "spec:", 4); + let containers = nested_block(&pod_spec, "containers:", 6); + let gateway = named_list_item_block(&containers, "gateway", 8); + let env = nested_block(&gateway, "env:", 10); + let admin_token_entries = env + .iter() + .filter(|line| yaml_named_entry_matches(line, 12, "ADMIN_TOKEN")) + .count(); + if admin_token_entries != 1 { + return None; + } - let env_block = named_list_item_block(&env, "ADMIN_TOKEN", 12); - if env_block - .iter() - .any(|line| line.trim().starts_with("value:")) - { - return None; - } - let secret_ref_index = env_block - .iter() - .position(|line| line.trim() == "secretKeyRef:")?; - let secret_ref_indent = leading_spaces(env_block[secret_ref_index]); - let secret_ref_block = env_block[secret_ref_index + 1..] - .iter() - .take_while(|line| line.trim().is_empty() || leading_spaces(line) > secret_ref_indent) - .copied() - .collect::>(); - - let secret_name = secret_ref_block.iter().find_map(|line| { - line.trim() - .strip_prefix("name:") - .map(str::trim) - .filter(|value| !value.is_empty()) - })?; - let secret_key = secret_ref_block.iter().find_map(|line| { - line.trim() - .strip_prefix("key:") - .map(str::trim) - .filter(|value| !value.is_empty()) - })?; - match secret_ref_block.iter().find_map(|line| { - line.trim() - .strip_prefix("optional:") - .map(str::trim) - .filter(|value| !value.is_empty()) - }) { - None | Some("false") => {} - Some(_) => return None, - } + let env_block = named_list_item_block(&env, "ADMIN_TOKEN", 12); + if env_block + .iter() + .any(|line| line.trim().starts_with("value:")) + { + return None; + } + let secret_ref_index = env_block + .iter() + .position(|line| line.trim() == "secretKeyRef:")?; + let secret_ref_indent = leading_spaces(env_block[secret_ref_index]); + let secret_ref_block = env_block[secret_ref_index + 1..] + .iter() + .take_while(|line| line.trim().is_empty() || leading_spaces(line) > secret_ref_indent) + .copied() + .collect::>(); + + let secret_name = secret_ref_block.iter().find_map(|line| { + line.trim() + .strip_prefix("name:") + .map(str::trim) + .filter(|value| !value.is_empty()) + })?; + let secret_key = secret_ref_block.iter().find_map(|line| { + line.trim() + .strip_prefix("key:") + .map(str::trim) + .filter(|value| !value.is_empty()) + })?; + match secret_ref_block.iter().find_map(|line| { + line.trim() + .strip_prefix("optional:") + .map(str::trim) + .filter(|value| !value.is_empty()) + }) { + None | Some("false") => {} + Some(_) => return None, + } - Some(ExternalAdminSecretRef { - namespace, - secret_name, - secret_key, - }) + Some(ExternalAdminSecretRef { + namespace, + secret_name, + secret_key, }) } @@ -388,6 +390,44 @@ spec: ); } +#[test] +fn duplicate_target_deployments_fail_closed_even_when_first_is_valid() { + let duplicate_target_manifest = r#"apiVersion: apps/v1 +kind: Deployment +metadata: + name: waf-ids-ai-soc + namespace: waf-ids-ai-soc +spec: + template: + spec: + containers: + - name: gateway + env: + - name: ADMIN_TOKEN + valueFrom: + secretKeyRef: + name: waf-ids-ai-soc-admin + key: ADMIN_TOKEN + optional: false +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: waf-ids-ai-soc + namespace: waf-ids-ai-soc +spec: + template: + spec: + containers: + - name: gateway + env: + - name: ADMIN_TOKEN + value: repository-visible-fallback +"#; + + assert_eq!(external_admin_secret_ref(duplicate_target_manifest), None); +} + #[test] fn init_container_cannot_satisfy_the_gateway_secret_contract() { let init_container_decoy = r#"apiVersion: apps/v1 diff --git a/tests/rust_toolchain_contract.rs b/tests/rust_toolchain_contract.rs new file mode 100644 index 00000000..f64d6c00 --- /dev/null +++ b/tests/rust_toolchain_contract.rs @@ -0,0 +1,67 @@ +//! Repository contracts for the reviewed Rust compiler baseline. + +const RUST_TOOLCHAIN: &str = include_str!("../rust-toolchain.toml"); +const CI_WORKFLOW: &str = include_str!("../.github/workflows/ci.yml"); +const DEPENDABOT: &str = include_str!("../.github/dependabot.yml"); +const DOCKERFILE: &str = include_str!("../Dockerfile"); + +fn pinned_channel() -> String { + RUST_TOOLCHAIN + .lines() + .map(str::trim) + .find_map(|line| line.strip_prefix("channel = \"")) + .and_then(|line| line.strip_suffix('"')) + .expect("rust-toolchain.toml must declare a channel") + .to_string() +} + +#[test] +fn pinned_toolchain_is_consistent_in_local_and_ci_contracts() { + let pinned_channel = pinned_channel(); + let parts: Vec<_> = pinned_channel.split('.').collect(); + assert!( + parts.len() == 3 + && parts + .iter() + .all(|part| { !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit()) }), + "toolchain channel must be an exact numeric version" + ); + assert_ne!(pinned_channel, "stable"); + assert_ne!(pinned_channel, "nightly"); + assert!(CI_WORKFLOW.contains("id: pinned-toolchain")); + assert!(CI_WORKFLOW.contains("sed -n 's/^channel = ")); + assert!(CI_WORKFLOW.contains("toolchain: ${{ steps.pinned-toolchain.outputs.version }}")); + assert!(CI_WORKFLOW.contains("components: llvm-tools-preview, rustfmt, clippy")); + assert!(DOCKERFILE.contains("COPY rust-toolchain.toml ./")); + assert!(DOCKERFILE.contains("RUN cargo build --locked --release")); + assert!(!DOCKERFILE.contains("FROM rust:1.")); + assert!( + DOCKERFILE.contains("FROM rust:bookworm@sha256:"), + "container build must consume rust-toolchain.toml instead of pinning a separate Rust version" + ); + assert!( + DEPENDABOT.contains("- package-ecosystem: rust-toolchain"), + "toolchain bumps must remain automated from rust-toolchain.toml" + ); +} + +#[test] +fn stable_toolchain_updates_are_reviewable() { + let mut in_rust_toolchain_block = false; + let mut saw_weekly = false; + + for line in DEPENDABOT.lines().map(str::trim) { + if line.starts_with("- package-ecosystem: ") { + in_rust_toolchain_block = line == "- package-ecosystem: rust-toolchain"; + continue; + } + if in_rust_toolchain_block && line == "interval: weekly" { + saw_weekly = true; + } + } + + assert!( + saw_weekly, + "rust-toolchain updater must stay on a weekly cadence" + ); +}