use std::sync::OnceLock;
use ahash::AHashSet;
use regex::{Regex, RegexSet};
fn credential_set() -> &'static RegexSet {
static SET: OnceLock<RegexSet> = OnceLock::new();
SET.get_or_init(|| {
RegexSet::new([
r"AKIA[0-9A-Z]{16}", r"ASIA[0-9A-Z]{16}", r"sk-ant-[a-zA-Z0-9_\-]{20,}", r"sk-[a-zA-Z0-9]{20,}", r"ghp_[a-zA-Z0-9]{36}", r"gho_[a-zA-Z0-9]{36}", r"ghu_[a-zA-Z0-9]{36}", r"ghs_[a-zA-Z0-9]{36}", r"ghr_[a-zA-Z0-9]{36}", r"github_pat_[a-zA-Z0-9_]{80,}", r"npm_[a-zA-Z0-9]{36}", r"hf_[a-zA-Z0-9]{34}", r"xox[baprs]-[0-9A-Za-z-]{10,}", r"sk_live_[a-zA-Z0-9]{24,}", r"rk_live_[a-zA-Z0-9]{24,}", r"AIza[0-9A-Za-z_\-]{35}", r"ya29\.[0-9A-Za-z_\-]{20,}", r"eyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}", r"-----BEGIN [A-Z ]*PRIVATE KEY-----", r"(?i)(?:postgres|postgresql|mysql|mongodb|mongodb\+srv|redis|amqp)://[^:\s/]+:[^@\s]+@", r"(?i)https?://[^:\s/@]+:[^@\s]+@", r"(?i)\b(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key)\s*[=:]\s*\S+", r"(?i)Bearer\s+[a-zA-Z0-9\-._~+/]+=*", ])
.expect("static credential patterns compile")
})
}
fn error_set() -> &'static RegexSet {
static SET: OnceLock<RegexSet> = OnceLock::new();
SET.get_or_init(|| {
RegexSet::new([
r"(?i)\berror\s*:",
r"(?i)\bfatal\s*:",
r"(?i)\bpanic\s*:",
r"(?i)\bexception\b",
r"(?i)\btraceback\b",
r"\bFAILED\b",
r"(?i)\bsegmentation fault\b",
r"(?i)\bunhandled\b",
r"(?i)\bnpm\s+err!",
r"(?i)\b(?:Syntax|Value|Key|Type|File\s*Not\s*Found|Module\s*Not\s*Found|Runtime|Reference|Range)Error\b",
r"\berror\[",
r"(?i)\b(?:command not found|permission denied|no such file or directory)\b",
r"(?i)\b(?:SIGKILL|SIGABRT|SIGSEGV|Killed|Aborted)\b",
])
.expect("static error patterns compile")
})
}
fn ansi_csi_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"\x1b\[[0-9;?]*[ -/]*[@-~]").expect("ansi csi compiles"))
}
fn ansi_osc8_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
Regex::new(r"\x1b\]8;[^\x07]*\x07([^\x1b]*)\x1b\]8;;\x07").expect("ansi osc8 compiles")
})
}
pub fn strip_ansi(text: &str) -> String {
if memchr::memchr(0x1b, text.as_bytes()).is_none() {
return text.to_string();
}
let delinked = ansi_osc8_re().replace_all(text, "$1");
ansi_csi_re().replace_all(&delinked, "").into_owned()
}
pub fn preserved_line_indices(text: &str) -> AHashSet<usize> {
let creds = credential_set();
let errors = error_set();
let mut preserved = AHashSet::new();
for (i, line) in text.lines().enumerate() {
if creds.is_match(line) || errors.is_match(line) {
preserved.insert(i);
}
}
preserved
}
pub fn contains_credential(text: &str) -> bool {
let creds = credential_set();
text.lines().any(|line| creds.is_match(line))
}
pub fn is_error_line(line: &str) -> bool {
error_set().is_match(line)
}
pub fn looks_like_failure(text: &str) -> bool {
error_set().is_match(text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_ansi_keeps_visible_text() {
let colored = "\x1b[31mred error\x1b[0m tail";
assert_eq!(strip_ansi(colored), "red error tail");
}
#[test]
fn strip_ansi_keeps_osc8_label() {
let link = "before \x1b]8;;https://x\x07ghp_visiblelabel\x1b]8;;\x07 after";
assert_eq!(strip_ansi(link), "before ghp_visiblelabel after");
}
#[test]
fn strip_ansi_noop_without_escape() {
assert_eq!(strip_ansi("plain text"), "plain text");
}
#[test]
fn detects_aws_key_line() {
let idx = preserved_line_indices("clean line\nkey AKIAIOSFODNN7EXAMPLE here\nmore");
assert!(idx.contains(&1), "AWS key line must be preserved");
assert_eq!(idx.len(), 1);
}
#[test]
fn detects_github_pat_and_private_key() {
let pat = format!("token {} done", "ghp_".to_string() + &"a".repeat(36));
assert!(contains_credential(&pat), "ghp_ PAT must match");
assert!(
contains_credential("-----BEGIN RSA PRIVATE KEY-----"),
"PEM header must match"
);
}
#[test]
fn detects_generic_secret_assignment() {
assert!(contains_credential("export password=hunter2"));
assert!(contains_credential("API_KEY: abc123def456"));
assert!(!contains_credential("the password field is empty"));
}
#[test]
fn looks_like_failure_on_error_marker() {
assert!(looks_like_failure("warming up\nerror: build failed\ndone"));
assert!(looks_like_failure("Traceback (most recent call last):"));
assert!(!looks_like_failure("everything is fine\nno problems here"));
}
#[test]
fn looks_like_failure_on_npm_err_bang() {
assert!(looks_like_failure(
"npm ERR! code ELIFECYCLE\nnpm ERR! errno 1"
));
}
#[test]
fn looks_like_failure_on_named_error_classes() {
assert!(looks_like_failure("SyntaxError: unexpected token"));
assert!(looks_like_failure("ValueError"));
assert!(looks_like_failure("KeyError"));
assert!(looks_like_failure("TypeError"));
assert!(looks_like_failure("FileNotFoundError"));
assert!(looks_like_failure("ModuleNotFoundError"));
assert!(looks_like_failure("RuntimeError"));
assert!(looks_like_failure("ReferenceError"));
assert!(looks_like_failure("RangeError"));
}
#[test]
fn looks_like_failure_on_rust_bracketed_diagnostic() {
assert!(looks_like_failure("error[E0599]: no method named foo"));
}
#[test]
fn looks_like_failure_on_shell_phrases() {
assert!(looks_like_failure("bash: frobnicate: command not found"));
assert!(looks_like_failure("open config: permission denied"));
assert!(looks_like_failure("cat x: No such file or directory"));
}
#[test]
fn looks_like_failure_on_fatal_signals() {
assert!(looks_like_failure("worker received SIGKILL"));
assert!(looks_like_failure("trace trap: SIGABRT"));
assert!(looks_like_failure("Segfault SIGSEGV at 0x0"));
assert!(looks_like_failure("Killed"));
assert!(looks_like_failure("Aborted (core dumped)"));
}
}