@@ -107,7 +107,8 @@ pub struct BomDependency {
107107pub fn to_cyclonedx_bom ( findings : & [ CryptoFinding ] ) -> CycloneDxBom {
108108 let mut components = Vec :: new ( ) ;
109109 let mut dependencies = Vec :: new ( ) ;
110- let mut library_algorithms: HashMap < String , Vec < String > > = HashMap :: new ( ) ;
110+ // Key by (ecosystem, library_name) to avoid collapsing libraries across ecosystems
111+ let mut library_algorithms: HashMap < ( String , String ) , Vec < String > > = HashMap :: new ( ) ;
111112
112113 // Group findings by bom_ref to aggregate detection contexts
113114 let mut algo_findings: HashMap < String , Vec < & CryptoFinding > > = HashMap :: new ( ) ;
@@ -117,7 +118,7 @@ pub fn to_cyclonedx_bom(findings: &[CryptoFinding]) -> CycloneDxBom {
117118 // Track library -> algorithm for `provides` relationships
118119 if let Some ( lib) = & finding. providing_library {
119120 library_algorithms
120- . entry ( lib. clone ( ) )
121+ . entry ( ( finding . ecosystem . clone ( ) , lib. clone ( ) ) )
121122 . or_default ( )
122123 . push ( bom_ref. clone ( ) ) ;
123124 }
@@ -135,7 +136,7 @@ pub fn to_cyclonedx_bom(findings: &[CryptoFinding]) -> CycloneDxBom {
135136
136137 let algo_props = AlgorithmProperties {
137138 primitive : primitive_str,
138- algorithm_family : Some ( first. algorithm . algorithm_family . clone ( ) ) ,
139+ algorithm_family : valid_algorithm_family ( & first. algorithm . algorithm_family ) ,
139140 parameter_set_identifier : first. algorithm . parameter_set . clone ( ) ,
140141 elliptic_curve : first. algorithm . elliptic_curve . clone ( ) ,
141142 mode : first. algorithm . mode . clone ( ) ,
@@ -209,14 +210,8 @@ pub fn to_cyclonedx_bom(findings: &[CryptoFinding]) -> CycloneDxBom {
209210 }
210211
211212 // Create library components with `provides` relationships
212- let mut seen_libs: HashSet < String > = HashSet :: new ( ) ;
213- for ( lib_name, algo_refs) in & library_algorithms {
214- if seen_libs. contains ( lib_name) {
215- continue ;
216- }
217- seen_libs. insert ( lib_name. clone ( ) ) ;
218-
219- let lib_ref = format ! ( "lib/{}" , lib_name) ;
213+ for ( ( ecosystem, lib_name) , algo_refs) in & library_algorithms {
214+ let lib_ref = format ! ( "lib/{}/{}" , ecosystem, lib_name) ;
220215
221216 components. push ( BomComponent {
222217 component_type : "library" . to_string ( ) ,
@@ -270,6 +265,27 @@ fn make_bom_ref(name: &str, oid: &Option<String>) -> String {
270265 }
271266}
272267
268+ /// Validate an algorithm family string against the CycloneDX 1.7 registry.
269+ /// Returns `Some(family)` if it's a known value, `None` otherwise.
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" ,
281+ ] ;
282+ if KNOWN_FAMILIES . iter ( ) . any ( |& known| known. eq_ignore_ascii_case ( family) ) {
283+ Some ( family. to_string ( ) )
284+ } else {
285+ None
286+ }
287+ }
288+
273289fn chrono_timestamp ( ) -> String {
274290 // Simple UTC timestamp without chrono dependency
275291 let now = std:: time:: SystemTime :: now ( )
0 commit comments