From 5c0dc5f6dc63330b14acf0e9f58eb53b67fc37e8 Mon Sep 17 00:00:00 2001 From: Ryan Kurtz Date: Mon, 28 Mar 2022 07:02:12 -0400 Subject: [PATCH] GP-1782: Software Bill of Materials (SBOM) --- GhidraBuild/LaunchSupport/build.gradle | 10 ++ GhidraDocs/InstallationGuide.html | 4 + gradle/root/distribution.gradle | 9 ++ gradle/support/sbom.gradle | 166 +++++++++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 gradle/support/sbom.gradle diff --git a/GhidraBuild/LaunchSupport/build.gradle b/GhidraBuild/LaunchSupport/build.gradle index 09adcd0328..5c16b4c765 100644 --- a/GhidraBuild/LaunchSupport/build.gradle +++ b/GhidraBuild/LaunchSupport/build.gradle @@ -22,6 +22,16 @@ eclipse.project.name = '_LaunchSupport' sourceCompatibility = 1.8 targetCompatibility = 1.8 +jar { + manifest { + attributes ( + "Specification-Title": "${project.name}", + "Specification-Version": "${rootProject.RELEASE_VERSION}", + "Specification-Vendor": "Ghidra" + ) + } +} + rootProject.assembleDistribution { from (jar) { into "support" diff --git a/GhidraDocs/InstallationGuide.html b/GhidraDocs/InstallationGuide.html index 4a8f6f700a..d9e66b0763 100644 --- a/GhidraDocs/InstallationGuide.html +++ b/GhidraDocs/InstallationGuide.html @@ -296,6 +296,10 @@ is complete.

licenses Contains licenses used by Ghidra. + + bom.json + Software Bill of Materials (SBOM) in CycloneDX JSON format. +

(Back to Top)

diff --git a/gradle/root/distribution.gradle b/gradle/root/distribution.gradle index 32ea4baf0f..f44c8267ba 100644 --- a/gradle/root/distribution.gradle +++ b/gradle/root/distribution.gradle @@ -24,6 +24,8 @@ import org.apache.tools.ant.filters.* * *********************************************************************************/ +apply from: "$rootProject.projectDir/gradle/support/sbom.gradle" + /******************************************************************************** * Local Vars *********************************************************************************/ @@ -359,6 +361,13 @@ task assembleDistribution (type: Copy) { into "Ghidra" } + ////////////////////////////////////// + // Software Bill of Materials (SBOM) + ////////////////////////////////////// + doLast { + def bomFile = file("${destinationDir}/bom.json") + writeSoftwareBillOfMaterials(destinationDir, bomFile) + } } /********************************************************************************* diff --git a/gradle/support/sbom.gradle b/gradle/support/sbom.gradle new file mode 100644 index 0000000000..ea348ec100 --- /dev/null +++ b/gradle/support/sbom.gradle @@ -0,0 +1,166 @@ +/* ### + * IP: GHIDRA + * + * Licensed 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.jar.JarFile +import groovy.json.JsonOutput + +/****************************************************************************************** + * + * Generates a hash of the given file with the given hash algorithm and returns it as a + * String. + * + ******************************************************************************************/ +import java.security.DigestInputStream +import java.security.MessageDigest + +def generateHash(File file, String alg) { + file.withInputStream { + new DigestInputStream(it, MessageDigest.getInstance(alg)).withStream { + it.eachByte {} + it.messageDigest.digest().encodeHex() as String + } + } +} + +/****************************************************************************************** + * + * Returns true if the given jar is a Ghidra jar (as opposed to an external lib jar). + * Ghidra jars will have a MANIFEST.MF file that contains the following property: + * + * Specification-Vendor: Ghidra + ******************************************************************************************/ + def isGhidraJar(File jarFile) { + def manifest = new JarFile(jarFile).manifest + return manifest && manifest.mainAttributes.getValue("Specification-Vendor") == "Ghidra" + } + + /****************************************************************************************** + * + * Gets the group, name, and version of the given jar from its pom.xml file, if it exists. + * Empty strings are returned for the group, name, and version if they could not be found + * in a pom.xml file. + * + * Note that some jars have more than one pom.xml for one reason or another, so we validate + * against the jar filename to ensure we'll get the right one. + * + ******************************************************************************************/ + def extractPomGroupNameVersion(File jarFile, FileTree jarFileTree) { + def group = "" + def name = "" + def version = "" + jarFileTree.matching { include "**/pom.xml" }.each { pomFile -> + def pomProject = new XmlSlurper().parse(pomFile) + def artifactId = pomProject.artifactId.toString() + if (jarFile.name.contains(artifactId)) { + name = artifactId + group = pomProject.groupId.toString() ?: pomProject.parent.groupId.toString() + version = pomProject.version.toString() ?: pomProject.parent.version.toString() + } + } + return [group, name, version] + } + + /****************************************************************************************** + * + * Returns the name and version of the given jar file, which we expect to be of the form + * -.jar. Beware that both the name and version parts can contain dashes of + * their own. We will assume that the first dash with a digit that directly follows begins + * the version substring. + * + ******************************************************************************************/ + def extractNameAndVersion(File jarFile) { + def name = jarFile.name[0..-5] // remove ".jar" extension + def version = "" + def matcher = name =~ ~/(?.+?)-(?\d.*)/ + if (matcher.matches()) { + name = matcher.group("name") + version = matcher.group("version") + } + return [name, version] +} + +/****************************************************************************************** + * + * Returns a mostly empty but initialized CycloneDX Software Bill of Materials (SBOM) map. + * + ******************************************************************************************/ +def initializeSoftwareBillOfMaterials() { + def sbom = ["bomFormat" : "CycloneDX", "specVersion" : "1.4", "version" : 1] + sbom.metadata = ["properties" : []] + sbom.components = [] + return sbom +} + +/****************************************************************************************** + * + * Returns a CycloneDX Software Bill of Materials (SBOM) component map for the given + * dependency arguments. + * + ******************************************************************************************/ +def getSoftwareBillOfMaterialsComponent(File distroDir, File jarFile, String group, String name, String version, String license) { + def component = [:] + component.type = "library" + component.group = group ?: "" + component.name = name ?: "" + component.version = version ?: "" + if (group && name && version) { + component.purl = "pkg:maven/${group}/${name}@${version}" + } + component.hashes = [] + ["MD5", "SHA-1"].each { alg -> + component.hashes << ["alg" : alg, "content" : generateHash(jarFile, alg)] + } + if (license) { + component.licenses = [["license" : ["name" : license]]] + } + def location = jarFile.toString().substring(distroDir.toString().length() + 1) + component.properties = [["name" : "location", "value" : location.replaceAll("\\\\", "/")]] + return component +} + +/****************************************************************************************** + * + * Generates a CycloneDX Software Bill of Materials (SBOM) for the given distibution + * directory and writes it to the given SBOM file. + * + * Note that the SBOM will only contain entries for non-Ghidra jars. + * + ******************************************************************************************/ +ext.writeSoftwareBillOfMaterials = { distroDir, sbomFile -> + def sbom = initializeSoftwareBillOfMaterials() + + fileTree(distroDir).matching { include "**/*.jar" }.each { jarFile -> + def jarFileTree = zipTree(jarFile) + + if (!isGhidraJar(jarFile)) { + + // First try to get the group, name, and version from a pom.xml (if it exists) + def (group, name, version) = extractPomGroupNameVersion(jarFile, jarFileTree) + + // If that didn't work, get the name and version from the filename. We are out of luck + // with the group for now. + if (!name) { + (name, version) = extractNameAndVersion(jarFile) + } + + // Add our jar to the SBOM + sbom.components << getSoftwareBillOfMaterialsComponent(distroDir, jarFile, group, name, version, "") + + } + } + + // Write the SBOM to a new file + sbomFile.write(JsonOutput.prettyPrint(JsonOutput.toJson(sbom))) +}