diff --git a/Makefile b/Makefile index e88271afd7..dc74935a7b 100644 --- a/Makefile +++ b/Makefile @@ -168,6 +168,14 @@ client-license-check: client-setup-env ## Run license compliance check @$(ACTIVATE_AND_CD) && pip-licenses @echo "--- License compliance check complete ---" +.PHONY: client-license-check +client-sbom: client-setup-env ## Generate SBOM + @echo "--- Starting SBOM gernation ---" + @$(ACTIVATE_AND_CD) && mkdir -p dist; \ + cyclonedx-py poetry --only main --output-reproducible --validate --output-format JSON --output-file dist/bom.json --verbose; \ + cyclonedx-py poetry --only main --output-reproducible --validate --output-format XML --output-file dist/bom.xml --verbose + @echo "--- SBOM gernation complete ---" + .PHONY: client-build client-build: client-setup-env ## Build client distribution. Pass FORMAT=sdist or FORMAT=wheel to build a specific format. @echo "--- Building client distribution ---" diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index cf06c59f1a..a5384133e4 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -21,6 +21,7 @@ plugins { `kotlin-dsl` } dependencies { implementation(gradleKotlinDsl()) + implementation(baselibs.cyclonedx) implementation(baselibs.errorprone) implementation(baselibs.idea.ext) implementation(baselibs.jandex) diff --git a/build-logic/src/main/kotlin/polaris-sbom-app.gradle.kts b/build-logic/src/main/kotlin/polaris-sbom-app.gradle.kts new file mode 100644 index 0000000000..7af05a0a4f --- /dev/null +++ b/build-logic/src/main/kotlin/polaris-sbom-app.gradle.kts @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.Base64 +import org.cyclonedx.gradle.CyclonedxDirectTask +import org.cyclonedx.model.AttachmentText +import org.cyclonedx.model.Component.Type.APPLICATION +import org.cyclonedx.model.License +import org.cyclonedx.model.LicenseChoice +import org.cyclonedx.model.Property +import sbom.CyclonedxBundleTask +import sbom.createCyclonedxConfigurations + +val cyclonedxApplicationBomTask = + tasks.register("cyclonedxApplicationBom") { + group = "publishing" + description = "Generate CycloneDX SBOMs for this as an application" + + val cyclonedxDirectBom = tasks.named("cyclonedxDirectBom") + inputBoms.from(cyclonedxDirectBom.map { it.jsonOutput }) + + // want to include the original license text in the generated SBOM as it may include variations + // from the "standard" license text + includeLicenseText = true + + projectType = APPLICATION + + // Needed for projects that use subdirectories in their build/ directory, like the Spark plugin + jsonOutput.set(project.layout.buildDirectory.file("reports/cyclonedx-app/bom.json")) + xmlOutput.set(project.layout.buildDirectory.file("reports/cyclonedx-app/bom.xml")) + + val relativeProjectDir = project.projectDir.relativeTo(project.rootProject.projectDir) + val gitInfo = GitInfo.memoized(project) + + licenseChoice.set( + LicenseChoice().apply { + addLicense( + License().apply { + val gitCommit = GitInfo.memoized(project).gitHead + id = "Apache-2.0" + // TODO URL or text ?? + url = gitInfo.rawGithubLink("$relativeProjectDir/distribution/LICENSE") + setLicenseText( + AttachmentText().apply() { + contentType = "plain/text" + encoding = "base64" + text = + Base64.getEncoder() + .encodeToString(project.file("distribution/LICENSE").readBytes()) + } + ) + + // TODO Is there a better way to include NOTICE + DISCLAIMER in a CycloneDX SBOM? + val props = mutableListOf() + props.add( + Property().apply { + name = "NOTICE" + value = + project.file("distribution/NOTICE").readText(Charsets.UTF_8).replace("\n", "\\n") + } + ) + val disclaimerFile = project.file("distribution/DISCLAIMER") + if (disclaimerFile.isFile) { + props.add( + Property().apply { + name = "DISCLAIMER" + value = disclaimerFile.readText(Charsets.UTF_8).replace("\n", "\\n") + } + ) + } + properties = props + } + ) + } + ) + } + +createCyclonedxConfigurations(project, cyclonedxApplicationBomTask) diff --git a/build-logic/src/main/kotlin/polaris-sbom-bundle.gradle.kts b/build-logic/src/main/kotlin/polaris-sbom-bundle.gradle.kts new file mode 100644 index 0000000000..9a4cdf0c0c --- /dev/null +++ b/build-logic/src/main/kotlin/polaris-sbom-bundle.gradle.kts @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.util.Base64 +import org.cyclonedx.model.AttachmentText +import org.cyclonedx.model.License +import org.cyclonedx.model.LicenseChoice +import org.cyclonedx.model.Property +import publishing.digestTaskOutputs +import publishing.signTaskOutputs +import sbom.CyclonedxBundleTask +import sbom.createCyclonedxConfigurations + +plugins { id("org.cyclonedx.bom") } + +val bundleSboms by + configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true + } + +val cyclonedxBundleBom = tasks.register("cyclonedxBundleBom") + +cyclonedxBundleBom.configure { + inputBoms = bundleSboms + // The distribution itself has no dependencies, just components + includeDependencies = false + + val relativeProjectDir = project.projectDir.relativeTo(project.rootProject.projectDir) + val gitInfo = GitInfo.memoized(project) + + licenseChoice.set( + LicenseChoice().apply { + addLicense( + License().apply { + id = "Apache-2.0" + // TODO URL or text ?? + url = gitInfo.rawGithubLink("$relativeProjectDir/LICENSE") + setLicenseText( + AttachmentText().apply() { + contentType = "plain/text" + encoding = "base64" + text = Base64.getEncoder().encodeToString(project.file("LICENSE").readBytes()) + } + ) + + // TODO Is there a better way to include NOTICE + DISCLAIMER in a CycloneDX SBOM? + val props = mutableListOf() + props.add( + Property().apply { + name = "NOTICE" + value = project.file("NOTICE").readText(Charsets.UTF_8).replace("\n", "\\n") + } + ) + val disclaimerFile = project.file("DISCLAIMER") + if (disclaimerFile.isFile) { + props.add( + Property().apply { + name = "DISCLAIMER" + value = disclaimerFile.readText(Charsets.UTF_8).replace("\n", "\\n") + } + ) + } + properties = props + } + ) + } + ) +} + +createCyclonedxConfigurations(project, cyclonedxBundleBom) + +tasks.named("assemble") { dependsOn(cyclonedxBundleBom) } + +digestTaskOutputs(cyclonedxBundleBom) + +signTaskOutputs(cyclonedxBundleBom) diff --git a/build-logic/src/main/kotlin/publishing/PublishingHelperPlugin.kt b/build-logic/src/main/kotlin/publishing/PublishingHelperPlugin.kt index bafc4cc959..1906f96e01 100644 --- a/build-logic/src/main/kotlin/publishing/PublishingHelperPlugin.kt +++ b/build-logic/src/main/kotlin/publishing/PublishingHelperPlugin.kt @@ -38,6 +38,7 @@ import org.gradle.kotlin.dsl.registering import org.gradle.kotlin.dsl.withType import org.gradle.plugins.signing.SigningExtension import org.gradle.plugins.signing.SigningPlugin +import sbom.configureCycloneDx /** * Release-publishing helper plugin to generate publications that pass Sonatype validations, @@ -94,6 +95,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl apply(plugin = "maven-publish") apply(plugin = "signing") + apply(plugin = "org.cyclonedx.bom") // Generate a source tarball for a release to be uploaded to // https://dist.apache.org/repos/dist/dev//apache--/ @@ -175,5 +177,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl } } } + + configureCycloneDx() } } diff --git a/build-logic/src/main/kotlin/sbom/cyclonedx.kt b/build-logic/src/main/kotlin/sbom/cyclonedx.kt new file mode 100644 index 0000000000..2df337e639 --- /dev/null +++ b/build-logic/src/main/kotlin/sbom/cyclonedx.kt @@ -0,0 +1,323 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package sbom + +import java.util.UUID +import java.util.function.Consumer +import javax.inject.Inject +import org.cyclonedx.gradle.BaseCyclonedxTask +import org.cyclonedx.gradle.CyclonedxAggregateTask +import org.cyclonedx.gradle.CyclonedxDirectTask +import org.cyclonedx.gradle.CyclonedxPlugin +import org.cyclonedx.gradle.utils.CyclonedxUtils +import org.cyclonedx.model.* +import org.cyclonedx.model.Component.Type.APPLICATION +import org.cyclonedx.parsers.BomParserFactory +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.model.ObjectFactory +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.named +import publishing.EffectiveAsfProject + +/** + * Configures [CycloneDX plugin tasks](https://github.com/CycloneDX/cyclonedx-gradle-plugin) for the + * Apache Polaris project. + * + * The `cyclonedxBom` task, only available on the root Gradle project, generates an aggregated BOM + * for the project and all its modules. + * + * The `cyclonedxDirectBom` task generates the SBOM for the projects' main artifacts. The SBOM for + * each published Maven artifact uses the classifier `cyclonedx` and is published as JSON and XML. + */ +fun Project.configureCycloneDx() { + tasks.withType(BaseCyclonedxTask::class.java).configureEach { configureAllForPolaris() } + + val cyclonedxBom = tasks.named("cyclonedxBom") + cyclonedxBom.configure { + if (project != rootProject) { + // Disable aggregation for subprojects + enabled = false + } + group = "publishing" + description = "Aggregate CycloneDX SBOMs from all subprojects" + } + + val cyclonedxDirectBom = tasks.named("cyclonedxDirectBom") + cyclonedxDirectBom.configure { + includeConfigs.addAll("runtimeClasspath") + group = "publishing" + description = "Generate CycloneDX SBOMs for this as a library" + } + + createCyclonedxConfigurations(this, cyclonedxBom) + createCyclonedxConfigurations(this, cyclonedxDirectBom) + + extensions.getByType(PublishingExtension::class.java).publications { + named("maven") { + artifact(cyclonedxDirectBom.map { t -> t.jsonOutput }) { + classifier = "cyclonedx" + builtBy(cyclonedxDirectBom) + } + artifact(cyclonedxDirectBom.map { t -> t.xmlOutput }) { + classifier = "cyclonedx" + builtBy(cyclonedxDirectBom) + } + } + } +} + +fun createCyclonedxConfigurations( + project: Project, + cdxTask: TaskProvider, +) { + val baseName = cdxTask.name + val cfgAll = project.configurations.create("${baseName}All") + val cfgJson = project.configurations.create("${baseName}Json") + val cfgXml = project.configurations.create("${baseName}Xml") + project.artifacts.add(cfgAll.name, cdxTask.map { t -> t.jsonOutput }) { builtBy(cdxTask) } + project.artifacts.add(cfgAll.name, cdxTask.map { t -> t.xmlOutput }) { builtBy(cdxTask) } + project.artifacts.add(cfgJson.name, cdxTask.map { t -> t.jsonOutput }) { builtBy(cdxTask) } + project.artifacts.add(cfgXml.name, cdxTask.map { t -> t.xmlOutput }) { builtBy(cdxTask) } + for (c in listOf(cfgAll, cfgJson, cfgXml)) { + c.isCanBeConsumed = true + c.isCanBeResolved = true + } +} + +internal fun BaseCyclonedxTask.configureAllForPolaris() { + // Needed for projects that use subdirectories in their build/ directory, like the Spark plugin + jsonOutput.set(project.layout.buildDirectory.file("reports/cyclonedx/bom.json")) + xmlOutput.set(project.layout.buildDirectory.file("reports/cyclonedx/bom.xml")) + + val prj = EffectiveAsfProject.forProject(project) + val gitInfo = GitInfo.memoized(project) + + organizationalEntity.set( + OrganizationalEntity().apply { + name = prj.fullName().get() + urls = listOf(prj.projectUrl().get()) + } + ) + externalReferences.add( + ExternalReference().apply { + type = ExternalReference.Type.VCS + url = prj.codeRepoUrl().get() + ".git" + } + ) + externalReferences.add( + ExternalReference().apply { + type = ExternalReference.Type.WEBSITE + url = prj.projectUrl().get() + } + ) + externalReferences.add( + ExternalReference().apply { + type = ExternalReference.Type.ISSUE_TRACKER + url = prj.issueTracker().get() + } + ) + externalReferences.add( + ExternalReference().apply { + type = ExternalReference.Type.MAILING_LIST + url = prj.mailingList("dev").archive() + } + ) + externalReferences.add( + ExternalReference().apply { + type = ExternalReference.Type.SECURITY_CONTACT + url = "mailto:security@apache.org" + } + ) + licenseChoice.convention( + LicenseChoice().apply { + addLicense( + License().apply { + id = "Apache-2.0" + url = gitInfo.rawGithubLink("LICENSE") + } + ) + } + ) +} + +@CacheableTask +abstract class CyclonedxBundleTask @Inject constructor(objectFactory: ObjectFactory) : + BaseCyclonedxTask() { + init { + group = "publishing" + description = "Generate CycloneDX SBOM for the distribution bundle" + + jsonOutput.set(project.layout.buildDirectory.file("reports/cyclonedx-bundle/bom.json")) + xmlOutput.set(project.layout.buildDirectory.file("reports/cyclonedx-bundle/bom.xml")) + } + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val inputBoms: ConfigurableFileCollection + + @get:Input val includeDependencies = objectFactory.property(Boolean::class.java).convention(true) + + @TaskAction + fun aggregate() { + val bom: Bom = buildBom() + logger.info("{} Writing BOM", CyclonedxPlugin.LOG_PREFIX) + if (jsonOutput.isPresent) { + CyclonedxUtils.writeJsonBom(schemaVersion.get(), bom, jsonOutput.asFile.get()) + } + if (xmlOutput.isPresent) { + CyclonedxUtils.writeXmlBom(schemaVersion.get(), bom, xmlOutput.asFile.get()) + } + } + + private fun buildBom(): Bom { + val rootComponent = buildRootComponent() + + val seenBomRefs = mutableSetOf() + val allDependencies = mutableMapOf() + + val bom = Bom() + bom.metadata = buildMetadata(rootComponent) + + logger.info("{} Reading BOMs", CyclonedxPlugin.LOG_PREFIX) + inputBoms + .map { file -> + logger.info("{} Reading BOM from {}", CyclonedxPlugin.LOG_PREFIX, file) + BomParserFactory.createParser(file).parse(file) + } + .forEach { + val metadata = it.metadata + // Add a component from the source SBOM's root component, with metadata information + val appComponent = createComponent(metadata) + if (bom.components == null) { + bom.addComponent(appComponent) + } else { + // This is to keep the included application components at the beginning, no technical + // reason for this + bom.components.add(0, appComponent) + } + // Add missing components + it.components?.forEach { c -> + if (seenBomRefs.add(c.bomRef)) { + bom.addComponent(c) + } + } + // Add dependencies + it.dependencies?.forEach { d -> allDependencies[d.ref] = d } + } + + if (includeBomSerialNumber.get()) { + bom.serialNumber = "urn:uuid:${UUID.randomUUID()}" + } + if (includeDependencies.get()) { + bom.dependencies = allDependencies.values.toMutableList() + } + return bom + } + + private fun buildMetadata(rootComponent: Component): Metadata { + val metadata = Metadata() + if (licenseChoice.isPresent) { + metadata.licenses = licenseChoice.get() + } + if (organizationalEntity.isPresent && (OrganizationalEntity() != organizationalEntity.get())) { + metadata.manufacturer = organizationalEntity.get() + } + metadata.component = rootComponent + return metadata + } + + private fun buildRootComponent(): Component { + val t = this + return Component().apply { + type = APPLICATION + name = t.componentName.get() + version = t.componentVersion.get() + if (t.externalReferences.isPresent) { + t.externalReferences + .get() + .forEach( + Consumer { externalReference: ExternalReference -> + addExternalReference(externalReference) + } + ) + } + } + } + + private fun createComponent(metadata: Metadata): Component = + Component().apply { + val src = metadata.component + this.bomRef = src.bomRef + this.mimeType = src.mimeType + this.type = src.type + this.supplier = src.supplier + this.author = src.author + this.publisher = src.publisher + this.group = src.group + this.name = src.name + this.version = src.version + this.description = src.description + this.scope = src.scope + this.hashes = src.hashes + this.licenses = src.licenses + this.copyright = src.copyright + this.cpe = src.cpe + this.purl = src.purl + this.omniborId = src.omniborId + this.swhid = src.swhid + this.swid = src.swid + this.modified = src.modified + this.pedigree = src.pedigree + this.externalReferences = src.externalReferences + this.properties = src.properties + this.components = src.components + this.evidence = src.evidence + this.releaseNotes = src.releaseNotes + this.modelCard = src.modelCard + this.data = src.data + this.cryptoProperties = src.cryptoProperties + this.provides = src.provides + this.tags = src.tags + this.authors = src.authors + this.manufacturer = src.manufacturer + this.signature = src.signature + cloneExtElem(src, this) + + this.licenses = metadata.licenses + this.manufacturer = metadata.manufacturer + metadata.properties?.forEach { p -> this.addProperty(p) } + this.authors = metadata.authors + this.supplier = metadata.supplier + } + + private fun cloneExtElem(src: ExtensibleElement, dst: ExtensibleElement) { + src.extensions?.forEach { (key, ext) -> dst.add(key, cloneExtension(ext)) } + src.extensibleTypes?.forEach { e -> dst.extensibleTypes.add(e) } + } + + private fun cloneExtension(src: Extension): Extension = + Extension(src.extensionType, src.extensions).apply { + this.prefix = src.prefix + this.namespaceURI = src.namespaceURI + } +} diff --git a/client/python/pyproject.toml b/client/python/pyproject.toml index d5f7011899..94ad7ad199 100644 --- a/client/python/pyproject.toml +++ b/client/python/pyproject.toml @@ -29,13 +29,6 @@ requires-python = ">=3.10,<4.0" license = "Apache-2.0" keywords = ["Apache Polaris", "Polaris", "Polaris Management Service", "Apache Iceberg REST Catalog API"] dynamic = ["classifiers"] -dependencies = [ - "urllib3>=1.25.3,<3.0.0", - "python-dateutil>=2.8.2", - "pydantic>=2.0.0,<2.12.0", - "typing-extensions>=4.7.1", - "boto3~=1.40.6", -] [project.urls] homepage = "https://polaris.apache.org/" @@ -45,6 +38,13 @@ repository = "https://github.com/apache/polaris/" polaris = "cli.polaris_cli:main" [tool.poetry] +name = "polaris" +version = "1.2.0" +license = "Apache-2.0" +description = "Apache Polaris" +urls.homepage = "https://polaris.apache.org/" +urls.repository = "https://github.com/apache/polaris/" + requires-poetry = "==2.2.1" include = [ { path = "polaris/**", format = ["sdist", "wheel"] }, @@ -53,6 +53,13 @@ include = [ { path = "spec/**", format = "sdist" }, ] +[tool.poetry.group.main.dependencies] +urllib3 = ">=1.25.3,<3.0.0" +python-dateutil = ">=2.8.2" +pydantic = ">=2.0.0,<2.12.0" +typing-extensions = ">=4.7.1" +boto3 = "~=1.40.6" + [tool.poetry.group.dev.dependencies] pytest = ">= 7.2.1" pytest-cov = ">= 2.8.1" @@ -63,6 +70,7 @@ pyiceberg = "==0.10.0" pre-commit = "==4.3.0" openapi-generator-cli = "==7.17.0" pip-licenses-cli = "==3.0.0" +cyclonedx-bom = "==7.2.0" [tool.pip-licenses] partial-match = true diff --git a/gradle/baselibs.versions.toml b/gradle/baselibs.versions.toml index a34de8a51e..05451f407f 100644 --- a/gradle/baselibs.versions.toml +++ b/gradle/baselibs.versions.toml @@ -18,6 +18,7 @@ # [libraries] +cyclonedx = { module = "org.cyclonedx:cyclonedx-gradle-plugin", version = "3.0.1" } errorprone = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version = "4.3.0" } idea-ext = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version = "1.3" } jandex = { module = "org.kordamp.gradle:jandex-gradle-plugin", version = "2.3.0" } diff --git a/runtime/admin/build.gradle.kts b/runtime/admin/build.gradle.kts index 103da5c79f..9a2e7f8b2c 100644 --- a/runtime/admin/build.gradle.kts +++ b/runtime/admin/build.gradle.kts @@ -21,6 +21,7 @@ plugins { alias(libs.plugins.quarkus) id("org.kordamp.gradle.jandex") id("polaris-runtime") + id("polaris-sbom-app") // id("polaris-license-report") } diff --git a/runtime/distribution/build.gradle.kts b/runtime/distribution/build.gradle.kts index 75ddcf7b4c..8433f43ec7 100644 --- a/runtime/distribution/build.gradle.kts +++ b/runtime/distribution/build.gradle.kts @@ -17,15 +17,18 @@ * under the License. */ +import org.gradle.kotlin.dsl.invoke import publishing.PublishingHelperPlugin import publishing.digestTaskOutputs import publishing.signTaskOutputs +import sbom.CyclonedxBundleTask plugins { id("distribution") id("signing") id("polaris-spotless") id("polaris-reproducible") + id("polaris-sbom-bundle") } description = "Apache Polaris Binary Distribution" @@ -51,6 +54,8 @@ val serverDistribution by dependencies { adminDistribution(project(":polaris-admin", "distributionElements")) serverDistribution(project(":polaris-server", "distributionElements")) + bundleSboms(project(":polaris-admin", "cyclonedxDirectBomJson")) + bundleSboms(project(":polaris-server", "cyclonedxDirectBomJson")) } distributions { @@ -88,3 +93,14 @@ digestTaskOutputs(distZip) signTaskOutputs(distTar) signTaskOutputs(distZip) + +tasks.named("cyclonedxBundleBom") { + val baseName = distributions.main.get().distributionBaseName.get() + jsonOutput.set( + project.layout.buildDirectory.file("distributions/$baseName-$version.cyclonedx.json") + ) + xmlOutput.set( + project.layout.buildDirectory.file("distributions/$baseName-$version.cyclonedx.xml") + ) + // Note: the polaris-sbom-bundle build plugin sets up signing. +} diff --git a/runtime/server/build.gradle.kts b/runtime/server/build.gradle.kts index dc2119983e..dce25ddf8d 100644 --- a/runtime/server/build.gradle.kts +++ b/runtime/server/build.gradle.kts @@ -24,6 +24,7 @@ plugins { alias(libs.plugins.quarkus) id("org.kordamp.gradle.jandex") id("polaris-runtime") + id("polaris-sbom-app") // id("polaris-license-report") }