Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

xtask/tests/supply_chain.rs

//! **What CI is allowed to be, asserted from inside the tree.**
//!
//! The console's CI checked formatting, lints, tests and a bundle size, and
//! nothing at all about the supply chain it does that with. Three properties
//! were missing, and each is the kind that is easy to add once and lose in the
//! next edit — so they are asserted here rather than remembered:
//!
//! * **a dependency gate exists.** `deny.toml` at the root and a job that runs
//!   `cargo deny` over it. gunnar has had this since 2026-08-31; the console,
//!   which is the half facing the public internet, had neither;
//! * **actions are pinned by commit.** `actions/checkout@v4` is a pointer the
//!   action's owner can move; a compromised release moves under this workflow
//!   without a commit in this repository;
//! * **the workflow declares `permissions:`.** Without it `GITHUB_TOKEN`
//!   carries the repository's default, which on an older repository is write,
//!   handed to every step including `cargo install` and every build script in
//!   the graph.
//!
//! # Why this counts what it found
//!
//! A scanner that matches nothing passes. If the `uses:` lines were renamed, or
//! the workflow deleted, a bare "no unpinned action was found" would be green
//! on an empty file — the same disease `readme_version.rs` names. So the census
//! is asserted to be non-zero and the count is printed in the failure.

use std::path::{Path, PathBuf};

fn workspace_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("xtask/ has a parent")
        .to_path_buf()
}

/// Every workflow file under `.github/workflows`, as (name, contents).
fn workflows(root: &Path) -> Vec<(String, String)> {
    let dir = root.join(".github/workflows");
    let mut out = Vec::new();
    for entry in std::fs::read_dir(&dir).expect("`.github/workflows` is there") {
        let path = entry.expect("a directory entry").path();
        let is_yaml = matches!(
            path.extension().and_then(|e| e.to_str()),
            Some("yml") | Some("yaml")
        );
        if is_yaml {
            let name = path
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or_default()
                .to_owned();
            out.push((name, std::fs::read_to_string(&path).expect("a workflow")));
        }
    }
    assert!(
        !out.is_empty(),
        "no workflow files: this guard found nothing"
    );
    out
}

/// The reference of every `uses:` line: what comes after the last `@`.
///
/// Comments are cut first, so the `# v4.4.0` that names the release a digest
/// belongs to does not become part of the reference.
fn used_actions(yaml: &str) -> Vec<(usize, String)> {
    let mut out = Vec::new();
    for (n, line) in yaml.lines().enumerate() {
        let trimmed = line.trim_start().trim_start_matches("- ").trim_start();
        let Some(rest) = trimmed.strip_prefix("uses:") else {
            continue;
        };
        let value = rest.split('#').next().unwrap_or_default().trim();
        if value.is_empty() {
            continue;
        }
        out.push((n + 1, value.to_owned()));
    }
    out
}

fn is_forty_hex(s: &str) -> bool {
    s.len() == 40 && s.bytes().all(|b| b.is_ascii_hexdigit())
}

/// ★ **No action runs from a pointer its owner can move.**
#[test]
fn every_action_is_pinned_to_a_commit() {
    let root = workspace_root();
    let mut census = 0usize;
    let mut floating: Vec<String> = Vec::new();

    for (name, yaml) in workflows(&root) {
        for (line, value) in used_actions(&yaml) {
            census += 1;
            // A local action — `./.github/actions/x` — is this repository's own
            // tree and is already pinned by the commit being built.
            if value.starts_with("./") {
                continue;
            }
            match value.rsplit_once('@') {
                Some((_, reference)) if is_forty_hex(reference) => {}
                _ => floating.push(format!("{name}:{line}  {value}")),
            }
        }
    }

    assert!(
        census > 0,
        "no `uses:` line was found at all, so this guard proved nothing"
    );
    assert!(
        floating.is_empty(),
        "{} of {census} actions are pinned by a MOVING reference. A tag is a \
         pointer the action's owner can move, so a coerced release runs here \
         without a commit in this repository. Pin the commit and name the \
         version in a trailing comment:\n  {}",
        floating.len(),
        floating.join("\n  ")
    );
}

/// ★ **Every job's `GITHUB_TOKEN` is narrowed, at the top of the workflow.**
///
/// Top-level and not per job: a job added tomorrow inherits it rather than
/// having to remember it, which is the property worth asserting.
#[test]
fn every_workflow_narrows_its_token() {
    for (name, yaml) in workflows(&workspace_root()) {
        let declared = yaml
            .lines()
            .any(|line| line.starts_with("permissions:") || line.starts_with("permissions :"));
        assert!(
            declared,
            "{name} has no top-level `permissions:` block, so its GITHUB_TOKEN \
             is the repository's default — handed to every step, including \
             `cargo install` and every build script in the dependency graph"
        );
    }
}

/// ★ **The dependency gate exists and CI runs it.**
///
/// Both halves. A `deny.toml` nothing invokes is a document; a `cargo deny`
/// with no policy beside it is a different gate every time the tool's defaults
/// move.
#[test]
fn the_dependency_gate_exists_and_is_run() {
    let root = workspace_root();
    let policy = root.join("deny.toml");
    assert!(
        policy.is_file(),
        "no deny.toml at the workspace root: gunnar has had one since \
         2026-08-31 and this is the half of the estate that faces the public \
         internet"
    );

    let policy = std::fs::read_to_string(&policy).expect("deny.toml reads");
    assert!(
        policy.contains("[advisories]"),
        "deny.toml carries no [advisories] section, which is the CVE half"
    );

    let invoked = workflows(&root)
        .iter()
        .any(|(_, yaml)| yaml.contains("cargo deny") || yaml.contains("cargo audit"));
    assert!(
        invoked,
        "no workflow runs `cargo deny` or `cargo audit`, so the policy beside \
         them is a document rather than a gate"
    );
}