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

mikronova/modgunn

src/provenance.rs

//! Provenance verification hooks (feature `scan`) — the A3/A24/A25/A26 surface.
//!
//! Engine A's [`crate::scan::Provenance`] is a bare "signed?" boolean. Real
//! supply-chain integrity wants more: *who* signed (sigstore/Fulcio OIDC
//! identity), at what **SLSA** build level, and whether the artifact's claimed
//! source repo + commit match. modgunn does not bundle a cosign/rekor client
//! (that is the dynamic plane's job, network-bound), so this module defines:
//!
//! - [`Attestation`] — the verified provenance facts a verifier produces.
//! - [`ProvenanceRequirement`] — the policy (min SLSA level, trusted signer set,
//!   expected source repo) and [`ProvenanceRequirement::evaluate`], which turns an
//!   attestation (or its absence) into [`Finding`]s + a [`crate::scan::Provenance`]
//!   Engine A can consume.
//! - [`attestation_delta`] — A25: builder-identity change / attestation
//!   appears-or-disappears / source-repo mismatch between two snapshots.
//! - [`ScorecardSignals`] — A14/A24: an OSSF Scorecard signal struct (maintained?,
//!   branch-protection, signed-releases, …) → advisory [`Finding`]s.

use serde::Deserialize;

use crate::core::{Finding, Severity};
use crate::scan::Provenance;

/// The provenance facts a verifier (cosign/rekor/slsa-verifier on the dynamic
/// plane) establishes about an artifact. `slsa_level` is 0 (none) … 3.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Attestation {
    /// The verified signer / OIDC (Fulcio) identity, e.g. a CI workload identity.
    pub signer_identity: Option<String>,
    /// SLSA build level the provenance attests (0 = none, up to 3).
    pub slsa_level: u8,
    /// The source repo the artifact claims to be built from.
    pub source_repo: Option<String>,
    /// The source commit SHA the artifact claims to be built from.
    pub source_sha: Option<String>,
}

impl Attestation {
    /// A verified sigstore attestation by `identity` at the given SLSA level.
    pub fn signed_by(identity: impl Into<String>, slsa_level: u8) -> Self {
        Self {
            signer_identity: Some(identity.into()),
            slsa_level,
            ..Default::default()
        }
    }
}

// ── Sigstore / Rekor transparency-log entries ────────────────────────────────
//
// A cosign signature is only half the story: the OTHER half is that the signature
// was written to a PUBLIC, append-only transparency log (Rekor), so a signer cannot
// quietly sign a malicious artifact for one victim. This module reads a Rekor entry
// offline and turns it into the facts modgunn's provenance verdict keys on — which
// artifact digest was logged, by which Fulcio/OIDC identity, at which log index and
// integration time.
//
// Pure parse, no network and no crypto: verifying the inclusion proof against the
// log's signed tree head is the dynamic plane's job (a live Rekor client). What
// modgunn owns is the *record* — the digest→identity→index binding that a promotion
// gate compares against its policy, and the honest statement of which parts are
// self-asserted vs. proof-backed.

/// One Sigstore **Rekor** transparency-log entry, decoded far enough to be useful to
/// a provenance verdict.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RekorEntry {
    /// The entry UUID (the log's leaf hash) — its stable identity.
    pub uuid: String,
    /// The monotonically-increasing position in the log. A *lower* index than an
    /// artifact you already trust means the entry predates it.
    pub log_index: u64,
    /// Unix seconds at which the log integrated the entry — the "signed at" time a
    /// gate compares against the artifact's build time.
    pub integrated_time: i64,
    /// The Rekor **type** of the entry body (`hashedrekord` / `intoto` / `dsse` / …).
    pub kind: String,
    /// The artifact digest the entry binds, lowercased hex (`""` when the body kind
    /// carries none in a shape we read).
    pub artifact_sha256: String,
    /// The signer identity extracted from the entry, when the body states one.
    pub signer_identity: Option<String>,
    /// Whether the entry carries an **inclusion proof** — the cryptographic evidence
    /// it really is in the log, as opposed to a bare self-asserted record.
    pub has_inclusion_proof: bool,
    /// Whether the entry carries a **signed entry timestamp** (the log's countersignature).
    pub has_signed_timestamp: bool,
}

impl RekorEntry {
    /// The provenance facts this log entry establishes, as the [`Attestation`] the
    /// A3 gate evaluates.
    ///
    /// **`slsa_level` is deliberately 0.** A Rekor entry proves *this digest was
    /// logged by this identity*; it says nothing about how the artifact was BUILT,
    /// which is what a SLSA level asserts. Inferring a level from log presence would
    /// be exactly the kind of unearned claim `provenance-verify` exists to prevent —
    /// a caller that also has a verified SLSA provenance predicate raises it
    /// explicitly.
    pub fn attestation(&self) -> Attestation {
        Attestation {
            signer_identity: self.signer_identity.clone(),
            slsa_level: 0,
            ..Default::default()
        }
    }

    /// Findings about the *strength* of this transparency record, worst first. An
    /// entry with both an inclusion proof and a signed timestamp is clean.
    pub fn findings(&self) -> Vec<Finding> {
        let mut out = Vec::new();
        if !self.has_inclusion_proof {
            out.push(Finding {
                severity: Severity::High,
                id: "MODGUNN-REKOR-NO-INCLUSION-PROOF".into(),
                summary: format!(
                    "Rekor entry {} carries no inclusion proof — the claim that it is in the log is self-asserted",
                    self.uuid
                ),
            });
        }
        if !self.has_signed_timestamp {
            out.push(Finding {
                severity: Severity::Medium,
                id: "MODGUNN-REKOR-NO-SIGNED-TIMESTAMP".into(),
                summary: format!(
                    "Rekor entry {} carries no signed entry timestamp (the log's countersignature)",
                    self.uuid
                ),
            });
        }
        if self.signer_identity.is_none() {
            out.push(Finding {
                severity: Severity::Medium,
                id: "MODGUNN-REKOR-NO-SIGNER-IDENTITY".into(),
                summary: format!("Rekor entry {} states no signer identity — nothing to match against trusted signers", self.uuid),
            });
        }
        out
    }
}

/// Decode standard base64 (`A–Z a–z 0–9 + /`, `=` padding). Rekor wraps the entry
/// body in it. A dozen lines of pure Rust rather than a `base64` dependency, so the
/// LEAN `scan` gate (holger / static-holger) stays dependency-clean — the whole point
/// of the feature split. Whitespace is ignored; any other invalid character, or a
/// truncated final group, yields `None` (never a partially-decoded body that would
/// parse into a *wrong* record).
fn base64_decode(s: &str) -> Option<Vec<u8>> {
    fn val(c: u8) -> Option<u8> {
        Some(match c {
            b'A'..=b'Z' => c - b'A',
            b'a'..=b'z' => c - b'a' + 26,
            b'0'..=b'9' => c - b'0' + 52,
            b'+' => 62,
            b'/' => 63,
            _ => return None,
        })
    }
    let (mut acc, mut bits, mut out, mut pad) = (0u32, 0u8, Vec::new(), 0usize);
    for c in s.bytes() {
        if c.is_ascii_whitespace() {
            continue;
        }
        if c == b'=' {
            pad += 1;
            continue;
        }
        if pad > 0 {
            return None; // data after padding — malformed
        }
        acc = (acc << 6) | val(c)? as u32;
        bits += 6;
        if bits >= 8 {
            bits -= 8;
            out.push((acc >> bits) as u8);
        }
    }
    // A leftover group of 6 bits is a truncated encoding, not a valid one.
    if bits >= 6 || (acc & ((1u32 << bits) - 1)) != 0 {
        return None;
    }
    Some(out)
}

