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 AiBom,
24 }
26
27impl BomProfile {
28 #[must_use]
44 pub fn detect(sbom: &NormalizedSbom) -> Self {
45 let total = sbom.components.len();
46 if total == 0 {
47 return Self::Sbom;
48 }
49
50 let crypto_count = sbom
51 .components
52 .values()
53 .filter(|c| c.component_type == ComponentType::Cryptographic)
54 .count();
55
56 let ml_count = sbom
57 .components
58 .values()
59 .filter(|c| c.component_type == ComponentType::MachineLearningModel)
60 .count();
61 let ai_count = ml_count
63 + sbom
64 .components
65 .values()
66 .filter(|c| c.component_type == ComponentType::Data)
67 .count();
68
69 let is_aibom = ml_count >= 1 && (ai_count * 2 > total || ai_count >= 3);
71 let is_cbom = crypto_count >= 3 && crypto_count * 2 > total;
72
73 match (is_aibom, is_cbom) {
74 (true, true) => {
75 if crypto_count > ai_count {
77 Self::Cbom
78 } else {
79 Self::AiBom
80 }
81 }
82 (true, false) => Self::AiBom,
83 (false, true) => Self::Cbom,
84 (false, false) => Self::Sbom,
85 }
86 }
87
88 #[must_use]
90 pub const fn label(&self) -> &'static str {
91 match self {
92 Self::Sbom => "SBOM",
93 Self::Cbom => "CBOM",
94 Self::AiBom => "AI-BOM",
95 }
96 }
97
98 #[must_use]
100 pub fn from_str_opt(s: &str) -> Option<Self> {
101 match s.to_lowercase().as_str() {
102 "sbom" => Some(Self::Sbom),
103 "cbom" => Some(Self::Cbom),
104 "ai" | "aibom" | "mlbom" => Some(Self::AiBom),
105 _ => None,
106 }
107 }
108}
109
110impl std::fmt::Display for BomProfile {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 write!(f, "{}", self.label())
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::model::Component;
120
121 #[test]
122 fn test_detect_sbom_empty() {
123 let sbom = NormalizedSbom::default();
124 assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
125 }
126
127 #[test]
128 fn test_detect_sbom_no_crypto() {
129 let mut sbom = NormalizedSbom::default();
130 for i in 0..10 {
131 let c = Component::new(format!("lib-{i}"), format!("lib-{i}@1.0"));
132 sbom.add_component(c);
133 }
134 assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
135 }
136
137 #[test]
138 fn test_detect_cbom_majority_crypto() {
139 let mut sbom = NormalizedSbom::default();
140 for i in 0..2 {
142 let c = Component::new(format!("app-{i}"), format!("app-{i}@1.0"));
143 sbom.add_component(c);
144 }
145 for i in 0..5 {
146 let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
147 c.component_type = ComponentType::Cryptographic;
148 sbom.add_component(c);
149 }
150 assert_eq!(BomProfile::detect(&sbom), BomProfile::Cbom);
151 }
152
153 #[test]
154 fn test_detect_sbom_minority_crypto() {
155 let mut sbom = NormalizedSbom::default();
156 for i in 0..8 {
158 let c = Component::new(format!("lib-{i}"), format!("lib-{i}@1.0"));
159 sbom.add_component(c);
160 }
161 for i in 0..3 {
162 let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
163 c.component_type = ComponentType::Cryptographic;
164 sbom.add_component(c);
165 }
166 assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
167 }
168
169 #[test]
170 fn test_detect_cbom_needs_minimum_3() {
171 let mut sbom = NormalizedSbom::default();
172 for i in 0..2 {
174 let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
175 c.component_type = ComponentType::Cryptographic;
176 sbom.add_component(c);
177 }
178 assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
179 }
180
181 fn ml_component(name: &str) -> Component {
182 let mut c = Component::new(name.to_string(), format!("{name}@1.0"));
183 c.component_type = ComponentType::MachineLearningModel;
184 c
185 }
186
187 fn data_component(name: &str) -> Component {
188 let mut c = Component::new(name.to_string(), format!("{name}@1.0"));
189 c.component_type = ComponentType::Data;
190 c
191 }
192
193 #[test]
194 fn test_detect_aibom_ml_majority() {
195 let mut sbom = NormalizedSbom::default();
196 sbom.add_component(Component::new("app".to_string(), "app@1.0".to_string()));
198 sbom.add_component(ml_component("classifier"));
199 sbom.add_component(data_component("reviews"));
200 assert_eq!(BomProfile::detect(&sbom), BomProfile::AiBom);
201 }
202
203 #[test]
204 fn test_detect_aibom_min_three_without_majority() {
205 let mut sbom = NormalizedSbom::default();
206 for i in 0..5 {
209 sbom.add_component(Component::new(format!("lib-{i}"), format!("lib-{i}@1.0")));
210 }
211 sbom.add_component(ml_component("model-a"));
212 sbom.add_component(ml_component("model-b"));
213 sbom.add_component(data_component("train"));
214 assert_eq!(BomProfile::detect(&sbom), BomProfile::AiBom);
215 }
216
217 #[test]
218 fn test_detect_not_aibom_datasets_only() {
219 let mut sbom = NormalizedSbom::default();
220 for i in 0..4 {
222 sbom.add_component(data_component(&format!("data-{i}")));
223 }
224 assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
225 }
226
227 #[test]
228 fn test_detect_aibom_beats_cbom_on_majority() {
229 let mut sbom = NormalizedSbom::default();
230 for i in 0..4 {
232 sbom.add_component(ml_component(&format!("model-{i}")));
233 }
234 for i in 0..3 {
235 let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
236 c.component_type = ComponentType::Cryptographic;
237 sbom.add_component(c);
238 }
239 assert_eq!(BomProfile::detect(&sbom), BomProfile::AiBom);
240 }
241
242 #[test]
243 fn test_detect_cbom_beats_aibom_when_crypto_dominates() {
244 let mut sbom = NormalizedSbom::default();
245 for i in 0..5 {
247 let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
248 c.component_type = ComponentType::Cryptographic;
249 sbom.add_component(c);
250 }
251 sbom.add_component(ml_component("model"));
252 sbom.add_component(data_component("data"));
253 assert_eq!(BomProfile::detect(&sbom), BomProfile::Cbom);
254 }
255
256 #[test]
257 fn test_from_str_opt() {
258 assert_eq!(BomProfile::from_str_opt("sbom"), Some(BomProfile::Sbom));
259 assert_eq!(BomProfile::from_str_opt("CBOM"), Some(BomProfile::Cbom));
260 assert_eq!(BomProfile::from_str_opt("cbom"), Some(BomProfile::Cbom));
261 assert_eq!(BomProfile::from_str_opt("ai"), Some(BomProfile::AiBom));
262 assert_eq!(BomProfile::from_str_opt("AiBom"), Some(BomProfile::AiBom));
263 assert_eq!(BomProfile::from_str_opt("mlbom"), Some(BomProfile::AiBom));
264 assert_eq!(BomProfile::from_str_opt("hbom"), None);
265 }
266
267 #[test]
268 fn test_label() {
269 assert_eq!(BomProfile::Sbom.label(), "SBOM");
270 assert_eq!(BomProfile::Cbom.label(), "CBOM");
271 assert_eq!(BomProfile::AiBom.label(), "AI-BOM");
272 }
273
274 #[test]
275 fn test_display() {
276 assert_eq!(format!("{}", BomProfile::Sbom), "SBOM");
277 assert_eq!(format!("{}", BomProfile::Cbom), "CBOM");
278 assert_eq!(format!("{}", BomProfile::AiBom), "AI-BOM");
279 }
280}