lean_ctx_patterns/
syft.rs1use crate::ansi::strip_ansi;
10
11pub fn compress(_cmd: &str, output: &str) -> Option<String> {
12 let trimmed = output.trim();
13 if trimmed.is_empty() {
14 return Some("syft: ok".to_string());
15 }
16
17 let lines: Vec<String> = trimmed
18 .lines()
19 .map(|l| strip_ansi(l).trim_end().to_string())
20 .collect();
21
22 let header = lines.iter().position(|l| {
23 let u = l.to_ascii_uppercase();
24 u.contains("NAME") && u.contains("VERSION") && u.contains("TYPE")
25 })?;
26
27 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
28 let mut total = 0usize;
29 for line in &lines[header + 1..] {
30 let cols = split_cols(line);
31 if cols.len() < 2 {
32 continue;
33 }
34 let pkg_type = cols[cols.len() - 1].clone();
35 total += 1;
36 *counts.entry(pkg_type).or_default() += 1;
37 }
38
39 if total == 0 {
40 return None;
41 }
42
43 let mut hist: Vec<(String, usize)> = counts.into_iter().collect();
45 hist.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
46 let breakdown: Vec<String> = hist.iter().map(|(t, n)| format!("{t}: {n}")).collect();
47
48 Some(format!("syft: {total} packages ({})", breakdown.join(", ")))
49}
50
51fn split_cols(line: &str) -> Vec<String> {
52 line.split(" ")
53 .map(str::trim)
54 .filter(|s| !s.is_empty())
55 .map(str::to_string)
56 .collect()
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 const SBOM: &str = " ✔ Parsed image\n ✔ Cataloged packages [4 packages]\nNAME VERSION TYPE\nadduser 3.118 deb\napt 2.6.1 deb\nlodash 4.17.21 npm\nflask 2.3.0 python\n";
64
65 #[test]
66 fn counts_packages_by_type() {
67 let r = compress("syft nginx", SBOM).unwrap();
68 assert!(r.contains("syft: 4 packages"), "{r}");
69 assert!(r.contains("deb: 2"), "{r}");
70 assert!(r.contains("npm: 1"), "{r}");
71 assert!(r.contains("python: 1"), "{r}");
72 assert!(!r.contains("adduser"), "drops package list: {r}");
73 }
74
75 #[test]
76 fn empty_is_ok() {
77 assert_eq!(compress("syft nginx", "").unwrap(), "syft: ok");
78 }
79}