/// Parse a **Rekor** log-entry response into [`RekorEntry`]s.
///
/// The `/api/v1/log/entries/{uuid}` response is a map `{ "<uuid>": { body,
/// integratedTime, logIndex, verification } }` (a retrieval by index or a search
/// returns several). The `body` is base64-encoded canonical JSON whose `kind` selects
/// the spec shape; the artifact digest and signer are read from the shapes that carry
/// them:
///
/// - **`hashedrekord`** — `spec.data.hash.value` is the artifact digest; the signer is
///   the `spec.signature.publicKey` (a Fulcio cert, reported as its base64 identity
///   only when the entry states a plain identity, see below).
/// - **`intoto` / `dsse`** — `spec.content.payloadHash.value` (or `spec.content.hash.value`)
///   is the payload digest; the signer identity is `spec.content.envelope.signatures[].keyid`
///   when present.
///
/// **The `verification` block is read, not assumed.** An entry with no
/// `inclusionProof` is a self-asserted record — modgunn records that fact
/// ([`RekorEntry::has_inclusion_proof`]) and [`RekorEntry::findings`] grades it
/// `High`, rather than treating "the JSON says it is in the log" as proof. A body
/// that is not valid base64, or not JSON once decoded, is skipped and does not
/// silently become an entry with empty fields.
pub fn parse_rekor_entries(json: &str) -> Result<Vec<RekorEntry>, serde_json::Error> {
    let doc: serde_json::Value = serde_json::from_str(json)?;
    // A single-entry object (already unwrapped) vs. the uuid-keyed map.
    let pairs: Vec<(String, &serde_json::Value)> = if doc.get("body").is_some() {
        vec![(
            doc.get("uuid")
                .and_then(|u| u.as_str())
                .unwrap_or("")
                .to_string(),
            &doc,
        )]
    } else if let Some(map) = doc.as_object() {
        map.iter().map(|(k, v)| (k.clone(), v)).collect()
    } else {
        return Ok(Vec::new());
    };

    let mut out = Vec::new();
    for (uuid, v) in pairs {
        let Some(body_b64) = v.get("body").and_then(|b| b.as_str()) else {
            continue;
        };
        let Some(bytes) = base64_decode(body_b64) else {
            continue;
        };
        let Ok(body) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
            continue;
        };
        let kind = body
            .get("kind")
            .and_then(|k| k.as_str())
            .unwrap_or("")
            .to_string();
        let spec = body.get("spec");

        // The digest, per body kind. `hashedrekord` puts it under data.hash;
        // intoto/dsse under content.payloadHash (older: content.hash).
        let digest = spec
            .and_then(|s| s.pointer("/data/hash/value"))
            .or_else(|| spec.and_then(|s| s.pointer("/content/payloadHash/value")))
            .or_else(|| spec.and_then(|s| s.pointer("/content/hash/value")))
            .and_then(|d| d.as_str())
            .unwrap_or("")
            .trim()
            .to_ascii_lowercase();

        let signer = spec
            .and_then(|s| s.pointer("/content/envelope/signatures/0/keyid"))
            .or_else(|| spec.and_then(|s| s.pointer("/signature/publicKey/identity")))
            .and_then(|k| k.as_str())
            .map(str::trim)
            .filter(|s| !s.is_empty())
            .map(str::to_string);

        let verification = v.get("verification");
        out.push(RekorEntry {
            uuid,
            log_index: v.get("logIndex").and_then(|i| i.as_u64()).unwrap_or(0),
            integrated_time: v
                .get("integratedTime")
                .and_then(|t| t.as_i64())
                .unwrap_or(0),
            kind,
            artifact_sha256: digest,
            signer_identity: signer,
            has_inclusion_proof: verification.and_then(|x| x.get("inclusionProof")).is_some(),
            has_signed_timestamp: verification
                .and_then(|x| x.get("signedEntryTimestamp"))
                .and_then(|t| t.as_str())
                .map(|t| !t.trim().is_empty())
                .unwrap_or(false),
        });
    }
    Ok(out)
}

/// Policy for verifying an [`Attestation`] (the A3 check). All-empty/zero is
/// "accept anything signed".
#[derive(Debug, Clone, Default)]
pub struct ProvenanceRequirement {
    /// Minimum acceptable SLSA build level (0 = don't care).
    pub require_min_slsa: u8,
    /// Signer identities the consumer trusts (empty = any signer accepted).
    pub trusted_signers: Vec<String>,
    /// The source repo the artifact must claim, if any.
    pub expected_source_repo: Option<String>,
    /// The exact source commit (VCS revision) the artifact must be built from, if
    /// pinned. Pinning the commit — not just the repo — is what closes the "right
    /// repo, unexpected commit" gap: a promotion can demand the artifact came from a
    /// reviewed/tagged revision, so a build off an unmerged or attacker-pushed commit
    /// on the trusted repo still fails. Empty = don't care.
    pub expected_source_sha: Option<String>,
}

impl ProvenanceRequirement {
    /// Evaluate an attestation against this requirement. Returns the
    /// [`Provenance`] Engine A consumes (its `signed` reflects "passed all
    /// checks") plus the [`Finding`]s explaining any shortfall. `None` ⇒ the
    /// artifact carries no attestation at all.
    pub fn evaluate(&self, att: Option<&Attestation>) -> (Provenance, Vec<Finding>) {
        let Some(att) = att else {
            return (
                Provenance::unsigned(),
                vec![Finding {
                    severity: Severity::High,
                    id: "MODGUNN-PROV-NO-ATTESTATION".into(),
                    summary: "artifact carries no provenance attestation".into(),
                }],
            );
        };
        let mut findings = Vec::new();

        if att.slsa_level < self.require_min_slsa {
            findings.push(Finding {
                severity: Severity::High,
                id: "MODGUNN-PROV-SLSA-TOO-LOW".into(),
                summary: format!(
                    "SLSA level {} below required {}",
                    att.slsa_level, self.require_min_slsa
                ),
            });
        }
        match &att.signer_identity {
            Some(id) if !self.trusted_signers.is_empty() && !self.trusted_signers.contains(id) => {
                findings.push(Finding {
                    severity: Severity::High,
                    id: "MODGUNN-PROV-UNTRUSTED-SIGNER".into(),
                    summary: format!("signer `{id}` is not in the trusted set"),
                });
            }
            None => findings.push(Finding {
                severity: Severity::Medium,
                id: "MODGUNN-PROV-NO-SIGNER".into(),
                summary: "attestation has no signer identity".into(),
            }),
            Some(_) => {}
        }
        if let Some(expected) = &self.expected_source_repo {
            if att.source_repo.as_deref() != Some(expected.as_str()) {
                findings.push(Finding {
                    severity: Severity::High,
                    id: "MODGUNN-PROV-SOURCE-MISMATCH".into(),
                    summary: format!(
                        "source repo `{}` ≠ expected `{expected}`",
                        att.source_repo.as_deref().unwrap_or("<none>")
                    ),
                });
            }
        }
        if let Some(expected) = &self.expected_source_sha {
            // Case-insensitive: git revisions are hex, and producers differ on case.
            let matches = att
                .source_sha
                .as_deref()
                .is_some_and(|got| got.eq_ignore_ascii_case(expected));
            if !matches {
                findings.push(Finding {
                    severity: Severity::High,
                    id: "MODGUNN-PROV-COMMIT-MISMATCH".into(),
                    summary: format!(
                        "source commit `{}` ≠ expected `{expected}`",
                        att.source_sha.as_deref().unwrap_or("<none>")
                    ),
                });
            }
        }

        let signer = att.signer_identity.clone();
        // `signed` = it passed verification (no findings) AND has a signer.
        let prov = Provenance {
            signed: findings.is_empty() && signer.is_some(),
            signer,
        };
        (prov, findings)
    }
}

/// Case-insensitive equality for two optional hex VCS revisions.
fn sha_eq(a: Option<&str>, b: Option<&str>) -> bool {
    match (a, b) {
        (Some(x), Some(y)) => x.eq_ignore_ascii_case(y),
        (None, None) => true,
        _ => false,
    }
}

/// A25 — the **attestation delta** between two snapshots of the same artifact.
/// Fires on the supply-chain-anomaly signals: attestation disappearing, builder
/// (signer) identity changing, or the claimed source repo moving.
pub fn attestation_delta(old: Option<&Attestation>, new: Option<&Attestation>) -> Vec<Finding> {
    let mut findings = Vec::new();
    match (old, new) {
        (Some(_), None) => findings.push(Finding {
            severity: Severity::High,
            id: "MODGUNN-PROV-ATTESTATION-DROPPED".into(),
            summary: "provenance attestation present before is now absent".into(),
        }),
        (None, Some(_)) => findings.push(Finding {
            severity: Severity::Info,
            id: "MODGUNN-PROV-ATTESTATION-ADDED".into(),
            summary: "provenance attestation newly present".into(),
        }),
        (Some(o), Some(n)) => {
            if o.signer_identity != n.signer_identity {
                findings.push(Finding {
                    severity: Severity::High,
                    id: "MODGUNN-PROV-SIGNER-CHANGED".into(),
                    summary: format!(
                        "signer identity changed: {:?} → {:?}",
                        o.signer_identity, n.signer_identity
                    ),
                });
            }
            if o.source_repo != n.source_repo {
                findings.push(Finding {
                    severity: Severity::Medium,
                    id: "MODGUNN-PROV-SOURCE-CHANGED".into(),
                    summary: format!(
                        "source repo changed: {:?} → {:?}",
                        o.source_repo, n.source_repo
                    ),
                });
            }
            // A commit move for what should be the SAME immutable artifact is a
            // rebuild-from-a-different-revision signal — a version ought to be
            // reproducible from a single source commit. Only flagged when BOTH sides
            // attest a commit (a first-time appearance is not a "change") and the
            // repo held steady (a repo move already carries its own, stronger finding
            // and would make a commit diff redundant noise).
            if o.source_repo == n.source_repo
                && o.source_sha.is_some()
                && n.source_sha.is_some()
                && !sha_eq(o.source_sha.as_deref(), n.source_sha.as_deref())
            {
                findings.push(Finding {
                    severity: Severity::Medium,
                    id: "MODGUNN-PROV-COMMIT-CHANGED".into(),
                    summary: format!(
                        "source commit changed on the same repo: {:?} → {:?}",
                        o.source_sha, n.source_sha
                    ),
                });
            }
        }
        (None, None) => {}
    }
    findings
}

/// A14/A24 — an OSSF **Scorecard** signal struct (a subset of the real checks).
/// `overall` is the 0..=10 aggregate; the booleans are the high-signal checks.
#[derive(Debug, Clone, PartialEq)]
pub struct ScorecardSignals {
    pub subject: String,
    pub overall: f64,
    pub maintained: bool,
    pub branch_protection: bool,
    pub signed_releases: bool,
    pub dangerous_workflow: bool,
}

