sbom_tools/diff/changes/
vulnerabilities.rs1use crate::diff::VulnerabilityDetail;
4use crate::diff::traits::{ChangeComputer, ComponentMatches, VulnerabilityChangeSet};
5use crate::model::{CanonicalId, NormalizedSbom};
6use std::collections::{HashMap, HashSet, VecDeque};
7
8pub struct VulnerabilityChangeComputer;
10
11impl VulnerabilityChangeComputer {
12 #[must_use]
14 pub const fn new() -> Self {
15 Self
16 }
17}
18
19impl Default for VulnerabilityChangeComputer {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25fn compute_depths(sbom: &NormalizedSbom) -> HashMap<CanonicalId, u32> {
28 let mut depths = HashMap::with_capacity(sbom.components.len());
29
30 let mut edges: HashMap<&CanonicalId, Vec<&CanonicalId>> =
32 HashMap::with_capacity(sbom.components.len());
33 let mut has_parents: HashSet<&CanonicalId> = HashSet::with_capacity(sbom.components.len());
34
35 for edge in &sbom.edges {
36 edges.entry(&edge.from).or_default().push(&edge.to);
37 has_parents.insert(&edge.to);
38 }
39
40 let roots: Vec<&CanonicalId> = sbom
42 .components
43 .keys()
44 .filter(|id| !has_parents.contains(id))
45 .collect();
46
47 let mut queue: VecDeque<(&CanonicalId, u32)> = VecDeque::new();
49
50 for root in &roots {
52 queue.push_back((*root, 0));
53 }
54
55 while let Some((id, depth)) = queue.pop_front() {
56 if let Some(&existing) = depths.get(id)
58 && depth >= existing
59 {
60 continue;
61 }
62 depths.insert(id.clone(), depth);
63
64 if let Some(children) = edges.get(id) {
66 for child in children {
67 let child_depth = depth + 1;
68 if depths.get(*child).is_none_or(|&d| d > child_depth) {
70 queue.push_back((*child, child_depth));
71 }
72 }
73 }
74 }
75
76 depths
77}
78
79impl ChangeComputer for VulnerabilityChangeComputer {
80 type ChangeSet = VulnerabilityChangeSet;
81
82 fn compute(
83 &self,
84 old: &NormalizedSbom,
85 new: &NormalizedSbom,
86 _matches: &ComponentMatches,
87 ) -> VulnerabilityChangeSet {
88 let mut result = VulnerabilityChangeSet::new();
89
90 let old_depths = compute_depths(old);
92 let new_depths = compute_depths(new);
93
94 let old_vuln_count: usize = old
96 .components
97 .values()
98 .map(|c| c.vulnerabilities.len())
99 .sum();
100 let new_vuln_count: usize = new
101 .components
102 .values()
103 .map(|c| c.vulnerabilities.len())
104 .sum();
105
106 let mut old_vulns: HashMap<String, VulnerabilityDetail> =
108 HashMap::with_capacity(old_vuln_count);
109 for (id, comp) in &old.components {
110 let depth = old_depths.get(id).copied();
111 for vuln in &comp.vulnerabilities {
112 let key = format!("{}:{}", vuln.id, id);
113 old_vulns.insert(
114 key,
115 VulnerabilityDetail::from_ref_with_depth(vuln, comp, depth),
116 );
117 }
118 }
119
120 let mut new_vulns: HashMap<String, VulnerabilityDetail> =
122 HashMap::with_capacity(new_vuln_count);
123 for (id, comp) in &new.components {
124 let depth = new_depths.get(id).copied();
125 for vuln in &comp.vulnerabilities {
126 let key = format!("{}:{}", vuln.id, id);
127 new_vulns.insert(
128 key,
129 VulnerabilityDetail::from_ref_with_depth(vuln, comp, depth),
130 );
131 }
132 }
133
134 for detail in new_vulns.values() {
136 let vuln_id = &detail.id;
138 let exists_in_old = old_vulns.values().any(|v| &v.id == vuln_id);
139 if !exists_in_old {
140 result.introduced.push(detail.clone());
141 }
142 }
143
144 for detail in old_vulns.values() {
146 let vuln_id = &detail.id;
147 let exists_in_new = new_vulns.values().any(|v| &v.id == vuln_id);
148 if !exists_in_new {
149 result.resolved.push(detail.clone());
150 }
151 }
152
153 for detail in new_vulns.values() {
155 let vuln_id = &detail.id;
156 let exists_in_old = old_vulns.values().any(|v| &v.id == vuln_id);
157 if exists_in_old {
158 result.persistent.push(detail.clone());
159 }
160 }
161
162 result.sort_by_severity();
164
165 result
166 }
167
168 fn name(&self) -> &'static str {
169 "VulnerabilityChangeComputer"
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_vulnerability_change_computer_default() {
179 let computer = VulnerabilityChangeComputer;
180 assert_eq!(computer.name(), "VulnerabilityChangeComputer");
181 }
182
183 #[test]
184 fn test_empty_sboms() {
185 let computer = VulnerabilityChangeComputer;
186 let old = NormalizedSbom::default();
187 let new = NormalizedSbom::default();
188 let matches = ComponentMatches::new();
189
190 let result = computer.compute(&old, &new, &matches);
191 assert!(result.is_empty());
192 }
193}