diff --git a/flake.nix b/flake.nix index 21bc456..e9331cc 100644 --- a/flake.nix +++ b/flake.nix @@ -104,6 +104,10 @@ packages = [ pkgs.clippy pkgs.rustfmt + pkgs.cargo-machete + pkgs.cargo-edit + pkgs.cargo-bloat + pkgs.cargo-deny ]; inputsFrom = [ transformer ]; diff --git a/nix/buildtime-dependencies.nix b/nix/buildtime-dependencies.nix index 1f61fdf..43a3b0b 100644 --- a/nix/buildtime-dependencies.nix +++ b/nix/buildtime-dependencies.nix @@ -41,10 +41,20 @@ let optionalGetAttrs = names: attrs: lib.genAttrs (builtins.filter (x: lib.hasAttr x attrs) names) (name: attrs.${name}); - # Retrieves only the required fields from a derivation and renames outPath so that - # builtins.toJSON actually emits JSON and not only the nix store path + # Retrieve only the required fields from a derivation. + # + # Also renames outPath so that builtins.toJSON actually emits JSON and not + # only the nix store path. fields = drv: - (optionalGetAttrs [ "name" "pname" "version" "meta" ] drv) // { path = drv.outPath; }; + (optionalGetAttrs [ "name" "pname" "version" "meta" ] drv) // { + path = drv.outPath; + } // lib.optionalAttrs (drv ? src && drv.src ? url) { + src = { + inherit (drv.src) url; + } // lib.optionalAttrs (drv.src ? outputHash) { + hash = drv.src.outputHash; + }; + }; in diff --git a/rust/transformer/Cargo.lock b/rust/transformer/Cargo.lock index 0891569..e29f6d8 100644 --- a/rust/transformer/Cargo.lock +++ b/rust/transformer/Cargo.lock @@ -11,60 +11,17 @@ dependencies = [ "memchr", ] -[[package]] -name = "anstream" -version = "0.6.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - [[package]] name = "anstyle" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" -[[package]] -name = "anstyle-parse" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" -dependencies = [ - "anstyle", - "windows-sys", -] - [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" [[package]] name = "autocfg" @@ -95,9 +52,10 @@ dependencies = [ [[package]] name = "bombon-transformer" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", + "base64", "clap", "cyclonedx-bom", "itertools", @@ -129,10 +87,8 @@ version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ - "anstream", "anstyle", "clap_lex", - "strsim", ] [[package]] @@ -153,12 +109,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" -[[package]] -name = "colorchoice" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" - [[package]] name = "cpufeatures" version = "0.2.12" @@ -245,9 +195,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -260,12 +210,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "is_terminal_polyfill" -version = "1.70.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" - [[package]] name = "itertools" version = "0.12.1" @@ -347,9 +291,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] @@ -394,24 +338,24 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", @@ -420,9 +364,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -455,17 +399,11 @@ dependencies = [ "smallvec", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "syn" -version = "2.0.60" +version = "2.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" dependencies = [ "proc-macro2", "quote", @@ -474,18 +412,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", @@ -535,12 +473,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - [[package]] name = "uuid" version = "1.8.0" @@ -562,79 +494,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" - [[package]] name = "xml-rs" version = "0.8.20" diff --git a/rust/transformer/Cargo.toml b/rust/transformer/Cargo.toml index 25e0385..18b996d 100644 --- a/rust/transformer/Cargo.toml +++ b/rust/transformer/Cargo.toml @@ -1,19 +1,18 @@ [package] name = "bombon-transformer" -version = "0.1.0" +version = "0.2.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -anyhow = "1.0" -clap = { version = "4.5", features = ["derive"] } -cyclonedx-bom = "0.5" -itertools = "0.12" -serde = { version = "1.0", features = [ "derive" ] } -serde_json = "1.0" -sha2 = "0.10" -uuid = "1.8" +anyhow = "1.0.83" +clap = { version = "4.5.4", default-features = false, features = ["std", "derive"] } +cyclonedx-bom = "0.5.0" +itertools = "0.12.1" +serde = { version = "1.0.201", features = [ "derive" ] } +serde_json = "1.0.117" +sha2 = "0.10.8" +uuid = "1.8.0" +base64 = "0.21.7" [lints.rust] unsafe_code = "forbid" diff --git a/rust/transformer/src/cyclonedx.rs b/rust/transformer/src/cyclonedx.rs index 1d10011..070bf14 100644 --- a/rust/transformer/src/cyclonedx.rs +++ b/rust/transformer/src/cyclonedx.rs @@ -1,18 +1,22 @@ use std::path::Path; +use std::str::FromStr; use anyhow::Result; +use cyclonedx_bom::external_models::normalized_string::NormalizedString; use cyclonedx_bom::external_models::uri::Purl; use cyclonedx_bom::models::bom::{Bom, UrnUuid}; -use cyclonedx_bom::models::component::{Classification, Component, Components}; +use cyclonedx_bom::models::component::{Classification, Component, Components, Scope}; use cyclonedx_bom::models::external_reference::{ ExternalReference, ExternalReferenceType, ExternalReferences, }; +use cyclonedx_bom::models::hash::{Hash, HashAlgorithm, HashValue, Hashes}; use cyclonedx_bom::models::license::{License, LicenseChoice, Licenses}; use cyclonedx_bom::models::metadata::Metadata; use cyclonedx_bom::models::tool::{Tool, Tools}; use sha2::{Digest, Sha256}; -use crate::derivation::{self, Derivation, Meta}; +use crate::derivation::{self, Derivation, Meta, Src}; +use crate::hash::{self, SriHash}; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -55,7 +59,7 @@ fn derive_serial_number(data: &[u8]) -> UrnUuid { pub struct CycloneDXComponents(Components); impl CycloneDXComponents { - pub fn new(derivations: impl IntoIterator) -> Self { + pub fn from_derivations(derivations: impl IntoIterator) -> Self { Self(Components( derivations .into_iter() @@ -95,11 +99,26 @@ impl CycloneDXComponent { .to_string(), ), ); + component.scope = Some(Scope::Required); component.purl = Purl::new("nix", &name, &version).ok(); + + let mut external_references = Vec::new(); + + if let Some(src) = derivation.src { + external_references.extend(convert_src(&src)); + } if let Some(meta) = derivation.meta { component.licenses = convert_licenses(&meta); - component.external_references = convert_homepage(&meta).map(ExternalReferences); + component.description = meta.description.map(|s| NormalizedString::new(&s)); + if let Some(homepage) = meta.homepage { + external_references.extend(convert_homepage(&homepage)); + } + } + + if !external_references.is_empty() { + component.external_references = Some(ExternalReferences(external_references)); } + Self(component) } } @@ -122,6 +141,36 @@ fn convert_licenses(meta: &Meta) -> Option { })) } +fn convert_src(src: &Src) -> Option { + Some(ExternalReference { + external_reference_type: ExternalReferenceType::Vcs, + url: src.url.clone().try_into().ok()?, + comment: None, + hashes: src.hash.clone().and_then(|s| convert_hash(&s)), + }) +} + +impl From for HashAlgorithm { + fn from(value: hash::Algorithm) -> Self { + match value { + hash::Algorithm::Md5 => HashAlgorithm::MD5, + hash::Algorithm::Sha1 => HashAlgorithm::SHA1, + hash::Algorithm::Sha256 => HashAlgorithm::SHA256, + hash::Algorithm::Sha512 => HashAlgorithm::SHA512, + } + } +} + +fn convert_hash(s: &str) -> Option { + // If it's not an SRI hash, we'll return None + let sri_hash = SriHash::from_str(s).ok()?; + let hash = Hash { + content: HashValue(sri_hash.hex_digest()), + alg: sri_hash.algorithm.into(), + }; + Some(Hashes(vec![hash])) +} + fn convert_license(license: derivation::License) -> LicenseChoice { match license.spdx_id { Some(spdx_id) => match License::license_id(&spdx_id) { @@ -132,19 +181,13 @@ fn convert_license(license: derivation::License) -> LicenseChoice { } } -fn convert_homepage(meta: &Meta) -> Option> { - match &meta.homepage { - Some(homepage) => Some(vec![ExternalReference { - external_reference_type: ExternalReferenceType::Website, - url: match homepage.to_owned().try_into() { - Ok(uri) => uri, - _ => return None, - }, - comment: None, - hashes: None, - }]), - _ => None, - } +fn convert_homepage(homepage: &str) -> Option { + Some(ExternalReference { + external_reference_type: ExternalReferenceType::Website, + url: homepage.to_owned().try_into().ok()?, + comment: None, + hashes: None, + }) } fn metadata_from_derivation(derivation: Derivation) -> Metadata { diff --git a/rust/transformer/src/derivation.rs b/rust/transformer/src/derivation.rs index 2ff96ed..222fea4 100644 --- a/rust/transformer/src/derivation.rs +++ b/rust/transformer/src/derivation.rs @@ -8,10 +8,14 @@ pub struct Derivation { pub pname: Option, pub version: Option, pub meta: Option, + pub src: Option, } impl Derivation { - pub fn new(store_path: &str) -> Self { + /// Create a `Derivation` from a store path. + /// + /// This can be used if we don't have any information besides the path itself. + pub fn from_store_path(store_path: &str) -> Self { // Because we only have the store path we have to derive the name from it let name = store_path.strip_prefix("/nix/store/").map(|s| { let mut split = s.split('-'); @@ -31,6 +35,7 @@ impl Derivation { pub struct Meta { pub license: Option, pub homepage: Option, + pub description: Option, } #[derive(Deserialize, Clone, Debug)] @@ -64,3 +69,9 @@ pub struct License { #[serde(rename = "spdxId")] pub spdx_id: Option, } + +#[derive(Deserialize, Clone, Debug)] +pub struct Src { + pub url: String, + pub hash: Option, +} diff --git a/rust/transformer/src/hash.rs b/rust/transformer/src/hash.rs new file mode 100644 index 0000000..fd5356f --- /dev/null +++ b/rust/transformer/src/hash.rs @@ -0,0 +1,66 @@ +use std::fmt::Write; +use std::str::FromStr; + +use anyhow::{anyhow, bail, Error, Result}; +use base64::prelude::{Engine as _, BASE64_STANDARD}; + +#[derive(Debug, Clone)] +#[allow(clippy::module_name_repetitions)] +pub struct SriHash { + pub algorithm: Algorithm, + pub digest: Vec, +} + +#[derive(Debug, Clone)] +pub enum Algorithm { + Md5, + Sha1, + Sha256, + Sha512, +} + +impl FromStr for SriHash { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut parsed = s.trim().split('-'); + + let algorithm: Algorithm = parsed + .next() + .and_then(|s| FromStr::from_str(s).ok()) + .ok_or(anyhow!("Failed to parse hash algorithm"))?; + + let digest = parsed + .next() + .and_then(|s| BASE64_STANDARD.decode(s).ok()) + .ok_or(anyhow!("Failed to decode hash digest"))?; + + Ok(Self { algorithm, digest }) + } +} + +impl FromStr for Algorithm { + type Err = Error; + + fn from_str(s: &str) -> Result { + let matched = match s { + "md5" => Self::Md5, + "sha1" => Self::Sha1, + "sha256" => Self::Sha256, + "sha512" => Self::Sha512, + _ => bail!("Failed to parse hash algorithm"), + }; + Ok(matched) + } +} + +impl SriHash { + /// Return the digest as a lower hex encoded string. + pub fn hex_digest(&self) -> String { + let mut buffer = String::new(); + for byte in &self.digest { + let _ = write!(&mut buffer, "{byte:02x}"); + } + buffer + } +} diff --git a/rust/transformer/src/main.rs b/rust/transformer/src/main.rs index 2c56f51..bdc97ee 100644 --- a/rust/transformer/src/main.rs +++ b/rust/transformer/src/main.rs @@ -2,6 +2,7 @@ mod buildtime_input; mod cli; mod cyclonedx; mod derivation; +mod hash; mod runtime_input; mod transform; diff --git a/rust/transformer/src/transform.rs b/rust/transformer/src/transform.rs index 37e2b60..37f6e2f 100644 --- a/rust/transformer/src/transform.rs +++ b/rust/transformer/src/transform.rs @@ -33,7 +33,7 @@ pub fn transform( .0 .get(store_path) .map(ToOwned::to_owned) - .unwrap_or(Derivation::new(store_path)) + .unwrap_or(Derivation::from_store_path(store_path)) }); let buildtime_derivations = buildtime_input @@ -45,9 +45,9 @@ pub fn transform( let components = if include_buildtime_dependencies { let all_derivations = runtime_derivations.chain(buildtime_derivations); - CycloneDXComponents::new(all_derivations) + CycloneDXComponents::from_derivations(all_derivations) } else { - CycloneDXComponents::new(runtime_derivations) + CycloneDXComponents::from_derivations(runtime_derivations) }; let bom = CycloneDXBom::build(target_derivation, components, output);