lean_ctx_patterns/
grype.rs1use crate::ansi::strip_ansi;
11
12pub fn compress(_cmd: &str, output: &str) -> Option<String> {
13 let trimmed = output.trim();
14 if trimmed.is_empty() {
15 return Some("grype: ok".to_string());
16 }
17 if trimmed.contains("No vulnerabilities found") {
18 return Some("grype: no vulnerabilities".to_string());
19 }
20
21 let lines: Vec<String> = trimmed
22 .lines()
23 .map(|l| strip_ansi(l).trim_end().to_string())
24 .collect();
25
26 let header = lines.iter().position(|l| {
27 let u = l.to_ascii_uppercase();
28 u.contains("VULNERABILITY") && u.contains("SEVERITY")
29 })?;
30
31 let order = ["Critical", "High", "Medium", "Low", "Negligible", "Unknown"];
33 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
34 let mut serious: Vec<String> = Vec::new();
35 let mut total = 0usize;
36
37 for line in &lines[header + 1..] {
38 let cols = split_cols(line);
39 if cols.len() < 3 {
40 continue;
41 }
42 let severity = cols[cols.len() - 1].clone();
43 let vuln = cols[cols.len() - 2].clone();
44 let name = cols[0].clone();
45 total += 1;
46 *counts.entry(severity.clone()).or_default() += 1;
47 if severity.eq_ignore_ascii_case("Critical") || severity.eq_ignore_ascii_case("High") {
48 serious.push(format!(" {name} {vuln} {severity}"));
49 }
50 }
51
52 if total == 0 {
53 return Some("grype: no vulnerabilities".to_string());
54 }
55
56 let hist: Vec<String> = order
57 .iter()
58 .filter_map(|sev| {
59 counts
60 .iter()
61 .find(|(k, _)| k.eq_ignore_ascii_case(sev))
62 .map(|(_, n)| format!("{sev}: {n}"))
63 })
64 .collect();
65
66 let mut parts = vec![format!("grype: {total} vulns ({})", hist.join(", "))];
67 parts.extend(serious.into_iter().take(15));
68 Some(parts.join("\n"))
69}
70
71fn split_cols(line: &str) -> Vec<String> {
73 line.split(" ")
74 .map(str::trim)
75 .filter(|s| !s.is_empty())
76 .map(str::to_string)
77 .collect()
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 const SCAN: &str = " ✔ Vulnerability DB [updated]\n ✔ Scanned for vulnerabilities [45 vulnerabilities]\nNAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY\nlibssl1.1 1.1.1n 1.1.1w deb CVE-2023-1234 Critical\nzlib1g 1.2.11 1.2.13 deb CVE-2022-5678 High\ncurl 7.74.0 7.88.0 deb CVE-2021-1111 Low\n";
85
86 #[test]
87 fn histogram_and_serious_rows() {
88 let r = compress("grype nginx", SCAN).unwrap();
89 assert!(r.contains("grype: 3 vulns"), "{r}");
90 assert!(r.contains("Critical: 1"), "{r}");
91 assert!(r.contains("High: 1"), "{r}");
92 assert!(r.contains("Low: 1"), "{r}");
93 assert!(r.contains("CVE-2023-1234"), "keeps critical row: {r}");
94 assert!(!r.contains("CVE-2021-1111"), "drops low row detail: {r}");
95 assert!(!r.contains("✔"), "drops progress: {r}");
96 }
97
98 #[test]
99 fn no_vulns() {
100 let r = compress("grype nginx", " ✔ Scanned\nNo vulnerabilities found").unwrap();
101 assert_eq!(r, "grype: no vulnerabilities");
102 }
103
104 #[test]
105 fn empty_is_ok() {
106 assert_eq!(compress("grype nginx", "").unwrap(), "grype: ok");
107 }
108}