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

nordisk/holger

server/lib/tests/tier1_sources_steps_rules.rs

//! **Tier 1's RON: `sources:`, `steps:` and `rules:`, on a real server** (Holger G0,
//! owner order 2026-10-23).
//!
//! * Two upload sources with different gates: the SAME package is refused from one and
//!   passes from the other, and the refusal names the rule and the source.
//! * The floor (`rules: (require_signed: true)`) refuses an unsigned package from every
//!   source.
//! * A source cannot loosen the floor: the config is refused at load, by name.
//! * A source can tighten the floor: its gate refuses what the floor lets in.
//! * The artifact's source and effective policy are in GetArtifactState and in the audit
//!   trail.
//! * Unknown keys in every block, and `steps:` beside `promotion:`, are refused by name.

// A server's test, never a gateway build's (lane HOL-CLEANUP): a gateway build is a
// downstream, and its sources fetch and its steps scan, which a downstream refuses (holger-downstream-1).
#![cfg(not(feature = "gateway"))]

use std::io::Write;
use traits::test_support::hold_port;

use holger_server_lib::grpc::holger_proto::{
    AdvanceArtifactRequest, ArtifactId as ProtoId, GetArtifactStateRequest, IntakeVerb,
};
use holger_server_lib::promote::duties::client;
use holger_server_lib::promote::intake::ArtifactState;
use holger_server_lib::{read_ron_config, wire_holger, ArtifactId, Holger};

const PRINCIPALS: [&str; 5] = ["ida", "ci", "bob", "adm", "aud"];

fn token(who: &str) -> String {
    format!("{who}-token-of-sufficient-length-for-holger")
}

struct Running {
    data: tempfile::TempDir,
    _cfg: tempfile::NamedTempFile,
    holger: Holger,
    base: String,
    grpc: String,
}

/// The whole config: four raw intake repositories, the principals, and the tier 1 blocks.
fn ron(d: &str, grpc: u16, http: u16, tier1: &str) -> String {
    let repo = |name: &str| {
        format!(
            r#"(ron_name: "{name}", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
         ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")),
         ron_data_dir: Some("{d}/{name}")),"#
        )
    };
    let methods: Vec<String> = PRINCIPALS
        .iter()
        .map(|p| {
            format!(
                r#"StaticToken(subject: "{p}", token: Some("{}"))"#,
                token(p)
            )
        })
        .collect();
    format!(
        r#"(
    repositories: [ {} {} {} {} ],
    exposed_endpoints: [ (ron_name: "door", ron_url: "127.0.0.1:{grpc}", ron_http_url: Some("127.0.0.1:{http}")) ],
    storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{d}") ],
    audit: (dir: "{d}/audit", enabled: true),
    auth: Some((
        methods: [ {} ],
        duties: {{ "ida": [Importer], "ci": [Importer], "bob": [SecurityApprover], "adm": [RepositoryAdmin], "aud": [Auditor] }},
    )),
    {tier1}
)"#,
        repo("incoming"),
        repo("quarantine"),
        repo("approved"),
        repo("production"),
        methods.join(", "),
    )
}

fn load(tier1: &str) -> anyhow::Result<Holger> {
    let data = tempfile::tempdir().unwrap();
    let mut cfg = tempfile::NamedTempFile::new().unwrap();
    cfg.write_all(ron(&data.path().display().to_string(), 1, 2, tier1).as_bytes())
        .unwrap();
    read_ron_config(cfg.path())
}

fn refused(tier1: &str) -> String {
    match load(tier1) {
        Ok(_) => panic!("must be refused: {tier1}"),
        Err(e) => format!("{e:#}"),
    }
}

fn start(tier1: &str) -> Running {
    let mut last = String::new();
    for _ in 0..8 {
        let (grpc, http) = (hold_port(), hold_port());
        let data = tempfile::tempdir().unwrap();
        let mut cfg = tempfile::NamedTempFile::new().unwrap();
        cfg.write_all(ron(&data.path().display().to_string(), grpc, http, tier1).as_bytes())
            .unwrap();
        let go = || -> Result<Holger, String> {
            let mut h = read_ron_config(cfg.path()).map_err(|e| format!("config: {e:#}"))?;
            h.instantiate_backends()
                .map_err(|e| format!("backends: {e:#}"))?;
            wire_holger(&mut h).map_err(|e| format!("wire: {e:#}"))?;
            h.start().map_err(|e| format!("start: {e:#}"))?;
            Ok(h)
        };
        match go() {
            Ok(holger) => {
                return Running {
                    data,
                    _cfg: cfg,
                    holger,
                    base: format!("http://127.0.0.1:{http}"),
                    grpc: format!("127.0.0.1:{grpc}"),
                }
            }
            Err(e) if e.starts_with("config") => panic!("{e}"),
            Err(e) => last = e,
        }
    }
    panic!("server never started; last error: {last}");
}

