server/lib/src/sources.rs
//! **Tier 1's RON: `sources:`, `steps:` and `rules:`** (Holger G0, owner order 2026-10-23).
//!
//! One shape for where packages come from and what they pass on the way in:
//!
//! ```ron
//! sources: [
//! (name: "team", kind: upload, gates: (license: (deny: ["AGPL-3.0-only"]))),
//! (name: "vendor", kind: upload, importers: ["ci"], gates: (provenance: require(slsa_min: 2))),
//! (name: "crates-io", kind: crates, url: "https://index.crates.io/"), // `online,jobs`: through the steps
//! (name: "npmjs", kind: npm, url: "https://registry.npmjs.org/"),
//! (name: "central", kind: maven, url: "https://repo1.maven.org/maven2/"),
//! (name: "upstream", kind: holger, url: "http://up:50051",
//! repositories: [(ron_from: "cargo", ron_to: "cargo")]),
//! ],
//! steps: (approvals: 2, scan: [vulnerability, license]),
//! rules: (require_signed: false, cve_max: high, deny_licenses: ["GPL-3.0-only"]),
//! ```
//!
//! * `sources` lowers onto the shapes that already exist: an `upload` source is a door
//! into the intake chain's `incoming` repository, `crates` a `rust-remote` repository
//! named as an upstream of the repositories in `serves`, and `holger` a
//! `mirror.ron_sources` entry.
//! * `steps` is the new spelling of `promotion:` with its `intake:` block: the four
//! intake repositories (`incoming`, `quarantine`, `approved`, `production`), the
//! scanners, the approvals, the per-state policy, and the default `gates` every
//! source starts from.
//! * `rules` is the GLOBAL FLOOR. It holds for every source. A source's `gates` (and
//! the `steps.gates` defaults) may only TIGHTEN it; a gate that would loosen it is
//! refused at load, by name, with the rule it breaks.
//!
//! Every block refuses an unknown key by name.
//!
//! An artifact's EFFECTIVE POLICY is the floor plus its source's gates
//! ([`EffectivePolicy`]). It is computed when the artifact is ingested, kept on every
//! line of its intake record, and checked at SCANNED against the scanners' evidence
//! ([`EffectivePolicy::refusals`]); a refusal names the rule and who set it (the
//! floor, `steps.gates`, or the source).
//!
//! The provenance fields are lane G3a's shape (`holger-provenance`): `ProvenanceGate`
//! is `require(…) | prefer | off`, and the floor's provenance fields are flattened into
//! `rules`. Until G3a's provenance scanner is in this build a required signature FAILS
//! CLOSED by name, and the default floor does not require one
//! ([`PROVENANCE_IN_BUILD`]).
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
/// **Whether this build carries a provenance scanner** (lane G3a). The default floor
/// requires a signature exactly when the build can check one: a security gateway's
/// default is to fail closed on every rule it can check, and to say which rules it
/// cannot check yet (README "Tier 1 in five minutes"). True since lane G3a's verifier
/// (`holger-provenance`) is the chain's provenance slot: the default floor requires a
/// signature.
pub const PROVENANCE_IN_BUILD: bool = true;
/// One entry of `sources:`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Source {
/// The source's name. An upload source's artifacts carry it on their record.
pub name: String,
pub kind: SourceKind,
/// `holger`: the upstream Holger's gRPC endpoint. `python`/`oci`/`npm`/`maven`/`crates`/
/// `apk`: the index or registry pulled through the steps. Absent on a remote kind: the
/// source SERVES ONLY ([`Source::serves_only`]). Refused on `upload`.
#[serde(default)]
pub url: Option<String>,
/// `upload`: the principals that may upload as this source. Empty = any Importer.
#[serde(default)]
pub importers: Vec<String>,
/// `crates`: the local repositories that pull through this source.
#[serde(default)]
pub serves: Vec<String>,
/// `holger`: the credential the mirror presents (`Anonymous`, `Tls`, …).
/// `python`/`oci`/`npm`/`maven`/`crates`: a basic login `(username: "…", password_env: "…")`.
#[serde(default)]
pub credential: Option<SourceCredential>,
/// `python`: which files of the newest release are pulled (`sdist`, or a part of
/// the file name). `python`/`oci` fields below pass to the pull-through as written.
#[serde(default)]
pub select: Vec<String>,
/// `python`: at most this many files per release.
#[serde(default)]
pub max_files: Option<usize>,
/// `oci`: the platforms kept from an image index (`linux/amd64`). Empty = all.
#[serde(default)]
pub platforms: Vec<String>,
/// A pull-through source: the cap on one package, in bytes.
#[serde(default)]
pub max_bytes: Option<u64>,
/// A pull-through source: how old a page's or tag's last pull may be before it is asked again.
#[serde(default)]
pub refresh_secs: Option<u64>,
/// A pull-through source: how long a request waits for its pull before it is
/// answered "not yet" (default 0; 30 s for `maven`: Maven before 3.9.5 does not retry a 503,
/// 3.9.5 and later retry it three times).
#[serde(default)]
pub wait_secs: Option<u64>,
/// `holger`: how often the mirror cycle runs, in seconds.
#[serde(default)]
pub every_secs: Option<u64>,
/// `holger`: which upstream repository lands in which local one.
#[serde(default)]
pub repositories: Vec<crate::mirror_cycle::MirrorPair>,
/// This source's gates: they tighten the floor for every artifact from it.
#[cfg(feature = "intake")]
#[serde(default)]
pub gates: Option<Gates>,
/// **Per-source automatic approval** (for the pull-through lane): `off` (the default)
/// or `when(provenance: require, cve_max: medium)`. It decides only the human
/// approval step and never skips a scan or a gate; the floor must allow it
/// (`rules: (allow_auto_approve: true)`).
#[cfg(feature = "intake")]
#[serde(default)]
pub auto_approve: AutoApprove,
}
/// A source's `credential`: the mirror's, or a remote's basic login.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SourceCredential {
Mirror(crate::mirror_cycle::MirrorCredential),
Login(RemoteLogin),
}
/// A remote source's basic login. The password is read from the environment or a
/// file at boot and never written anywhere.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RemoteLogin {
pub username: String,
#[serde(default)]
pub password_env: Option<String>,
#[serde(default)]
pub password_file: Option<String>,
}
/// What a source is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceKind {
/// Packages pushed into the intake chain's `incoming` repository.
Upload,
/// A cargo sparse index (crates.io). With `url` it is pulled THROUGH the steps
/// (`pull_intake`, `--features online,jobs`); with `serves` it is the old
/// `rust-remote` upstream of those repositories, served as fetched; with neither it
/// serves only ([`Source::serves_only`]).
Crates,
/// An upstream Holger, mirrored on a timer (`mirror.ron_sources`).
Holger,
/// A PEP 503 / PEP 691 index (PyPI, Chainguard Libraries for Python), pulled THROUGH
/// the steps (`pull_intake`); needs `--features online,jobs`. Without `url` it serves
/// only ([`Source::serves_only`]), as do the next three.
Python,
/// An npm registry (registry.npmjs.org, Chainguard Libraries for JavaScript), pulled
/// THROUGH the steps; needs `--features online,jobs`.
Npm,
/// A Maven 2 repository (Maven Central, Chainguard Libraries for Java), pulled
/// THROUGH the steps; needs `--features online,jobs`.
Maven,
/// An OCI registry (`cgr.dev`), pulled THROUGH the steps; needs `--features online,jobs`.
Oci,
/// APK packages (Wolfi, Chainguard, Alpine). With `url` (the repository,
/// `https://packages.wolfi.dev/os`) they are pulled THROUGH the steps (`pull_intake::apk`,
/// lane T8; `--features online,jobs`), each held against the upstream's signed
/// `APKINDEX.tar.gz`. Without `url` they are uploaded into the steps like `upload`, their
/// provenance the signed index read from `<data_root>/holger-provenance/apk/<name>/`
/// (lane G3). Either way the index verifies against the apk keys the trust bundle pins.
Apk,
}
impl SourceKind {
pub fn name(self) -> &'static str {
match self {
SourceKind::Upload => "upload",
SourceKind::Crates => "crates",
SourceKind::Holger => "holger",
SourceKind::Python => "python",
SourceKind::Npm => "npm",
SourceKind::Maven => "maven",
SourceKind::Oci => "oci",
SourceKind::Apk => "apk",
}
}
/// A source whose artifacts are pushed into the steps (`upload`, `apk`).
pub fn is_upload(self) -> bool {
matches!(self, SourceKind::Upload | SourceKind::Apk)
}
/// A kind whose every source with a `url` the pull-through ingests into the steps as
/// `pull-through:<name>` (`crates` decides per source, [`Source::pulls_into_the_chain`]).
pub fn pulls_into_the_chain(self) -> bool {
matches!(
self,
SourceKind::Python | SourceKind::Oci | SourceKind::Npm | SourceKind::Maven
)
}
}
impl Source {
/// A remote that the pull-through ingests into the steps as `pull-through:<name>`:
/// `python`, `oci`, `npm`, `maven`, `crates` (without `serves`) or `apk`, with a `url`.
pub fn pulls_into_the_chain(&self) -> bool {
self.url.is_some() && (self.is_remote() || self.kind == SourceKind::Apk)
}
/// ★ **A source that SERVES ONLY** (a downstream's view, H3; field test F9): `python`,
/// `oci`, `npm`, `maven` or `crates` (without `serves`) with no `url`. The AVAILABLE set
/// that arrived under its name (a signed bundle, a sync from the upstream Holger) is
/// served with its kind's own index (`/v2/<production>/<name>/…`,
/// `/<production>/<name>/…`) and a miss is never fetched (`holger-downstream-4`), so it
/// needs no `online` build. It lowers onto a `pull_through` source with an empty `url`.
pub fn serves_only(&self) -> bool {
self.url.is_none() && self.is_remote()
}
/// A kind the pull-through serves under the source's name: `python`, `oci`, `npm`,
/// `maven`, and `crates` without `serves`.
fn is_remote(&self) -> bool {
self.kind.pulls_into_the_chain()
|| (self.kind == SourceKind::Crates && self.serves.is_empty())
}
/// A source whose artifacts are pushed into the steps: `upload`, and `apk` without `url`.
pub fn is_upload(&self) -> bool {
self.kind.is_upload() && !self.pulls_into_the_chain()
}
/// A pull-through or mirror source whose bytes are served or copied as fetched and
/// do not pass the steps (A18 §2.6 gap 5): `holger`, and `crates` with `serves`.
pub fn skips_the_chain(&self) -> bool {
self.kind == SourceKind::Holger
|| (self.kind == SourceKind::Crates && !self.serves.is_empty())
}
}
/// **How Holger reads its RON**: with `implicit_some`, so an optional field is written
/// as its value (`rules: (cve_max: high)`, `gates: (provenance: prefer)`) and the old
/// `Some(...)` spelling still loads.
pub fn ron_options() -> ron::Options {
ron::Options::default().with_default_extension(ron::extensions::Extensions::IMPLICIT_SOME)
}
/// Refuse what a source list cannot mean, by name. `online` is whether this build
/// carries the crates.io pull-through. A source that serves only ([`Source::serves_only`])
/// fetches nothing, so it needs no `online` build, only the `jobs` build whose doors serve it.
pub fn validate_sources(sources: &[Source], online: bool) -> Result<()> {
let mut seen = std::collections::BTreeSet::new();
for s in sources {
let n = &s.name;
if n.trim().is_empty() || n.contains('/') {
bail!("config `sources`: a source needs a name without '/' (got {n:?})");
}
if !seen.insert(n.as_str()) {
bail!("config `sources`: two sources are named '{n}'; a name is one source");
}
let kind = s.kind.name();
if s.kind == SourceKind::Crates && !s.serves.is_empty() && s.url.is_some() {
bail!(
"config `sources`: source '{n}' (kind `crates`) has both `url` (pulled through the \
steps) and `serves` (the old upstream served as fetched); keep one"
);
}
match s.kind {
_ if s.pulls_into_the_chain() && !(online && cfg!(feature = "jobs")) => bail!(
"config `sources`: source '{n}' has kind `{kind}`, a pull-through into the steps, \
which needs the `online` and `jobs` build (`cargo build --release --features \
online,jobs`)"
),
_ if s.serves_only() && !cfg!(all(feature = "jobs", feature = "intake")) => bail!(
"config `sources`: source '{n}' (kind `{kind}`) names no `url`: it serves only what \
arrived under its name (a downstream's view of what its upstream sent), and the \
doors that serve it are in the `jobs` build (every `holger-downstream`; \
`cargo build --release --features jobs`)"
),
SourceKind::Crates if !online && s.skips_the_chain() => bail!(
"config `sources`: source '{n}' has kind `crates`, the crates.io pull-through, \
which needs the `online` build (`cargo build --release --features online`)"
),
_ => {}
}
if s.serves_only() {
#[cfg_attr(not(feature = "intake"), allow(unused_mut))]
let mut shaped = vec![
("select", !s.select.is_empty()),
("max_files", s.max_files.is_some()),
("platforms", !s.platforms.is_empty()),
("max_bytes", s.max_bytes.is_some()),
("refresh_secs", s.refresh_secs.is_some()),
("wait_secs", s.wait_secs.is_some()),
("credential", s.credential.is_some()),
("importers", !s.importers.is_empty()),
("every_secs", s.every_secs.is_some()),
("repositories", !s.repositories.is_empty()),
];
// What passes this side's steps from it: nothing (it passed the upstream's).
#[cfg(feature = "intake")]
shaped.extend([
("gates", s.gates.is_some()),
(
"auto_approve",
matches!(s.auto_approve, AutoApprove::When { .. }),
),
]);
if let Some((field, _)) = shaped.iter().find(|(_, set)| *set) {
bail!(
"config `sources`: source '{n}' (kind `{kind}`) names no `url`: it serves only \
what arrived under its name and fetches nothing, so it takes no `{field}`"
);
}
}
let not_here = |field: &str, set: bool| -> Result<()> {
if set {
bail!("config `sources`: source '{n}' (kind `{kind}`) takes no `{field}`");
}
Ok(())
};
let remote = s.pulls_into_the_chain();
not_here(
"url",
s.url.is_some() && s.kind != SourceKind::Holger && !remote,
)?;
not_here(
"select",
!s.select.is_empty() && s.kind != SourceKind::Python,
)?;
not_here(
"max_files",
s.max_files.is_some() && s.kind != SourceKind::Python,
)?;
not_here(
"platforms",
!s.platforms.is_empty() && s.kind != SourceKind::Oci,
)?;
not_here("max_bytes", s.max_bytes.is_some() && !remote)?;
not_here("refresh_secs", s.refresh_secs.is_some() && !remote)?;
not_here("wait_secs", s.wait_secs.is_some() && !remote)?;
match (&s.credential, s.kind) {
(Some(SourceCredential::Login(_)), _) if !remote => {
bail!("config `sources`: source '{n}' (kind `{kind}`) takes no basic login")
}
(Some(SourceCredential::Mirror(_)), k) if k != SourceKind::Holger => {
bail!("config `sources`: source '{n}' (kind `{kind}`) takes `credential: (username: …)`, not a mirror credential")
}
_ => {}
}
not_here("importers", !s.importers.is_empty() && !s.is_upload())?;
not_here(
"serves",
!s.serves.is_empty() && s.kind != SourceKind::Crates,
)?;
let mirror_only = s.kind != SourceKind::Holger;
not_here(
"credential",
s.credential.is_some() && mirror_only && !remote,
)?;
not_here("every_secs", s.every_secs.is_some() && mirror_only)?;
not_here("repositories", !s.repositories.is_empty() && mirror_only)?;
if s.kind == SourceKind::Holger && s.url.is_none() {
bail!("config `sources`: source '{n}' (kind `holger`) needs `url`, the upstream's gRPC endpoint");
}
}
Ok(())
}
#[cfg(feature = "intake")]
pub use policy::*;
#[cfg(feature = "intake")]
mod policy {
use super::*;
use crate::scanner::{FindingSeverity, ScanEvidence, ScanVerdict, ScannerKind};
/// The licence judgement a policy re-reads at SCANNED; a build without scanners
/// has none (H3).
#[cfg(feature = "scanners")]
pub type LicenseJudgement = crate::scanner::license::LicenseJudgement;
#[cfg(not(feature = "scanners"))]
pub type LicenseJudgement = std::convert::Infallible;
// ── the shared vocabulary ────────────────────────────────────────────────
/// `on` | `off`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Switch {
On,
Off,
}
/// `secrets:` (lane HOLGER-G3): what a secret found in an artifact means.
/// `block` refuses every high or critical finding of the secrets scanner, by
/// name; `warn` records them; `off` lets them pass (the record still lists
/// them). Medium and lower findings (a bare `.env`, a high-entropy string next
/// to a secret-like name) always warn. Strictness: `off` < `warn` < `block`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SecretsMode {
Off,
Warn,
Block,
}
impl SecretsMode {
pub fn as_str(self) -> &'static str {
match self {
SecretsMode::Off => "off",
SecretsMode::Warn => "warn",
SecretsMode::Block => "block",
}
}
}
/// `require` | `off`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Need {
Require,
Off,
}
/// The provenance vocabulary is lane G3a's (`holger-provenance`): one shape, one parser.
pub use holger_provenance::{
check_tightens, effective, BuilderRule, Identity, KeyAlg, ProvenanceFloor, ProvenanceGate,
TrustedKey,
};
/// How strict a provenance mode is, so "the strictest of two" is a comparison.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProvenanceMode {
Off,
Prefer,
Require,
}
/// A source's `auto_approve:`.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum AutoApprove {
/// A person approves. The default.
#[default]
Off,
/// Approve automatically when the scan evidence meets these conditions, which may
/// only be as strict as the artifact's effective policy or stricter.
When {
/// `require`: a `pass` from the provenance verifier is on the SCANNED
/// record. Absent = what the artifact's policy already demands.
#[serde(default)]
provenance: Option<Need>,
/// The highest advisory severity the scan may report. Absent = the policy's.
#[serde(default)]
cve_max: Option<FindingSeverity>,
},
}
/// The actor a pull-through source ingests as.
pub fn pull_through_actor(source: &str) -> String {
format!("pull-through:{source}")
}
/// The automatic approver of a source; never equal to its importer.
pub fn auto_approver(source: &str) -> String {
format!("auto-approve:{source}")
}
/// An effective auto-approval rule: its conditions, already at least as strict as the
/// artifact's policy.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AutoApproveRule {
pub provenance: ProvenanceMode,
pub cve_max: FindingSeverity,
/// The approver it votes as: `auto-approve:<source>`.
pub approver: String,
}
/// `cve: (max: high)`: the highest advisory severity an artifact may carry.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CveGate {
pub max: FindingSeverity,
}
/// `license: (deny: [...])`: SPDX ids refused on top of the floor's.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LicenseGate {
#[serde(default)]
pub deny: Vec<String>,
}
/// A source's `gates:` (and `steps.gates`, the defaults). Every field is optional;
/// absent = what the floor (or `steps.gates`) says.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Gates {
#[serde(default)]
pub provenance: Option<ProvenanceGate>,
#[serde(default)]
pub sbom: Option<Need>,
#[serde(default)]
pub malware: Option<Switch>,
#[serde(default)]
pub license: Option<LicenseGate>,
#[serde(default)]
pub cve: Option<CveGate>,
/// `block | warn | off`: what a secret found in the artifact means.
#[serde(default)]
pub secrets: Option<SecretsMode>,
/// File formats (extensions, `pickle` for the pickle family) refused at ingest.
#[serde(default)]
pub block_formats: Vec<String>,
}
// ── the floor ─────────────────────────────────────────────────────────────
fn default_cve_max() -> FindingSeverity {
FindingSeverity::High
}
fn default_on() -> Switch {
Switch::On
}
fn default_secrets() -> SecretsMode {
SecretsMode::Block
}
fn default_block_formats() -> Vec<String> {
vec!["pickle".into()]
}
fn default_signed() -> bool {
PROVENANCE_IN_BUILD
}
/// **`rules:`, the global floor.** Every field's default is the security gateway's
/// default, so writing one rule never loosens another.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Rules {
/// Nothing unsigned comes in. Default: on when this build checks signatures.
#[serde(default = "default_signed")]
pub require_signed: bool,
/// Every artifact carries verified provenance. Default as `require_signed`.
#[serde(default = "default_signed")]
pub require_provenance: bool,
/// The least SLSA build level. 0 = no level asked.
#[serde(default)]
pub slsa_min: u8,
/// The signer identities trusted at all; a source may only narrow them.
#[serde(default)]
pub identities: Vec<Identity>,
/// The SLSA builders trusted at all: `"id"` or `(id: "…", level: N)`.
#[serde(default)]
pub builders: Vec<BuilderRule>,
#[serde(default)]
pub keys: Vec<TrustedKey>,
/// The signed sigstore trust bundle the provenance scanner verifies against.
#[serde(default)]
pub trust_root: Option<String>,
/// Every artifact needs an SBOM.
#[serde(default)]
pub require_sbom: bool,
/// File inspection and every configured malware engine must pass. Default on.
#[serde(default = "default_on")]
pub malware: Switch,
/// The highest advisory severity allowed. Default `high` (critical is refused).
#[serde(default = "default_cve_max")]
pub cve_max: FindingSeverity,
/// What a secret found in an artifact means: `block` (the default: high and
/// critical findings are refused), `warn` or `off`.
#[serde(default = "default_secrets")]
pub secrets: SecretsMode,
/// SPDX ids refused everywhere.
#[serde(default)]
pub deny_licenses: Vec<String>,
/// Formats refused everywhere. Default `["pickle"]`.
#[serde(default = "default_block_formats")]
pub block_formats: Vec<String>,
/// Whether any source may approve automatically (`auto_approve: when(...)`). Default
/// false: auto-approval needs this AND the source asking for it.
#[serde(default)]
pub allow_auto_approve: bool,
/// A pull-through or mirror source serves bytes that skip the steps. Default
/// false: such a source is refused at load until the floor says it accepts that.
#[serde(default)]
pub allow_unchecked_sources: bool,
/// **A native executable inside a crate** (HOL-DOGFOOD-4, owner 2026-11-03). `false`
/// (the default): the inspection refuses it, as before. `true`: it comes in with a
/// warning on the package page (the inspection's crate rule becomes `executables: warn`).
#[serde(default)]
pub allow_native_executables: bool,
/// **Test-fixture keys** (HOL-DOGFOOD-7, owner 2026-11-03). `false` (the default): a
/// high or critical secret refuses the package wherever it is. `true`: a secret found only
/// under a `tests/` or `fixtures/` directory is a warning; one anywhere else still refuses.
#[serde(default)]
pub allow_test_fixture_keys: bool,
/// **An advisory pinned only in a crate's own `Cargo.lock`** (HOL-DOGFOOD-6, owner
/// 2026-11-03): `warn` (the default) puts it on the package page as a warning, never
/// a refusal (cargo never reads a dependency's lock); `refuse` judges it as before.
/// Lowers onto `promotion.intake.scan.crate_lock_advisories`.
#[serde(default)]
pub crate_lock_advisories: crate::scanner::CrateLockAdvisories,
/// **Every scan runs in the scan sandbox** (lane SANDBOX-SCAN, owner
/// 2026-10-23): `required` (the default) or `off`, which only a test build
/// accepts (`holger-sandbox-6`). Lowers onto `promotion.intake.rules.sandbox`.
#[serde(default)]
pub sandbox: crate::job_runner::SandboxRule,
}
impl Default for Rules {
fn default() -> Self {
Self {
require_signed: default_signed(),
require_provenance: default_signed(),
slsa_min: 0,
identities: Vec::new(),
builders: Vec::new(),
keys: Vec::new(),
trust_root: None,
require_sbom: false,
malware: Switch::On,
cve_max: default_cve_max(),
secrets: default_secrets(),
deny_licenses: Vec::new(),
block_formats: default_block_formats(),
allow_auto_approve: false,
allow_unchecked_sources: false,
allow_native_executables: false,
allow_test_fixture_keys: false,
crate_lock_advisories: crate::scanner::CrateLockAdvisories::Warn,
sandbox: crate::job_runner::SandboxRule::Required,
}
}
}
impl Rules {
/// The provenance mode the floor demands.
pub fn provenance_mode(&self) -> ProvenanceMode {
if self.require_signed || self.require_provenance || self.slsa_min > 0 {
ProvenanceMode::Require
} else {
ProvenanceMode::Off
}
}
fn validate(&self) -> Result<()> {
for i in &self.identities {
i.validate()
.map_err(|e| anyhow::anyhow!("config `rules.identities`: {e}"))?;
}
Ok(())
}
/// **The provenance half of the floor**, as G3a's verifier takes it.
pub fn provenance_floor(&self) -> ProvenanceFloor {
ProvenanceFloor {
require_signed: self.require_signed,
require_provenance: self.require_provenance,
slsa_min: self.slsa_min,
identities: self.identities.clone(),
builders: self.builders.clone(),
keys: self.keys.clone(),
trust_root: self.trust_root.clone(),
}
}
}
// ── steps ─────────────────────────────────────────────────────────────────
fn s(v: &str) -> String {
v.to_string()
}
fn d_incoming() -> String {
s("incoming")
}
fn d_quarantine() -> String {
s("quarantine")
}
fn d_approved() -> String {
s("approved")
}
fn d_production() -> String {
s("production")
}
fn two() -> u32 {
2
}
/// **`steps:`, the new spelling of `promotion:` + `promotion.intake`.**
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Steps {
#[serde(default = "d_incoming")]
pub incoming: String,
#[serde(default = "d_quarantine")]
pub quarantine: String,
#[serde(default = "d_approved")]
pub approved: String,
#[serde(default = "d_production")]
pub production: String,
/// Scanner kinds that must report for EVERY artifact (the floor adds its own).
#[serde(default)]
pub scan: Vec<ScannerKind>,
/// Distinct approvers APPROVED needs; never the importer. Default 2 (two-person
/// approval, lane HOL-NCSC); `approvals: 1` is a choice the file writes down.
#[serde(default = "two")]
pub approvals: u32,
/// The policy per state, as `promotion.intake.transitions`.
#[serde(default)]
pub transitions: Vec<crate::promote::intake::TransitionPolicy>,
/// The file and archive inspector (and the optional ClamAV / YARA engines).
#[cfg(feature = "scanners")]
#[serde(default)]
pub inspect: holger_inspect::InspectConfig,
/// The advisory snapshot the vulnerability scanner reads (OSV JSON).
#[serde(default)]
pub advisory_db: Option<String>,
/// SPDX ids allowed (empty = every id not denied).
#[serde(default)]
pub allow_licenses: Vec<String>,
/// What an unknown licence gets: `Hold` (default), `Block` or `Pass`.
#[cfg(feature = "scanners")]
#[serde(default)]
pub unknown_license: crate::scanner::license::UnknownLicense,
/// The bundle trust policy the import doors verify against.
#[serde(default)]
pub trust_policy: Option<String>,
/// The chain's npm door (a `ron_repo_type: "npm"` repository).
#[serde(default)]
pub npm: Option<String>,
/// The chain's cargo door (a `ron_repo_type: "rust"` repository with no storage, lane
/// HOL-CARGO-PUBLISH-DOOR).
#[serde(default)]
pub cargo: Option<String>,
/// The chain's bundles door (a `raw` repository, lane HOL-BUNDLES-DOOR): a PUT there is
/// a signed Holger bundle, verified against `bundles_trust_policy` (else `trust_policy`).
#[serde(default)]
pub bundles: Option<String>,
/// The bundles door's trust policy (`holger.bundle-trust-policy/v1` RON).
#[serde(default)]
pub bundles_trust_policy: Option<String>,
/// The `oci` registries whose pushes are NOT scanned (`scan: off`); every other
/// `oci` repository takes its pushes into the chain. Lowers onto
/// `promotion.intake.oci_push` (lane HOL-OCI-EVIDENCE).
#[serde(default)]
pub oci_push: Vec<crate::promote::oci_push::OciPushRule>,
/// `promotion.intake.unscanned` (lane HOL-FIELD-2 P19).
#[serde(default)]
pub unscanned: Vec<String>,
/// The gates every source starts from; they may only tighten the floor.
#[serde(default)]
pub gates: Gates,
/// The secrets scanner's rule file (`(rules: "/path.ron")`); absent: the
/// built-in set. Lowers onto `promotion.intake.secrets`.
#[cfg(feature = "scanners")]
#[serde(default)]
pub secrets: Option<crate::scanner::secrets::SecretsSettings>,
/// The scan sandbox: the engine, the worker image, its limits (SANDBOX-SCAN).
/// Absent: the defaults. Lowers onto `promotion.intake.sandbox`.
#[serde(default)]
pub sandbox: Option<holger_sandbox::SandboxConfig>,
}
impl Steps {
/// The `promotion:` block this spelling means. `floor` adds its licence denials
/// and the scanner kinds its rules need.
#[cfg_attr(not(feature = "scanners"), allow(unused_variables))]
pub fn lower(&self, floor: &Rules, sources: &[Source]) -> crate::PromotionSettings {
let stages = [
&self.incoming,
&self.quarantine,
&self.approved,
&self.production,
]
.into_iter()
.map(|r| crate::promote::Stage::new(r.clone(), r.clone()))
.collect();
let mut required = self.scan.clone();
for k in floor_needs(floor) {
if !required.contains(&k) {
required.push(k);
}
}
crate::PromotionSettings {
ladder_name: "intake".into(),
stages,
advisory_db: self.advisory_db.clone(),
deny_licenses: floor.deny_licenses.clone(),
allow_licenses: self.allow_licenses.clone(),
#[cfg(feature = "scanners")]
unknown_license: self.unknown_license,
ingest_gates: Vec::new(),
require_attestation: None,
require_property: None,
provenance: Default::default(),
allow_skipping: false,
intake: Some(crate::promote::intake::IntakeSettings {
incoming: self.incoming.clone(),
quarantine: self.quarantine.clone(),
approved: self.approved.clone(),
production: self.production.clone(),
transitions: self.transitions.clone(),
#[cfg(feature = "scanners")]
inspect: inspect_for(&self.inspect, floor),
#[cfg(feature = "scanners")]
secrets: self.secrets.clone(),
rules: crate::job_runner::IntakeRules {
sandbox: floor.sandbox,
},
sandbox: self.sandbox.clone(),
// A build without scanners scans nothing: `steps:` there is the chain's
// states, approvals and audit only.
#[cfg(not(feature = "scanners"))]
scan: None,
#[cfg(feature = "scanners")]
scan: Some(crate::promote::intake::IntakeScanSettings {
required,
// G3a's verifier: the floor's provenance half, each source's
// `gates.provenance`, and `steps.gates.provenance` for the rest.
provenance: Some(crate::scanner::provenance::ProvenanceScanSettings {
rules: floor.provenance_floor(),
sources: sources
.iter()
.filter_map(|s| {
let g = s.gates.as_ref().and_then(|g| g.provenance.clone());
g.or_else(|| self.gates.provenance.clone())
.map(|g| (s.name.clone(), g))
})
.collect(),
default: self.gates.provenance.clone().unwrap_or(ProvenanceGate::Off),
inbox: None,
apk_sources: sources
.iter()
.filter(|s| s.kind == SourceKind::Apk && s.is_upload())
.map(|s| s.name.clone())
.collect(),
}),
crate_lock_advisories: floor.crate_lock_advisories,
}),
approvals: self.approvals,
trust_policy: self.trust_policy.clone(),
oci_push: self.oci_push.clone(),
unscanned: self.unscanned.clone(),
npm: self.npm.clone(),
cargo: self.cargo.clone(),
bundles: self.bundles.clone(),
bundles_trust_policy: self.bundles_trust_policy.clone(),
}),
}
}
}
/// **The chain's inspection under the floor** (HOL-DOGFOOD-4): with
/// `allow_native_executables: true` a crate's native executable warns instead of refusing
/// (an operator's own crate policy that already allows or warns stays as written).
#[cfg(feature = "scanners")]
fn inspect_for(
inspect: &holger_inspect::InspectConfig,
floor: &Rules,
) -> holger_inspect::InspectConfig {
use holger_inspect::{ExecPolicy, Format, Rule};
let mut i = inspect.clone();
if floor.allow_native_executables {
let p = i
.policies
.entry(Format::Crate)
.or_insert_with(|| ExecPolicy::default_for(Format::Crate));
if p.executables == Rule::Block {
p.executables = Rule::Warn;
}
}
i
}
/// **Is a secret finding's place a test fixture?** (HOL-DOGFOOD-7): its summary ends
/// `at <archive>!<path>[:<line>]`, and the path has a `tests` or `fixtures` directory.
fn in_test_fixture(summary: &str) -> bool {
let Some((_, at)) = summary.rsplit_once(" at ") else {
return false;
};
let inner = at.rsplit('!').next().unwrap_or(at);
let path = match inner.rsplit_once(':') {
Some((p, line)) if !line.is_empty() && line.bytes().all(|b| b.is_ascii_digit()) => p,
_ => inner,
};
let segs: Vec<&str> = path.split('/').collect();
segs[..segs.len().saturating_sub(1)]
.iter()
.any(|s| *s == "tests" || *s == "fixtures")
}
/// The scanner kinds the floor's rules need on every artifact.
fn floor_needs(floor: &Rules) -> Vec<ScannerKind> {
let mut k = vec![ScannerKind::Inspection, ScannerKind::Vulnerability];
if floor.require_sbom {
k.push(ScannerKind::Sbom);
}
if floor.provenance_mode() == ProvenanceMode::Require {
k.push(ScannerKind::Provenance);
}
if floor.secrets != SecretsMode::Off {
k.push(ScannerKind::Secrets);
}
k
}
// ── the effective policy ──────────────────────────────────────────────────
/// One rule's value and WHO set it: `floor`, `steps.gates`, or `source '<name>'`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Bound<T> {
pub value: T,
pub by: String,
}
fn bound<T>(value: T, by: &str) -> Bound<T> {
Bound {
value,
by: by.to_string(),
}
}
/// **An artifact's effective policy**: the floor plus its source's gates. Kept on
/// every line of the artifact's intake record and shown by GetArtifactState and
/// `/-/intake/state`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EffectivePolicy {
/// The source the artifact came from.
pub source: String,
pub provenance: Bound<ProvenanceMode>,
pub slsa_min: Bound<u8>,
#[serde(default)]
pub identities: Vec<Identity>,
#[serde(default)]
pub builders: Vec<BuilderRule>,
#[serde(default)]
pub keys: Vec<String>,
pub sbom: Bound<bool>,
pub malware: Bound<bool>,
pub cve_max: Bound<FindingSeverity>,
/// What a secret found means (lane HOLGER-G3).
#[serde(default = "default_secrets_bound")]
pub secrets: Bound<SecretsMode>,
/// HOL-DOGFOOD-7: the floor's `allow_test_fixture_keys` (a secret found only under
/// `tests/` or `fixtures/` is a warning, not a refusal).
#[serde(default)]
pub test_fixture_keys: bool,
/// Each denied id with who denied it.
#[serde(default)]
pub deny_licenses: Vec<Bound<String>>,
#[serde(default)]
pub block_formats: Vec<Bound<String>>,
/// The source's automatic approval, when the floor allows it and the source asks.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_approve: Option<AutoApproveRule>,
}
fn default_secrets_bound() -> Bound<SecretsMode> {
bound(default_secrets(), "floor")
}
fn sev_rank(s: FindingSeverity) -> u8 {
match s {
FindingSeverity::Info => 0,
FindingSeverity::Low => 1,
FindingSeverity::Medium => 2,
FindingSeverity::High => 3,
FindingSeverity::Critical => 4,
}
}
fn sev_name(s: FindingSeverity) -> &'static str {
match s {
FindingSeverity::Info => "info",
FindingSeverity::Low => "low",
FindingSeverity::Medium => "medium",
FindingSeverity::High => "high",
FindingSeverity::Critical => "critical",
}
}
fn mode_name(m: ProvenanceMode) -> &'static str {
match m {
ProvenanceMode::Off => "off",
ProvenanceMode::Prefer => "prefer",
ProvenanceMode::Require => "require",
}
}
/// The floor rule a provenance mode answers to, for the refusal's words.
fn provenance_rule(floor: &Rules) -> String {
if floor.require_signed {
"require_signed: true".into()
} else if floor.require_provenance {
"require_provenance: true".into()
} else {
format!("slsa_min: {}", floor.slsa_min)
}
}
impl EffectivePolicy {
/// The floor alone, as a source with no gates gets it.
pub fn floor(source: &str, floor: &Rules) -> Self {
Self {
source: source.to_string(),
provenance: bound(
floor.provenance_mode(),
&if floor.provenance_mode() == ProvenanceMode::Require {
format!("floor `{}`", provenance_rule(floor))
} else {
"floor".to_string()
},
),
slsa_min: bound(floor.slsa_min, "floor"),
identities: floor.identities.clone(),
builders: floor.builders.clone(),
keys: floor.keys.iter().map(|k| k.id.clone()).collect(),
sbom: bound(floor.require_sbom, "floor"),
malware: bound(floor.malware == Switch::On, "floor"),
cve_max: bound(floor.cve_max, "floor"),
secrets: bound(floor.secrets, "floor"),
test_fixture_keys: floor.allow_test_fixture_keys,
deny_licenses: floor
.deny_licenses
.iter()
.map(|l| bound(l.clone(), "floor"))
.collect(),
block_formats: floor
.block_formats
.iter()
.map(|f| bound(f.clone(), "floor"))
.collect(),
auto_approve: None,
}
}
/// **Apply a source's `auto_approve`.** Refused when the floor does not allow it,
/// or when a condition is looser than this policy.
pub fn with_auto_approve(
mut self,
a: &AutoApprove,
by: &str,
floor: &Rules,
) -> Result<Self> {
let AutoApprove::When {
provenance,
cve_max,
} = a
else {
return Ok(self);
};
if !floor.allow_auto_approve {
bail!(
"config `{by}: auto_approve: when(...)` needs the floor's `rules: (allow_auto_approve: \
true)`; the floor forbids automatic approval"
);
}
let mode = match provenance {
None => self.provenance.value,
Some(Need::Off) => ProvenanceMode::Off,
Some(Need::Require) => ProvenanceMode::Require,
};
if mode < self.provenance.value {
bail!(
"config `{by}: auto_approve.provenance: {}` is looser than the artifact's \
`provenance: {}` ({}); auto-approval may only be stricter",
mode_name(mode),
mode_name(self.provenance.value),
self.provenance.by
);
}
let max = cve_max.unwrap_or(self.cve_max.value);
if sev_rank(max) > sev_rank(self.cve_max.value) {
bail!(
"config `{by}: auto_approve.cve_max: {}` is looser than `cve_max: {}` ({}); \
auto-approval may only be stricter",
sev_name(max),
sev_name(self.cve_max.value),
self.cve_max.by
);
}
self.auto_approve = Some(AutoApproveRule {
provenance: mode,
cve_max: max,
approver: auto_approver(&self.source),
});
Ok(self)
}
/// **Does this artifact's scan evidence meet its source's auto-approval?** The
/// approver to vote as when it does. It decides only the approval vote; with
/// `approvals: 2` it counts as at most one of the two.
pub fn auto_approval(&self, records: &[ScanEvidence], importer: &str) -> Option<String> {
let rule = self.auto_approve.as_ref()?;
if rule.approver == importer {
return None;
}
if rule.provenance == ProvenanceMode::Require
&& !records
.iter()
.any(|r| r.kind == ScannerKind::Provenance && r.verdict == ScanVerdict::Pass)
{
return None;
}
let vuln: Vec<&ScanEvidence> = records
.iter()
.filter(|r| r.kind == ScannerKind::Vulnerability)
.collect();
if vuln.is_empty()
|| vuln
.iter()
.any(|r| !matches!(r.verdict, ScanVerdict::Pass | ScanVerdict::Warn))
{
return None;
}
if vuln
.iter()
.flat_map(|r| &r.findings)
.any(|f| sev_rank(f.severity) > sev_rank(rule.cve_max))
{
return None;
}
Some(rule.approver.clone())
}
/// **Apply `gates` on top, tightening only.** A gate that would loosen the
/// floor is an error naming the gate, `by`, and the rule it breaks.
pub fn tighten(mut self, gates: &Gates, by: &str, floor: &Rules) -> Result<Self> {
let at = format!("{by}: gates");
if let Some(p) = &gates.provenance {
let mode = match p {
ProvenanceGate::Off => ProvenanceMode::Off,
ProvenanceGate::Prefer => ProvenanceMode::Prefer,
ProvenanceGate::Require { .. } => ProvenanceMode::Require,
};
if mode < self.provenance.value {
bail!(
"config `{at}.provenance: {}` loosens the floor rule `{}` (which needs \
`{}`); a source may only tighten the floor",
mode_name(mode),
provenance_rule(floor),
mode_name(self.provenance.value)
);
}
// G3a's own check: slsa_min, identities, builders and keys only narrow the
// floor, and a `require` names who signs.
let pf = floor.provenance_floor();
check_tightens(&pf, p, by)
.map_err(|e| anyhow::anyhow!("config `{at}.provenance`: {e}"))?;
if mode > self.provenance.value {
self.provenance = bound(mode, by);
}
let eff = effective(&pf, p);
if eff.slsa_min > self.slsa_min.value {
self.slsa_min = bound(eff.slsa_min, by);
}
self.identities = eff.identities;
self.builders = eff.builders;
self.keys = eff.keys.iter().map(|k| k.id.clone()).collect();
}
if let Some(n) = gates.sbom {
let want = n == Need::Require;
if !want && self.sbom.value {
bail!(
"config `{at}.sbom: off` loosens the rule `require_sbom: true` ({})",
self.sbom.by
);
}
if want && !self.sbom.value {
self.sbom = bound(true, by);
}
}
if let Some(m) = gates.malware {
let want = m == Switch::On;
if !want && self.malware.value {
bail!(
"config `{at}.malware: off` loosens the rule `malware: on` ({})",
self.malware.by
);
}
if want && !self.malware.value {
self.malware = bound(true, by);
}
}
if let Some(c) = gates.cve {
if sev_rank(c.max) > sev_rank(self.cve_max.value) {
bail!(
"config `{at}.cve.max: {}` loosens the rule `cve_max: {}` ({})",
sev_name(c.max),
sev_name(self.cve_max.value),
self.cve_max.by
);
}
if sev_rank(c.max) < sev_rank(self.cve_max.value) {
self.cve_max = bound(c.max, by);
}
}
if let Some(m) = gates.secrets {
if m < self.secrets.value {
bail!(
"config `{at}.secrets: {}` loosens the rule `secrets: {}` ({}); a source may only tighten the floor",
m.as_str(),
self.secrets.value.as_str(),
self.secrets.by
);
}
if m > self.secrets.value {
self.secrets = bound(m, by);
}
}
if let Some(l) = &gates.license {
for id in &l.deny {
if !self
.deny_licenses
.iter()
.any(|b| b.value.eq_ignore_ascii_case(id))
{
self.deny_licenses.push(bound(id.clone(), by));
}
}
}
for f in &gates.block_formats {
if !self
.block_formats
.iter()
.any(|b| b.value.eq_ignore_ascii_case(f))
{
self.block_formats.push(bound(f.clone(), by));
}
}
Ok(self)
}
/// **Refused at ingest?** A blocked format, by its extension (`pickle` names the
/// pickle family).
#[cfg(feature = "scanners")]
pub fn ingest_refusal(&self, file_name: &str) -> Option<String> {
let lower = file_name.to_ascii_lowercase();
for b in &self.block_formats {
let f = b.value.to_ascii_lowercase();
let hit = if f == "pickle" {
crate::scanner::model::PICKLE_EXTENSIONS
.iter()
.any(|e| lower.ends_with(&format!(".{e}")))
} else {
lower.ends_with(&format!(".{}", f.trim_start_matches('.')))
};
if hit {
return Some(format!(
"rule `block_formats: [\"{}\"]` ({}) refuses '{file_name}' from source '{}'",
b.value, b.by, self.source
));
}
}
None
}
/// **What this policy refuses at SCANNED**, from the scanners' records. Each
/// reason names the rule and who set it. `license` re-judges the declared
/// licence against this policy's denials (it is `None` when nothing is denied).
pub fn refusals(
&self,
records: &[ScanEvidence],
license: Option<&LicenseJudgement>,
) -> Vec<String> {
let mut out = Vec::new();
let src = &self.source;
let ok = |r: &&ScanEvidence| matches!(r.verdict, ScanVerdict::Pass | ScanVerdict::Warn);
let of = |k: ScannerKind| records.iter().filter(move |r| r.kind == k);
let why = |k: ScannerKind| -> String {
of(k)
.map(|r| format!("{} {}: {}", r.scanner_id, r.verdict, r.detail))
.collect::<Vec<_>>()
.join("; ")
};
let named = |rule: String, by: &str, what: String| {
format!("rule `{rule}` ({by}) for source '{src}': {what}")
};
if (self.provenance.value == ProvenanceMode::Require || self.slsa_min.value > 0)
&& !of(ScannerKind::Provenance).any(|r| r.verdict == ScanVerdict::Pass)
{
let detail = why(ScannerKind::Provenance);
out.push(named(
"provenance: require".into(),
&self.provenance.by,
format!(
"no verified signature or provenance{}",
if detail.is_empty() {
" (this build has no provenance scanner)".into()
} else {
format!(" ({detail})")
}
),
));
}
if self.sbom.value && !of(ScannerKind::Sbom).any(|r| ok(&r)) {
out.push(named(
"require_sbom: true".into(),
&self.sbom.by,
format!("no SBOM ({})", why(ScannerKind::Sbom)),
));
}
if self.malware.value {
for k in [ScannerKind::Inspection, ScannerKind::Malware] {
for r in of(k).filter(|r| r.verdict == ScanVerdict::Block) {
out.push(named(
"malware: on".into(),
&self.malware.by,
format!("{} blocked: {}", r.scanner_id, r.detail),
));
}
}
if !of(ScannerKind::Inspection).any(|r| ok(&r)) {
out.push(named(
"malware: on".into(),
&self.malware.by,
format!(
"file inspection did not pass ({})",
why(ScannerKind::Inspection)
),
));
}
}
let max = self.cve_max.value;
for r in of(ScannerKind::Vulnerability) {
let over: Vec<String> = r
.findings
.iter()
.filter(|f| sev_rank(f.severity) > sev_rank(max))
// HOL-DOGFOOD-6: an advisory pinned only in a crate's own Cargo.lock
// under `crate_lock_advisories: warn` is a warning, never over `cve_max`.
.filter(|f| !f.summary.contains(crate::scanner::LOCK_ONLY_WORDS))
.map(|f| format!("{} ({})", f.id, sev_name(f.severity)))
.collect();
if !over.is_empty() {
out.push(named(
format!("cve_max: {}", sev_name(max)),
&self.cve_max.by,
format!("advisories above it: {}", over.join(", ")),
));
}
}
if self.secrets.value == SecretsMode::Block {
let found: Vec<String> = of(ScannerKind::Secrets)
.flat_map(|r| &r.findings)
.filter(|f| sev_rank(f.severity) >= sev_rank(FindingSeverity::High))
.filter(|f| !(self.test_fixture_keys && in_test_fixture(&f.summary)))
.map(|f| f.summary.clone())
.collect();
if !found.is_empty() {
let more = if found.len() > 5 {
format!(" and {} more", found.len() - 5)
} else {
String::new()
};
out.push(named(
"secrets: block".into(),
&self.secrets.by,
format!(
"{} secret(s) found: {}{more}",
found.len(),
found.iter().take(5).cloned().collect::<Vec<_>>().join("; ")
),
));
}
}
#[cfg(not(feature = "scanners"))]
let _ = license;
#[cfg(feature = "scanners")]
if let Some(j) = license {
if let crate::scanner::license::LicenseVerdict::Denied(ids) = &j.verdict {
for id in ids {
let by = self
.deny_licenses
.iter()
.find(|b| {
id.to_ascii_lowercase().starts_with(
&b.value
.to_ascii_lowercase()
.replace("-only", "")
.replace("-or-later", ""),
)
})
.map(|b| b.by.as_str())
.unwrap_or("floor");
out.push(named(
"deny_licenses".into(),
by,
format!("licence {id} is denied"),
));
}
}
}
out
}
/// One line for the audit trail.
pub fn summary(&self) -> String {
let deny: Vec<&str> = self
.deny_licenses
.iter()
.map(|b| b.value.as_str())
.collect();
let fmts: Vec<&str> = self
.block_formats
.iter()
.map(|b| b.value.as_str())
.collect();
format!(
"source={} provenance={}({}) slsa_min={} sbom={} malware={} cve_max={}({}) secrets={}({}) deny_licenses={deny:?} block_formats={fmts:?}",
self.source,
mode_name(self.provenance.value),
self.provenance.by,
self.slsa_min.value,
self.sbom.value,
if self.malware.value { "on" } else { "off" },
sev_name(self.cve_max.value),
self.cve_max.by,
self.secrets.value.as_str(),
self.secrets.by,
)
}
}
// ── the per-source policies a chain holds ─────────────────────────────────
/// **The effective policy of every upload source**, built once at load, and how an
/// upload finds its source.
#[derive(Debug, Clone)]
pub struct SourcePolicies {
uploads: Vec<(Source, EffectivePolicy)>,
/// `python` / `oci` sources: the pull-through ingests as `pull-through:<name>`.
remotes: Vec<(Source, EffectivePolicy)>,
/// The floor plus `steps.gates`, for a pull-through source `sources:` does not name.
defaults: EffectivePolicy,
}
impl SourcePolicies {
/// Validate `sources`, `steps.gates` and `rules` together and compute each
/// upload source's effective policy. With no upload source declared, uploads
/// come from one implicit source named `upload`.
pub fn build(sources: &[Source], steps: &Steps, floor: &Rules) -> Result<Self> {
floor.validate()?;
let defaults =
EffectivePolicy::floor("", floor).tighten(&steps.gates, "steps.gates", floor)?;
for s in sources.iter().filter(|s| s.skips_the_chain()) {
if !floor.allow_unchecked_sources {
bail!(
"config `sources`: source '{}' (kind `{}`) serves its bytes as fetched, \
outside the steps, so the floor cannot hold for it; the floor refuses \
such a source unless `rules: (allow_unchecked_sources: true)` says it \
accepts that",
s.name,
s.kind.name()
);
}
if s.gates.is_some() {
bail!(
"config `sources`: source '{}' (kind `{}`) has `gates`, but its bytes do \
not pass the steps, so no gate could run on them",
s.name,
s.kind.name()
);
}
}
let mut uploads = Vec::new();
let mut remotes = Vec::new();
for s in sources.iter().filter(|s| s.pulls_into_the_chain()) {
let mut p = defaults.clone();
p.source = s.name.clone();
let by = format!("source '{}'", s.name);
if let Some(g) = &s.gates {
p = p.tighten(g, &by, floor)?;
}
// A29 row 11 (owner): a container image is never auto-approved, whatever
// the floor says; `pull_intake::never_auto_approved` holds the same at run time.
if s.kind == SourceKind::Oci && matches!(s.auto_approve, AutoApprove::When { .. }) {
bail!(
"config `sources`: source '{}' is an OCI registry, and a container image is never \
auto-approved (owner, A29 row 11): every image waits for its approvers, whatever \
`allow_auto_approve` says; set the source's `auto_approve: off`",
s.name
);
}
p = p.with_auto_approve(&s.auto_approve, &by, floor)?;
remotes.push((s.clone(), p));
}
for s in sources.iter().filter(|s| s.is_upload()) {
let mut p = defaults.clone();
p.source = s.name.clone();
let by = format!("source '{}'", s.name);
if let Some(g) = &s.gates {
p = p.tighten(g, &by, floor)?;
}
p = p.with_auto_approve(&s.auto_approve, &by, floor)?;
uploads.push((s.clone(), p));
}
if uploads.is_empty() {
let mut p = defaults.clone();
p.source = "upload".into();
let implicit = Source {
name: "upload".into(),
kind: SourceKind::Upload,
url: None,
importers: Vec::new(),
serves: Vec::new(),
credential: None,
every_secs: None,
repositories: Vec::new(),
select: Vec::new(),
max_files: None,
platforms: Vec::new(),
max_bytes: None,
refresh_secs: None,
wait_secs: None,
gates: None,
auto_approve: AutoApprove::Off,
};
uploads.push((implicit, p));
}
Ok(Self {
uploads,
remotes,
defaults,
})
}
pub fn is_empty(&self) -> bool {
self.uploads.is_empty()
}
/// **Which source an upload is from.** One upload source: every upload. Several:
/// the first path segment of the artifact's name names it (`<source>/<file>`).
/// A source with `importers` takes uploads only from those principals.
pub fn resolve(
&self,
actor: &str,
artifact_name: &str,
) -> std::result::Result<EffectivePolicy, String> {
// A package the pull-through fetched: its remote source decides.
if let Some(name) = actor.strip_prefix("pull-through:") {
return Ok(match self.remotes.iter().find(|(s, _)| s.name == name) {
Some((_, p)) => p.clone(),
None => EffectivePolicy {
source: name.to_string(),
..self.defaults.clone()
},
});
}
let pick = if self.uploads.len() == 1 {
&self.uploads[0]
} else {
let first = artifact_name
.trim_start_matches('/')
.split('/')
.next()
.unwrap_or("");
match self.uploads.iter().find(|(s, _)| s.name == first) {
Some(u) => u,
None => {
let names: Vec<&str> =
self.uploads.iter().map(|(s, _)| s.name.as_str()).collect();
return Err(format!(
"'{artifact_name}' names no upload source; with several upload sources \
the path starts with the source's name ({})",
names.join(", ")
));
}
}
};
let (s, p) = pick;
if !s.importers.is_empty() && !s.importers.iter().any(|i| i == actor) {
return Err(format!(
"principal '{actor}' may not upload as source '{}' (its importers: {})",
s.name,
s.importers.join(", ")
));
}
Ok(p.clone())
}
/// **The effective policy of the source named `name`**, upload or remote. The
/// implicit upload source (`upload`, or the board's `uploads` row) is the one upload
/// source when there is only one.
pub fn named(&self, name: &str) -> Option<&EffectivePolicy> {
if let Some((_, p)) = self
.uploads
.iter()
.chain(self.remotes.iter())
.find(|(s, _)| s.name == name)
{
return Some(p);
}
match (name, self.uploads.as_slice()) {
("upload" | "uploads", [(_, p)]) => Some(p),
_ => None,
}
}
/// The effective policy of a `python` / `oci` source.
pub fn remote(&self, name: &str) -> Option<&EffectivePolicy> {
self.remotes
.iter()
.find(|(s, _)| s.name == name)
.map(|(_, p)| p)
}
}
#[cfg(all(test, feature = "scanners"))]
mod tests {
use super::*;
fn rules(r: &str) -> Rules {
ron_options()
.from_str(r)
.unwrap_or_else(|e| panic!("{r}: {e}"))
}
fn gates(g: &str) -> Gates {
ron_options()
.from_str(g)
.unwrap_or_else(|e| panic!("{g}: {e}"))
}
fn upload(name: &str, g: Option<&str>) -> Source {
Source {
name: name.into(),
kind: SourceKind::Upload,
url: None,
importers: Vec::new(),
serves: Vec::new(),
credential: None,
every_secs: None,
repositories: Vec::new(),
select: Vec::new(),
max_files: None,
platforms: Vec::new(),
max_bytes: None,
refresh_secs: None,
wait_secs: None,
gates: g.map(gates),
auto_approve: AutoApprove::Off,
}
}
/// ★ **HOL-DOGFOOD-4 and -7 are floor settings, default NO** (owner 2026-11-03). A
/// native executable inside a crate is refused by the inspection, and a high secret found
/// in a package is refused, unless the floor says otherwise:
/// `allow_native_executables: true` makes the crate rule `executables: warn` (the package
/// page shows the warning), and `allow_test_fixture_keys: true` lets a key found only
/// under `tests/` or `fixtures/` through as a warning; a key anywhere else still refuses.
#[cfg(feature = "scanners")]
#[test]
fn native_executables_and_test_fixture_keys_refuse_unless_the_floor_allows_them() {
use crate::scanner::{FindingSeverity as S, ScanFinding};
use holger_inspect::{ExecPolicy, Format, Rule};
let crate_exec = |floor: &Rules| -> Rule {
let lowered = steps().lower(floor, &[upload("vendor", None)]);
let inspect = lowered.intake.expect("steps lower onto an intake").inspect;
inspect
.policies
.get(&Format::Crate)
.map(|p| p.executables)
.unwrap_or(ExecPolicy::default_for(Format::Crate).executables)
};
assert_eq!(
crate_exec(&Rules::default()),
Rule::Block,
"default: refused"
);
assert_eq!(
crate_exec(&rules("(allow_native_executables: true)")),
Rule::Warn,
"allowed: a warning"
);
let rec = |findings: Vec<ScanFinding>| ScanEvidence {
scanner_id: "holger-secrets".into(),
scanner_version: "1".into(),
kind: ScannerKind::Secrets,
verdict: ScanVerdict::Warn,
findings,
inputs: Vec::new(),
document: None,
detail: "d".into(),
required: true,
intel_sequence: None,
hold: false,
};
let fixture = ScanFinding::new(
"secret:rsa-private-key",
S::High,
"an RSA private key (high) at rsa-0.9.8.crate!rsa-0.9.8/tests/examples/key.pem",
);
let fixture2 = ScanFinding::new(
"secret:stripe-live",
S::Critical,
"a Stripe live secret key (critical) at nornir-vault-0.2.0.crate!nornir-vault-0.2.0/fixtures/k.rs:289",
);
let real = ScanFinding::new(
"secret:aws-key",
S::High,
"an AWS access key (high) at x-1.0.crate!x-1.0/src/lib.rs:3",
);
let sec = |p: &EffectivePolicy, r: &[ScanEvidence]| -> Vec<String> {
p.refusals(r, None)
.into_iter()
.filter(|x| x.contains("secrets: "))
.collect()
};
let strict = EffectivePolicy::floor("crates", &Rules::default());
let only_fixtures = vec![rec(vec![fixture.clone(), fixture2.clone()])];
assert_eq!(
sec(&strict, &only_fixtures).len(),
1,
"default: a fixture key refuses"
);
let allowing =
EffectivePolicy::floor("crates", &rules("(allow_test_fixture_keys: true)"));
assert!(
sec(&allowing, &only_fixtures).is_empty(),
"allowed: keys only under tests/ or fixtures/ pass (as the scan's warning)"
);
let mixed = vec![rec(vec![fixture, real])];
let r = sec(&allowing, &mixed);
assert!(
r.len() == 1 && r[0].contains("src/lib.rs:3") && !r[0].contains("key.pem"),
"a key outside tests/ and fixtures/ still refuses, and names only itself: {r:?}"
);
}
fn steps() -> Steps {
ron_options().from_str("()").unwrap()
}
#[test]
fn an_apk_source_is_an_upload_source_whose_index_is_its_provenance() {
let src: Source = ron_options()
.from_str(r#"(name: "wolfi", kind: apk, gates: (provenance: require(apk_keys: ["wolfi-signing.rsa.pub"])))"#)
.unwrap();
assert_eq!(src.kind, SourceKind::Apk);
validate_sources(std::slice::from_ref(&src), false).unwrap();
let floor = Rules::default();
let p = SourcePolicies::build(std::slice::from_ref(&src), &steps(), &floor).unwrap();
assert_eq!(
p.resolve("alice", "wolfi/libffi-3.4.2-r0.apk")
.unwrap()
.source,
"wolfi"
);
let lowered = steps().lower(&floor, std::slice::from_ref(&src));
let prov = lowered.intake.unwrap().scan.unwrap().provenance.unwrap();
assert_eq!(prov.apk_sources, vec!["wolfi".to_string()]);
assert!(
matches!(&prov.sources["wolfi"], ProvenanceGate::Require { apk_keys, .. } if apk_keys == &["wolfi-signing.rsa.pub".to_string()])
);
}
/// ★ F9 (the field test of 2026-11-01): **a remote kind with no `url` serves only**
/// (a downstream's view of what its upstream sent), so it needs no `online` build: only
/// the `jobs` build whose doors serve it. With a `url` it pulls into the steps and still
/// needs `online` and `jobs`.
#[test]
fn a_remote_source_with_no_url_serves_only_and_needs_no_online_build() {
for kind in ["oci", "python", "npm", "maven", "crates"] {
let src: Source = ron_options()
.from_str(&format!(r#"(name: "s", kind: {kind})"#))
.unwrap();
assert!(!src.pulls_into_the_chain(), "{kind}: it fetches nothing");
let r = validate_sources(std::slice::from_ref(&src), false);
if cfg!(feature = "jobs") {
r.unwrap_or_else(|e| panic!("{kind}: {e}"));
} else {
let e = r.unwrap_err().to_string();
assert!(
e.contains("no `url`") && e.contains("`jobs`"),
"{kind}: {e}"
);
}
let with_url = Source {
url: Some("https://registry.example/".into()),
..src
};
assert!(with_url.pulls_into_the_chain(), "{kind}");
let e = validate_sources(&[with_url], false)
.unwrap_err()
.to_string();
assert!(
e.contains("needs the `online` and `jobs` build"),
"{kind}: {e}"
);
}
}
#[test]
fn the_owners_example_gates_parse() {
let g = gates(
r#"(provenance: require(identity: (issuer: "https://token.actions.githubusercontent.com", subject: "repo:a/b"), slsa_min: 2),
sbom: require, malware: on, license: (deny: ["AGPL-3.0-only"]), cve: (max: high))"#,
);
assert!(matches!(
g.provenance,
Some(ProvenanceGate::Require {
slsa_min: Some(2),
..
})
));
assert_eq!(
gates("(provenance: prefer)").provenance,
Some(ProvenanceGate::Prefer)
);
assert_eq!(
gates("(provenance: off)").provenance,
Some(ProvenanceGate::Off)
);
}
#[test]
fn unknown_keys_are_refused_by_name_in_every_block() {
for (what, r) in [
(
"cev",
ron_options()
.from_str::<Gates>("(cev: (max: high))")
.map(|_| ()),
),
(
"slsa",
ron_options()
.from_str::<Gates>("(provenance: require(slsa: 2))")
.map(|_| ()),
),
(
"requre_signed",
ron_options()
.from_str::<Rules>("(requre_signed: true)")
.map(|_| ()),
),
(
"aprovals",
ron_options().from_str::<Steps>("(aprovals: 2)").map(|_| ()),
),
(
"gatez",
ron_options()
.from_str::<Source>(r#"(name: "a", kind: upload, gatez: ())"#)
.map(|_| ()),
),
(
"maximum",
ron_options()
.from_str::<Gates>("(cve: (maximum: high))")
.map(|_| ()),
),
] {
let e = r.expect_err(what).to_string();
assert!(e.contains(what), "{what}: {e}");
}
}
#[test]
fn the_default_floor_fails_closed_on_what_this_build_checks() {
let r = Rules::default();
assert_eq!(r, rules("()"), "an empty `rules: ()` is the default floor");
assert_eq!(r.malware, Switch::On);
assert_eq!(r.cve_max, FindingSeverity::High);
assert_eq!(r.block_formats, vec!["pickle".to_string()]);
assert!(!r.allow_unchecked_sources);
assert_eq!(r.require_signed, PROVENANCE_IN_BUILD);
// Writing one rule keeps the others at the gateway's default.
let one = rules("(require_sbom: true)");
assert_eq!(one.cve_max, FindingSeverity::High);
assert_eq!(one.malware, Switch::On);
}
#[test]
fn a_source_cannot_loosen_the_floor_and_the_refusal_names_the_rule() {
let floor = rules("(require_signed: true)");
for (g, want) in [
("(provenance: off)", "require_signed: true"),
("(provenance: prefer)", "require_signed: true"),
("(malware: off)", "malware: on"),
("(cve: (max: critical))", "cve_max: high"),
(
r#"(provenance: require(identity: (issuer: "https://i", subject: "a"), slsa_min: 0))"#,
"",
),
] {
let r = SourcePolicies::build(&[upload("pypi", Some(g))], &steps(), &floor);
if want.is_empty() {
r.expect("slsa_min 0 equals the floor's 0: not a loosening");
continue;
}
let e = r.expect_err(g).to_string();
assert!(
e.contains("source 'pypi'") && e.contains(want) && e.contains("loosens"),
"{g}: {e}"
);
}
let e = SourcePolicies::build(
&[upload("a", Some(r#"(provenance: require(identity: (issuer: "https://i", subject: "a"), slsa_min: 1))"#))],
&steps(),
&rules("(slsa_min: 2)"),
)
.unwrap_err()
.to_string();
assert!(e.contains("slsa_min 1 is below the floor's 2"), "{e}");
let e = SourcePolicies::build(
&[upload("a", Some("(sbom: off)"))],
&steps(),
&rules("(require_sbom: true)"),
)
.unwrap_err()
.to_string();
assert!(e.contains("require_sbom: true"), "{e}");
}
#[test]
fn steps_gates_cannot_loosen_the_floor_either() {
let s: Steps = ron_options()
.from_str("(gates: (cve: (max: critical)))")
.unwrap();
let e = SourcePolicies::build(&[], &s, &Rules::default())
.unwrap_err()
.to_string();
assert!(
e.contains("steps.gates") && e.contains("cve_max: high"),
"{e}"
);
}
#[test]
fn a_source_can_tighten_the_floor_and_the_policy_says_who() {
let p = SourcePolicies::build(
&[upload("chainguard", Some(r#"(provenance: require(identity: (issuer: "https://i", subject: "a"), slsa_min: 2), sbom: require, cve: (max: medium), license: (deny: ["AGPL-3.0-only"]), block_formats: ["exe"])"#))],
&steps(),
&rules(r#"(deny_licenses: ["GPL-3.0-only"])"#),
)
.unwrap();
let e = p.resolve("ida", "pkg.whl").unwrap();
assert_eq!(e.source, "chainguard");
assert_eq!(
e.provenance.value,
ProvenanceMode::Require,
"the default floor requires it already"
);
assert_eq!(e.slsa_min, bound(2, "source 'chainguard'"));
assert!(e.sbom.value);
assert_eq!(
e.cve_max,
bound(FindingSeverity::Medium, "source 'chainguard'")
);
assert!(e
.deny_licenses
.contains(&bound("GPL-3.0-only".into(), "floor")));
assert!(e
.deny_licenses
.contains(&bound("AGPL-3.0-only".into(), "source 'chainguard'")));
assert!(e
.ingest_refusal("tool.exe")
.unwrap()
.contains("source 'chainguard'"));
assert!(
e.ingest_refusal("model.pkl").unwrap().contains("floor"),
"pickle is in the default floor"
);
assert!(e.ingest_refusal("pkg.whl").is_none());
}
#[test]
fn identities_and_keys_may_only_narrow_the_floors_lists() {
let floor = rules(
r#"(identities: [(issuer: "https://i", subject: "a")], keys: [(id: "k1", alg: ed25519, public_key: "PEM")])"#,
);
let bad = r#"(provenance: require(identity: (issuer: "https://i", subject: "b")))"#;
let e = SourcePolicies::build(&[upload("s", Some(bad))], &steps(), &floor)
.unwrap_err()
.to_string();
assert!(e.contains("the floor's `identities` allow"), "{e}");
let e = SourcePolicies::build(
&[upload("s", Some(r#"(provenance: require(keys: ["k2"]))"#))],
&steps(),
&floor,
)
.unwrap_err()
.to_string();
assert!(e.contains("not defined in the floor's `keys`"), "{e}");
SourcePolicies::build(&[upload("s", Some(r#"(provenance: require(keys: ["k1"], identity: (issuer: "https://i", subject: "a")))"#))], &steps(), &floor)
.expect("a subset narrows");
}
#[test]
fn several_upload_sources_are_told_apart_by_the_first_path_segment() {
let mut vendor = upload("vendor", None);
vendor.importers = vec!["ci".into()];
let p =
SourcePolicies::build(&[upload("team", None), vendor], &steps(), &Rules::default())
.unwrap();
assert_eq!(p.resolve("ida", "team/pkg-1.0.txt").unwrap().source, "team");
assert!(p
.resolve("ida", "other/pkg-1.0.txt")
.unwrap_err()
.contains("names no upload source"));
assert!(p
.resolve("ida", "vendor/x.txt")
.unwrap_err()
.contains("may not upload as source 'vendor'"));
assert_eq!(p.resolve("ci", "vendor/x.txt").unwrap().source, "vendor");
}
#[test]
fn a_source_that_skips_the_chain_is_refused_unless_the_floor_accepts_it() {
let mirror = Source {
kind: SourceKind::Holger,
url: Some("http://up:50051".into()),
..upload("up", None)
};
let e =
SourcePolicies::build(std::slice::from_ref(&mirror), &steps(), &Rules::default())
.unwrap_err()
.to_string();
assert!(e.contains("allow_unchecked_sources"), "{e}");
SourcePolicies::build(
&[mirror],
&steps(),
&rules("(allow_unchecked_sources: true)"),
)
.expect("accepted by the floor");
}
#[test]
fn auto_approval_needs_the_floor_and_may_only_be_stricter() {
let mut s = upload("pypi", None);
s.auto_approve = ron_options()
.from_str("when(provenance: require, cve_max: medium)")
.unwrap();
let e = SourcePolicies::build(std::slice::from_ref(&s), &steps(), &Rules::default())
.unwrap_err()
.to_string();
assert!(e.contains("allow_auto_approve"), "{e}");
let floor = rules("(allow_auto_approve: true)");
let p = SourcePolicies::build(std::slice::from_ref(&s), &steps(), &floor).unwrap();
let rule = p
.resolve("x", "p")
.unwrap()
.auto_approve
.clone()
.expect("the rule");
assert_eq!(rule.approver, "auto-approve:pypi");
assert_eq!(rule.cve_max, FindingSeverity::Medium);
s.auto_approve = ron_options().from_str("when(cve_max: critical)").unwrap();
let e = SourcePolicies::build(std::slice::from_ref(&s), &steps(), &floor)
.unwrap_err()
.to_string();
assert!(
e.contains("auto_approve.cve_max: critical") && e.contains("stricter"),
"{e}"
);
let s2 = Source {
auto_approve: AutoApprove::Off,
..upload(
"pypi",
Some(r#"(provenance: require(identity: (issuer: "https://i", subject: "a")))"#),
)
};
let mut s3 = s2.clone();
s3.auto_approve = ron_options().from_str("when(provenance: off)").unwrap();
let e = SourcePolicies::build(&[s3], &steps(), &floor)
.unwrap_err()
.to_string();
assert!(e.contains("auto_approve.provenance: off"), "{e}");
assert_eq!(pull_through_actor("pypi"), "pull-through:pypi");
assert!(ron_options()
.from_str::<AutoApprove>("when(cve: medium)")
.unwrap_err()
.to_string()
.contains("cve"));
}
#[test]
fn the_refusals_name_the_rule_and_who_set_it() {
use crate::scanner::{FindingSeverity as S, ScanFinding};
let rec = |kind, verdict, findings: Vec<ScanFinding>| ScanEvidence {
scanner_id: "x".into(),
scanner_version: "1".into(),
kind,
verdict,
findings,
inputs: Vec::new(),
document: None,
detail: "d".into(),
required: false,
intel_sequence: None,
hold: false,
};
let records = vec![
rec(ScannerKind::Inspection, ScanVerdict::Pass, vec![]),
rec(
ScannerKind::Vulnerability,
ScanVerdict::Warn,
vec![ScanFinding::new("GHSA-1", S::High, "h")],
),
rec(ScannerKind::Provenance, ScanVerdict::Error, vec![]),
];
let strict = SourcePolicies::build(
&[upload(
"strict",
Some(r#"(provenance: require(identity: (issuer: "https://i", subject: "a")), cve: (max: medium))"#),
)],
&steps(),
&rules("(require_signed: false, require_provenance: false)"),
)
.unwrap();
let r = strict.resolve("a", "p").unwrap().refusals(&records, None);
assert!(
r.iter()
.any(|x| x.contains("provenance: require") && x.contains("source 'strict'")),
"{r:?}"
);
assert!(
r.iter()
.any(|x| x.contains("cve_max: medium") && x.contains("GHSA-1")),
"{r:?}"
);
let open = SourcePolicies::build(
&[upload("open", Some("(provenance: prefer)"))],
&steps(),
&rules("(require_signed: false, require_provenance: false)"),
)
.unwrap();
assert!(
open.resolve("a", "p")
.unwrap()
.refusals(&records, None)
.is_empty(),
"high is within the floor's cve_max"
);
// Auto-approval reads the same records: a high advisory is over `cve_max: medium`.
let floor = rules(
"(allow_auto_approve: true, require_signed: false, require_provenance: false)",
);
let mut src = upload("pypi", None);
src.auto_approve = ron_options().from_str("when(cve_max: medium)").unwrap();
let p = SourcePolicies::build(std::slice::from_ref(&src), &steps(), &floor).unwrap();
assert_eq!(
p.resolve("a", "x")
.unwrap()
.auto_approval(&records, "pull-through:pypi"),
None
);
src.auto_approve = ron_options().from_str("when(cve_max: high)").unwrap();
let p = SourcePolicies::build(&[src], &steps(), &floor).unwrap();
let e = p.resolve("a", "x").unwrap();
assert_eq!(
e.auto_approval(&records, "pull-through:pypi").as_deref(),
Some("auto-approve:pypi")
);
assert_eq!(
e.auto_approval(&records, "auto-approve:pypi"),
None,
"never the importer"
);
}
#[test]
fn the_secrets_gate_tightens_only_and_block_refuses_high_findings_by_name() {
use crate::scanner::{FindingSeverity as S, ScanFinding};
// The default floor blocks; a source may not loosen it.
assert_eq!(Rules::default().secrets, SecretsMode::Block);
let e = SourcePolicies::build(
&[upload("team", Some("(secrets: warn)"))],
&steps(),
&Rules::default(),
)
.unwrap_err()
.to_string();
assert!(e.contains("secrets: warn") && e.contains("loosens"), "{e}");
// A floor of `warn` lets one source tighten to `block` and another stay.
let floor = rules("(secrets: warn)");
let p = SourcePolicies::build(
&[
upload("vendor", Some("(secrets: block)")),
upload("team", None),
],
&steps(),
&floor,
)
.unwrap();
let vendor = p.resolve("a", "vendor/x.whl").unwrap();
let team = p.resolve("a", "team/x.whl").unwrap();
assert_eq!(
(vendor.secrets.value, vendor.secrets.by.as_str()),
(SecretsMode::Block, "source 'vendor'")
);
assert_eq!(team.secrets.value, SecretsMode::Warn);
assert!(
vendor.summary().contains("secrets=block(source 'vendor')"),
"{}",
vendor.summary()
);
let rec = |findings: Vec<ScanFinding>| ScanEvidence {
scanner_id: "holger-secrets".into(),
scanner_version: "1".into(),
kind: ScannerKind::Secrets,
verdict: if findings.is_empty() {
ScanVerdict::Pass
} else {
ScanVerdict::Warn
},
findings,
inputs: Vec::new(),
document: None,
detail: "d".into(),
required: true,
intel_sequence: None,
hold: false,
};
let high = vec![rec(vec![ScanFinding::new(
"secret:github-token",
S::High,
"a GitHub token (high) at x.whl!a.py:3",
)])];
let medium = vec![rec(vec![ScanFinding::new(
"secret:dotenv-file",
S::Medium,
"a .env file (medium) at x.whl!.env",
)])];
// Only the secrets rule is under test here (no inspection record is handed in).
let sec = |p: &EffectivePolicy, r: &[ScanEvidence]| -> Vec<String> {
p.refusals(r, None)
.into_iter()
.filter(|x| x.contains("secrets: "))
.collect()
};
let r = sec(&vendor, &high);
assert_eq!(r.len(), 1, "{r:?}");
assert!(
r[0].contains("rule `secrets: block` (source 'vendor')")
&& r[0].contains("x.whl!a.py:3"),
"{}",
r[0]
);
assert!(sec(&team, &high).is_empty(), "warn records, never refuses");
assert!(
sec(&vendor, &medium).is_empty(),
"medium findings always warn"
);
// `off` on the floor is honoured, and the secrets kind is then not required.
let off = rules("(secrets: off)");
assert!(sec(&EffectivePolicy::floor("x", &off), &high).is_empty());
assert!(!floor_needs(&off).contains(&ScannerKind::Secrets));
assert!(floor_needs(&Rules::default()).contains(&ScannerKind::Secrets));
}
}
}