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

nordisk/holger

server/lib/src/promote/oci_push.rs

//! **Scanned pushes** (lane HOL-OCI-EVIDENCE item 2, owner 2026-10-31): an image pushed to a
//! writable `oci` registry goes through the intake chain like any other upload.
//!
//! Until now the plain `oci` registry served what was pushed the moment the manifest landed:
//! no scan, no policy, no approval, and no Holger evidence. On a Holger with an intake chain,
//! every `ron_repo_type: "oci"` repository is now a **scanned registry** unless the RON opts it
//! out by name:
//!
//! ```ron
//! promotion: Some((intake: Some((…, oci_push: [ (repository: "scratch", scan: off) ])))),
//! ```
//!
//! An opt-out is refused at boot, by name, where the policy requires a scan
//! (`security_profile: Critical`, or `promotion.intake.scan.required` naming any kind):
//! `holger-oci-1`. A rule that names no `oci` repository is `holger-oci-2`.
//!
//! What a scanned registry does:
//!
//! * **writes** need the Importer duty (as an upload into `incoming` does). Blobs and
//!   manifests land in the registry's own store, which now only STAGES them: nothing staged
//!   is served. A `DELETE` is refused (`holger-oci-3`): the chain holds what was pushed.
//! * **a manifest pushed by TAG** (`docker push`, `skopeo copy`, `helm push`) takes the image
//!   into the chain: Holger reads it back from the staging store whole (every platform
//!   manifest of an index, each config and layer, its cosign `.sig`/`.att`/`.sbom` tags and
//!   OCI 1.1 referrers already pushed), writes ONE image layout (`.oci.tar`, the same the
//!   pull-through keeps), files the signatures in the provenance inbox, and INGESTS it into
//!   `incoming` as the pusher, at `<registry>/<image>/sha256-<hex>.oci.tar`, origin
//!   `<registry>` (a provenance gate for `<registry>` applies). Then it walks SCANNED (the
//!   scan worker: inspection, malware, secrets, the image SBOM, the CVE match of the image's
//!   packages, provenance) and POLICY_CHECKED, and waits for its approvals (never
//!   automatic, and never the pusher's). A refusal answers the push itself: `403 DENIED`
//!   with Holger's reason, so `docker push` fails with it.
//! * a manifest pushed by DIGEST (an index's platform manifests, before the index) and an
//!   **attached artifact** (a `sha256-<hex>.sig|.att|.sbom` tag, a manifest with a `subject`)
//!   are staged; an attachment pushed before the image's tag rides in its layout (so its
//!   signature is verified on intake), one pushed after is served beside the image once
//!   the image is AVAILABLE.
//! * **reads** serve only what is AVAILABLE: a manifest from its sealed layout (by digest, or
//!   by the tag the staging store names, which must be AVAILABLE), its blobs, its tags, its
//!   attached artifacts and Holger's own evidence (the scan verdict, the image SBOM) as OCI
//!   referrers and in the `.att` tag ([`crate::referrers`]). Anything else is the registry's
//!   "not yet" (`404 MANIFEST_UNKNOWN` naming the state) or `403 DENIED` once refused.

use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Arc, Mutex};

use serde::{Deserialize, Serialize};
use serde_json::Value;
use traits::{ArtifactId, RepositoryBackendTrait};

use super::intake::{ArtifactState, IntakeChain, IntakeError, TransitionRequest};
use crate::lock::LockOrRecover as _;
use crate::pull_intake::http::{Fetch, Reply as HttpReply};
use crate::pull_intake::oci::{self, Layout, OciPath};

/// Whether a registry's pushes are scanned.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PushScan {
    /// Through the chain (the default for every `oci` repository of a chain Holger).
    #[default]
    Required,
    /// Served as pushed, unscanned (refused where the policy requires a scan).
    Off,
}

/// One `promotion.intake.oci_push` row.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OciPushRule {
    pub repository: String,
    #[serde(default)]
    pub scan: PushScan,
}

/// **The scanned registries of a chain**, by name. `oci_repos` are the config's
/// `ron_repo_type: "oci"` repositories, `scan_required` whether the policy requires a scan
/// (`Critical`, or `scan.required` naming a kind), `pull_sources` the pull-through's sources
/// (a scanned registry's layouts share their coordinates' first segment, so the names must
/// differ). A refusal is tagged.
pub fn scanned_registries(
    rules: &[OciPushRule],
    oci_repos: &[&str],
    scan_required: Option<&str>,
    pull_sources: &[&str],
) -> Result<BTreeSet<String>, String> {
    let mut out: BTreeSet<String> = oci_repos.iter().map(|s| s.to_string()).collect();
    let mut seen = BTreeSet::new();
    for r in rules {
        if !oci_repos.contains(&r.repository.as_str()) {
            return Err(holger_errcode::OCI_PUSH_RULE_UNKNOWN.tag(format!(
                "`promotion.intake.oci_push` names '{}', which is no `ron_repo_type: \"oci\"` repository of this Holger",
                r.repository
            )));
        }
        if !seen.insert(r.repository.as_str()) {
            return Err(holger_errcode::OCI_PUSH_RULE_UNKNOWN.tag(format!(
                "`promotion.intake.oci_push` names '{}' twice",
                r.repository
            )));
        }
        if r.scan == PushScan::Off {
            if let Some(why) = scan_required {
                return Err(holger_errcode::OCI_PUSH_SCAN_REQUIRED.tag(format!(
                    "`promotion.intake.oci_push: [(repository: \"{}\", scan: off)]` would serve pushed images unscanned, and this Holger's policy requires a scan ({why})",
                    r.repository
                )));
            }
            out.remove(&r.repository);
        }
    }
    for s in &out {
        if pull_sources.contains(&s.as_str()) {
            return Err(holger_errcode::OCI_PUSH_RULE_UNKNOWN.tag(format!(
                "the scanned registry '{s}' and a pull-through source share the name; their images would share coordinates"
            )));
        }
    }
    Ok(out)
}

