sbom_tools/diff/
traits.rs1use super::{
7 ComponentChange, ComponentLicenseChange, DependencyChange, LicenseChange, VexStatusChange,
8 VulnerabilityDetail,
9};
10use crate::model::{CanonicalId, NormalizedSbom};
11use std::collections::HashMap;
12
13pub type ComponentMatches = HashMap<CanonicalId, Option<CanonicalId>>;
15
16pub trait ChangeComputer: Send + Sync {
21 type ChangeSet;
23
24 fn compute(
26 &self,
27 old: &NormalizedSbom,
28 new: &NormalizedSbom,
29 matches: &ComponentMatches,
30 ) -> Self::ChangeSet;
31
32 fn name(&self) -> &str;
34}
35
36#[derive(Debug, Clone, Default)]
38pub struct ComponentChangeSet {
39 pub added: Vec<ComponentChange>,
40 pub removed: Vec<ComponentChange>,
41 pub modified: Vec<ComponentChange>,
42}
43
44impl ComponentChangeSet {
45 #[must_use]
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 #[must_use]
51 pub fn is_empty(&self) -> bool {
52 self.added.is_empty() && self.removed.is_empty() && self.modified.is_empty()
53 }
54
55 #[must_use]
56 pub fn total(&self) -> usize {
57 self.added.len() + self.removed.len() + self.modified.len()
58 }
59}
60
61#[derive(Debug, Clone, Default)]
63pub struct DependencyChangeSet {
64 pub added: Vec<DependencyChange>,
65 pub removed: Vec<DependencyChange>,
66}
67
68impl DependencyChangeSet {
69 #[must_use]
70 pub fn new() -> Self {
71 Self::default()
72 }
73
74 #[must_use]
75 pub fn is_empty(&self) -> bool {
76 self.added.is_empty() && self.removed.is_empty()
77 }
78
79 #[must_use]
80 pub fn total(&self) -> usize {
81 self.added.len() + self.removed.len()
82 }
83}
84
85#[derive(Debug, Clone, Default)]
87pub struct LicenseChangeSet {
88 pub new_licenses: Vec<LicenseChange>,
89 pub removed_licenses: Vec<LicenseChange>,
90 pub component_changes: Vec<ComponentLicenseChange>,
91}
92
93impl LicenseChangeSet {
94 #[must_use]
95 pub fn new() -> Self {
96 Self::default()
97 }
98
99 #[must_use]
100 pub fn is_empty(&self) -> bool {
101 self.new_licenses.is_empty()
102 && self.removed_licenses.is_empty()
103 && self.component_changes.is_empty()
104 }
105}
106
107#[derive(Debug, Clone, Default)]
109pub struct VulnerabilityChangeSet {
110 pub introduced: Vec<VulnerabilityDetail>,
111 pub resolved: Vec<VulnerabilityDetail>,
112 pub persistent: Vec<VulnerabilityDetail>,
113 pub vex_changes: Vec<VexStatusChange>,
115}
116
117impl VulnerabilityChangeSet {
118 #[must_use]
119 pub fn new() -> Self {
120 Self::default()
121 }
122
123 #[must_use]
124 pub fn is_empty(&self) -> bool {
125 self.introduced.is_empty() && self.resolved.is_empty()
126 }
127
128 pub fn sort_by_severity(&mut self) {
133 let severity_order = |s: &str| match s {
134 "Critical" => 0,
135 "High" => 1,
136 "Medium" => 2,
137 "Low" => 3,
138 _ => 4,
139 };
140 let detail_order = |a: &VulnerabilityDetail, b: &VulnerabilityDetail| {
141 severity_order(&a.severity)
142 .cmp(&severity_order(&b.severity))
143 .then_with(|| a.id.cmp(&b.id))
144 .then_with(|| a.component_id.cmp(&b.component_id))
145 };
146
147 self.introduced.sort_by(detail_order);
148 self.resolved.sort_by(detail_order);
149 self.persistent.sort_by(detail_order);
150 self.vex_changes.sort_by(|a, b| {
151 a.vuln_id
152 .cmp(&b.vuln_id)
153 .then_with(|| a.component_name.cmp(&b.component_name))
154 });
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_component_change_set_empty() {
164 let set = ComponentChangeSet::new();
165 assert!(set.is_empty());
166 assert_eq!(set.total(), 0);
167 }
168
169 #[test]
170 fn test_dependency_change_set_empty() {
171 let set = DependencyChangeSet::new();
172 assert!(set.is_empty());
173 assert_eq!(set.total(), 0);
174 }
175
176 #[test]
177 fn test_license_change_set_empty() {
178 let set = LicenseChangeSet::new();
179 assert!(set.is_empty());
180 }
181
182 #[test]
183 fn test_vulnerability_change_set_empty() {
184 let set = VulnerabilityChangeSet::new();
185 assert!(set.is_empty());
186 }
187}