impl ScorecardSignals {
    /// Turn the signals into advisory [`Finding`]s. A low overall score warns; an
    /// unmaintained project or a dangerous workflow is a stronger signal (the
    /// bus-factor / takeover-risk A14/A24 features).
    pub fn findings(&self) -> Vec<Finding> {
        let mut out = Vec::new();
        if self.overall < 5.0 {
            out.push(Finding {
                severity: Severity::Medium,
                id: "MODGUNN-SCORECARD-LOW".into(),
                summary: format!(
                    "OSSF Scorecard {:.1}/10 for `{}`",
                    self.overall, self.subject
                ),
            });
        }
        if !self.maintained {
            out.push(Finding {
                severity: Severity::Medium,
                id: "MODGUNN-SCORECARD-UNMAINTAINED".into(),
                summary: format!("`{}` appears unmaintained (bus-factor risk)", self.subject),
            });
        }
        if self.dangerous_workflow {
            out.push(Finding {
                severity: Severity::High,
                id: "MODGUNN-SCORECARD-DANGEROUS-WORKFLOW".into(),
                summary: format!("`{}` has a dangerous CI workflow", self.subject),
            });
        }
        if !self.branch_protection {
            out.push(Finding {
                severity: Severity::Low,
                id: "MODGUNN-SCORECARD-NO-BRANCH-PROTECTION".into(),
                summary: format!(
                    "`{}` lacks branch protection on its default branch",
                    self.subject
                ),
            });
        }
        out
    }
}

/// Parse an **OpenSSF Scorecard** JSON report (`scorecard --format json`) into
/// [`ScorecardSignals`] — the read-side counterpart to
/// [`ScorecardSignals::findings`]'s fold. Mirrors `feeds::parse_*`: pure serde_json,
/// offline, one fixture-driven parser, no twin of the signal model (L5 — it produces
/// the SAME [`ScorecardSignals`] the warehouse round-trips and `provenance-log` reads).
///
/// Scorecard scores every check `0..=10` (higher = better; `-1` = inconclusive /
/// not-run) plus a top-level aggregate `score`. The high-signal checks fold to the
/// booleans: a check *passes* at Scorecard's own **Medium** risk tier (`score >= 5`).
/// **Dangerous-Workflow is inverted** — it scores `10` when clean and `0` when a
/// dangerous CI pattern (self-hosted-runner injection / script-injection) is found,
/// so a present score in `0..10` sets [`ScorecardSignals::dangerous_workflow`] (which
/// `findings()` maps to a High). Unknown / absent checks stay `false` (no signal).
pub fn parse_scorecard(json: &str) -> Result<ScorecardSignals, serde_json::Error> {
    let doc: ScorecardDoc = serde_json::from_str(json)?;
    // Scorecard's own risk tiers put "Medium" at 5/10; use it as the pass line.
    const PASS: i64 = 5;
    let score_of = |name: &str| {
        doc.checks
            .iter()
            .find(|c| c.name.eq_ignore_ascii_case(name))
            .map(|c| c.score)
    };
    let passes = |name: &str| score_of(name).is_some_and(|s| s >= PASS);
    let signals = ScorecardSignals {
        subject: doc.repo.name,
        overall: doc.score,
        maintained: passes("Maintained"),
        branch_protection: passes("Branch-Protection"),
        signed_releases: passes("Signed-Releases"),
        // 10 ⇒ no dangerous workflow found; 0 ⇒ found; -1 ⇒ inconclusive → not flagged.
        dangerous_workflow: matches!(score_of("Dangerous-Workflow"), Some(s) if (0..10).contains(&s)),
    };
    // Introspection marker — the Scorecard ingest surface reports each report it
    // folds (GREEN when parsed; the dangerous-workflow flag rides the detail) so
    // `nornir test` SEES the A14/A24 seam. Feature-gated: a compiled-out no-op with
    // no nornir dep in a lean `scan` build.
    #[cfg(feature = "testmatrix")]
    crate::functional_status(
        "modgunn/scorecard-ingest",
        "parse_scorecard",
        true,
        &format!(
            "{}: {:.1}/10 dangerous_workflow={} branch_protection={}",
            signals.subject, signals.overall, signals.dangerous_workflow, signals.branch_protection
        ),
    );
    Ok(signals)
}

/// The subset of the OpenSSF Scorecard JSON report `parse_scorecard` folds. `serde`
/// ignores the many other fields (`date`, `scorecard`, per-check `reason`/`details`).
#[derive(Debug, Deserialize)]
struct ScorecardDoc {
    #[serde(default)]
    score: f64,
    #[serde(default)]
    repo: ScorecardRepo,
    #[serde(default)]
    checks: Vec<ScorecardCheck>,
}

#[derive(Debug, Default, Deserialize)]
struct ScorecardRepo {
    #[serde(default)]
    name: String,
}

#[derive(Debug, Deserialize)]
struct ScorecardCheck {
    #[serde(default)]
    name: String,
    #[serde(default)]
    score: i64,
}

// ─── Real znippy-sealed build-provenance verifier (feature `provenance-verify`) ──
//
// The A3 surface above only POLICY-SCORES an `Attestation` it is handed. This is
// the missing half of the build-provenance chain (Tier-1 bug #2): OPEN the
// `<artifact>.build-provenance.znippy` blob nornir's `build::seal::dump_beside_artifact`
// writes, cryptographically VERIFY it, and PRODUCE the `Attestation` `evaluate`
// consumes — so a verdict runs on a genuinely-verified attestation, never an
// assumed one.
//
// Format (matched byte-for-byte to nornir's seal): the archive is a znippy archive
// holding one entry `attestation.json` = a serialized
// `nornir_build_thing::provenance::Attestation` (record + 32-byte digest + a
// detached CMS `Seal`). Verification: (1) recompute the record's canonical SHA-256
// and confirm it equals the stored digest (record-tamper check); (2) verify the
// detached CMS `SignedData` over that digest, chaining the signer cert to a trusted
// root (`znippy_common::sign::verify_digest`); (3) confirm the artifact's
// `blob_sha256` is one the provenance attests (content-addressing). Only then is an
// `Attestation` produced.
#[cfg(feature = "provenance-verify")]
mod verify {
    use std::path::Path;

    use anyhow::{anyhow, Context, Result};
    use nornir_build_thing::provenance::Attestation as SealedAttestation;
    use znippy_common::archive::{ZnippyArchive, ZnippyReader};
    use znippy_common::sign::{dsse_verify, verify_digest, CertStore, DsseEnvelope, VerifiedSigner};

    use super::{Attestation, ProvenanceRequirement};
    use crate::core::{Finding, Severity};
    use crate::scan::Provenance;

    /// The suffix nornir's `seal::dump_beside_artifact` appends to the artifact name.
    pub const SEALED_PROVENANCE_SUFFIX: &str = ".build-provenance.znippy";
    /// The single entry inside the sealed archive (nornir stages `attestation.json`).
    pub const ATTESTATION_ENTRY: &str = "attestation.json";
    /// SLSA build level a verified znippy-sealed build-provenance attests: L2 — a
    /// scripted/hosted build emitting a **signed, content-addressed** provenance
    /// record. This is a floor (the seal proves builder identity + integrity, not
    /// the hermetic-build / non-falsifiable guarantees SLSA L3 additionally wants),
    /// so a policy requiring L3 still (correctly) fails a merely-sealed artifact.
    pub const VERIFIED_SLSA_LEVEL: u8 = 2;

    /// The verdict of verifying one sealed build-provenance blob — the row the
    /// `build_provenance` warehouse ledger records.
    #[derive(Debug, Clone)]
    pub struct ProvenanceVerification {
        /// Whether the seal genuinely verified (signature + chain + digest + content).
        pub verified: bool,
        /// The provenance record's UUID (empty when the blob could not be read).
        pub artifact_id: String,
        /// The artifact content hash the verdict is about (the checked
        /// `blob_sha256`, else the record's primary artifact hash).
        pub blob_sha256: String,
        /// The repo the record claims (for warehouse scoping).
        pub repo: String,
        /// The verified signer identity (CN, else full subject); `None` when unverified.
        pub signer: Option<String>,
        /// A clear human reason — "verified …" or the exact reason it was rejected.
        pub reason: String,
        /// The genuinely-verified attestation `evaluate` may consume. `Some` **iff**
        /// `verified`; `None` for a tampered / unverifiable blob (never an assumed one).
        pub attestation: Option<Attestation>,
    }

    impl ProvenanceVerification {
        fn unreadable(reason: String) -> Self {
            Self {
                verified: false,
                artifact_id: String::new(),
                blob_sha256: String::new(),
                repo: String::new(),
                signer: None,
                reason,
                attestation: None,
            }
        }
    }

    /// Open the sealed archive and parse its `attestation.json`. Tolerates a
    /// path-prefixed entry (nornir's `compress_dir` stages the file under a dir, so
    /// the stored name may carry that prefix) by matching on the basename.
    fn read_sealed_attestation(archive_path: &Path) -> Result<SealedAttestation> {
        let archive = ZnippyArchive::open(archive_path).with_context(|| {
            format!("open sealed provenance archive {}", archive_path.display())
        })?;
        let bytes = match archive.extract_file(ATTESTATION_ENTRY) {
            Ok(b) => b,
            Err(_) => {
                let files = archive
                    .list_files()
                    .context("list sealed archive entries")?;
                let entry = files
                    .iter()
                    .find(|p| p.rsplit('/').next() == Some(ATTESTATION_ENTRY))
                    .ok_or_else(|| {
                        anyhow!("sealed archive carries no `{ATTESTATION_ENTRY}` entry")
                    })?;
                archive
                    .extract_file(entry)
                    .with_context(|| format!("extract {entry} from sealed archive"))?
            }
        };
        let text = String::from_utf8(bytes).context("attestation.json is not valid UTF-8")?;
        SealedAttestation::from_json(&text).context("parse sealed attestation")
    }