/// The chain coordinate of a pushed image (the pull-through's shape, the registry as source).
pub fn push_id(repo: &str, image: &str, digest: &str) -> ArtifactId {
    ArtifactId {
        namespace: None,
        name: format!("{repo}/{image}/{}.oci.tar", digest.replace(':', "-")),
        version: String::new(),
    }
}

/// The largest image a push takes into the chain whole (every platform, every blob).
pub const MAX_IMAGE_BYTES: u64 = 8 << 30;
/// The layouts kept parsed in memory.
const LAYOUT_CACHE: usize = 32;

/// **A chain's scanned registries**, attached to the chain at boot.
pub struct OciPush {
    scanned: BTreeSet<String>,
    #[cfg(feature = "scanners")]
    inbox: Option<crate::scanner::provenance::ProvenanceInbox>,
    layouts: Mutex<Vec<(String, Arc<Layout>)>>,
}

impl std::fmt::Debug for OciPush {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("OciPush")
            .field("scanned", &self.scanned)
            .finish()
    }
}

impl OciPush {
    pub fn new(scanned: BTreeSet<String>) -> Self {
        Self {
            scanned,
            #[cfg(feature = "scanners")]
            inbox: None,
            layouts: Mutex::new(Vec::new()),
        }
    }

    /// File a pushed image's signatures in the chain's provenance inbox too.
    #[cfg(feature = "scanners")]
    pub fn with_provenance_inbox(
        mut self,
        inbox: crate::scanner::provenance::ProvenanceInbox,
    ) -> Self {
        self.inbox = Some(inbox);
        self
    }

    /// Does `repo` scan its pushes?
    pub fn scans(&self, repo: &str) -> bool {
        self.scanned.contains(repo)
    }

    /// The scanned registries.
    pub fn registries(&self) -> &BTreeSet<String> {
        &self.scanned
    }

    fn layout(&self, chain: &IntakeChain, id: &ArtifactId) -> Option<Arc<Layout>> {
        let sha = chain.status(id)?.sha256;
        if let Some((_, l)) = self
            .layouts
            .lock_or_recover()
            .iter()
            .find(|(s, _)| *s == sha)
        {
            return Some(Arc::clone(l));
        }
        let bytes = chain
            .serve(id)
            .ok()
            .flatten()
            .or_else(|| chain.incoming_artifact(id).ok())?;
        let l = Arc::new(
            Layout::read(&bytes)
                .map_err(|e| log::warn!("oci push: the layout of {}: {e}", id.name))
                .ok()?,
        );
        let mut c = self.layouts.lock_or_recover();
        c.push((sha, Arc::clone(&l)));
        if c.len() > LAYOUT_CACHE {
            c.remove(0);
        }
        Some(l)
    }
}

// ── the staging store, read back as a registry ──────────────────────────────

/// The registry's own store as a [`Fetch`]: `local:/v2/<path>` is answered by the backend's
/// registry door, so the pull-through's registry client reads a push back whole.
struct Staged<'a> {
    backend: &'a dyn RepositoryBackendTrait,
    repo: &'a str,
}

impl Fetch for Staged<'_> {
    fn get(
        &self,
        url: &str,
        _headers: &[(String, String)],
        _auth: Option<(&str, &str)>,
        cap: u64,
    ) -> anyhow::Result<HttpReply> {
        let path = url
            .strip_prefix("local:/v2/")
            .ok_or_else(|| anyhow::anyhow!("{url} is not a staged path"))?;
        let (status, headers, body) =
            self.backend
                .handle_http2_request("GET", &format!("/v2/{}/{path}", self.repo), &[])?;
        if body.len() as u64 > cap {
            anyhow::bail!("{url}: {} bytes, past the cap of {cap}", body.len());
        }
        Ok(HttpReply {
            status,
            headers: headers
                .into_iter()
                .map(|(k, v)| (k.to_ascii_lowercase(), v))
                .collect(),
            body,
            url: url.to_string(),
        })
    }
}

