Introduction to SPDX Software Bill of Materials
SPDX (Software Package Data Exchange) is an open standard for communicating software bill of materials information. This implementation provides comprehensive SPDX SBOM generation and parsing capabilities in Java.
Maven Dependencies
<properties>
<spdx.version>1.1.6</spdx.version>
<jackson.version>2.15.2</jackson.version>
<cyclonedx.version>8.0.3</cyclonedx.version>
</properties>
<dependencies>
<!-- SPDX Java Tools -->
<dependency>
<groupId>org.spdx</groupId>
<artifactId>spdx-java-library</artifactId>
<version>${spdx.version}</version>
</dependency>
<!-- SPDX Jackson Support -->
<dependency>
<groupId>org.spdx</groupId>
<artifactId>spdx-jackson-store</artifactId>
<version>${spdx.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- CycloneDX for conversion -->
<dependency>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-core-java</artifactId>
<version>${cyclonedx.version}</version>
</dependency>
<!-- Maven Integration -->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>3.9.4</version>
<optional>true</optional>
</dependency>
</dependencies>
Core SPDX Models
SPDX Document Builder
@Component
public class SpdxDocumentBuilder {
private static final String SPDX_VERSION = "SPDX-2.3";
private static final String DATA_LICENSE = "CC0-1.0";
public SpdxDocument createSpdxDocument(String name, String namespace, String creator)
throws InvalidSPDXAnalysisException {
// Create document container
SpdxDocument document = new SpdxDocument(
name,
"", // comment
new ArrayList<>(), // annotations
new ArrayList<>(), // relationships
null, // specVersion
null, // dataLicense
null, // externalDocumentRefs
null, // extractedLicenseInfos
namespace,
null, // creationInfo
null, // documentDescribes
name
);
// Set SPDX version
document.setSpecVersion(SPDX_VERSION);
// Set data license
document.setDataLicense(createDataLicense());
// Set creation info
document.setCreationInfo(createCreationInfo(creator));
return document;
}
private AnyLicenseInfo createDataLicense() throws InvalidSPDXAnalysisException {
return new SpdxListedLicense(DATA_LICENSE);
}
private SpdxCreatorInformation createCreationInfo(String creator) {
List<String> creators = new ArrayList<>();
creators.add(creator);
return new SpdxCreatorInformation(
creators,
Instant.now().toString(),
null, // comment
null // licenseListVersion
);
}
public SpdxPackage createPackage(SpdxDocument document, String name, String version,
String downloadUrl, String licenseDeclared)
throws InvalidSPDXAnalysisException {
SpdxPackage pkg = new SpdxPackage(
document.getModelStore(),
document.getDocumentUri(),
generateSpdxId("Package"),
document.getCopyManager(),
true
);
// Basic package information
pkg.setName(name);
pkg.setVersionInfo(version);
pkg.setDownloadLocation(downloadUrl);
// License information
pkg.setLicenseDeclared(parseLicense(licenseDeclared));
// Package verification
pkg.setChecksums(createChecksums(downloadUrl));
// External references
pkg.setExternalRefs(createExternalRefs(name, version));
return pkg;
}
private List<Checksum> createChecksums(String downloadUrl) {
List<Checksum> checksums = new ArrayList<>();
try {
// Calculate SHA1 checksum (in practice, you'd compute this from actual content)
String sha1 = calculateSha1(downloadUrl);
checksums.add(new Checksum(ChecksumAlgorithm.SHA1, sha1));
// Calculate SHA256 checksum
String sha256 = calculateSha256(downloadUrl);
checksums.add(new Checksum(ChecksumAlgorithm.SHA256, sha256));
} catch (Exception e) {
// Log warning but continue
System.err.println("Could not calculate checksums for: " + downloadUrl);
}
return checksums;
}
private List<ExternalRef> createExternalRefs(String name, String version) {
List<ExternalRef> refs = new ArrayList<>();
// PURL (Package URL) reference
String purl = generatePurl(name, version);
refs.add(new ExternalRef(
ReferenceCategory.PACKAGE_MANAGER,
new ReferenceType("http://spdx.org/rdf/references/purl"),
purl,
"Package URL identifier"
));
// Maven Central reference
if (isMavenPackage(name)) {
String mavenRef = generateMavenReference(name, version);
refs.add(new ExternalRef(
ReferenceCategory.PACKAGE_MANAGER,
new ReferenceType("http://spdx.org/rdf/references/maven"),
mavenRef,
"Maven Central coordinates"
));
}
return refs;
}
private AnyLicenseInfo parseLicense(String licenseExpression) throws InvalidSPDXAnalysisException {
if (licenseExpression == null || licenseExpression.trim().isEmpty()) {
return new SpdxNoAssertionLicense();
}
try {
return LicenseInfoFactory.parseSPDXLicenseString(licenseExpression);
} catch (InvalidLicenseStringException e) {
// Fall back to non-standard license
return new ExtractedLicenseInfo(generateSpdxId("License"), licenseExpression);
}
}
private String generateSpdxId(String prefix) {
return "SPDXRef-" + prefix + "-" + UUID.randomUUID().toString().substring(0, 8);
}
private String generatePurl(String name, String version) {
return "pkg:maven/" + name.replace(':', '/') + "@" + version;
}
private String generateMavenReference(String name, String version) {
String[] parts = name.split(":");
if (parts.length == 2) {
return String.format("maven:%s:%s:%s", parts[0], parts[1], version);
}
return name + ":" + version;
}
private boolean isMavenPackage(String name) {
return name.contains(":");
}
// These would be implemented to actually compute checksums
private String calculateSha1(String url) { return "sha1-placeholder"; }
private String calculateSha256(String url) { return "sha256-placeholder"; }
}
SBOM Component Models
public class SbomComponent {
private String groupId;
private String artifactId;
private String version;
private String type; // jar, war, pom, etc.
private String scope; // compile, runtime, test, provided, system
private String license;
private String purl;
private String description;
private List<SbomComponent> dependencies;
private Map<String, String> properties;
private List<SbomVulnerability> vulnerabilities;
public SbomComponent() {
this.dependencies = new ArrayList<>();
this.properties = new HashMap<>();
this.vulnerabilities = new ArrayList<>();
}
public SbomComponent(String groupId, String artifactId, String version) {
this();
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
this.purl = generatePurl();
}
private String generatePurl() {
return String.format("pkg:maven/%s/%s@%s",
groupId, artifactId, version);
}
// Getters and setters
public String getGroupId() { return groupId; }
public void setGroupId(String groupId) { this.groupId = groupId; }
public String getArtifactId() { return artifactId; }
public void setArtifactId(String artifactId) { this.artifactId = artifactId; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getScope() { return scope; }
public void setScope(String scope) { this.scope = scope; }
public String getLicense() { return license; }
public void setLicense(String license) { this.license = license; }
public String getPurl() { return purl; }
public void setPurl(String purl) { this.purl = purl; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public List<SbomComponent> getDependencies() { return dependencies; }
public void setDependencies(List<SbomComponent> dependencies) { this.dependencies = dependencies; }
public Map<String, String> getProperties() { return properties; }
public void setProperties(Map<String, String> properties) { this.properties = properties; }
public List<SbomVulnerability> getVulnerabilities() { return vulnerabilities; }
public void setVulnerabilities(List<SbomVulnerability> vulnerabilities) {
this.vulnerabilities = vulnerabilities;
}
public void addDependency(SbomComponent dependency) {
this.dependencies.add(dependency);
}
public void addProperty(String key, String value) {
this.properties.put(key, value);
}
public void addVulnerability(SbomVulnerability vulnerability) {
this.vulnerabilities.add(vulnerability);
}
}
public class SbomVulnerability {
private String id; // CVE, GHSA, etc.
private String source; // NVD, GitHub, etc.
private String severity; // CRITICAL, HIGH, MEDIUM, LOW
private String description;
private List<String> affectedVersions;
private String patchedVersion;
private String referenceUrl;
private Instant publishedDate;
// Getters and setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public String getSeverity() { return severity; }
public void setSeverity(String severity) { this.severity = severity; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public List<String> getAffectedVersions() { return affectedVersions; }
public void setAffectedVersions(List<String> affectedVersions) { this.affectedVersions = affectedVersions; }
public String getPatchedVersion() { return patchedVersion; }
public void setPatchedVersion(String patchedVersion) { this.patchedVersion = patchedVersion; }
public String getReferenceUrl() { return referenceUrl; }
public void setReferenceUrl(String referenceUrl) { this.referenceUrl = referenceUrl; }
public Instant getPublishedDate() { return publishedDate; }
public void setPublishedDate(Instant publishedDate) { this.publishedDate = publishedDate; }
}
public class SbomMetadata {
private String name;
private String version;
private String description;
private String supplier;
private String author;
private String license;
private Instant created;
private List<String> authors;
private Map<String, String> properties;
public SbomMetadata() {
this.authors = new ArrayList<>();
this.properties = new HashMap<>();
this.created = Instant.now();
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getSupplier() { return supplier; }
public void setSupplier(String supplier) { this.supplier = supplier; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public String getLicense() { return license; }
public void setLicense(String license) { this.license = license; }
public Instant getCreated() { return created; }
public void setCreated(Instant created) { this.created = created; }
public List<String> getAuthors() { return authors; }
public void setAuthors(List<String> authors) { this.authors = authors; }
public Map<String, String> getProperties() { return properties; }
public void setProperties(Map<String, String> properties) { this.properties = properties; }
}
Maven Project SBOM Generator
@Component
public class MavenSbomGenerator {
private final SpdxDocumentBuilder spdxBuilder;
private final ObjectMapper objectMapper;
public MavenSbomGenerator(SpdxDocumentBuilder spdxBuilder) {
this.spdxBuilder = spdxBuilder;
this.objectMapper = new ObjectMapper();
this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
}
public SpdxDocument generateFromPom(File pomFile) throws Exception {
MavenProject project = readMavenProject(pomFile);
return generateSbom(project);
}
public SpdxDocument generateFromDependencies(List<Dependency> dependencies, SbomMetadata metadata)
throws Exception {
String namespace = generateNamespace(metadata);
SpdxDocument document = spdxBuilder.createSpdxDocument(
metadata.getName(),
namespace,
"Tool: MavenSBOMGenerator/1.0.0"
);
// Add root package
SpdxPackage rootPackage = createRootPackage(document, metadata);
document.getDocumentDescribes().add(rootPackage);
// Add dependency packages
for (Dependency dependency : dependencies) {
SpdxPackage depPackage = createDependencyPackage(document, dependency);
document.getDocumentDescribes().add(depPackage);
// Create relationship
Relationship relationship = new Relationship(
rootPackage,
RelationshipType.DEPENDS_ON,
depPackage,
null // comment
);
document.getRelationships().add(relationship);
}
return document;
}
private MavenProject readMavenProject(File pomFile) throws Exception {
MavenXpp3Reader reader = new MavenXpp3Reader();
try (FileReader fileReader = new FileReader(pomFile)) {
Model model = reader.read(fileReader);
return new MavenProject(model);
}
}
private SpdxDocument generateSbom(MavenProject project) throws Exception {
SbomMetadata metadata = extractMetadata(project);
List<Dependency> dependencies = extractDependencies(project);
return generateFromDependencies(dependencies, metadata);
}
private SbomMetadata extractMetadata(MavenProject project) {
SbomMetadata metadata = new SbomMetadata();
metadata.setName(project.getArtifactId());
metadata.setVersion(project.getVersion());
metadata.setDescription(project.getDescription());
if (project.getOrganization() != null) {
metadata.setSupplier(project.getOrganization().getName());
}
if (project.getLicenses() != null && !project.getLicenses().isEmpty()) {
metadata.setLicense(project.getLicenses().get(0).getName());
}
if (project.getDevelopers() != null) {
project.getDevelopers().forEach(dev ->
metadata.getAuthors().add(dev.getName()));
}
return metadata;
}
private List<Dependency> extractDependencies(MavenProject project) {
List<Dependency> dependencies = new ArrayList<>();
// Collect dependencies from all scopes
collectDependenciesFromModel(project.getModel(), dependencies);
// Collect dependency management dependencies
if (project.getDependencyManagement() != null) {
project.getDependencyManagement().getDependencies()
.forEach(dep -> dependencies.add(convertMavenDependency(dep)));
}
return dependencies;
}
private void collectDependenciesFromModel(Model model, List<Dependency> dependencies) {
if (model.getDependencies() != null) {
model.getDependencies().forEach(dep ->
dependencies.add(convertMavenDependency(dep)));
}
}
private Dependency convertMavenDependency(org.apache.maven.model.Dependency mavenDep) {
Dependency dep = new Dependency();
dep.setGroupId(mavenDep.getGroupId());
dep.setArtifactId(mavenDep.getArtifactId());
dep.setVersion(mavenDep.getVersion());
dep.setType(mavenDep.getType());
dep.setScope(mavenDep.getScope());
dep.setOptional(mavenDep.isOptional());
return dep;
}
private SpdxPackage createRootPackage(SpdxDocument document, SbomMetadata metadata)
throws InvalidSPDXAnalysisException {
String downloadUrl = generateDownloadUrl(metadata);
return spdxBuilder.createPackage(
document,
metadata.getName(),
metadata.getVersion(),
downloadUrl,
metadata.getLicense()
);
}
private SpdxPackage createDependencyPackage(SpdxDocument document, Dependency dependency)
throws InvalidSPDXAnalysisException {
String name = dependency.getGroupId() + ":" + dependency.getArtifactId();
String downloadUrl = generateMavenDownloadUrl(dependency);
// Try to determine license (this would be enhanced with actual license detection)
String license = determineLicense(dependency);
return spdxBuilder.createPackage(
document,
name,
dependency.getVersion(),
downloadUrl,
license
);
}
private String generateNamespace(SbomMetadata metadata) {
return "https://spdx.org/spdxdocs/" +
metadata.getName() + "-" +
metadata.getVersion() + "-" +
UUID.randomUUID().toString();
}
private String generateDownloadUrl(SbomMetadata metadata) {
// Generate a plausible download URL
return "https://example.com/" + metadata.getName() + "/" + metadata.getVersion();
}
private String generateMavenDownloadUrl(Dependency dependency) {
return String.format("https://repo1.maven.org/maven2/%s/%s/%s/%s-%s.jar",
dependency.getGroupId().replace('.', '/'),
dependency.getArtifactId(),
dependency.getVersion(),
dependency.getArtifactId(),
dependency.getVersion());
}
private String determineLicense(Dependency dependency) {
// In practice, this would query Maven Central or other sources
// For now, return a common license or NOASSERTION
return "NOASSERTION";
}
public void writeSpdxJson(SpdxDocument document, File outputFile) throws Exception {
// Convert to JSON using SPDX Jackson store
SpdxJacksonStore store = new SpdxJacksonStore();
String json = store.serialize(document);
Files.writeString(outputFile.toPath(), json, StandardCharsets.UTF_8);
}
public void writeSpdxTagValue(SpdxDocument document, File outputFile) throws Exception {
// Convert to tag-value format
SpdxDocumentWriter writer = new SpdxDocumentWriter();
String tagValue = writer.write(document);
Files.writeString(outputFile.toPath(), tagValue, StandardCharsets.UTF_8);
}
// Helper classes
public static class Dependency {
private String groupId;
private String artifactId;
private String version;
private String type;
private String scope;
private boolean optional;
// Getters and setters
public String getGroupId() { return groupId; }
public void setGroupId(String groupId) { this.groupId = groupId; }
public String getArtifactId() { return artifactId; }
public void setArtifactId(String artifactId) { this.artifactId = artifactId; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getScope() { return scope; }
public void setScope(String scope) { this.scope = scope; }
public boolean isOptional() { return optional; }
public void setOptional(boolean optional) { this.optional = optional; }
}
}
Vulnerability Integration
@Component
public class VulnerabilityEnricher {
private final OsvApiClient osvApiClient;
private final NvdApiClient nvdApiClient;
public VulnerabilityEnricher() {
this.osvApiClient = new OsvApiClient();
this.nvdApiClient = new NvdApiClient();
}
public List<SbomVulnerability> getVulnerabilities(SbomComponent component) {
List<SbomVulnerability> vulnerabilities = new ArrayList<>();
// Query OSV (Open Source Vulnerabilities)
vulnerabilities.addAll(queryOsv(component));
// Query NVD (National Vulnerability Database)
vulnerabilities.addAll(queryNvd(component));
return vulnerabilities;
}
private List<SbomVulnerability> queryOsv(SbomComponent component) {
try {
OsvQuery query = new OsvQuery(component.getPurl());
OsvResponse response = osvApiClient.query(query);
return response.getVulns().stream()
.map(this::convertOsvVulnerability)
.collect(Collectors.toList());
} catch (Exception e) {
System.err.println("Failed to query OSV for: " + component.getPurl());
return new ArrayList<>();
}
}
private List<SbomVulnerability> queryNvd(SbomComponent component) {
try {
// Extract CPE from PURL or component information
String cpe = generateCpe(component);
if (cpe != null) {
return nvdApiClient.searchByCpe(cpe).stream()
.map(this::convertNvdVulnerability)
.collect(Collectors.toList());
}
} catch (Exception e) {
System.err.println("Failed to query NVD for: " + component.getPurl());
}
return new ArrayList<>();
}
private SbomVulnerability convertOsvVulnerability(OsvVulnerability osvVuln) {
SbomVulnerability vuln = new SbomVulnerability();
vuln.setId(osvVuln.getId());
vuln.setSource("OSV");
vuln.setSeverity(mapSeverity(osvVuln.getSeverity()));
vuln.setDescription(osvVuln.getSummary());
vuln.setAffectedVersions(osvVuln.getAffectedVersions());
vuln.setReferenceUrl(osvVuln.getReferences().get(0));
if (osvVuln.getPublished() != null) {
vuln.setPublishedDate(Instant.parse(osvVuln.getPublished()));
}
return vuln;
}
private SbomVulnerability convertNvdVulnerability(NvdVulnerability nvdVuln) {
SbomVulnerability vuln = new SbomVulnerability();
vuln.setId(nvdVuln.getCve().getId());
vuln.setSource("NVD");
vuln.setSeverity(mapCvssSeverity(nvdVuln.getCvssScore()));
vuln.setDescription(nvdVuln.getCve().getDescription());
vuln.setReferenceUrl("https://nvd.nist.gov/vuln/detail/" + nvdVuln.getCve().getId());
if (nvdVuln.getCve().getPublished() != null) {
vuln.setPublishedDate(nvdVuln.getCve().getPublished().toInstant());
}
return vuln;
}
private String mapSeverity(String osvSeverity) {
if (osvSeverity == null) return "UNKNOWN";
switch (osvSeverity.toUpperCase()) {
case "CRITICAL": return "CRITICAL";
case "HIGH": return "HIGH";
case "MEDIUM": return "MEDIUM";
case "LOW": return "LOW";
default: return "UNKNOWN";
}
}
private String mapCvssSeverity(Double cvssScore) {
if (cvssScore == null) return "UNKNOWN";
if (cvssScore >= 9.0) return "CRITICAL";
if (cvssScore >= 7.0) return "HIGH";
if (cvssScore >= 4.0) return "MEDIUM";
if (cvssScore > 0.0) return "LOW";
return "NONE";
}
private String generateCpe(SbomComponent component) {
// Generate CPE from component information
// Format: cpe:2.3:a:vendor:product:version:*:*:*:*:*:*:*
String vendor = component.getGroupId();
String product = component.getArtifactId();
if (vendor != null && product != null) {
return String.format("cpe:2.3:a:%s:%s:%s:*:*:*:*:*:*:*",
vendor.toLowerCase(),
product.toLowerCase(),
component.getVersion());
}
return null;
}
}
// API Client implementations (simplified)
@Component
public class OsvApiClient {
private final RestTemplate restTemplate;
private final String OSV_API_URL = "https://api.osv.dev/v1/query";
public OsvApiClient() {
this.restTemplate = new RestTemplate();
}
public OsvResponse query(OsvQuery query) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<OsvQuery> request = new HttpEntity<>(query, headers);
ResponseEntity<OsvResponse> response = restTemplate.postForEntity(
OSV_API_URL, request, OsvResponse.class);
return response.getBody();
}
public static class OsvQuery {
private Map<String, Object> packageInfo;
public OsvQuery(String purl) {
this.packageInfo = Map.of("purl", purl);
}
public Map<String, Object> getPackageInfo() { return packageInfo; }
public void setPackageInfo(Map<String, Object> packageInfo) { this.packageInfo = packageInfo; }
}
public static class OsvResponse {
private List<OsvVulnerability> vulns;
public List<OsvVulnerability> getVulns() { return vulns; }
public void setVulns(List<OsvVulnerability> vulns) { this.vulns = vulns; }
}
public static class OsvVulnerability {
private String id;
private String summary;
private String severity;
private List<String> affectedVersions;
private List<String> references;
private String published;
// 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 List<String> getAffectedVersions() { return affectedVersions; }
public void setAffectedVersions(List<String> affectedVersions) { this.affectedVersions = affectedVersions; }
public List<String> getReferences() { return references; }
public void setReferences(List<String> references) { this.references = references; }
public String getPublished() { return published; }
public void setPublished(String published) { this.published = published; }
}
}
SPDX Report Generation
@Component
public class SpdxReportGenerator {
public String generateHumanReadableReport(SpdxDocument document) throws InvalidSPDXAnalysisException {
StringBuilder report = new StringBuilder();
report.append("SPDX Software Bill of Materials Report\n");
report.append("=======================================\n\n");
// Document information
report.append("Document Information:\n");
report.append(" Name: ").append(document.getName()).append("\n");
report.append(" SPDX Version: ").append(document.getSpecVersion()).append("\n");
report.append(" Data License: ").append(document.getDataLicense().toString()).append("\n");
report.append(" Namespace: ").append(document.getDocumentNamespace()).append("\n\n");
// Creation info
SpdxCreatorInformation creationInfo = document.getCreationInfo();
if (creationInfo != null) {
report.append("Created: ").append(creationInfo.getCreated()).append("\n");
report.append("Creators: ").append(String.join(", ", creationInfo.getCreators())).append("\n\n");
}
// Packages
report.append("Packages:\n");
report.append("---------\n");
for (SpdxItem item : document.getDocumentDescribes()) {
if (item instanceof SpdxPackage) {
SpdxPackage pkg = (SpdxPackage) item;
report.append(generatePackageReport(pkg)).append("\n");
}
}
// Relationships
report.append("Relationships:\n");
report.append("--------------\n");
for (Relationship relationship : document.getRelationships()) {
report.append(generateRelationshipReport(relationship)).append("\n");
}
return report.toString();
}
private String generatePackageReport(SpdxPackage pkg) throws InvalidSPDXAnalysisException {
StringBuilder packageReport = new StringBuilder();
packageReport.append("Package: ").append(pkg.getName()).append("\n");
packageReport.append(" Version: ").append(pkg.getVersionInfo()).append("\n");
packageReport.append(" Download: ").append(pkg.getDownloadLocation()).append("\n");
packageReport.append(" License: ").append(pkg.getLicenseDeclared().toString()).append("\n");
// Checksums
if (pkg.getChecksums() != null && !pkg.getChecksums().isEmpty()) {
packageReport.append(" Checksums:\n");
for (Checksum checksum : pkg.getChecksums()) {
packageReport.append(" ").append(checksum.getAlgorithm())
.append(": ").append(checksum.getValue()).append("\n");
}
}
// External references
if (pkg.getExternalRefs() != null && !pkg.getExternalRefs().isEmpty()) {
packageReport.append(" External References:\n");
for (ExternalRef ref : pkg.getExternalRefs()) {
packageReport.append(" ").append(ref.getReferenceType().toString())
.append(": ").append(ref.getReferenceLocator()).append("\n");
}
}
return packageReport.toString();
}
private String generateRelationshipReport(Relationship relationship) {
return String.format("%s %s %s",
relationship.getSpdxElement().toString(),
relationship.getRelationshipType().toString(),
relationship.getRelatedSpdxElement().toString());
}
public String generateVulnerabilityReport(List<SbomComponent> components) {
StringBuilder report = new StringBuilder();
report.append("Vulnerability Report\n");
report.append("====================\n\n");
int totalVulnerabilities = 0;
Map<String, Integer> severityCounts = new HashMap<>();
for (SbomComponent component : components) {
if (!component.getVulnerabilities().isEmpty()) {
report.append("Component: ").append(component.getGroupId())
.append(":").append(component.getArtifactId())
.append(":").append(component.getVersion()).append("\n");
for (SbomVulnerability vuln : component.getVulnerabilities()) {
report.append(" ").append(vuln.getId()).append(" [").append(vuln.getSeverity()).append("]\n");
report.append(" ").append(vuln.getDescription()).append("\n");
report.append(" Reference: ").append(vuln.getReferenceUrl()).append("\n\n");
totalVulnerabilities++;
severityCounts.merge(vuln.getSeverity(), 1, Integer::sum);
}
}
}
// Summary
report.append("Summary:\n");
report.append(" Total Vulnerabilities: ").append(totalVulnerabilities).append("\n");
severityCounts.forEach((severity, count) ->
report.append(" ").append(severity).append(": ").append(count).append("\n"));
return report.toString();
}
public void writeReportToFile(String report, Path outputPath) throws IOException {
Files.writeString(outputPath, report, StandardCharsets.UTF_8);
}
}
CI/CD Integration
GitHub Actions Workflow
name: Generate SPDX SBOM
on:
push:
branches: [ main, develop ]
release:
types: [ published ]
jobs:
generate-sbom:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build project
run: mvn compile -DskipTests
- name: Generate SPDX SBOM
run: |
java -cp target/classes com.example.SbomGenerator \
--pom pom.xml \
--output sbom.spdx.json \
--format json
- name: Scan for vulnerabilities
run: |
java -cp target/classes com.example.VulnerabilityScanner \
--sbom sbom.spdx.json \
--output vulnerabilities.json
- name: Generate human-readable report
run: |
java -cp target/classes com.example.ReportGenerator \
--sbom sbom.spdx.json \
--vulnerabilities vulnerabilities.json \
--output report.txt
- name: Upload SBOM artifacts
uses: actions/upload-artifact@v3
with:
name: sbom-artifacts
path: |
sbom.spdx.json
vulnerabilities.json
report.txt
- name: Upload SBOM to release
if: github.event_name == 'release'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: sbom.spdx.json
asset_name: sbom.spdx.json
asset_content_type: application/json
Maven Plugin
<plugin>
<groupId>com.example</groupId>
<artifactId>sbom-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<outputFormat>JSON</outputFormat>
<outputFile>${project.build.directory}/sbom/${project.artifactId}-${project.version}.spdx.json</outputFile>
<includeVulnerabilities>true</includeVulnerabilities>
<vulnerabilitySources>OSV,NVD</vulnerabilitySources>
</configuration>
</plugin>
Usage Examples
@Service
public class SbomService {
private final MavenSbomGenerator sbomGenerator;
private final VulnerabilityEnricher vulnerabilityEnricher;
private final SpdxReportGenerator reportGenerator;
public SbomService(MavenSbomGenerator sbomGenerator, VulnerabilityEnricher vulnerabilityEnricher,
SpdxReportGenerator reportGenerator) {
this.sbomGenerator = sbomGenerator;
this.vulnerabilityEnricher = vulnerabilityEnricher;
this.reportGenerator = reportGenerator;
}
public void generateCompleteSbom(File pomFile, Path outputDir) throws Exception {
// Generate SPDX document
SpdxDocument document = sbomGenerator.generateFromPom(pomFile);
// Write SPDX JSON
Path spdxJson = outputDir.resolve("sbom.spdx.json");
sbomGenerator.writeSpdxJson(document, spdxJson.toFile());
// Write SPDX Tag-Value
Path spdxTagValue = outputDir.resolve("sbom.spdx");
sbomGenerator.writeSpdxTagValue(document, spdxTagValue.toFile());
// Generate components for vulnerability scanning
List<SbomComponent> components = extractComponents(document);
// Enrich with vulnerability information
components.parallelStream().forEach(component -> {
List<SbomVulnerability> vulnerabilities = vulnerabilityEnricher.getVulnerabilities(component);
component.setVulnerabilities(vulnerabilities);
});
// Generate reports
String humanReport = reportGenerator.generateHumanReadableReport(document);
Path reportPath = outputDir.resolve("sbom-report.txt");
reportGenerator.writeReportToFile(humanReport, reportPath);
String vulnReport = reportGenerator.generateVulnerabilityReport(components);
Path vulnReportPath = outputDir.resolve("vulnerability-report.txt");
reportGenerator.writeReportToFile(vulnReport, vulnReportPath);
System.out.println("SBOM generation completed:");
System.out.println(" SPDX JSON: " + spdxJson);
System.out.println(" SPDX Tag-Value: " + spdxTagValue);
System.out.println(" SBOM Report: " + reportPath);
System.out.println(" Vulnerability Report: " + vulnReportPath);
}
private List<SbomComponent> extractComponents(SpdxDocument document) throws InvalidSPDXAnalysisException {
List<SbomComponent> components = new ArrayList<>();
for (SpdxItem item : document.getDocumentDescribes()) {
if (item instanceof SpdxPackage) {
SpdxPackage pkg = (SpdxPackage) item;
SbomComponent component = convertToSbomComponent(pkg);
components.add(component);
}
}
return components;
}
private SbomComponent convertToSbomComponent(SpdxPackage pkg) throws InvalidSPDXAnalysisException {
// Extract Maven coordinates from package name (groupId:artifactId)
String[] parts = pkg.getName().split(":");
SbomComponent component = new SbomComponent(
parts.length > 0 ? parts[0] : "",
parts.length > 1 ? parts[1] : pkg.getName(),
pkg.getVersionInfo() != null ? pkg.getVersionInfo() : ""
);
// Extract license
if (pkg.getLicenseDeclared() != null) {
component.setLicense(pkg.getLicenseDeclared().toString());
}
// Extract download URL
if (pkg.getDownloadLocation() != null) {
component.addProperty("downloadUrl", pkg.getDownloadLocation());
}
return component;
}
}
Conclusion
This comprehensive SPDX SBOM implementation provides:
- SPDX Document Generation - Create standards-compliant SBOM documents
- Maven Integration - Extract dependencies from Maven projects
- Vulnerability Enrichment - Integrate with OSV and NVD databases
- Multiple Formats - Support for JSON, Tag-Value, and human-readable reports
- CI/CD Integration - GitHub Actions and Maven plugin integration
- Security Scanning - Vulnerability detection and reporting
The implementation enables organizations to maintain accurate software inventories, track dependencies, and identify security vulnerabilities in their software supply chain.
Secure Java Dependency Management, Vulnerability Scanning & Software Supply Chain Protection (SBOM, SCA, CI Security & License Compliance)
https://macronepal.com/blog/github-code-scanning-in-java-complete-guide/
Explains GitHub Code Scanning for Java using tools like CodeQL to automatically analyze source code and detect security vulnerabilities directly inside CI/CD pipelines before deployment.
https://macronepal.com/blog/license-compliance-in-java-comprehensive-guide/
Explains software license compliance in Java projects, ensuring dependencies follow legal requirements (MIT, Apache, GPL, etc.) and preventing license violations in enterprise software.
https://macronepal.com/blog/container-security-for-java-uncovering-vulnerabilities-with-grype/
Explains using Grype to scan Java container images and filesystems for known CVEs in OS packages and application dependencies to improve container security.
https://macronepal.com/blog/syft-sbom-generation-in-java-comprehensive-software-bill-of-materials-for-jvm-applications/
Explains using Syft to generate SBOMs (Software Bill of Materials) for Java applications, listing all dependencies, libraries, and components for supply chain transparency.
https://macronepal.com/blog/comprehensive-dependency-analysis-generating-and-scanning-sboms-with-trivy-for-java/
Explains using Trivy to generate SBOMs and scan Java dependencies and container images for vulnerabilities, integrating security checks into CI/CD pipelines.
https://macronepal.com/blog/dependabot-for-java-in-java/
Explains GitHub Dependabot for Java projects, which automatically detects vulnerable dependencies and creates pull requests to update them securely.
https://macronepal.com/blog/parasoft-jtest-in-java-comprehensive-guide-to-code-analysis-and-testing/
Explains Parasoft Jtest, a static analysis and testing tool for Java that helps detect bugs, security issues, and code quality problems early in development.
https://macronepal.com/blog/snyk-open-source-in-java-comprehensive-dependency-vulnerability-management-2/
Explains Snyk Open Source for Java, which continuously scans dependencies for vulnerabilities and provides automated fix suggestions and monitoring.
https://macronepal.com/blog/owasp-dependency-check-in-java-complete-vulnerability-scanning-guide/
Explains OWASP Dependency-Check, which scans Java dependencies against the National Vulnerability Database (NVD) to detect known security vulnerabilities.
https://macronepal.com/blog/securing-your-dependencies-a-java-developers-guide-to-whitesource-mend-bolt/
Explains Mend (WhiteSource) Bolt for Java, a dependency management and SCA tool that provides vulnerability detection, license compliance, and security policy enforcement in enterprise environments.