sbom_tools/model/
bom_profile.rs1use super::metadata::ComponentType;
8use super::sbom::NormalizedSbom;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
15#[non_exhaustive]
16pub enum BomProfile {
17 #[default]
19 Sbom,
20 Cbom,
22 }
24
25impl BomProfile {
26 #[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 #[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 #[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 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 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 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}