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

nordisk/heimdall

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)
}