fn staged_manifest(
    backend: &dyn RepositoryBackendTrait,
    repo: &str,
    image: &str,
    reference: &str,
) -> Option<(String, Vec<u8>, String)> {
    let (status, headers, body) = backend
        .handle_http2_request(
            "GET",
            &format!("/v2/{repo}/{image}/manifests/{reference}"),
            &[],
        )
        .ok()?;
    if status != 200 {
        return None;
    }
    let h = |n: &str| {
        headers
            .iter()
            .find(|(k, _)| k.eq_ignore_ascii_case(n))
            .map(|(_, v)| v.clone())
    };
    let digest =
        h("Docker-Content-Digest").unwrap_or_else(|| crate::referrers::sha256_digest(&body));
    let mt = h("Content-Type")
        .or_else(|| oci::media_type_of(&body))
        .unwrap_or_else(|| crate::referrers::MANIFEST_MEDIA_TYPE.into());
    Some((mt, body, digest))
}

fn staged_blob(
    backend: &dyn RepositoryBackendTrait,
    repo: &str,
    image: &str,
    digest: &str,
) -> Option<Vec<u8>> {
    let (status, _, body) = backend
        .handle_http2_request("GET", &format!("/v2/{repo}/{image}/blobs/{digest}"), &[])
        .ok()?;
    (status == 200 && crate::referrers::sha256_digest(&body) == digest).then_some(body)
}

/// The staged referrers of `digest` (OCI 1.1, as the registry recorded them).
fn staged_referrers(
    backend: &dyn RepositoryBackendTrait,
    repo: &str,
    image: &str,
    digest: &str,
) -> Vec<Value> {
    let Ok((200, _, body)) = backend.handle_http2_request(
        "GET",
        &format!("/v2/{repo}/{image}/referrers/{digest}"),
        &[],
    ) else {
        return Vec::new();
    };
    serde_json::from_slice::<Value>(&body)
        .ok()
        .and_then(|v| v.get("manifests").and_then(Value::as_array).cloned())
        .unwrap_or_default()
}

// ── a push into the chain ───────────────────────────────────────────────────

/// What a manifest PUT did beside staging the manifest.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pushed {
    /// An attached artifact, or a manifest by digest: staged only.
    Staged(String),
    /// Taken into the chain: where it stands now.
    InChain {
        id: ArtifactId,
        state: ArtifactState,
        line: String,
    },
    /// The chain refused it: the reason, tagged.
    Refused { id: Option<ArtifactId>, why: String },
}

/// The most bytes an attachment's blobs may declare together (SEC-REVIEW 2026-11-01 P7): a
/// signature, an attestation or an SBOM, never an image.
pub const ATTACHMENT_MAX_BYTES: u64 = 16 * 1024 * 1024;

/// **Is this manifest an inert artifact** (SEC-REVIEW 2026-11-01 P7): one manifest (no index),
/// no layer of an image's layer types (`application/vnd.oci.image.layer.*`, Docker's
/// `rootfs.diff`), every blob's size declared and all of them within
/// [`ATTACHMENT_MAX_BYTES`]. Cosign's signatures and attestations and the SBOM referrers are;
/// a runnable image is not, whatever it names as its `subject`.
pub fn is_inert_attachment(manifest: &[u8]) -> bool {
    let Ok(v) = serde_json::from_slice::<Value>(manifest) else {
        return false;
    };
    if v.get("manifests").is_some() {
        return false;
    }
    let layers = v
        .get("layers")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();
    let mut total: u64 = 0;
    for blob in layers.iter().chain(v.get("config")) {
        let Some(size) = blob.get("size").and_then(Value::as_u64) else {
            return false;
        };
        total = total.saturating_add(size);
    }
    let runnable = layers.iter().any(|l| {
        let mt = l.get("mediaType").and_then(Value::as_str).unwrap_or("");
        mt.starts_with("application/vnd.oci.image.layer.")
            || mt.starts_with("application/vnd.docker.image.rootfs.")
    });
    !runnable && total <= ATTACHMENT_MAX_BYTES
}

fn is_attachment(reference: &str, manifest: &[u8]) -> bool {
    (crate::referrers::cosign_tag(reference).is_some()
        || serde_json::from_slice::<Value>(manifest)
            .ok()
            .is_some_and(|v| v.get("subject").is_some_and(|s| !s.is_null())))
        && is_inert_attachment(manifest)
}

