server/lib/tests/cargo_publish_walks_the_intake_chain.rs
//! **`cargo publish` is a door of the intake chain** (lane HOL-CARGO-PUBLISH-DOOR, Holger 3.1.1).
//!
//! P19 made every repository outside the intake chain refuse writes where the policy requires a
//! scan, so `cargo publish` stopped working on a chain Holger. The repository
//! `promotion.intake.cargo` names (`ron_repo_type: "rust"`, no storage of its own) is now the
//! chain's cargo door, the way `promotion.intake.npm` is its npm door:
//!
//! * `cargo publish` (`PUT /<door>/api/v1/crates/new`) is INGESTED into `incoming` at the
//! coordinate `<door>/<name>/<name>-<version>.crate`, and nothing of it is served yet;
//! * the walk to AVAILABLE is the chain's: SCANNED, POLICY_CHECKED, two approvers, SEALED;
//! * the sparse index lists only the AVAILABLE versions, its line read from the sealed crate's
//! own `Cargo.toml` (`cksum` the sealed bytes' sha256), and `cargo install` installs from it;
//! * `cargo yank` and `cargo yank --undo` go through the door (audited, AVAILABLE versions only);
//! * a crate carrying a malware test file is refused at the publish, by the rule's name;
//! * a crate pinned to a dependency with a known CVE is refused at SCANNED, by the advisory's
//! name and the dependency's.
//!
//! Every client step is the real `cargo` (it is on `PATH` in the toolchain container), offline:
//! the crates.io dependency of the CVE case resolves from a local directory source.
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use holger_server_lib::{read_ron_config, wire_holger, Holger};
use sha2::Digest as _;
use traits::test_support::hold_port;
fn token(who: &str) -> String {
format!("{who}-token-of-sufficient-length-for-holger")
}
const EICAR_RULE: &str =
"rule EICAR_Test_File { strings: $a = \"EICAR-STANDARD-ANTIVIRUS-TEST-FILE\" condition: $a }";
/// RUSTSEC-2021-0003 (CVE-2021-25900, smallvec `insert_many` buffer overflow), as OSV writes it.
const SMALLVEC_ADVISORY: &str = r#"{"id": "RUSTSEC-2021-0003", "modified": "2021-01-08T00:00:00Z",
"aliases": ["CVE-2021-25900"], "summary": "Buffer overflow in SmallVec::insert_many",
"database_specific": {"severity": "CRITICAL"},
"affected": [ { "package": { "ecosystem": "crates.io", "name": "smallvec" },
"ranges": [ { "type": "SEMVER", "events": [ { "introduced": "0.6.3" }, { "fixed": "0.6.14" },
{ "introduced": "1.0.0" }, { "fixed": "1.6.1" } ] } ] } ] }"#;
struct Running {
_data: tempfile::TempDir,
_cfg: tempfile::NamedTempFile,
_holger: Holger,
_rt: tokio::runtime::Runtime,
http: u16,
}
fn install_intel(dir: &Path) {
use holger_intel::chrono::Utc;
use holger_intel::{AdvisorySet, InstalledIntel, LocalIntelDb};
let advisories = holger_intel::feeds::parse_osv_feed(SMALLVEC_ADVISORY.as_bytes()).unwrap();
let set = AdvisorySet::from_advisories(Utc::now(), vec![], &advisories);
let n = set.advisories.len() as u64;
let db = LocalIntelDb::open(dir);
let bytes = set.to_json_bytes().unwrap();
db.stage_generation(1, &bytes).unwrap();
db.activate(&InstalledIntel {
schema: holger_intel::db::INSTALLED_SCHEMA.into(),
bundle_id: "holger-intel-1".into(),
sequence: 1,
supplier: holger_intel::DEFAULT_SUPPLIER.into(),
snapshot_at: Utc::now(),
imported_at: Utc::now(),
candidate_digest: "00".into(),
release_signer_fingerprint: "00".into(),
advisories_file: LocalIntelDb::generation_file(1),
advisories_sha256: holger_server_lib::security::blob_digest_hex(&bytes),
advisories: n,
})
.unwrap();
}
fn start() -> Running {
let mut last = String::new();
for _ in 0..8 {
match try_start(hold_port(), hold_port()) {
Ok(r) => return r,
Err(e) => last = e,
}
}
panic!("server never started; last error: {last}");
}
fn try_start(grpc: u16, http: u16) -> Result<Running, String> {
let data = tempfile::tempdir().unwrap();
let d = data.path().display().to_string();
let intel = data.path().join("intel");
install_intel(&intel);
let yara = data.path().join("yara");
std::fs::create_dir_all(&yara).unwrap();
std::fs::write(yara.join("eicar.yar"), EICAR_RULE).unwrap();
let raw = |name: &str| {
format!(
r#"(ron_name: "{name}", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")),
ron_data_dir: Some("{d}/{name}")),"#
)
};
let methods: Vec<String> = ["ida", "bob", "carol", "dev", "ivy", "adm"]
.iter()
.map(|p| {
format!(
r#"StaticToken(subject: "{p}", token: Some("{}"))"#,
token(p)
)
})
.collect();
let ron = format!(
r#"(
repositories: [ {incoming} {quarantine} {approved} {production}
(ron_name: "crates", ron_repo_type: "rust", ron_base_url: Some("http://127.0.0.1:{http}"), ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door"))),
],
exposed_endpoints: [ (ron_name: "door", ron_url: "127.0.0.1:{grpc}", ron_http_url: Some("127.0.0.1:{http}")) ],
storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{d}") ],
audit: (dir: "{d}/audit", enabled: true),
auth: Some((
methods: [ {methods} ],
roles: {{ "ida": Writer, "bob": Reader, "carol": Reader, "dev": Writer, "ivy": Writer, "adm": Admin }},
duties: {{ "ida": [Importer], "bob": [SecurityApprover], "carol": [SecurityApprover], "dev": [Developer], "ivy": [Importer], "adm": [RepositoryAdmin] }},
)),
intel: Some((db_dir: "{intel}", offline: true)),
promotion: Some((
ladder_name: "intake",
stages: [
(name: "incoming", repo: "incoming"), (name: "quarantine", repo: "quarantine"),
(name: "approved", repo: "approved"), (name: "production", repo: "production"),
],
intake: Some((
incoming: "incoming", quarantine: "quarantine", approved: "approved", production: "production",
rules: (sandbox: off),
approvals: 2,
scan: Some((required: [vulnerability])),
inspect: (yara: Some((rules: Some("{yara}"), required: true))),
cargo: Some("crates"),
)),
)),
)"#,
incoming = raw("incoming"),
quarantine = raw("quarantine"),
approved = raw("approved"),
production = raw("production"),
methods = methods.join(", "),
intel = intel.display(),
yara = yara.display(),
);
let mut cfg = tempfile::NamedTempFile::new().unwrap();
cfg.write_all(ron.as_bytes()).unwrap();
let mut holger: Holger = read_ron_config(cfg.path()).map_err(|e| format!("config: {e:#}"))?;
holger
.instantiate_backends()
.map_err(|e| format!("backends: {e:#}"))?;
wire_holger(&mut holger).map_err(|e| format!("wire: {e:#}"))?;
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap();
{
let _in = rt.enter();
holger.start().map_err(|e| format!("start: {e:#}"))?;
}
let r = Running {
_data: data,
_cfg: cfg,
_holger: holger,
_rt: rt,
http,
};
for _ in 0..200 {
if request(http, "GET", "/healthz", Some("ida"), &[]).0 == 200 {
return Ok(r);
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
Err("never healthy".into())
}
/// One HTTP/1.1 request, as `who` when named: the status and the body.
fn request(port: u16, method: &str, path: &str, who: Option<&str>, body: &[u8]) -> (u16, String) {
let mut c = std::net::TcpStream::connect(("127.0.0.1", port)).expect("connect");
let auth = who
.map(|w| format!("Authorization: Bearer {}\r\n", token(w)))
.unwrap_or_default();
let head = format!(
"{method} {path} HTTP/1.1\r\nHost: 127.0.0.1\r\n{auth}Content-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
c.write_all(head.as_bytes()).unwrap();
c.write_all(body).unwrap();
let mut out = Vec::new();
c.read_to_end(&mut out).unwrap();
let text = String::from_utf8_lossy(&out).to_string();
let status = text
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let body = text
.split_once("\r\n\r\n")
.map(|(_, b)| b.to_string())
.unwrap_or_default();
(status, body)
}
/// The chain's walk: whoever may take the next step takes it, until AVAILABLE. `Err` names the
/// state it was held at and every refusal.
fn walk(port: u16, coordinate: &str) -> Result<Vec<String>, String> {
let mut walk = Vec::new();
let mut state = String::from("INGESTED");
'walk: for _ in 0..12 {
if state == "AVAILABLE" {
return Ok(walk);
}
let mut refusals = Vec::new();
for who in ["ida", "bob", "carol"] {
let (status, body) = request(
port,
"POST",
&format!("/-/intake/advance?name={coordinate}&verb=advance"),
Some(who),
&[],
);
if status == 200 {
let v: serde_json::Value = serde_json::from_str(&body).unwrap_or_default();
state = v["state"].as_str().unwrap_or("?").to_string();
walk.push(format!("{who}→{state}"));
continue 'walk;
}
refusals.push(format!("{who}: {status} {body}"));
}
return Err(format!(
"held at {state}: {}; walk so far: {walk:?}",
refusals.join(" | ")
));
}
Err(format!("never AVAILABLE: {walk:?}"))
}
fn have_cargo() -> bool {
Command::new("cargo")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn write(path: &Path, body: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, body).unwrap();
}
/// A cargo home of its own: the Holger registry, and no other credential.
fn cargo_home(dir: &Path, http: u16) {
write(
&dir.join("config.toml"),
&format!(
"[registries.holger]\nindex = \"sparse+http://127.0.0.1:{http}/crates/index/\"\ncredential-provider = \"cargo:token\"\n\n[net]\nretry = 0\noffline = false\n\n[publish]\ntimeout = 1\n"
),
);
}
/// `cargo <args>` in `dir` as `who` (`None`: no credential); its exit and its output.
fn cargo(home: &Path, who: Option<&str>, dir: &Path, args: &[&str]) -> (bool, String) {
let mut cmd = Command::new("cargo");
cmd.args(args)
.current_dir(dir)
.env("CARGO_HOME", home)
.env("CARGO_TARGET_DIR", dir.join("target"))
.env_remove("RUSTC_WRAPPER")
.env_remove("CARGO_BUILD_JOBS")
.env_remove("CARGO_REGISTRIES_HOLGER_TOKEN")
// `-Zpublish-timeout` only (the wait after a publish), on the stable toolchain.
.env("RUSTC_BOOTSTRAP", "1");
if let Some(w) = who {
cmd.env(
"CARGO_REGISTRIES_HOLGER_TOKEN",
format!("Bearer {}", token(w)),
);
}
let out = cmd.output().unwrap_or_else(|e| panic!("{cmd:?}: {e}"));
(
out.status.success(),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
/// A crate in `dir`: its `Cargo.toml` (publishing to Holger only) and its files.
fn a_crate(dir: &Path, name: &str, version: &str, deps: &str, files: &[(&str, &str)]) -> PathBuf {
let root = dir.join(format!("{name}-{version}"));
write(
&root.join("Cargo.toml"),
&format!(
"[package]\nname = \"{name}\"\nversion = \"{version}\"\nedition = \"2021\"\ndescription = \"lane HOL-CARGO-PUBLISH-DOOR probe\"\nlicense = \"MIT\"\npublish = [\"holger\"]\n\n[dependencies]\n{deps}"
),
);
for (path, body) in files {
write(&root.join(path), body);
}
root
}
/// `cargo publish` of the crate in `dir`. cargo then waits for the version to show in the
/// index, which a publish into the chain never does before its walk: the wait is cut to one
/// second (`publish.timeout`, `-Zpublish-timeout`), and cargo says it timed out.
fn publish(home: &Path, who: Option<&str>, dir: &Path) -> (bool, String) {
cargo(
home,
who,
dir,
&[
"-Zpublish-timeout",
"publish",
"--registry",
"holger",
"--no-verify",
"--allow-dirty",
],
)
}
/// The `.crate` cargo packaged in `dir` (below its target directory).
fn packaged(dir: &Path, file: &str) -> Vec<u8> {
fn find(d: &Path, file: &str) -> Option<PathBuf> {
for e in std::fs::read_dir(d).ok()?.flatten() {
let p = e.path();
if p.is_dir() {
if let Some(f) = find(&p, file) {
return Some(f);
}
} else if p.file_name().is_some_and(|n| n == file) {
return Some(p);
}
}
None
}
let p = find(&dir.join("target"), file)
.unwrap_or_else(|| panic!("cargo packaged no {file} below {}", dir.display()));
std::fs::read(p).unwrap()
}
fn sha256(b: &[u8]) -> String {
hex::encode(sha2::Sha256::digest(b))
}
/// The lines of one crate's sparse index file, parsed.
fn index_lines(port: u16, path: &str) -> (u16, Vec<serde_json::Value>) {
let (status, body) = request(
port,
"GET",
&format!("/crates/index/{path}"),
Some("dev"),
&[],
);
let lines = body
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| serde_json::from_str(l).ok())
.collect();
(status, lines)
}
#[test]
fn cargo_publish_walks_the_intake_chain_and_cargo_install_takes_only_what_is_available() {
if !have_cargo() {
eprintln!("cargo is not on PATH: skipped (the toolchain container carries it)");
return;
}
let r = start();
let work = tempfile::tempdir().unwrap();
let home = work.path().join("cargo-home");
cargo_home(&home, r.http);
// ── the door's config.json names the download and the publish API under the door ──
let (status, body) = request(r.http, "GET", "/crates/index/config.json", Some("dev"), &[]);
assert_eq!(status, 200, "{body}");
let cfg: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(
cfg["api"],
format!("http://127.0.0.1:{}/crates", r.http),
"{cfg}"
);
assert_eq!(
cfg["dl"],
format!("http://127.0.0.1:{}/crates/crates", r.http),
"{cfg}"
);
let main = "fn main() { println!(\"hello from the intake chain\"); }\n";
let v1 = a_crate(
work.path(),
"door-demo",
"0.1.0",
"",
&[("src/main.rs", main)],
);
// ── an anonymous publish is challenged; a Developer may not ingest ──
let (ok, out) = publish(&home, None, &v1);
assert!(!ok, "an anonymous publish is refused: {out}");
let (ok, out) = publish(&home, Some("dev"), &v1);
assert!(
!ok && out.contains("holger-duty-2"),
"a Developer does not ingest: {out}"
);
// ── the publish is INGESTED; nothing of it is served ──
let (ok, out) = publish(&home, Some("ida"), &v1);
assert!(ok, "the Importer publishes: {out}");
assert!(
out.contains("INGESTED"),
"cargo prints where the crate went: {out}"
);
let packaged = packaged(&v1, "door-demo-0.1.0.crate");
let (status, lines) = index_lines(r.http, "do/or/door-demo");
assert_eq!(status, 404, "nothing is listed before the walk: {lines:?}");
assert_ne!(
request(
r.http,
"GET",
"/crates/crates/door-demo/0.1.0/download",
Some("dev"),
&[]
)
.0,
200,
"nothing is downloadable before the walk"
);
let root = work.path().join("installed");
let (ok, out) = cargo(
&home,
Some("dev"),
work.path(),
&[
"install",
"--registry",
"holger",
"door-demo",
"--root",
root.to_str().unwrap(),
],
);
assert!(!ok, "cargo install finds nothing before the walk: {out}");
// ── the chain's walk: scanned, two approvers, sealed ──
let walked = walk(r.http, "crates/door-demo/door-demo-0.1.0.crate").expect("the walk");
assert!(
walked.iter().any(|s| s.starts_with("bob"))
&& walked.iter().any(|s| s.starts_with("carol")),
"two people approved: {walked:?}"
);
// ── the index lists it, read from the sealed crate; cargo install installs it ──
let (status, lines) = index_lines(r.http, "do/or/door-demo");
assert_eq!(status, 200, "{lines:?}");
assert_eq!(lines.len(), 1, "{lines:?}");
assert_eq!(lines[0]["name"], "door-demo");
assert_eq!(lines[0]["vers"], "0.1.0");
assert_eq!(
lines[0]["cksum"],
sha256(&packaged),
"cksum is the sealed bytes'"
);
assert_eq!(lines[0]["yanked"], false);
let (ok, out) = cargo(
&home,
Some("dev"),
work.path(),
&[
"install",
"--registry",
"holger",
"door-demo",
"--root",
root.to_str().unwrap(),
],
);
assert!(ok, "cargo install from the AVAILABLE set: {out}");
let ran = Command::new(root.join("bin/door-demo")).output().unwrap();
assert_eq!(
String::from_utf8_lossy(&ran.stdout).trim(),
"hello from the intake chain"
);
// ── a second version on its walk is not listed: the index is the AVAILABLE set ──
let v2 = a_crate(
work.path(),
"door-demo",
"0.2.0",
"",
&[("src/main.rs", main)],
);
let (ok, out) = publish(&home, Some("ida"), &v2);
assert!(ok, "{out}");
let (_, lines) = index_lines(r.http, "do/or/door-demo");
let versions: Vec<_> = lines.iter().map(|l| l["vers"].clone()).collect();
assert_eq!(
versions,
vec![serde_json::json!("0.1.0")],
"only AVAILABLE versions"
);
// ── a sealed version is never published again: cargo sees it in the index, and the door
// refuses the body itself ──
let (ok, out) = publish(&home, Some("ida"), &v1);
assert!(
!ok && out.contains("already exists"),
"cargo sees the AVAILABLE version: {out}"
);
let meta = br#"{"name":"door-demo","vers":"0.1.0","deps":[],"features":{}}"#;
let mut body = Vec::new();
body.extend_from_slice(&(meta.len() as u32).to_le_bytes());
body.extend_from_slice(meta);
body.extend_from_slice(&(packaged.len() as u32).to_le_bytes());
body.extend_from_slice(&packaged);
let (status, said) = request(
r.http,
"PUT",
"/crates/api/v1/crates/new",
Some("ida"),
&body,
);
assert!(
status >= 400 && said.contains("holger-intake-10"),
"a sealed version is immutable: {status} {said}"
);
// ── yank and unyank go through the door ──
let yank = |who: &str, version: &str, undo: bool| {
let mut args = vec![
"yank",
"--registry",
"holger",
"door-demo",
"--version",
version,
];
if undo {
args.push("--undo");
}
cargo(&home, Some(who), work.path(), &args)
};
let (ok, out) = yank("dev", "0.1.0", false);
assert!(
!ok && out.contains("holger-duty-2"),
"a Developer does not yank: {out}"
);
let (ok, out) = yank("ida", "0.2.0", false);
assert!(
!ok && out.contains("holger-cargo-3") && out.contains("not AVAILABLE"),
"only an AVAILABLE version is yanked: {out}"
);
let (ok, out) = yank("ida", "0.1.0", false);
assert!(ok, "cargo yank: {out}");
let (_, lines) = index_lines(r.http, "do/or/door-demo");
assert_eq!(lines[0]["yanked"], true, "{lines:?}");
assert_eq!(
request(
r.http,
"GET",
"/crates/crates/door-demo/0.1.0/download",
Some("dev"),
&[]
)
.0,
200,
"a yanked version still downloads (a lock file names it)"
);
let (ok, out) = yank("ida", "0.1.0", true);
assert!(ok, "cargo yank --undo: {out}");
let (_, lines) = index_lines(r.http, "do/or/door-demo");
assert_eq!(lines[0]["yanked"], false, "{lines:?}");
// SEC-REVIEW 2026-11-01 P12: a yank is its publisher's or a Repository Admin's, and only
// an Admin undoes an Admin's yank.
let (ok, out) = yank("ivy", "0.1.0", false);
assert!(
!ok && out.contains("holger-cargo-3") && out.contains("publisher"),
"another Importer never yanks ida's crate: {out}"
);
let (ok, out) = yank("adm", "0.1.0", false);
assert!(ok, "a Repository Admin yanks: {out}");
let (ok, out) = yank("ida", "0.1.0", true);
assert!(
!ok && out.contains("holger-cargo-3") && out.contains("Admin"),
"the publisher never undoes an Admin's yank: {out}"
);
let (_, lines) = index_lines(r.http, "do/or/door-demo");
assert_eq!(lines[0]["yanked"], true, "{lines:?}");
let (ok, out) = yank("adm", "0.1.0", true);
assert!(ok, "an Admin undoes it: {out}");
}
#[test]
fn a_crate_with_a_malware_test_file_is_refused_at_the_publish_by_name() {
if !have_cargo() {
eprintln!("cargo is not on PATH: skipped");
return;
}
let r = start();
let work = tempfile::tempdir().unwrap();
let home = work.path().join("cargo-home");
cargo_home(&home, r.http);
let eicar = format!(
"{}{}\n",
"X5O!P%@AP[4\\PZX54(P^)7CC)7}$", "EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
);
let evil = a_crate(
work.path(),
"door-evil",
"0.1.0",
"",
&[
("src/lib.rs", "pub fn x() {}\n"),
("assets/eicar.com", &eicar),
],
);
let (ok, out) = publish(&home, Some("ida"), &evil);
assert!(!ok, "the publish is refused: {out}");
assert!(out.contains("EICAR_Test_File"), "by the rule's name: {out}");
assert_eq!(index_lines(r.http, "do/or/door-evil").0, 404);
}
#[test]
fn a_crate_pinned_to_a_known_cve_dependency_is_refused_at_scanned_by_name() {
if !have_cargo() {
eprintln!("cargo is not on PATH: skipped");
return;
}
let r = start();
let work = tempfile::tempdir().unwrap();
let home = work.path().join("cargo-home");
cargo_home(&home, r.http);
// crates.io, offline: smallvec 1.6.0 from a local directory source.
let vendor = work.path().join("vendor");
write(
&vendor.join("smallvec/Cargo.toml"),
"[package]\nname = \"smallvec\"\nversion = \"1.6.0\"\nedition = \"2018\"\n",
);
write(
&vendor.join("smallvec/src/lib.rs"),
"pub struct SmallVec;\n",
);
write(
&vendor.join("smallvec/.cargo-checksum.json"),
"{\"files\":{},\"package\":null}",
);
let user = a_crate(
work.path(),
"door-cve",
"0.1.0",
"smallvec = \"=1.6.0\"\n",
&[("src/lib.rs", "pub use smallvec::SmallVec;\n")],
);
write(
&user.join(".cargo/config.toml"),
&format!(
"[source.crates-io]\nreplace-with = \"vendored\"\n\n[source.vendored]\ndirectory = \"{}\"\n",
vendor.display()
),
);
let (ok, out) = publish(&home, Some("ida"), &user);
assert!(ok, "the publish is INGESTED: {out}");
let held = walk(r.http, "crates/door-cve/door-cve-0.1.0.crate")
.expect_err("a crate pinned to a vulnerable dependency never becomes AVAILABLE");
assert!(
held.contains("RUSTSEC-2021-0003") && held.contains("smallvec") && held.contains("1.6.0"),
"refused by the advisory and the dependency: {held}"
);
assert_eq!(index_lines(r.http, "do/or/door-cve").0, 404, "never listed");
}