sbom_tools/verification/
audit.rs1use crate::model::{HashAlgorithm, NormalizedSbom};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub enum HashAuditResult {
12 Strong,
14 WeakOnly,
16 Missing,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ComponentHashAudit {
23 pub name: String,
25 pub version: Option<String>,
27 pub result: HashAuditResult,
29 pub algorithms: Vec<String>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct HashAuditReport {
36 pub total_components: usize,
38 pub strong_count: usize,
40 pub weak_only_count: usize,
42 pub missing_count: usize,
44 pub components: Vec<ComponentHashAudit>,
46}
47
48impl HashAuditReport {
49 #[must_use]
51 pub fn pass_rate(&self) -> f64 {
52 if self.total_components == 0 {
53 return 100.0;
54 }
55 (self.strong_count as f64 / self.total_components as f64) * 100.0
56 }
57}
58
59fn is_strong_algorithm(alg: &HashAlgorithm) -> bool {
61 matches!(
62 alg,
63 HashAlgorithm::Sha256
64 | HashAlgorithm::Sha384
65 | HashAlgorithm::Sha512
66 | HashAlgorithm::Sha3_256
67 | HashAlgorithm::Sha3_384
68 | HashAlgorithm::Sha3_512
69 | HashAlgorithm::Blake2b256
70 | HashAlgorithm::Blake2b384
71 | HashAlgorithm::Blake2b512
72 | HashAlgorithm::Blake3
73 )
74}
75
76#[must_use]
80pub fn audit_component_hashes(sbom: &NormalizedSbom) -> HashAuditReport {
81 let mut strong_count = 0;
82 let mut weak_only_count = 0;
83 let mut missing_count = 0;
84 let mut components = Vec::new();
85
86 for comp in sbom.components.values() {
87 let algorithms: Vec<String> = comp
88 .hashes
89 .iter()
90 .map(|h| format!("{}", h.algorithm))
91 .collect();
92
93 let result = if comp.hashes.is_empty() {
94 missing_count += 1;
95 HashAuditResult::Missing
96 } else if comp
97 .hashes
98 .iter()
99 .any(|h| is_strong_algorithm(&h.algorithm))
100 {
101 strong_count += 1;
102 HashAuditResult::Strong
103 } else {
104 weak_only_count += 1;
105 HashAuditResult::WeakOnly
106 };
107
108 components.push(ComponentHashAudit {
109 name: comp.name.clone(),
110 version: comp.version.clone(),
111 result,
112 algorithms,
113 });
114 }
115
116 HashAuditReport {
117 total_components: sbom.components.len(),
118 strong_count,
119 weak_only_count,
120 missing_count,
121 components,
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::model::{Component, Hash, NormalizedSbom};
129
130 fn make_sbom_with_hashes(hash_specs: &[Vec<HashAlgorithm>]) -> NormalizedSbom {
131 let mut sbom = NormalizedSbom::default();
132 for (i, algs) in hash_specs.iter().enumerate() {
133 let mut comp = Component::new(format!("comp-{i}"), format!("id-{i}"));
134 for alg in algs {
135 comp.hashes
136 .push(Hash::new(alg.clone(), "deadbeef".to_string()));
137 }
138 sbom.components.insert(comp.canonical_id.clone(), comp);
139 }
140 sbom
141 }
142
143 #[test]
144 fn audit_empty_sbom() {
145 let sbom = NormalizedSbom::default();
146 let report = audit_component_hashes(&sbom);
147 assert_eq!(report.total_components, 0);
148 assert_eq!(report.pass_rate(), 100.0);
149 }
150
151 #[test]
152 fn audit_all_strong() {
153 let sbom =
154 make_sbom_with_hashes(&[vec![HashAlgorithm::Sha256], vec![HashAlgorithm::Sha512]]);
155 let report = audit_component_hashes(&sbom);
156 assert_eq!(report.strong_count, 2);
157 assert_eq!(report.missing_count, 0);
158 assert_eq!(report.pass_rate(), 100.0);
159 }
160
161 #[test]
162 fn audit_mixed() {
163 let sbom = make_sbom_with_hashes(&[
164 vec![HashAlgorithm::Sha256],
165 vec![HashAlgorithm::Md5],
166 vec![],
167 ]);
168 let report = audit_component_hashes(&sbom);
169 assert_eq!(report.strong_count, 1);
170 assert_eq!(report.weak_only_count, 1);
171 assert_eq!(report.missing_count, 1);
172 }
173
174 #[test]
175 fn audit_weak_with_strong_upgrade() {
176 let sbom = make_sbom_with_hashes(&[vec![HashAlgorithm::Sha1, HashAlgorithm::Sha256]]);
177 let report = audit_component_hashes(&sbom);
178 assert_eq!(report.strong_count, 1);
179 assert_eq!(report.weak_only_count, 0);
180 }
181}