Skip to main content

sbom_tools/model/
bom_profile.rs

1//! BOM profile detection and configuration.
2//!
3//! Determines the type of Bill of Materials (SBOM, CBOM, etc.) and provides
4//! profile-specific defaults for quality scoring, compliance standards, and
5//! TUI tab selection.
6
7use super::metadata::ComponentType;
8use super::sbom::NormalizedSbom;
9use serde::{Deserialize, Serialize};
10
11/// BOM profile — determines mode-specific behavior across TUI and CLI.
12///
13/// Auto-detected from SBOM content or overridden via `--bom-type`.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
15#[non_exhaustive]
16pub enum BomProfile {
17    /// Standard Software Bill of Materials
18    #[default]
19    Sbom,
20    /// Cryptographic Bill of Materials (CycloneDX 1.6+ cryptoProperties)
21    Cbom,
22    // Future: AiBom, Hbom
23}
24
25impl BomProfile {
26    /// Auto-detect the BOM profile from SBOM content.
27    ///
28    /// Classifies as CBOM when >50% of components are `ComponentType::Cryptographic`
29    /// and there are at least 3 crypto components.
30    #[must_use]
31    pub fn detect(sbom: &NormalizedSbom) -> Self {
32        let total = sbom.components.len();
33        if total == 0 {
34            return Self::Sbom;
35        }
36
37        let crypto_count = sbom
38            .components
39            .values()
40            .filter(|c| c.component_type == ComponentType::Cryptographic)
41            .count();
42
43        if crypto_count >= 3 && crypto_count * 2 > total {
44            Self::Cbom
45        } else {
46            Self::Sbom
47        }
48    }
49
50    /// Human-readable label for display.
51    #[must_use]
52    pub const fn label(&self) -> &'static str {
53        match self {
54            Self::Sbom => "SBOM",
55            Self::Cbom => "CBOM",
56        }
57    }
58
59    /// Parse from a string (CLI `--bom-type` flag).
60    #[must_use]
61    pub fn from_str_opt(s: &str) -> Option<Self> {
62        match s.to_lowercase().as_str() {
63            "sbom" => Some(Self::Sbom),
64            "cbom" => Some(Self::Cbom),
65            _ => None,
66        }
67    }
68}
69
70impl std::fmt::Display for BomProfile {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "{}", self.label())
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::model::Component;
80
81    #[test]
82    fn test_detect_sbom_empty() {
83        let sbom = NormalizedSbom::default();
84        assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
85    }
86
87    #[test]
88    fn test_detect_sbom_no_crypto() {
89        let mut sbom = NormalizedSbom::default();
90        for i in 0..10 {
91            let c = Component::new(format!("lib-{i}"), format!("lib-{i}@1.0"));
92            sbom.add_component(c);
93        }
94        assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
95    }
96
97    #[test]
98    fn test_detect_cbom_majority_crypto() {
99        let mut sbom = NormalizedSbom::default();
100        // 2 software + 5 crypto = 71% crypto → CBOM
101        for i in 0..2 {
102            let c = Component::new(format!("app-{i}"), format!("app-{i}@1.0"));
103            sbom.add_component(c);
104        }
105        for i in 0..5 {
106            let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
107            c.component_type = ComponentType::Cryptographic;
108            sbom.add_component(c);
109        }
110        assert_eq!(BomProfile::detect(&sbom), BomProfile::Cbom);
111    }
112
113    #[test]
114    fn test_detect_sbom_minority_crypto() {
115        let mut sbom = NormalizedSbom::default();
116        // 8 software + 3 crypto = 27% crypto → SBOM (below 50%)
117        for i in 0..8 {
118            let c = Component::new(format!("lib-{i}"), format!("lib-{i}@1.0"));
119            sbom.add_component(c);
120        }
121        for i in 0..3 {
122            let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
123            c.component_type = ComponentType::Cryptographic;
124            sbom.add_component(c);
125        }
126        assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
127    }
128
129    #[test]
130    fn test_detect_cbom_needs_minimum_3() {
131        let mut sbom = NormalizedSbom::default();
132        // 2 crypto only but < 3 minimum → SBOM
133        for i in 0..2 {
134            let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
135            c.component_type = ComponentType::Cryptographic;
136            sbom.add_component(c);
137        }
138        assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
139    }
140
141    #[test]
142    fn test_from_str_opt() {
143        assert_eq!(BomProfile::from_str_opt("sbom"), Some(BomProfile::Sbom));
144        assert_eq!(BomProfile::from_str_opt("CBOM"), Some(BomProfile::Cbom));
145        assert_eq!(BomProfile::from_str_opt("cbom"), Some(BomProfile::Cbom));
146        assert_eq!(BomProfile::from_str_opt("hbom"), None);
147    }
148
149    #[test]
150    fn test_label() {
151        assert_eq!(BomProfile::Sbom.label(), "SBOM");
152        assert_eq!(BomProfile::Cbom.label(), "CBOM");
153    }
154
155    #[test]
156    fn test_display() {
157        assert_eq!(format!("{}", BomProfile::Sbom), "SBOM");
158        assert_eq!(format!("{}", BomProfile::Cbom), "CBOM");
159    }
160}