From ae8181fbd4e92c9aa90dc2ee0233e2ffcafbfbd4 Mon Sep 17 00:00:00 2001 From: Patrick Ziegler Date: Thu, 8 Feb 2024 21:47:18 +0100 Subject: [PATCH] Add p2-aware model converter for CycloneDX SBOM generation This change consists of two parts. The first part consists of generating a valid package URL for a given p2 artifact. The PURL contains the symbolic name, version and classifier of the artifact key, as well as the repository it is located in. The second part handles the dependencies between two PURLs. For the sake of consistency, this dependency tree matches the tree calculated by the dependency-tree mojo. --- RELEASE_NOTES.md | 19 ++ pom.xml | 3 +- .../tycho/core/maven/TychoReactorReader.java | 15 +- tycho-its/pom.xml | 8 +- tycho-its/projects/sbom/.mvn/extensions.xml | 8 + tycho-its/projects/sbom/.mvn/maven.config | 1 + .../sbom/example.feature/build.properties | 1 + .../projects/sbom/example.feature/feature.xml | 23 ++ .../sbom/example.plugin/META-INF/MANIFEST.MF | 11 + .../sbom/example.plugin/build.properties | 4 + tycho-its/projects/sbom/pom.xml | 86 +++++ .../projects/sbom/product/example.product | 29 ++ .../projects/sbom/repository/category.xml | 5 + .../projects/sbom/target-definition.target | 11 + .../tycho/test/reactor/BomCreationTest.java | 267 ++++++++++++++++ .../.settings/org.eclipse.jdt.core.prefs | 8 + tycho-sbom/pom.xml | 50 +++ .../tycho/sbom/TychoModelConverter.java | 302 ++++++++++++++++++ .../TychoProjectDependenciesConverter.java | 151 +++++++++ 19 files changed, 996 insertions(+), 6 deletions(-) create mode 100644 tycho-its/projects/sbom/.mvn/extensions.xml create mode 100644 tycho-its/projects/sbom/.mvn/maven.config create mode 100644 tycho-its/projects/sbom/example.feature/build.properties create mode 100644 tycho-its/projects/sbom/example.feature/feature.xml create mode 100644 tycho-its/projects/sbom/example.plugin/META-INF/MANIFEST.MF create mode 100644 tycho-its/projects/sbom/example.plugin/build.properties create mode 100644 tycho-its/projects/sbom/pom.xml create mode 100644 tycho-its/projects/sbom/product/example.product create mode 100644 tycho-its/projects/sbom/repository/category.xml create mode 100644 tycho-its/projects/sbom/target-definition.target create mode 100644 tycho-its/src/test/java/org/eclipse/tycho/test/reactor/BomCreationTest.java create mode 100644 tycho-sbom/.settings/org.eclipse.jdt.core.prefs create mode 100644 tycho-sbom/pom.xml create mode 100644 tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoModelConverter.java create mode 100644 tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoProjectDependenciesConverter.java diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c7ab900f49..c7b66b4afe 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,6 +6,25 @@ If you are reading this in the browser, then you can quickly jump to specific ve ## 5.0.0 (under development) +### Support for CycloneDX Maven Plugin + +The `tycho-sbom` plugin can be added as a dependency to the CycloneDX plugin, +in order to handle the PURL creation of p2 artifacts: + +``` + + org.cyclonedx + cyclonedx-maven-plugin + + + org.eclipse.tycho + tycho-sbom + ${tycho-version} + + + +``` + ### Support for parallel execution of product assembly / archiving The mojos `materialize-products` and `archive-products` now support a new `` parameter diff --git a/pom.xml b/pom.xml index bcd72968fb..f53b628f5e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ tycho-surefire diff --git a/tycho-core/src/main/java/org/eclipse/tycho/core/maven/TychoReactorReader.java b/tycho-core/src/main/java/org/eclipse/tycho/core/maven/TychoReactorReader.java index d53be3b151..3469cedce2 100644 --- a/tycho-core/src/main/java/org/eclipse/tycho/core/maven/TychoReactorReader.java +++ b/tycho-core/src/main/java/org/eclipse/tycho/core/maven/TychoReactorReader.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Christoph Läubrich and others. + * Copyright (c) 2023, 2024 Christoph Läubrich and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -67,7 +67,7 @@ public File findArtifact(Artifact artifact) { }).orElse(null); } - private Optional getTychoReactorProject(Artifact artifact) { + public Optional getTychoReactorProject(Artifact artifact) { if (isTychoReactorArtifact(artifact)) { String projectKey = ArtifactUtils.key(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()); @@ -81,13 +81,20 @@ private Optional getTychoReactorProject(Artifact artifact) { return Optional.empty(); } - private boolean isTychoReactorArtifact(Artifact artifact) { + public boolean isTychoReactorArtifact(Artifact artifact) { if (artifact.getClassifier() == null || artifact.getClassifier().isBlank()) { - return PackagingType.TYCHO_PACKAGING_TYPES.contains(artifact.getProperty("type", "")); + return PackagingType.TYCHO_PACKAGING_TYPES.contains(getPackagingType(artifact)); } return false; } + public String getPackagingType(Artifact artifact) { + if (artifact != null) { + return artifact.getProperty("type", ""); + } + return null; + } + @Override public List findVersions(Artifact artifact) { return getTychoReactorProject(artifact).map(project -> List.of(artifact.getVersion())).orElse(List.of()); diff --git a/tycho-its/pom.xml b/tycho-its/pom.xml index 8ea65cd6e8..e5fd9535d3 100644 --- a/tycho-its/pom.xml +++ b/tycho-its/pom.xml @@ -1,6 +1,6 @@ + + org.cyclonedx + cyclonedx-core-java + 8.0.3 + diff --git a/tycho-its/projects/sbom/.mvn/extensions.xml b/tycho-its/projects/sbom/.mvn/extensions.xml new file mode 100644 index 0000000000..ff2889110d --- /dev/null +++ b/tycho-its/projects/sbom/.mvn/extensions.xml @@ -0,0 +1,8 @@ + + + + org.eclipse.tycho + tycho-build + ${tycho-version} + + diff --git a/tycho-its/projects/sbom/.mvn/maven.config b/tycho-its/projects/sbom/.mvn/maven.config new file mode 100644 index 0000000000..babb6c469f --- /dev/null +++ b/tycho-its/projects/sbom/.mvn/maven.config @@ -0,0 +1 @@ +-Dtycho-version=5.0.0-SNAPSHOT diff --git a/tycho-its/projects/sbom/example.feature/build.properties b/tycho-its/projects/sbom/example.feature/build.properties new file mode 100644 index 0000000000..64f93a9f0b --- /dev/null +++ b/tycho-its/projects/sbom/example.feature/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/tycho-its/projects/sbom/example.feature/feature.xml b/tycho-its/projects/sbom/example.feature/feature.xml new file mode 100644 index 0000000000..653f3f980f --- /dev/null +++ b/tycho-its/projects/sbom/example.feature/feature.xml @@ -0,0 +1,23 @@ + + + + + [Enter Feature Description here.] + + + + [Enter Copyright Description here.] + + + + [Enter License Description here.] + + + + + diff --git a/tycho-its/projects/sbom/example.plugin/META-INF/MANIFEST.MF b/tycho-its/projects/sbom/example.plugin/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..49a9b4e514 --- /dev/null +++ b/tycho-its/projects/sbom/example.plugin/META-INF/MANIFEST.MF @@ -0,0 +1,11 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Plugin with SBOM +Bundle-SymbolicName: example.plugin +Bundle-Version: 1.0.0.qualifier +Require-Bundle: org.eclipse.core.databinding;bundle-version="1.13.100", + org.eclipse.core.databinding.beans;bundle-version="1.10.100", + org.eclipse.core.databinding.observable;bundle-version="1.13.100", + org.eclipse.core.databinding.property;bundle-version="1.10.100" +Automatic-Module-Name: example.plugin +Bundle-RequiredExecutionEnvironment: JavaSE-17 diff --git a/tycho-its/projects/sbom/example.plugin/build.properties b/tycho-its/projects/sbom/example.plugin/build.properties new file mode 100644 index 0000000000..34d2e4d2da --- /dev/null +++ b/tycho-its/projects/sbom/example.plugin/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/tycho-its/projects/sbom/pom.xml b/tycho-its/projects/sbom/pom.xml new file mode 100644 index 0000000000..6247a9b633 --- /dev/null +++ b/tycho-its/projects/sbom/pom.xml @@ -0,0 +1,86 @@ + + 4.0.0 + tycho-demo + sbom + 1.0.0-SNAPSHOT + pom + + + 5.0.0-SNAPSHOT + https://www.example.p2.repo/ + + + + example.feature + example.plugin + product + repository + + + + + + org.eclipse.tycho + tycho-maven-plugin + ${tycho-version} + true + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + ../target-definition.target + + + + linux + gtk + x86_64 + + + + + + org.eclipse.tycho + tycho-packaging-plugin + ${tycho-version} + + today + + + + org.cyclonedx + cyclonedx-maven-plugin + 2.7.9 + + true + + + + + makeBom + + + + + + org.eclipse.tycho + tycho-sbom + ${tycho-version} + + + + + + + + org.eclipse.tycho + tycho-p2-director-plugin + ${tycho-version} + + + + + diff --git a/tycho-its/projects/sbom/product/example.product b/tycho-its/projects/sbom/product/example.product new file mode 100644 index 0000000000..251a1e1fb4 --- /dev/null +++ b/tycho-its/projects/sbom/product/example.product @@ -0,0 +1,29 @@ + + + + + + + + + + -XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts + + + + + + + + + + + + + + + + + + + diff --git a/tycho-its/projects/sbom/repository/category.xml b/tycho-its/projects/sbom/repository/category.xml new file mode 100644 index 0000000000..11a78fe996 --- /dev/null +++ b/tycho-its/projects/sbom/repository/category.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tycho-its/projects/sbom/target-definition.target b/tycho-its/projects/sbom/target-definition.target new file mode 100644 index 0000000000..938d5b5f7b --- /dev/null +++ b/tycho-its/projects/sbom/target-definition.target @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tycho-its/src/test/java/org/eclipse/tycho/test/reactor/BomCreationTest.java b/tycho-its/src/test/java/org/eclipse/tycho/test/reactor/BomCreationTest.java new file mode 100644 index 0000000000..7ebd6df798 --- /dev/null +++ b/tycho-its/src/test/java/org/eclipse/tycho/test/reactor/BomCreationTest.java @@ -0,0 +1,267 @@ +/******************************************************************************* + * Copyright (c) 2024 Patrick Ziegler and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.test.reactor; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.apache.maven.it.Verifier; +import org.cyclonedx.exception.ParseException; +import org.cyclonedx.model.Bom; +import org.cyclonedx.model.Dependency; +import org.cyclonedx.parsers.Parser; +import org.cyclonedx.parsers.XmlParser; +import org.eclipse.tycho.test.AbstractTychoIntegrationTest; +import org.junit.Before; +import org.junit.Test; + +public class BomCreationTest extends AbstractTychoIntegrationTest { + private static Verifier verifier; + + @Before + public void setUp() throws Exception { + if (verifier == null) { + verifier = getVerifier("sbom", false); + verifier.executeGoal("verify"); + verifyErrorFreeLog(verifier); + } + } + + @Test + public void verifyBundle() throws Exception { + String bomPath = getBomPath("example.plugin"); + verifier.verifyFilePresent(bomPath); + + Bom bom = getBom(bomPath); + List dependencies = bom.getDependencies(); + verifyBundleDependencies(dependencies); + verifyCommonDependencies(dependencies); + assertEquals(dependencies.size(), 7); + } + + @Test + public void verifyFeature() throws Exception { + String bomPath = getBomPath("example.feature"); + verifier.verifyFilePresent(bomPath); + + Bom bom = getBom(bomPath); + List dependencies = bom.getDependencies(); + verifyFeatureDependencies(dependencies); + verifyBundleDependencies(dependencies); + verifyCommonDependencies(dependencies); + assertEquals(dependencies.size(), 8); + } + + @Test + public void verifyRepository() throws Exception { + String bomPath = getBomPath("repository"); + verifier.verifyFilePresent(bomPath); + + Bom bom = getBom(bomPath); + List dependencies = bom.getDependencies(); + verifyRepositoryDependencies(dependencies); + verifyBundleDependencies(dependencies); + verifyCommonDependencies(dependencies); + // dependency to example.plugin is already satisfied by repository + // verifyFeatureDependencies(dependencies); + Dependency feature = getDependency(dependencies, + "pkg:p2/example.feature@1.0.0.today?classifier=org.eclipse.update.feature&location=https://www.example.p2.repo/"); + assertNull(feature.getDependencies()); + assertEquals(dependencies.size(), 9); + } + + @Test + public void verifyProduct() throws Exception { + String bomPath = getBomPath("product"); + verifier.verifyFilePresent(bomPath); + + Bom bom = getBom(bomPath); + List dependencies = bom.getDependencies(); + verifyProductDependencies(dependencies); + // All dependencies are already satisfied by the product + // verifyBundleDependencies(dependencies); + // verifyFeatureDependencies(dependencies); + // verifyRepositoryDependencies(dependencies); + Dependency dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.beans@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.observable@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.property@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.common@3.18.200.v20231106-1826?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.osgi@3.18.600.v20231110-1900?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.launcher@1.6.600.v20231106-1826?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.launcher.gtk.linux.x86_64@1.2.800.v20231003-1442?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.executable@3.8.2300.v20231106-1826?classifier=org.eclipse.update.feature&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.executable_root.gtk.linux.x86_64@3.8.2300.v20231106-1826?classifier=binary&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:maven/p2.p2.installable.unit/org.eclipse.equinox.executable_root.gtk.linux.x86_64@3.8.2300.v20231106-1826?type=p2-installable-unit"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/example.plugin@1.0.0.today?classifier=osgi.bundle&location=https://www.example.p2.repo/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/example.feature@1.0.0.today?classifier=org.eclipse.update.feature&location=https://www.example.p2.repo/"); + assertNull(dependency.getDependencies()); + // + assertEquals(dependencies.size(), 14); + } + + private void verifyBundleDependencies(List dependencies) { + Dependency dependency = getDependency(dependencies, + "pkg:p2/example.plugin@1.0.0.today?classifier=osgi.bundle&location=https://www.example.p2.repo/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.core.databinding@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.core.databinding.beans@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.core.databinding.observable@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.core.databinding.property@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertEquals(dependency.getDependencies().size(), 4); + } + + private void verifyFeatureDependencies(List dependencies) { + Dependency dependency = getDependency(dependencies, + "pkg:p2/example.feature@1.0.0.today?classifier=org.eclipse.update.feature&location=https://www.example.p2.repo/"); + verifyDependency(dependency, + "pkg:p2/example.plugin@1.0.0.today?classifier=osgi.bundle&location=https://www.example.p2.repo/"); + assertEquals(dependency.getDependencies().size(), 1); + } + + private void verifyRepositoryDependencies(List dependencies) { + Dependency dependency = getDependency(dependencies, + "pkg:maven/tycho-demo/repository.eclipse-repository@1.0.0-SNAPSHOT?type=eclipse-repository"); + verifyDependency(dependencies, + "pkg:p2/example.plugin@1.0.0.today?classifier=osgi.bundle&location=https://www.example.p2.repo/"); + verifyDependency(dependency, + "pkg:p2/example.feature@1.0.0.today?classifier=org.eclipse.update.feature&location=https://www.example.p2.repo/"); + assertEquals(dependency.getDependencies().size(), 2); + } + + private void verifyCommonDependencies(List dependencies) { + Dependency dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.equinox.common@3.18.200.v20231106-1826?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependency, + "pkg:p2/org.eclipse.osgi@3.18.600.v20231110-1900?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertEquals(dependency.getDependencies().size(), 2); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.beans@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.observable@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.property@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.equinox.common@3.18.200.v20231106-1826?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + // + dependency = getDependency(dependencies, + "pkg:p2/org.eclipse.osgi@3.18.600.v20231110-1900?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertNull(dependency.getDependencies()); + } + + private void verifyProductDependencies(List dependencies) { + Dependency dependency = getDependency(dependencies, + "pkg:maven/tycho-demo/example@1.0.0-SNAPSHOT?type=eclipse-repository"); + verifyDependency(dependencies, + "pkg:p2/example.plugin@1.0.0.today?classifier=osgi.bundle&location=https://www.example.p2.repo/"); + verifyDependency(dependency, + "pkg:p2/example.feature@1.0.0.today?classifier=org.eclipse.update.feature&location=https://www.example.p2.repo/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.beans@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.observable@1.13.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.core.databinding.property@1.10.100.v20230708-0916?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.equinox.common@3.18.200.v20231106-1826?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + verifyDependency(dependencies, + "pkg:p2/org.eclipse.osgi@3.18.600.v20231110-1900?classifier=osgi.bundle&location=https://download.eclipse.org/releases/2023-12/"); + assertEquals(dependency.getDependencies().size(), 8); + } + + private void verifyDependency(Dependency parent, String ref) { + verifyDependency(parent.getDependencies(), ref); + } + + private void verifyDependency(List dependencies, String ref) { + if (dependencies.stream().noneMatch(dependency -> match(dependency, ref))) { + fail("No dependency found matching: " + ref); + } + } + + private Dependency getDependency(List dependencies, String ref) { + return dependencies.stream().filter(dependency -> match(dependency, ref)).findFirst().orElseThrow(); + } + + private boolean match(Dependency dependency, String ref) { + return URLDecoder.decode(dependency.getRef(), StandardCharsets.UTF_8).equals(ref); + } + + private String getBomPath(String projectName) { + return projectName + "/target/bom.xml"; + } + + private Bom getBom(String bomPath) throws ParseException { + Parser parser = new XmlParser(); + File bom = new File(verifier.getBasedir(), bomPath); + return parser.parse(bom); + } +} diff --git a/tycho-sbom/.settings/org.eclipse.jdt.core.prefs b/tycho-sbom/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..cf2cd4590a --- /dev/null +++ b/tycho-sbom/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/tycho-sbom/pom.xml b/tycho-sbom/pom.xml new file mode 100644 index 0000000000..4ec09d123a --- /dev/null +++ b/tycho-sbom/pom.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + org.eclipse.tycho + tycho + 5.0.0-SNAPSHOT + + tycho-sbom + Tycho SBOM model extension + + + + org.eclipse.tycho + tycho-core + ${project.version} + + + org.eclipse.tycho + tycho-api + ${project.version} + + + + org.apache.maven + maven-plugin-api + + + + org.cyclonedx + cyclonedx-maven-plugin + 2.7.10 + + + + + + + org.codehaus.plexus + plexus-component-metadata + + + + generate-metadata + + + + + + + \ No newline at end of file diff --git a/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoModelConverter.java b/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoModelConverter.java new file mode 100644 index 0000000000..4cd9ca814a --- /dev/null +++ b/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoModelConverter.java @@ -0,0 +1,302 @@ +/******************************************************************************* + * Copyright (c) 2024 Patrick Ziegler and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.sbom; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import javax.inject.Inject; + +import org.apache.maven.RepositoryUtils; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.LegacySupport; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.component.annotations.Component; +import org.cyclonedx.maven.DefaultModelConverter; +import org.cyclonedx.maven.ModelConverter; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.equinox.p2.core.ProvisionException; +import org.eclipse.equinox.p2.metadata.IArtifactKey; +import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository; +import org.eclipse.tycho.ArtifactKey; +import org.eclipse.tycho.DefaultArtifactKey; +import org.eclipse.tycho.MavenRepositoryLocation; +import org.eclipse.tycho.ReactorProject; +import org.eclipse.tycho.core.TargetPlatformConfiguration; +import org.eclipse.tycho.core.TychoProjectManager; +import org.eclipse.tycho.core.maven.TychoReactorReader; +import org.eclipse.tycho.core.osgitools.DefaultReactorProject; +import org.eclipse.tycho.core.resolver.target.ArtifactTypeHelper; +import org.eclipse.tycho.p2maven.repository.P2RepositoryManager; +import org.eclipse.tycho.targetplatform.TargetDefinition.InstallableUnitLocation; +import org.eclipse.tycho.targetplatform.TargetDefinition.Location; +import org.eclipse.tycho.targetplatform.TargetDefinition.Repository; +import org.eclipse.tycho.targetplatform.TargetDefinitionFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Custom implementation of the CycloneDX model converter with support for both + * Maven and p2 artifacts. The generated PURL is usually of the form: + * + *
+ * pkg:/p2/<id>@<version>?classifier=<classifier>&location=<download-url>
+ * 
+ * + * This converter can be used with the {@code cyclonedx-maven-plugin} by adding + * it as a dependency as follows: + * + *
+ * <plugin>
+ *   <groupId>org.cyclonedx</groupId>
+ *   <artifactId>cyclonedx-maven-plugin</artifactId>
+ *   <dependencies>
+ *     <dependency>
+ *       <groupId>org.eclipse.tycho.extras</groupId>
+ *       <artifactId>tycho-sbom</artifactId>
+ *     </dependency>
+ *   </dependencies>
+ * </plugin>
+ * 
+ */ +@Component(role = ModelConverter.class) +public class TychoModelConverter extends DefaultModelConverter { + private static final Logger LOG = LoggerFactory.getLogger(TychoModelConverter.class); + + @Inject + private P2RepositoryManager repositoryManager; + + @Inject + private TychoProjectManager projectManager; + + @Inject + private TychoReactorReader reactorReader; + + @Inject + private LegacySupport legacySupport; + + @Override + public String generatePackageUrl(org.apache.maven.artifact.Artifact mavenArtifact) { + Artifact artifact = RepositoryUtils.toArtifact(mavenArtifact); + return generatePackageUrl(artifact, true, true, () -> super.generatePackageUrl(mavenArtifact)); + } + + @Override + public String generatePackageUrl(Artifact artifact) { + return generatePackageUrl(RepositoryUtils.toArtifact(artifact)); + } + + @Override + public String generateVersionlessPackageUrl(org.apache.maven.artifact.Artifact mavenArtifact) { + Artifact artifact = RepositoryUtils.toArtifact(mavenArtifact); + return generatePackageUrl(artifact, false, true, () -> super.generateVersionlessPackageUrl(mavenArtifact)); + } + + @Override + public String generateVersionlessPackageUrl(Artifact artifact) { + return generateVersionlessPackageUrl(artifact); + } + + @Override + public String generateClassifierlessPackageUrl(Artifact artifact) { + return generatePackageUrl(artifact, true, false, () -> super.generateClassifierlessPackageUrl(artifact)); + } + + /** + * Calculates the package URL for the given artifact. For OSGi artifacts, the + * {@code p2} type is chosen, otherwise {@code maven}. + * + * @param artifact One of the artifacts available in the current reactor + * build. + * @param withVersion Whether the version should be included in the PURL. For + * OSGi bundles, the {@code -SNAPSHOT} is replaced by the + * actual build qualifier. + * @param withClassifier Whether the classifier should be included in the PURL. + * @param fallback The method for generating the Maven PURL, in case the + * artifact is not an OSGi bundle. + * @return + */ + private String generatePackageUrl(Artifact artifact, boolean withVersion, boolean withClassifier, + Supplier fallback) { + if (reactorReader.isTychoReactorArtifact(artifact)) { + ArtifactKey artifactKey = getQualifiedArtifactKey(artifact); + IArtifactKey p2artifactKey = ArtifactTypeHelper.toP2ArtifactKey(artifactKey); + boolean isReactorProject = reactorReader.getTychoReactorProject(artifact).isPresent(); + if (p2artifactKey != null) { + String p2purl = generateP2PackageUrl(p2artifactKey, withVersion, withClassifier, isReactorProject); + if (p2purl != null) { + return p2purl; + } + } + } + return fallback.get(); + } + + /** + * Creates the actual package URL for the p2 artifact key, using the following + * format: + *
scheme:type/namespace/name@version?qualifiers#subpath
+ * The scheme and type of the artifact are always {@code pkg} and {@code p2}, + * respectively. The namespace is optional and not used. The name and version + * corresponds to {@link IArtifactKey#getId()} and + * {@link ArtifactKey#getVersion()}. The qualifier contains the optional key + * {@code classifier} with value {@link IArtifactKey#getClassifier()}and the + * mandatory key {@code location} with the percentage-encoded repository URL + * containing this artifact. + * + * @param p2artifactKey One of the p2 artifacts available in the current + * reactor build. + * @param withVersion Whether the version should be included in the PURL. + * For OSGi bundles, the {@code -SNAPSHOT} is replaced + * by the actual build qualifier. + * @param withClassifier Whether the classifier should be included in the + * PURL. + * @param isReactorProject if {@code true}, then the given artifact key + * corresponds to a reactor project. + * @return + * @see here + */ + /* package */ String generateP2PackageUrl(IArtifactKey p2artifactKey, boolean withVersion, boolean withClassifier, + boolean isReactorProject) { + String location = getRepositoryLocation(p2artifactKey, isReactorProject); + if (location == null) { + LOG.warn("Unknown p2 repository for artifact: " + p2artifactKey.getId()); + return null; + } + String encodedLocation = URLEncoder.encode(location, StandardCharsets.UTF_8); + StringBuilder builder = new StringBuilder(); + builder.append("pkg:p2/"); + builder.append(p2artifactKey.getId()); + if (withVersion) { + builder.append('@'); + builder.append(p2artifactKey.getVersion()); + } + builder.append('?'); + if (withClassifier) { + builder.append("classifier="); + builder.append(p2artifactKey.getClassifier()); + builder.append('&'); + } + builder.append("location="); + builder.append(encodedLocation); + return builder.toString(); + } + + /** + * Calculates the first repository from which the given artifact can be + * downloaded. If the artifact is part of the local reactor build, the URL + * specified in the {@code tycho.sbom.url} property is returned (as it has not + * yet been deployed to any update site). If the artifact is not available on + * any of the repositories, {@code null} is returned. + * + * @param p2artifactKey The P2 coordinates of the artifact. Used to check + * whether the artifact is available on one of the p2 + * update sites. + * @param isReactorProject if {@code true}, then the given artifact key + * corresponds to a reactor project. + * @return The base URl of the repository or {@code null}, if the artifact isn't + * hosted on any known repository. + */ + private final String getRepositoryLocation(IArtifactKey p2artifactKey, boolean isReactorProject) { + MavenSession currentSession = legacySupport.getSession(); + if (currentSession == null) { + LOG.error("Maven session couldn't be found."); + return null; + } + + MavenProject currentProject = currentSession.getCurrentProject(); + + // Artifacts from the current reactor build haven't been deployed yet. Use a + // build property to determine which repository they are published to. + if (isReactorProject) { + String defaultUrl = currentProject.getProperties().getProperty("tycho.sbom.url"); + if (defaultUrl == null) { + LOG.error("'tycho.sbom.url' property not set."); + } + return defaultUrl; + } + + // Iterate over all p2 repository and return the first one containing the + // artifact. Note that the location might be arbitrary, if the artifact is + // contained by multiple repositories. + for (Repository repository : getTargetRepositories(currentProject)) { + String id = repository.getId(); + URI location = URI.create(repository.getLocation()); + MavenRepositoryLocation mavenRepository = new MavenRepositoryLocation(id, location); + try { + IArtifactRepository artifactRepository = repositoryManager.getArtifactRepository(mavenRepository); + if (artifactRepository.contains(p2artifactKey)) { + return repository.getLocation(); + } + } catch (ProvisionException e) { + LOG.error(e.getMessage(), e); + } + } + return null; + } + + /** + * Returns the Eclipse/OSGi {@link ArtifactKey} of the given artifact. For + * reactor projects, the (optional) {@code -SNAPSHOT} of suffix of the version + * string is replaced by its expanded version. + * + * @param artifact A Maven artifact with valid GAV + * @return The Eclipse/OSGi artifact key of the given artifact. + */ + private ArtifactKey getQualifiedArtifactKey(Artifact artifact) { + String expandedVersion = artifact.getVersion(); + + MavenProject mavenProject = reactorReader.getTychoReactorProject(artifact).orElse(null); + if (mavenProject != null) { + ReactorProject reactorProject = DefaultReactorProject.adapt(mavenProject); + if (reactorProject != null) { + expandedVersion = reactorProject.getExpandedVersion(); + } else { + LOG.error(mavenProject + " is not a Tycho project."); + } + } + + String type = reactorReader.getPackagingType(artifact); + return new DefaultArtifactKey(type, artifact.getArtifactId(), expandedVersion); + } + + /** + * Returns a list of all target repositories which are accessible by the given + * Maven project. All IUs required by this proejct should be accessible via one + * of those repositories. + * + * @param currentProject The current project of the reactor build, for which the + * SBOM is generated. + * @return An unmodifiable list of all target repositories. + */ + private List getTargetRepositories(MavenProject currentProject) { + TargetPlatformConfiguration targetConfiguration = projectManager.getTargetPlatformConfiguration(currentProject); + List p2repositories = new ArrayList<>(); + + for (TargetDefinitionFile targetFile : targetConfiguration.getTargets()) { + for (Location location : targetFile.getLocations()) { + if (location instanceof InstallableUnitLocation iuLocation) { + p2repositories.addAll(iuLocation.getRepositories()); + } + } + } + + return Collections.unmodifiableList(p2repositories); + } +} diff --git a/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoProjectDependenciesConverter.java b/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoProjectDependenciesConverter.java new file mode 100644 index 0000000000..28cd2c73ca --- /dev/null +++ b/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/TychoProjectDependenciesConverter.java @@ -0,0 +1,151 @@ +/******************************************************************************* + * Copyright (c) 2024 Patrick Ziegler and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.sbom; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import javax.inject.Inject; + +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.LegacySupport; +import org.apache.maven.project.MavenProject; +import org.cyclonedx.maven.DefaultProjectDependenciesConverter; +import org.cyclonedx.maven.ProjectDependenciesConverter; +import org.cyclonedx.model.Component; +import org.cyclonedx.model.Dependency; +import org.cyclonedx.model.Metadata; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.equinox.p2.metadata.IArtifactKey; +import org.eclipse.equinox.p2.metadata.IInstallableUnit; +import org.eclipse.tycho.p2.tools.P2DependencyTreeGenerator; +import org.eclipse.tycho.p2.tools.P2DependencyTreeGenerator.DependencyTreeNode; +import org.eclipse.tycho.p2maven.MavenProjectDependencyProcessor; +import org.eclipse.tycho.p2maven.MavenProjectDependencyProcessor.ProjectDependencyClosure; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@org.codehaus.plexus.component.annotations.Component(role = ProjectDependenciesConverter.class) +public class TychoProjectDependenciesConverter extends DefaultProjectDependenciesConverter { + private static final Logger LOG = LoggerFactory.getLogger(TychoProjectDependenciesConverter.class); + + @Inject + private LegacySupport legacySupport; + + @Inject + private TychoModelConverter modelConverter; + + @Inject + private P2DependencyTreeGenerator dependencyGenerator; + + @Inject + private MavenProjectDependencyProcessor dependencyProcessor; + + @Override + public void cleanupBomDependencies(Metadata metadata, Map components, + Map dependencies) { + final MavenSession mavenSession = legacySupport.getSession(); + final MavenProject currentProject = mavenSession.getCurrentProject(); + // + try { + Set unmapped = new HashSet<>(); + List rootNodes = dependencyGenerator.buildDependencyTree(currentProject, unmapped); + // Something doesn't seem right... + if (rootNodes.isEmpty() && unmapped.isEmpty()) { + LOG.info("Project " + currentProject + " doesn't seem to be a Tycho project. Skip..."); + return; + } + // Synchronize with dependency tree node + TreeSet newDependencies = new TreeSet<>(Comparator.comparing(Dependency::getRef)); + for (DependencyTreeNode rootNode : rootNodes) { + convertToDependency(rootNode, newDependencies); + } + for (IInstallableUnit iu : unmapped) { + for (String bomRef : getBomRepresentation(iu)) { + newDependencies.add(new Dependency(bomRef)); + } + } + for (Dependency dependency : newDependencies) { + dependencies.put(dependency.getRef(), dependency); + } + } catch (CoreException e) { + LOG.error(e.getMessage()); + } + } + + private void convertToDependency(DependencyTreeNode node, Set dependencies) { + for (String bomRef : getBomRepresentation(node.getInstallableUnit())) { + Dependency dependency = new Dependency(bomRef); + for (DependencyTreeNode childNode : node.getChildren()) { + for (String childBomRef : getBomRepresentation(childNode.getInstallableUnit())) { + dependency.addDependency(new Dependency(childBomRef)); + } + convertToDependency(childNode, dependencies); + } + dependencies.add(dependency); + } + } + + /** + * Calculates the BOM representation of the give {@link IInstallableUnit}. We + * need to distinguish between IUs that are part of the current reactor build + * and IUs external IUs, in order to properly handle e.g. the version qualifier. + * For reactor IUs, the BOM is calculated using the Maven artifact, otherwise + * via the {@link IArtifactKey}s. If no BOM representation can be calculated, an + * empty list is returned. + * + * @param iu The installable unit for which to generate + * @return A {@code mutable} list of all bom representation. matching the given + * IU. + */ + private List getBomRepresentation(IInstallableUnit iu) { + final MavenSession mavenSession = legacySupport.getSession(); + final List reactorProjects = mavenSession.getAllProjects(); + // mutable! + List bomRefs = new ArrayList<>(); + // (I) IU describes local reactor project + try { + ProjectDependencyClosure dependencyClosure = dependencyProcessor + .computeProjectDependencyClosure(reactorProjects, mavenSession); + MavenProject iuProject = dependencyClosure.getProject(iu).orElse(null); + if (iuProject != null) { + String bomRef = modelConverter.generatePackageUrl(iuProject.getArtifact()); + if (bomRef == null) { + LOG.error("Unable to calculate BOM for: " + iuProject); + return bomRefs; + } + bomRefs.add(bomRef); + return bomRefs; + } + } catch (CoreException e) { + LOG.error(e.getMessage(), e); + return Collections.emptyList(); + } + // (II) IU describes external artifact + for (IArtifactKey p2artifactKey : iu.getArtifacts()) { + String bomRef = modelConverter.generateP2PackageUrl(p2artifactKey, true, true, false); + if (bomRef == null) { + LOG.error("Unable to calculate BOM for: " + p2artifactKey); + continue; + } + bomRefs.add(bomRef); + } + return bomRefs; // mutable! + } +}