server/lib/src/security.rs
//! Móðguðr (modgunn) security integration — Engine A verdicts for holger.
//!
//! modgunn is the **single home** of the verdict logic (shared with nornir to
//! break the dependency cycle). This module is only the holger↔modgunn *adapter*:
//!
//! - map a holger artifact (a file inside a repository's znippy archive, or an
//! explicit coord) → a [`modgunn::scan::Artifact`];
//! - run the lean `OsvScanner` verdict (CVE / license / provenance) over a
//! listing;
//! - aggregate the per-artifact [`Verdict`]s into a [`ScanReport`] the Tools#3
//! Security pane (UI) and the `holger-server scan` CLI verb both surface.
//!
//! Only modgunn's DEFAULT `scan` feature (Engine A) is pulled — never the heavy
//! `scip`/`warehouse` engines. The advisory DB + policy here are a representative
//! **offline** set for the demo + tests; a real deployment swaps in an imported
//! OSV/GHSA snapshot via [`modgunn::osv`].
use serde_json::{json, Value};
use traits::ArtifactFormat;
pub use modgunn::core::{ArtifactRef, Decision, Finding, Severity, Verdict};
pub use modgunn::scan::{
Advisory, AdvisoryDb, Artifact, LicensePolicy, OsvScanner, Policy, Provenance,
ProvenancePolicy, ScanEngine,
};
/// The modgunn ecosystem label for a holger repository type / format string.
/// Stable, lowercase; used consistently for BOTH the advisory DB and the
/// artifacts so [`Advisory::matches`] keys line up. Falls back to the raw lower
/// of an unknown string so a custom format still scans (license/provenance
/// checks still apply even with no advisories for that ecosystem).
pub fn ecosystem_for(repo_type: &str) -> String {
match ArtifactFormat::from_format_str(repo_type) {
Some(fmt) => ecosystem_of(fmt).to_string(),
None => repo_type.to_ascii_lowercase(),
}
}
/// The canonical modgunn ecosystem label for a known [`ArtifactFormat`]. Shared
/// by [`ecosystem_for`] (string path) and [`ecosystem_for_format`] (the
/// promotion gate, which has a repository backend's `format()` directly).
fn ecosystem_of(fmt: ArtifactFormat) -> &'static str {
match fmt {
ArtifactFormat::Rust => "cargo",
ArtifactFormat::Pip => "pip",
ArtifactFormat::Maven3 => "maven",
ArtifactFormat::Go => "go",
ArtifactFormat::Nuget => "nuget",
ArtifactFormat::Npm => "npm",
ArtifactFormat::Gem => "gem",
ArtifactFormat::Deb => "deb",
ArtifactFormat::Rpm => "rpm",
ArtifactFormat::Helm => "helm",
ArtifactFormat::Docker => "docker",
ArtifactFormat::Conda => "conda",
ArtifactFormat::Composer => "composer",
ArtifactFormat::Znippy => "znippy",
ArtifactFormat::Raw => "raw",
}
}
/// The modgunn ecosystem label for a repository backend's [`ArtifactFormat`] —
/// the seam the promotion policy gate uses to key its scan (the source repo's
/// `format()` is known directly, so no repo-type string round-trip is needed).
pub fn ecosystem_for_format(fmt: ArtifactFormat) -> String {
ecosystem_of(fmt).to_string()
}
/// Archive entries that are index/metadata, not artifacts — skipped by the
/// scanner (they carry no coords to key a verdict by).
fn is_metadata_path(path: &str) -> bool {
let lower = path.to_ascii_lowercase();
let base = lower.rsplit('/').next().unwrap_or(&lower);
lower.contains("/index/")
|| lower.starts_with("index/")
|| base.ends_with(".json")
|| base.ends_with(".xml")
|| base == "packages"
|| base == "packages.gz"
|| base == "release"
|| base == "release.gz"
|| base.starts_with("repomd")
|| base.starts_with("repodata")
|| base == "config.json"
}
/// Parse `(name, version)` from an artifact filename/path across ecosystems.
///
/// Heuristic but real: takes the last path segment, strips a known archive
/// extension, then splits at the first `-` that is immediately followed by a
/// digit (the name↔version boundary that holds for cargo `.crate`, npm `.tgz`,
/// Python wheels/sdists, Maven `.jar`/`.pom`, `.gem`, `.nupkg`, …). The version
/// is the run up to the next `-` (so a wheel's `-py3-none-any` build tags are
/// dropped). Returns `None` for metadata files or anything without a coord.
pub fn parse_coords(path: &str) -> Option<(String, String)> {
if is_metadata_path(path) {
return None;
}
let base = path.rsplit('/').next().unwrap_or(path);
// Strip the longest matching known extension (handles multi-part ones).
let exts = [
".tar.gz", ".tar.zst", ".tar.bz2", ".crate", ".whl", ".tgz", ".jar", ".pom", ".war",
".gem", ".deb", ".rpm", ".nupkg", ".conda", ".zip", ".tar", ".gz", ".tbz",
];
let stem = exts
.iter()
.find_map(|e| base.strip_suffix(e))
.unwrap_or(base);
if stem.is_empty() {
return None;
}
let bytes = stem.as_bytes();
let mut split = None;
for i in 0..bytes.len().saturating_sub(1) {
if bytes[i] == b'-' && bytes[i + 1].is_ascii_digit() {
split = Some(i);
break;
}
}
let idx = split?;
let name = &stem[..idx];
let rest = &stem[idx + 1..];
// Version = run up to the next '-' (drops wheel build tags etc.).
let version = rest.split('-').next().unwrap_or(rest);
if name.is_empty() || version.is_empty() {
return None;
}
Some((name.to_string(), version.to_string()))
}
/// Build a modgunn [`Artifact`] for a file in `ecosystem`. Provenance is taken
/// from `signed` (a holger znippy archive that passed signature verification can
/// pass `true`); the declared `license` is optional (filename listings carry
/// none — `None`). `None` when the path has no parseable coords.
pub fn artifact_for(
ecosystem: &str,
path: &str,
license: Option<String>,
signed: bool,
) -> Option<Artifact> {
let (name, version) = parse_coords(path)?;
let provenance = if signed {
Provenance::signed_by("holger-archive")
} else {
Provenance::unsigned()
};
Some(Artifact::new(
ArtifactRef {
ecosystem: ecosystem.to_string(),
name,
version,
blob_sha256: String::new(),
},
license,
provenance,
))
}
/// Build a modgunn [`Artifact`] from EXPLICIT coordinates (`name`/`version`) in
/// `ecosystem`. This is the promotion path: the coords are known directly from
/// the [`traits::ArtifactId`] crossing the boundary, so there is no filename to
/// parse. `signed` sets provenance (a signature-verified znippy archive passes
/// `true`). This is the REAL scan seam the promotion policy gate feeds — it uses
/// the caller's configured scanner, never `demo_*`.
///
/// ★ **This overload hands modgunn an EMPTY digest, and that is a measured
/// defect, not a simplification.** `modgunn::core::Verdict::cache_key()` is
/// `(blob_sha256, policy_version, advisory_db_rev)` — CONTENT-ADDRESSED BY
/// DESIGN — and `blob_sha256: String::new()` collapses every artifact in a
/// promotion onto the cache key `("", policy, db)`. Two consequences follow, and
/// both are visible today:
///
/// * the verdict can only ever be a `name@version` advisory lookup, so it is a
/// verdict about a COORDINATE and not about bytes — exactly the binding defect
/// `crate::attest` exists to close one layer up; and
/// * a verdict cache keyed on that tuple cannot tell two artifacts apart, so a
/// clean result for one would answer for all of them.
///
/// Kept unchanged because callers depend on this signature (`promote/**` is
/// another owner's file this slice must not touch). Every caller that HOLDS the
/// bytes must move to [`artifact_from_coords_with_digest`]; this one now names
/// the hole instead of hiding it.
/// ★ **The PACKAGE-reading scan seam** (B2): the same artifact as
/// [`artifact_from_coords_with_digest`], plus the licence the package's own
/// metadata declares ([`license_from_package`]). This is what the promotion gate
/// calls, because it holds the bytes. Before it existed every gate subject carried
/// `license: None`, so `deny_licenses` could never block a real package.
pub fn artifact_from_package(
ecosystem: &str,
name: &str,
version: &str,
bytes: &[u8],
blob_sha256: &str,
signed: bool,
) -> Artifact {
let mut art = artifact_from_coords_with_digest(ecosystem, name, version, blob_sha256, signed);
art.license = license_from_package(ecosystem, bytes);
art
}
/// The SPDX licence expression a package declares in its OWN metadata.
///
/// This is the licence scanner's reader ([`crate::scanner::license::detect`]);
/// there is no second one. Wheel METADATA (`License-Expression`, then `License`,
/// then the classifiers), sdist PKG-INFO, npm package.json, the `.crate`'s
/// Cargo.toml, the nuspec, the Maven pom, the gemspec, a `.deb`'s DEP-5
/// copyright and a helm chart's annotation are read; see that module.
///
/// `None` for bytes that are not a readable package, for a package that states
/// no licence, and for one whose licence does not parse as SPDX. `None` is
/// decided by [`UnknownLicense`], never passed silently by default.
pub fn license_from_package(ecosystem: &str, bytes: &[u8]) -> Option<String> {
crate::scanner::license::detect(ecosystem, bytes).and_then(|d| d.expression)
}
/// **What the licence policy does with a package whose licence is unknown**.
/// It lives with the licence scanner, which is the one code path that applies it.
pub use crate::scanner::license::UnknownLicense;
/// The finding id modgunn gives a package that declares no licence.
pub const LICENSE_UNKNOWN_FINDING: &str = "MODGUNN-LICENSE-UNKNOWN";
pub fn artifact_from_coords(ecosystem: &str, name: &str, version: &str, signed: bool) -> Artifact {
artifact_from_coords_with_digest(ecosystem, name, version, "", signed)
}
/// ★ The DIGEST-CARRYING scan seam — the same artifact, keyed by its bytes.
///
/// `blob_sha256` is the artifact's lowercase-hex SHA-256. Pass it and
/// `Verdict::cache_key()` becomes what modgunn designed it to be: a key that
/// distinguishes two artifacts, survives a re-upload under the same coordinate,
/// and can be cross-referenced with an attestation in
/// [`crate::attest::AttestStore`] — which is keyed by the SAME digest, so a
/// verdict and the evidence behind it can finally be joined.
///
/// An empty `blob_sha256` is accepted rather than refused: this is a shared seam
/// and a caller genuinely scanning a filename listing has no bytes to hash (see
/// [`artifact_for`], which is the listing path). The refusal belongs at the
/// promotion boundary, which knows whether a digest was AVAILABLE — a refusal
/// here would only force that caller to invent one.
pub fn artifact_from_coords_with_digest(
ecosystem: &str,
name: &str,
version: &str,
blob_sha256: &str,
signed: bool,
) -> Artifact {
let provenance = if signed {
Provenance::signed_by("holger-archive")
} else {
Provenance::unsigned()
};
Artifact::new(
ArtifactRef {
ecosystem: ecosystem.to_string(),
name: name.to_string(),
version: version.to_string(),
blob_sha256: blob_sha256.to_string(),
},
None,
provenance,
)
}
/// The SHA-256 hex of an artifact's bytes, in the exact spelling
/// `modgunn::core::ArtifactRef::blob_sha256` and `crate::attest::ArtifactDigest`
/// both use. One hashing call site so a verdict and its attestation can never be
/// keyed by two different spellings of the same bytes — the failure mode that
/// looks like "no evidence for this artifact" and is really "evidence filed under
/// the other capitalisation".
pub fn blob_digest_hex(bytes: &[u8]) -> String {
nornir_hash::sha256_hex(bytes)
}
/// A representative **offline** advisory DB for the demo + tests — a handful of
/// well-known advisories across ecosystems so a real corpus surfaces a mix of
/// Pass / Warn / Block. A real deployment replaces this with an imported OSV/
/// GHSA/RUSTSEC snapshot (`modgunn::osv`).
pub fn demo_advisory_db() -> AdvisoryDb {
let mut log4shell = Advisory::basic(
"CVE-2021-44228",
"maven",
"log4j-core",
vec!["2.14.1".into(), "2.14.0".into(), "2.15.0".into()],
Severity::Critical,
"Log4Shell: JNDI RCE in Apache Log4j2",
);
log4shell.kev = true; // confirmed exploited in the wild
AdvisoryDb::new(
"holger-demo-db-v1",
vec![
Advisory::basic(
"RUSTSEC-2023-0044",
"cargo",
"openssl",
vec!["0.10.55".into()],
Severity::High,
"use-after-free in openssl::x509",
),
Advisory::basic(
"GHSA-pad-left",
"npm",
"left-pad",
vec![], // every version
Severity::Low,
"left-pad unpublish incident",
),
Advisory::basic(
"CVE-2018-18074",
"pip",
"requests",
vec!["2.19.0".into()],
Severity::Medium,
"requests leaks Authorization header on redirect",
),
log4shell,
],
)
}
/// The demo [`Policy`]: deny strong copyleft, allow anything else (an empty
/// allow-list ⇒ no "unreviewed" noise), and treat an unstated license as Pass
/// (filename listings carry no license, so unknown must NOT flood Warn).
/// Provenance is `Ignore` for the demo (synthetic artifacts are unsigned).
pub fn demo_policy() -> Policy {
Policy {
version: "holger-demo-v1".into(),
license: LicensePolicy {
allow: Vec::new(),
deny: vec!["GPL-3.0".into(), "AGPL-3.0".into()],
on_unknown: Decision::Pass,
},
provenance: ProvenancePolicy::Ignore,
..Default::default()
}
}
/// The default holger demo scanner (demo policy + demo advisory DB).
pub fn demo_scanner() -> OsvScanner {
OsvScanner::new(demo_policy(), demo_advisory_db())
}
/// The aggregated result of scanning a repository listing — the per-artifact
/// [`Verdict`]s plus Pass/Warn/Block tallies and the overall (strongest)
/// decision. Surfaced as `state_json` for the Tools#3 pane + asserted headlessly.
#[derive(Debug, Clone, Default)]
pub struct ScanReport {
pub ecosystem: String,
pub verdicts: Vec<Verdict>,
pub pass: usize,
pub warn: usize,
pub block: usize,
/// Files that had no parseable coords (index/metadata), recorded for honesty.
pub skipped: usize,
}
impl ScanReport {
/// The overall decision — the strongest across all verdicts (Block > Warn >
/// Pass); Pass for an empty/clean report.
pub fn decision(&self) -> Decision {
self.verdicts
.iter()
.map(|v| v.decision)
.max()
.unwrap_or(Decision::Pass)
}
/// Observable state for the robot/headless tests: counts, overall decision,
/// and the findings (id + severity + summary + coords) — never raw bytes.
pub fn state_json(&self) -> Value {
let decision = match self.decision() {
Decision::Pass => "pass",
Decision::Warn => "warn",
Decision::Block => "block",
};
let findings: Vec<Value> = self
.verdicts
.iter()
.filter(|v| !v.findings.is_empty())
.map(|v| {
json!({
"name": v.artifact.name,
"version": v.artifact.version,
"decision": match v.decision {
Decision::Pass => "pass",
Decision::Warn => "warn",
Decision::Block => "block",
},
"findings": v.findings.iter().map(|f| json!({
"id": f.id,
"severity": format!("{:?}", f.severity),
"summary": f.summary,
})).collect::<Vec<_>>(),
})
})
.collect();
json!({
"ecosystem": self.ecosystem,
"scanned": self.verdicts.len(),
"skipped": self.skipped,
"pass": self.pass,
"warn": self.warn,
"block": self.block,
"decision": decision,
"findings": findings,
})
}
}
/// Scan a repository `files` listing in `ecosystem` with `scanner`, folding the
/// per-artifact verdicts into a [`ScanReport`]. Files without parseable coords
/// (index/metadata) are counted as `skipped`, not scanned. The signature carries
/// over to the modgunn verdict so the report is fully reproducible.
pub fn scan_listing<'a, I>(ecosystem: &str, files: I, scanner: &OsvScanner) -> ScanReport
where
I: IntoIterator<Item = &'a str>,
{
let mut report = ScanReport {
ecosystem: ecosystem.to_string(),
..Default::default()
};
for path in files {
match artifact_for(ecosystem, path, None, false) {
Some(art) => {
let v = scanner.scan(&art);
match v.decision {
Decision::Pass => report.pass += 1,
Decision::Warn => report.warn += 1,
Decision::Block => report.block += 1,
}
report.verdicts.push(v);
}
None => report.skipped += 1,
}
}
report
}
/// Import an offline OSV / GHSA / RUSTSEC advisory snapshot from a single JSON
/// file into an [`AdvisoryDb`] — the REAL feed the promotion policy gate scans
/// against (the airgap-served advisory mirror the design names, §4 v2 / §13-10),
/// replacing the `demo_*` stub on the enforcement path. Uses modgunn's `osv`
/// importer (one advisory per vuln×affected-package). A range-only entry (no
/// explicit `versions`) imports as "all versions" per the importer's contract.
pub fn advisory_db_from_osv_file(path: &str) -> anyhow::Result<AdvisoryDb> {
let text = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("reading OSV advisory file '{path}': {e}"))?;
let advisories = modgunn::osv::parse_doc(&text)
.map_err(|e| anyhow::anyhow!("parsing OSV advisory file '{path}': {e}"))?;
Ok(AdvisoryDb::new(format!("osv-file:{path}"), advisories))
}
/// A REAL (non-demo) promotion [`Policy`]: deny the given SPDX license ids, HOLD
/// an unknown license for review ([`UnknownLicense::default`]; it was `Pass`
/// until B2, which made `deny_licenses` inert), and ignore provenance (znippy signature verification is a
/// separate gate). The CVE verdict is driven by the imported advisory DB.
/// ★ `provenance: ProvenancePolicy::Ignore` is a FAIL-OPEN, and it is left in
/// place here on purpose — changing it would change the verdict of every existing
/// deployment's promotion gate, and that gate lives in `promote/**`, which this
/// slice does not own. It is called out rather than silently kept:
///
/// * an `Ignore` provenance policy means an UNSIGNED artifact and a
/// signature-verified one score identically. The gate then enforces CVEs and
/// licences only, and the word "provenance" in its report means nothing.
/// * the stated justification is that "znippy signature verification is a
/// separate gate" — true for a znippy-backed repo with `ron_trusted_roots`
/// configured, and vacuous for everything else, because nothing else on the
/// promote path verifies anything.
/// * it compounds with the empty digest above: with `blob_sha256` empty AND
/// provenance ignored, the verdict is a pure `name@version` advisory lookup.
///
/// Use [`promotion_policy_with_provenance`] to require provenance without
/// touching this function's callers. See this module's `a_provenance_ignoring_
/// policy_passes_an_unsigned_artifact` test, which states the consequence as an
/// executable fact rather than a claim.
pub fn promotion_policy(deny_licenses: Vec<String>) -> Policy {
promotion_policy_with_provenance(deny_licenses, ProvenancePolicy::Ignore)
}
/// The promotion [`Policy`] with an EXPLICIT provenance stance — the same policy
/// [`promotion_policy`] builds, except the caller says what an unsigned artifact
/// is worth instead of inheriting `Ignore`.
///
/// Additive: no existing caller changes behaviour, and a deployment that wants
/// provenance enforced at the gate can now ask for it without a second policy
/// type. The `version` string still reads `holger-promote-v1` because it is part
/// of `Verdict::cache_key()` and re-pointing it would invalidate every cached
/// verdict for a change that may not be in force.
pub fn promotion_policy_with_provenance(
deny_licenses: Vec<String>,
provenance: ProvenancePolicy,
) -> Policy {
promotion_policy_for(deny_licenses, UnknownLicense::default(), provenance)
}
/// The promotion [`Policy`] with every stance stated: the deny list, what an
/// unknown licence gets, and what an unsigned artifact is worth.
pub fn promotion_policy_for(
deny_licenses: Vec<String>,
unknown_license: UnknownLicense,
provenance: ProvenancePolicy,
) -> Policy {
Policy {
version: "holger-promote-v1".into(),
license: LicensePolicy {
allow: Vec::new(),
deny: deny_licenses,
on_unknown: unknown_license.decision(),
},
provenance,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ecosystem_mapping_is_canonical() {
assert_eq!(ecosystem_for("rust"), "cargo");
assert_eq!(ecosystem_for("cargo"), "cargo");
assert_eq!(ecosystem_for("maven2"), "maven");
assert_eq!(ecosystem_for("pypi"), "pip");
assert_eq!(ecosystem_for("npm"), "npm");
// Unknown format still yields a usable lowercase label.
assert_eq!(ecosystem_for("WeirdFmt"), "weirdfmt");
}
#[test]
fn parse_coords_across_ecosystems() {
assert_eq!(
parse_coords("crates/serde-1.0.0.crate"),
Some(("serde".into(), "1.0.0".into()))
);
assert_eq!(
parse_coords("left-pad-1.3.0.tgz"),
Some(("left-pad".into(), "1.3.0".into()))
);
assert_eq!(
parse_coords("requests-2.19.0-py3-none-any.whl"),
Some(("requests".into(), "2.19.0".into()))
);
assert_eq!(
parse_coords("org/apache/logging/log4j/log4j-core/2.14.1/log4j-core-2.14.1.jar"),
Some(("log4j-core".into(), "2.14.1".into()))
);
assert_eq!(
parse_coords("openssl-0.10.55.crate"),
Some(("openssl".into(), "0.10.55".into()))
);
}
#[test]
fn parse_coords_skips_metadata() {
assert_eq!(parse_coords("index/config.json"), None);
assert_eq!(parse_coords("repodata.json"), None);
assert_eq!(parse_coords("dists/stable/Packages"), None);
assert_eq!(parse_coords("repomd.xml"), None);
}
#[test]
fn clean_listing_passes() {
let s = demo_scanner();
let report = scan_listing("cargo", ["serde-1.0.0.crate", "tokio-1.40.0.crate"], &s);
assert_eq!(report.decision(), Decision::Pass);
assert_eq!(report.pass, 2);
assert_eq!(report.warn, 0);
assert_eq!(report.block, 0);
}
#[test]
fn known_high_vuln_blocks() {
let s = demo_scanner();
let report = scan_listing("cargo", ["openssl-0.10.55.crate", "serde-1.0.0.crate"], &s);
assert_eq!(report.decision(), Decision::Block);
assert_eq!(report.block, 1);
assert_eq!(report.pass, 1);
let blocked = report
.verdicts
.iter()
.find(|v| v.decision == Decision::Block)
.unwrap();
assert_eq!(blocked.findings[0].id, "RUSTSEC-2023-0044");
}
#[test]
fn low_npm_advisory_warns_all_versions() {
let s = demo_scanner();
let report = scan_listing("npm", ["left-pad-1.3.0.tgz"], &s);
assert_eq!(report.decision(), Decision::Warn);
assert_eq!(report.warn, 1);
}
#[test]
fn log4shell_kev_blocks_as_critical() {
let s = demo_scanner();
let report = scan_listing("maven", ["log4j-core-2.14.1.jar"], &s);
assert_eq!(report.decision(), Decision::Block);
let v = &report.verdicts[0];
assert_eq!(v.findings[0].id, "CVE-2021-44228");
assert_eq!(v.findings[0].severity, Severity::Critical);
assert!(v.findings[0].summary.contains("[KEV]"));
}
#[test]
fn metadata_files_are_skipped_not_scanned() {
let s = demo_scanner();
let report = scan_listing(
"cargo",
[
"serde-1.0.0.crate",
"index/config.json",
"crates.io-index/cfg",
],
&s,
);
assert_eq!(report.verdicts.len(), 1);
assert!(report.skipped >= 1);
}
/// Every holger repository type maps to a non-empty, stable modgunn ecosystem
/// label, and a clean filename in that ecosystem scans to Pass — the
/// 14-ecosystem coverage sweep (the modgunn↔holger integration table).
#[test]
fn all_repository_types_map_and_scan_clean() {
let cases: &[(&str, &str)] = &[
("rust", "serde-1.0.0.crate"),
("pip", "flask-3.0.0-py3-none-any.whl"),
("maven3", "com/foo/bar/1.0/bar-1.0.jar"),
("go", "golang.org/x/text-0.14.0.zip"),
("nuget", "Newtonsoft.Json-13.0.3.nupkg"),
("npm", "express-4.18.2.tgz"),
("gem", "rails-7.1.0.gem"),
("deb", "nginx-1.24.0.deb"),
("rpm", "httpd-2.4.57.rpm"),
("helm", "mychart-1.2.3.tgz"),
("docker", "alpine-3.20.tar"),
("conda", "numpy-1.26.0.conda"),
("composer", "monolog-2.9.0.zip"),
("znippy", "bundle-1.0.0.crate"),
];
let s = demo_scanner();
for (repo_type, file) in cases {
let eco = ecosystem_for(repo_type);
assert!(!eco.is_empty(), "{repo_type} has no ecosystem label");
let report = scan_listing(&eco, [*file], &s);
assert_eq!(
report.decision(),
Decision::Pass,
"clean {repo_type} artifact {file} (eco {eco}) should Pass: {report:?}"
);
}
}
/// The three verdict states are reachable and ordered: a clean dep Passes, a
/// Low advisory Warns, a High/Critical advisory Blocks — and the overall
/// report decision is the strongest across a mixed listing.
#[test]
fn verdict_states_pass_warn_block_and_strongest_wins() {
let s = demo_scanner();
assert_eq!(
scan_listing("cargo", ["serde-1.0.0.crate"], &s).decision(),
Decision::Pass
);
assert_eq!(
scan_listing("npm", ["left-pad-1.3.0.tgz"], &s).decision(),
Decision::Warn
);
assert_eq!(
scan_listing("cargo", ["openssl-0.10.55.crate"], &s).decision(),
Decision::Block
);
// Mixed listing: Pass + Warn + Block ⇒ overall Block.
let mixed = scan_listing("cargo", ["serde-1.0.0.crate", "openssl-0.10.55.crate"], &s);
assert_eq!(mixed.decision(), Decision::Block);
assert_eq!(mixed.pass, 1);
assert_eq!(mixed.block, 1);
}
#[test]
fn report_state_json_carries_counts_and_findings() {
let s = demo_scanner();
let report = scan_listing("cargo", ["openssl-0.10.55.crate", "serde-1.0.0.crate"], &s);
let j = report.state_json();
assert_eq!(j["ecosystem"], "cargo");
assert_eq!(j["scanned"], 2);
assert_eq!(j["block"], 1);
assert_eq!(j["decision"], "block");
assert!(j["findings"]
.as_array()
.unwrap()
.iter()
.any(|f| f["name"] == "openssl"));
}
// ── The content-addressing seam (the measured defect, and its fix) ────────
/// ★ RED-when-broken, and it states the DEFECT as an executable fact: the
/// coordinate-only seam hands modgunn an EMPTY `blob_sha256`, so two DIFFERENT
/// artifacts collapse onto the SAME `Verdict::cache_key()`. modgunn's cache key
/// is `(blob_sha256, policy_version, advisory_db_rev)` — content-addressed by
/// design — and an empty digest is the one value that defeats it.
#[test]
fn the_coordinate_only_seam_collapses_two_artifacts_onto_one_cache_key() {
let s = demo_scanner();
let a = s.scan(&artifact_from_coords("cargo", "serde", "1.0.0", false));
let b = s.scan(&artifact_from_coords("cargo", "tokio", "1.40.0", false));
assert_eq!(
a.cache_key().0,
"",
"the coordinate-only seam still carries no digest"
);
assert_eq!(
a.cache_key(),
b.cache_key(),
"two different artifacts share one verdict cache key — this is the defect"
);
}
/// …and the digest-carrying seam separates them. Same coords, same policy, same
/// advisory DB: only the bytes differ, and only the bytes need to.
#[test]
fn the_digest_carrying_seam_keys_the_verdict_by_bytes() {
let s = demo_scanner();
let one = blob_digest_hex(b"the real serde 1.0.0 bytes");
let two = blob_digest_hex(b"something else entirely");
assert_ne!(one, two);
let a = s.scan(&artifact_from_coords_with_digest(
"cargo", "serde", "1.0.0", &one, false,
));
// The SAME coordinate, different bytes — the substitution a coordinate-keyed
// store cannot see.
let b = s.scan(&artifact_from_coords_with_digest(
"cargo", "serde", "1.0.0", &two, false,
));
assert_eq!(a.cache_key().0, one);
assert_ne!(
a.cache_key(),
b.cache_key(),
"different bytes ⇒ different verdict key"
);
// …and the policy/db halves of the key are untouched.
assert_eq!(a.cache_key().1, b.cache_key().1);
assert_eq!(a.cache_key().2, b.cache_key().2);
}
/// The digest spelling is the ONE spelling: lowercase hex SHA-256, identical to
/// what `crate::attest::ArtifactDigest` keys by. A verdict and its attestation
/// must be joinable, and they cannot be if one side capitalises.
#[test]
fn the_digest_spelling_matches_the_attestation_stores_key() {
let bytes = b"artifact bytes";
let hexstr = blob_digest_hex(bytes);
assert_eq!(hexstr.len(), 64);
assert!(hexstr
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)));
assert_eq!(
crate::attest::ArtifactDigest::of_sha256(bytes).hex(),
hexstr,
"the scan seam and the attestation store must hash to the same string"
);
// And it is a digest the attestation store will actually key by.
assert!(crate::attest::ArtifactDigest::sha256(&hexstr).is_ok());
}
/// ★ The consequence of `provenance: ProvenancePolicy::Ignore`, stated rather
/// than asserted: an UNSIGNED artifact passes the promotion policy exactly as a
/// signed one does. The line is left in `promotion_policy` (its gate lives in
/// `promote/**`), so this test is what stops it being forgotten.
#[test]
fn a_provenance_ignoring_policy_passes_an_unsigned_artifact() {
// Both subjects declare a licence, so the only variable is provenance
// (an unknown licence is held for review since B2).
let licensed = |mut a: Artifact| {
a.license = Some("MIT".into());
a
};
let unsigned = licensed(artifact_from_coords("cargo", "serde", "1.0.0", false));
let ignoring = OsvScanner::new(promotion_policy(vec![]), demo_advisory_db());
assert_eq!(
ignoring.scan(&unsigned).decision,
Decision::Pass,
"Ignore ⇒ an unsigned artifact is indistinguishable from a signed one"
);
// With the stance made explicit, the same artifact is refused.
let requiring = OsvScanner::new(
promotion_policy_with_provenance(vec![], ProvenancePolicy::Require),
demo_advisory_db(),
);
assert_eq!(requiring.scan(&unsigned).decision, Decision::Block);
let signed = licensed(artifact_from_coords("cargo", "serde", "1.0.0", true));
assert_eq!(requiring.scan(&signed).decision, Decision::Pass);
}
/// `promotion_policy` is what it always was except for ONE deliberate change
/// (B2, 2026-10-19): an unknown licence is no longer `Pass` but `Warn`, which
/// the promotion gate turns into "held for review".
#[test]
fn the_existing_promotion_policy_is_unchanged() {
let p = promotion_policy(vec!["GPL-3.0".into()]);
assert_eq!(p.version, "holger-promote-v1");
assert_eq!(p.license.deny, vec!["GPL-3.0".to_string()]);
assert!(p.license.allow.is_empty());
assert_eq!(
p.license.on_unknown,
Decision::Warn,
"unknown is HELD, never passed"
);
assert_eq!(p.provenance, ProvenancePolicy::Ignore);
}
/// Each stated stance maps to its own decision, and the default is Hold.
#[test]
fn the_unknown_licence_stance_is_stated_and_defaults_to_hold() {
assert_eq!(UnknownLicense::default(), UnknownLicense::Hold);
for (stance, want) in [
(UnknownLicense::Hold, Decision::Warn),
(UnknownLicense::Block, Decision::Block),
(UnknownLicense::Pass, Decision::Pass),
] {
let p = promotion_policy_for(vec![], stance, ProvenancePolicy::Ignore);
assert_eq!(p.license.on_unknown, want, "{stance:?}");
}
}
fn wheel_with_metadata(meta: &str) -> Vec<u8> {
zip_entry::fixture::zip(&[
("thing/__init__.py", b"".as_slice()),
("thing-1.0.0.dist-info/METADATA", meta.as_bytes()),
])
}
#[test]
fn a_wheel_licence_is_read_from_its_metadata_header_only() {
let w = wheel_with_metadata(
"Name: thing\nVersion: 1.0.0\nLicense-Expression: MIT OR Apache-2.0\n\nbody\n",
);
assert_eq!(
license_from_package("pip", &w).as_deref(),
Some("MIT OR Apache-2.0")
);
// A field in the long description is not a header.
let w = wheel_with_metadata("Name: thing\nVersion: 1.0.0\n\nLicense-Expression: MIT\n");
assert_eq!(license_from_package("pip", &w), None);
// A free-text License field that is not an SPDX expression is unknown, not "declared".
let w = wheel_with_metadata(
"Name: thing\nVersion: 1.0.0\nLicense: GNU General Public License v3\n",
);
assert_eq!(license_from_package("pip", &w), None);
// A METADATA deeper in the tree is not the wheel's own.
let nested = zip_entry::fixture::zip(&[(
"vendor/x-1.0.dist-info/METADATA",
b"License-Expression: MIT\n".as_slice(),
)]);
assert_eq!(license_from_package("pip", &nested), None);
// Bytes that are not a package at all read as unknown.
assert_eq!(license_from_package("pip", b"not a zip"), None);
assert_eq!(license_from_package("cargo", b"not a crate"), None);
}
}