use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static PROGRESS_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^(Downloading|Updating|Fetching|Loading|Detecting|Checking) [^\n]+\n?")
.unwrap()
});
pub fn compress_trivy(subcmd: &str, raw: &str) -> String {
let cleaned = compactor::normalise(raw);
if cleaned.trim_start().starts_with('{') && cleaned.contains("\"Vulnerabilities\"") {
return compress_trivy_json(&cleaned);
}
let _ = subcmd; compress_trivy_text(&cleaned)
}
fn compress_trivy_text(raw: &str) -> String {
let s = PROGRESS_RE.replace_all(raw, "");
let mut critical: Vec<String> = Vec::new();
let mut high: Vec<String> = Vec::new();
let mut medium_count = 0usize;
let mut low_count = 0usize;
let mut other_lines: Vec<&str> = Vec::new();
for line in s.lines() {
let t = line.trim();
if t.is_empty() {
continue;
}
if t.starts_with("Total:") || t.starts_with("Legend:") || t.contains("scan") {
continue;
}
if t.starts_with("==") || t.starts_with("Target:") || t.starts_with("Type:") {
other_lines.push(line);
continue;
}
let tl = t.to_uppercase();
if tl.contains("CRITICAL") {
critical.push(t.to_string());
} else if tl.contains("HIGH") {
high.push(t.to_string());
} else if tl.contains("MEDIUM") {
medium_count += 1;
} else if tl.contains("LOW") || tl.contains("NEGLIGIBLE") || tl.contains("UNKNOWN") {
low_count += 1;
}
}
if critical.is_empty() && high.is_empty() && medium_count == 0 && low_count == 0 {
let target = other_lines
.iter()
.find(|l| l.contains("Target:"))
.copied()
.unwrap_or("");
return format!(
"trivy: no vulnerabilities found{}",
if target.is_empty() {
String::new()
} else {
format!(" ({target})")
}
);
}
let mut result: Vec<String> = Vec::new();
for l in &other_lines {
result.push(l.to_string());
}
let mut parts: Vec<String> = Vec::new();
if !critical.is_empty() {
parts.push(format!("{} CRITICAL", critical.len()));
}
if !high.is_empty() {
parts.push(format!("{} HIGH", high.len()));
}
if medium_count > 0 {
parts.push(format!("{medium_count} MEDIUM"));
}
if low_count > 0 {
parts.push(format!("{low_count} LOW/UNKNOWN (suppressed)"));
}
result.push(parts.join(", "));
result.extend(critical);
result.extend(high);
if medium_count > 0 {
result.push(format!(
"… {medium_count} MEDIUM vulnerabilities (use trivy --severity MEDIUM to view)"
));
}
result.join("\n")
}
fn compress_trivy_json(raw: &str) -> String {
use once_cell::sync::Lazy;
use regex::Regex;
static VUL_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#""Severity"\s*:\s*"(CRITICAL|HIGH|MEDIUM|LOW|UNKNOWN)""#).unwrap()
});
static PKG_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#""PkgName"\s*:\s*"([^"]+)""#).unwrap());
static CVE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r#""VulnerabilityID"\s*:\s*"(CVE-[^"]+)""#).unwrap());
let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
for cap in VUL_RE.captures_iter(raw) {
*counts
.entry(cap.get(1).map(|m| m.as_str()).unwrap_or("UNKNOWN"))
.or_insert(0) += 1;
}
if counts.is_empty() {
return "trivy [json]: no vulnerabilities found".to_string();
}
let pkgs: Vec<&str> = PKG_RE
.captures_iter(raw)
.filter_map(|c| c.get(1).map(|m| m.as_str()))
.take(10)
.collect();
let cves: Vec<&str> = CVE_RE
.captures_iter(raw)
.filter_map(|c| c.get(1).map(|m| m.as_str()))
.take(10)
.collect();
let total: usize = counts.values().sum();
let summary = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"]
.iter()
.filter_map(|s| counts.get(s).map(|c| format!("{c} {s}")))
.collect::<Vec<_>>()
.join(", ");
let pairs: Vec<String> = pkgs
.iter()
.zip(cves.iter())
.map(|(p, c)| format!("{p} ({c})"))
.collect();
format!(
"trivy [json]: {total} vulnerabilities — {summary}\n{}{}",
pairs.join(", "),
if total > 10 {
format!("\n… {} more", total - 10)
} else {
String::new()
}
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_progress_lines() {
let raw = "Downloading vulnerability database...\nUpdating vulnerability database...\nCRITICAL CVE-2024-1234 openssl 1.0\n";
let out = compress_trivy("image", raw);
assert!(!out.contains("Downloading"), "{out}");
assert!(out.contains("CRITICAL"), "{out}");
}
#[test]
fn suppresses_medium_low() {
let raw = "CRITICAL CVE-2024-0001 libssl bad\nHIGH CVE-2024-0002 zlib bad\nMEDIUM CVE-2024-0003 curl ok\nMEDIUM CVE-2024-0004 curl ok\nLOW CVE-2024-0005 gzip ok\n";
let out = compress_trivy("image", raw);
assert!(out.contains("CRITICAL"), "{out}");
assert!(out.contains("HIGH"), "{out}");
assert!(!out.contains("CVE-2024-0003"), "{out}"); assert!(out.contains("2 MEDIUM") || out.contains("MEDIUM"), "{out}");
}
#[test]
fn no_cves_clean_message() {
let raw = "Target: nginx:latest\nType: debian\nTotal: 0 (HIGH: 0, CRITICAL: 0)\n";
let out = compress_trivy("image", raw);
assert!(out.contains("no vulnerabilities"), "{out}");
}
#[test]
fn json_mode_extracts_counts() {
let raw = r#"{"Results":[{"Vulnerabilities":[{"VulnerabilityID":"CVE-2024-0001","PkgName":"openssl","Severity":"CRITICAL"},{"VulnerabilityID":"CVE-2024-0002","PkgName":"zlib","Severity":"HIGH"}]}]}"#;
let out = compress_trivy("image", raw);
assert!(out.contains("json") || out.contains("CRITICAL"), "{out}");
}
}