use super::ComplianceLevel;
use clap::ValueEnum;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
#[non_exhaustive]
pub enum StandardSelector {
#[value(name = "ntia")]
Ntia,
#[value(name = "fda")]
Fda,
#[value(name = "cra", alias = "cra-phase2")]
Cra,
#[value(name = "cra-phase1", alias = "cra-2026")]
CraPhase1,
#[value(name = "ssdf", alias = "nist-ssdf", alias = "nist_ssdf")]
Ssdf,
#[value(name = "eo14028", alias = "eo-14028", alias = "eo_14028")]
Eo14028,
#[value(name = "cnsa2", alias = "cnsa-2", alias = "cnsa_2", alias = "cnsa2.0")]
Cnsa2,
#[value(name = "pqc", alias = "nist-pqc", alias = "nist_pqc")]
Pqc,
#[value(
name = "bsi",
alias = "tr-03183",
alias = "tr03183",
alias = "bsi-tr-03183-2"
)]
Bsi,
#[value(
name = "oss-steward",
alias = "cra-oss-steward",
alias = "cra-oss",
alias = "cra-art24",
alias = "art24"
)]
OssSteward,
#[value(name = "eucc", alias = "eucc-substantial", alias = "common-criteria")]
Eucc,
#[value(
name = "ai-act",
alias = "ai_act",
alias = "aiact",
alias = "eu-ai-act"
)]
AiAct,
#[value(
name = "bsi-ai",
alias = "bsi_ai",
alias = "bsiai",
alias = "sbom-for-ai",
alias = "ai-bom"
)]
BsiAi,
#[value(
name = "cisa-2026",
alias = "cisa",
alias = "cisa2026",
alias = "minimum-elements-2026"
)]
Cisa2026,
#[value(
name = "pci-dss",
alias = "pci",
alias = "pci-dss-6-3-2",
alias = "pci-dss-4"
)]
PciDss,
#[value(name = "fsct", alias = "fsct-3", alias = "component-transparency")]
Fsct,
}
impl StandardSelector {
#[must_use]
pub const fn level(self) -> ComplianceLevel {
match self {
Self::Ntia => ComplianceLevel::NtiaMinimum,
Self::Fda => ComplianceLevel::FdaMedicalDevice,
Self::Cra => ComplianceLevel::CraPhase2,
Self::CraPhase1 => ComplianceLevel::CraPhase1,
Self::Ssdf => ComplianceLevel::NistSsdf,
Self::Eo14028 => ComplianceLevel::Eo14028,
Self::Cnsa2 => ComplianceLevel::Cnsa2,
Self::Pqc => ComplianceLevel::NistPqc,
Self::Bsi => ComplianceLevel::BsiTr03183_2,
Self::OssSteward => ComplianceLevel::CraOssSteward,
Self::Eucc => ComplianceLevel::EuccSubstantial,
Self::AiAct => ComplianceLevel::EuAiAct,
Self::BsiAi => ComplianceLevel::BsiSbomForAi,
Self::Cisa2026 => ComplianceLevel::Cisa2026,
Self::PciDss => ComplianceLevel::PciDss632,
Self::Fsct => ComplianceLevel::Fsct,
}
}
#[must_use]
pub fn canonical_name(self) -> &'static str {
match self {
Self::Ntia => "ntia",
Self::Fda => "fda",
Self::Cra => "cra",
Self::CraPhase1 => "cra-phase1",
Self::Ssdf => "ssdf",
Self::Eo14028 => "eo14028",
Self::Cnsa2 => "cnsa2",
Self::Pqc => "pqc",
Self::Bsi => "bsi",
Self::OssSteward => "oss-steward",
Self::Eucc => "eucc",
Self::AiAct => "ai-act",
Self::BsiAi => "bsi-ai",
Self::Cisa2026 => "cisa-2026",
Self::PciDss => "pci-dss",
Self::Fsct => "fsct",
}
}
#[must_use]
pub fn valid_values() -> String {
Self::value_variants()
.iter()
.map(|v| v.canonical_name())
.collect::<Vec<_>>()
.join(", ")
}
}
impl std::fmt::Display for StandardSelector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.canonical_name())
}
}
impl std::str::FromStr for StandardSelector {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
for variant in Self::value_variants() {
if variant
.to_possible_value()
.is_some_and(|pv| pv.matches(s, true))
{
return Ok(*variant);
}
}
Err(format!(
"unknown compliance standard '{s}'. Valid values: {} \
(aliases such as nist-ssdf, tr-03183, cra-art24 are also accepted; \
see `sbom-tools validate --help`)",
Self::valid_values()
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_documented_alias_parses_to_the_right_level() {
let table: &[(&str, ComplianceLevel)] = &[
("ntia", ComplianceLevel::NtiaMinimum),
("fda", ComplianceLevel::FdaMedicalDevice),
("cra", ComplianceLevel::CraPhase2),
("cra-phase2", ComplianceLevel::CraPhase2),
("cra-phase1", ComplianceLevel::CraPhase1),
("cra-2026", ComplianceLevel::CraPhase1),
("ssdf", ComplianceLevel::NistSsdf),
("nist-ssdf", ComplianceLevel::NistSsdf),
("nist_ssdf", ComplianceLevel::NistSsdf),
("eo14028", ComplianceLevel::Eo14028),
("eo-14028", ComplianceLevel::Eo14028),
("eo_14028", ComplianceLevel::Eo14028),
("cnsa2", ComplianceLevel::Cnsa2),
("cnsa-2", ComplianceLevel::Cnsa2),
("cnsa_2", ComplianceLevel::Cnsa2),
("cnsa2.0", ComplianceLevel::Cnsa2),
("pqc", ComplianceLevel::NistPqc),
("nist-pqc", ComplianceLevel::NistPqc),
("nist_pqc", ComplianceLevel::NistPqc),
("bsi", ComplianceLevel::BsiTr03183_2),
("tr-03183", ComplianceLevel::BsiTr03183_2),
("tr03183", ComplianceLevel::BsiTr03183_2),
("bsi-tr-03183-2", ComplianceLevel::BsiTr03183_2),
("oss-steward", ComplianceLevel::CraOssSteward),
("cra-oss-steward", ComplianceLevel::CraOssSteward),
("cra-oss", ComplianceLevel::CraOssSteward),
("cra-art24", ComplianceLevel::CraOssSteward),
("art24", ComplianceLevel::CraOssSteward),
("eucc", ComplianceLevel::EuccSubstantial),
("eucc-substantial", ComplianceLevel::EuccSubstantial),
("common-criteria", ComplianceLevel::EuccSubstantial),
("ai-act", ComplianceLevel::EuAiAct),
("ai_act", ComplianceLevel::EuAiAct),
("aiact", ComplianceLevel::EuAiAct),
("eu-ai-act", ComplianceLevel::EuAiAct),
("bsi-ai", ComplianceLevel::BsiSbomForAi),
("bsi_ai", ComplianceLevel::BsiSbomForAi),
("bsiai", ComplianceLevel::BsiSbomForAi),
("sbom-for-ai", ComplianceLevel::BsiSbomForAi),
("ai-bom", ComplianceLevel::BsiSbomForAi),
("cisa-2026", ComplianceLevel::Cisa2026),
("cisa", ComplianceLevel::Cisa2026),
("cisa2026", ComplianceLevel::Cisa2026),
("minimum-elements-2026", ComplianceLevel::Cisa2026),
("pci-dss", ComplianceLevel::PciDss632),
("pci", ComplianceLevel::PciDss632),
("pci-dss-6-3-2", ComplianceLevel::PciDss632),
("pci-dss-4", ComplianceLevel::PciDss632),
("fsct", ComplianceLevel::Fsct),
("fsct-3", ComplianceLevel::Fsct),
("component-transparency", ComplianceLevel::Fsct),
];
for (spelling, expected) in table {
let parsed: StandardSelector = spelling
.parse()
.unwrap_or_else(|e| panic!("'{spelling}' must parse: {e}"));
assert_eq!(
parsed.level(),
*expected,
"'{spelling}' mapped to the wrong compliance level"
);
}
}
#[test]
fn parse_is_case_insensitive_and_trims() {
assert_eq!(
"NTIA".parse::<StandardSelector>().unwrap(),
StandardSelector::Ntia
);
assert_eq!(
" Cra-Phase1 ".parse::<StandardSelector>().unwrap(),
StandardSelector::CraPhase1
);
assert_eq!(
"CNSA2.0".parse::<StandardSelector>().unwrap(),
StandardSelector::Cnsa2
);
}
#[test]
fn unknown_standard_error_lists_valid_values() {
let err = "not-a-standard".parse::<StandardSelector>().unwrap_err();
assert!(err.contains("not-a-standard"));
for canonical in [
"ntia",
"fda",
"cra",
"cra-phase1",
"ssdf",
"eo14028",
"cnsa2",
"pqc",
"bsi",
"oss-steward",
"eucc",
"ai-act",
"bsi-ai",
"cisa-2026",
"pci-dss",
"fsct",
] {
assert!(err.contains(canonical), "error must list '{canonical}'");
}
}
#[test]
fn every_variant_has_a_level_and_canonical_name_roundtrip() {
for variant in StandardSelector::value_variants() {
let name = variant.canonical_name();
let reparsed: StandardSelector = name.parse().unwrap();
assert_eq!(reparsed, *variant, "canonical '{name}' must round-trip");
let _ = variant.level().name();
}
}
#[test]
fn cra_selects_phase2_and_phase1_is_reachable() {
assert_eq!(
"cra".parse::<StandardSelector>().unwrap().level(),
ComplianceLevel::CraPhase2
);
assert_eq!(
"cra-phase1".parse::<StandardSelector>().unwrap().level(),
ComplianceLevel::CraPhase1
);
}
}