Skip to main content

assay_core/render_safety/
corpus.rs

1//! Shared render-safety corpus (MCP01a), mirroring the E38/M3 secret-sink corpus.
2//!
3//! Two-sided by construction: `HOSTILE` probes MUST be neutralised in every sink; `BENIGN` near
4//! matches MUST survive (an over-aggressive renderer must not look "safe" by destroying useful
5//! output). Hostile values are obviously-fake shapes; the PEM marker is assembled from split tokens
6//! so no contiguous private-key marker is committed.
7
8use lazy_static::lazy_static;
9use serde_json::json;
10use sha2::{Digest, Sha256};
11
12/// A corpus entry. For HOSTILE, `needle` is the dangerous substring that must be ABSENT from rendered
13/// output; for BENIGN, `needle` is an identifying substring that must SURVIVE rendering.
14pub struct Probe {
15    pub name: &'static str,
16    pub class: &'static str, // "secret" | "pii" | "control" | "benign"
17    pub input: String,
18    pub needle: String,
19}
20
21fn p(name: &'static str, class: &'static str, input: String, needle: String) -> Probe {
22    Probe {
23        name,
24        class,
25        input,
26        needle,
27    }
28}
29
30lazy_static! {
31    /// Assembled from split tokens so no contiguous private-key marker is committed to source.
32    static ref FAKE_PEM: String = format!("{}{}{}", "-----BEGIN ", "PRIVATE ", "KEY-----");
33    pub static ref HOSTILE: Vec<Probe> = {
34        let github = format!("ghp_{}", "A".repeat(36));
35        let aws = format!("AKIA{}", "A".repeat(16));
36        // Built from split tokens so no contiguous fake-credential literal is committed to source.
37        let bearer = format!("Bearer {}", "abcdABCD0123".repeat(2));
38        let slack = format!("xoxb-{}", "0123456789abcdef");
39        // A word-separated token near the truncation boundary: redact-first replaces it whole;
40        // truncate-first cuts it so the shape no longer matches and a raw `ghp_` fragment leaks.
41        let boundary = format!("{} ghp_{}", "x".repeat(239), "D".repeat(36));
42        vec![
43            p("github_pat", "secret", github.clone(), github),
44            p("aws_key", "secret", aws.clone(), aws),
45            p("bearer_token", "secret", bearer.clone(), bearer),
46            p("slack_token", "secret", slack.clone(), slack),
47            p("private_key", "secret", FAKE_PEM.clone(), FAKE_PEM.clone()),
48            p("email", "pii", "alice@example.com".to_string(), "alice@example.com".to_string()),
49            p("slack_user_id", "pii", "U01ABCDEFG".to_string(), "U01ABCDEFG".to_string()),
50            p("ansi_escape", "control", "\u{1b}[31mRED\u{1b}[0m".to_string(), "\u{1b}".to_string()),
51            p("c1_csi", "control", "\u{009b}31mRED".to_string(), "\u{009b}".to_string()),
52            p("unicode_bidi", "control", "\u{202e}reversed\u{202c}".to_string(), "\u{202e}".to_string()),
53            // Secret near the truncation boundary: guards redact-before-truncate.
54            p("long_secret_prefix", "secret", boundary, "ghp_".to_string()),
55        ]
56    };
57    pub static ref BENIGN: Vec<Probe> = vec![
58        p("fake_short_token", "benign", "tok_12345".to_string(), "tok_12345".to_string()),
59        p(
60            "schema_text",
61            "benign",
62            "assay.mcp_server_inventory.v0".to_string(),
63            "mcp_server_inventory".to_string(),
64        ),
65        p(
66            "ordinary_uuid",
67            "benign",
68            "id 123e4567-e89b-12d3-a456-426614174000".to_string(),
69            "123e4567".to_string(),
70        ),
71        p(
72            "content_hash",
73            "benign",
74            format!("digest sha256:{}", "a".repeat(64)),
75            "sha256:".to_string(),
76        ),
77        p(
78            "non_secret_path",
79            "benign",
80            "/usr/local/bin/assay".to_string(),
81            "/usr/local/bin/assay".to_string(),
82        ),
83    ];
84}
85
86/// Deterministic content digest over the corpus (names + inputs), so the conformance report binds the
87/// exact corpus it was run against.
88pub fn corpus_digest() -> String {
89    let value = json!({
90        "hostile": HOSTILE.iter().map(|x| json!({"name": x.name, "input": x.input})).collect::<Vec<_>>(),
91        "benign": BENIGN.iter().map(|x| json!({"name": x.name, "input": x.input})).collect::<Vec<_>>(),
92    });
93    let bytes = serde_jcs::to_vec(&value).expect("corpus is JSON-serializable");
94    format!("sha256:{}", hex::encode(Sha256::digest(&bytes)))
95}