use crate::model::{HashAlgorithm, NormalizedSbom};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum HashAuditResult {
Strong,
WeakOnly,
Missing,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentHashAudit {
pub name: String,
pub version: Option<String>,
pub result: HashAuditResult,
pub algorithms: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HashAuditReport {
pub total_components: usize,
pub strong_count: usize,
pub weak_only_count: usize,
pub missing_count: usize,
pub components: Vec<ComponentHashAudit>,
}
impl HashAuditReport {
#[must_use]
pub fn pass_rate(&self) -> f64 {
if self.total_components == 0 {
return 100.0;
}
(self.strong_count as f64 / self.total_components as f64) * 100.0
}
}
fn is_strong_algorithm(alg: &HashAlgorithm) -> bool {
matches!(
alg,
HashAlgorithm::Sha256
| HashAlgorithm::Sha384
| HashAlgorithm::Sha512
| HashAlgorithm::Sha3_256
| HashAlgorithm::Sha3_384
| HashAlgorithm::Sha3_512
| HashAlgorithm::Blake2b256
| HashAlgorithm::Blake2b384
| HashAlgorithm::Blake2b512
| HashAlgorithm::Blake3
)
}
#[must_use]
pub fn audit_component_hashes(sbom: &NormalizedSbom) -> HashAuditReport {
let mut strong_count = 0;
let mut weak_only_count = 0;
let mut missing_count = 0;
let mut components = Vec::new();
for comp in sbom.components.values() {
let algorithms: Vec<String> = comp
.hashes
.iter()
.map(|h| format!("{}", h.algorithm))
.collect();
let result = if comp.hashes.is_empty() {
missing_count += 1;
HashAuditResult::Missing
} else if comp
.hashes
.iter()
.any(|h| is_strong_algorithm(&h.algorithm))
{
strong_count += 1;
HashAuditResult::Strong
} else {
weak_only_count += 1;
HashAuditResult::WeakOnly
};
components.push(ComponentHashAudit {
name: comp.name.clone(),
version: comp.version.clone(),
result,
algorithms,
});
}
HashAuditReport {
total_components: sbom.components.len(),
strong_count,
weak_only_count,
missing_count,
components,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Component, Hash, NormalizedSbom};
fn make_sbom_with_hashes(hash_specs: &[Vec<HashAlgorithm>]) -> NormalizedSbom {
let mut sbom = NormalizedSbom::default();
for (i, algs) in hash_specs.iter().enumerate() {
let mut comp = Component::new(format!("comp-{i}"), format!("id-{i}"));
for alg in algs {
comp.hashes
.push(Hash::new(alg.clone(), "deadbeef".to_string()));
}
sbom.components.insert(comp.canonical_id.clone(), comp);
}
sbom
}
#[test]
fn audit_empty_sbom() {
let sbom = NormalizedSbom::default();
let report = audit_component_hashes(&sbom);
assert_eq!(report.total_components, 0);
assert_eq!(report.pass_rate(), 100.0);
}
#[test]
fn audit_all_strong() {
let sbom =
make_sbom_with_hashes(&[vec![HashAlgorithm::Sha256], vec![HashAlgorithm::Sha512]]);
let report = audit_component_hashes(&sbom);
assert_eq!(report.strong_count, 2);
assert_eq!(report.missing_count, 0);
assert_eq!(report.pass_rate(), 100.0);
}
#[test]
fn audit_mixed() {
let sbom = make_sbom_with_hashes(&[
vec![HashAlgorithm::Sha256],
vec![HashAlgorithm::Md5],
vec![],
]);
let report = audit_component_hashes(&sbom);
assert_eq!(report.strong_count, 1);
assert_eq!(report.weak_only_count, 1);
assert_eq!(report.missing_count, 1);
}
#[test]
fn audit_weak_with_strong_upgrade() {
let sbom = make_sbom_with_hashes(&[vec![HashAlgorithm::Sha1, HashAlgorithm::Sha256]]);
let report = audit_component_hashes(&sbom);
assert_eq!(report.strong_count, 1);
assert_eq!(report.weak_only_count, 0);
}
}