impl OciPush {
    /// **A manifest PUT into the scanned registry `repo` landed** (the backend staged it).
    /// A tag takes the image into the chain as `actor` (from `peer`) and walks it as far as
    /// policy lets it.
    #[allow(clippy::too_many_arguments)]
    pub fn manifest_pushed(
        &self,
        chain: &IntakeChain,
        backend: &dyn RepositoryBackendTrait,
        repo: &str,
        image: &str,
        reference: &str,
        manifest: &[u8],
        actor: &str,
        peer: &str,
    ) -> Pushed {
        if is_attachment(reference, manifest) {
            return Pushed::Staged(format!("{image}:{reference}: an attached artifact, served beside its image once that is AVAILABLE"));
        }
        if oci::is_digest(reference) {
            return Pushed::Staged(format!(
                "{image}@{reference}: staged; a tag that names it takes the image into the chain"
            ));
        }
        let fetch = Staged { backend, repo };
        let reg = oci::Registry::new(&fetch, "local:", None);
        let pulled = match oci::pull(&reg, image, reference, &[], MAX_IMAGE_BYTES) {
            Ok(Some(p)) => p,
            Ok(None) => {
                return Pushed::Refused {
                    id: None,
                    why: format!(
                        "{image}:{reference} is not in the registry's store after its push"
                    ),
                }
            }
            Err(e) => {
                return Pushed::Refused {
                    id: None,
                    why: format!("{image}:{reference} cannot be read back whole: {e:#}"),
                }
            }
        };
        let id = push_id(repo, image, &pulled.digest);
        if let Some(st) = chain.status(&id) {
            // The same image under another tag: it is in the chain already.
            return Pushed::InChain {
                id,
                state: st.state,
                line: format!(
                    "{image}@{} is in the chain already ({})",
                    pulled.digest, st.state
                ),
            };
        }
        let tar = match oci::layout_tar(&pulled) {
            Ok(t) => t,
            Err(e) => {
                return Pushed::Refused {
                    id: Some(id),
                    why: format!("the image layout could not be written: {e:#}"),
                }
            }
        };
        #[cfg(feature = "scanners")]
        if let Some(inbox) = &self.inbox {
            let sha = crate::attest::ArtifactDigest::of_sha256(&tar)
                .hex()
                .to_string();
            for m in &pulled.materials {
                let safe: String = m
                    .name
                    .chars()
                    .map(|c| {
                        if c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_') {
                            c
                        } else {
                            '_'
                        }
                    })
                    .collect();
                if let Err(e) = inbox.put(&sha, safe.trim_start_matches('.'), &m.bytes) {
                    return Pushed::Refused { id: Some(id), why: format!("the image's signatures could not be filed for the provenance check: {e:#}") };
                }
            }
        }
        if let Err(e) = chain.ingest_pushed(actor, peer, repo, &id, &tar) {
            return Pushed::Refused {
                id: Some(id),
                why: e.tagged(),
            };
        }
        self.walk(chain, repo, &id)
    }

    /// **An approved image goes on to AVAILABLE** (SEALED, PROMOTED, AVAILABLE) when it is
    /// next asked for, as a pulled one does; where it stands then.
    fn finish(&self, chain: &IntakeChain, repo: &str, id: &ArtifactId) -> ArtifactState {
        let actor = format!("oci-push:{repo}");
        loop {
            let s = chain.state(id);
            let next = match s {
                ArtifactState::Approved => ArtifactState::Sealed,
                ArtifactState::Sealed => ArtifactState::Promoted,
                ArtifactState::Promoted => ArtifactState::Available,
                s => return s,
            };
            if let Err(e) = chain
                .advance(TransitionRequest::new(id.clone(), next, &actor).from_source("oci-push"))
            {
                log::warn!(
                    "oci push: {} could not go on to {next}: {}",
                    id.name,
                    e.tagged()
                );
                return chain.state(id);
            }
        }
    }

    /// INGESTED → SCANNED → POLICY_CHECKED, then the approvals; a refusal blocks it.
    fn walk(&self, chain: &IntakeChain, repo: &str, id: &ArtifactId) -> Pushed {
        let actor = format!("oci-push:{repo}");
        loop {
            let Some(st) = chain.status(id) else {
                return Pushed::Refused {
                    id: Some(id.clone()),
                    why: "not in the chain".into(),
                };
            };
            let to = match st.state {
                ArtifactState::Ingested => ArtifactState::Scanned,
                ArtifactState::Scanned => ArtifactState::PolicyChecked,
                s => {
                    return Pushed::InChain {
                        id: id.clone(),
                        state: s,
                        line: match s {
                            ArtifactState::PolicyChecked => {
                                format!("{}: POLICY_CHECKED, waiting for its approval(s)", id.name)
                            }
                            s => format!("{}: {s}", id.name),
                        },
                    }
                }
            };
            let step = chain.advance(
                TransitionRequest::new(id.clone(), to, &actor)
                    .from_source("oci-push")
                    .with_reason(format!("pushed to '{repo}'")),
            );
            if let Err(e) = step {
                let why = e.tagged();
                let block = TransitionRequest::new(id.clone(), ArtifactState::Blocked, &actor)
                    .from_source("oci-push")
                    .with_reason(format!("refused on the push to '{repo}': {why}"));
                return match chain.advance(block) {
                    Ok(_) => Pushed::Refused {
                        id: Some(id.clone()),
                        why,
                    },
                    Err(e2) => Pushed::Refused {
                        id: Some(id.clone()),
                        why: format!("{why}; the block failed: {}", e2.tagged()),
                    },
                };
            }
        }
    }
}

// ── the registry door of a scanned registry ─────────────────────────────────