    /// Open + cryptographically verify a znippy-sealed
    /// `<artifact>.build-provenance.znippy`. `trusted_roots` are DER CA certs the
    /// signer must chain to. When `expected_blob_sha256` is given, the artifact's
    /// content hash must be one the provenance attests (content-addressing). Never
    /// panics / errors: an unreadable / tampered / untrusted / mismatched blob is a
    /// clear un-verified verdict, not an `Err`.
    pub fn verify_sealed_provenance(
        archive_path: &Path,
        trusted_roots: &[Vec<u8>],
        expected_blob_sha256: Option<&str>,
    ) -> ProvenanceVerification {
        let sealed = match read_sealed_attestation(archive_path) {
            Ok(a) => a,
            Err(e) => {
                return ProvenanceVerification::unreadable(format!(
                    "cannot read sealed provenance: {e:#}"
                ))
            }
        };

        let artifact_id = sealed.record.id.clone();
        let repo = sealed.record.repo.clone();
        let blob_sha256 = expected_blob_sha256
            .map(str::to_string)
            .or_else(|| sealed.record.artifacts.first().map(|a| a.sha256.clone()))
            .unwrap_or_default();
        let verdict =
            |verified: bool, signer: Option<String>, reason: String, att: Option<Attestation>| {
                ProvenanceVerification {
                    verified,
                    artifact_id: artifact_id.clone(),
                    blob_sha256: blob_sha256.clone(),
                    repo: repo.clone(),
                    signer,
                    reason,
                    attestation: att,
                }
            };

        // 1. The stored digest must still match the record (record-tamper check).
        match sealed.digest_matches_record() {
            Ok(true) => {}
            Ok(false) => {
                return verdict(
                    false,
                    None,
                    "attestation digest does not match its record (record tampered)".into(),
                    None,
                )
            }
            Err(e) => {
                return verdict(
                    false,
                    None,
                    format!("cannot recompute attestation digest: {e:#}"),
                    None,
                )
            }
        }

        // 2. It must carry a cryptographic seal — CMS (`seal`) OR the additive DSSE
        //    (`dsse`, the in-toto SLSA envelope). An unsealed record is unverifiable.
        if sealed.seal.is_none() && sealed.dsse.is_none() {
            return verdict(
                false,
                None,
                "attestation carries no cryptographic seal (unverifiable)".into(),
                None,
            );
        }

        // 3. Verify + chain the signer to a root. modgunn already reads a Rekor
        //    `content.envelope.signatures[].keyid`; verifying our OWN DSSE envelope
        //    here IS the producer↔verifier agreement.
        let store = match CertStore::from_der_certs(trusted_roots) {
            Ok(s) => s,
            Err(e) => {
                return verdict(
                    false,
                    None,
                    format!("cannot load trusted roots: {e:#}"),
                    None,
                )
            }
        };
        let signer_id: VerifiedSigner = if let Some(dsse) = sealed.dsse.as_ref() {
            // DSSE: the envelope's payload MUST be the SLSA statement this record
            // derives (else a genuine envelope was spliced onto a different record),
            // then the DSSE signature must verify against the carried signer cert.
            let env = match DsseEnvelope::from_json(&dsse.envelope_json) {
                Ok(e) => e,
                Err(e) => {
                    return verdict(false, None, format!("cannot parse DSSE envelope: {e:#}"), None)
                }
            };
            let expected = match sealed.record.slsa_statement_bytes() {
                Ok(b) => b,
                Err(e) => {
                    return verdict(
                        false,
                        None,
                        format!("cannot recompute SLSA statement: {e:#}"),
                        None,
                    )
                }
            };
            if env.payload != expected {
                return verdict(
                    false,
                    None,
                    "DSSE envelope payload does not match the record's SLSA statement \
                     (record or envelope tampered)"
                        .into(),
                    None,
                );
            }
            match dsse_verify(&env, &dsse.signer_certs, &store) {
                Ok(mut v) if !v.is_empty() => v.remove(0),
                Ok(_) => {
                    return verdict(
                        false,
                        None,
                        "DSSE envelope produced no verified signer".into(),
                        None,
                    )
                }
                Err(e) => {
                    return verdict(
                        false,
                        None,
                        format!("DSSE signature/chain verification failed: {e:#}"),
                        None,
                    )
                }
            }
        } else {
            let seal = sealed.seal.as_ref().expect("checked present above");
            match verify_digest(&sealed.digest, &seal.cms_der, &store) {
                Ok(id) => id,
                Err(e) => {
                    return verdict(
                        false,
                        None,
                        format!("seal signature/chain verification failed: {e:#}"),
                        None,
                    )
                }
            }
        };
        // `verify_digest` returns a `VerifiedSigner` (the identity PLUS the signed
        // attributes it actually verified), so the identity is read through the
        // accessors rather than as bare fields.
        let signer_label = if signer_id.common_name().is_empty() {
            signer_id.subject().to_string()
        } else {
            signer_id.common_name().to_string()
        };

        // 4. Content-addressing: the artifact's blob hash must be one this
        //    provenance attests (a genuine seal over an UNRELATED artifact is not a
        //    verdict for THIS artifact).
        if let Some(want) = expected_blob_sha256 {
            let attested = sealed
                .record
                .artifacts
                .iter()
                .any(|a| a.sha256.eq_ignore_ascii_case(want));
            if !attested {
                return verdict(
                    false,
                    Some(signer_label),
                    format!(
                        "content digest {want} is not attested by this provenance \
                         (no matching artifact hash)"
                    ),
                    None,
                );
            }
        }

        // Genuinely verified → produce the attestation `evaluate` consumes.
        let attestation = Attestation {
            signer_identity: Some(signer_label.clone()),
            slsa_level: VERIFIED_SLSA_LEVEL,
            source_repo: Some(sealed.record.repo.clone()),
            source_sha: Some(sealed.record.inputs.vcs_rev.clone()),
        };
        verdict(
            true,
            Some(signer_label.clone()),
            format!(
                "verified: znippy-sealed build-provenance signed by `{signer_label}`; \
                 record digest + content hash confirmed"
            ),
            Some(attestation),
        )
    }

    impl ProvenanceRequirement {
        /// Open + cryptographically VERIFY a znippy-sealed
        /// `<artifact>.build-provenance.znippy`, then run [`Self::evaluate`] on the
        /// genuinely-verified attestation. When the seal cannot be verified (absent,
        /// tampered, untrusted signer, or a content-hash mismatch) the artifact is
        /// treated as UNSIGNED with a single clear finding — policy is never scored
        /// against unverified/assumed provenance. Returns the Engine-A [`Provenance`],
        /// the [`Finding`]s, and the raw [`ProvenanceVerification`] verdict (which the
        /// `build_provenance` warehouse ledger records).
        pub fn evaluate_sealed(
            &self,
            archive_path: &Path,
            trusted_roots: &[Vec<u8>],
            expected_blob_sha256: Option<&str>,
        ) -> (Provenance, Vec<Finding>, ProvenanceVerification) {
            let v = verify_sealed_provenance(archive_path, trusted_roots, expected_blob_sha256);
            if v.verified {
                let (prov, findings) = self.evaluate(v.attestation.as_ref());
                (prov, findings, v)
            } else {
                let finding = Finding {
                    severity: Severity::High,
                    id: "MODGUNN-PROV-UNVERIFIABLE".into(),
                    summary: v.reason.clone(),
                };
                (Provenance::unsigned(), vec![finding], v)
            }
        }
    }

    /// Mint a REAL znippy-sealed `<name>.build-provenance.znippy` blob + its trusted
    /// CA DER root, for driving the `verify-provenance` verb end-to-end. Seals the
    /// SAME way nornir does (an `nornir_build_thing` attestation → detached CMS via
    /// `znippy_common::sign` → one-entry (`attestation.json`) znippy archive), so a
    /// caller gets a blob the production verifier genuinely accepts.
    ///
    /// Returns `(blob_path, ca_der_root, artifact_sha256)`. `signer_cn` becomes the
    /// verified signer identity (e.g. `"nordisk-ci"` to satisfy `--trusted-signer`).
    /// Exposed under `test-support` so an OUT-OF-CRATE integration test (the
    /// `tests/cli_provenance.rs` subprocess harness) can reuse the exact fixture the
    /// in-crate `verify_tests` seal, instead of each re-inventing a private one.
    #[cfg(any(test, feature = "test-support"))]
    pub fn seal_provenance_fixture(
        dir: &Path,
        signer_cn: &str,
    ) -> Result<(std::path::PathBuf, Vec<u8>, String)> {
        use nornir_build_thing::provenance::{BuildProvenance, HostInfo, Outcome, Seal, Toolchain};
        use nornir_build_thing::variant::{BuildVariant, Profile};
        use znippy_common::meta_sink_append::create_archive_to_vec;
        use znippy_common::sign::dev::{mint_ca, new_ed25519_signer};
        use znippy_common::sign::ArchiveSigner;

        std::fs::create_dir_all(dir.join("target/release")).context("create fixture target dir")?;
        let art = dir.join("target/release/holger-full");
        std::fs::write(&art, b"sealed holger-full bytes").context("write fixture artifact")?;

        let variant = BuildVariant {
            name: "full".to_string(),
            package: Some("holger-full".to_string()),
            bin: Some("holger-full".to_string()),
            features: vec!["full".to_string()],
            profile: Profile::Release,
            ..Default::default()
        };
        let att = BuildProvenance::capture(
            &variant,
            "holger",
            dir,
            Toolchain::default(),
            HostInfo::default(),
            Outcome::Success,
            &[art],
        )
        .context("capture build provenance")?
        .attest()
        .context("attest build provenance")?;

        let (ca_key, ca_der) = mint_ca("modgunn-verify-test CA").context("mint CA")?;
        let signer = new_ed25519_signer(&ca_key, &ca_der, signer_cn).context("mint signer")?;
        let cms = signer.sign_digest(&att.digest).context("sign digest")?;

        let art_sha = att.record.artifacts[0].sha256.clone();
        let mut sealed = att;
        sealed.seal = Some(Seal {
            alg: "ed25519".to_string(),
            signer_cn: signer_cn.to_string(),
            cms_der: cms,
        });

        let json = sealed.to_json().context("serialize sealed attestation")?;
        let (bytes, _report) =
            create_archive_to_vec(&[(ATTESTATION_ENTRY.to_string(), json.into_bytes())], 3)
                .context("pack sealed archive")?;
        let blob = dir.join(format!("holger-full{SEALED_PROVENANCE_SUFFIX}"));
        std::fs::write(&blob, &bytes).context("write sealed blob")?;
        Ok((blob, ca_der, art_sha))
    }

