pub fn secret_class(line: &str) -> Option<&'static str> {
let lower = line.to_ascii_lowercase();
if lower.contains("kernel command line") || lower.contains("/proc/cmdline") || lower.contains("korp.args") || lower.contains("korp.log_udp=") || lower.contains("korp.env.") || lower.contains("clock_key=") {
return Some("cmdline");
}
if lower.contains("token") || lower.contains("bearer ") || lower.contains("password") || lower.contains("secret") || stripe_secret(&lower) || has_url_userinfo(line) {
return Some("token");
}
if lower.contains("private key") || lower.contains("begin openssh") || lower.contains("ssh-ed25519") || lower.contains("ssh-rsa") || lower.contains("signing.key") || lower.contains("time_ed25519") {
return Some("key");
}
if lower.contains("pubkey") || lower.contains("public key") || lower.contains("sha256:") {
return Some("pubkey");
}
if lower.contains(".env") {
return Some("env");
}
if has_env_record(line) {
return Some("envrecord");
}
if longest_opaque_run(line) >= 40 {
return Some("highentropy");
}
None
}
fn stripe_secret(lower: &str) -> bool {
["sk_live_", "sk_test_", "rk_live_", "rk_test_", "whsec_"].iter().any(|p| lower.contains(p))
}
fn has_env_record(line: &str) -> bool {
line.split_whitespace().any(|tok| {
let Some((name, value)) = tok.split_once('=') else { return false };
!value.is_empty() && name.len() >= 3 && name.starts_with(|c: char| c.is_ascii_uppercase()) && name.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
})
}
fn longest_opaque_run(line: &str) -> usize {
let mut best = 0usize;
let mut run = 0usize;
for c in line.chars() {
if c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=' {
run += 1;
best = best.max(run);
} else {
run = 0;
}
}
best
}
fn withhold_runs(line: &str) -> String {
let mut out = String::with_capacity(line.len());
let mut run = String::new();
let flush = |run: &mut String, out: &mut String| {
if run.chars().count() >= 40 {
out.push_str(VALUE_WITHHELD);
} else {
out.push_str(run);
}
run.clear();
};
for c in line.chars() {
if c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=' {
run.push(c);
} else {
flush(&mut run, &mut out);
out.push(c);
}
}
flush(&mut run, &mut out);
out
}
pub fn withheld(class: &str, bytes: usize) -> String {
format!("<<WITHHELD: a {class}-class line of {bytes} bytes was not recorded>>")
}
pub fn withheld_run(class: &str, count: u64, bytes: u64) -> String {
if count == 1 {
return withheld(class, bytes as usize);
}
format!("<<WITHHELD: {count} {class}-class lines in a row, {bytes} bytes, were not recorded>>")
}
pub const VALUE_WITHHELD: &str = "<withheld>";
pub fn is_marker(line: &str) -> bool {
line.starts_with("<<WITHHELD: ")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Cleaned<'a> {
Kept(std::borrow::Cow<'a, str>),
Withheld { class: &'static str, bytes: usize },
}
pub fn cleaned(line: &str) -> Cleaned<'_> {
if is_marker(line) {
return Cleaned::Kept(std::borrow::Cow::Borrowed(line));
}
match secret_class(line) {
None => Cleaned::Kept(std::borrow::Cow::Borrowed(line)),
Some("pubkey") => match redact_values(line) {
Some(kept) => Cleaned::Kept(std::borrow::Cow::Owned(kept)),
None => Cleaned::Kept(std::borrow::Cow::Borrowed(line)),
},
Some("highentropy") => Cleaned::Kept(std::borrow::Cow::Owned(withhold_runs(line))),
Some("token") => match token_line_kept(line) {
Some(kept) => Cleaned::Kept(std::borrow::Cow::Owned(kept)),
None => Cleaned::Withheld { class: "token", bytes: line.len() },
},
Some(class) => Cleaned::Withheld { class, bytes: line.len() },
}
}
pub fn clean(line: &str) -> std::borrow::Cow<'_, str> {
match cleaned(line) {
Cleaned::Kept(kept) => kept,
Cleaned::Withheld { class, bytes } => std::borrow::Cow::Owned(withheld(class, bytes)),
}
}
pub fn clean_lines<'a, I: IntoIterator<Item = &'a str>>(lines: I) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut run: Option<(&'static str, u64, u64)> = None;
for line in lines {
match cleaned(line) {
Cleaned::Kept(kept) => {
out.push(kept.into_owned());
run = None;
}
Cleaned::Withheld { class, bytes } => {
if let Some((c, n, b)) = run.as_mut() {
if *c == class {
*n += 1;
*b += bytes as u64;
let text = withheld_run(class, *n, *b);
if let Some(last) = out.last_mut() {
*last = text;
}
continue;
}
}
out.push(withheld(class, bytes));
run = Some((class, 1, bytes as u64));
}
}
}
out
}
fn names_a_secret(word: &str) -> bool {
let w = word.to_ascii_lowercase();
["token", "bearer", "password", "passwd", "secret", "authorization", "credential", "apikey", "api_key", "api-key"].iter().any(|n| w.contains(n))
}
const PLAIN_AFTER_A_NAME: &[&str] = &[
"a", "an", "the", "is", "was", "are", "were", "be", "been", "has", "have", "had", "it", "its", "this", "that", "and", "or", "not", "no", "for", "from", "to", "in", "of", "on", "at",
"by", "with", "as", "into", "over", "via", "missing", "incorrect", "accepted", "refused", "rejected", "expired", "unfed", "fed", "declared", "written", "set", "unset", "required", "invalid",
"empty", "ok", "file", "rotated", "revoked", "minted", "read", "loaded", "saved", "changed", "unchanged", "present", "absent", "given", "wrong", "failed", "denied", "mismatch", "too",
"short", "weak", "unknown", "yet", "never", "again", "now", "still", "already", "issued", "verified", "checked", "matched", "kept", "dropped", "cleared", "renewed", "sealed", "derived",
"offered", "waits", "waiting", "until", "after", "before", "since", "when", "while",
];
fn plain_after_a_name(word: &str) -> bool {
let w = word.trim_matches(|c: char| !c.is_ascii_alphanumeric()).to_ascii_lowercase();
PLAIN_AFTER_A_NAME.contains(&w.as_str())
}
fn kv_sep(seg: &str) -> Option<usize> {
seg.char_indices().find(|&(i, c)| (c == '=' || c == ':') && !seg[i..].starts_with("://")).map(|(i, _)| i)
}
fn withhold_userinfo(seg: &str) -> Option<String> {
let at = seg.find("://")? + 3;
let rest = &seg[at..];
let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
let auth = &rest[..end];
let amp = auth.rfind('@')?;
let colon = auth[..amp].find(':')?;
let pass = &auth[colon + 1..amp];
(!pass.is_empty() && pass != VALUE_WITHHELD).then(|| format!("{}{}{VALUE_WITHHELD}{}", &seg[..at], &auth[..=colon], &seg[at + amp..]))
}
fn has_url_userinfo(line: &str) -> bool {
line.split_whitespace().any(|w| withhold_userinfo(w).is_some())
}
fn withhold_kv_parts(w: &str) -> Option<String> {
let mut out = String::with_capacity(w.len());
let mut changed = false;
for (i, seg) in w.split(['?', '&', ';']).enumerate() {
if i > 0 {
let at: usize = w.split(['?', '&', ';']).take(i).map(|s| s.len() + 1).sum::<usize>() - 1;
out.push_str(&w[at..at + 1]);
}
let seg = match withhold_userinfo(seg) {
Some(s) => {
changed = true;
std::borrow::Cow::Owned(s)
}
None => std::borrow::Cow::Borrowed(seg),
};
match kv_sep(&seg).filter(|&at| names_a_secret(&seg[..at])) {
Some(at) if !matches!(seg[at + 1..].trim_matches(|c: char| matches!(c, '"' | '\'' | '`' | ',' | '}' | ')')), "" | VALUE_WITHHELD) => {
out.push_str(&seg[..=at]);
out.push_str(VALUE_WITHHELD);
changed = true;
}
_ => out.push_str(&seg),
}
}
changed.then_some(out)
}
fn token_line_kept(line: &str) -> Option<String> {
let lower = line.to_ascii_lowercase();
let key_material = lower.contains("private key") || lower.contains("begin openssh") || lower.contains("ssh-ed25519") || lower.contains("ssh-rsa") || lower.contains("signing.key") || lower.contains("time_ed25519");
if stripe_secret(&lower) || key_material || lower.contains(".env") || has_env_record(line) {
return None;
}
let mut pieces: Vec<(bool, &str)> = Vec::new();
let mut start = 0;
let mut in_word = false;
for (i, c) in line.char_indices() {
let word_char = !c.is_whitespace();
if i > start && word_char != in_word {
pieces.push((in_word, &line[start..i]));
start = i;
}
in_word = word_char;
}
if start < line.len() {
pieces.push((in_word, &line[start..]));
}
let mut out = String::with_capacity(line.len());
let mut prev = "";
for (is_word, w) in pieces {
if !is_word {
out.push_str(w);
continue;
}
let bare = |x: &str| x.trim_matches(|c: char| !c.is_ascii_alphanumeric()).to_ascii_lowercase();
let has_alnum = w.chars().any(|c| c.is_ascii_alphanumeric());
let auth_scheme = matches!(bare(w).as_str(), "bearer" | "basic");
let prev_bare_name = names_a_secret(prev) && !prev.contains(['=', ':']);
let word = if w == VALUE_WITHHELD {
w.to_string()
} else if let Some(k) = withhold_kv_parts(w) {
k
} else if prev == "Basic" && has_alnum {
VALUE_WITHHELD.to_string()
} else if names_a_secret(prev) && prev.ends_with([':', '=']) && !auth_scheme && !names_a_secret(w) && has_alnum {
VALUE_WITHHELD.to_string()
} else if prev_bare_name && has_alnum && !names_a_secret(w) && !auth_scheme && !plain_after_a_name(w) {
VALUE_WITHHELD.to_string()
} else if longest_opaque_run(w) >= 16 {
VALUE_WITHHELD.to_string()
} else {
w.to_string()
};
out.push_str(&word);
prev = w;
}
Some(redact_values(&out).unwrap_or(out))
}
fn names_a_key(word: &str) -> bool {
let w = word.to_ascii_lowercase();
w.contains("pubkey") || w.contains("public_key") || w.contains("public-key") || w.contains("fingerprint")
}
fn looks_like_key(value: &str) -> bool {
value.to_ascii_lowercase().contains("sha256:") || longest_opaque_run(value) >= 16
}
fn short_value(word: &str) -> bool {
!word.is_empty()
&& word.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '=' | ':'))
&& word.chars().any(|c| c.is_ascii_digit())
}
fn redact_values(line: &str) -> Option<String> {
let words: Vec<&str> = line.split(' ').collect();
let mut out: Vec<String> = Vec::with_capacity(words.len());
let mut after_name = false;
for (i, w) in words.iter().enumerate() {
let word = if *w == VALUE_WITHHELD {
(*w).to_string()
} else if let Some((k, v)) = w.split_once('=') {
let bare = v.trim_matches('"');
if !bare.is_empty() && bare != VALUE_WITHHELD && (names_a_key(k) || looks_like_key(bare)) {
format!("{k}={VALUE_WITHHELD}")
} else {
(*w).to_string()
}
} else if looks_like_key(w) || (after_name && short_value(w)) {
VALUE_WITHHELD.to_string()
} else {
(*w).to_string()
};
let public_key = i > 0 && words[i - 1].eq_ignore_ascii_case("public") && w.eq_ignore_ascii_case("key");
after_name = !w.contains('=') && (names_a_key(w.trim_end_matches(':')) || public_key);
out.push(word);
}
let kept = out.join(" ");
(kept != line).then_some(kept)
}
#[cfg(test)]
mod tests {
#[test]
fn an_opaque_run_is_withheld_in_place_and_its_line_kept() {
let sha = "0123456789abcdef0123456789abcdef01234567";
let l = format!("2026-09-30T07:00:00.1Z ERROR gunnar::store: push rejected for {sha}: object missing");
match cleaned(&l) {
Cleaned::Kept(k) => {
assert!(!k.contains(sha), "{k}");
assert!(k.contains("ERROR gunnar::store: push rejected for <withheld>: object missing"), "{k}");
}
w => panic!("the line was withheld whole: {w:?}"),
}
let digest = "dba8e30fe8af4a881572f52144afa386df347e80f5029066f695810784e751dd";
let reg = format!("2026-09-30T07:00:00.1Z INFO gunnar_server::self_register: plugin: ADDED — gunnar blake3 {digest} event=\"plugin.self_register\"");
let kept = clean(®);
assert!(kept.contains("plugin: ADDED — gunnar blake3 <withheld>") && !kept.contains(digest), "{kept}");
let key = "AAAAC3NzaC1lZDI1NTE5AAAAIGx8r8b4v8v5r7q2Zk1pQ3aQ";
assert!(!clean(&format!("x {key} y")).contains(key));
assert_eq!(clean(&clean(®)), clean(®), "idempotent");
}
use super::*;
#[test]
fn a_line_that_names_a_token_is_kept_with_only_the_values_withheld() {
let w = VALUE_WITHHELD;
for (line, kept) in [
("pairing: waiting for holger_console_token from the shop".to_string(), "pairing: waiting for holger_console_token from the shop".to_string()),
(
"pair status: main: step serve (try 81: waiting for the server: the boot waits for the installer to feed `holger_console_token` (/data/holger-console-token is declared and not written yet)) · not paired".to_string(),
"pair status: main: step serve (try 81: waiting for the server: the boot waits for the installer to feed `holger_console_token` (/data/holger-console-token is declared and not written yet)) · not paired".to_string(),
),
("REFUSED [holger-console-token-unfed] the installer never fed the token in 3600 s".to_string(), "REFUSED [holger-console-token-unfed] the installer never fed the token in 3600 s".to_string()),
("password incorrect for user admin".to_string(), "password incorrect for user admin".to_string()),
("console: token=Q7zP2xY9abc accepted".to_string(), format!("console: token={w} accepted")),
("auth: Authorization: Bearer eyJhbGciOiJFZERTQSJ9 from 10.44.0.1".to_string(), format!("auth: Authorization: Bearer {w} from 10.44.0.1")),
("login: password: hunter2 refused".to_string(), format!("login: password: {w} refused")),
(r#"feed {"admin_token":"0123456789abcdef0123"} kept"#.to_string(), format!(r#"feed {{"admin_token":{w} kept"#)),
("fed the console token 9f8e7d6c5b4a39281706f5e4d3c2b1a0 to main".to_string(), format!("fed the console token {w} to main")),
] {
assert_eq!(secret_class(&line), Some("token"), "{line}");
assert_eq!(clean(&line), kept, "{line}");
assert_eq!(clean(&clean(&line)), clean(&line), "idempotent: {line}");
}
for line in [
"monetize-server: stripe: POST /v1/checkout/sessions with sk_live_51HabcDEF token",
"GUNNAR_UI_ADMIN_TOKEN=xyz",
"reading GUNNAR_UI_ADMIN_TOKEN=abc from the env",
"token in -----BEGIN OPENSSH PRIVATE KEY-----",
"the secret is in /etc/monetize/monetize.env",
] {
assert!(clean(line).starts_with("<<WITHHELD: a token-class"), "{line} -> {}", clean(line));
}
let out = clean_lines(["GUNNAR_UI_ADMIN_TOKEN=a", "GUNNAR_UI_ADMIN_TOKEN=b", "waiting for the token", "GUNNAR_UI_ADMIN_TOKEN=c"]);
assert_eq!(out, vec![withheld_run("token", 2, 46), "waiting for the token".to_string(), withheld("token", 23)]);
}
#[test]
fn a_url_secret_a_plain_value_after_a_name_and_a_tab_separated_value_are_withheld() {
let w = VALUE_WITHHELD;
for (line, kept) in [
("GET https://x.example/cb?token=ab12cd&state=1 200".to_string(), format!("GET https://x.example/cb?token={w}&state=1 200")),
("redirect https://x.example/cb?state=1&access_token=Zz&client_secret=q;password=p".to_string(), format!("redirect https://x.example/cb?state=1&access_token={w}&client_secret={w};password={w}")),
("clone https://user:hunter@git.example/r.git".to_string(), format!("clone https://user:{w}@git.example/r.git")),
("login: password correcthorsebattery refused".to_string(), format!("login: password {w} refused")),
("secret letmein".to_string(), format!("secret {w}")),
("console token\tq7zp2xy9abc\tok".to_string(), format!("console token\t{w}\tok")),
("password\tincorrect for admin".to_string(), "password\tincorrect for admin".to_string()),
("the console token is missing: holger_console_token was never fed".to_string(), "the console token is missing: holger_console_token was never fed".to_string()),
("REFUSED the bearer expired at 18:12Z".to_string(), "REFUSED the bearer expired at 18:12Z".to_string()),
(
"INFO gunnar_ui::auth: admin token 3f9a8b7c6d5e4f30a1b2c3d4e5f60718 issued x-gunnar-request-id=r-4711".to_string(),
format!("INFO gunnar_ui::auth: admin token {w} issued x-gunnar-request-id=r-4711"),
),
(
"WARN gunnar_front: console bearer dGhpc2lzYXZlcnlsb25nYmFzZTY0c2VjcmV0 refused x-gunnar-request-id: r-4712".to_string(),
format!("WARN gunnar_front: console bearer {w} refused x-gunnar-request-id: r-4712"),
),
] {
assert_eq!(clean(&line), kept, "{line}");
assert_eq!(clean(&clean(&line)), clean(&line), "idempotent: {line}");
}
}
#[test]
fn progress_is_kept_and_every_secret_class_is_withheld_without_its_content() {
assert_eq!(clean("korp-start: stage net up at 6.34 s"), "korp-start: stage net up at 6.34 s");
for (line, class) in [
("[ 0.000000] Kernel command line: korp.args=monetize_pubkey=abc", "cmdline"),
("GUNNAR_UI_ADMIN_TOKEN=xyz", "token"),
("-----BEGIN OPENSSH PRIVATE KEY-----", "key"),
("reading /etc/monetize/monetize.env", "env"),
("monetize-server: stripe: POST /v1/checkout/sessions with sk_live_51Habc", "token"),
("monetize-server: hmac key whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH", "token"),
("monetize-server: vendor key rk_test_abc", "token"),
] {
assert_eq!(secret_class(line), Some(class), "{line}");
let c = clean(line);
assert!(c.starts_with("<<WITHHELD: a ") && c.contains(class) && !c.contains(line), "{c}");
assert_eq!(clean(&c), c, "a marker cleans to itself");
}
let run = "AAAAC3NzaC1lZDI1NTE5AAAAIGx8r8b4v8v5r7q2Zk1pQ3a";
assert_eq!(secret_class(run), Some("highentropy"));
assert_eq!(clean(run), VALUE_WITHHELD);
}
#[test]
fn an_environment_record_is_withheld_and_names_no_value() {
for (line, class) in [
("kmsg 6,368,90210,-;with environment: HOME=/ TERM=linux KORP_INSTALLER_ROOT=/run/installer", "envrecord"),
("kmsg 6,369,90211,-;with environment: MONETIZE_VERIFY_KEY=MCowBQYDK2VwAyEA", "envrecord"),
("box: STRIPE_ACCOUNT=acct_1Qx9 MONETIZE_SEAL=q7Zp2", "envrecord"),
("app @4s korp.env.GUNNAR_DATA_ROOT applied", "cmdline"),
] {
assert_eq!(secret_class(line), Some(class), "{line}");
let c = clean(line);
assert!(c.starts_with("<<WITHHELD: a ") && c.contains(class) && !c.contains("q7Zp2") && !c.contains("MCowBQ"), "{c}");
}
assert_eq!(secret_class(r#"INFO gunnar::audit: served event="grpc.call" decision="allow" elapsed_ms=3"#), None);
assert_eq!(secret_class("HTTP/1.1 200 OK"), None);
}
#[test]
fn a_pubkey_line_is_kept_with_its_values_withheld_in_place() {
let w = VALUE_WITHHELD;
for (line, kept) in [
("fed monetize pubkey eb16".to_string(), format!("fed monetize pubkey {w}")),
(
"WARN gunnar::audit: event=\"ssh.auth.accepted\" fingerprint=SHA256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU principal=admin".to_string(),
format!("WARN gunnar::audit: event=\"ssh.auth.accepted\" fingerprint={w} principal=admin"),
),
("feed: monetize_pubkey=abc public key 1f2e3d4c".to_string(), format!("feed: monetize_pubkey={w} public key {w}")),
("public key revoked".to_string(), "public key revoked".to_string()),
] {
assert_eq!(secret_class(&line), Some("pubkey"), "{line}");
let c = clean(&line);
assert_eq!(c, kept);
assert_eq!(clean(&c), c, "idempotent: the VM and the installer both clean");
}
}
#[test]
fn a_run_of_withheld_lines_is_one_counted_marker() {
let lines = ["boot", "A_TOKEN=1", "B_TOKEN=22", "reading x.env", "up", "C_TOKEN=3", "D_TOKEN=4"];
let out = clean_lines(lines);
assert_eq!(
out,
vec![
"boot".to_string(),
withheld_run("token", 2, 19),
withheld("env", 13),
"up".to_string(),
withheld_run("token", 2, 18),
]
);
assert!(out.iter().all(|l| clean(l) == l.as_str()), "markers clean to themselves");
assert_eq!(withheld_run("key", 1, 7), withheld("key", 7));
}
}