fn wheel() -> Vec<u8> {
    zip_entry::fixture::zip(&[
        ("demo_pkg/__init__.py", b"".as_slice()),
        (
            "demo_pkg-1.0.0.dist-info/METADATA",
            b"Metadata-Version: 2.1\nName: demo-pkg\nVersion: 1.0.0\nLicense-Expression: MIT\n"
                .as_slice(),
        ),
    ])
}

const WHEEL: &str = "demo_pkg-1.0.0-py3-none-any.whl";

fn id(path: &str) -> ArtifactId {
    ArtifactId {
        namespace: None,
        name: path.into(),
        version: String::new(),
    }
}

async fn wait_healthy(c: &reqwest::Client, base: &str) {
    for _ in 0..100 {
        if let Ok(r) = c.get(format!("{base}/healthz")).send().await {
            if r.status() == 200 {
                return;
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(20)).await;
    }
    panic!("gateway never became healthy at {base}");
}

async fn put(r: &Running, who: &str, path: &str) -> (u16, String) {
    put_bytes(r, who, path, wheel()).await
}

async fn put_bytes(r: &Running, who: &str, path: &str, body: Vec<u8>) -> (u16, String) {
    let _ = rustls::crypto::ring::default_provider().install_default();
    let c = reqwest::Client::new();
    wait_healthy(&c, &r.base).await;
    let resp = c
        .put(format!("{}/incoming/{path}", r.base))
        .header("Authorization", format!("Bearer {}", token(who)))
        .body(body)
        .send()
        .await
        .unwrap();
    (
        resp.status().as_u16(),
        resp.text().await.unwrap_or_default(),
    )
}

/// INGESTED → SCANNED by the approver, through the real door
/// (`PromotionService.AdvanceArtifact` over gRPC): the source's gates refuse there.
async fn scan(r: &Running, path: &str) -> Result<(), String> {
    let req = AdvanceArtifactRequest {
        artifact: Some(ProtoId {
            namespace: String::new(),
            name: path.into(),
            version: String::new(),
        }),
        verb: IntakeVerb::Advance as i32,
        ..Default::default()
    };
    let a = client::advance(&r.grpc, Some(&token("bob")), req).await?;
    if a.state == ArtifactState::Scanned.name() {
        Ok(())
    } else {
        Err(format!("the door answered {} for {path}", a.state))
    }
}

fn audit_text(r: &Running) -> String {
    let mut out = String::new();
    for e in std::fs::read_dir(r.data.path().join("audit"))
        .unwrap()
        .flatten()
    {
        out.push_str(&String::from_utf8_lossy(
            &std::fs::read(e.path()).unwrap_or_default(),
        ));
    }
    out
}

#[tokio::test(flavor = "multi_thread")]
async fn the_same_package_is_refused_from_one_source_and_passed_from_the_other() {
    let r = start(
        r#"sources: [
            (name: "team", kind: upload, gates: (provenance: prefer)),
            (name: "vendor", kind: upload, importers: ["ci"], gates: (license: (deny: ["MIT"]))),
        ],
        steps: (unknown_license: Pass),
        rules: (sandbox: off, require_signed: false, require_provenance: false),"#,
    );
    let (team, vendor) = (format!("team/{WHEEL}"), format!("vendor/{WHEEL}"));
    assert_eq!(put(&r, "ida", &team).await.0, 201, "team ingests");
    let (code, body) = put(&r, "ida", &vendor).await;
    assert!(
        code >= 400 && body.contains("may not upload as source 'vendor'"),
        "{code} {body}"
    );
    assert_eq!(
        put(&r, "ci", &vendor).await.0,
        201,
        "vendor's own importer ingests"
    );
    let (code, body) = put(&r, "ida", &format!("nobody/{WHEEL}")).await;
    assert!(
        code >= 400 && body.contains("names no upload source"),
        "{code} {body}"
    );

    scan(&r, &team)
        .await
        .expect("the team's gates pass the MIT wheel");
    let why = scan(&r, &vendor)
        .await
        .expect_err("the vendor's gate refuses MIT");
    assert!(
        why.contains("deny_licenses") && why.contains("source 'vendor'") && why.contains("MIT"),
        "{why}"
    );
    let chain = r.holger.intake().unwrap();
    assert_eq!(chain.state(&id(&team)), ArtifactState::Scanned);
    assert_eq!(
        chain.state(&id(&vendor)),
        ArtifactState::Ingested,
        "a refusal moves nothing"
    );

    // The source and the effective policy are in GetArtifactState …
    let s = client::state(
        &r.grpc,
        Some(&token("aud")),
        GetArtifactStateRequest {
            artifact: Some(ProtoId {
                namespace: String::new(),
                name: vendor.clone(),
                version: String::new(),
            }),
        },
    )
    .await
    .expect("an auditor reads the state");
    assert_eq!(s.source, "vendor");
    let p: serde_json::Value =
        serde_json::from_str(&s.effective_policy).expect("the policy is JSON");
    assert_eq!(p["source"], "vendor");
    assert!(
        p["deny_licenses"]
            .as_array()
            .unwrap()
            .iter()
            .any(|b| b["value"] == "MIT" && b["by"] == "source 'vendor'"),
        "{p}"
    );
    // … and in the audit trail, with the rule that refused it.
    let audit = audit_text(&r);
    assert!(
        audit.contains("source=vendor"),
        "the ingest names its source and policy"
    );
    assert!(
        audit.contains("deny_licenses") && audit.contains("source 'vendor'"),
        "the refusal names its rule"
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn the_floor_refuses_an_unsigned_package_from_every_source() {
    let r = start(
        r#"sources: [ (name: "team", kind: upload), (name: "vendor", kind: upload, gates: (cve: (max: medium))) ],
        steps: (unknown_license: Pass),
        rules: (sandbox: off, require_signed: true),"#,
    );
    for src in ["team", "vendor"] {
        let path = format!("{src}/{WHEEL}");
        assert_eq!(put(&r, "ida", &path).await.0, 201);
        let why = scan(&r, &path).await.expect_err("unsigned is refused");
        assert!(
            why.contains("require_signed: true") && why.contains(&format!("source '{src}'")),
            "{src}: {why}"
        );
    }
}

#[tokio::test(flavor = "multi_thread")]
async fn a_source_can_tighten_the_floor() {
    let r = start(
        r#"sources: [ (name: "team", kind: upload), (name: "strict", kind: upload, gates: (block_formats: ["whl"], sbom: require)) ],
        steps: (unknown_license: Pass),
        rules: (sandbox: off, require_signed: false, require_provenance: false),"#,
    );
    assert_eq!(
        put(&r, "ida", &format!("team/{WHEEL}")).await.0,
        201,
        "the floor lets a wheel in"
    );
    let (code, body) = put(&r, "ida", &format!("strict/{WHEEL}")).await;
    assert!(code >= 400, "{code}");
    assert!(
        body.contains("block_formats") && body.contains("source 'strict'"),
        "{body}"
    );
}

/// The wheel with a planted GitHub token (assembled at run time, never whole in
/// this file) and a planted ACME token for an operator's own rule.
fn leaky_wheel() -> (Vec<u8>, String) {
    let gh = ["ghp_", "R8kLm2Qx9Tz4Vb7Nc1Wd5Ys3Hf6Jg0Pa2Ue8"].concat();
    let acme = ["acme_", "0123456789abcdef"].concat();
    let code = format!("TOKEN = '{gh}'\nOTHER = '{acme}'\n");
    let w = zip_entry::fixture::zip(&[
        ("demo_pkg/__init__.py", code.as_bytes()),
        (
            "demo_pkg-1.0.0.dist-info/METADATA",
            b"Metadata-Version: 2.1\nName: demo-pkg\nVersion: 1.0.0\nLicense-Expression: MIT\n"
                .as_slice(),
        ),
    ]);
    (w, gh)
}

#[tokio::test(flavor = "multi_thread")]
async fn a_planted_secret_is_refused_where_the_source_blocks_and_recorded_where_it_warns() {
    let r = start(
        r#"sources: [ (name: "team", kind: upload), (name: "vendor", kind: upload, gates: (secrets: block)) ],
        steps: (unknown_license: Pass),
        rules: (sandbox: off, require_signed: false, require_provenance: false, secrets: warn),"#,
    );
    let (w, gh) = leaky_wheel();
    let (team, vendor) = (format!("team/{WHEEL}"), format!("vendor/{WHEEL}"));
    assert_eq!(put_bytes(&r, "ida", &team, w.clone()).await.0, 201);
    assert_eq!(put_bytes(&r, "ida", &vendor, w).await.0, 201);
    scan(&r, &team)
        .await
        .expect("the floor only warns about secrets");
    let why = scan(&r, &vendor)
        .await
        .expect_err("the vendor's gate blocks secrets");
    assert!(
        why.contains("secrets: block")
            && why.contains("source 'vendor'")
            && why.contains("demo_pkg/__init__.py:1"),
        "{why}"
    );
    assert!(
        !why.contains(&gh[..12]),
        "the refusal never holds the secret: {why}"
    );
    let chain = r.holger.intake().unwrap();
    assert_eq!(chain.state(&id(&team)), ArtifactState::Scanned);
    assert_eq!(chain.state(&id(&vendor)), ArtifactState::Ingested);
    let audit = audit_text(&r);
    assert!(
        audit.contains("secrets=block(source 'vendor')"),
        "the ingest names the secrets rule"
    );
    assert!(
        !audit.contains(&gh[..12]),
        "the audit trail never holds the secret"
    );
    // The team's warning is on its stored evidence, by rule and path.
    let ev = std::fs::read_dir(r.data.path().join("holger-scan-evidence"))
        .map(|d| d.flatten().map(|e| e.path()).collect::<Vec<_>>())
        .unwrap_or_default();
    let mut text = String::new();
    fn walk(p: &std::path::Path, out: &mut String) {
        if p.is_dir() {
            for e in std::fs::read_dir(p).unwrap().flatten() {
                walk(&e.path(), out);
            }
        } else {
            out.push_str(&String::from_utf8_lossy(
                &std::fs::read(p).unwrap_or_default(),
            ));
        }
    }
    for p in ev {
        walk(&p, &mut text);
    }
    assert!(
        text.contains("secret:github-token"),
        "the evidence names the rule"
    );
    assert!(
        !text.contains(&gh[..12]),
        "the evidence never holds the secret"
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn the_default_floor_blocks_secrets_and_an_operators_rule_file_is_data() {
    let rules_dir = tempfile::tempdir().unwrap();
    let file = rules_dir.path().join("site-secrets.ron");
    std::fs::write(
        &file,
        r#"(schema: "holger.secret-rules/v1", version: "site-1",
            rules: [(id: "acme-token", title: "an ACME token", severity: critical, pattern: "acme_[a-z0-9]{16}")])"#,
    )
    .unwrap();
    let r = start(&format!(
        r#"sources: [ (name: "team", kind: upload) ],
        steps: (unknown_license: Pass, secrets: (rules: "{}")),
        rules: (sandbox: off, require_signed: false, require_provenance: false),"#,
        file.display()
    ));
    let path = format!("team/{WHEEL}");
    assert_eq!(put_bytes(&r, "ida", &path, leaky_wheel().0).await.0, 201);
    let why = scan(&r, &path)
        .await
        .expect_err("the floor's default is `secrets: block`");
    assert!(
        why.contains("secrets: block") && why.contains("(floor)") && why.contains("an ACME token"),
        "{why}"
    );
    assert!(
        !why.contains("GitHub"),
        "the site's file replaces the built-in rules: {why}"
    );
}

#[test]
fn a_source_cannot_loosen_the_floor() {
    let e = refused(
        r#"sources: [ (name: "pypi", kind: upload, gates: (provenance: off)) ],
        steps: (),
        rules: (sandbox: off, require_signed: true),"#,
    );
    assert!(
        e.contains("source 'pypi'") && e.contains("loosens") && e.contains("require_signed: true"),
        "{e}"
    );
    let e = refused(r#"sources: [ (name: "s", kind: upload, gates: (malware: off)) ], steps: (),"#);
    assert!(
        e.contains("malware: on"),
        "the default floor holds too: {e}"
    );
}

#[test]
fn the_tier1_blocks_refuse_what_they_cannot_mean_by_name() {
    for (tier1, want) in [
        (
            r#"sources: [ (name: "a", kind: upload, gatez: ()) ], steps: (),"#,
            "gatez",
        ),
        (r#"steps: (aprovals: 2),"#, "aprovals"),
        (
            r#"steps: (), rules: (sandbox: off, requre_signed: true),"#,
            "requre_signed",
        ),
        (r#"steps: (gates: (cev: (max: low))),"#, "cev"),
        // A remote kind pulls into the steps, which only a `jobs` build carries: without it
        // the source is refused by name for the build it needs, before its fields are read.
        (
            r#"sources: [ (name: "p", kind: npm) ], steps: (),"#,
            if cfg!(feature = "jobs") {
                "needs `url`"
            } else {
                "needs the `online` and `jobs` build"
            },
        ),
        (
            r#"sources: [ (name: "c", kind: crates, url: "https://index.crates.io/", serves: ["cargo"]) ], steps: (),"#,
            "keep one",
        ),
        (r#"rules: (sandbox: off),"#, "no `steps:`"),
        (
            r#"steps: (), promotion: Some((ladder_name: "x", stages: [])),"#,
            "both `steps:` and `promotion:`",
        ),
        (
            r#"sources: [ (name: "up", kind: holger, url: "http://h:50051") ], steps: (),"#,
            "allow_unchecked_sources",
        ),
    ] {
        let e = refused(tier1);
        assert!(e.contains(want), "{tier1}\n  wants {want:?}\n  got {e}");
    }
}

#[test]
fn the_steps_lower_onto_the_intake_chain() {
    let h = load("steps: (approvals: 2),").expect("loads");
    let p = h.promotion.as_ref().expect("steps became promotion");
    let i = p.intake.as_ref().expect("with its intake block");
    assert_eq!(
        i.repos(),
        ["incoming", "quarantine", "approved", "production"]
    );
    assert_eq!(i.approvals, 2);
    let stages: Vec<&str> = p.stages.iter().map(|s| s.repo.as_str()).collect();
    assert_eq!(stages, ["incoming", "quarantine", "approved", "production"]);
}

/// **`python` and `oci` sources pull INTO the steps** (the pull-through lane's remote
/// kinds): they lower onto `pull_through`, keep their gates and auto-approval, and the
/// default floor's `allow_unchecked_sources: false` accepts them.
#[cfg(feature = "jobs")]
#[test]
fn python_and_oci_sources_lower_onto_the_pull_through() {
    let h = load(
        r#"sources: [
            (name: "pypi", kind: python, url: "https://pypi.org/simple/", gates: (cve: (max: medium)),
             auto_approve: when(provenance: require, cve_max: low)),
            (name: "cgr", kind: oci, url: "https://cgr.dev", platforms: ["linux/amd64"],
             gates: (provenance: require(identity: (issuer: "https://token.actions.githubusercontent.com", subject: "repo:chainguard-images/images"), slsa_min: 2))),
        ],
        steps: (),
        rules: (sandbox: off, allow_auto_approve: true),"#,
    )
    .expect("the remote kinds load");
    let pt = h
        .pull_through
        .as_ref()
        .expect("lowered onto the pull-through");
    let names: Vec<&str> = pt.sources.iter().map(|s| s.name.as_str()).collect();
    assert_eq!(names, ["pypi", "cgr"]);
    assert!(
        pt.rules.allow_auto_approve && pt.rules.require_signed,
        "the floor came along"
    );
    let pypi = &pt.sources[0];
    assert_eq!(
        pypi.gates.cve.map(|c| c.max),
        Some(holger_server_lib::scanner::FindingSeverity::Medium)
    );
    assert!(
        format!("{:?}", pypi.auto_approve).contains("Low"),
        "{:?}",
        pypi.auto_approve
    );
    assert_eq!(pt.sources[1].platforms, ["linux/amd64"]);
    let pol = h.source_policies.as_ref().unwrap();
    assert_eq!(
        pol.resolve("pull-through:pypi", "x").unwrap().source,
        "pypi"
    );
    assert_eq!(pol.remote("cgr").unwrap().slsa_min.value, 2);
    // The floor forbids auto-approval unless it says otherwise.
    let e = refused(
        r#"sources: [ (name: "pypi", kind: python, url: "https://pypi.org/simple/", auto_approve: when(cve_max: low)) ], steps: (),"#,
    );
    assert!(e.contains("allow_auto_approve"), "{e}");
}

/// ★ A29 row 11 (owner): **a container image is NEVER auto-approved.** An `oci` source in
/// `sources:` that asks `auto_approve: when(...)` beside a permissive floor is refused by
/// name when the config is READ, so no view of the loaded config (the effective sources,
/// the security page) ever shows an image source that approves itself.
#[cfg(feature = "jobs")]
#[test]
fn an_oci_source_asking_auto_approval_is_refused_at_load() {
    let e = refused(
        r#"sources: [
            (name: "cgr", kind: oci, url: "https://cgr.dev", platforms: ["linux/amd64"],
             auto_approve: when(provenance: require, cve_max: low)),
        ],
        steps: (),
        rules: (sandbox: off, allow_auto_approve: true),"#,
    );
    assert!(
        e.contains("never auto-approved") && e.contains("cgr"),
        "the refusal names the rule and the source: {e}"
    );
}

/// **`npm`, `maven` and `crates` (with `url`) pull INTO the steps too** (lane
/// HOLGER-PT2): they lower onto `pull_through` with the floor's keys and builders,
/// and npm is no longer refused by name.
#[cfg(all(feature = "jobs", feature = "online"))]
#[test]
fn npm_maven_and_crates_sources_lower_onto_the_pull_through() {
    let h = load(
        r#"sources: [
            (name: "npmjs", kind: npm, url: "https://registry.npmjs.org/"),
            (name: "cg-js", kind: npm, url: "https://libraries.cgr.dev/javascript/",
             credential: (username: "id", password_file: "/dev/null")),
            (name: "central", kind: maven, url: "https://repo1.maven.org/maven2/"),
            (name: "crates-io", kind: crates, url: "https://index.crates.io/"),
        ],
        steps: (),
        rules: (sandbox: off, keys: [(id: "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U", alg: ecdsa_p256,
                public_key: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g==")]),"#,
    )
    .expect("the remote kinds load");
    let pt = h
        .pull_through
        .as_ref()
        .expect("lowered onto the pull-through");
    let kinds: Vec<&str> = pt.sources.iter().map(|s| s.kind.name()).collect();
    assert_eq!(kinds, ["npm", "npm", "maven", "crates"]);
    assert_eq!(pt.rules.keys.len(), 1, "the floor's keys came along");
    // A `crates` source with `url` is not the old rust-remote: no repository was made for it.
    assert!(!h.repositories.iter().any(|r| r.ron_name == "crates-io"));
}

/// **`apk` with `url` pulls INTO the steps** (lane T8): it lowers onto the
/// pull-through as kind `apk`, its `apk_keys` gate carried; `apk` without `url` is
/// still the upload source whose index waits in the inbox.
#[cfg(all(feature = "jobs", feature = "online"))]
#[test]
fn an_apk_source_with_a_url_lowers_onto_the_pull_through() {
    let h = load(
        r#"sources: [
            (name: "wolfi", kind: apk, url: "https://packages.wolfi.dev/os",
             gates: (provenance: require(apk_keys: ["wolfi-signing.rsa.pub"]))),
            (name: "pushed", kind: apk),
        ],
        steps: (),
        rules: (sandbox: off),"#,
    )
    .expect("an apk pull-through loads");
    let pt = h
        .pull_through
        .as_ref()
        .expect("lowered onto the pull-through");
    assert_eq!(pt.sources.len(), 1, "only the source with a url pulls");
    assert_eq!(
        (pt.sources[0].name.as_str(), pt.sources[0].kind.name()),
        ("wolfi", "apk")
    );
    let g = format!("{:?}", pt.sources[0].gates.provenance);
    assert!(g.contains("wolfi-signing.rsa.pub"), "{g}");
    let e = refused(
        r#"sources: [ (name: "w", kind: apk, url: "https://packages.wolfi.dev/os", importers: ["ida"]) ], steps: (),"#,
    );
    assert!(
        e.contains("importers"),
        "a pulled source has no importers: {e}"
    );
}