    /// Like [`seal_provenance_fixture`] but seals with a **DSSE** envelope over the
    /// in-toto SLSA statement (the SLSA-interop shape) in the additive
    /// `Attestation::dsse` slot, so a test can prove modgunn verifies our DSSE
    /// envelope end-to-end. Seals the same way nornir's `ZnippySealer` DSSE mode
    /// does — `dsse_sign` over `BuildProvenance::slsa_statement_bytes`, carrying the
    /// signer cert — without depending on nornir (modgunn is upstream of it).
    #[cfg(any(test, feature = "test-support"))]
    pub fn seal_provenance_fixture_dsse(
        dir: &Path,
        signer_cn: &str,
    ) -> Result<(std::path::PathBuf, Vec<u8>, String)> {
        use nornir_build_thing::provenance::{
            BuildProvenance, DsseSeal, HostInfo, Outcome, Toolchain, DSSE_PAYLOAD_TYPE_INTOTO,
        };
        use nornir_build_thing::variant::{BuildVariant, Profile};
        use znippy_common::meta_sink_append::create_archive_to_vec;
        use znippy_common::sign::dev::{mint_ca, new_ed25519_signer};
        use znippy_common::sign::{dsse_sign, ArchiveSigner};

        std::fs::create_dir_all(dir.join("target/release")).context("create fixture target dir")?;
        let art = dir.join("target/release/holger-full");
        std::fs::write(&art, b"sealed holger-full bytes").context("write fixture artifact")?;

        let variant = BuildVariant {
            name: "full".to_string(),
            package: Some("holger-full".to_string()),
            bin: Some("holger-full".to_string()),
            features: vec!["full".to_string()],
            profile: Profile::Release,
            ..Default::default()
        };
        let att = BuildProvenance::capture(
            &variant,
            "holger",
            dir,
            Toolchain::default(),
            HostInfo::default(),
            Outcome::Success,
            &[art],
        )
        .context("capture build provenance")?
        .attest()
        .context("attest build provenance")?;

        let (ca_key, ca_der) = mint_ca("modgunn-dsse-test CA").context("mint CA")?;
        let signer = new_ed25519_signer(&ca_key, &ca_der, signer_cn).context("mint signer")?;

        let payload = att.record.slsa_statement_bytes().context("SLSA statement bytes")?;
        let signer_ref: &dyn ArchiveSigner = &signer;
        let env =
            dsse_sign(DSSE_PAYLOAD_TYPE_INTOTO, &payload, &[signer_ref]).context("DSSE sign")?;

        let art_sha = att.record.artifacts[0].sha256.clone();
        let mut sealed = att;
        sealed.dsse = Some(DsseSeal {
            alg: "ed25519".to_string(),
            signer_cn: signer_cn.to_string(),
            payload_type: DSSE_PAYLOAD_TYPE_INTOTO.to_string(),
            envelope_json: env.to_json().context("serialize DSSE envelope")?,
            signer_certs: vec![signer_ref.certificate_der().to_vec()],
        });

        let json = sealed.to_json().context("serialize sealed attestation")?;
        let (bytes, _report) =
            create_archive_to_vec(&[(ATTESTATION_ENTRY.to_string(), json.into_bytes())], 3)
                .context("pack sealed DSSE archive")?;
        let blob = dir.join(format!("holger-full{SEALED_PROVENANCE_SUFFIX}"));
        std::fs::write(&blob, &bytes).context("write sealed blob")?;
        Ok((blob, ca_der, art_sha))
    }
}

#[cfg(feature = "provenance-verify")]
pub use verify::{
    verify_sealed_provenance, ProvenanceVerification, ATTESTATION_ENTRY, SEALED_PROVENANCE_SUFFIX,
    VERIFIED_SLSA_LEVEL,
};