/// One answer of the door.
#[derive(Debug, Clone)]
pub struct Reply {
    pub status: u16,
    pub headers: Vec<(&'static str, String)>,
    pub body: Vec<u8>,
    /// The audit note.
    pub note: String,
}

impl Reply {
    fn with(mut self, k: &'static str, v: String) -> Self {
        self.headers.push((k, v));
        self
    }
}

/// A registry error body.
pub fn registry_error(status: u16, code: &str, message: String) -> Reply {
    let body =
        serde_json::json!({ "errors": [ { "code": code, "message": message, "detail": {} } ] })
            .to_string()
            .into_bytes();
    Reply {
        status,
        headers: vec![
            ("Content-Type", "application/json".into()),
            ("Docker-Distribution-API-Version", "registry/2.0".into()),
        ],
        body,
        note: format!("{code}: {message}"),
    }
}

fn manifest_reply(mt: String, body: Vec<u8>, note: String) -> Reply {
    let digest = crate::referrers::sha256_digest(&body);
    Reply {
        status: 200,
        headers: vec![
            ("Content-Type", mt),
            ("Docker-Content-Digest", digest),
            ("Docker-Distribution-API-Version", "registry/2.0".into()),
            ("X-Holger-Intake-State", "AVAILABLE".into()),
        ],
        body,
        note,
    }
}

fn blob_reply(digest: String, body: Vec<u8>, note: String) -> Reply {
    Reply {
        status: 200,
        headers: vec![
            ("Content-Type", "application/octet-stream".into()),
            ("Docker-Content-Digest", digest),
            ("X-Holger-Intake-State", "AVAILABLE".into()),
        ],
        body,
        note,
    }
}

/// **What an image of a scanned registry shows**: its AVAILABLE layouts, Holger's evidence
/// about each image digest, and what was attached to them after they were taken in.
struct View<'a> {
    backend: &'a dyn RepositoryBackendTrait,
    repo: &'a str,
    image: &'a str,
    layouts: Vec<Arc<Layout>>,
    evidence: crate::referrers::Evidence,
    /// `sha256-<hex>.att` → the merged manifest (own layers, then Holger's).
    att: BTreeMap<String, Vec<u8>>,
}

impl<'a> View<'a> {
    fn of(
        push: &OciPush,
        chain: &IntakeChain,
        backend: &'a dyn RepositoryBackendTrait,
        attest: Option<&crate::attest::AttestStore>,
        repo: &'a str,
        image: &'a str,
    ) -> Self {
        let layouts: Vec<Arc<Layout>> = ids(chain, repo, image)
            .into_iter()
            .filter(|id| push.finish(chain, repo, id) == ArtifactState::Available)
            .filter_map(|id| push.layout(chain, &id))
            .collect();
        let mut evidence = crate::referrers::Evidence::default();
        let mut att = BTreeMap::new();
        for l in &layouts {
            for d in &l.images {
                let Some((mt, m)) = l.manifest(d) else {
                    continue;
                };
                let mut e =
                    crate::referrers::Evidence::of(attest, &crate::referrers::Subject::of(mt, m));
                let tag = format!("sha256-{}.att", d.trim_start_matches("sha256:"));
                // The image's own `.att`: what was pushed since (it may carry Holger's layers
                // already, which are not added twice), else what rode in its layout.
                let own = staged_manifest(backend, repo, image, &tag)
                    .map(|(_, b, _)| b)
                    .or_else(|| {
                        l.attached_tags
                            .get(&tag)
                            .and_then(|ad| l.manifest(ad))
                            .map(|(_, b)| b.to_vec())
                    });
                if let Some(m) = e.att_manifest(own.as_deref()) {
                    att.insert(tag, m);
                }
                evidence.absorb(e);
            }
        }
        Self {
            backend,
            repo,
            image,
            layouts,
            evidence,
            att,
        }
    }

    /// Is `digest` an image digest of an AVAILABLE layout?
    fn available(&self, digest: &str) -> bool {
        self.layouts
            .iter()
            .any(|l| l.images.iter().any(|d| d == digest))
    }

    fn manifest(&self, digest: &str) -> Option<(String, Vec<u8>)> {
        self.layouts.iter().find_map(|l| {
            l.manifest(digest)
                .map(|(mt, b)| (mt.to_string(), b.to_vec()))
        })
    }

    /// The attached artifacts pushed after the image was taken in: `(digest, (mt, bytes))`
    /// of every staged manifest that is a referrer or a cosign tag of an AVAILABLE digest.
    fn staged_attachment(&self, digest: &str) -> Option<(String, Vec<u8>)> {
        let (mt, body, d) = staged_manifest(self.backend, self.repo, self.image, digest)?;
        if d != digest {
            return None;
        }
        if !is_inert_attachment(&body) {
            return None;
        }
        let v: Value = serde_json::from_slice(&body).ok()?;
        let subject = v.pointer("/subject/digest").and_then(Value::as_str)?;
        self.available(subject).then_some((mt, body))
    }

