diff --git a/tycho-extras/tycho-sbom/.settings/org.eclipse.jdt.core.prefs b/tycho-extras/tycho-sbom/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000000..cf2cd4590a
--- /dev/null
+++ b/tycho-extras/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-extras/tycho-sbom/pom.xml b/tycho-extras/tycho-sbom/pom.xml
new file mode 100644
index 0000000000..caac2ab4de
--- /dev/null
+++ b/tycho-extras/tycho-sbom/pom.xml
@@ -0,0 +1,55 @@
+
+ 4.0.0
+
+ org.eclipse.tycho.extras
+ tycho-extras
+ 5.0.0-SNAPSHOT
+
+ tycho-sbom
+ Tycho SBOM model extension
+
+
+
+ org.eclipse.tycho
+ tycho-core
+
+
+ org.eclipse.tycho
+ tycho-api
+ ${project.version}
+
+
+ org.eclipse.tycho
+ p2-maven-plugin
+ ${project.version}
+ maven-plugin
+
+
+ org.eclipse.tycho
+ tycho-versions-plugin
+ ${project.version}
+ maven-plugin
+
+
+
+ org.apache.maven
+ maven-plugin-api
+
+
+ org.codehaus.plexus
+ plexus-component-annotations
+
+
+
+ org.cyclonedx
+ cyclonedx-maven-plugin
+ 2.7.10
+
+
+
+ junit
+ junit
+ test
+
+
+
\ No newline at end of file
diff --git a/tycho-extras/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/P2ModelConverter.java b/tycho-extras/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/P2ModelConverter.java
new file mode 100644
index 0000000000..ba31336985
--- /dev/null
+++ b/tycho-extras/tycho-sbom/src/main/java/org/eclipse/tycho/sbom/P2ModelConverter.java
@@ -0,0 +1,127 @@
+/*******************************************************************************
+ * Copyright (c) 2023 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.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Set;
+
+import org.apache.maven.RepositoryUtils;
+import org.apache.maven.artifact.Artifact;
+import org.codehaus.plexus.component.annotations.Component;
+import org.cyclonedx.maven.DefaultModelConverter;
+import org.cyclonedx.maven.ModelConverter;
+import org.eclipse.equinox.p2.metadata.IArtifactKey;
+import org.eclipse.tycho.ArtifactKey;
+import org.eclipse.tycho.ArtifactType;
+import org.eclipse.tycho.DefaultArtifactKey;
+import org.eclipse.tycho.core.resolver.target.ArtifactTypeHelper;
+import org.eclipse.tycho.versions.engine.Versions;
+
+/**
+ * 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, hint = "p2")
+public class P2ModelConverter extends DefaultModelConverter {
+ private static final Set SUPPORTED_TYPES = Set.of(ArtifactType.TYPE_BUNDLE_FRAGMENT,
+ ArtifactType.TYPE_ECLIPSE_PLUGIN, ArtifactType.TYPE_ECLIPSE_FEATURE);
+
+ @Override
+ public String generatePackageUrl(Artifact artifact) {
+ if (SUPPORTED_TYPES.contains(artifact.getType())) {
+ return generateP2PackageUrl(artifact, true, true);
+ }
+ return super.generatePackageUrl(artifact);
+ }
+
+ @Override
+ public String generatePackageUrl(org.eclipse.aether.artifact.Artifact artifact) {
+ return generatePackageUrl(RepositoryUtils.toArtifact(artifact));
+ }
+
+ @Override
+ public String generateVersionlessPackageUrl(Artifact artifact) {
+ if (SUPPORTED_TYPES.contains(artifact.getType())) {
+ return generateP2PackageUrl(artifact, false, true);
+ }
+ return super.generateVersionlessPackageUrl(artifact);
+ }
+
+ @Override
+ public String generateVersionlessPackageUrl(org.eclipse.aether.artifact.Artifact artifact) {
+ return generateVersionlessPackageUrl(RepositoryUtils.toArtifact(artifact));
+ }
+
+ @Override
+ public String generateClassifierlessPackageUrl(org.eclipse.aether.artifact.Artifact artifact) {
+ Artifact mavenArtifact = RepositoryUtils.toArtifact(artifact);
+ if (SUPPORTED_TYPES.contains(mavenArtifact.getType())) {
+ return generateP2PackageUrl(mavenArtifact, true, false);
+ }
+ return super.generateClassifierlessPackageUrl(artifact);
+ }
+
+ private String generateP2PackageUrl(Artifact artifact, boolean withVersion, boolean withClassifier) {
+ // TODO Resolve "artifact" and use e.g. TychoProjectManager w/ proper qualifier
+ ArtifactKey artifactKey = new DefaultArtifactKey(artifact.getType(), artifact.getArtifactId(),
+ Versions.toCanonicalVersion(artifact.getVersion()));
+ IArtifactKey p2artifactKey = ArtifactTypeHelper.toP2ArtifactKey(artifactKey);
+ // TODO Try to find p2 location
+ String location = "unknown";
+ if (artifact.getFile() != null) {
+ location = artifact.getFile().toURI().toString();
+ }
+ if (artifact.getDownloadUrl() != null) {
+ location = artifact.getDownloadUrl();
+ }
+ 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();
+ }
+}
diff --git a/tycho-extras/tycho-sbom/src/main/resources/META-INF/plexus/components.xml b/tycho-extras/tycho-sbom/src/main/resources/META-INF/plexus/components.xml
new file mode 100644
index 0000000000..9e4c319a81
--- /dev/null
+++ b/tycho-extras/tycho-sbom/src/main/resources/META-INF/plexus/components.xml
@@ -0,0 +1,9 @@
+
+
+
+ org.cyclonedx.maven.ModelConverter
+ p2
+ org.eclipse.tycho.sbom.P2ModelConverter
+
+
+
\ No newline at end of file
diff --git a/tycho-extras/tycho-sbom/src/test/java/org/eclipse/tycho/sbom/P2ModelConverterTest.java b/tycho-extras/tycho-sbom/src/test/java/org/eclipse/tycho/sbom/P2ModelConverterTest.java
new file mode 100644
index 0000000000..540fda3255
--- /dev/null
+++ b/tycho-extras/tycho-sbom/src/test/java/org/eclipse/tycho/sbom/P2ModelConverterTest.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * Copyright (c) 2023 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 static org.junit.Assert.assertEquals;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.DefaultArtifact;
+import org.cyclonedx.maven.ModelConverter;
+import org.eclipse.tycho.ArtifactType;
+import org.eclipse.tycho.p2maven.repository.EclipsePluginArtifactHandler;
+import org.junit.Before;
+import org.junit.Test;
+
+public class P2ModelConverterTest {
+ private Artifact artifact;
+ private ModelConverter modelConverter;
+
+ @Before
+ public void setUp() {
+ artifact = new DefaultArtifact("p2.eclipse.plugin", "org.eclipse.platform", "4.30.0.v20231201-0110", "compile",
+ ArtifactType.TYPE_ECLIPSE_PLUGIN, null, new EclipsePluginArtifactHandler());
+ artifact.setDownloadUrl(
+ "https://download.eclipse.org/releases/2023-12/202312061001/plugins/org.eclipse.platform_4.30.0.v20231201-0110.jar");
+ modelConverter = new P2ModelConverter();
+ }
+
+ @Test
+ public void testGeneratePackageUrl() {
+ String purl = modelConverter.generatePackageUrl(artifact);
+ String location = URLEncoder.encode(artifact.getDownloadUrl(), StandardCharsets.UTF_8);
+ assertEquals(purl,
+ "pkg:p2/org.eclipse.platform@4.30.0.v20231201-0110?classifier=osgi.bundle&location=" + location);
+ }
+
+ @Test
+ public void testGeneratePackageUrlWithoutVersion() {
+ String purl = modelConverter.generateVersionlessPackageUrl(artifact);
+ String location = URLEncoder.encode(artifact.getDownloadUrl(), StandardCharsets.UTF_8);
+ assertEquals(purl, "pkg:p2/org.eclipse.platform?classifier=osgi.bundle&location=" + location);
+ }
+}