#[cfg(all(feature = "provenance-verify", any(test, feature = "test-support")))]
pub use verify::{seal_provenance_fixture, seal_provenance_fixture_dsse};

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

    /// Real base64-encoded Rekor bodies (produced by an actual encoder, not hand-
    /// written), so the pure-Rust decoder is exercised on genuine input. Both body
    /// kinds are covered, and the two edges that decide whether this collector is
    /// honest: an entry with NO inclusion proof is recorded as self-asserted rather
    /// than trusted, and slsa_level stays 0 (log presence says nothing about how the
    /// artifact was built).
    #[test]
    fn rekor_entries_decode_both_kinds_and_grade_the_evidence() {
        let json = format!(
            r#"{{
              "uuid-1": {{ "body": "eyJhcGlWZXJzaW9uIjogIjAuMC4xIiwgImtpbmQiOiAiaGFzaGVkcmVrb3JkIiwgInNwZWMiOiB7ImRhdGEiOiB7Imhhc2giOiB7ImFsZ29yaXRobSI6ICJzaGEyNTYiLCAidmFsdWUiOiAiQUJBQkFCQUJBQkFCQUJBQkFCQUJBQkFCQUJBQkFCQUJBQkFCQUJBQkFCQUJBQkFCQUJBQkFCQUJBQkFCQUJBQiJ9fSwgInNpZ25hdHVyZSI6IHsiY29udGVudCI6ICJjMmxuIiwgInB1YmxpY0tleSI6IHsiaWRlbnRpdHkiOiAiaHR0cHM6Ly9naXRodWIuY29tL25vcmRpc2svbW9kZ3Vubi8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbEByZWZzL3RhZ3MvdjEifX19fQ==", "integratedTime": 1700000000, "logIndex": 42,
                           "verification": {{ "inclusionProof": {{ "logIndex": 42 }},
                                              "signedEntryTimestamp": "MEUCIQ==" }} }},
              "uuid-2": {{ "body": "eyJhcGlWZXJzaW9uIjogIjAuMC4xIiwgImtpbmQiOiAiaW50b3RvIiwgInNwZWMiOiB7ImNvbnRlbnQiOiB7InBheWxvYWRIYXNoIjogeyJhbGdvcml0aG0iOiAic2hhMjU2IiwgInZhbHVlIjogImNkY2RjZGNkY2RjZGNkY2RjZGNkY2RjZGNkY2RjZGNkY2RjZGNkY2RjZGNkY2RjZGNkY2RjZGNkY2RjZGNkY2QifSwgImVudmVsb3BlIjogeyJzaWduYXR1cmVzIjogW3sia2V5aWQiOiAiZnVsY2lvLWNpQGV4YW1wbGUifV19fX19", "integratedTime": 1700000001, "logIndex": 43,
                           "verification": {{}} }},
              "uuid-3": {{ "body": "!!!not base64!!!", "logIndex": 44 }}
            }}"#
        );
        let mut entries = parse_rekor_entries(&json).unwrap();
        entries.sort_by_key(|e| e.log_index);
        assert_eq!(
            entries.len(),
            2,
            "the un-decodable body is SKIPPED, not landed with empty fields"
        );

        // ── hashedrekord: fully-evidenced ──
        let hr = &entries[0];
        assert_eq!(hr.kind, "hashedrekord");
        assert_eq!(
            hr.artifact_sha256,
            "ab".repeat(32),
            "digest lowercased for the join"
        );
        assert_eq!(hr.log_index, 42);
        assert_eq!(hr.integrated_time, 1_700_000_000);
        assert_eq!(
            hr.signer_identity.as_deref(),
            Some("https://github.com/nordisk/modgunn/.github/workflows/release.yml@refs/tags/v1")
        );
        assert!(hr.has_inclusion_proof && hr.has_signed_timestamp);
        assert!(
            hr.findings().is_empty(),
            "a fully-evidenced entry is clean: {:?}",
            hr.findings()
        );
        assert_eq!(
            hr.attestation().slsa_level,
            0,
            "log presence is NOT a SLSA level — never an unearned claim"
        );
        assert_eq!(hr.attestation().signer_identity, hr.signer_identity);

        // ── intoto: no verification block ⇒ self-asserted ──
        let it = &entries[1];
        assert_eq!(it.kind, "intoto");
        assert_eq!(
            it.artifact_sha256,
            "cd".repeat(32),
            "intoto digest read from content.payloadHash"
        );
        assert_eq!(it.signer_identity.as_deref(), Some("fulcio-ci@example"));
        assert!(!it.has_inclusion_proof && !it.has_signed_timestamp);
        let it_findings = it.findings();
        let ids: Vec<&str> = it_findings.iter().map(|f| f.id.as_str()).collect();
        assert_eq!(
            ids,
            vec![
                "MODGUNN-REKOR-NO-INCLUSION-PROOF",
                "MODGUNN-REKOR-NO-SIGNED-TIMESTAMP"
            ],
            "an unproven entry is graded, not trusted"
        );
        assert_eq!(it_findings[0].severity, Severity::High);
    }

    /// The base64 decoder is the collector's trust boundary — a body that decodes
    /// WRONG would parse into a plausible but false record. Truncated groups, data
    /// after padding and stray characters all yield `None` rather than partial bytes.
    #[test]
    fn base64_decoder_rejects_malformed_input_rather_than_partially_decoding() {
        assert_eq!(base64_decode("aGVsbG8=").unwrap(), b"hello");
        assert_eq!(base64_decode("aGVsbG8h").unwrap(), b"hello!");
        assert_eq!(base64_decode("").unwrap(), Vec::<u8>::new());
        assert_eq!(
            base64_decode("aGVs bG8=\n").unwrap(),
            b"hello",
            "whitespace ignored"
        );
        assert_eq!(
            base64_decode("a"),
            None,
            "a lone 6-bit group is truncated, not 0 bytes"
        );
        assert_eq!(
            base64_decode("aGVsbG8=X"),
            None,
            "data after padding is malformed"
        );
        assert_eq!(
            base64_decode("aGVs*G8="),
            None,
            "a stray character is rejected"
        );
        assert_eq!(base64_decode("!!!not base64!!!"), None);
        // Non-zero trailing bits mean the encoder disagreed with us about the length.
        assert_eq!(base64_decode("aGVsbG9="), None);
    }

    #[test]
    fn missing_attestation_is_high_and_unsigned() {
        let req = ProvenanceRequirement {
            require_min_slsa: 2,
            ..Default::default()
        };
        let (prov, findings) = req.evaluate(None);
        assert!(!prov.signed);
        assert_eq!(findings.len(), 1);
        assert_eq!(findings[0].id, "MODGUNN-PROV-NO-ATTESTATION");
    }

    #[test]
    fn trusted_signer_at_required_slsa_passes_clean() {
        let req = ProvenanceRequirement {
            require_min_slsa: 2,
            trusted_signers: vec!["nordisk-ci".into()],
            expected_source_repo: Some("git.gunnar.rs/nordisk/x".into()),
            // Pin the commit too — and prove case-insensitive matching (DEADBEEF).
            expected_source_sha: Some("DEADBEEF".into()),
        };
        let att = Attestation {
            signer_identity: Some("nordisk-ci".into()),
            slsa_level: 3,
            source_repo: Some("git.gunnar.rs/nordisk/x".into()),
            source_sha: Some("deadbeef".into()),
        };
        let (prov, findings) = req.evaluate(Some(&att));
        assert!(prov.signed, "clean attestation is treated as signed");
        assert_eq!(prov.signer.as_deref(), Some("nordisk-ci"));
        assert!(
            findings.is_empty(),
            "commit pin matches case-insensitively: {findings:?}"
        );
    }

    #[test]
    fn expected_commit_mismatch_and_absence_each_block() {
        let req = ProvenanceRequirement {
            expected_source_sha: Some("cafebabe".into()),
            ..Default::default()
        };
        // Wrong commit on the artifact → COMMIT-MISMATCH.
        let wrong = Attestation {
            signer_identity: Some("nordisk-ci".into()),
            slsa_level: 2,
            source_repo: Some("r".into()),
            source_sha: Some("deadbeef".into()),
        };
        let (prov, findings) = req.evaluate(Some(&wrong));
        assert!(!prov.signed, "a commit-pin miss fails the artifact");
        assert!(
            findings
                .iter()
                .any(|f| f.id == "MODGUNN-PROV-COMMIT-MISMATCH"),
            "{findings:?}"
        );

        // No commit attested at all, but one is required → still MISMATCH (not silent).
        let none = Attestation {
            source_sha: None,
            ..wrong.clone()
        };
        let (_p, findings) = req.evaluate(Some(&none));
        assert!(
            findings
                .iter()
                .any(|f| f.id == "MODGUNN-PROV-COMMIT-MISMATCH"),
            "{findings:?}"
        );
    }

    #[test]
    fn untrusted_signer_low_slsa_and_source_mismatch_each_flag() {
        let req = ProvenanceRequirement {
            require_min_slsa: 3,
            trusted_signers: vec!["nordisk-ci".into()],
            expected_source_repo: Some("good/repo".into()),
            ..Default::default()
        };
        let att = Attestation {
            signer_identity: Some("attacker".into()),
            slsa_level: 1,
            source_repo: Some("evil/fork".into()),
            source_sha: None,
        };
        let (prov, findings) = req.evaluate(Some(&att));
        assert!(!prov.signed);
        let ids: Vec<&str> = findings.iter().map(|f| f.id.as_str()).collect();
        assert!(ids.contains(&"MODGUNN-PROV-SLSA-TOO-LOW"));
        assert!(ids.contains(&"MODGUNN-PROV-UNTRUSTED-SIGNER"));
        assert!(ids.contains(&"MODGUNN-PROV-SOURCE-MISMATCH"));
    }

    #[test]
    fn attestation_delta_flags_signer_change_and_drop() {
        let a = Attestation::signed_by("ci-a", 3);
        let b = Attestation::signed_by("ci-b", 3);
        let changed = attestation_delta(Some(&a), Some(&b));
        assert_eq!(changed[0].id, "MODGUNN-PROV-SIGNER-CHANGED");

        let dropped = attestation_delta(Some(&a), None);
        assert_eq!(dropped[0].id, "MODGUNN-PROV-ATTESTATION-DROPPED");
        assert_eq!(dropped[0].severity, Severity::High);
    }

    #[test]
    fn attestation_delta_flags_commit_change_on_same_repo_only() {
        let at = |sha: &str| Attestation {
            signer_identity: Some("ci".into()),
            slsa_level: 2,
            source_repo: Some("nordisk/x".into()),
            source_sha: Some(sha.into()),
        };
        // Same repo, different commit → COMMIT-CHANGED (a rebuild-from-elsewhere signal).
        let ids: Vec<String> = attestation_delta(Some(&at("aaaa")), Some(&at("bbbb")))
            .into_iter()
            .map(|f| f.id)
            .collect();
        assert!(
            ids.contains(&"MODGUNN-PROV-COMMIT-CHANGED".to_string()),
            "{ids:?}"
        );
        assert!(
            !ids.contains(&"MODGUNN-PROV-SOURCE-CHANGED".to_string()),
            "repo held: {ids:?}"
        );

        // Same commit (case-insensitive) → no finding.
        assert!(attestation_delta(Some(&at("ABCD")), Some(&at("abcd"))).is_empty());

        // A repo move already carries SOURCE-CHANGED; the commit diff is suppressed as
        // redundant noise (only the stronger repo-change signal fires).
        let moved = Attestation {
            source_repo: Some("attacker/fork".into()),
            ..at("bbbb")
        };
        let ids: Vec<String> = attestation_delta(Some(&at("aaaa")), Some(&moved))
            .into_iter()
            .map(|f| f.id)
            .collect();
        assert!(
            ids.contains(&"MODGUNN-PROV-SOURCE-CHANGED".to_string()),
            "{ids:?}"
        );
        assert!(
            !ids.contains(&"MODGUNN-PROV-COMMIT-CHANGED".to_string()),
            "no redundant commit noise: {ids:?}"
        );

        // First-time commit appearance (None → Some) is not a "change".
        let first = Attestation {
            source_sha: None,
            ..at("aaaa")
        };
        let ids: Vec<String> = attestation_delta(Some(&first), Some(&at("aaaa")))
            .into_iter()
            .map(|f| f.id)
            .collect();
        assert!(
            !ids.contains(&"MODGUNN-PROV-COMMIT-CHANGED".to_string()),
            "{ids:?}"
        );
    }

    #[test]
    fn scorecard_low_and_unmaintained_warn() {
        let s = ScorecardSignals {
            subject: "left-pad".into(),
            overall: 3.2,
            maintained: false,
            branch_protection: false,
            signed_releases: false,
            dangerous_workflow: false,
        };
        let f = s.findings();
        let ids: Vec<&str> = f.iter().map(|x| x.id.as_str()).collect();
        assert!(ids.contains(&"MODGUNN-SCORECARD-LOW"));
        assert!(ids.contains(&"MODGUNN-SCORECARD-UNMAINTAINED"));
    }

    /// RED-when-broken: a real OpenSSF Scorecard JSON report with a
    /// `Dangerous-Workflow` check scored 0 (a dangerous CI pattern was found) MUST
    /// fold to a **High** `MODGUNN-SCORECARD-DANGEROUS-WORKFLOW` finding, and a
    /// failing `Branch-Protection` to a Low finding. If the score→severity mapping
    /// (or Scorecard's inverted dangerous-workflow scale) is broken, the label /
    /// severity assertion fires red.
    #[test]
    fn parse_scorecard_maps_dangerous_workflow_to_high() {
        #[cfg(feature = "testmatrix")]
        let _guard = crate::testmatrix_test_guard(); // serialize the shared drain buffer
        #[cfg(feature = "testmatrix")]
        let _ = nornir_testmatrix::drain_functional_rows(); // clean buffer

        // Shape mirrors `scorecard --format json`: aggregate `score`, `repo.name`,
        // and a `checks[]` of `{name, score, reason}` on Scorecard's 0..=10 scale
        // (higher = better; -1 = inconclusive).
        let json = r#"{
            "date": "2026-07-14",
            "repo": { "name": "github.com/acme/widget", "commit": "deadbeef" },
            "scorecard": { "version": "v5.0.0" },
            "score": 3.4,
            "checks": [
                { "name": "Maintained", "score": 8, "reason": "30 commits in 90d" },
                { "name": "Branch-Protection", "score": 2, "reason": "no required reviews" },
                { "name": "Signed-Releases", "score": -1, "reason": "no releases" },
                { "name": "Dangerous-Workflow", "score": 0, "reason": "script injection in CI" }
            ]
        }"#;
        let s = parse_scorecard(json).unwrap();

        assert_eq!(s.subject, "github.com/acme/widget");
        assert_eq!(s.overall, 3.4);
        assert!(s.maintained, "Maintained 8 ≥ 5 passes");
        assert!(!s.branch_protection, "Branch-Protection 2 < 5 fails");
        assert!(!s.signed_releases, "inconclusive (-1) is not a pass");
        assert!(
            s.dangerous_workflow,
            "Dangerous-Workflow 0 ⇒ a dangerous pattern was found"
        );

        // The load-bearing severity/label mapping.
        let dw = s
            .findings()
            .into_iter()
            .find(|f| f.id == "MODGUNN-SCORECARD-DANGEROUS-WORKFLOW")
            .expect("dangerous-workflow finding present");
        assert_eq!(
            dw.severity,
            Severity::High,
            "dangerous workflow is a High signal"
        );
        assert_eq!(
            dw.severity.as_str(),
            "High",
            "canonical severity label consumers render"
        );

        let ids: Vec<String> = s.findings().into_iter().map(|f| f.id).collect();
        assert!(
            ids.contains(&"MODGUNN-SCORECARD-NO-BRANCH-PROTECTION".to_string()),
            "{ids:?}"
        );

        #[cfg(feature = "testmatrix")]
        {
            use nornir_testmatrix::{drain_functional_rows, status};
            let rows = drain_functional_rows();
            let hit = rows
                .iter()
                .find(|r| r.suite == "modgunn/scorecard-ingest" && r.test_name == "parse_scorecard")
                .unwrap_or_else(|| panic!("no scorecard ingest row: {rows:#?}"));
            assert!(
                status::is_green(&hit.status),
                "parsed OK ⇒ GREEN row: {hit:#?}"
            );
            assert_eq!(
                hit.aspect, "functional",
                "ingest markers ride the `functional` aspect"
            );
        }
    }

    /// Guards the inverted Dangerous-Workflow scale (RED if the `< 10` comparison is
    /// flipped): a clean report (score 10) must NOT emit the High finding.
    #[test]
    fn parse_scorecard_clean_workflow_not_flagged() {
        let json = r#"{
            "repo": { "name": "github.com/acme/safe" },
            "score": 8.1,
            "checks": [
                { "name": "Maintained", "score": 10 },
                { "name": "Branch-Protection", "score": 9 },
                { "name": "Signed-Releases", "score": 10 },
                { "name": "Dangerous-Workflow", "score": 10 }
            ]
        }"#;
        let s = parse_scorecard(json).unwrap();
        assert!(!s.dangerous_workflow, "score 10 ⇒ no dangerous workflow");
        assert!(
            s.branch_protection && s.signed_releases && s.maintained,
            "all high-signal checks pass"
        );
        let ids: Vec<String> = s.findings().into_iter().map(|f| f.id).collect();
        assert!(
            ids.is_empty(),
            "a clean, well-scored project has no scorecard findings: {ids:?}"
        );
    }
}

