Article
SPDX (Software Package Data Exchange) is an open standard for communicating software bill of materials information. With increasing regulatory requirements and supply chain security concerns, generating accurate SPDX SBOMs has become essential for Java applications.
This guide covers everything from basic SPDX generation to advanced tooling integration and compliance workflows.
SPDX SBOM Overview
- Standard Format: ISO/IEC 5962:2021 standard
- Component Inventory: Complete list of software components
- License Information: Clear licensing data for all components
- Security Data: Vulnerability and security information
- Provenance: Origin and build information
1. Project Setup and Dependencies
Maven Dependencies:
<properties>
<spdx.tools.version>1.1.6</spdx.tools.version>
<cyclonedx.version>2.7.9</cyclonedx.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- SPDX Java Tools -->
<dependency>
<groupId>org.spdx</groupId>
<artifactId>spdx-tools</artifactId>
<version>${spdx.tools.version}</version>
</dependency>
<!-- CycloneDX for SBOM generation -->
<dependency>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>${cyclonedx.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- XML Processing -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
2. Core SPDX Model Classes
SPDX Document Model:
package com.example.sbom.spdx;
import java.util.*;
public class SpdxDocument {
private final String spdxVersion = "SPDX-2.3";
private final String dataLicense = "CC0-1.0";
private String spdxId;
private String name;
private String documentNamespace;
private List<String> creators = new ArrayList<>();
private String created;
private String comment;
private List<SpdxPackage> packages = new ArrayList<>();
private List<SpdxRelationship> relationships = new ArrayList<>();
public SpdxDocument(String spdxId, String name, String documentNamespace) {
this.spdxId = spdxId;
this.name = name;
this.documentNamespace = documentNamespace;
this.created = java.time.Instant.now().toString();
this.creators.add("Tool: Java-SBOM-Generator-1.0");
}
// Getters and setters
public String getSpdxVersion() { return spdxVersion; }
public String getDataLicense() { return dataLicense; }
public String getSpdxId() { return spdxId; }
public String getName() { return name; }
public String getDocumentNamespace() { return documentNamespace; }
public List<String> getCreators() { return Collections.unmodifiableList(creators); }
public String getCreated() { return created; }
public String getComment() { return comment; }
public List<SpdxPackage> getPackages() { return Collections.unmodifiableList(packages); }
public List<SpdxRelationship> getRelationships() { return Collections.unmodifiableList(relationships); }
public void addCreator(String creator) {
this.creators.add(creator);
}
public void addPackage(SpdxPackage pkg) {
this.packages.add(pkg);
}
public void addRelationship(SpdxRelationship relationship) {
this.relationships.add(relationship);
}
public void setComment(String comment) {
this.comment = comment;
}
}
class SpdxPackage {
private String spdxId;
private String name;
private String version;
private String downloadLocation;
private String fileName;
private String supplier;
private String originator;
private String description;
private String copyrightText;
private String licenseDeclared;
private String licenseConcluded;
private List<String> licenseInfoFromFiles = new ArrayList<>();
private boolean filesAnalyzed = false;
private String homepage;
private String sourceInfo;
private String summary;
private List<SpdxChecksum> checksums = new ArrayList<>();
private String externalRef;
private String comment;
public SpdxPackage(String spdxId, String name, String version) {
this.spdxId = spdxId;
this.name = name;
this.version = version;
}
// Getters and setters
public String getSpdxId() { return spdxId; }
public String getName() { return name; }
public String getVersion() { return version; }
public String getDownloadLocation() { return downloadLocation; }
public String getFileName() { return fileName; }
public String getSupplier() { return supplier; }
public String getOriginator() { return originator; }
public String getDescription() { return description; }
public String getCopyrightText() { return copyrightText; }
public String getLicenseDeclared() { return licenseDeclared; }
public String getLicenseConcluded() { return licenseConcluded; }
public List<String> getLicenseInfoFromFiles() { return Collections.unmodifiableList(licenseInfoFromFiles); }
public boolean isFilesAnalyzed() { return filesAnalyzed; }
public String getHomepage() { return homepage; }
public String getSourceInfo() { return sourceInfo; }
public String getSummary() { return summary; }
public List<SpdxChecksum> getChecksums() { return Collections.unmodifiableList(checksums); }
public String getExternalRef() { return externalRef; }
public String getComment() { return comment; }
public void setDownloadLocation(String downloadLocation) {
this.downloadLocation = downloadLocation;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public void setLicenseDeclared(String licenseDeclared) {
this.licenseDeclared = licenseDeclared;
}
public void setLicenseConcluded(String licenseConcluded) {
this.licenseConcluded = licenseConcluded;
}
public void addLicenseInfoFromFile(String license) {
this.licenseInfoFromFiles.add(license);
}
public void addChecksum(SpdxChecksum checksum) {
this.checksums.add(checksum);
}
public void setSupplier(String supplier) {
this.supplier = supplier;
}
public void setHomepage(String homepage) {
this.homepage = homepage;
}
}
class SpdxChecksum {
private String algorithm;
private String value;
public SpdxChecksum(String algorithm, String value) {
this.algorithm = algorithm;
this.value = value;
}
public String getAlgorithm() { return algorithm; }
public String getValue() { return value; }
}
class SpdxRelationship {
private String elementId;
private String relationshipType;
private String relatedElementId;
private String comment;
public SpdxRelationship(String elementId, String relationshipType, String relatedElementId) {
this.elementId = elementId;
this.relationshipType = relationshipType;
this.relatedElementId = relatedElementId;
}
public String getElementId() { return elementId; }
public String getRelationshipType() { return relationshipType; }
public String getRelatedElementId() { return relatedElementId; }
public String getComment() { return comment; }
public void setComment(String comment) {
this.comment = comment;
}
}
3. SPDX Document Generator
Core SBOM Generator:
package com.example.sbom.generator;
import com.example.sbom.spdx.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class SpdxSbomGenerator {
private final ObjectMapper jsonMapper;
private final XmlMapper xmlMapper;
private final LicenseResolver licenseResolver;
public SpdxSbomGenerator() {
this.jsonMapper = new ObjectMapper();
this.jsonMapper.enable(SerializationFeature.INDENT_OUTPUT);
this.xmlMapper = new XmlMapper();
this.xmlMapper.enable(SerializationFeature.INDENT_OUTPUT);
this.licenseResolver = new LicenseResolver();
}
public SpdxDocument generateFromMavenProject(File pomFile, String projectName, String version)
throws IOException {
String documentId = "SPDXRef-DOCUMENT";
String namespace = "https://spdx.org/spdxdocs/" + projectName + "-" + version +
"-" + UUID.randomUUID().toString();
SpdxDocument document = new SpdxDocument(documentId, projectName, namespace);
// Add creation info
document.addCreator("Organization: " + System.getenv().getOrDefault("COMPANY_NAME", "Unknown"));
document.addCreator("Person: " + System.getenv().getOrDefault("USER", "Unknown"));
// Parse Maven dependencies and add as packages
List<MavenDependency> dependencies = parseMavenDependencies(pomFile);
for (MavenDependency dep : dependencies) {
SpdxPackage pkg = createPackageFromMavenDependency(dep);
document.addPackage(pkg);
// Add relationship
SpdxRelationship relationship = new SpdxRelationship(
documentId,
"DESCRIBES",
pkg.getSpdxId()
);
document.addRelationship(relationship);
}
// Add root package
SpdxPackage rootPackage = createRootPackage(projectName, version, pomFile);
document.addPackage(rootPackage);
return document;
}
public SpdxDocument generateFromJarFile(File jarFile) throws IOException {
String projectName = jarFile.getName().replace(".jar", "");
String documentId = "SPDXRef-DOCUMENT";
String namespace = "https://spdx.org/spdxdocs/" + projectName +
"-" + UUID.randomUUID().toString();
SpdxDocument document = new SpdxDocument(documentId, projectName, namespace);
// Analyze JAR file
try (JarFile jar = new JarFile(jarFile)) {
// Add JAR as main package
SpdxPackage mainPackage = createPackageFromJar(jarFile);
document.addPackage(mainPackage);
// Analyze contained libraries
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().endsWith(".jar") && entry.getName().contains("lib/")) {
// This is an embedded JAR - extract and analyze
analyzeEmbeddedJar(jar, entry, document);
}
}
}
return document;
}
private SpdxPackage createPackageFromMavenDependency(MavenDependency dependency) {
String spdxId = "SPDXRef-" + dependency.getGroupId() + "." +
dependency.getArtifactId() + "." + dependency.getVersion();
SpdxPackage pkg = new SpdxPackage(spdxId, dependency.getArtifactId(), dependency.getVersion());
// Set download location (Maven Central)
pkg.setDownloadLocation("pkg:maven/" + dependency.getGroupId() + "/" +
dependency.getArtifactId() + "@" + dependency.getVersion());
// Set supplier
pkg.setSupplier("NOASSERTION");
// Resolve license
String license = licenseResolver.resolveLicense(
dependency.getGroupId(),
dependency.getArtifactId(),
dependency.getVersion()
);
pkg.setLicenseDeclared(license);
pkg.setLicenseConcluded(license);
// Add checksums
pkg.addChecksum(new SpdxChecksum("SHA1", dependency.getSha1()));
pkg.addChecksum(new SpdxChecksum("MD5", dependency.getMd5()));
// Set external reference (PURL)
pkg.setExternalRef("purl pkg:maven/" + dependency.getGroupId() + "/" +
dependency.getArtifactId() + "@" + dependency.getVersion());
return pkg;
}
private SpdxPackage createRootPackage(String projectName, String version, File source)
throws IOException {
String spdxId = "SPDXRef-" + projectName.replaceAll("[^a-zA-Z0-9]", "-") + "-" + version;
SpdxPackage rootPackage = new SpdxPackage(spdxId, projectName, version);
rootPackage.setDownloadLocation("NOASSERTION");
rootPackage.setSupplier("Organization: " + System.getenv().getOrDefault("COMPANY_NAME", "Unknown"));
rootPackage.setFilesAnalyzed(false);
// Add source file checksum if available
if (source != null && source.exists()) {
String sha1 = calculateChecksum(source, "SHA-1");
rootPackage.addChecksum(new SpdxChecksum("SHA1", sha1));
}
return rootPackage;
}
private SpdxPackage createPackageFromJar(File jarFile) throws IOException {
String fileName = jarFile.getName();
String name = fileName.replace(".jar", "");
String spdxId = "SPDXRef-" + name;
SpdxPackage pkg = new SpdxPackage(spdxId, name, "1.0.0"); // Version would need to be extracted
pkg.setFileName(fileName);
pkg.setDownloadLocation("file://" + jarFile.getAbsolutePath());
pkg.setFilesAnalyzed(true);
// Calculate checksums
String sha1 = calculateChecksum(jarFile, "SHA-1");
String sha256 = calculateChecksum(jarFile, "SHA-256");
pkg.addChecksum(new SpdxChecksum("SHA1", sha1));
pkg.addChecksum(new SpdxChecksum("SHA256", sha256));
return pkg;
}
private void analyzeEmbeddedJar(JarFile parentJar, JarEntry embeddedEntry, SpdxDocument document) {
// Implementation for analyzing embedded JAR files
// This would extract the embedded JAR and analyze its dependencies
}
private String calculateChecksum(File file, String algorithm) throws IOException {
try {
MessageDigest digest = MessageDigest.getInstance(algorithm);
byte[] fileBytes = Files.readAllBytes(file.toPath());
byte[] hash = digest.digest(fileBytes);
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
hexString.append(String.format("%02x", b));
}
return hexString.toString();
} catch (Exception e) {
throw new IOException("Failed to calculate checksum", e);
}
}
public void exportToJson(SpdxDocument document, File outputFile) throws IOException {
jsonMapper.writeValue(outputFile, document);
}
public void exportToXml(SpdxDocument document, File outputFile) throws IOException {
xmlMapper.writeValue(outputFile, document);
}
public void exportToTagValue(SpdxDocument document, File outputFile) throws IOException {
List<String> lines = new ArrayList<>();
// Document header
lines.add("SPDXVersion: " + document.getSpdxVersion());
lines.add("DataLicense: " + document.getDataLicense());
lines.add("SPDXID: " + document.getSpdxId());
lines.add("DocumentName: " + document.getName());
lines.add("DocumentNamespace: " + document.getDocumentNamespace());
// Creation info
lines.add("Creator: " + String.join(", ", document.getCreators()));
lines.add("Created: " + document.getCreated());
// Packages
for (SpdxPackage pkg : document.getPackages()) {
lines.add("");
lines.add("PackageName: " + pkg.getName());
lines.add("SPDXID: " + pkg.getSpdxId());
lines.add("PackageVersion: " + pkg.getVersion());
lines.add("PackageDownloadLocation: " + pkg.getDownloadLocation());
lines.add("FilesAnalyzed: " + pkg.isFilesAnalyzed());
if (pkg.getLicenseDeclared() != null) {
lines.add("LicenseDeclared: " + pkg.getLicenseDeclared());
}
// Checksums
for (SpdxChecksum checksum : pkg.getChecksums()) {
lines.add("PackageChecksum: " + checksum.getAlgorithm() + " " + checksum.getValue());
}
}
// Relationships
for (SpdxRelationship rel : document.getRelationships()) {
lines.add("Relationship: " + rel.getElementId() + " " +
rel.getRelationshipType() + " " + rel.getRelatedElementId());
}
Files.write(outputFile.toPath(), lines);
}
}
4. Maven Integration
Maven Plugin Configuration:
<!-- CycloneDX Maven Plugin -->
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.7.9</version>
<configuration>
<projectType>library</projectType>
<schemaVersion>1.4</schemaVersion>
<includeBomSerialNumber>true</includeBomSerialNumber>
<includeCompileScope>true</includeCompileScope>
<includeProvidedScope>true</includeProvidedScope>
<includeRuntimeScope>true</includeRuntimeScope>
<includeSystemScope>true</includeSystemScope>
<includeTestScope>false</includeTestScope>
<outputFormat>all</outputFormat>
<outputName>bom</outputName>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Custom SBOM Generation Plugin -->
<plugin>
<groupId>com.example</groupId>
<artifactId>sbom-maven-plugin</artifactId>
<version>1.0.0</version>
<configuration>
<outputDirectory>${project.build.directory}/sbom</outputDirectory>
<formats>json,xml,tag</formats>
<includeDependencies>true</includeDependencies>
<includePlugins>false</includePlugins>
</configuration>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
Maven Commands:
# Generate CycloneDX BOM mvn cyclonedx:makeBom # Generate with specific format mvn cyclonedx:makeBom -DoutputFormat=json # Generate aggregate BOM for multi-module project mvn cyclonedx:makeAggregateBom # Generate SPDX SBOM mvn sbom:generate
5. Supporting Classes
License Resolver:
package com.example.sbom.generator;
import java.util.HashMap;
import java.util.Map;
public class LicenseResolver {
private final Map<String, String> licenseCache = new HashMap<>();
private final Map<String, String> commonLicenses = new HashMap<>();
public LicenseResolver() {
initializeCommonLicenses();
}
private void initializeCommonLicenses() {
commonLicenses.put("apache-2.0", "Apache-2.0");
commonLicenses.put("apache2", "Apache-2.0");
commonLicenses.put("mit", "MIT");
commonLicenses.put("gpl-2.0", "GPL-2.0-only");
commonLicenses.put("gpl-3.0", "GPL-3.0-only");
commonLicenses.put("lgpl-2.1", "LGPL-2.1-only");
commonLicenses.put("lgpl-3.0", "LGPL-3.0-only");
commonLicenses.put("bsd-2-clause", "BSD-2-Clause");
commonLicenses.put("bsd-3-clause", "BSD-3-Clause");
commonLicenses.put("epl-1.0", "EPL-1.0");
commonLicenses.put("epl-2.0", "EPL-2.0");
commonLicenses.put("mpl-2.0", "MPL-2.0");
}
public String resolveLicense(String groupId, String artifactId, String version) {
String cacheKey = groupId + ":" + artifactId + ":" + version;
if (licenseCache.containsKey(cacheKey)) {
return licenseCache.get(cacheKey);
}
// In a real implementation, this would query:
// 1. Maven Central metadata
// 2. Local repository POM files
// 3. SPDX license list
// 4. ClearlyDefined API
String license = guessLicenseFromPatterns(groupId, artifactId);
licenseCache.put(cacheKey, license);
return license;
}
private String guessLicenseFromPatterns(String groupId, String artifactId) {
// Simple heuristic-based license guessing
// In production, use proper license detection
if (groupId.contains("apache") || artifactId.contains("apache")) {
return "Apache-2.0";
} else if (groupId.contains("spring")) {
return "Apache-2.0";
} else if (groupId.startsWith("com.fasterxml")) {
return "Apache-2.0";
} else if (groupId.startsWith("org.junit")) {
return "EPL-1.0";
} else {
return "NOASSERTION";
}
}
public String normalizeLicense(String rawLicense) {
if (rawLicense == null || rawLicense.trim().isEmpty()) {
return "NOASSERTION";
}
String lowerLicense = rawLicense.toLowerCase();
for (Map.Entry<String, String> entry : commonLicenses.entrySet()) {
if (lowerLicense.contains(entry.getKey())) {
return entry.getValue();
}
}
return rawLicense;
}
}
Maven Dependency Model:
package com.example.sbom.model;
public class MavenDependency {
private final String groupId;
private final String artifactId;
private final String version;
private final String scope;
private final String type;
private String sha1;
private String md5;
private String license;
public MavenDependency(String groupId, String artifactId, String version, String scope, String type) {
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
this.scope = scope;
this.type = type;
}
// Getters and setters
public String getGroupId() { return groupId; }
public String getArtifactId() { return artifactId; }
public String getVersion() { return version; }
public String getScope() { return scope; }
public String getType() { return type; }
public String getSha1() { return sha1; }
public String getMd5() { return md5; }
public String getLicense() { return license; }
public void setSha1(String sha1) { this.sha1 = sha1; }
public void setMd5(String md5) { this.md5 = md5; }
public void setLicense(String license) { this.license = license; }
public String getPurl() {
return "pkg:maven/" + groupId + "/" + artifactId + "@" + version;
}
@Override
public String toString() {
return groupId + ":" + artifactId + ":" + version;
}
}
6. Advanced SBOM Features
Vulnerability Integration:
package com.example.sbom.vulnerability;
import com.example.sbom.spdx.SpdxDocument;
import com.example.sbom.spdx.SpdxPackage;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
public class VulnerabilityEnricher {
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
public VulnerabilityEnricher() {
this.httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
this.objectMapper = new ObjectMapper();
}
public void enrichWithVulnerabilities(SpdxDocument document) {
for (SpdxPackage pkg : document.getPackages()) {
List<SecurityAdvisory> vulnerabilities = fetchVulnerabilities(pkg);
if (!vulnerabilities.isEmpty()) {
addSecurityAnnotations(pkg, vulnerabilities);
}
}
}
private List<SecurityAdvisory> fetchVulnerabilities(SpdxPackage pkg) {
List<SecurityAdvisory> advisories = new ArrayList<>();
try {
// Query OSV database
String purl = extractPurl(pkg);
if (purl != null) {
advisories.addAll(queryOsvDatabase(purl));
}
// Query NVD API
advisories.addAll(queryNvdApi(pkg.getName(), pkg.getVersion()));
} catch (Exception e) {
System.err.println("Failed to fetch vulnerabilities for " + pkg.getName() + ": " + e.getMessage());
}
return advisories;
}
private List<SecurityAdvisory> queryOsvDatabase(String purl) throws IOException, InterruptedException {
String requestBody = """
{
"package": {
"purl": "%s"
}
}
""".formatted(purl);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.osv.dev/v1/query"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
return parseOsvResponse(response.body());
}
return new ArrayList<>();
}
private List<SecurityAdvisory> parseOsvResponse(String responseBody) throws IOException {
List<SecurityAdvisory> advisories = new ArrayList<>();
JsonNode root = objectMapper.readTree(responseBody);
if (root.has("vulns")) {
for (JsonNode vuln : root.get("vulns")) {
SecurityAdvisory advisory = new SecurityAdvisory();
advisory.setId(vuln.get("id").asText());
advisory.setSummary(vuln.get("summary").asText());
advisory.setSeverity(extractSeverity(vuln));
advisories.add(advisory);
}
}
return advisories;
}
private List<SecurityAdvisory> queryNvdApi(String packageName, String version) {
// Implementation for NVD API query
return new ArrayList<>();
}
private String extractPurl(SpdxPackage pkg) {
if (pkg.getExternalRef() != null && pkg.getExternalRef().contains("pkg:")) {
return pkg.getExternalRef().substring(pkg.getExternalRef().indexOf("pkg:"));
}
return null;
}
private String extractSeverity(JsonNode vuln) {
if (vuln.has("severity")) {
return vuln.get("severity").asText();
}
return "UNKNOWN";
}
private void addSecurityAnnotations(SpdxPackage pkg, List<SecurityAdvisory> vulnerabilities) {
StringBuilder annotation = new StringBuilder();
annotation.append("Security Vulnerabilities:\n");
for (SecurityAdvisory advisory : vulnerabilities) {
annotation.append(String.format("- %s: %s (Severity: %s)\n",
advisory.getId(), advisory.getSummary(), advisory.getSeverity()));
}
pkg.setComment(annotation.toString());
}
}
class SecurityAdvisory {
private String id;
private String summary;
private String severity;
private String cvssScore;
// Getters and setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getSummary() { return summary; }
public void setSummary(String summary) { this.summary = summary; }
public String getSeverity() { return severity; }
public void setSeverity(String severity) { this.severity = severity; }
public String getCvssScore() { return cvssScore; }
public void setCvssScore(String cvssScore) { this.cvssScore = cvssScore; }
}
7. CI/CD Integration
Jenkins Pipeline:
// Jenkinsfile
pipeline {
agent any
tools {
maven 'Maven-3.8'
jdk 'Java-11'
}
stages {
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Generate SBOM') {
steps {
script {
// Generate CycloneDX BOM
sh 'mvn cyclonedx:makeBom -DoutputFormat=json'
// Generate SPDX SBOM
sh 'mvn sbom:generate'
// Convert between formats if needed
sh 'java -jar sbom-tool.jar convert --input bom.json --output spdx.json --format spdx'
}
}
post {
always {
// Archive SBOM files
archiveArtifacts artifacts: 'target/*.json, target/sbom/*', fingerprint: true
// Publish SBOM
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'target',
reportFiles: 'bom.json',
reportName: 'Software Bill of Materials'
])
}
}
}
stage('SBOM Validation') {
steps {
script {
// Validate SBOM
sh 'java -jar sbom-validator.jar --file target/bom.json'
// Check for policy compliance
def compliance = checkSbomCompliance('target/bom.json')
if (!compliance.passed) {
error "SBOM compliance check failed: ${compliance.violations}"
}
}
}
}
stage('SBOM Signing') {
steps {
script {
// Sign SBOM for authenticity
sh 'gpg --armor --detach-sign --output target/bom.json.asc target/bom.json'
}
}
}
}
}
def checkSbomCompliance(sbomFile) {
// Implement SBOM compliance checks
// - All dependencies have licenses
// - No banned licenses
// - All components have versions
// - etc.
return [passed: true, violations: []]
}
GitHub Actions Workflow:
# .github/workflows/sbom-generation.yml
name: Generate SBOM
on:
push:
branches: [ main, develop ]
release:
types: [ published ]
workflow_dispatch:
jobs:
generate-sbom:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Generate SPDX SBOM
run: |
mvn com.example:sbom-maven-plugin:generate
cp target/sbom/*.json sbom.spdx.json
- name: Generate CycloneDX BOM
run: |
mvn org.cyclonedx:cyclonedx-maven-plugin:makeBom
cp target/bom.json bom.cyclonedx.json
- name: Upload SBOM artifacts
uses: actions/upload-artifact@v3
with:
name: sbom-files
path: |
*.json
target/sbom/
retention-days: 30
- name: Upload to Dependency Track
run: |
curl -X "POST" "https://deptrack.example.com/api/v1/bom" \
-H "Content-Type: multipart/form-data" \
-H "X-API-Key: ${{ secrets.DEPENDENCY_TRACK_API_KEY }}" \
-F "project=${{ github.event.repository.name }}" \
-F "[email protected]"
8. SBOM Validation and Compliance
SBOM Validator:
package com.example.sbom.validation;
import com.example.sbom.spdx.SpdxDocument;
import com.example.sbom.spdx.SpdxPackage;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class SbomValidator {
private final List<ValidationRule> rules;
public SbomValidator() {
this.rules = new ArrayList<>();
initializeDefaultRules();
}
private void initializeDefaultRules() {
// SPDX specification rules
rules.add(new RequiredFieldsRule());
rules.add(new SpdxIdFormatRule());
rules.add(new LicenseFormatRule());
// Organizational policies
rules.add(new BannedLicensesRule());
rules.add(new MissingLicenseRule());
rules.add(new VersionFormatRule());
}
public ValidationResult validate(SpdxDocument document) {
ValidationResult result = new ValidationResult();
for (ValidationRule rule : rules) {
try {
rule.validate(document, result);
} catch (Exception e) {
result.addError("Rule " + rule.getName() + " failed: " + e.getMessage());
}
}
return result;
}
public void addRule(ValidationRule rule) {
rules.add(rule);
}
}
class ValidationResult {
private final List<String> errors = new ArrayList<>();
private final List<String> warnings = new ArrayList<>();
public void addError(String error) {
errors.add(error);
}
public void addWarning(String warning) {
warnings.add(warning);
}
public boolean isValid() {
return errors.isEmpty();
}
public List<String> getErrors() { return errors; }
public List<String> getWarnings() { return warnings; }
}
interface ValidationRule {
String getName();
void validate(SpdxDocument document, ValidationResult result);
}
class RequiredFieldsRule implements ValidationRule {
@Override
public String getName() { return "RequiredFieldsRule"; }
@Override
public void validate(SpdxDocument document, ValidationResult result) {
if (document.getName() == null || document.getName().trim().isEmpty()) {
result.addError("Document name is required");
}
if (document.getDocumentNamespace() == null || document.getDocumentNamespace().trim().isEmpty()) {
result.addError("Document namespace is required");
}
for (SpdxPackage pkg : document.getPackages()) {
if (pkg.getName() == null || pkg.getName().trim().isEmpty()) {
result.addError("Package name is required for " + pkg.getSpdxId());
}
if (pkg.getDownloadLocation() == null || pkg.getDownloadLocation().trim().isEmpty()) {
result.addError("Download location is required for " + pkg.getSpdxId());
}
}
}
}
class BannedLicensesRule implements ValidationRule {
private final List<String> bannedLicenses = List.of(
"GPL-1.0-only", "GPL-1.0-or-later", "AGPL-1.0-only"
);
@Override
public String getName() { return "BannedLicensesRule"; }
@Override
public void validate(SpdxDocument document, ValidationResult result) {
for (SpdxPackage pkg : document.getPackages()) {
String license = pkg.getLicenseDeclared();
if (license != null && bannedLicenses.contains(license)) {
result.addError("Banned license " + license + " found in " + pkg.getSpdxId());
}
}
}
}
class SpdxIdFormatRule implements ValidationRule {
private final Pattern spdxIdPattern = Pattern.compile("^SPDXRef-[a-zA-Z0-9.-]+$");
@Override
public String getName() { return "SpdxIdFormatRule"; }
@Override
public void validate(SpdxDocument document, ValidationResult result) {
if (!spdxIdPattern.matcher(document.getSpdxId()).matches()) {
result.addError("Invalid SPDX ID format: " + document.getSpdxId());
}
for (SpdxPackage pkg : document.getPackages()) {
if (!spdxIdPattern.matcher(pkg.getSpdxId()).matches()) {
result.addError("Invalid SPDX ID format: " + pkg.getSpdxId());
}
}
}
}
9. Usage Examples
Basic SBOM Generation:
package com.example.sbom;
import com.example.sbom.generator.SpdxSbomGenerator;
import com.example.sbom.spdx.SpdxDocument;
import java.io.File;
public class SbomExample {
public static void main(String[] args) {
try {
SpdxSbomGenerator generator = new SpdxSbomGenerator();
// Generate from Maven project
File pomFile = new File("pom.xml");
SpdxDocument document = generator.generateFromMavenProject(
pomFile, "My Application", "1.0.0");
// Export to multiple formats
generator.exportToJson(document, new File("sbom.json"));
generator.exportToXml(document, new File("sbom.xml"));
generator.exportToTagValue(document, new File("sbom.tag"));
System.out.println("SBOM generated successfully");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Spring Boot Integration:
package com.example.sbom.config;
import com.example.sbom.generator.SpdxSbomGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SbomConfig {
@Bean
public SpdxSbomGenerator sbomGenerator() {
return new SpdxSbomGenerator();
}
@Bean
public SbomService sbomService(SpdxSbomGenerator generator) {
return new SbomService(generator);
}
}
@Service
class SbomService {
private final SpdxSbomGenerator generator;
public SbomService(SpdxSbomGenerator generator) {
this.generator = generator;
}
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
try {
// Generate SBOM on startup
File pomFile = new File("pom.xml");
SpdxDocument document = generator.generateFromMavenProject(
pomFile, "My Spring Boot App", "2.0.0");
generator.exportToJson(document, new File("sbom.json"));
} catch (Exception e) {
System.err.println("Failed to generate SBOM: " + e.getMessage());
}
}
@GetMapping("/api/sbom")
public ResponseEntity<String> getSbom() {
try {
File sbomFile = new File("sbom.json");
if (sbomFile.exists()) {
String sbomContent = new String(Files.readAllBytes(sbomFile.toPath()));
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(sbomContent);
} else {
return ResponseEntity.notFound().build();
}
} catch (IOException e) {
return ResponseEntity.internalServerError().build();
}
}
}
10. Best Practices
SBOM Management Strategy:
// SBOM Management Service
package com.example.sbom.management;
import java.util.*;
public class SbomManagementService {
private final SbomStorage storage;
private final SbomValidator validator;
private final VulnerabilityEnricher vulnerabilityEnricher;
public void processNewBuild(String projectName, String version, File buildArtifact) {
// Generate SBOM
SpdxDocument sbom = generateSbom(projectName, version, buildArtifact);
// Validate
ValidationResult validation = validator.validate(sbom);
if (!validation.isValid()) {
throw new SbomValidationException("SBOM validation failed", validation);
}
// Enrich with vulnerabilities
vulnerabilityEnricher.enrichWithVulnerabilities(sbom);
// Store
storage.storeSbom(projectName, version, sbom);
// Notify stakeholders
notifyStakeholders(projectName, version, sbom);
}
public SbomDiff compareSboms(String projectName, String version1, String version2) {
SpdxDocument sbom1 = storage.retrieveSbom(projectName, version1);
SpdxDocument sbom2 = storage.retrieveSbom(projectName, version2);
return SbomComparator.compare(sbom1, sbom2);
}
}
Conclusion
SPDX SBOM generation in Java provides:
- Compliance: Meet regulatory and security requirements
- Transparency: Clear visibility into software components
- Security: Vulnerability tracking and management
- Automation: CI/CD integration for continuous SBOM generation
- Standardization: Industry-standard format for interoperability
Key Benefits:
- Supply chain security management
- License compliance tracking
- Vulnerability impact analysis
- Software provenance verification
- Regulatory compliance (NTIA, CISA, etc.)
By implementing comprehensive SPDX SBOM generation, organizations can significantly improve their software supply chain security and meet evolving regulatory requirements.
Secure Java Supply Chain, Minimal Containers & Runtime Security (Alpine, Distroless, Signing, SBOM & Kubernetes Controls)
https://macronepal.com/blog/alpine-linux-security-in-java-complete-guide/
Explains how Alpine Linux is used as a lightweight base for Java containers to reduce image size and attack surface, while discussing tradeoffs like musl compatibility, CVE handling, and additional hardening requirements for production security.
https://macronepal.com/blog/the-minimalists-approach-building-ultra-secure-java-applications-with-scratch-base-images/
Explains using scratch base images for Java applications to create extremely minimal containers with almost zero attack surface, where only the compiled Java application and runtime dependencies exist.
https://macronepal.com/blog/distroless-containers-in-java-minimal-secure-containers-for-jvm-applications/
Explains distroless Java containers that remove shells, package managers, and unnecessary OS tools, significantly reducing vulnerabilities while improving security posture for JVM workloads.
https://macronepal.com/blog/revolutionizing-container-security-implementing-chainguard-images-for-java-applications/
Explains Chainguard images for Java, which are secure-by-default, CVE-minimized container images with SBOMs and cryptographic signing, designed for modern supply-chain security.
https://macronepal.com/blog/seccomp-filtering-in-java-comprehensive-security-sandboxing/
Explains seccomp syscall filtering in Linux to restrict what system calls Java applications can make, reducing the impact of exploits by limiting kernel-level access.
https://macronepal.com/blog/in-toto-attestations-in-java/
Explains in-toto framework integration in Java to create cryptographically verifiable attestations across the software supply chain, ensuring every build step is trusted and auditable.
https://macronepal.com/blog/fulcio-integration-in-java-code-signing-certificate-infrastructure/
Explains Fulcio integration for Java, which issues short-lived certificates for code signing in a zero-trust supply chain, enabling secure identity-based signing of artifacts.
https://macronepal.com/blog/tekton-supply-chain-in-java-comprehensive-ci-cd-pipeline-implementation/
Explains using Tekton CI/CD pipelines for Java applications to automate secure builds, testing, signing, and deployment with supply-chain security controls built in.
https://macronepal.com/blog/slsa-provenance-in-java-complete-guide-to-supply-chain-security-2/
Explains SLSA (Supply-chain Levels for Software Artifacts) provenance in Java builds, ensuring traceability of how software is built, from source code to final container image.
https://macronepal.com/blog/notary-project-in-java-complete-implementation-guide/
Explains the Notary Project for Java container security, enabling cryptographic signing and verification of container images and artifacts to prevent tampering in deployment pipelines.