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/an_image_carries_holgers_signed_cyclonedx_sbom.rs

//! **An image gets Holger's own CycloneDX SBOM** (lane HOL-OCI-EVIDENCE item 1, owner
//! 2026-10-31). HOL-REFERRERS served Holger's evidence as OCI referrers, but its SBOM slot
//! was the wheel scanner: an image carried a scan verdict and no bill of materials.
//!
//! An image is pulled through the intake chain from a fake upstream (built in the test: a
//! Wolfi apk database, a Python package, an npm package and a Java archive in two layers),
//! approved, and AVAILABLE. Then, through Holger's registry door:
//!
//! * the referrers API lists Holger's CycloneDX statement about the image's manifest digest
//!   (a Sigstore bundle, signed with the key `/-/evidence-key` serves);
//! * the statement's predicate is a CycloneDX 1.5 document whose components are the image's
//!   packages by purl, read from its layers in the scan worker (`holger-image-sbom`);
//! * the cosign `.att` tag of the image carries the same envelope, so an older cosign reads it;
//! * the evidence record that made it names the scanner.
#![cfg(all(feature = "supply-chain", feature = "jobs", feature = "sign"))]

mod oci_evidence;

use holger_server_lib::promote::intake::ArtifactState;
use oci_evidence::*;

#[test]
fn a_pulled_image_carries_holgers_signed_cyclonedx_sbom_as_a_referrer() {
    let up = Upstream::start();
    let img = demo_image("pulled");
    up.put_image("demo/app", "1", &img);
    let r = start(
        &Boot {
            pull_through: Some(
                r#"sources: [ (name: "up", kind: oci, url: "{up}") ], retry_after_secs: Some(1)"#
                    .into(),
            ),
            ..Default::default()
        },
        Some(&up),
    );
    let path = "/v2/production/up/demo/app/manifests/1";
    let (s, _, _) = request(r.http, "GET", path, &[("Accept", MANIFEST)], b"");
    assert_eq!(s, 503, "first: not yet, Holger is pulling it");
    let chain = r.chain();
    let id = layout_in(&chain, "up/demo/app/", ArtifactState::PolicyChecked);
    approve(&chain, &id, &["bob"]);
    let (s, h, body) = wait_for("AVAILABLE", || {
        let got = request(r.http, "GET", path, &[("Accept", MANIFEST)], b"");
        (got.0 == 200).then_some(got)
    });
    assert_eq!(s, 200);
    assert_eq!(body, img.manifest);
    assert_eq!(
        header(&h, "docker-content-digest").as_deref(),
        Some(img.digest.as_str())
    );

    let evidence = holger_evidence(&r, "production/up/demo/app", &img.digest);
    let kinds: Vec<&str> = evidence.iter().map(|(t, _)| t.as_str()).collect();
    assert!(kinds.contains(&SCAN_EVIDENCE), "{kinds:?}");
    let (_, st) = evidence
        .iter()
        .find(|(t, _)| t == CYCLONEDX)
        .unwrap_or_else(|| panic!("Holger's CycloneDX SBOM is a referrer of the image: {kinds:?}"));
    // The statement is about the image as a client pulls it.
    let subjects: Vec<String> = st["subject"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|s| {
            s["digest"]["sha256"]
                .as_str()
                .map(|h| format!("sha256:{h}"))
        })
        .collect();
    assert!(subjects.contains(&img.digest), "{subjects:?}");
    let doc = &st["predicate"];
    assert_eq!(doc["bomFormat"], "CycloneDX", "{doc}");
    assert_eq!(doc["metadata"]["component"]["type"], "container", "{doc}");
    assert_eq!(
        doc["metadata"]["component"]["bom-ref"],
        img.digest.as_str(),
        "{doc}"
    );
    let purls: Vec<&str> = doc["components"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|c| c["purl"].as_str())
        .collect();
    for want in [
        "pkg:apk/wolfi/openssl@3.0.13-r2?distro=wolfi",
        "pkg:apk/wolfi/busybox@1.36.1-r5?distro=wolfi",
        "pkg:pypi/idna@3.7",
        "pkg:npm/lodash@4.17.20",
        "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
    ] {
        assert!(purls.contains(&want), "{want} not in {purls:?}");
    }

    // The `.att` tag (cosign's older scheme) carries the same envelope.
    let tag = format!("sha256-{}.att", img.digest.trim_start_matches("sha256:"));
    let (s, _, att) = request(
        r.http,
        "GET",
        &format!("/v2/production/up/demo/app/manifests/{tag}"),
        &[("Accept", MANIFEST)],
        b"",
    );
    assert_eq!(s, 200, "{}", String::from_utf8_lossy(&att));
    let att: serde_json::Value = serde_json::from_slice(&att).unwrap();
    assert!(
        att["layers"]
            .as_array()
            .unwrap()
            .iter()
            .any(|l| l["annotations"]["predicateType"] == CYCLONEDX),
        "the .att manifest names the CycloneDX layer: {att}"
    );

    // Made by the image SBOM scanner, in the worker's slot.
    let (s, _, ev) = request(
        r.http,
        "GET",
        &format!(
            "/-/intake/evidence?sha256={}",
            chain.status(&id).unwrap().sha256
        ),
        &[("Authorization", &bearer("bob"))],
        b"",
    );
    assert_eq!(s, 200, "{}", String::from_utf8_lossy(&ev));
    assert!(
        String::from_utf8_lossy(&ev).contains("holger-image-sbom"),
        "the evidence names the scanner"
    );
}