// Round-trip / tamper tests for the real seal verifier. Seals a fixture exactly
// the way nornir does — an `nornir_build_thing` attestation, a detached CMS via
// `znippy_common::sign`, packed into a znippy archive — then verifies it back.
#[cfg(all(test, feature = "provenance-verify"))]
mod verify_tests {
    use super::verify::{
        verify_sealed_provenance, ATTESTATION_ENTRY, SEALED_PROVENANCE_SUFFIX, VERIFIED_SLSA_LEVEL,
    };
    use super::ProvenanceRequirement;

    use nornir_build_thing::provenance::{
        Attestation as SealedAttestation, BuildProvenance, HostInfo, Outcome, Seal, Toolchain,
    };
    use nornir_build_thing::variant::{BuildVariant, Profile};
    use std::path::PathBuf;
    use std::time::{SystemTime, UNIX_EPOCH};
    use znippy_common::meta_sink_append::create_archive_to_vec;
    use znippy_common::sign::dev::{mint_ca, new_ed25519_signer};
    use znippy_common::sign::ArchiveSigner;

    fn scratch_dir(tag: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let dir = std::env::temp_dir().join(format!("modgunn-prov-{tag}-{nanos}"));
        std::fs::create_dir_all(&dir).unwrap();
        dir
    }

    /// Capture + seal a build-provenance for a fake `holger-full` artifact.
    /// Returns `(scratch_dir, signed_attestation, ca_der, artifact_sha256, signer_cn)`.
    fn seal_fixture(cn: &str) -> (PathBuf, SealedAttestation, Vec<u8>, String) {
        let dir = scratch_dir("seal");
        std::fs::create_dir_all(dir.join("target/release")).unwrap();
        let art = dir.join("target/release/holger-full");
        std::fs::write(&art, b"sealed holger-full bytes").unwrap();

        let variant = BuildVariant {
            name: "full".to_string(),
            package: Some("holger-full".to_string()),
            bin: Some("holger-full".to_string()),
            features: vec!["full".to_string()],
            profile: Profile::Release,
            ..Default::default()
        };
        let att = BuildProvenance::capture(
            &variant,
            "holger",
            &dir,
            Toolchain::default(),
            HostInfo::default(),
            Outcome::Success,
            &[art],
        )
        .unwrap()
        .attest()
        .unwrap();

        let (ca_key, ca_der) = mint_ca("modgunn-verify-test CA").unwrap();
        let signer = new_ed25519_signer(&ca_key, &ca_der, cn).unwrap();
        let cms = signer.sign_digest(&att.digest).unwrap();

        let art_sha = att.record.artifacts[0].sha256.clone();
        let mut sealed = att;
        sealed.seal = Some(Seal {
            alg: "ed25519".to_string(),
            signer_cn: cn.to_string(),
            cms_der: cms,
        });
        (dir, sealed, ca_der, art_sha)
    }

    /// Pack a sealed attestation into a `<name>.build-provenance.znippy` beside it —
    /// the same one-entry (`attestation.json`) znippy archive nornir's seal writes.
    fn pack(dir: &std::path::Path, name: &str, att: &SealedAttestation) -> PathBuf {
        let json = att.to_json().unwrap();
        let (bytes, _report) =
            create_archive_to_vec(&[(ATTESTATION_ENTRY.to_string(), json.into_bytes())], 3)
                .unwrap();
        let path = dir.join(format!("{name}{SEALED_PROVENANCE_SUFFIX}"));
        std::fs::write(&path, &bytes).unwrap();
        path
    }

    #[test]
    fn seal_then_verify_round_trip_ok() {
        let (dir, sealed, ca_der, art_sha) = seal_fixture("nornir-build-signer");
        let archive = pack(&dir, "holger-full", &sealed);

        let v = verify_sealed_provenance(&archive, &[ca_der], Some(&art_sha));
        assert!(v.verified, "expected verified, got: {}", v.reason);
        assert_eq!(v.signer.as_deref(), Some("nornir-build-signer"));
        assert_eq!(v.blob_sha256, art_sha);
        assert_eq!(v.repo, "holger");
        let att = v.attestation.expect("verified ⇒ attestation produced");
        assert_eq!(att.slsa_level, VERIFIED_SLSA_LEVEL);
        assert_eq!(att.signer_identity.as_deref(), Some("nornir-build-signer"));
        assert_eq!(att.source_repo.as_deref(), Some("holger"));

        std::fs::remove_dir_all(&dir).ok();
    }

