From ddd5af48ad3ae8f455105b57762ada785d03cf8d Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sat, 1 Aug 2026 11:25:05 +0200 Subject: [PATCH] SOLR-17328: Add CycloneDX SBOMs to Solr binary distributions Each binary distribution (full and slim) now ships a CycloneDX 1.6 bom.json describing its actual contents. Two resolvable configurations in :solr:packaging (bomFull, bomSlim) mirror the distribution assembly and are rendered by the CycloneDX Gradle plugin, then a post-processing step adjusts the result: * The metadata declares the "build" lifecycle phase (a CISA Build SBOM) and lists the post-processing and cyclonedx-npm next to the plugin in the tools. * The main component identifies the binary release with a draft "sid" purl (purl-spec issue #516, pkg:sid/apache.org/solr/solr@ with an edition qualifier) and the Solr CPE used by the NVD. * Maven BOM/platform dependencies and the internal :platform project, which are not part of the distribution, are stripped. * Solr project components get valid Maven purls (artifactId instead of the Gradle project name, with the ASF snapshots repository for snapshot builds), a description and the Apache-2.0 license. * The UI artifacts inside the webapp are covered: the npm packages bundled by browserify into the OpenAPI JS client and the Maven artifacts compiled into the wasmJs UI are nested as subassemblies of first-party solr-js-client and solr-ui components, from child SBOMs generated in :solr:webapp:js-client (official cyclonedx-npm tool, runtime dependencies only) and :solr:ui (plugin task over wasmJsRuntimeClasspath). The vendored JavaScript libraries of the AngularJS admin UI are listed from a curated list. * The location of every JAR and JavaScript file in the distribution is recorded as evidence.occurrences by matching SHA-256 hashes against the assembled directories, which also proves the vendored libraries ship unmodified. * Only the SHA-256 hash of each component is kept; the plugin emits eight algorithms per artifact, which only adds bulk. The BOM configurations resolve strictly with the JVM runtime attributes (the packaging project applies jvm-ecosystem for the attribute schema), so BOM-managed and variant-aware dependencies resolve like a runtime classpath instead of silently disappearing. bomFull resolves consistently with bomSlim, matching the distribution layout where server libraries win over module-pulled versions. The child SBOMs degrade gracefully when the UI projects are disabled with -PdisableJsClient / -PdisableUiModule. Assisted-By: Claude Fable 5 --- build.gradle | 1 + .../unreleased/cyclonedx-sboms-SOLR-17328.yml | 12 + gradle/libs.versions.toml | 4 + solr/packaging/build.gradle | 502 +++++++++++++++++- solr/packaging/gradle.lockfile | 331 +++++++++++- solr/server/build.gradle | 35 +- solr/ui/build.gradle.kts | 34 ++ solr/webapp/js-client/build.gradle.kts | 76 ++- 8 files changed, 987 insertions(+), 8 deletions(-) create mode 100644 changelog/unreleased/cyclonedx-sboms-SOLR-17328.yml diff --git a/build.gradle b/build.gradle index 80c97fe1a0f0..e8c107888470 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ plugins { alias(libs.plugins.diffplug.spotless) apply false alias(libs.plugins.nodegradle.node) apply false alias(libs.plugins.openapi.generator) apply false + alias(libs.plugins.cyclonedx) apply false alias(libs.plugins.logchange) } diff --git a/changelog/unreleased/cyclonedx-sboms-SOLR-17328.yml b/changelog/unreleased/cyclonedx-sboms-SOLR-17328.yml new file mode 100644 index 000000000000..ed288fec8f89 --- /dev/null +++ b/changelog/unreleased/cyclonedx-sboms-SOLR-17328.yml @@ -0,0 +1,12 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc + +title: > + Ship a CycloneDX SBOM (bom.json) in the root of the full and slim binary distributions, + covering the Java libraries, the Solr artifacts and the UI content of the webapp +type: added +authors: + - name: Piotr P. Karwasz + nick: ppkarwasz +links: + - name: SOLR-17328 + url: https://issues.apache.org/jira/browse/SOLR-17328 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe7d4d6d756a..7fae6881d6e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -79,6 +79,9 @@ commons-io = "2.22.0" compose = "1.11.1" cuvs-java = "26.06.0" cuvs-lucene = "25.12.0" +cyclonedx = "3.0.2" +# @keep npm tool generating the SBOM of the OpenAPI JS client, installed by :solr:webapp:js-client +cyclonedx-npm = "6.0.0" decompose = "3.5.0" diffplug-spotless = "8.7.0" # @keep Use for dockerfile JRE version @@ -203,6 +206,7 @@ xerial-snappy = "1.1.10.8" [plugins] benmanes-versions = { id = "com.github.ben-manes.versions", version.ref = "benmanes-versions" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +cyclonedx = { id = "org.cyclonedx.bom", version.ref = "cyclonedx" } diffplug-spotless = { id = "com.diffplug.spotless", version.ref = "diffplug-spotless" } jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/solr/packaging/build.gradle b/solr/packaging/build.gradle index 63c467388ef1..3ac372418772 100644 --- a/solr/packaging/build.gradle +++ b/solr/packaging/build.gradle @@ -15,8 +15,13 @@ * limitations under the License. */ +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import java.security.MessageDigest import org.apache.tools.ant.filters.ReplaceTokens import org.apache.tools.ant.util.TeeOutputStream +import org.cyclonedx.gradle.CyclonedxDirectTask +import org.cyclonedx.model.Component // This project puts together a "distribution", assembling dependencies from // various other projects. @@ -24,11 +29,424 @@ import org.apache.tools.ant.util.TeeOutputStream plugins { id 'base' id 'distribution' + // Registers the JVM attribute schema, so that the variant-aware resolution of + // the CycloneDX BOM configurations below works like a Java runtime classpath. + id 'jvm-ecosystem' +} + +final APACHE_SNAPSHOTS_QUALIFIER = '&repository_url=https:%2F%2Frepository.apache.org%2Fcontent%2Fgroups%2Fsnapshots%2F' + +// Post-processing of the SBOMs generated by the CycloneDX plugin: +// +// 1. Metadata: +// The "build" lifecycle phase is recorded (this is a "Build SBOM" in CISA's classification) +// and this post-processing step is listed in the tools, next to the CycloneDX plugin. +// 2. Main component: +// The Solr binary archive published on downloads.apache.org is identified by a "sid" purl +// (a draft purl type for software distributed outside package registries, +// see https://github.com/package-url/purl-spec/issues/516) and by the Solr CPE. +// 3. Removed components: +// Maven BOM/platform dependencies (purl qualifier "type=pom") and the internal ":platform" +// project are stripped: they are not part of the binary distribution and the plugin has no +// option to exclude them. +// 4. Solr components: +// The plugin emits invalid purls for Solr projects, built from the Gradle project name and a +// non-standard "project_path" qualifier (e.g. pkg:maven/org.apache.solr/core@11.0.0?project_path=:solr:core). +// They are replaced with the Maven artifactId (base.archivesName) and a "type=jar" qualifier +// (e.g. pkg:maven/org.apache.solr/solr-core@11.0.0?type=jar). +// The project description and the Apache-2.0 license are also added. +// 5. Vendored JavaScript libraries: +// The AngularJS admin UI ships third-party JavaScript files checked into solr/webapp/web/libs. +// Components for them are added from a curated list (version and license read from the file +// headers); the hash matching of step 8 proves that each file still ships unmodified. +// 6. JavaScript client bundle: +// The npm packages bundled by browserify into the OpenAPI JS client +// (server/solr-webapp/webapp/libs/solr/index.js) are nested as subassemblies of a first-party +// "solr-js-client" component, using the SBOM generated by cyclonedx-npm in :solr:webapp:js-client. +// 7. New UI bundle: +// The Maven dependencies compiled into the wasmJs UI (server/solr-webapp/webapp/ui) are nested +// as subassemblies of a first-party "solr-ui" component, using the SBOM generated in :solr:ui, +// together with the npm package bundled by the Kotlin toolchain (see kotlin-js-store/wasm/yarn.lock). +// 8. Archive locations: +// The location of each JAR and JavaScript file within the distribution is recorded as +// "evidence.occurrences": the directories assembled for the distribution are scanned and +// their files are matched to the components by SHA-256 hash. +// 9. Hashes: +// Only the SHA-256 hash of each component is kept: the plugin emits eight algorithms +// per artifact, which only adds bulk. +def postProcessBom = { File bomFile, String edition, Map scanDirs, File jsClientSbomFile, File uiSbomFile -> + def json = new JsonSlurper().parse(bomFile) + + def sha256Of = { File file -> + def digest = MessageDigest.getInstance('SHA-256') + file.eachByte(8192) { buffer, length -> digest.update(buffer, 0, length) } + digest.digest().encodeHex().toString() + } + + // Copies a child SBOM's dependency graph into this one: the child root is replaced + // by the given bundle ref, dropped refs are skipped and entries for refs that + // already exist (the same artifact in both graphs) are merged. + def mergeChildGraph = { List childDeps, String childRootRef, String bundleRef, Set droppedRefs -> + childDeps.each { dep -> + if (dep.ref in droppedRefs) { + return + } + def ref = dep.ref == childRootRef ? bundleRef : dep.ref + def dependsOn = (dep.dependsOn ?: []).findAll { !(it in droppedRefs) } + .collect { it == childRootRef ? bundleRef : it } + def existing = json.dependencies.find { it.ref == ref } + if (existing == null) { + json.dependencies << [ref: ref, dependsOn: dependsOn] + } else { + existing.dependsOn = ((existing.dependsOn ?: []) + dependsOn).unique() + } + } + } + + // 1. Metadata + json.metadata.lifecycles = [[phase: 'build']] + + // Record this post-processing step next to the CycloneDX plugin + if (json.metadata.tools == null) { + json.metadata.tools = [components: []] + } + json.metadata.tools.components << [ + type: 'application', + author: 'The Apache Software Foundation', + name: 'solr-sbom-post-processing', + version: project.version, + description: 'Post-processing of the generated SBOM by the Solr Gradle build (:solr:packaging)', + ] + + // 2. Main component + // Old bom-ref -> new purl, applied to the dependency graph below + def rewrittenRefs = [:] + + Map mainComponent = json.metadata.component + def mainPurl = "pkg:sid/apache.org/solr/solr@${mainComponent.version}?edition=${edition}".toString() + rewrittenRefs[mainComponent.'bom-ref'] = mainPurl + mainComponent.remove('group') + mainComponent.name = 'Apache Solr binary release' + mainComponent.cpe = "cpe:2.3:a:apache:solr:${mainComponent.version}:*:*:*:*:*:*:*".toString() + mainComponent.purl = mainPurl + mainComponent.'bom-ref' = mainPurl + + // Gradle project path encoded in the purls the plugin generates for Solr projects + def projectPathOf = { purl -> + def matcher = purl =~ /[?&]project_path=([^&]+)(&|$)/ + matcher ? URLDecoder.decode(matcher.group(1), 'UTF-8') : null + } + + // 3. Removed components + def removedRefs = json.components.findAll { + it.purl =~ /[?&]type=pom(&|$)/ || projectPathOf(it.purl) == ':platform' + }.collect { it.'bom-ref' } as Set + + // 4. Solr components + Map artifactIdByPath = rootProject.allprojects.collectEntries { + [(it.path): it.base.archivesName.get()] + } + json.components = json.components.collect { Map component -> + def projectPath = projectPathOf(component.purl) + // Skip external components and those about to be removed + if (projectPath == null || component.'bom-ref' in removedRefs) { + return component + } + def artifactId = artifactIdByPath[projectPath] + def repositoryUrlQualifier = component.version.endsWith("-SNAPSHOT") ? APACHE_SNAPSHOTS_QUALIFIER : '' + def purl = "pkg:maven/${component.group}/${artifactId}@${component.version}?type=jar${repositoryUrlQualifier}".toString() + rewrittenRefs[component.'bom-ref'] = purl + component.name = artifactId + component.purl = purl + component.'bom-ref' = purl + component.description = rootProject.project(projectPath).description + component.licenses = [[license: [id: 'Apache-2.0', url: 'https://www.apache.org/licenses/LICENSE-2.0']]] + // Restore the field order the plugin uses for external components + def ordered = [:] + ['type', 'bom-ref', 'group', 'name', 'version', 'description', 'hashes', + 'licenses', 'purl', 'modified', 'properties'].each { key -> + if (component.containsKey(key)) { + ordered[key] = component[key] + } + } + component.forEach { key, value -> + if (!ordered.containsKey(key)) { + ordered[key] = value + } + } + return ordered + } + + // Apply the removals (3) and the ref rewrites (2, 4) to the dependency graph + json.components.removeAll { it.'bom-ref' in removedRefs } + json.dependencies?.removeAll { it.ref in removedRefs } + json.dependencies?.each { dep -> + dep.ref = rewrittenRefs.getOrDefault(dep.ref, dep.ref) + dep.dependsOn?.removeAll { it in removedRefs } + if (dep.dependsOn != null) { + dep.dependsOn = dep.dependsOn.collect { rewrittenRefs.getOrDefault(it, it) } + } + } + + // The UI artifacts of steps 5 to 7 all ship inside the webapp + def webappRef = json.components.find { + it.purl?.startsWith('pkg:maven/org.apache.solr/solr-webapp@') + }?.'bom-ref' ?: mainComponent.'bom-ref' + def webappDependsOn = json.dependencies.find { it.ref == webappRef }.dependsOn + + // 5. Vendored JavaScript libraries + // Entries without a version marker in the file get no version and no purl + def vendoredJsLibs = [ + [file: 'angular.min.js', name: 'angular', version: '1.8.0', license: 'MIT'], + [file: 'angular-chosen.min.js', name: 'angular-chosen-localytics', version: '1.9.2', license: 'MIT'], + [file: 'angular-cookies.min.js', name: 'angular-cookies', version: '1.8.0', license: 'MIT'], + [file: 'angular-resource.min.js', name: 'angular-resource', version: '1.8.0', license: 'MIT'], + [file: 'angular-route.min.js', name: 'angular-route', version: '1.8.0', license: 'MIT'], + [file: 'angular-sanitize.min.js', name: 'angular-sanitize', version: '1.8.0', license: 'MIT'], + [file: 'angular-utf8-base64.min.js', name: 'angular-utf8-base64', license: 'MIT'], + [file: 'chosen.jquery.min.js', name: 'chosen-js', version: '1.8.7', license: 'MIT'], + [file: 'd3.js', name: 'd3', version: '2.8.1', license: 'BSD-3-Clause'], + [file: 'highlight.js', name: 'highlight.js', license: 'BSD-3-Clause'], + [file: 'jquery-3.5.1.min.js', name: 'jquery', version: '3.5.1', license: 'MIT'], + [file: 'jquery-ui.min.js', name: 'jquery-ui', version: '1.12.1', license: 'MIT'], + [file: 'jssha-3.3.1-sha256.min.js', name: 'jssha', version: '3.3.1', license: 'BSD-3-Clause'], + [file: 'jstree.min.js', name: 'jstree', version: '3.3.10', license: 'MIT'], + [file: 'ngtimeago.js', name: 'ngtimeago', license: 'MIT'], + [file: 'ui-grid.min.js', name: 'angular-ui-grid', version: '4.10.0', license: 'MIT'], + ] + def webLibsDir = rootProject.file('solr/webapp/web/libs') + vendoredJsLibs.each { lib -> + def purl = lib.version != null ? "pkg:npm/${lib.name}@${lib.version}".toString() : null + def component = [ + type: 'library', + 'bom-ref': purl ?: "vendored-js:${lib.name}".toString(), + name: lib.name, + ] + if (lib.version != null) { + component.version = lib.version + } + component.hashes = [[alg: 'SHA-256', content: sha256Of(new File(webLibsDir, lib.file))]] + component.licenses = [[license: [id: lib.license]]] + if (purl != null) { + component.purl = purl + } + json.components << component + json.dependencies << [ref: component.'bom-ref', dependsOn: []] + webappDependsOn << component.'bom-ref' + } + + // 6. JavaScript client bundle + if (jsClientSbomFile != null) { + def jsClientBom = new JsonSlurper().parse(jsClientSbomFile) + def bundleRef = "solr-js-client@${project.version}".toString() + json.components << [ + type: 'library', + 'bom-ref': bundleRef, + name: 'solr-js-client', + version: project.version.toString(), + description: 'JavaScript client for the Solr v2 API, generated from its OpenAPI specification', + licenses: [[license: [id: 'Apache-2.0', url: 'https://www.apache.org/licenses/LICENSE-2.0']]], + // The npm packages bundled into the single shipped file by browserify + components: jsClientBom.components, + evidence: [occurrences: [[location: 'server/solr-webapp/webapp/libs/solr/index.js']]], + ] + mergeChildGraph(jsClientBom.dependencies ?: [], jsClientBom.metadata.component.'bom-ref', bundleRef, [] as Set) + webappDependsOn << bundleRef + // Record cyclonedx-npm next to the other tools + jsClientBom.metadata?.tools?.components?.each { tool -> + if (!json.metadata.tools.components.any { it.name == tool.name && it.version == tool.version }) { + json.metadata.tools.components << tool + } + } + } + + // 7. New UI bundle + def uiBundle = null + if (uiSbomFile != null) { + def uiBom = new JsonSlurper().parse(uiSbomFile) + // The ":platform" project and Maven BOMs are on the UI classpath too, see step 3 + def uiDroppedRefs = uiBom.components.findAll { + it.purl =~ /[?&]type=pom(&|$)/ || projectPathOf(it.purl) == ':platform' + }.collect { it.'bom-ref' } as Set + // An artifact present in the Java graph too keeps its top-level component only + def existingRefs = json.components.collect { it.'bom-ref' } as Set + def nested = uiBom.components.findAll { + !(it.'bom-ref' in uiDroppedRefs) && !(it.'bom-ref' in existingRefs) + } + // Bundled by the Kotlin toolchain, see kotlin-js-store/wasm/yarn.lock + nested << [ + type: 'library', + 'bom-ref': 'pkg:npm/%40js-joda/core@3.2.0', + name: '@js-joda/core', + version: '3.2.0', + licenses: [[license: [id: 'BSD-3-Clause']]], + purl: 'pkg:npm/%40js-joda/core@3.2.0', + ] + def bundleRef = "solr-ui@${project.version}".toString() + uiBundle = [ + type: 'library', + 'bom-ref': bundleRef, + name: 'solr-ui', + version: project.version.toString(), + description: 'New Solr admin UI, compiled to WebAssembly', + licenses: [[license: [id: 'Apache-2.0', url: 'https://www.apache.org/licenses/LICENSE-2.0']]], + // The Maven artifacts compiled into the bundle and the npm package above + components: nested, + // Occurrences are filled by the scan of step 8, the bundle file names are content-hashed + ] + json.components << uiBundle + mergeChildGraph(uiBom.dependencies ?: [], uiBom.metadata.component.'bom-ref', bundleRef, uiDroppedRefs) + def uiEntry = json.dependencies.find { it.ref == bundleRef } + uiEntry.dependsOn << 'pkg:npm/%40js-joda/core@3.2.0' + json.dependencies << [ref: 'pkg:npm/%40js-joda/core@3.2.0', dependsOn: []] + webappDependsOn << bundleRef + } + + // 8. Archive locations + // Distribution files indexed by SHA-256 hash; UI bundle files collected on the way + def locationsByHash = [:].withDefault { [] } + def uiLocations = [] + scanDirs.each { prefix, configuration -> + configuration.files.each { root -> + fileTree(root).matching { + include '**/*.jar' + include '**/*.js' + include 'solr-webapp/webapp/ui/**' + }.visit { entry -> + if (!entry.directory) { + def location = "${prefix}/${entry.relativePath}".toString() + locationsByHash[sha256Of(entry.file)] << location + if (entry.relativePath.pathString.startsWith('solr-webapp/webapp/ui/')) { + uiLocations << location + } + } + } + } + } + json.components.each { component -> + def sha256 = component.hashes?.find { it.alg == 'SHA-256' }?.content + if (sha256 != null && locationsByHash.containsKey(sha256)) { + component.evidence = [occurrences: locationsByHash[sha256].sort().collect { [location: it] }] + } + } + if (uiBundle != null) { + uiBundle.evidence = [occurrences: uiLocations.sort().collect { [location: it] }] + } + + // 9. Hashes + // The npm integrity hashes on "externalReferences" are kept, they describe + // the registry tarballs and have no SHA-256 equivalent. + def keepSha256Only + keepSha256Only = { List components -> + components.each { component -> + if (component.hashes != null) { + component.hashes = component.hashes.findAll { it.alg == 'SHA-256' } + if (component.hashes.isEmpty()) { + component.remove('hashes') + } + } + keepSha256Only(component.components ?: []) + } + } + keepSha256Only(json.components) + + bomFile.text = JsonOutput.prettyPrint(JsonOutput.toJson(json)) +} + +tasks.register('cyclonedxFull', CyclonedxDirectTask) { + group = 'Bill of Materials' + description = 'Generates CycloneDX BOM for the full Solr distribution' + + includeConfigs = ['bomFull'] + projectType = Component.Type.APPLICATION + + // The plugin resolves the configuration leniently and without depending on it, + // so the jars of Solr projects must be built first or their hashes are missing. + inputs.files(configurations.bomFull) + .withPropertyName('bomFullArtifacts') + .withNormalizer(ClasspathNormalizer) + + // Distribution directories scanned for the archive location of each artifact + inputs.files(configurations.server, configurations.modules, configurations.crossDcManager) + .withPropertyName('distributionDirs') + .withPathSensitivity(PathSensitivity.RELATIVE) + + // Child SBOMs of the UI bundles; empty when the projects are disabled + inputs.files(configurations.jsClientSbom, configurations.uiSbom) + .withPropertyName('childSboms') + .withPathSensitivity(PathSensitivity.NONE) + + // Sources of the statically listed JavaScript components + inputs.dir(rootProject.file('solr/webapp/web/libs')) + .withPropertyName('vendoredJsLibs') + .withPathSensitivity(PathSensitivity.RELATIVE) + inputs.file(rootProject.file('kotlin-js-store/wasm/yarn.lock')) + .withPropertyName('uiYarnLock') + .withPathSensitivity(PathSensitivity.NONE) + + jsonOutput = cyclonedxDir.get().file("bom-full.json").asFile + + doLast { + postProcessBom(jsonOutput.get().asFile, 'full', [ + 'server': configurations.server, + 'modules': configurations.modules, + 'cross-dc-manager': configurations.crossDcManager, + ], configurations.jsClientSbom.files.find(), configurations.uiSbom.files.find()) + } +} + +tasks.register('cyclonedxSlim', CyclonedxDirectTask) { + group = 'Bill of Materials' + description = 'Generates CycloneDX BOM for the slim Solr distribution' + + includeConfigs = ['bomSlim'] + projectType = Component.Type.APPLICATION + + // The plugin resolves the configuration leniently and without depending on it, + // so the jars of Solr projects must be built first or their hashes are missing. + inputs.files(configurations.bomSlim) + .withPropertyName('bomSlimArtifacts') + .withNormalizer(ClasspathNormalizer) + + // Distribution directories scanned for the archive location of each artifact + inputs.files(configurations.server) + .withPropertyName('distributionDirs') + .withPathSensitivity(PathSensitivity.RELATIVE) + + // Child SBOMs of the UI bundles; empty when the projects are disabled + inputs.files(configurations.jsClientSbom, configurations.uiSbom) + .withPropertyName('childSboms') + .withPathSensitivity(PathSensitivity.NONE) + + // Sources of the statically listed JavaScript components + inputs.dir(rootProject.file('solr/webapp/web/libs')) + .withPropertyName('vendoredJsLibs') + .withPathSensitivity(PathSensitivity.RELATIVE) + inputs.file(rootProject.file('kotlin-js-store/wasm/yarn.lock')) + .withPropertyName('uiYarnLock') + .withPathSensitivity(PathSensitivity.NONE) + + jsonOutput = cyclonedxDir.get().file("bom-slim.json").asFile + + doLast { + postProcessBom(jsonOutput.get().asFile, 'slim', [ + 'server': configurations.server, + ], configurations.jsClientSbom.files.find(), configurations.uiSbom.files.find()) + } +} + +tasks.register('cyclonedx') { + group = 'Bill of Materials' + description = 'Generates CycloneDX BOMs for Solr distributions' + + dependsOn 'cyclonedxFull' + dependsOn 'cyclonedxSlim' } description = 'Solr distribution packaging' ext { + cyclonedxDir = layout.buildDirectory.dir("cyclonedx") distDir = file("$buildDir/solr-${version}") slimDistDir = file("$buildDir/solr-${version}-slim") devDir = file("$buildDir/dev") @@ -49,17 +467,79 @@ configurations { solrSlimTgz solrFullTgzSignature solrSlimTgzSignature + // For the CycloneDX BOM generation + bomSlim { + canBeResolved = true + canBeConsumed = false + } + bomFull { + canBeResolved = true + canBeConsumed = false + extendsFrom bomSlim + } + // Child SBOMs of the UI bundles, merged into the distribution SBOMs by postProcessBom + jsClientSbom { + canBeResolved = true + canBeConsumed = false + } + uiSbom { + canBeResolved = true + canBeConsumed = false + } +} + +// Request the standard JVM runtime variants, like runtimeClasspath does. +// +// The CycloneDX plugin resolves configurations leniently. Without these attributes, +// variant-aware dependencies (e.g. Guava) and platform constraints (e.g. the Jersey BOM) fail to resolve +// and silently disappear. +[configurations.bomSlim, configurations.bomFull].each { conf -> + conf.attributes { + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY)) + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.JAR)) + attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL)) + attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objects.named(TargetJvmEnvironment, TargetJvmEnvironment.STANDARD_JVM)) + } } +// The distribution keeps the server versions of the libraries shared between the +// server and the modules, so align the full BOM with the versions of the slim one +// (e.g. a module may pull in a newer slf4j-api than the one in server/lib/ext). +configurations.bomFull.shouldResolveConsistentlyWith(configurations.bomSlim) + dependencies { - rootProject.project(":solr:modules").childProjects.values().stream().map {project -> project.path}.each { - module -> modules project(path: module, configuration: "packaging") + rootProject.project(":solr:modules").childProjects.values().stream().map {project -> project.path}.each { module -> + modules project(path: module, configuration: "packaging") + // No "configuration:" here on purpose. + // + // Gradle then selects the variant of the module that matches the attributes declared on the bom configurations above, + // just like it does for a runtime classpath ("variant-aware" resolution). + // Naming a configuration would bypass attribute matching, and imported Maven BOMs (e.g. Jersey's) would no + // longer provide the versions of their managed dependencies. + bomFull project(path: module) } crossDcManager project(path: ":solr:cross-dc-manager", configuration: "packaging") + bomFull project(path: ':solr:cross-dc-manager') example project(path: ":solr:example", configuration: "packaging") server project(path: ":solr:server", configuration: "packaging") + bomSlim project(path: ':solr:server', configuration: 'startJar') + bomSlim project(path: ':solr:server', configuration: 'serverLib') + bomSlim project(path: ':solr:server', configuration: 'libExt') + // Variant-aware dependency instead of server's by-name 'solrCore' + // configuration, see the comment on the modules above. + bomSlim project(path: ':solr:core') + bomSlim project(path: ':solr:server', configuration: 'webapp') + + // Child SBOMs of the UI bundles; empty when the projects are disabled + if (gradle.ext.withJsClient) { + jsClientSbom project(path: ':solr:webapp:js-client', configuration: 'jsClientSbom') + } + if (gradle.ext.withUiModule) { + uiSbom project(path: ':solr:ui', configuration: 'uiSbom') + } docker project(path: ':solr:docker', configuration: 'packaging') @@ -119,6 +599,12 @@ distributions { } }) + // Include CycloneDX BOM + from(cyclonedxDir) { + include 'bom-slim.json' + rename 'bom-slim.json', 'bom.json' + } + // Manually correct posix permissions (matters when packaging on Windows). filesMatching([ "**/*.sh", @@ -127,7 +613,6 @@ distributions { ]) {copy -> copy.permissions { unix("0755") } } - } } full { @@ -140,6 +625,13 @@ distributions { into "modules" }) + // Include CycloneDX BOM + from(cyclonedxDir) { + include 'bom-full.json' + rename 'bom-full.json', 'bom.json' + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + from(configurations.crossDcManager, { into "cross-dc-manager" filesMatching([ @@ -153,10 +645,12 @@ distributions { } installFullDist { + dependsOn 'cyclonedx' into distDir } installSlimDist { + dependsOn 'cyclonedx' into slimDistDir } @@ -195,10 +689,12 @@ task dev { } fullDistTar { + dependsOn 'cyclonedx' compression = Compression.GZIP } slimDistTar { + dependsOn 'cyclonedx' compression = Compression.GZIP } diff --git a/solr/packaging/gradle.lockfile b/solr/packaging/gradle.lockfile index 67666c69405d..6f7191470d53 100644 --- a/solr/packaging/gradle.lockfile +++ b/solr/packaging/gradle.lockfile @@ -2,4 +2,333 @@ # Manual edits can break the build and are not advised. # This file is expected to be part of source control. # To regenerate this file, run: ./gradlew :solr:packaging:dependencies --write-locks -empty=crossDcManager,docker,docs,example,jarValidation,modules,server,solrFullTgz,solrFullTgzSignature,solrSlimTgz,solrSlimTgzSignature +at.yawk.lz4:lz4-java:1.10.1=bomFull +com.carrotsearch:hppc:0.10.0=bomFull,bomSlim +com.fasterxml.jackson.core:jackson-annotations:2.22=bomFull,bomSlim +com.fasterxml.jackson.core:jackson-core:2.22.0=bomFull,bomSlim +com.fasterxml.jackson.core:jackson-databind:2.22.0=bomFull,bomSlim +com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.22.0=bomFull,bomSlim +com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.22.0=bomFull +com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.22.0=bomFull,bomSlim +com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.22.0=bomFull +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.22.0=bomFull +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.22.0=bomFull +com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.22.0=bomFull,bomSlim +com.fasterxml.jackson.module:jackson-module-scala_2.13:2.22.0=bomFull +com.fasterxml.jackson:jackson-bom:2.22.0=bomFull,bomSlim +com.fasterxml.woodstox:woodstox-core:7.2.1=bomFull,bomSlim +com.github.ben-manes.caffeine:caffeine:3.2.4=bomFull,bomSlim +com.github.luben:zstd-jni:1.5.6-4=bomFull +com.google.android:annotations:4.1.1.4=bomFull +com.google.api-client:google-api-client:2.7.2=bomFull +com.google.api.grpc:gapic-google-cloud-storage-v2:2.69.0=bomFull +com.google.api.grpc:grpc-google-cloud-storage-v2:2.69.0=bomFull +com.google.api.grpc:proto-google-cloud-storage-v2:2.69.0=bomFull +com.google.api.grpc:proto-google-common-protos:2.72.0=bomFull +com.google.api.grpc:proto-google-iam-v1:1.67.0=bomFull +com.google.api:api-common:2.64.0=bomFull +com.google.api:gax-grpc:2.81.0=bomFull +com.google.api:gax-httpjson:2.81.0=bomFull +com.google.api:gax:2.81.0=bomFull +com.google.apis:google-api-services-storage:v1-rev20260204-2.0.0=bomFull +com.google.auth:google-auth-library-credentials:1.48.0=bomFull +com.google.auth:google-auth-library-oauth2-http:1.48.0=bomFull +com.google.auto.value:auto-value-annotations:1.11.1=bomFull +com.google.cloud:google-cloud-bom:0.265.0=bomFull +com.google.cloud:google-cloud-core-grpc:2.71.0=bomFull +com.google.cloud:google-cloud-core-http:2.71.0=bomFull +com.google.cloud:google-cloud-core:2.71.0=bomFull +com.google.cloud:google-cloud-storage:2.69.0=bomFull +com.google.code.gson:gson:2.14.0=bomFull +com.google.errorprone:error_prone_annotations:2.47.0=bomFull,bomSlim +com.google.guava:failureaccess:1.0.3=bomFull,bomSlim +com.google.guava:guava:33.6.0-jre=bomFull,bomSlim +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=bomFull,bomSlim +com.google.http-client:google-http-client-apache-v2:2.1.0=bomFull +com.google.http-client:google-http-client-appengine:2.1.0=bomFull +com.google.http-client:google-http-client-gson:2.1.0=bomFull +com.google.http-client:google-http-client-jackson2:2.1.0=bomFull +com.google.http-client:google-http-client:2.1.0=bomFull +com.google.j2objc:j2objc-annotations:3.1=bomFull,bomSlim +com.google.oauth-client:google-oauth-client:1.39.0=bomFull +com.google.protobuf:protobuf-java-util:4.35.1=bomFull +com.google.protobuf:protobuf-java:4.35.1=bomFull +com.google.re2j:re2j:1.8=bomFull +com.googlecode.json-simple:json-simple:1.1.1=bomFull +com.ibm.icu:icu4j:78.3=bomFull +com.j256.simplemagic:simplemagic:1.17=bomFull,bomSlim +com.jayway.jsonpath:json-path:3.0.0=bomFull,bomSlim +com.knuddels:jtokkit:1.1.0=bomFull +com.lmax:disruptor:4.0.0=bomFull,bomSlim +com.microsoft.onnxruntime:onnxruntime:1.26.0=bomFull +com.nvidia.cuvs.lucene:cuvs-lucene:25.12.0=bomFull +com.nvidia.cuvs:cuvs-java:26.06.0=bomFull +com.squareup.okhttp3:okhttp-jvm:5.4.0=bomFull +com.squareup.okhttp3:okhttp:5.4.0=bomFull +com.squareup.okio:okio-jvm:3.17.0=bomFull +com.squareup.okio:okio:3.17.0=bomFull +com.squareup.retrofit2:converter-jackson:2.9.0=bomFull +com.squareup.retrofit2:retrofit:2.9.0=bomFull +com.tdunning:t-digest:3.3=bomFull,bomSlim +com.thoughtworks.paranamer:paranamer:2.8.3=bomFull +com.typesafe.scala-logging:scala-logging_2.13:3.9.5=bomFull +com.yammer.metrics:metrics-core:2.2.0=bomFull +commons-beanutils:commons-beanutils:1.11.0=bomFull +commons-cli:commons-cli:1.11.0=bomFull,bomSlim +commons-codec:commons-codec:1.22.0=bomFull,bomSlim +commons-collections:commons-collections:3.2.2=bomFull +commons-digester:commons-digester:2.1=bomFull +commons-io:commons-io:2.22.0=bomFull,bomSlim +commons-validator:commons-validator:1.10.1=bomFull +dev.langchain4j:langchain4j-bom:1.17.0=bomFull +dev.langchain4j:langchain4j-cohere:1.17.0-beta27=bomFull +dev.langchain4j:langchain4j-core:1.17.0=bomFull +dev.langchain4j:langchain4j-http-client-jdk:1.17.0=bomFull +dev.langchain4j:langchain4j-http-client:1.17.0=bomFull +dev.langchain4j:langchain4j-hugging-face:1.17.0-beta27=bomFull +dev.langchain4j:langchain4j-mistral-ai:1.17.0=bomFull +dev.langchain4j:langchain4j-open-ai:1.17.0=bomFull +io.dropwizard.metrics:metrics-core:4.2.39=bomFull,bomSlim +io.github.azagniotov:language-detection:12.5.2=bomFull +io.grpc:grpc-alts:1.82.0=bomFull +io.grpc:grpc-api:1.82.0=bomFull +io.grpc:grpc-auth:1.82.0=bomFull +io.grpc:grpc-bom:1.82.0=bomFull +io.grpc:grpc-context:1.82.0=bomFull +io.grpc:grpc-core:1.82.0=bomFull +io.grpc:grpc-googleapis:1.82.0=bomFull +io.grpc:grpc-grpclb:1.82.0=bomFull +io.grpc:grpc-inprocess:1.82.0=bomFull +io.grpc:grpc-netty-shaded:1.82.0=bomFull +io.grpc:grpc-protobuf-lite:1.82.0=bomFull +io.grpc:grpc-protobuf:1.82.0=bomFull +io.grpc:grpc-rls:1.82.0=bomFull +io.grpc:grpc-services:1.82.0=bomFull +io.grpc:grpc-stub:1.82.0=bomFull +io.grpc:grpc-util:1.82.0=bomFull +io.grpc:grpc-xds:1.82.0=bomFull +io.netty:netty-buffer:4.2.15.Final=bomFull,bomSlim +io.netty:netty-codec-base:4.2.15.Final=bomFull,bomSlim +io.netty:netty-common:4.2.15.Final=bomFull,bomSlim +io.netty:netty-handler:4.2.15.Final=bomFull,bomSlim +io.netty:netty-resolver:4.2.15.Final=bomFull,bomSlim +io.netty:netty-tcnative-boringssl-static:2.0.79.Final=bomFull,bomSlim +io.netty:netty-tcnative-classes:2.0.79.Final=bomFull,bomSlim +io.netty:netty-transport-classes-epoll:4.2.15.Final=bomFull,bomSlim +io.netty:netty-transport-native-epoll:4.2.15.Final=bomFull,bomSlim +io.netty:netty-transport-native-unix-common:4.2.15.Final=bomFull,bomSlim +io.netty:netty-transport:4.2.15.Final=bomFull,bomSlim +io.opencensus:opencensus-api:0.31.1=bomFull +io.opencensus:opencensus-contrib-http-util:0.31.1=bomFull +io.opentelemetry.contrib:opentelemetry-gcp-resources:1.37.0-alpha=bomFull +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator:2.22.0-alpha=bomFull,bomSlim +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:2.22.0=bomFull,bomSlim +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17:2.22.0-alpha=bomFull,bomSlim +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8:2.22.0-alpha=bomFull,bomSlim +io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=bomFull,bomSlim +io.opentelemetry:opentelemetry-api-incubator:1.56.0-alpha=bomFull,bomSlim +io.opentelemetry:opentelemetry-api:1.56.0=bomFull,bomSlim +io.opentelemetry:opentelemetry-bom:1.56.0=bomFull +io.opentelemetry:opentelemetry-common:1.56.0=bomFull,bomSlim +io.opentelemetry:opentelemetry-context:1.56.0=bomFull,bomSlim +io.opentelemetry:opentelemetry-exporter-common:1.56.0=bomFull +io.opentelemetry:opentelemetry-exporter-otlp-common:1.56.0=bomFull +io.opentelemetry:opentelemetry-exporter-otlp:1.56.0=bomFull +io.opentelemetry:opentelemetry-exporter-prometheus:1.56.0-alpha=bomFull,bomSlim +io.opentelemetry:opentelemetry-exporter-sender-jdk:1.56.0=bomFull +io.opentelemetry:opentelemetry-sdk-common:1.56.0=bomFull,bomSlim +io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.56.0=bomFull +io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.56.0=bomFull +io.opentelemetry:opentelemetry-sdk-logs:1.56.0=bomFull +io.opentelemetry:opentelemetry-sdk-metrics:1.56.0=bomFull,bomSlim +io.opentelemetry:opentelemetry-sdk-trace:1.56.0=bomFull,bomSlim +io.opentelemetry:opentelemetry-sdk:1.56.0=bomFull,bomSlim +io.perfmark:perfmark-api:0.27.0=bomFull +io.prometheus:prometheus-metrics-exposition-formats:1.1.0=bomFull,bomSlim +io.prometheus:prometheus-metrics-model:1.1.0=bomFull,bomSlim +io.sgr:s2-geometry-library-java:1.0.0=bomFull,bomSlim +io.swagger.core.v3:swagger-annotations-jakarta:2.2.52=bomFull,bomSlim +jakarta.activation:jakarta.activation-api:2.1.3=bomFull,bomSlim +jakarta.annotation:jakarta.annotation-api:3.0.0=bomFull,bomSlim +jakarta.inject:jakarta.inject-api:2.0.1=bomFull,bomSlim +jakarta.servlet:jakarta.servlet-api:6.1.0=bomFull,bomSlim +jakarta.validation:jakarta.validation-api:3.1.0=bomFull,bomSlim +jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=bomFull,bomSlim +jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=bomFull,bomSlim +net.sf.jopt-simple:jopt-simple:5.0.4=bomFull +net.sourceforge.argparse4j:argparse4j:0.7.0=bomFull +org.antlr:antlr4-runtime:4.13.2=bomFull,bomSlim +org.apache.calcite.avatica:avatica-core:1.25.0=bomFull +org.apache.calcite.avatica:avatica-metrics:1.25.0=bomFull +org.apache.calcite:calcite-core:1.37.0=bomFull +org.apache.calcite:calcite-linq4j:1.37.0=bomFull +org.apache.commons:commons-exec:1.6.0=bomFull,bomSlim +org.apache.commons:commons-lang3:3.20.0=bomFull,bomSlim +org.apache.commons:commons-math3:3.6.1=bomFull,bomSlim +org.apache.commons:commons-text:1.15.0=bomFull +org.apache.curator:curator-client:5.9.0=bomFull,bomSlim +org.apache.curator:curator-framework:5.9.0=bomFull,bomSlim +org.apache.httpcomponents.client5:httpclient5:5.2.1=bomFull +org.apache.httpcomponents.core5:httpcore5-h2:5.2=bomFull +org.apache.httpcomponents.core5:httpcore5:5.2.3=bomFull +org.apache.httpcomponents:httpclient:4.5.14=bomFull +org.apache.httpcomponents:httpcore:4.4.16=bomFull +org.apache.kafka:kafka-clients:3.9.2=bomFull +org.apache.kafka:kafka-group-coordinator-api:3.9.2=bomFull +org.apache.kafka:kafka-group-coordinator:3.9.2=bomFull +org.apache.kafka:kafka-metadata:3.9.2=bomFull +org.apache.kafka:kafka-raft:3.9.2=bomFull +org.apache.kafka:kafka-server-common:3.9.2=bomFull +org.apache.kafka:kafka-server:3.9.2=bomFull +org.apache.kafka:kafka-storage-api:3.9.2=bomFull +org.apache.kafka:kafka-storage:3.9.2=bomFull +org.apache.kafka:kafka-streams:3.9.2=bomFull +org.apache.kafka:kafka-tools-api:3.9.2=bomFull +org.apache.kafka:kafka-transaction-coordinator:3.9.2=bomFull +org.apache.kafka:kafka_2.13:3.9.2=bomFull +org.apache.logging.log4j:log4j-1.2-api:2.26.0=bomFull,bomSlim +org.apache.logging.log4j:log4j-api:2.26.0=bomFull,bomSlim +org.apache.logging.log4j:log4j-core:2.26.0=bomFull,bomSlim +org.apache.logging.log4j:log4j-layout-template-json:2.26.0=bomFull,bomSlim +org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0=bomFull,bomSlim +org.apache.logging.log4j:log4j-web:2.26.0=bomFull,bomSlim +org.apache.lucene:lucene-analysis-common:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-analysis-icu:10.4.0=bomFull +org.apache.lucene:lucene-analysis-kuromoji:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-analysis-morfologik:10.4.0=bomFull +org.apache.lucene:lucene-analysis-nori:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-analysis-opennlp:10.4.0=bomFull +org.apache.lucene:lucene-analysis-phonetic:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-analysis-smartcn:10.4.0=bomFull +org.apache.lucene:lucene-analysis-stempel:10.4.0=bomFull +org.apache.lucene:lucene-backward-codecs:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-classification:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-codecs:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-core:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-expressions:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-facet:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-grouping:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-highlighter:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-join:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-memory:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-misc:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-queries:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-queryparser:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-sandbox:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-spatial-extras:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-spatial3d:10.4.0=bomFull,bomSlim +org.apache.lucene:lucene-suggest:10.4.0=bomFull,bomSlim +org.apache.opennlp:opennlp-dl:2.5.10=bomFull +org.apache.opennlp:opennlp-tools:2.5.10=bomFull +org.apache.tika:tika-core:3.3.1=bomFull +org.apache.zookeeper:zookeeper-jute:3.9.5=bomFull,bomSlim +org.apache.zookeeper:zookeeper:3.9.5=bomFull,bomSlim +org.apiguardian:apiguardian-api:1.1.2=bomFull +org.bitbucket.b_c:jose4j:0.9.6=bomFull +org.carrot2:carrot2-core:4.8.6=bomFull +org.carrot2:morfologik-fsa:2.1.9=bomFull +org.carrot2:morfologik-polish:2.1.9=bomFull +org.carrot2:morfologik-stemming:2.1.9=bomFull +org.checkerframework:checker-qual:4.2.0=bomFull +org.codehaus.janino:commons-compiler:3.1.11=bomFull +org.codehaus.janino:janino:3.1.11=bomFull +org.codehaus.woodstox:stax2-api:4.3.0=bomFull,bomSlim +org.conscrypt:conscrypt-openjdk-uber:2.5.2=bomFull +org.eclipse.jetty.compression:jetty-compression-common:12.1.10=bomFull,bomSlim +org.eclipse.jetty.compression:jetty-compression-gzip:12.1.10=bomFull,bomSlim +org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.10=bomFull,bomSlim +org.eclipse.jetty.ee10:jetty-ee10-servlets:12.1.10=bomFull,bomSlim +org.eclipse.jetty.ee10:jetty-ee10-webapp:12.1.10=bomFull,bomSlim +org.eclipse.jetty.ee:jetty-ee-webapp:12.1.10=bomFull,bomSlim +org.eclipse.jetty.http2:jetty-http2-client-transport:12.1.10=bomFull,bomSlim +org.eclipse.jetty.http2:jetty-http2-client:12.1.10=bomFull,bomSlim +org.eclipse.jetty.http2:jetty-http2-common:12.1.10=bomFull,bomSlim +org.eclipse.jetty.http2:jetty-http2-hpack:12.1.10=bomFull,bomSlim +org.eclipse.jetty.http2:jetty-http2-server:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-alpn-client:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-alpn-java-client:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-alpn-java-server:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-alpn-server:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-client:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-deploy:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-http:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-io:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-jmx:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-rewrite:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-security:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-server:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-session:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-start:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-util:12.1.10=bomFull,bomSlim +org.eclipse.jetty:jetty-xml:12.1.10=bomFull,bomSlim +org.glassfish.hk2.external:aopalliance-repackaged:4.0.1=bomFull,bomSlim +org.glassfish.hk2:hk2-api:4.0.1=bomFull,bomSlim +org.glassfish.hk2:hk2-locator:4.0.1=bomFull,bomSlim +org.glassfish.hk2:hk2-utils:4.0.1=bomFull,bomSlim +org.glassfish.hk2:osgi-resource-locator:3.0.0=bomFull,bomSlim +org.glassfish.jersey.containers:jersey-container-jetty-http:4.0.2=bomFull,bomSlim +org.glassfish.jersey.core:jersey-client:4.0.2=bomFull,bomSlim +org.glassfish.jersey.core:jersey-common:4.0.2=bomFull,bomSlim +org.glassfish.jersey.core:jersey-server:4.0.2=bomFull,bomSlim +org.glassfish.jersey.ext:jersey-entity-filtering:4.0.2=bomFull,bomSlim +org.glassfish.jersey.inject:jersey-hk2:4.0.2=bomFull,bomSlim +org.glassfish.jersey.media:jersey-media-json-jackson:4.0.2=bomFull,bomSlim +org.glassfish.jersey:jersey-bom:4.0.2=bomFull,bomSlim +org.javassist:javassist:3.30.2-GA=bomFull,bomSlim +org.jetbrains.kotlin:kotlin-stdlib:2.2.21=bomFull +org.jetbrains:annotations:26.1.0=bomFull +org.jspecify:jspecify:1.0.0=bomFull,bomSlim +org.locationtech.jts.io:jts-io-common:1.19.0=bomFull +org.locationtech.jts:jts-core:1.19.0=bomFull +org.locationtech.proj4j:proj4j:1.2.2=bomFull +org.locationtech.spatial4j:spatial4j:0.8=bomFull,bomSlim +org.ow2.asm:asm-commons:9.10.1=bomFull,bomSlim +org.ow2.asm:asm-tree:9.10.1=bomFull,bomSlim +org.ow2.asm:asm:9.10.1=bomFull,bomSlim +org.pcollections:pcollections:4.0.1=bomFull +org.reactivestreams:reactive-streams:1.0.4=bomFull +org.rocksdb:rocksdbjni:7.9.2=bomFull +org.scala-lang.modules:scala-collection-compat_2.13:2.10.0=bomFull +org.scala-lang.modules:scala-java8-compat_2.13:1.0.2=bomFull +org.scala-lang:scala-library:2.13.18=bomFull +org.scala-lang:scala-reflect:2.13.15=bomFull +org.semver4j:semver4j:6.0.0=bomFull,bomSlim +org.slf4j:jcl-over-slf4j:2.0.17=bomFull,bomSlim +org.slf4j:jul-to-slf4j:2.0.17=bomFull,bomSlim +org.slf4j:slf4j-api:2.0.17=bomFull,bomSlim +org.threeten:threetenbp:1.7.3=bomFull +org.xerial.snappy:snappy-java:1.1.10.8=bomFull,bomSlim +software.amazon.awssdk:annotations:2.42.37=bomFull +software.amazon.awssdk:apache-client:2.42.37=bomFull +software.amazon.awssdk:arns:2.42.37=bomFull +software.amazon.awssdk:auth:2.42.37=bomFull +software.amazon.awssdk:aws-core:2.42.37=bomFull +software.amazon.awssdk:aws-query-protocol:2.42.37=bomFull +software.amazon.awssdk:aws-xml-protocol:2.42.37=bomFull +software.amazon.awssdk:bom:2.42.37=bomFull +software.amazon.awssdk:checksums-spi:2.42.37=bomFull +software.amazon.awssdk:checksums:2.42.37=bomFull +software.amazon.awssdk:crt-core:2.42.37=bomFull +software.amazon.awssdk:endpoints-spi:2.42.37=bomFull +software.amazon.awssdk:http-auth-aws-eventstream:2.42.37=bomFull +software.amazon.awssdk:http-auth-aws:2.42.37=bomFull +software.amazon.awssdk:http-auth-spi:2.42.37=bomFull +software.amazon.awssdk:http-auth:2.42.37=bomFull +software.amazon.awssdk:http-client-spi:2.42.37=bomFull +software.amazon.awssdk:identity-spi:2.42.37=bomFull +software.amazon.awssdk:json-utils:2.42.37=bomFull +software.amazon.awssdk:metrics-spi:2.42.37=bomFull +software.amazon.awssdk:profiles:2.42.37=bomFull +software.amazon.awssdk:protocol-core:2.42.37=bomFull +software.amazon.awssdk:regions:2.42.37=bomFull +software.amazon.awssdk:retries-spi:2.42.37=bomFull +software.amazon.awssdk:retries:2.42.37=bomFull +software.amazon.awssdk:s3:2.42.37=bomFull +software.amazon.awssdk:sdk-core:2.42.37=bomFull +software.amazon.awssdk:sts:2.42.37=bomFull +software.amazon.awssdk:third-party-jackson-core:2.42.37=bomFull +software.amazon.awssdk:utils-lite:2.42.37=bomFull +software.amazon.awssdk:utils:2.42.37=bomFull +software.amazon.eventstream:eventstream:1.0.1=bomFull +ua.net.nlp:morfologik-ukrainian-search:4.9.1=bomFull +empty=crossDcManager,docker,docs,example,jarValidation,jsClientSbom,modules,server,solrFullTgz,solrFullTgzSignature,solrSlimTgz,solrSlimTgzSignature,uiSbom diff --git a/solr/server/build.gradle b/solr/server/build.gradle index a22c084d94b0..99120e7ca922 100644 --- a/solr/server/build.gradle +++ b/solr/server/build.gradle @@ -25,14 +25,43 @@ javadoc.enabled(false) compileJava.enabled(false) configurations { - libExt + // === Custom configurations used to assemble the Solr server binary distribution === + + // 1. Jetty Bootstrap JAR + // Output Path: server/start.jar + // Description: Contains the Jetty bootstrap JAR responsible for launching the Solr server. + startJar + + // 2. Server Libraries + // Output Path: server/lib/ + // Description: Contains core libraries required by the Solr server at runtime (mostly Jetty-related JARs). serverLib + + // 3. Extended Server Libraries + // Output Path: server/lib/ext/ + // Description: Includes optional runtime libraries such as logging (SLF4J, Log4j) and metrics (Dropwizard, etc.). + libExt + + // 4. Solr Core JAR + // Output Path: server/solr-webapp/webapp/WEB-INF/lib/ + // Description: Contains the solr-core JAR, which includes the core functionality and indexing logic of Solr. solrCore + + // 5. Solr Web Application Libraries + // Output Path: server/solr-webapp/webapp/WEB-INF/lib/ + // Description: Contains the remaining Solr modules, packaged in exploded WAR format for deployment via Jetty. + webapp + + // === Runtime Configuration === + + // Combines core runtime dependencies for launching the Solr server, + // aggregating required libraries from serverLib, libExt, and solrCore. runtimeClasspath { extendsFrom serverLib, libExt, solrCore } - startJar - webapp + + // Internal configuration used by packaging tasks (e.g., creating distributions). + // This configuration only includes the `packagingDir` folder generated during assembly packaging } diff --git a/solr/ui/build.gradle.kts b/solr/ui/build.gradle.kts index b91ee268c906..cc7e2067fc88 100644 --- a/solr/ui/build.gradle.kts +++ b/solr/ui/build.gradle.kts @@ -15,6 +15,8 @@ * limitations under the License. */ +import org.cyclonedx.gradle.CyclonedxDirectTask +import org.cyclonedx.model.Component import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig @@ -239,3 +241,35 @@ artifacts { ) } } + +// CycloneDX SBOM of the Maven dependencies compiled into the wasmJs UI bundle, +// merged into the distribution SBOMs by :solr:packaging. + +val uiSbomFile = layout.buildDirectory.file("cyclonedx/bom-ui.json").get().asFile + +val cyclonedxUi = tasks.register("cyclonedxUi") { + group = "Bill of Materials" + description = "Generates a CycloneDX BOM of the dependencies compiled into the wasmJs UI bundle" + + includeConfigs.set(listOf("wasmJsRuntimeClasspath")) + projectType.set(Component.Type.LIBRARY) + + // The plugin resolves the configuration leniently and without depending on it, + // so the artifacts must be present first or their hashes are missing. + inputs.files(configurations.named("wasmJsRuntimeClasspath")) + .withPropertyName("wasmJsRuntimeArtifacts") + .withNormalizer(ClasspathNormalizer::class) + + jsonOutput.set(uiSbomFile) +} + +val uiSbom = configurations.create("uiSbom") { + isCanBeConsumed = true + isCanBeResolved = false +} + +artifacts { + add("uiSbom", uiSbomFile) { + builtBy(cyclonedxUi) + } +} diff --git a/solr/webapp/js-client/build.gradle.kts b/solr/webapp/js-client/build.gradle.kts index 1ce43c25978b..26a2d9ed00a0 100644 --- a/solr/webapp/js-client/build.gradle.kts +++ b/solr/webapp/js-client/build.gradle.kts @@ -17,6 +17,8 @@ import com.github.gradle.node.npm.task.NpmTask import com.github.gradle.node.npm.task.NpxTask +import groovy.json.JsonOutput +import groovy.json.JsonSlurper // Builds the OpenAPI-generated JS client (from :solr:api) into a single bundled // file, for :solr:webapp to include in the war. This is the only place in the @@ -49,9 +51,29 @@ val syncJSClientSourceCode = tasks.register("syncJSClientSourceCode") { into(jsClientWorkspace) - // Keep the node modules, so that they don't need to be re-downloaded + // Keep the outputs of "npm install", so that they don't need to be regenerated preserve { include("node_modules/**") + include("package-lock.json") + } + + // The OpenAPI generator wrongly declares the @babel/cli build tool as a runtime + // dependency; move it to devDependencies, so that the SBOM of the bundle + // (generated with --omit dev) only lists what browserify actually bundles. + doLast { + val packageJson = File(jsClientWorkspace, "package.json") + @Suppress("UNCHECKED_CAST") + val json = JsonSlurper().parse(packageJson) as MutableMap + + @Suppress("UNCHECKED_CAST") + val dependencies = json["dependencies"] as? MutableMap + dependencies?.remove("@babel/cli")?.let { babelCliVersion -> + @Suppress("UNCHECKED_CAST") + val devDependencies = + json.getOrPut("devDependencies") { mutableMapOf() } as MutableMap + devDependencies["@babel/cli"] = babelCliVersion + } + packageJson.writeText(JsonOutput.prettyPrint(JsonOutput.toJson(json))) } } @@ -64,6 +86,7 @@ val jsClientDownloadDeps = tasks.register("jsClientDownloadDeps") { inputs.dir("$jsClientWorkspace/src") inputs.file("$jsClientWorkspace/package.json") outputs.dir("$jsClientWorkspace/node_modules") + outputs.file("$jsClientWorkspace/package-lock.json") } val jsClientBuild = tasks.register("jsClientBuild") { @@ -117,3 +140,54 @@ artifacts { builtBy(finalizeJsBundleDir) } } + +// CycloneDX SBOM of the bundle, merged into the distribution SBOMs by :solr:packaging + +val jsClientSbomFile = layout.buildDirectory.file("cyclonedx/bom-js-client.json").get().asFile + +val downloadCyclonedxNpm = tasks.register("downloadCyclonedxNpm") { + args.set(listOf("install", "@cyclonedx/cyclonedx-npm@${libs.versions.cyclonedx.npm.get()}")) + + inputs.property("cyclonedx-npm version", libs.versions.cyclonedx.npm.get()) + outputs.dir(project.extra["nodeProjectDir"].toString() + "/node_modules/@cyclonedx/cyclonedx-npm") +} + +val generateJsClientSbom = tasks.register("generateJsClientSbom") { + dependsOn(downloadCyclonedxNpm) + // Needs the package-lock.json and node_modules produced by the install + dependsOn(jsClientDownloadDeps) + + // The full package spec, since the bare "cyclonedx-npm" command name resolves to an + // unrelated npm package. Runs from the node project dir, where downloadCyclonedxNpm + // installed the pinned version, and points at the workspace manifest instead. + command.set("@cyclonedx/cyclonedx-npm@${libs.versions.cyclonedx.npm.get()}") + args.set( + listOf( + // Only the packages bundled into the shipped file, not the build tooling + "--omit", "dev", + // Match the spec version emitted by the CycloneDX Gradle plugin in :solr:packaging + "--spec-version", "1.6", + "--output-reproducible", + "--output-format", "JSON", + "--output-file", jsClientSbomFile.absolutePath, + "$jsClientWorkspace/package.json", + ), + ) + workingDir.set(File(project.extra["nodeProjectDir"].toString())) + + inputs.file("$jsClientWorkspace/package.json") + inputs.file("$jsClientWorkspace/package-lock.json") + inputs.property("cyclonedx-npm version", libs.versions.cyclonedx.npm.get()) + outputs.file(jsClientSbomFile) +} + +val jsClientSbom = configurations.create("jsClientSbom") { + isCanBeConsumed = true + isCanBeResolved = false +} + +artifacts { + add("jsClientSbom", jsClientSbomFile) { + builtBy(generateJsClientSbom) + } +}