From 1e421c51e8c3e0822d27e59d3cbc5b74bd750cda Mon Sep 17 00:00:00 2001 From: Manuel Fuchs Date: Wed, 13 Sep 2023 14:20:54 +0200 Subject: [PATCH] Add experimental CycloneDX SBOM support --- Cargo.lock | 172 ++++++++++++++++++++- buildpacks/maven/Cargo.toml | 2 + buildpacks/maven/buildpack.toml | 1 + buildpacks/maven/src/main.rs | 27 ++++ buildpacks/maven/tests/integration/main.rs | 1 + buildpacks/maven/tests/integration/sbom.rs | 82 ++++++++++ 6 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 buildpacks/maven/tests/integration/sbom.rs diff --git a/Cargo.lock b/Cargo.lock index 1d9d2026..2d4950da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "adler" version = "1.0.2" @@ -17,6 +27,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + [[package]] name = "autocfg" version = "1.1.0" @@ -113,6 +129,8 @@ dependencies = [ "libherokubuildpack", "regex", "serde", + "serde-cyclonedx", + "serde_json", "shell-words", "tar", "tempfile", @@ -227,6 +245,72 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.10.7" @@ -387,6 +471,12 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -510,7 +600,7 @@ dependencies = [ "cargo_metadata", "fancy-regex", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -623,6 +713,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + [[package]] name = "proc-macro2" version = "1.0.66" @@ -794,6 +894,33 @@ dependencies = [ "ureq", ] +[[package]] +name = "schemafy_core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bec29dddcfe60f92f3c0d422707b8b56473983ef0481df8d5236ed3ab8fdf24" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "schemafy_lib" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3d87f1df246a9b7e2bfd1f4ee5f88e48b11ef9cfc62e63f0dead255b1a6f5f" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "schemafy_core", + "serde", + "serde_derive", + "serde_json", + "syn 1.0.109", + "uriparse", +] + [[package]] name = "sct" version = "0.7.0" @@ -822,6 +949,24 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-cyclonedx" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "156badd831b352c7e64c85c15c16e8c5513a336c22e409fbc05bbc85a268ba2c" +dependencies = [ + "anyhow", + "derive_builder", + "prettyplease", + "proc-macro2", + "quote", + "schemafy_lib", + "serde", + "serde_json", + "syn 1.0.109", + "thiserror", +] + [[package]] name = "serde_derive" version = "1.0.188" @@ -830,7 +975,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -876,6 +1021,23 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.29" @@ -937,7 +1099,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", ] [[package]] @@ -1098,7 +1260,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -1120,7 +1282,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/buildpacks/maven/Cargo.toml b/buildpacks/maven/Cargo.toml index 9fa6064f..7d4e3985 100644 --- a/buildpacks/maven/Cargo.toml +++ b/buildpacks/maven/Cargo.toml @@ -21,3 +21,5 @@ tar = "0.4.39" libcnb-test.workspace = true java-properties = "2" buildpacks-jvm-shared-test.workspace = true +serde-cyclonedx = "0.8.4" +serde_json = "1.0.104" diff --git a/buildpacks/maven/buildpack.toml b/buildpacks/maven/buildpack.toml index c3e67c53..8b9c79ae 100644 --- a/buildpacks/maven/buildpack.toml +++ b/buildpacks/maven/buildpack.toml @@ -8,6 +8,7 @@ clear-env = true homepage = "https://github.com/heroku/buildpacks-jvm" description = "Official Heroku buildpack for Maven applications." keywords = ["java", "maven", "mvn"] +sbom-formats = ["application/vnd.cyclonedx+json"] [[buildpack.licenses]] type = "BSD-3-Clause" diff --git a/buildpacks/maven/src/main.rs b/buildpacks/maven/src/main.rs index a68a4e89..bc1db704 100644 --- a/buildpacks/maven/src/main.rs +++ b/buildpacks/maven/src/main.rs @@ -36,6 +36,8 @@ use std::process::{Command, ExitStatus}; use buildpacks_jvm_shared_test as _; #[cfg(test)] use java_properties as _; +use libcnb::data::sbom::SbomFormat; +use libcnb::sbom::Sbom; #[cfg(test)] use libcnb_test as _; @@ -267,8 +269,33 @@ impl Buildpack for MavenBuildpack { MavenBuildpackError::MavenBuildUnexpectedExitCode, )?; + util::run_command( + Command::new(&mvn_executable) + .current_dir(&context.app_dir) + .args( + maven_options.iter().chain(&internal_maven_options).chain( + [ + String::from("-DoutputDirectory=bom/"), + String::from("org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom"), + ] + .iter(), + ), + ) + .envs(&mvn_env), + MavenBuildpackError::MavenBuildIoError, + MavenBuildpackError::MavenBuildUnexpectedExitCode, + )?; + let mut build_result_builder = BuildResultBuilder::new(); + build_result_builder = build_result_builder.launch_sbom( + Sbom::from_path( + SbomFormat::CycloneDxJson, + context.app_dir.join("bom/bom.json"), + ) + .unwrap(), + ); + if let Some(process) = framework::default_app_process(&context.app_dir) .map_err(MavenBuildpackError::DefaultAppProcessError)? { diff --git a/buildpacks/maven/tests/integration/main.rs b/buildpacks/maven/tests/integration/main.rs index b90fb0ea..bc8c1547 100644 --- a/buildpacks/maven/tests/integration/main.rs +++ b/buildpacks/maven/tests/integration/main.rs @@ -13,6 +13,7 @@ mod caching; mod customization; mod misc; mod polyglot; +mod sbom; mod settings_xml; mod smoke; mod versions; diff --git a/buildpacks/maven/tests/integration/sbom.rs b/buildpacks/maven/tests/integration/sbom.rs new file mode 100644 index 00000000..3f4d2f61 --- /dev/null +++ b/buildpacks/maven/tests/integration/sbom.rs @@ -0,0 +1,82 @@ +use crate::default_config; +use libcnb::data::buildpack_id; +use libcnb::data::sbom::SbomFormat; +use libcnb_test::{SbomType, TestRunner}; +use serde_cyclonedx::cyclonedx::v_1_4::{Component, CycloneDx, HashAlg}; + +#[test] +#[ignore = "integration test"] +pub(crate) fn sbom() { + TestRunner::default().build(default_config(), |context| { + context.download_sbom_files(|sbom_files| { + let sbom_path = sbom_files.path_for( + buildpack_id!("heroku/maven"), + SbomType::Launch, + SbomFormat::CycloneDxJson, + ); + + let sbom_simple_components = serde_json::from_str::(&std::fs::read_to_string(sbom_path).unwrap()) + .unwrap() + .components + .unwrap_or_default() + .iter() + .map(TryInto::try_into) + .collect::, _>>(); + + assert_eq!(sbom_simple_components, Ok(vec![ + SimpleSbomComponent { purl: String::from("pkg:maven/io.undertow/undertow-core@2.3.5.Final?type=jar"), sha256_hash: String::from("6a74380bc67a6b4a63eef12b882a076662fc1bb831c3dc4688ca2026ea7f9754"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/org.jboss.logging/jboss-logging@3.4.3.Final?type=jar"), sha256_hash: String::from("0b324cca4d550060e51e70cc0045a6cce62f264278ec1f5082aafeb670fcac49"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/org.jboss.xnio/xnio-api@3.8.8.Final?type=jar"), sha256_hash: String::from("701988bea1c7426d0cdbbd94c02141031cfe3001a470750e2d25b6ac166b7873"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/org.wildfly.common/wildfly-common@1.5.4.Final?type=jar"), sha256_hash: String::from("9fda3caf8bd528dec56ebc70daf78f5a9ff5d0bfcea8b3e41ab7ae838747e46a"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/org.wildfly.client/wildfly-client-config@1.0.1.Final?type=jar"), sha256_hash: String::from("80a4e963ce94ebb043ecb0f2c0e77d327f23dc87d81350b863752eedfa2c3bb3"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/org.jboss.xnio/xnio-nio@3.8.8.Final?type=jar"), sha256_hash: String::from("714c2d102c16aba245e5f50007bff49aba4d5e06c5303bd398df071c7614bc5f"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/org.jboss.threads/jboss-threads@3.5.0.Final?type=jar"), sha256_hash: String::from("e150b67a7f528525fe68dd60841520c22d59e0a831ea237c45a704de48b990b1"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/com.google.guava/guava@32.0.0-jre?type=jar"), sha256_hash: String::from("39f3550b0343d8d19dd4e83bd165b58ea3389d2ddb9f2148e63903f79ecdb114"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/com.google.guava/failureaccess@1.0.1?type=jar"), sha256_hash: String::from("a171ee4c734dd2da837e4b16be9df4661afab72a41adaf31eb84dfdaf936ca26"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/com.google.guava/listenablefuture@9999.0-empty-to-avoid-conflict-with-guava?type=jar"), sha256_hash: String::from("b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/com.google.code.findbugs/jsr305@3.0.2?type=jar"), sha256_hash: String::from("766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/org.checkerframework/checker-qual@3.33.0?type=jar"), sha256_hash: String::from("e316255bbfcd9fe50d165314b85abb2b33cb2a66a93c491db648e498a82c2de1"), main_license_id: String::from("MIT") }, + SimpleSbomComponent { purl: String::from("pkg:maven/com.google.errorprone/error_prone_annotations@2.18.0?type=jar"), sha256_hash: String::from("9e6814cb71816988a4fd1b07a993a8f21bb7058d522c162b1de849e19bea54ae"), main_license_id: String::from("Apache-2.0") }, + SimpleSbomComponent { purl: String::from("pkg:maven/com.google.j2objc/j2objc-annotations@2.8?type=jar"), sha256_hash: String::from("f02a95fa1a5e95edb3ed859fd0fb7df709d121a35290eff8b74dce2ab7f4d6ed"), main_license_id: String::from("Apache-2.0") } + ])); + }); + }); +} + +/// A simple representation of an CycloneDX SBOM component for testing purposes. +#[derive(Debug, Eq, PartialEq)] +struct SimpleSbomComponent { + purl: String, + sha256_hash: String, + main_license_id: String, +} + +impl TryFrom<&Component> for SimpleSbomComponent { + type Error = (); + + fn try_from(component: &Component) -> Result { + Ok(SimpleSbomComponent { + purl: component.purl.clone().ok_or(())?, + sha256_hash: component + .hashes + .clone() + .unwrap_or_default() + .into_iter() + .find(|hash| hash.alg == HashAlg::Sha256) + .map(|hash| hash.content) + .ok_or(())?, + main_license_id: component + .licenses + .clone() + .and_then(|license_choices| { + license_choices.first().and_then(|license_choice| { + license_choice + .license + .clone() + .and_then(|license| license.id) + }) + }) + .ok_or(())?, + }) + } +}