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