guests/static-scan/src/params.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! The job parameters (string pairs). Heimdall is a defensive sandbox. An
//! unknown, repeated or malformed key is a refusal, never ignored.
//!
//! | key | value | default |
//! |---|---|---|
//! | `purl` | the package URL (required) | — |
//! | `checks` | comma list of check names | all five |
//! | `origin` | `public` or `internal`: where the caller resolved the package | — (dependency confusion is then `unknown`) |
//! | `max-unpacked-bytes` | bytes the archive may unpack to | 64 MiB (at most 1 GiB) |
//! | `max-entries` | archive members | 100 000 |
//! | `max-manifest-bytes` | one manifest read into memory | 1 MiB |
//! | `max-depth` | path components of a member | 32 |
use heimdall_scan::{codes, Check, Purl};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Origin {
Public,
Internal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limits {
pub max_unpacked_bytes: u64,
pub max_entries: u64,
pub max_manifest_bytes: u64,
pub max_depth: usize,
}
impl Default for Limits {
fn default() -> Self {
Limits {
max_unpacked_bytes: 64 << 20,
max_entries: 100_000,
max_manifest_bytes: 1 << 20,
max_depth: 32,
}
}
}
/// The largest `max-unpacked-bytes` a job may ask for.
pub const MAX_UNPACKED_CAP: u64 = 1 << 30;
#[derive(Debug, Clone)]
pub struct Params {
pub purl_text: String,
pub purl: Purl,
pub checks: Vec<Check>,
pub origin: Option<Origin>,
pub limits: Limits,
}
/// A refusal reason with its bracketed code.
pub fn refusal(code: &str, why: impl std::fmt::Display) -> String {
format!("[{code}] {why}")
}
pub fn parse<'a>(pairs: impl IntoIterator<Item = (&'a str, &'a str)>) -> Result<Params, String> {
let bad = |why: String| refusal(codes::BAD_PARAMS, why);
let mut purl = None;
let mut checks = None;
let mut origin = None;
let mut limits = Limits::default();
let mut seen = std::collections::BTreeSet::new();
for (k, v) in pairs {
if !seen.insert(k.to_owned()) {
return Err(bad(format!("parameter {k:?} given twice")));
}
match k {
"purl" => purl = Some(v.to_owned()),
"checks" => checks = Some(parse_checks(v)?),
"origin" => {
origin = Some(match v {
"public" => Origin::Public,
"internal" => Origin::Internal,
_ => return Err(bad(format!("origin {v:?} is not `public` or `internal`"))),
})
}
"max-unpacked-bytes" => {
limits.max_unpacked_bytes = number(k, v, 1, MAX_UNPACKED_CAP)?;
}
"max-entries" => limits.max_entries = number(k, v, 1, 10_000_000)?,
"max-manifest-bytes" => limits.max_manifest_bytes = number(k, v, 1, 64 << 20)?,
"max-depth" => limits.max_depth = number(k, v, 1, 4096)? as usize,
_ => return Err(bad(format!("unknown parameter {k:?}"))),
}
}
let purl_text = purl.ok_or_else(|| refusal(codes::BAD_PURL, "no `purl` parameter"))?;
let parsed = Purl::parse(&purl_text).map_err(|e| refusal(codes::BAD_PURL, e))?;
Ok(Params {
purl_text,
purl: parsed,
checks: checks.unwrap_or_else(|| Check::ALL.to_vec()),
origin,
limits,
})
}
fn parse_checks(v: &str) -> Result<Vec<Check>, String> {
let mut out = Vec::new();
for name in v.split(',').map(str::trim) {
let c = Check::from_name(name).ok_or_else(|| {
refusal(
codes::UNKNOWN_CHECK,
format!("no check {name:?} in this guest"),
)
})?;
if out.contains(&c) {
return Err(refusal(
codes::BAD_PARAMS,
format!("check {name:?} listed twice"),
));
}
out.push(c);
}
out.sort();
Ok(out)
}
fn number(k: &str, v: &str, min: u64, max: u64) -> Result<u64, String> {
let n: u64 = v
.parse()
.ok()
.filter(|_| v.bytes().all(|b| b.is_ascii_digit()))
.ok_or_else(|| refusal(codes::BAD_PARAMS, format!("{k} {v:?} is not a number")))?;
if n < min || n > max {
return Err(refusal(
codes::BAD_PARAMS,
format!("{k} {n} is outside {min}..={max}"),
));
}
Ok(n)
}