    /// Every blob an attachment of an AVAILABLE digest (pushed after) names.
    fn staged_attachment_blob(&self, digest: &str) -> Option<Vec<u8>> {
        for l in &self.layouts {
            for d in &l.images {
                let hex = d.trim_start_matches("sha256:");
                let mut manifests: Vec<Vec<u8>> = Vec::new();
                for suffix in ["sig", "att", "sbom"] {
                    if let Some((_, b, _)) = staged_manifest(
                        self.backend,
                        self.repo,
                        self.image,
                        &format!("sha256-{hex}.{suffix}"),
                    ) {
                        manifests.push(b);
                    }
                }
                for r in staged_referrers(self.backend, self.repo, self.image, d) {
                    if let Some(rd) = r.get("digest").and_then(Value::as_str) {
                        if let Some((_, b, _)) =
                            staged_manifest(self.backend, self.repo, self.image, rd)
                        {
                            manifests.push(b);
                        }
                    }
                }
                for m in manifests.into_iter().filter(|m| is_inert_attachment(m)) {
                    let v: Value = serde_json::from_slice(&m).unwrap_or_default();
                    let names = v
                        .pointer("/config/digest")
                        .and_then(Value::as_str)
                        .into_iter()
                        .chain(
                            v.get("layers")
                                .and_then(Value::as_array)
                                .into_iter()
                                .flatten()
                                .filter_map(|x| x.get("digest").and_then(Value::as_str)),
                        );
                    if names.into_iter().any(|n| n == digest) {
                        return staged_blob(self.backend, self.repo, self.image, digest);
                    }
                }
            }
        }
        None
    }

    fn tag(&self, tag: &str) -> Option<(String, Vec<u8>)> {
        let (subject, _) = crate::referrers::cosign_tag(tag)?;
        if !self.available(&subject) {
            return None;
        }
        if let Some(m) = self.att.get(tag) {
            return Some((crate::referrers::MANIFEST_MEDIA_TYPE.to_string(), m.clone()));
        }
        if let Some((mt, b, _)) = staged_manifest(self.backend, self.repo, self.image, tag) {
            return is_inert_attachment(&b).then_some((mt, b));
        }
        self.layouts.iter().find_map(|l| {
            l.attached_tags
                .get(tag)
                .and_then(|d| l.manifest(d))
                .map(|(mt, b)| (mt.to_string(), b.to_vec()))
        })
    }

    fn referrers(&self, digest: &str) -> Vec<Value> {
        if !self.available(digest) {
            return Vec::new();
        }
        let mut out: Vec<Value> = self
            .layouts
            .iter()
            .flat_map(|l| l.referrers.get(digest).cloned().unwrap_or_default())
            .collect();
        out.extend(staged_referrers(
            self.backend,
            self.repo,
            self.image,
            digest,
        ));
        out.extend(
            self.evidence
                .referrers
                .iter()
                .filter(|r| {
                    self.evidence
                        .object(r.get("digest").and_then(Value::as_str).unwrap_or(""))
                        .and_then(|(_, m)| serde_json::from_slice::<Value>(m).ok())
                        .is_some_and(|m| {
                            m.pointer("/subject/digest").and_then(Value::as_str) == Some(digest)
                        })
                })
                .cloned(),
        );
        out
    }
}

/// The chain coordinates of `image`'s pushed layouts in `repo`.
fn ids(chain: &IntakeChain, repo: &str, image: &str) -> Vec<ArtifactId> {
    let prefix = format!("{repo}/{image}/");
    chain
        .all()
        .into_iter()
        .map(|(id, _)| id)
        .filter(|id| {
            id.name
                .strip_prefix(&prefix)
                .is_some_and(|r| !r.contains('/') && r.ends_with(".oci.tar"))
        })
        .collect()
}

/// "Not yet", in the registry's words: where the image stands in the chain.
fn not_yet(chain: &IntakeChain, id: &ArtifactId, what: &str) -> Reply {
    let st = chain.status(id);
    match st.as_ref().map(|s| s.state) {
        Some(ArtifactState::Blocked) | Some(ArtifactState::Quarantined) => registry_error(
            403,
            "DENIED",
            format!(
                "{what} was refused by Holger's intake chain and is never served: {}",
                st.map(|s| s.last.reason).unwrap_or_default()
            ),
        ),
        Some(s) => registry_error(
            404,
            "MANIFEST_UNKNOWN",
            format!("{what} is not yet AVAILABLE: Holger's intake chain holds it at {s}"),
        ),
        None => registry_error(404, "MANIFEST_UNKNOWN", format!("{what}: no such image")),
    }
}

