server/lib/tests/pushed_images_go_through_the_intake_chain.rs
//! **A push to the `oci` registry goes through the intake chain** (lane HOL-OCI-EVIDENCE
//! item 2, owner 2026-10-31). HOL-REFERRERS left it open: an image pushed to the plain `oci`
//! registry was served the moment its manifest landed, unscanned, with no Holger evidence.
//!
//! On a Holger with an intake chain every `oci` repository is a SCANNED REGISTRY unless the
//! RON opts it out by name (`promotion.intake.oci_push: [(repository: …, scan: off)]`):
//!
//! * a push by tag takes the image into the chain as the pusher: SCANNED (the image SBOM,
//! the secrets scan of every layer, the CVE match), POLICY_CHECKED, then the approvals
//! (never the pusher's); nothing of it is served before it is AVAILABLE, and then its
//! manifest, blobs and tags are, with Holger's evidence as referrers;
//! * a push the chain refuses fails the push itself (`403 DENIED` with the reason) and is
//! never served;
//! * a write needs the Importer duty; a DELETE is refused (`holger-oci-3`);
//! * a signature pushed before the tag rides in the image's layout; an artifact attached
//! after the image is AVAILABLE is served beside it;
//! * an opt-out serves as pushed, and is refused at boot where the policy requires a scan
//! (`holger-oci-1`).
#![cfg(all(feature = "supply-chain", feature = "jobs", feature = "sign"))]
mod oci_evidence;
use holger_server_lib::promote::intake::{ArtifactState, TransitionRequest};
use oci_evidence::*;
fn chained() -> Running {
start(
&Boot {
oci_repo: true,
approvals: 2,
yara: true,
..Default::default()
},
None,
)
}
fn get(r: &Running, path: &str) -> (u16, String, Vec<u8>) {
request(r.http, "GET", path, &[("Accept", MANIFEST)], b"")
}
fn text(b: &[u8]) -> String {
String::from_utf8_lossy(b).to_string()
}
#[test]
fn a_pushed_image_is_scanned_approved_and_only_then_served() {
let r = chained();
let img = demo_image("pushed");
let (s, h, b) = push(&r, "ida", "demo/app", "1", &img);
assert_eq!(s, 201, "the push is taken: {}", text(&b));
assert_eq!(
header(&h, "x-holger-intake-state").as_deref(),
Some("policy_checked"),
"{h}"
);
// Nothing of it is served before it is AVAILABLE.
let (s, _, b) = get(&r, "/v2/oci/demo/app/manifests/1");
assert_eq!(s, 404, "{}", text(&b));
assert!(
text(&b).contains("not yet AVAILABLE") && text(&b).contains("POLICY_CHECKED"),
"{}",
text(&b)
);
assert_eq!(
get(&r, &format!("/v2/oci/demo/app/manifests/{}", img.digest)).0,
404
);
assert_eq!(
get(
&r,
&format!("/v2/oci/demo/app/blobs/{}", digest(&img.layers[0]))
)
.0,
404,
"a staged blob is never served"
);
let (_, _, tags) = get(&r, "/v2/oci/demo/app/tags/list");
assert!(!text(&tags).contains("\"1\""), "{}", text(&tags));
// In the chain as the pusher, under the registry's name; the pusher never approves it.
let chain = r.chain();
let id = layout_in(&chain, "oci/demo/app/", ArtifactState::PolicyChecked);
assert_eq!(
id.name,
format!("oci/demo/app/{}.oci.tar", img.digest.replace(':', "-"))
);
let st = chain.status(&id).unwrap();
let first = chain.history(&id).into_iter().next().unwrap();
assert_eq!(first.actor, "ida", "INGESTED as the pusher");
assert_eq!(first.origin, "oci", "its origin is the registry");
assert!(
chain
.advance(
TransitionRequest::new(id.clone(), ArtifactState::Approved, "ida")
.with_reason("mine")
)
.is_err(),
"the importer never approves"
);
let _ = st;
approve(&chain, &id, &["bob", "carol"]);
// Now it is served (the first read seals it, as the pull-through does): by tag, by
// digest, its blobs and its tag.
let (s, h, b) = get(&r, "/v2/oci/demo/app/manifests/1");
assert_eq!(s, 200, "{}", text(&b));
assert_eq!(chain.state(&id), ArtifactState::Available);
assert_eq!(b, img.manifest);
assert_eq!(
header(&h, "docker-content-digest").as_deref(),
Some(img.digest.as_str())
);
assert_eq!(
header(&h, "x-holger-intake-state").as_deref(),
Some("available")
);
assert_eq!(
get(&r, &format!("/v2/oci/demo/app/manifests/{}", img.digest)).2,
img.manifest
);
for blob in img.blobs() {
let (s, _, got) = get(&r, &format!("/v2/oci/demo/app/blobs/{}", digest(&blob)));
assert_eq!(s, 200);
assert_eq!(got, blob);
}
let (_, _, tags) = get(&r, "/v2/oci/demo/app/tags/list");
assert!(text(&tags).contains("\"1\""), "{}", text(&tags));
// And Holger's evidence rides beside it: the scan verdict and the image SBOM.
let ev = holger_evidence(&r, "oci/demo/app", &img.digest);
let kinds: Vec<&str> = ev.iter().map(|(t, _)| t.as_str()).collect();
assert!(
kinds.contains(&SCAN_EVIDENCE) && kinds.contains(&CYCLONEDX),
"{kinds:?}"
);
let (_, sbom) = ev.iter().find(|(t, _)| t == CYCLONEDX).unwrap();
let purls: Vec<&str> = sbom["predicate"]["components"]
.as_array()
.unwrap()
.iter()
.filter_map(|c| c["purl"].as_str())
.collect();
assert!(
purls.contains(&"pkg:apk/wolfi/openssl@3.0.13-r2?distro=wolfi"),
"{purls:?}"
);
let att = format!("sha256-{}.att", img.digest.trim_start_matches("sha256:"));
assert_eq!(
get(&r, &format!("/v2/oci/demo/app/manifests/{att}")).0,
200,
"the .att tag carries Holger's envelopes"
);
}
/// **A client that pushes the manifest by digest and then tags it gets its push through**
/// (lane HOL-FIELD-2): `helm push` 3.18+ (oras-go v2) PUTs the manifest by digest, reads it
/// back by digest and PUTs it again under the tag. The scanned registry answered the read
/// with 404 (staged, not AVAILABLE), so helm 3.19 failed with `failed to perform "Tag" on
/// destination: sha256:…: not found` and no chart could be pushed. The pusher (an Importer
/// with write on the registry) reads its own staged manifest back by digest; nobody else
/// does, and no staged blob is ever served.
#[test]
fn a_digest_push_then_tag_goes_through_and_only_a_pusher_reads_the_staged_manifest() {
let r = chained();
let img = demo_image("digest-first");
push_blobs(&r, "ida", "demo/chart", &img.blobs());
let (s, _, b) = push_manifest(&r, "ida", "demo/chart", &img.digest, &img.manifest);
assert_eq!(s, 201, "the digest push is staged: {}", text(&b));
let path = format!("/v2/oci/demo/chart/manifests/{}", img.digest);
let ida = bearer("ida");
let (s, h, b) = request(
r.http,
"GET",
&path,
&[("Accept", MANIFEST), ("Authorization", &ida)],
b"",
);
assert_eq!(
s,
200,
"the pusher reads its staged manifest back by digest: {}",
text(&b)
);
assert_eq!(b, img.manifest);
assert_eq!(
header(&h, "docker-content-digest").as_deref(),
Some(img.digest.as_str())
);
let (s, _, _) = request(
r.http,
"HEAD",
&path,
&[("Accept", MANIFEST), ("Authorization", &ida)],
b"",
);
assert_eq!(s, 200, "and HEADs it");
// Nobody else reads it, and its blobs stay unserved.
assert_eq!(
get(&r, &path).0,
404,
"an anonymous read of a staged manifest"
);
let bob = bearer("bob");
let (s, _, _) = request(
r.http,
"GET",
&path,
&[("Accept", MANIFEST), ("Authorization", &bob)],
b"",
);
assert_eq!(s, 404, "an approver without the Importer duty");
let blob = format!("/v2/oci/demo/chart/blobs/{}", digest(&img.layers[0]));
let (s, _, _) = request(r.http, "GET", &blob, &[("Authorization", &ida)], b"");
assert_eq!(
s, 404,
"a staged blob is never served, not even to its pusher"
);
// The tag push takes the image into the chain, as `docker push` does.
let (s, h, b) = push_manifest(&r, "ida", "demo/chart", "0.1.0", &img.manifest);
assert_eq!(s, 201, "the tag is taken: {}", text(&b));
assert_eq!(
header(&h, "x-holger-intake-state").as_deref(),
Some("policy_checked"),
"{h}"
);
}
#[test]
fn a_refused_push_fails_the_push_itself_and_is_never_served() {
let r = chained();
// Two halves, so this source file is not itself the test string.
let eicar = format!(
"{}{}",
"X5O!P%@AP[4\\PZX54(P^)7CC)7}$", "EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
)
.into_bytes();
let img = Img::new(&[vec![
("etc/os-release", b"ID=wolfi\n"),
("usr/bin/dropper", &eicar),
]]);
let (s, _, b) = push(&r, "ida", "leaky/app", "1", &img);
assert_eq!(
s,
403,
"docker push fails with Holger's refusal: {}",
text(&b)
);
let v: serde_json::Value = serde_json::from_slice(&b).unwrap();
assert_eq!(v["errors"][0]["code"], "DENIED", "{v}");
assert!(
v["errors"][0]["message"]
.as_str()
.unwrap()
.contains("EICAR"),
"the reason is named: {v}"
);
let chain = r.chain();
// Refused at the door: nothing of it was INGESTED, and it is never served.
assert!(
chain
.all()
.iter()
.all(|(id, _)| !id.name.starts_with("oci/leaky/")),
"refused before a byte entered the chain"
);
let (s, _, b) = get(&r, "/v2/oci/leaky/app/manifests/1");
assert_eq!(s, 404, "a refused image is never served: {}", text(&b));
assert_eq!(
get(
&r,
&format!("/v2/oci/leaky/app/blobs/{}", digest(&img.layers[0]))
)
.0,
404
);
}
#[test]
fn a_push_needs_the_importer_duty_and_a_delete_is_refused() {
// ida may delete by her role; the scanned registry refuses it all the same.
let r = start(
&Boot {
oci_repo: true,
approvals: 2,
auth_extra: r#"roles: {"ida": Admin, "adm": Writer},"#.into(),
..Default::default()
},
None,
);
// A repository admin configures; it does not bring artifacts in.
let (s, _, b) = request(
r.http,
"POST",
"/v2/oci/demo/app/blobs/uploads/",
&[("Authorization", &bearer("adm"))],
b"",
);
assert_eq!(s, 403, "{}", text(&b));
assert!(
text(&b).to_ascii_lowercase().contains("importer"),
"the duty is named: {}",
text(&b)
);
let img = demo_image("del");
assert_eq!(push(&r, "ida", "demo/del", "1", &img).0, 201);
let (s, _, b) = request(
r.http,
"DELETE",
&format!("/v2/oci/demo/del/manifests/{}", img.digest),
&[("Authorization", &bearer("ida"))],
b"",
);
assert_eq!(s, 403, "{}", text(&b));
assert!(text(&b).contains("[holger-oci-3]"), "{}", text(&b));
assert!(
r.chain()
.all()
.iter()
.any(|(id, _)| id.name.starts_with("oci/demo/del/")),
"still in the chain"
);
}
/// A cosign-style `.sig` manifest for `subject` (one layer, the simple-signing payload).
fn sig_for(subject: &str) -> (Vec<u8>, Vec<Vec<u8>>) {
let payload = serde_json::to_vec(&serde_json::json!({
"critical": {"identity": {"docker-reference": "demo"}, "image": {"docker-manifest-digest": subject}, "type": "cosign container image signature"},
"optional": null,
}))
.unwrap();
let config =
br#"{"architecture":"","os":"","config":{},"rootfs":{"type":"layers","diff_ids":[]}}"#
.to_vec();
let manifest = serde_json::to_vec(&serde_json::json!({
"schemaVersion": 2, "mediaType": MANIFEST,
"config": {"mediaType": CONFIG, "digest": digest(&config), "size": config.len()},
"layers": [{"mediaType": "application/vnd.dev.cosign.simplesigning.v1+json", "digest": digest(&payload), "size": payload.len(),
"annotations": {"dev.cosignproject.cosign/signature": "MEUCIQ=="}}],
}))
.unwrap();
(manifest, vec![config, payload])
}
/// An SBOM attached the OCI 1.1 way (a manifest with a `subject`).
fn referrer_for(subject: &str, subject_len: usize) -> (Vec<u8>, Vec<Vec<u8>>) {
let layer = br#"{"spdxVersion":"SPDX-2.3","name":"later","packages":[]}"#.to_vec();
let empty = b"{}".to_vec();
let manifest = serde_json::to_vec(&serde_json::json!({
"schemaVersion": 2, "mediaType": MANIFEST, "artifactType": "application/spdx+json",
"config": {"mediaType": "application/vnd.oci.empty.v1+json", "digest": digest(&empty), "size": 2},
"layers": [{"mediaType": "application/spdx+json", "digest": digest(&layer), "size": layer.len()}],
"subject": {"mediaType": MANIFEST, "digest": subject, "size": subject_len},
}))
.unwrap();
(manifest, vec![empty, layer])
}
#[test]
fn a_signature_pushed_before_the_tag_rides_in_the_layout_and_an_attachment_after_is_served_once_available(
) {
let r = chained();
let img = demo_image("signed");
// The image by digest first (staged), its `.sig` tag, then the tag that takes it in.
push_blobs(&r, "ida", "demo/signed", &img.blobs());
let (s, h, _) = push_manifest(&r, "ida", "demo/signed", &img.digest, &img.manifest);
assert_eq!(s, 201);
assert_eq!(
header(&h, "x-holger-intake-state"),
None,
"a manifest by digest is only staged"
);
let sig_tag = format!("sha256-{}.sig", img.digest.trim_start_matches("sha256:"));
let (sig, sig_blobs) = sig_for(&img.digest);
push_blobs(&r, "ida", "demo/signed", &sig_blobs);
assert_eq!(
push_manifest(&r, "ida", "demo/signed", &sig_tag, &sig).0,
201
);
assert_eq!(
get(&r, &format!("/v2/oci/demo/signed/manifests/{sig_tag}")).0,
404,
"not served before its image is AVAILABLE"
);
let (s, h, _) = push_manifest(&r, "ida", "demo/signed", "v1", &img.manifest);
assert_eq!(s, 201);
assert_eq!(
header(&h, "x-holger-intake-state").as_deref(),
Some("policy_checked")
);
let chain = r.chain();
let id = layout_in(&chain, "oci/demo/signed/", ArtifactState::PolicyChecked);
// The signature rode in: the layout holds it.
let layout =
holger_server_lib::pull_intake::oci::Layout::read(&chain.incoming_artifact(&id).unwrap())
.unwrap();
assert_eq!(
layout.attached_tags.get(&sig_tag).map(String::as_str),
Some(digest(&sig).as_str()),
"the .sig rides in the layout"
);
approve(&chain, &id, &["bob", "carol"]);
let (s, _, b) = get(&r, &format!("/v2/oci/demo/signed/manifests/{sig_tag}"));
assert_eq!(s, 200, "{}", text(&b));
assert_eq!(b, sig);
// An SBOM attached after it is AVAILABLE is served beside it.
let (refm, ref_blobs) = referrer_for(&img.digest, img.manifest.len());
push_blobs(&r, "ida", "demo/signed", &ref_blobs);
assert_eq!(
push_manifest(&r, "ida", "demo/signed", &digest(&refm), &refm).0,
201
);
let (s, _, idx) = get(&r, &format!("/v2/oci/demo/signed/referrers/{}", img.digest));
assert_eq!(s, 200);
assert!(
text(&idx).contains(&digest(&refm)),
"the later attachment is a referrer: {}",
text(&idx)
);
assert_eq!(
get(
&r,
&format!("/v2/oci/demo/signed/manifests/{}", digest(&refm))
)
.2,
refm
);
assert_eq!(
get(
&r,
&format!("/v2/oci/demo/signed/blobs/{}", digest(&ref_blobs[1]))
)
.2,
ref_blobs[1]
);
// An attachment to an image that is not AVAILABLE is not.
let other = demo_image("pending");
assert_eq!(push(&r, "ida", "demo/pending", "1", &other).0, 201);
let (refm, ref_blobs) = referrer_for(&other.digest, other.manifest.len());
push_blobs(&r, "ida", "demo/pending", &ref_blobs);
assert_eq!(
push_manifest(&r, "ida", "demo/pending", &digest(&refm), &refm).0,
201
);
let (_, _, idx) = get(
&r,
&format!("/v2/oci/demo/pending/referrers/{}", other.digest),
);
assert!(!text(&idx).contains(&digest(&refm)), "{}", text(&idx));
assert_eq!(
get(
&r,
&format!("/v2/oci/demo/pending/manifests/{}", digest(&refm))
)
.0,
404
);
}
#[test]
fn an_opt_out_serves_pushes_unscanned_and_is_refused_where_the_policy_requires_a_scan() {
let r = start(
&Boot {
oci_repo: true,
intake_extra: r#"oci_push: [(repository: "oci", scan: off)],"#.into(),
..Default::default()
},
None,
);
let img = demo_image("scratch");
let (s, h, _) = push(&r, "adm", "demo/scratch", "1", &img);
assert_eq!(s, 201);
assert_eq!(header(&h, "x-holger-intake-state"), None, "{h}");
assert_eq!(
get(&r, "/v2/oci/demo/scratch/manifests/1").2,
img.manifest,
"served as pushed"
);
assert!(
r.chain()
.all()
.iter()
.all(|(id, _)| !id.name.starts_with("oci/")),
"nothing went into the chain"
);
let boot = Boot {
oci_repo: true,
scan: Some("Some((required: [sbom]))".into()),
intake_extra: r#"oci_push: [(repository: "oci", scan: off)],"#.into(),
..Default::default()
};
let e = try_start(
traits::test_support::hold_port(),
traits::test_support::hold_port(),
&boot,
None,
)
.err()
.expect("refused at boot");
assert!(
e.contains("[holger-oci-1]") && e.contains("required"),
"{e}"
);
let boot = Boot {
oci_repo: true,
intake_extra: r#"oci_push: [(repository: "nope", scan: off)],"#.into(),
..Default::default()
};
let e = try_start(
traits::test_support::hold_port(),
traits::test_support::hold_port(),
&boot,
None,
)
.err()
.expect("refused at boot");
assert!(e.contains("[holger-oci-2]"), "{e}");
}
/// **A path that is not canonical never reaches past the scanned registry** (SEC-REVIEW
/// 2026-11-01 P1). The router dropped empty segments and the scanned view did not: `/v2//oci/…`
/// keyed the `oci` repository, the view's `strip_prefix("/v2/oci/")` missed, and the request
/// fell through to the raw backend. A manifest PUT so stored a tag without the intake chain,
/// and a GET served staged (or BLOCKED) bytes. Every path with an empty, `.` or `..` segment
/// is refused with 400 before any door sees it.
#[test]
fn a_double_slash_or_dot_segment_never_reaches_past_the_scanned_registry() {
let r = chained();
let img = demo_image("double-slash");
push_blobs(&r, "ida", "demo/ds", &img.blobs());
let ida = bearer("ida");
// The write: a tag stored around the chain.
let (s, _, b) = request(
r.http,
"PUT",
"/v2//oci/demo/ds/manifests/evil",
&[("Authorization", &ida), ("Content-Type", MANIFEST)],
&img.manifest,
);
assert_eq!(
s,
400,
"a double-slash manifest PUT is refused: {}",
text(&b)
);
assert!(text(&b).contains("holger-server-14"), "{}", text(&b));
// The image goes in by the front door and is staged, not AVAILABLE.
let (s, _, b) = push_manifest(&r, "ida", "demo/ds", "1", &img.manifest);
assert_eq!(s, 201, "{}", text(&b));
let blob = digest(&img.layers[0]);
for path in [
"/v2//oci/demo/ds/manifests/1".to_string(),
"/v2//oci/demo/ds/manifests/evil".to_string(),
format!("/v2//oci/demo/ds/blobs/{blob}"),
format!("/v2//oci/demo/ds/manifests/{}", img.digest),
"/v2/./oci/demo/ds/manifests/1".to_string(),
"/v2/oci/demo/../demo/ds/manifests/1".to_string(),
format!("/v2/oci//demo/ds/blobs/{blob}"),
"/v2/%2e/oci/demo/ds/manifests/1".to_string(),
] {
let (s, _, b) = get(&r, &path);
assert_eq!(s, 400, "{path} is refused, never served: {}", text(&b));
}
// The canonical path still holds it back, and the base endpoint still answers.
assert_eq!(get(&r, "/v2/oci/demo/ds/manifests/1").0, 404);
assert_eq!(get(&r, "/v2/oci/demo/ds/manifests/evil").0, 404);
assert_ne!(get(&r, "/v2/").0, 400, "a trailing slash is canonical");
}
/// **A runnable image that names a `subject` is no attachment** (SEC-REVIEW 2026-11-01 P7).
/// Any manifest with `subject` was an "attachment": staged unscanned and served beside its
/// AVAILABLE subject, so an Importer pushed a whole image with `subject = <a clean digest>`
/// and `docker pull` got it unscanned. An attachment is only an inert artifact (no image
/// layer, bounded size); anything else walks the chain like every image.
#[test]
fn a_runnable_image_naming_a_subject_is_scanned_not_attached() {
let r = chained();
let clean = demo_image("clean-subject");
assert_eq!(push(&r, "ida", "demo/subj", "1", &clean).0, 201);
let chain = r.chain();
let id = layout_in(&chain, "oci/demo/subj/", ArtifactState::PolicyChecked);
approve(&chain, &id, &["bob", "carol"]);
assert_eq!(get(&r, "/v2/oci/demo/subj/manifests/1").0, 200);
let evil = demo_image("evil-subject");
let mut v: serde_json::Value = serde_json::from_slice(&evil.manifest).unwrap();
v["subject"] = serde_json::json!({
"mediaType": MANIFEST, "digest": clean.digest, "size": clean.manifest.len()
});
let m = serde_json::to_vec(&v).unwrap();
push_blobs(&r, "ida", "demo/subj", &evil.blobs());
assert_eq!(
push_manifest(&r, "ida", "demo/subj", &digest(&m), &m).0,
201
);
assert_ne!(
get(&r, &format!("/v2/oci/demo/subj/manifests/{}", digest(&m))).0,
200,
"a runnable image is never served as an attachment"
);
// The layers the clean image does not share (the first layer is the same bytes in both).
for layer in evil.layers.iter().filter(|l| !clean.layers.contains(l)) {
assert_ne!(
get(&r, &format!("/v2/oci/demo/subj/blobs/{}", digest(layer))).0,
200,
"its layers are never served unscanned"
);
}
let (s, h, b) = push_manifest(&r, "ida", "demo/subj", "evil", &m);
assert_eq!(s, 201, "{}", text(&b));
assert!(
header(&h, "x-holger-intake-state").is_some(),
"a tag takes it into the chain like any image: {h}"
);
assert_ne!(get(&r, "/v2/oci/demo/subj/manifests/evil").0, 200);
}