1use super::ComplianceLevel;
11use clap::ValueEnum;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
19#[non_exhaustive]
20pub enum StandardSelector {
21 #[value(name = "ntia")]
23 Ntia,
24 #[value(name = "fda")]
26 Fda,
27 #[value(name = "cra", alias = "cra-phase2")]
30 Cra,
31 #[value(name = "cra-phase1", alias = "cra-2026")]
33 CraPhase1,
34 #[value(name = "ssdf", alias = "nist-ssdf", alias = "nist_ssdf")]
36 Ssdf,
37 #[value(name = "eo14028", alias = "eo-14028", alias = "eo_14028")]
39 Eo14028,
40 #[value(name = "cnsa2", alias = "cnsa-2", alias = "cnsa_2", alias = "cnsa2.0")]
42 Cnsa2,
43 #[value(name = "pqc", alias = "nist-pqc", alias = "nist_pqc")]
45 Pqc,
46 #[value(
48 name = "bsi",
49 alias = "tr-03183",
50 alias = "tr03183",
51 alias = "bsi-tr-03183-2"
52 )]
53 Bsi,
54 #[value(
56 name = "oss-steward",
57 alias = "cra-oss-steward",
58 alias = "cra-oss",
59 alias = "cra-art24",
60 alias = "art24"
61 )]
62 OssSteward,
63 #[value(name = "eucc", alias = "eucc-substantial", alias = "common-criteria")]
65 Eucc,
66 #[value(
68 name = "ai-act",
69 alias = "ai_act",
70 alias = "aiact",
71 alias = "eu-ai-act"
72 )]
73 AiAct,
74 #[value(
76 name = "bsi-ai",
77 alias = "bsi_ai",
78 alias = "bsiai",
79 alias = "sbom-for-ai",
80 alias = "ai-bom"
81 )]
82 BsiAi,
83 #[value(
86 name = "cisa-2026",
87 alias = "cisa",
88 alias = "cisa2026",
89 alias = "minimum-elements-2026"
90 )]
91 Cisa2026,
92 #[value(
94 name = "pci-dss",
95 alias = "pci",
96 alias = "pci-dss-6-3-2",
97 alias = "pci-dss-4"
98 )]
99 PciDss,
100 #[value(name = "fsct", alias = "fsct-3", alias = "component-transparency")]
102 Fsct,
103}
104
105impl StandardSelector {
106 #[must_use]
108 pub const fn level(self) -> ComplianceLevel {
109 match self {
110 Self::Ntia => ComplianceLevel::NtiaMinimum,
111 Self::Fda => ComplianceLevel::FdaMedicalDevice,
112 Self::Cra => ComplianceLevel::CraPhase2,
113 Self::CraPhase1 => ComplianceLevel::CraPhase1,
114 Self::Ssdf => ComplianceLevel::NistSsdf,
115 Self::Eo14028 => ComplianceLevel::Eo14028,
116 Self::Cnsa2 => ComplianceLevel::Cnsa2,
117 Self::Pqc => ComplianceLevel::NistPqc,
118 Self::Bsi => ComplianceLevel::BsiTr03183_2,
119 Self::OssSteward => ComplianceLevel::CraOssSteward,
120 Self::Eucc => ComplianceLevel::EuccSubstantial,
121 Self::AiAct => ComplianceLevel::EuAiAct,
122 Self::BsiAi => ComplianceLevel::BsiSbomForAi,
123 Self::Cisa2026 => ComplianceLevel::Cisa2026,
124 Self::PciDss => ComplianceLevel::PciDss632,
125 Self::Fsct => ComplianceLevel::Fsct,
126 }
127 }
128
129 #[must_use]
131 pub fn canonical_name(self) -> &'static str {
132 match self {
133 Self::Ntia => "ntia",
134 Self::Fda => "fda",
135 Self::Cra => "cra",
136 Self::CraPhase1 => "cra-phase1",
137 Self::Ssdf => "ssdf",
138 Self::Eo14028 => "eo14028",
139 Self::Cnsa2 => "cnsa2",
140 Self::Pqc => "pqc",
141 Self::Bsi => "bsi",
142 Self::OssSteward => "oss-steward",
143 Self::Eucc => "eucc",
144 Self::AiAct => "ai-act",
145 Self::BsiAi => "bsi-ai",
146 Self::Cisa2026 => "cisa-2026",
147 Self::PciDss => "pci-dss",
148 Self::Fsct => "fsct",
149 }
150 }
151
152 #[must_use]
154 pub fn valid_values() -> String {
155 Self::value_variants()
156 .iter()
157 .map(|v| v.canonical_name())
158 .collect::<Vec<_>>()
159 .join(", ")
160 }
161}
162
163impl std::fmt::Display for StandardSelector {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 f.write_str(self.canonical_name())
166 }
167}
168
169impl std::str::FromStr for StandardSelector {
170 type Err = String;
171
172 fn from_str(s: &str) -> Result<Self, Self::Err> {
174 let s = s.trim();
175 for variant in Self::value_variants() {
176 if variant
177 .to_possible_value()
178 .is_some_and(|pv| pv.matches(s, true))
179 {
180 return Ok(*variant);
181 }
182 }
183 Err(format!(
184 "unknown compliance standard '{s}'. Valid values: {} \
185 (aliases such as nist-ssdf, tr-03183, cra-art24 are also accepted; \
186 see `sbom-tools validate --help`)",
187 Self::valid_values()
188 ))
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
201 fn every_documented_alias_parses_to_the_right_level() {
202 let table: &[(&str, ComplianceLevel)] = &[
203 ("ntia", ComplianceLevel::NtiaMinimum),
204 ("fda", ComplianceLevel::FdaMedicalDevice),
205 ("cra", ComplianceLevel::CraPhase2),
206 ("cra-phase2", ComplianceLevel::CraPhase2),
207 ("cra-phase1", ComplianceLevel::CraPhase1),
208 ("cra-2026", ComplianceLevel::CraPhase1),
209 ("ssdf", ComplianceLevel::NistSsdf),
210 ("nist-ssdf", ComplianceLevel::NistSsdf),
211 ("nist_ssdf", ComplianceLevel::NistSsdf),
212 ("eo14028", ComplianceLevel::Eo14028),
213 ("eo-14028", ComplianceLevel::Eo14028),
214 ("eo_14028", ComplianceLevel::Eo14028),
215 ("cnsa2", ComplianceLevel::Cnsa2),
216 ("cnsa-2", ComplianceLevel::Cnsa2),
217 ("cnsa_2", ComplianceLevel::Cnsa2),
218 ("cnsa2.0", ComplianceLevel::Cnsa2),
219 ("pqc", ComplianceLevel::NistPqc),
220 ("nist-pqc", ComplianceLevel::NistPqc),
221 ("nist_pqc", ComplianceLevel::NistPqc),
222 ("bsi", ComplianceLevel::BsiTr03183_2),
223 ("tr-03183", ComplianceLevel::BsiTr03183_2),
224 ("tr03183", ComplianceLevel::BsiTr03183_2),
225 ("bsi-tr-03183-2", ComplianceLevel::BsiTr03183_2),
226 ("oss-steward", ComplianceLevel::CraOssSteward),
227 ("cra-oss-steward", ComplianceLevel::CraOssSteward),
228 ("cra-oss", ComplianceLevel::CraOssSteward),
229 ("cra-art24", ComplianceLevel::CraOssSteward),
230 ("art24", ComplianceLevel::CraOssSteward),
231 ("eucc", ComplianceLevel::EuccSubstantial),
232 ("eucc-substantial", ComplianceLevel::EuccSubstantial),
233 ("common-criteria", ComplianceLevel::EuccSubstantial),
234 ("ai-act", ComplianceLevel::EuAiAct),
235 ("ai_act", ComplianceLevel::EuAiAct),
236 ("aiact", ComplianceLevel::EuAiAct),
237 ("eu-ai-act", ComplianceLevel::EuAiAct),
238 ("bsi-ai", ComplianceLevel::BsiSbomForAi),
239 ("bsi_ai", ComplianceLevel::BsiSbomForAi),
240 ("bsiai", ComplianceLevel::BsiSbomForAi),
241 ("sbom-for-ai", ComplianceLevel::BsiSbomForAi),
242 ("ai-bom", ComplianceLevel::BsiSbomForAi),
243 ("cisa-2026", ComplianceLevel::Cisa2026),
244 ("cisa", ComplianceLevel::Cisa2026),
245 ("cisa2026", ComplianceLevel::Cisa2026),
246 ("minimum-elements-2026", ComplianceLevel::Cisa2026),
247 ("pci-dss", ComplianceLevel::PciDss632),
248 ("pci", ComplianceLevel::PciDss632),
249 ("pci-dss-6-3-2", ComplianceLevel::PciDss632),
250 ("pci-dss-4", ComplianceLevel::PciDss632),
251 ("fsct", ComplianceLevel::Fsct),
252 ("fsct-3", ComplianceLevel::Fsct),
253 ("component-transparency", ComplianceLevel::Fsct),
254 ];
255 for (spelling, expected) in table {
256 let parsed: StandardSelector = spelling
257 .parse()
258 .unwrap_or_else(|e| panic!("'{spelling}' must parse: {e}"));
259 assert_eq!(
260 parsed.level(),
261 *expected,
262 "'{spelling}' mapped to the wrong compliance level"
263 );
264 }
265 }
266
267 #[test]
268 fn parse_is_case_insensitive_and_trims() {
269 assert_eq!(
270 "NTIA".parse::<StandardSelector>().unwrap(),
271 StandardSelector::Ntia
272 );
273 assert_eq!(
274 " Cra-Phase1 ".parse::<StandardSelector>().unwrap(),
275 StandardSelector::CraPhase1
276 );
277 assert_eq!(
278 "CNSA2.0".parse::<StandardSelector>().unwrap(),
279 StandardSelector::Cnsa2
280 );
281 }
282
283 #[test]
284 fn unknown_standard_error_lists_valid_values() {
285 let err = "not-a-standard".parse::<StandardSelector>().unwrap_err();
286 assert!(err.contains("not-a-standard"));
287 for canonical in [
288 "ntia",
289 "fda",
290 "cra",
291 "cra-phase1",
292 "ssdf",
293 "eo14028",
294 "cnsa2",
295 "pqc",
296 "bsi",
297 "oss-steward",
298 "eucc",
299 "ai-act",
300 "bsi-ai",
301 "cisa-2026",
302 "pci-dss",
303 "fsct",
304 ] {
305 assert!(err.contains(canonical), "error must list '{canonical}'");
306 }
307 }
308
309 #[test]
310 fn every_variant_has_a_level_and_canonical_name_roundtrip() {
311 for variant in StandardSelector::value_variants() {
312 let name = variant.canonical_name();
313 let reparsed: StandardSelector = name.parse().unwrap();
314 assert_eq!(reparsed, *variant, "canonical '{name}' must round-trip");
315 let _ = variant.level().name();
317 }
318 }
319
320 #[test]
321 fn cra_selects_phase2_and_phase1_is_reachable() {
322 assert_eq!(
323 "cra".parse::<StandardSelector>().unwrap().level(),
324 ComplianceLevel::CraPhase2
325 );
326 assert_eq!(
327 "cra-phase1".parse::<StandardSelector>().unwrap().level(),
328 ComplianceLevel::CraPhase1
329 );
330 }
331}