impl OciPush {
    /// **A read below `/v2/<repo>/`** of a scanned registry. `rest` is the path below the
    /// repository (with its query); `attest` the store Holger's evidence is read from.
    ///
    /// `pusher`: the reader may push here (write on the registry and the Importer duty). A
    /// pusher reads its own STAGED manifest back by digest (lane HOL-FIELD-2): `helm push`
    /// 3.18+ and oras push by digest, read it back, then PUT it under the tag. Only the
    /// manifest; a staged blob is never served, to anyone.
    pub fn read(
        &self,
        chain: &IntakeChain,
        backend: &dyn RepositoryBackendTrait,
        attest: Option<&crate::attest::AttestStore>,
        repo: &str,
        rest: &str,
        pusher: bool,
    ) -> Reply {
        let Some(p) = oci::parse_path(rest) else {
            return registry_error(
                404,
                "NAME_UNKNOWN",
                format!("'{rest}' is not a registry path Holger serves"),
            );
        };
        match p {
            OciPath::Referrers {
                name,
                digest,
                artifact_type,
            } => {
                let v = View::of(self, chain, backend, attest, repo, &name);
                let found = v.referrers(&digest);
                let n = found.len();
                let (body, filtered) = crate::referrers::index(found, artifact_type.as_deref());
                let mut r = Reply {
                    status: 200,
                    headers: vec![("Content-Type", crate::referrers::INDEX_MEDIA_TYPE.into()), ("Docker-Distribution-API-Version", "registry/2.0".into())],
                    body,
                    note: format!("oci push: the referrers of {name}@{digest} ({n} before any filter), of an AVAILABLE image"),
                };
                if filtered {
                    r = r.with("OCI-Filters-Applied", "artifactType".into());
                }
                r
            }
            OciPath::Manifest { name, reference }
                if crate::referrers::cosign_tag(&reference).is_some() =>
            {
                let v = View::of(self, chain, backend, attest, repo, &name);
                match v.tag(&reference) {
                    Some((mt, body)) => manifest_reply(
                        mt,
                        body,
                        format!("oci push: {name}:{reference}, attached to an AVAILABLE image"),
                    ),
                    None => registry_error(
                        404,
                        "MANIFEST_UNKNOWN",
                        format!("{name}:{reference}: no AVAILABLE image carries it"),
                    ),
                }
            }
            OciPath::Manifest { name, reference } => {
                let v = View::of(self, chain, backend, attest, repo, &name);
                // By tag: the digest the staging store names now, which must be AVAILABLE.
                let digest = if oci::is_digest(&reference) {
                    reference.clone()
                } else {
                    match staged_manifest(backend, repo, &name, &reference) {
                        Some((_, _, d)) => d,
                        None => {
                            return registry_error(
                                404,
                                "MANIFEST_UNKNOWN",
                                format!("{name}:{reference}: no such tag"),
                            )
                        }
                    }
                };
                if let Some((mt, body)) = v.manifest(&digest) {
                    return manifest_reply(
                        mt,
                        body,
                        format!(
                            "oci push: {name}:{reference} AVAILABLE, served from the sealed layout"
                        ),
                    );
                }
                if let Some((mt, body)) = v
                    .evidence
                    .object(&digest)
                    .map(|(m, b)| (m.to_string(), b.to_vec()))
                {
                    return manifest_reply(
                        mt,
                        body,
                        format!("oci push: Holger's evidence about {name}, {digest}"),
                    );
                }
                if let Some((mt, body)) = v.staged_attachment(&digest) {
                    return manifest_reply(
                        mt,
                        body,
                        format!("oci push: {name}@{digest}, attached to an AVAILABLE image"),
                    );
                }
                let id = push_id(repo, &name, &digest);
                let refused = matches!(
                    chain.status(&id).map(|s| s.state),
                    Some(ArtifactState::Blocked | ArtifactState::Quarantined)
                );
                if pusher && oci::is_digest(&reference) && !refused {
                    if let Some((mt, body, d)) = staged_manifest(backend, repo, &name, &digest) {
                        if d == digest {
                            return manifest_reply(
                                mt,
                                body,
                                format!(
                                    "oci push: {name}@{digest} STAGED, read back by a pusher \
                                     (a digest push before its tag); never served to readers"
                                ),
                            );
                        }
                    }
                }
                not_yet(
                    chain,
                    &push_id(repo, &name, &digest),
                    &format!("{name}:{reference}"),
                )
            }
            OciPath::Blob { name, digest } => {
                let v = View::of(self, chain, backend, attest, repo, &name);
                if let Some(b) = v
                    .layouts
                    .iter()
                    .find_map(|l| l.blob(&digest).map(<[u8]>::to_vec))
                {
                    return blob_reply(
                        digest,
                        b,
                        format!(
                            "oci push: a blob of {name} (AVAILABLE), served from the sealed layout"
                        ),
                    );
                }
                if let Some((_, b)) = v.evidence.object(&digest) {
                    return blob_reply(
                        digest.clone(),
                        b.to_vec(),
                        format!("oci push: Holger's evidence about {name}"),
                    );
                }
                if let Some(b) = v.staged_attachment_blob(&digest) {
                    return blob_reply(
                        digest,
                        b,
                        format!("oci push: a blob attached to an AVAILABLE image of {name}"),
                    );
                }
                registry_error(
                    404,
                    "BLOB_UNKNOWN",
                    format!("{digest} is in no AVAILABLE image of {name}"),
                )
            }
            OciPath::Tags { name } => {
                let v = View::of(self, chain, backend, attest, repo, &name);
                let staged: Vec<String> = backend
                    .handle_http2_request("GET", &format!("/v2/{repo}/{name}/tags/list"), &[])
                    .ok()
                    .filter(|(s, _, _)| *s == 200)
                    .and_then(|(_, _, b)| serde_json::from_slice::<Value>(&b).ok())
                    .and_then(|v| v.get("tags").and_then(Value::as_array).cloned())
                    .unwrap_or_default()
                    .into_iter()
                    .filter_map(|t| t.as_str().map(str::to_string))
                    .collect();
                let tags: Vec<String> = staged
                    .into_iter()
                    .filter(|t| match crate::referrers::cosign_tag(t) {
                        Some((subject, _)) => v.available(&subject),
                        None => staged_manifest(backend, repo, &name, t)
                            .is_some_and(|(_, _, d)| v.available(&d)),
                    })
                    .collect();
                let body = serde_json::json!({ "name": format!("{repo}/{name}"), "tags": tags })
                    .to_string()
                    .into_bytes();
                Reply {
                    status: 200,
                    headers: vec![("Content-Type", "application/json".into())],
                    body,
                    note: format!("oci push: the AVAILABLE tags of {name}"),
                }
            }
        }
    }
}

