|
1 | 1 | use serde::{Deserialize, Serialize}; |
2 | | -use std::collections::{HashMap, HashSet}; |
| 2 | +use std::collections::{BTreeMap, HashSet}; |
3 | 3 | use uuid::Uuid; |
4 | 4 |
|
5 | 5 | use crate::crypto_algorithm::CryptoFinding; |
@@ -108,10 +108,10 @@ pub fn to_cyclonedx_bom(findings: &[CryptoFinding]) -> CycloneDxBom { |
108 | 108 | let mut components = Vec::new(); |
109 | 109 | let mut dependencies = Vec::new(); |
110 | 110 | // Key by (ecosystem, library_name) to avoid collapsing libraries across ecosystems |
111 | | - let mut library_algorithms: HashMap<(String, String), Vec<String>> = HashMap::new(); |
| 111 | + let mut library_algorithms: BTreeMap<(String, String), Vec<String>> = BTreeMap::new(); |
112 | 112 |
|
113 | 113 | // Group findings by bom_ref to aggregate detection contexts |
114 | | - let mut algo_findings: HashMap<String, Vec<&CryptoFinding>> = HashMap::new(); |
| 114 | + let mut algo_findings: BTreeMap<String, Vec<&CryptoFinding>> = BTreeMap::new(); |
115 | 115 | for finding in findings { |
116 | 116 | let bom_ref = make_bom_ref(&finding.algorithm.name, &finding.algorithm.oid); |
117 | 117 |
|
@@ -265,25 +265,39 @@ fn make_bom_ref(name: &str, oid: &Option<String>) -> String { |
265 | 265 | } |
266 | 266 | } |
267 | 267 |
|
268 | | -/// Validate an algorithm family string against the CycloneDX 1.7 registry. |
269 | | -/// Returns `Some(family)` if it's a known value, `None` otherwise. |
| 268 | +/// Validate an algorithm family string against the official CycloneDX 1.7 enum. |
| 269 | +/// Returns `Some(canonical_form)` if matched, `None` otherwise. |
270 | 270 | fn valid_algorithm_family(family: &str) -> Option<String> { |
271 | | - const KNOWN_FAMILIES: &[&str] = &[ |
272 | | - "AES", "RSA", "EC", "SHA-1", "SHA-2", "SHA-3", "SHAKE", |
273 | | - "3DES", "DES", "Blowfish", "RC4", "RC2", "CAST5", "IDEA", |
274 | | - "Camellia", "SEED", "ARIA", "Serpent", "Twofish", "Threefish", |
275 | | - "ChaCha20-Poly1305", "Salsa20", "HMAC", "CMAC", "GMAC", "KMAC", |
276 | | - "Poly1305", "SipHash", "ECDSA", "EdDSA", "DSA", |
277 | | - "ECDH", "DH", "X25519", "X448", "HKDF", "PBKDF2", |
278 | | - "scrypt", "Argon2", "bcrypt", "BLAKE2", "BLAKE3", |
279 | | - "MD5", "MD4", "RIPEMD", "Whirlpool", |
280 | | - "ML-KEM", "ML-DSA", "SLH-DSA", "FN-DSA", |
| 271 | + // Official CycloneDX 1.7 algorithmFamily enum values (case-sensitive in schema) |
| 272 | + const CANONICAL_FAMILIES: &[&str] = &[ |
| 273 | + "3DES", "3GPP-XOR", "A5/1", "A5/2", "AES", "ARIA", "Ascon", |
| 274 | + "BLAKE2", "BLAKE3", "BLS", "Blowfish", |
| 275 | + "CAMELLIA", "CAST5", "CAST6", "CMAC", "CMEA", "ChaCha", "ChaCha20", |
| 276 | + "DES", "DSA", |
| 277 | + "ECDH", "ECDSA", "ECIES", "EdDSA", "ElGamal", |
| 278 | + "FFDH", "Fortuna", |
| 279 | + "GOST", |
| 280 | + "HC", "HKDF", "HMAC", |
| 281 | + "IDEA", "IKE-PRF", |
| 282 | + "KMAC", |
| 283 | + "LMS", |
| 284 | + "MD2", "MD4", "MD5", "MILENAGE", "ML-DSA", "ML-KEM", "MQV", |
| 285 | + "PBES1", "PBES2", "PBKDF1", "PBKDF2", "PBMAC1", "Poly1305", |
| 286 | + "RABBIT", "RC2", "RC4", "RC5", "RC6", "RIPEMD", |
| 287 | + "RSAES-OAEP", "RSAES-PKCS1", "RSASSA-PKCS1", "RSASSA-PSS", |
| 288 | + "SEED", "SHA-1", "SHA-2", "SHA-3", "SLH-DSA", "SNOW3G", "SP800-108", |
| 289 | + "Salsa20", "Serpent", "SipHash", "Skipjack", |
| 290 | + "TUAK", "Twofish", |
| 291 | + "Whirlpool", |
| 292 | + "X3DH", "XMSS", |
| 293 | + "Yarrow", |
| 294 | + "ZUC", |
| 295 | + "bcrypt", |
281 | 296 | ]; |
282 | | - if KNOWN_FAMILIES.iter().any(|&known| known.eq_ignore_ascii_case(family)) { |
283 | | - Some(family.to_string()) |
284 | | - } else { |
285 | | - None |
286 | | - } |
| 297 | + CANONICAL_FAMILIES |
| 298 | + .iter() |
| 299 | + .find(|&&canonical| canonical.eq_ignore_ascii_case(family)) |
| 300 | + .map(|&canonical| canonical.to_string()) |
287 | 301 | } |
288 | 302 |
|
289 | 303 | fn chrono_timestamp() -> String { |
|
0 commit comments