Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental CycloneDX SBOM support #581

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 236 additions & 65 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions buildpacks/maven/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ tempfile = "3"
buildpacks-jvm-shared-test.workspace = true
java-properties = "2"
libcnb-test = "=0.23.0"
serde-cyclonedx = "0.8.4"
serde_json = "1.0.104"
1 change: 1 addition & 0 deletions buildpacks/maven/buildpack.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ homepage = "https://github.com/heroku/buildpacks-jvm"
description = "Heroku's Maven buildpack. A component of the 'heroku/java' buildpack."
keywords = ["maven", "heroku"]
clear-env = true
sbom-formats = ["application/vnd.cyclonedx+json"]

[[buildpack.licenses]]
type = "BSD-3-Clause"
Expand Down
10 changes: 10 additions & 0 deletions buildpacks/maven/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ pub(crate) fn on_error_maven_buildpack(error: MavenBuildpackError) {
"Could not download Maven distribution.",
error,
),
MavenBuildpackError::CannotCreateTemporarySbomDirectory(error) => log_please_try_again_error(
"Creating temporary directory failed",
"Creating temporary directory for SBOM files failed",
error,
),
MavenBuildpackError::CannotReadMavenSbomFile(error) => log_please_try_again_error(
"Reading Maven SBOM failed",
"Failed to read SBOM produced by Maven.",
error,
),
MavenBuildpackError::MavenTarballDecompressError(error) => log_please_try_again_error(
"Maven download failed",
"Could not download Maven distribution.",
Expand Down
43 changes: 43 additions & 0 deletions buildpacks/maven/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ 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 _;
#[cfg(test)]
use serde_cyclonedx as _;
#[cfg(test)]
use serde_json as _;

mod errors;
mod framework;
Expand Down Expand Up @@ -58,6 +64,8 @@ enum MavenBuildpackError {
MavenBuildIoError(std::io::Error),
CannotSetMavenWrapperExecutableBit(std::io::Error),
DefaultAppProcessError(DefaultAppProcessError),
CannotCreateTemporarySbomDirectory(std::io::Error),
CannotReadMavenSbomFile(std::io::Error),
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -245,6 +253,41 @@ impl Buildpack for MavenBuildpack {

let mut build_result_builder = BuildResultBuilder::new();

if current_or_platform_env
.get("HEROKU_CNB_ENABLE_EXPERIMENTAL_SBOM")
.is_some_and(|value| value == "true")
{
let sbom_dir = std::env::temp_dir().join("heroku-maven-sbom");
fs::create_dir_all(&sbom_dir)
.map_err(MavenBuildpackError::CannotCreateTemporarySbomDirectory)?;

util::run_command(
Command::new(&mvn_executable)
.current_dir(&context.app_dir)
.args(
maven_options.iter().chain(&internal_maven_options).chain(
[
format!("-DoutputDirectory={}", sbom_dir.to_string_lossy()),
String::from("-DoutputName=bom"),
String::from("-DschemaVersion=1.4"),
String::from(
"org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom",
),
]
.iter(),
),
)
.envs(&mvn_env),
MavenBuildpackError::MavenBuildIoError,
MavenBuildpackError::MavenBuildUnexpectedExitCode,
)?;

let launch_sbom = Sbom::from_path(SbomFormat::CycloneDxJson, sbom_dir.join("bom.json"))
.map_err(MavenBuildpackError::CannotReadMavenSbomFile)?;

build_result_builder = build_result_builder.launch_sbom(launch_sbom);
}

if let Some(process) = framework::default_app_process(&context.app_dir)
.map_err(MavenBuildpackError::DefaultAppProcessError)?
{
Expand Down
1 change: 1 addition & 0 deletions buildpacks/maven/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod caching;
mod customization;
mod misc;
mod polyglot;
mod sbom;
mod settings_xml;
mod smoke;
mod versions;
Expand Down
82 changes: 82 additions & 0 deletions buildpacks/maven/tests/integration/sbom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use crate::default_build_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_build_config("test-apps/simple-http-service").env("HEROKU_CNB_ENABLE_EXPERIMENTAL_SBOM", "true"), |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::<CycloneDx>(&std::fs::read_to_string(sbom_path).unwrap())
.unwrap()
.components
.unwrap_or_default()
.iter()
.map(TryInto::try_into)
.collect::<Result<Vec<SimpleSbomComponent>, _>>();

assert_eq!(sbom_simple_components, Ok(vec![
SimpleSbomComponent { purl: String::from("pkg:maven/io.undertow/[email protected]?type=jar"), sha256_hash: String::from("3da2764c7a487e9bf196c9d28c95277648e0c510aa7449e17027b99a1052a53e"), main_license_id: String::from("Apache-2.0") },
SimpleSbomComponent { purl: String::from("pkg:maven/org.jboss.logging/[email protected]?type=jar"), sha256_hash: String::from("0b324cca4d550060e51e70cc0045a6cce62f264278ec1f5082aafeb670fcac49"), main_license_id: String::from("Apache-2.0") },
SimpleSbomComponent { purl: String::from("pkg:maven/org.jboss.xnio/[email protected]?type=jar"), sha256_hash: String::from("701988bea1c7426d0cdbbd94c02141031cfe3001a470750e2d25b6ac166b7873"), main_license_id: String::from("Apache-2.0") },
SimpleSbomComponent { purl: String::from("pkg:maven/org.wildfly.common/[email protected]?type=jar"), sha256_hash: String::from("9fda3caf8bd528dec56ebc70daf78f5a9ff5d0bfcea8b3e41ab7ae838747e46a"), main_license_id: String::from("Apache-2.0") },
SimpleSbomComponent { purl: String::from("pkg:maven/org.wildfly.client/[email protected]?type=jar"), sha256_hash: String::from("80a4e963ce94ebb043ecb0f2c0e77d327f23dc87d81350b863752eedfa2c3bb3"), main_license_id: String::from("Apache-2.0") },
SimpleSbomComponent { purl: String::from("pkg:maven/org.jboss.xnio/[email protected]?type=jar"), sha256_hash: String::from("714c2d102c16aba245e5f50007bff49aba4d5e06c5303bd398df071c7614bc5f"), main_license_id: String::from("Apache-2.0") },
SimpleSbomComponent { purl: String::from("pkg:maven/org.jboss.threads/[email protected]?type=jar"), sha256_hash: String::from("e150b67a7f528525fe68dd60841520c22d59e0a831ea237c45a704de48b990b1"), main_license_id: String::from("Apache-2.0") },
SimpleSbomComponent { purl: String::from("pkg:maven/com.google.guava/[email protected]?type=jar"), sha256_hash: String::from("39f3550b0343d8d19dd4e83bd165b58ea3389d2ddb9f2148e63903f79ecdb114"), main_license_id: String::from("Apache-2.0") },
SimpleSbomComponent { purl: String::from("pkg:maven/com.google.guava/[email protected]?type=jar"), sha256_hash: String::from("a171ee4c734dd2da837e4b16be9df4661afab72a41adaf31eb84dfdaf936ca26"), main_license_id: String::from("Apache-2.0") },
SimpleSbomComponent { purl: String::from("pkg:maven/com.google.guava/[email protected]?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/[email protected]?type=jar"), sha256_hash: String::from("766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7"), main_license_id: String::from("Apache-2.0") },
SimpleSbomComponent { purl: String::from("pkg:maven/org.checkerframework/[email protected]?type=jar"), sha256_hash: String::from("e316255bbfcd9fe50d165314b85abb2b33cb2a66a93c491db648e498a82c2de1"), main_license_id: String::from("MIT") },
SimpleSbomComponent { purl: String::from("pkg:maven/com.google.errorprone/[email protected]?type=jar"), sha256_hash: String::from("9e6814cb71816988a4fd1b07a993a8f21bb7058d522c162b1de849e19bea54ae"), main_license_id: String::from("Apache-2.0") },
SimpleSbomComponent { purl: String::from("pkg:maven/com.google.j2objc/[email protected]?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<Self, Self::Error> {
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(())?,
})
}
}
2 changes: 1 addition & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
allow-unwrap-in-tests = true
doc-valid-idents = ["OpenJDK", ".."]
doc-valid-idents = ["OpenJDK", "CycloneDX", ".."]
Loading