Skip to main content

sbom_tools/verification/
audit.rs

1//! Component hash auditing.
2//!
3//! Analyzes hash coverage and strength across all components in an SBOM,
4//! producing a detailed audit report.
5
6use crate::model::{HashAlgorithm, NormalizedSbom};
7use serde::{Deserialize, Serialize};
8
9/// Status of a component's hash coverage
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub enum HashAuditResult {
12    /// Component has at least one strong hash (SHA-256+)
13    Strong,
14    /// Component only has weak hashes (MD5, SHA-1)
15    WeakOnly,
16    /// Component has no hashes at all
17    Missing,
18}
19
20/// Audit report for a single component
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ComponentHashAudit {
23    /// Component name
24    pub name: String,
25    /// Component version
26    pub version: Option<String>,
27    /// Audit result
28    pub result: HashAuditResult,
29    /// Algorithms present
30    pub algorithms: Vec<String>,
31}
32
33/// Overall hash audit report
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct HashAuditReport {
36    /// Total components analyzed
37    pub total_components: usize,
38    /// Components with strong hashes
39    pub strong_count: usize,
40    /// Components with only weak hashes
41    pub weak_only_count: usize,
42    /// Components with no hashes
43    pub missing_count: usize,
44    /// Per-component audit details
45    pub components: Vec<ComponentHashAudit>,
46}
47
48impl HashAuditReport {
49    /// Overall pass rate (components with strong hashes / total)
50    #[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
59/// Returns true if a hash algorithm is considered strong (SHA-256 or better)
60fn 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/// Audit all component hashes in an SBOM.
77///
78/// Returns a detailed report of hash coverage and strength.
79#[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}