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