use super::metadata::ComponentType;
use super::sbom::NormalizedSbom;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum BomProfile {
#[default]
Sbom,
Cbom,
AiBom,
}
impl BomProfile {
#[must_use]
pub fn detect(sbom: &NormalizedSbom) -> Self {
let total = sbom.components.len();
if total == 0 {
return Self::Sbom;
}
let crypto_count = sbom
.components
.values()
.filter(|c| c.component_type == ComponentType::Cryptographic)
.count();
let ml_count = sbom
.components
.values()
.filter(|c| c.component_type == ComponentType::MachineLearningModel)
.count();
let ai_count = ml_count
+ sbom
.components
.values()
.filter(|c| c.component_type == ComponentType::Data)
.count();
let is_aibom = ml_count >= 1 && (ai_count * 2 > total || ai_count >= 3);
let is_cbom = crypto_count >= 3 && crypto_count * 2 > total;
match (is_aibom, is_cbom) {
(true, true) => {
if crypto_count > ai_count {
Self::Cbom
} else {
Self::AiBom
}
}
(true, false) => Self::AiBom,
(false, true) => Self::Cbom,
(false, false) => Self::Sbom,
}
}
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::Sbom => "SBOM",
Self::Cbom => "CBOM",
Self::AiBom => "AI-BOM",
}
}
#[must_use]
pub fn from_str_opt(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"sbom" => Some(Self::Sbom),
"cbom" => Some(Self::Cbom),
"ai" | "aibom" | "mlbom" => Some(Self::AiBom),
_ => None,
}
}
}
impl std::fmt::Display for BomProfile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Component;
#[test]
fn test_detect_sbom_empty() {
let sbom = NormalizedSbom::default();
assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
}
#[test]
fn test_detect_sbom_no_crypto() {
let mut sbom = NormalizedSbom::default();
for i in 0..10 {
let c = Component::new(format!("lib-{i}"), format!("lib-{i}@1.0"));
sbom.add_component(c);
}
assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
}
#[test]
fn test_detect_cbom_majority_crypto() {
let mut sbom = NormalizedSbom::default();
for i in 0..2 {
let c = Component::new(format!("app-{i}"), format!("app-{i}@1.0"));
sbom.add_component(c);
}
for i in 0..5 {
let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
c.component_type = ComponentType::Cryptographic;
sbom.add_component(c);
}
assert_eq!(BomProfile::detect(&sbom), BomProfile::Cbom);
}
#[test]
fn test_detect_sbom_minority_crypto() {
let mut sbom = NormalizedSbom::default();
for i in 0..8 {
let c = Component::new(format!("lib-{i}"), format!("lib-{i}@1.0"));
sbom.add_component(c);
}
for i in 0..3 {
let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
c.component_type = ComponentType::Cryptographic;
sbom.add_component(c);
}
assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
}
#[test]
fn test_detect_cbom_needs_minimum_3() {
let mut sbom = NormalizedSbom::default();
for i in 0..2 {
let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
c.component_type = ComponentType::Cryptographic;
sbom.add_component(c);
}
assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
}
fn ml_component(name: &str) -> Component {
let mut c = Component::new(name.to_string(), format!("{name}@1.0"));
c.component_type = ComponentType::MachineLearningModel;
c
}
fn data_component(name: &str) -> Component {
let mut c = Component::new(name.to_string(), format!("{name}@1.0"));
c.component_type = ComponentType::Data;
c
}
#[test]
fn test_detect_aibom_ml_majority() {
let mut sbom = NormalizedSbom::default();
sbom.add_component(Component::new("app".to_string(), "app@1.0".to_string()));
sbom.add_component(ml_component("classifier"));
sbom.add_component(data_component("reviews"));
assert_eq!(BomProfile::detect(&sbom), BomProfile::AiBom);
}
#[test]
fn test_detect_aibom_min_three_without_majority() {
let mut sbom = NormalizedSbom::default();
for i in 0..5 {
sbom.add_component(Component::new(format!("lib-{i}"), format!("lib-{i}@1.0")));
}
sbom.add_component(ml_component("model-a"));
sbom.add_component(ml_component("model-b"));
sbom.add_component(data_component("train"));
assert_eq!(BomProfile::detect(&sbom), BomProfile::AiBom);
}
#[test]
fn test_detect_not_aibom_datasets_only() {
let mut sbom = NormalizedSbom::default();
for i in 0..4 {
sbom.add_component(data_component(&format!("data-{i}")));
}
assert_eq!(BomProfile::detect(&sbom), BomProfile::Sbom);
}
#[test]
fn test_detect_aibom_beats_cbom_on_majority() {
let mut sbom = NormalizedSbom::default();
for i in 0..4 {
sbom.add_component(ml_component(&format!("model-{i}")));
}
for i in 0..3 {
let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
c.component_type = ComponentType::Cryptographic;
sbom.add_component(c);
}
assert_eq!(BomProfile::detect(&sbom), BomProfile::AiBom);
}
#[test]
fn test_detect_cbom_beats_aibom_when_crypto_dominates() {
let mut sbom = NormalizedSbom::default();
for i in 0..5 {
let mut c = Component::new(format!("algo-{i}"), format!("algo-{i}@1.0"));
c.component_type = ComponentType::Cryptographic;
sbom.add_component(c);
}
sbom.add_component(ml_component("model"));
sbom.add_component(data_component("data"));
assert_eq!(BomProfile::detect(&sbom), BomProfile::Cbom);
}
#[test]
fn test_from_str_opt() {
assert_eq!(BomProfile::from_str_opt("sbom"), Some(BomProfile::Sbom));
assert_eq!(BomProfile::from_str_opt("CBOM"), Some(BomProfile::Cbom));
assert_eq!(BomProfile::from_str_opt("cbom"), Some(BomProfile::Cbom));
assert_eq!(BomProfile::from_str_opt("ai"), Some(BomProfile::AiBom));
assert_eq!(BomProfile::from_str_opt("AiBom"), Some(BomProfile::AiBom));
assert_eq!(BomProfile::from_str_opt("mlbom"), Some(BomProfile::AiBom));
assert_eq!(BomProfile::from_str_opt("hbom"), None);
}
#[test]
fn test_label() {
assert_eq!(BomProfile::Sbom.label(), "SBOM");
assert_eq!(BomProfile::Cbom.label(), "CBOM");
assert_eq!(BomProfile::AiBom.label(), "AI-BOM");
}
#[test]
fn test_display() {
assert_eq!(format!("{}", BomProfile::Sbom), "SBOM");
assert_eq!(format!("{}", BomProfile::Cbom), "CBOM");
assert_eq!(format!("{}", BomProfile::AiBom), "AI-BOM");
}
}