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