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

nordisk/dwarves

ci/verbs/tests/openssf_badge.rs

//! The nightly OpenSSF badge step (BADGE-7): the forge fetches each project's Best
//! Practices badge and publishes a COPY in nordisk/ci-status, so gunnar.rs draws the
//! badge from its own origin and no visitor's browser asks bestpractices.dev for it.
//! Only a small SVG with no script, no event attribute and no `javascript:` URL is
//! written.

use std::io::{Read, Write};
use std::net::TcpListener;

use ci_verbs::badge;

const PASSING: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="184" height="20" role="img" aria-label="openssf best practices: passing"><title>openssf best practices: passing</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/></linearGradient><g fill="#fff" text-anchor="middle"><text x="60" y="14">openssf best practices</text><text x="155" y="14">passing</text></g></svg>"##;

#[test]
fn a_small_plain_svg_passes_and_script_events_and_size_do_not() {
    assert_eq!(badge::check_svg(PASSING.as_bytes()), Ok(()));
    let prolog = format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n{PASSING}");
    assert_eq!(badge::check_svg(prolog.as_bytes()), Ok(()), "an XML prolog is fine");
    // the text of a badge may say "on" anywhere: only an attribute inside a tag counts
    let words = PASSING.replace(">passing<", "> on=passing online <");
    assert_eq!(badge::check_svg(words.as_bytes()), Ok(()), "text is not an attribute");

    let bad = [
        ("not svg", "<html><body>Rate limited</body></html>".to_string()),
        ("script", PASSING.replace("<title>", "<script>alert(1)</script><title>")),
        ("SCRIPT", PASSING.replace("<title>", "<SCRIPT >alert(1)</SCRIPT><title>")),
        ("onload", PASSING.replace("role=\"img\"", "role=\"img\" onload=\"alert(1)\"")),
        ("onclick", PASSING.replace("<text x=\"60\"", "<text\nONCLICK = 'x' x=\"60\"")),
        ("javascript", PASSING.replace("<title>", "<a href=\"javascript:alert(1)\">x</a><title>")),
        ("big", PASSING.replace("</svg>", &format!("<desc>{}</desc></svg>", "x".repeat(20 * 1024)))),
        ("binary", "\u{0}\u{1}".to_string()),
    ];
    for (why, body) in bad {
        assert!(badge::check_svg(body.as_bytes()).is_err(), "{why} must be refused");
    }
}

#[test]
fn the_step_picks_the_checkouts_project_and_names_the_file_after_the_repo() {
    let pairs: Vec<String> = ["gunnar=16084", "holger=16086"].iter().map(|s| s.to_string()).collect();
    assert_eq!(badge::project_for(&pairs, "gunnar"), Some("16084".to_string()));
    assert_eq!(badge::project_for(&pairs, "holger"), Some("16086".to_string()));
    assert_eq!(badge::project_for(&pairs, "edda"), None);
    assert_eq!(badge::project_for(&["gunnar=16084/../x".to_string()], "gunnar"), None, "an id is digits");
    assert_eq!(badge::file_for("gunnar"), "publish/badges/openssf-gunnar.svg");
}

/// One HTTP answer from a local server, then the request line it saw.
fn serve_once(status: &'static str, body: String) -> (String, std::thread::JoinHandle<String>) {
    let l = TcpListener::bind("127.0.0.1:0").unwrap();
    let base = format!("http://{}", l.local_addr().unwrap());
    let h = std::thread::spawn(move || {
        let (mut s, _) = l.accept().unwrap();
        let mut buf = [0u8; 4096];
        let n = s.read(&mut buf).unwrap_or(0);
        let req = String::from_utf8_lossy(&buf[..n]).lines().next().unwrap_or("").to_string();
        let _ = write!(s, "HTTP/1.1 {status}\r\nContent-Type: image/svg+xml\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len());
        req
    });
    (base, h)
}

fn args(v: &[&str]) -> Vec<String> {
    v.iter().map(|s| s.to_string()).collect()
}

#[test]
fn the_verb_fetches_checks_and_writes_the_copy_into_the_runs_publish_dir() {
    let tmp = std::env::temp_dir().join(format!("ci-verbs-badge-{}", std::process::id()));
    let checkout = tmp.join("gunnar");
    let out = tmp.join("out");
    std::fs::create_dir_all(&checkout).unwrap();

    let (base, h) = serve_once("200 OK", PASSING.to_string());
    let a = args(&["--checkout", checkout.to_str().unwrap(), "--out", out.to_str().unwrap(), "--base", &base, "--badge", "holger=16086", "--badge", "gunnar=16084"]);
    assert_eq!(badge::verb(&a), 0);
    assert_eq!(h.join().unwrap(), "GET /projects/16084/badge HTTP/1.1");
    assert_eq!(std::fs::read_to_string(out.join("publish/badges/openssf-gunnar.svg")).unwrap(), PASSING);

    // a bad answer writes nothing and fails the step by name
    let _ = std::fs::remove_dir_all(&out);
    let (base, h) = serve_once("200 OK", PASSING.replace("<title>", "<script>x</script><title>"));
    let a = args(&["--checkout", checkout.to_str().unwrap(), "--out", out.to_str().unwrap(), "--base", &base, "--badge", "gunnar=16084"]);
    assert_eq!(badge::verb(&a), 1);
    let _ = h.join();
    assert!(!out.join("publish/badges/openssf-gunnar.svg").exists());

    let (base, h) = serve_once("503 Service Unavailable", "busy".into());
    let a = args(&["--checkout", checkout.to_str().unwrap(), "--out", out.to_str().unwrap(), "--base", &base, "--badge", "gunnar=16084"]);
    assert_eq!(badge::verb(&a), 1);
    let _ = h.join();

    // a checkout with no project named: usage, not a fetch
    let a = args(&["--checkout", checkout.to_str().unwrap(), "--out", out.to_str().unwrap(), "--badge", "holger=16086"]);
    assert_eq!(badge::verb(&a), 2);
    // and the binary dispatches the verb by name
    let mut named = vec!["openssf-badge".to_string()];
    named.extend(a);
    assert_eq!(ci_verbs::run(&named, &tmp), 2);
    let _ = std::fs::remove_dir_all(tmp);
}