epugh commented on code in PR #4690:
URL: https://github.com/apache/solr/pull/4690#discussion_r3695655957


##########
solr/packaging/build.gradle:
##########
@@ -15,20 +15,438 @@
  * 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.
 
 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'

Review Comment:
   this block of code seems like a lot, should it go in it's own .gradle file?  
 There is a lot of docs about how this works in here, and I guess I don't 
totally know if it's needed.  It would seem like a `sbom.gradle` might be a 
better home?



##########
solr/packaging/build.gradle:
##########
@@ -15,20 +15,438 @@
  * 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.
 
 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/[email protected]?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/[email protected]?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<String, Object> 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<String, String> artifactIdByPath = 
rootProject.allprojects.collectEntries {
+        [(it.path): it.base.archivesName.get()]
+    }
+    json.components = json.components.collect { Map<String, Object> 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'],

Review Comment:
   i don't love the hard coding here...    CAn we consult other files like the 
licenses etc?   Yes, most of these won't ever be updated at this point..  but 
what happens if they are bumped?



##########
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

Review Comment:
   what does `@keep` mean?   Do we need this comment?   the -npm suffix seems 
clear.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to