Merge remote-tracking branch 'origin/GP-3824_ryanmkurtz_sbom--SQUASHED'

(Closes #5513)
This commit is contained in:
Ryan Kurtz 2023-09-11 09:43:03 -04:00
commit d65ded69f3

View file

@ -48,28 +48,30 @@ def generateHash(File file, String alg) {
/****************************************************************************************** /******************************************************************************************
* *
* Gets the group, name, and version of the given jar from its pom.xml file, if it exists. * Gets the group, name, description, and version of the given jar from its pom.xml file, if
* Empty strings are returned for the group, name, and version if they could not be found * it exists. Empty strings are returned for the group, name, description, and version if
* in a pom.xml file. * 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 * 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. * against the jar filename to ensure we'll get the right one.
* *
******************************************************************************************/ ******************************************************************************************/
def extractPomGroupNameVersion(File jarFile, FileTree jarFileTree) { def extractFromPom(File jarFile, FileTree jarFileTree) {
def group = "" def group = ""
def name = "" def name = ""
def description = ""
def version = "" def version = ""
jarFileTree.matching { include "**/pom.xml" }.each { pomFile -> jarFileTree.matching { include "**/pom.xml" }.each { pomFile ->
def pomProject = new XmlSlurper().parse(pomFile) def pomProject = new XmlSlurper().parse(pomFile)
def artifactId = pomProject.artifactId.toString() def artifactId = pomProject.artifactId.toString()
if (jarFile.name.contains(artifactId)) { if (jarFile.name.contains(artifactId)) {
name = artifactId name = artifactId
description = pomProject.description.toString()
group = pomProject.groupId.toString() ?: pomProject.parent.groupId.toString() group = pomProject.groupId.toString() ?: pomProject.parent.groupId.toString()
version = pomProject.version.toString() ?: pomProject.parent.version.toString() version = pomProject.version.toString() ?: pomProject.parent.version.toString()
} }
} }
return [group, name, version] return [group ?: "", name ?: "", description ?: "", version ?: ""]
} }
/****************************************************************************************** /******************************************************************************************
@ -77,20 +79,57 @@ def generateHash(File file, String alg) {
* Returns the name and version of the given jar file, which we expect to be of the form * Returns the name and version of the given jar file, which we expect to be of the form
* <name>-<version>.jar. Beware that both the name and version parts can contain dashes of * <name>-<version>.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 * their own. We will assume that the first dash with a digit that directly follows begins
* the version substring. * the version substring. An empty string is returned for the version if it could not be
* found.
* *
******************************************************************************************/ ******************************************************************************************/
def extractNameAndVersion(File jarFile) { def extractFromFilename(File jarFile) {
def name = jarFile.name[0..-5] // remove ".jar" extension def name = jarFile.name[0..<-4] // remove ".jar" extension
def version = "" def version = ""
def matcher = name =~ ~/(?<name>.+?)-(?<version>\d.*)/ def matcher = name =~ ~/(?<name>.+?)-(?<version>\d.*)/
if (matcher.matches()) { if (matcher.matches()) {
name = matcher.group("name") name = matcher.group("name")
version = matcher.group("version") version = matcher.group("version")
} }
return [name, version] return [name, version ?: ""]
} }
/******************************************************************************************
*
* Gets the group, description and version of the given jar from its MANIFEST.MF file, if it
* exists. Empty strings are returned for the group, description, and version if they could
* not be found in a MANIFEST.MF file.
*
******************************************************************************************/
def extractFromManifest(File jarFile) {
def group = ""
def description = ""
def version = ""
def manifest = new JarFile(jarFile).manifest
if (manifest) {
version = manifest.mainAttributes.getValue("Bundle-Version")
if (!version) {
version = manifest.mainAttributes.getValue("Specification-Version")
}
if (!version) {
version = manifest.mainAttributes.getValue("Implementation-Version")
}
if (manifest.mainAttributes.getValue("Specification-Vendor") == "Ghidra") {
def name = jarFile.getName()[0..<-4] // remove ".jar" extension
description = "Ghidra ${name} module"
group = "ghidra"
}
if (!description) {
description = manifest.mainAttributes.getValue("Bundle-Description")
}
if (!description) {
description = manifest.mainAttributes.getValue("Specification-Title")
}
}
return [group ?: "", description ?: "", version ?: ""]
}
/****************************************************************************************** /******************************************************************************************
* *
* Returns a mostly empty but initialized CycloneDX Software Bill of Materials (SBOM) map. * Returns a mostly empty but initialized CycloneDX Software Bill of Materials (SBOM) map.
@ -109,13 +148,15 @@ def initializeSoftwareBillOfMaterials() {
* dependency arguments. * dependency arguments.
* *
******************************************************************************************/ ******************************************************************************************/
def getSoftwareBillOfMaterialsComponent(File distroDir, File jarFile, String group, String name, String version, String license) { def getSoftwareBillOfMaterialsComponent(File distroDir, File jarFile, String group, String name,
String description, String version, String license) {
def component = [:] def component = [:]
component.type = "library" component.type = "library"
component.group = group ?: "" component.group = group ?: ""
component.name = name ?: "" component.name = name ?: ""
component.description = description ?: ""
component.version = version ?: "" component.version = version ?: ""
if (group && name && version) { if (group && name && version && !group.equals("ghidra")) {
component.purl = "pkg:maven/${group}/${name}@${version}" component.purl = "pkg:maven/${group}/${name}@${version}"
} }
component.hashes = [] component.hashes = []
@ -135,8 +176,6 @@ def getSoftwareBillOfMaterialsComponent(File distroDir, File jarFile, String gro
* Generates a CycloneDX Software Bill of Materials (SBOM) for the given distibution * Generates a CycloneDX Software Bill of Materials (SBOM) for the given distibution
* directory and writes it to the given SBOM file. * 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 -> ext.writeSoftwareBillOfMaterials = { distroDir, sbomFile ->
def sbom = initializeSoftwareBillOfMaterials() def sbom = initializeSoftwareBillOfMaterials()
@ -144,21 +183,37 @@ ext.writeSoftwareBillOfMaterials = { distroDir, sbomFile ->
fileTree(distroDir).matching { include "**/*.jar" }.each { jarFile -> fileTree(distroDir).matching { include "**/*.jar" }.each { jarFile ->
def jarFileTree = zipTree(jarFile) def jarFileTree = zipTree(jarFile)
if (!isGhidraJar(jarFile)) { // First try to get the group, name, description, and version from a pom.xml (if it exists)
def (group, name, description, version) = extractFromPom(jarFile, jarFileTree)
// 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 // If that didn't work, get the name and version from the filename. We are out of luck
// with the group for now. // with the group for now.
if (!name) { if (!name) {
(name, version) = extractNameAndVersion(jarFile) (name, version) = extractFromFilename(jarFile)
}
// Now try to get the description from a MANIFEST.MF file (if it exists). If we were unable
// to get the group and/or version from prior lookups, try to get them from the MANIFEST.MF
// file.
if (!description) {
def manifestGroup
def manifestVersion
(manifestGroup, description, manifestVersion) = extractFromManifest(jarFile)
if (!group) {
group = manifestGroup
}
if (!version) {
version = manifestVersion
}
}
// Normalize the whitespace in the description
if (description) {
description = description.trim().replaceAll("\\s+", " ")
} }
// Add our jar to the SBOM // Add our jar to the SBOM
sbom.components << getSoftwareBillOfMaterialsComponent(distroDir, jarFile, group, name, version, "") sbom.components << getSoftwareBillOfMaterialsComponent(distroDir, jarFile, group, name, description, version, "")
}
} }
// Write the SBOM to a new file // Write the SBOM to a new file