/// The registry path below the repository: `/v2/<repo>/<rest>` → `<rest>`.
pub fn rest_of<'p>(path_and_query: &'p str, repo: &str) -> Option<&'p str> {
    path_and_query.strip_prefix(&format!("/v2/{repo}/"))
}

/// `(image, reference)` of a manifest path below the repository.
pub fn manifest_target(rest: &str) -> Option<(String, String)> {
    match oci::parse_path(rest)? {
        OciPath::Manifest { name, reference } => Some((name, reference)),
        _ => None,
    }
}

/// The refusal of a write a scanned registry never takes.
pub fn delete_refused(what: &str) -> IntakeError {
    IntakeError::DoorRefused {
        repo: what.to_string(),
        why: holger_errcode::OCI_PUSH_DELETE_REFUSED.tag("a scanned registry never deletes or rewrites what was pushed: the intake chain holds it"),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn rule(repo: &str, scan: PushScan) -> OciPushRule {
        OciPushRule {
            repository: repo.into(),
            scan,
        }
    }

    #[test]
    fn every_oci_registry_is_scanned_unless_the_ron_opts_it_out() {
        let s = scanned_registries(&[], &["oci", "scratch"], None, &[]).unwrap();
        assert_eq!(
            s.iter().map(String::as_str).collect::<Vec<_>>(),
            vec!["oci", "scratch"]
        );
        let s = scanned_registries(
            &[rule("scratch", PushScan::Off)],
            &["oci", "scratch"],
            None,
            &[],
        )
        .unwrap();
        assert_eq!(
            s.iter().map(String::as_str).collect::<Vec<_>>(),
            vec!["oci"]
        );
        let ron: Vec<OciPushRule> =
            ron::from_str(r#"[(repository: "scratch", scan: off), (repository: "oci")]"#).unwrap();
        assert_eq!(
            ron,
            vec![
                rule("scratch", PushScan::Off),
                rule("oci", PushScan::Required)
            ]
        );
        assert!(ron::from_str::<Vec<OciPushRule>>(r#"[(repository: "x", scan: maybe)]"#).is_err());
    }

    #[test]
    fn an_opt_out_where_the_policy_requires_a_scan_is_refused_by_name() {
        let e = scanned_registries(
            &[rule("oci", PushScan::Off)],
            &["oci"],
            Some("security_profile: Critical"),
            &[],
        )
        .unwrap_err();
        assert!(
            e.contains("[holger-oci-1]") && e.contains("Critical"),
            "{e}"
        );
        let e =
            scanned_registries(&[rule("nope", PushScan::Off)], &["oci"], None, &[]).unwrap_err();
        assert!(e.contains("[holger-oci-2]"), "{e}");
        let e = scanned_registries(&[], &["up"], None, &["up"]).unwrap_err();
        assert!(e.contains("[holger-oci-2]"), "{e}");
    }

    #[test]
    fn attachments_and_digests_are_staged_and_a_tag_is_taken_in() {
        assert!(is_attachment(
            &format!("sha256-{}.sig", "a".repeat(64)),
            b"{}"
        ));
        assert!(is_attachment("x", br#"{"subject":{"digest":"sha256:x"}}"#));
        assert!(!is_attachment("1.0", br#"{"layers":[]}"#));
        // P7: a runnable image or an oversized blob is no attachment, whatever its subject.
        assert!(!is_attachment(
            "x",
            br#"{"subject":{"digest":"sha256:x"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":10}]}"#
        ));
        assert!(!is_attachment(
            "x",
            br#"{"subject":{"digest":"sha256:x"},"layers":[{"mediaType":"application/vnd.dsse.envelope.v1+json","size":99999999}]}"#
        ));
        assert!(!is_attachment(
            "x",
            br#"{"subject":{"digest":"sha256:x"},"layers":[{"mediaType":"application/vnd.dsse.envelope.v1+json"}]}"#
        ));
        assert_eq!(
            push_id("oci", "demo/app", "sha256:ab").name,
            "oci/demo/app/sha256-ab.oci.tar"
        );
    }
}