diff --git a/Ghidra/Features/FileFormats/build.gradle b/Ghidra/Features/FileFormats/build.gradle
index 5a91b1a922..1d6dad9fa5 100644
--- a/Ghidra/Features/FileFormats/build.gradle
+++ b/Ghidra/Features/FileFormats/build.gradle
@@ -60,10 +60,10 @@ dependencies {
// at the top of ghidra.file.formats.sevenzip.SevenZipCustomInitializer.
// This gradle task can be removed when SevenZipCustomInitializer is no longer needed.
String getSevenZipJarPath() {
- List libs = getExternalRuntimeDependencies(project);
- for(String lib: libs) {
- if (lib.contains("sevenzipjbinding-all-platforms")) {
- return lib;
+ def libs = getExternalRuntimeDependencies(project);
+ for (String path : libs.keySet()) {
+ if (path.contains("sevenzipjbinding-all-platforms")) {
+ return path;
}
}
return null
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/build.gradle b/build.gradle
index dce3a5149d..1ea16869dd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -196,28 +196,28 @@ def getCurrentDateTimeLong() {
}
/*********************************************************************************
- * Returns a list of all the external library paths declared as dependencies for the
- * given project
+ * Returns a map of all the external library paths declared as dependencies for the
+ * given project, mapped to their respective ExternalDependency objects.
*
*********************************************************************************/
-List getExternalRuntimeDependencies(Project project) {
- List list = new ArrayList()
+Map getExternalRuntimeDependencies(Project project) {
+ def map = [:]
if (project.configurations.find { it.name == 'api' }) {
- list.addAll(getExternalRuntimeDependencies(project, project.configurations.api));
+ map.putAll(getExternalRuntimeDependencies(project, project.configurations.api));
}
if (project.configurations.find { it.name == 'implementation' }) {
- list.addAll(getExternalRuntimeDependencies(project, project.configurations.implementation));
+ map.putAll(getExternalRuntimeDependencies(project, project.configurations.implementation));
}
if (project.configurations.find { it.name == 'runtimeOnly' }) {
- list.addAll(getExternalRuntimeDependencies(project, project.configurations.runtimeOnly));
+ map.putAll(getExternalRuntimeDependencies(project, project.configurations.runtimeOnly));
}
- return list
+ return map
}
-List getExternalRuntimeDependencies(Project project, Configuration configuration) {
- List list = new ArrayList<>();
+Map getExternalRuntimeDependencies(Project project, Configuration configuration) {
+ def map = [:]
configuration.dependencies.each { dep ->
// if the dependency is an external jar
@@ -248,11 +248,11 @@ List getExternalRuntimeDependencies(Project project, Configuration confi
}
// if we found the path, then add it to the list
if (depPath) {
- list.add(depPath)
+ map.put(depPath, dep)
}
}
}
- return list;
+ return map;
}
@@ -275,10 +275,10 @@ String generateLibraryDependencyMapping() {
libsFile.withWriter { out ->
subprojects { p ->
p.plugins.withType(JavaPlugin) {
- List libs = getExternalRuntimeDependencies(p);
+ def libs = getExternalRuntimeDependencies(p);
if (libs != null) {
out.println "Module: $p.name"
- libs.each { path ->
+ libs.each { path, dep ->
out.println "\t$path"
}
}
@@ -288,5 +288,81 @@ String generateLibraryDependencyMapping() {
return libsFile.absolutePath
}
+/******************************************************************************************
+ *
+ * Generates a hash of the given file with the given hash algorithm.
+ *
+ ******************************************************************************************/
+import java.security.DigestInputStream
+import java.security.MessageDigest
+
+String generateHash(File file, String alg) {
+ file.withInputStream {
+ new DigestInputStream(it, MessageDigest.getInstance(alg)).withStream {
+ it.eachByte {}
+ it.messageDigest.digest().encodeHex() as String
+ }
+ }
+}
+
+/******************************************************************************************
+ *
+ * Creates a CycloneDX Software Bill of Materials (SBOM) for the given project and
+ * returns it as a map.
+ *
+ ******************************************************************************************/
+def generateSoftwareBillOfMaterials(Project p) {
+
+ // Get license info from the Module.manifest file (if it exists)
+ def licenses = [:]
+ def manifestFile = file("${p.projectDir}/Module.manifest")
+ if (manifestFile.exists()) {
+ manifestFile.readLines().each { line ->
+ line = line.trim()
+ if (line.startsWith("MODULE FILE LICENSE:")) {
+ // Expected line: "MODULE FILE LICENSE: relative_path/to/jar License Type"
+ def value = line.substring("MODULE FILE LICENSE:".length()).trim()
+ def libAndLicense = value.split(" ", 2)
+ if (libAndLicense.size() != 2) {
+ throw new GradleException("Error parsing " + manifestFile + ":\n\t" + line)
+ }
+ def libPath = libAndLicense[0].trim()
+ def libName = libPath.substring(libPath.lastIndexOf("/") + 1)
+ def license = libAndLicense[1].trim()
+ licenses[libName] = license
+ }
+ }
+ }
+
+ // SBOM header
+ def sbom = ["bomFormat" : "CycloneDX", "specVersion" : "1.4", "version" : 1]
+
+ // SBOM components
+ sbom.components = []
+ getExternalRuntimeDependencies(p).each { path, dep ->
+ def f = file(path)
+ def component = [:]
+ component.type = "library"
+ component.group = dep.group ?: ""
+ component.name = dep.name
+ component.version = dep.version ?: ""
+ component.properties = [["ghidra-module" : p.name]]
+ if (dep.group && dep.version) {
+ component.purl = "pkg:maven/${dep.group}/${dep.name}@${dep.version}"
+ }
+ component.hashes = []
+ ["MD5", "SHA-1"].each { alg ->
+ component.hashes << ["alg" : alg, "content" : generateHash(f, alg)]
+ }
+ def license = licenses[f.name]
+ if (license) {
+ component.licenses = [["license" : ["name" : license]]]
+ }
+ sbom.components << component
+ }
+
+ return sbom
+}
+
task allSleighCompile {
}
diff --git a/gradle/distributableGhidraModule.gradle b/gradle/distributableGhidraModule.gradle
index b83bf52123..b8898c97ff 100644
--- a/gradle/distributableGhidraModule.gradle
+++ b/gradle/distributableGhidraModule.gradle
@@ -188,8 +188,8 @@ plugins.withType(JavaPlugin) {
// External Libraries
gradle.taskGraph.whenReady { taskGraph ->
- List externalPaths = getExternalRuntimeDependencies(p)
- externalPaths.each { path ->
+ def libs = getExternalRuntimeDependencies(p)
+ libs.each { path, dep ->
from (path) {
into {zipPath + "/lib" }
}
diff --git a/gradle/root/distribution.gradle b/gradle/root/distribution.gradle
index 32ea4baf0f..cd15d4a0e7 100644
--- a/gradle/root/distribution.gradle
+++ b/gradle/root/distribution.gradle
@@ -226,7 +226,43 @@ task zipJavadocs(type: Zip) {
description "Zips javadocs for Ghidra api. [gradle/root/distribution.gradle]"
}
+/******************************************************************************************
+ * TASK generateSoftwareBillOfMaterials
+ *
+ * Summary: Creates a file that lists the libraries used by each module.
+ ******************************************************************************************/
+import groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+task generateSoftwareBillOfMaterials {
+
+ doFirst {
+ // Create an SBOM map for each project.
+ // TODO: Write each SBOM to its project directory and use it as a replacement for
+ // the Module.manifest.
+ def projectSboms = []
+ subprojects { p ->
+ p.plugins.withType(JavaPlugin) {
+ projectSboms << generateSoftwareBillOfMaterials(p)
+ }
+ }
+
+ // Generate aggregated SBOM file for all of Ghidra
+ def sbom = ["bomFormat" : "CycloneDX", "specVersion" : "1.4", "version" : 1]
+ sbom.components = []
+ projectSboms.each { projectSbom ->
+ sbom.components += projectSbom.components
+ }
+
+ // Write SBOM to JSON file
+ def buildDir = file("$buildDir")
+ if (!buildDir.exists()) {
+ buildDir.mkdirs()
+ }
+ def sbomFile = file("$buildDir/bom.json")
+ sbomFile.write(JsonOutput.prettyPrint(JsonOutput.toJson(sbom)))
+ }
+}
/**********************************************************************************************
*
@@ -239,6 +275,8 @@ task assembleDistribution (type: Copy) {
// Not sure why this is necessary, but without it, gradle thinks this task is "up to date"
// every other time it is run even though in both cases the output directory has been removed
outputs.upToDateWhen {false}
+
+ dependsOn generateSoftwareBillOfMaterials
group 'private'
description "Copies core files/folders to the distribution location."
@@ -358,6 +396,13 @@ task assembleDistribution (type: Copy) {
include "settings.gradle"
into "Ghidra"
}
+
+ /////////////////////////////////////
+ // Software Bill of Materials (SBOM)
+ /////////////////////////////////////
+ from (ROOT_PROJECT_DIR + "/build") {
+ include "bom.json"
+ }
}
@@ -428,6 +473,13 @@ task createExternalExtensions(type: Copy) {
}
+/*********************************************************************************
+ * Update sla file timestamps to current time plus timeOffsetMinutes value.
+ *
+ * distributionDirectoryPath - Contains files/folders used by gradle zip task.
+ * timeOffsetMinutes - Number of minutes to increase sla file timestamp.
+ *
+**********************************************************************************/
import groovy.io.FileType
import java.nio.file.Path
import java.nio.file.Files
@@ -436,13 +488,6 @@ import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
import java.time.ZoneId
-/*********************************************************************************
- * Update sla file timestamps to current time plus timeOffsetMinutes value.
- *
- * distributionDirectoryPath - Contains files/folders used by gradle zip task.
- * timeOffsetMinutes - Number of minutes to increase sla file timestamp.
- *
-**********************************************************************************/
def updateSlaFilesTimestamp(String distributionDirectoryPath, int timeOffsetMinutes) {
logger.debug("updateSlaFilesTimestamp: distributionDirectoryPath = '$distributionDirectoryPath' and timeOffsetMinutes = '$timeOffsetMinutes',")
diff --git a/gradle/support/extensionCommon.gradle b/gradle/support/extensionCommon.gradle
index d1a11b101d..687aa44ee2 100644
--- a/gradle/support/extensionCommon.gradle
+++ b/gradle/support/extensionCommon.gradle
@@ -76,8 +76,8 @@ task zipExtensions (type: Zip) {
/////////////////
gradle.taskGraph.whenReady { taskGraph ->
if (project.plugins.withType(JavaPlugin)) {
- List externalPaths = getExternalRuntimeDependencies(p)
- externalPaths.each { path ->
+ def libs = getExternalRuntimeDependencies(p)
+ libs.each { path, dep ->
from (path) {
into { getBaseProjectName(p) + "/lib" }
}
diff --git a/gradle/support/ip.gradle b/gradle/support/ip.gradle
index 2b01fac95d..798e33e7a0 100644
--- a/gradle/support/ip.gradle
+++ b/gradle/support/ip.gradle
@@ -96,9 +96,9 @@ def Map getModuleManifestIp(Project project) {
*********************************************************************************/
def checkExternalLibsInMap(Map map, Project project) {
if (project.plugins.withType(JavaPlugin)) {
- List libs = getExternalRuntimeDependencies(project)
- libs.each { lib ->
- String libName = new File(lib).getName() // get just the filename without the path
+ def libs = getExternalRuntimeDependencies(project)
+ libs.each { path, dep ->
+ String libName = new File(path).getName() // get just the filename without the path
String relativePath = "lib/"+libName;
assert map.containsKey(relativePath) : "No License specified for external library: "+relativePath+ " in module "+project.projectDir
}