    /// **The producer↔verifier agreement test.** nornir's DSSE mode seals a build
    /// provenance as a DSSE envelope over the in-toto SLSA statement; modgunn — the
    /// independent verifier that already reads a Rekor `content.envelope.signatures[]
    /// .keyid` — must accept it end-to-end through the SAME `verify_sealed_provenance`
    /// the CLI drives.
    #[test]
    fn dsse_sealed_provenance_verifies_end_to_end() {
        let dir = scratch_dir("dsse");
        let (archive, ca_der, art_sha) =
            super::verify::seal_provenance_fixture_dsse(&dir, "nordisk-ci").unwrap();

        let v = verify_sealed_provenance(&archive, &[ca_der.clone()], Some(&art_sha));
        assert!(v.verified, "modgunn must verify our DSSE envelope, got: {}", v.reason);
        assert_eq!(v.signer.as_deref(), Some("nordisk-ci"));
        assert_eq!(v.blob_sha256, art_sha);
        let att = v.attestation.expect("verified ⇒ attestation produced");
        assert_eq!(att.slsa_level, VERIFIED_SLSA_LEVEL);
        assert_eq!(att.source_repo.as_deref(), Some("holger"));

        // RED — an untrusted root cannot chain the DSSE signer.
        let (_rogue_key, rogue_ca) = mint_ca("rogue DSSE CA").unwrap();
        let bad = verify_sealed_provenance(&archive, &[rogue_ca], Some(&art_sha));
        assert!(!bad.verified, "an untrusted CA must be rejected for DSSE too");

        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn tampered_record_fails_verification() {
        let (dir, sealed, ca_der, _art_sha) = seal_fixture("nornir-build-signer");
        // Mutate the record but keep the original (now-stale) seal + digest: the
        // recomputed canonical digest no longer matches the sealed digest.
        let mut tampered = sealed.clone();
        tampered.record.inputs.vcs_rev = "deadbeefdeadbeef".to_string();
        let archive = pack(&dir, "tampered", &tampered);

        let v = verify_sealed_provenance(&archive, &[ca_der], None);
        assert!(!v.verified, "tampered record must not verify");
        assert!(
            v.reason.contains("does not match") || v.reason.contains("tampered"),
            "unexpected reason: {}",
            v.reason
        );
        assert!(
            v.attestation.is_none(),
            "no attestation for a tampered blob"
        );

        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn untrusted_root_is_rejected() {
        let (dir, sealed, _ca_der, art_sha) = seal_fixture("nornir-build-signer");
        let archive = pack(&dir, "holger-full", &sealed);
        let (_rogue_key, rogue_ca) = mint_ca("rogue CA").unwrap();

        let v = verify_sealed_provenance(&archive, &[rogue_ca], Some(&art_sha));
        assert!(!v.verified, "an untrusted CA must be rejected");
        assert!(
            v.reason.contains("verification failed") || v.reason.contains("chain"),
            "unexpected reason: {}",
            v.reason
        );

        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn content_hash_mismatch_is_rejected() {
        let (dir, sealed, ca_der, _art_sha) = seal_fixture("nornir-build-signer");
        let archive = pack(&dir, "holger-full", &sealed);

        // A genuine seal, but for an artifact whose content hash the provenance
        // does not attest.
        let wrong = "0".repeat(64);
        let v = verify_sealed_provenance(&archive, &[ca_der], Some(&wrong));
        assert!(!v.verified);
        assert!(
            v.reason.contains("not attested"),
            "unexpected reason: {}",
            v.reason
        );

        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn evaluate_sealed_scores_a_verified_attestation() {
        let (dir, sealed, ca_der, art_sha) = seal_fixture("nordisk-ci");
        let archive = pack(&dir, "holger-full", &sealed);

        // A clean policy: require the level the seal attests, trust the signer, and
        // pin the source repo — the verified attestation satisfies all three.
        let req = ProvenanceRequirement {
            require_min_slsa: VERIFIED_SLSA_LEVEL,
            trusted_signers: vec!["nordisk-ci".to_string()],
            expected_source_repo: Some("holger".to_string()),
            // The seal's attested commit (the fixture's vcs_rev) — pinned exactly.
            expected_source_sha: Some(sealed.record.inputs.vcs_rev.clone()),
        };
        let (prov, findings, v) = req.evaluate_sealed(&archive, &[ca_der.clone()], Some(&art_sha));
        assert!(v.verified);
        assert!(
            prov.signed,
            "a verified, policy-clean attestation is treated as signed"
        );
        assert_eq!(prov.signer.as_deref(), Some("nordisk-ci"));
        assert!(
            findings.is_empty(),
            "clean policy ⇒ no findings, got {findings:?}"
        );

        // The unverifiable path never scores policy on assumed data: one clear finding.
        let mut tampered = sealed.clone();
        tampered.record.inputs.vcs_rev = "cafebabe".to_string();
        let bad = pack(&dir, "tampered", &tampered);
        let (prov2, findings2, v2) = req.evaluate_sealed(&bad, &[ca_der], None);
        assert!(!v2.verified);
        assert!(!prov2.signed);
        assert_eq!(findings2.len(), 1);
        assert_eq!(findings2[0].id, "MODGUNN-PROV-UNVERIFIABLE");

        std::fs::remove_dir_all(&dir).ok();
    }

    /// A genuinely-verified seal that FAILS the A3 policy still fires the right
    /// policy findings — the CLI `verify-provenance --min-slsa / --trusted-signer`
    /// enforcement path. A merely-sealed blob attests only SLSA L2, so a policy
    /// requiring L3 fires SLSA-TOO-LOW; a signer outside the trusted set fires
    /// UNTRUSTED-SIGNER; and a mismatched --expected-repo fires SOURCE-MISMATCH.
    #[test]
    fn evaluate_sealed_flags_policy_shortfalls_on_a_verified_seal() {
        let (dir, sealed, ca_der, art_sha) = seal_fixture("nornir-build-signer");
        let archive = pack(&dir, "holger-full", &sealed);

        // Require L3 (above the L2 a seal attests), trust only someone else, and pin
        // a different source repo — all three fire against the genuinely-verified att.
        let strict = ProvenanceRequirement {
            require_min_slsa: 3,
            trusted_signers: vec!["someone-else".to_string()],
            expected_source_repo: Some("not-holger".to_string()),
            // Pin a commit the seal does not attest → COMMIT-MISMATCH also fires.
            expected_source_sha: Some("0000000000000000000000000000000000000000".to_string()),
        };
        let (prov, findings, v) =
            strict.evaluate_sealed(&archive, &[ca_der.clone()], Some(&art_sha));
        assert!(
            v.verified,
            "the seal itself verifies (policy is a separate layer)"
        );
        assert!(
            !prov.signed,
            "a policy-failing attestation is not treated as signed"
        );
        let ids: Vec<&str> = findings.iter().map(|f| f.id.as_str()).collect();
        assert!(
            ids.contains(&"MODGUNN-PROV-SLSA-TOO-LOW"),
            "L3 unmet: {ids:?}"
        );
        assert!(
            ids.contains(&"MODGUNN-PROV-UNTRUSTED-SIGNER"),
            "untrusted signer: {ids:?}"
        );
        assert!(
            ids.contains(&"MODGUNN-PROV-SOURCE-MISMATCH"),
            "repo mismatch: {ids:?}"
        );
        assert!(
            ids.contains(&"MODGUNN-PROV-COMMIT-MISMATCH"),
            "commit pin unmet: {ids:?}"
        );

        // Only the min-SLSA knob: exactly SLSA-TOO-LOW, nothing else (the seal is L2).
        let slsa_only = ProvenanceRequirement {
            require_min_slsa: 3,
            ..Default::default()
        };
        let (_p, f2, _v) = slsa_only.evaluate_sealed(&archive, &[ca_der.clone()], Some(&art_sha));
        let ids2: Vec<&str> = f2.iter().map(|f| f.id.as_str()).collect();
        assert_eq!(
            ids2,
            vec!["MODGUNN-PROV-SLSA-TOO-LOW"],
            "only the slsa knob fires: {ids2:?}"
        );

        // Only the trusted-signer knob, with the real signer trusted at L2 → clean.
        let signer_ok = ProvenanceRequirement {
            require_min_slsa: VERIFIED_SLSA_LEVEL,
            trusted_signers: vec!["nornir-build-signer".to_string()],
            ..Default::default()
        };
        let (prov3, f3, _v) = signer_ok.evaluate_sealed(&archive, &[ca_der], Some(&art_sha));
        assert!(
            prov3.signed,
            "trusted signer at the attested level passes clean"
        );
        assert!(f3.is_empty(), "no findings for a satisfied policy: {f3:?}");

        std::fs::remove_dir_all(&dir).ok();
    }

    /// The `--blob-dir` batch verify: each sealed blob in a dir gets its OWN verdict
    /// against the trusted root (the batch-discovery + per-blob verdict path the CLI
    /// globs) — a blob signed by a different CA is independently rejected.
    #[test]
    fn batch_blobs_get_independent_per_blob_verdicts() {
        let (dir, sealed_a, ca_der, _sha_a) = seal_fixture("nornir-build-signer");
        let a = pack(&dir, "holger-full", &sealed_a);
        // A second, independently-sealed artifact packed beside the first.
        let (dir2, sealed_b, _ca_b, _sha_b) = seal_fixture("nornir-build-signer");
        let b_bytes = std::fs::read(pack(&dir2, "other-artifact", &sealed_b)).unwrap();
        let b = dir.join(format!("other-artifact{SEALED_PROVENANCE_SUFFIX}"));
        std::fs::write(&b, &b_bytes).unwrap();

        for blob in [&a, &b] {
            let v = verify_sealed_provenance(blob, &[ca_der.clone()], None);
            // `b` was signed by a DIFFERENT CA, so only `a` verifies against ca_der;
            // this asserts the per-blob independence of the verdict rather than a
            // shared trust decision.
            if blob == &a {
                assert!(
                    v.verified,
                    "blob a verifies against its own CA: {}",
                    v.reason
                );
            } else {
                assert!(!v.verified, "blob b was signed by a different CA");
            }
        }
        std::fs::remove_dir_all(&dir).ok();
        std::fs::remove_dir_all(&dir2).ok();
    }
}