Skip to content

Commit

Permalink
Add experimental CycloneDX SBOM support
Browse files Browse the repository at this point in the history
  • Loading branch information
Malax committed Sep 30, 2024
1 parent aa6a45b commit fbff0f8
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 0 deletions.
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
5 changes: 5 additions & 0 deletions buildpacks/maven/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ 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::MavenTarballDecompressError(error) => log_please_try_again_error(
"Maven download failed",
"Could not download Maven distribution.",
Expand Down
32 changes: 32 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,7 @@ enum MavenBuildpackError {
MavenBuildIoError(std::io::Error),
CannotSetMavenWrapperExecutableBit(std::io::Error),
DefaultAppProcessError(DefaultAppProcessError),
CannotCreateTemporarySbomDirectory(std::io::Error),
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -243,8 +250,33 @@ impl Buildpack for MavenBuildpack {
MavenBuildpackError::MavenBuildUnexpectedExitCode,
)?;

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("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, sbom_dir.join("bom.json")).unwrap(),
);

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_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::<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("6a74380bc67a6b4a63eef12b882a076662fc1bb831c3dc4688ca2026ea7f9754"), 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(())?,
})
}
}

0 comments on commit fbff0f8

Please sign in to comment.