SBOM Generation with Syft in Java

1. Core SBOM Models and Data Structures

// SBOM models
package com.sbom.core;
import java.time.Instant;
import java.util.*;
public class SbomDocument {
private final String id;
private final String version;
private final Instant createdAt;
private final SbomMetadata metadata;
private final List<Component> components;
private final List<Relationship> relationships;
private final Map<String, Object> properties;
public SbomDocument(String id, String version, SbomMetadata metadata, 
List<Component> components, List<Relationship> relationships) {
this.id = id;
this.version = version;
this.createdAt = Instant.now();
this.metadata = metadata;
this.components = components != null ? new ArrayList<>(components) : new ArrayList<>();
this.relationships = relationships != null ? new ArrayList<>(relationships) : new ArrayList<>();
this.properties = new HashMap<>();
}
// Getters
public String getId() { return id; }
public String getVersion() { return version; }
public Instant getCreatedAt() { return createdAt; }
public SbomMetadata getMetadata() { return metadata; }
public List<Component> getComponents() { return Collections.unmodifiableList(components); }
public List<Relationship> getRelationships() { return Collections.unmodifiableList(relationships); }
public Map<String, Object> getProperties() { return Collections.unmodifiableMap(properties); }
public void addComponent(Component component) {
components.add(component);
}
public void addRelationship(Relationship relationship) {
relationships.add(relationship);
}
public void setProperty(String key, Object value) {
properties.put(key, value);
}
public Optional<Component> findComponentById(String id) {
return components.stream()
.filter(component -> id.equals(component.getId()))
.findFirst();
}
public List<Component> findComponentsByType(ComponentType type) {
return components.stream()
.filter(component -> type == component.getType())
.toList();
}
public List<Component> findVulnerableComponents() {
return components.stream()
.filter(Component::hasVulnerabilities)
.toList();
}
public static class Builder {
private String id;
private String version = "1.0";
private SbomMetadata metadata;
private List<Component> components = new ArrayList<>();
private List<Relationship> relationships = new ArrayList<>();
public Builder id(String id) {
this.id = id;
return this;
}
public Builder version(String version) {
this.version = version;
return this;
}
public Builder metadata(SbomMetadata metadata) {
this.metadata = metadata;
return this;
}
public Builder component(Component component) {
this.components.add(component);
return this;
}
public Builder components(List<Component> components) {
this.components.addAll(components);
return this;
}
public Builder relationship(Relationship relationship) {
this.relationships.add(relationship);
return this;
}
public SbomDocument build() {
return new SbomDocument(id, version, metadata, components, relationships);
}
}
}
// Component model
package com.sbom.core;
import java.util.*;
public class Component {
private final String id;
private final String name;
private final String version;
private final ComponentType type;
private final String purl;
private final String cpe;
private final Map<String, String> properties;
private final List<License> licenses;
private final List<Vulnerability> vulnerabilities;
private final List<Hash> hashes;
private final String supplier;
private final String description;
public Component(String id, String name, String version, ComponentType type, 
String purl, String cpe, Map<String, String> properties,
List<License> licenses, List<Vulnerability> vulnerabilities,
List<Hash> hashes, String supplier, String description) {
this.id = id;
this.name = name;
this.version = version;
this.type = type;
this.purl = purl;
this.cpe = cpe;
this.properties = properties != null ? new HashMap<>(properties) : new HashMap<>();
this.licenses = licenses != null ? new ArrayList<>(licenses) : new ArrayList<>();
this.vulnerabilities = vulnerabilities != null ? new ArrayList<>(vulnerabilities) : new ArrayList<>();
this.hashes = hashes != null ? new ArrayList<>(hashes) : new ArrayList<>();
this.supplier = supplier;
this.description = description;
}
// Getters
public String getId() { return id; }
public String getName() { return name; }
public String getVersion() { return version; }
public ComponentType getType() { return type; }
public String getPurl() { return purl; }
public String getCpe() { return cpe; }
public Map<String, String> getProperties() { return Collections.unmodifiableMap(properties); }
public List<License> getLicenses() { return Collections.unmodifiableList(licenses); }
public List<Vulnerability> getVulnerabilities() { return Collections.unmodifiableList(vulnerabilities); }
public List<Hash> getHashes() { return Collections.unmodifiableList(hashes); }
public String getSupplier() { return supplier; }
public String getDescription() { return description; }
public boolean hasVulnerabilities() {
return !vulnerabilities.isEmpty();
}
public boolean isVulnerable() {
return vulnerabilities.stream().anyMatch(Vulnerability::isCritical);
}
public void addVulnerability(Vulnerability vulnerability) {
vulnerabilities.add(vulnerability);
}
public void addLicense(License license) {
licenses.add(license);
}
public static class Builder {
private String id;
private String name;
private String version;
private ComponentType type;
private String purl;
private String cpe;
private Map<String, String> properties = new HashMap<>();
private List<License> licenses = new ArrayList<>();
private List<Vulnerability> vulnerabilities = new ArrayList<>();
private List<Hash> hashes = new ArrayList<>();
private String supplier;
private String description;
public Builder id(String id) {
this.id = id;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder version(String version) {
this.version = version;
return this;
}
public Builder type(ComponentType type) {
this.type = type;
return this;
}
public Builder purl(String purl) {
this.purl = purl;
return this;
}
public Builder cpe(String cpe) {
this.cpe = cpe;
return this;
}
public Builder property(String key, String value) {
this.properties.put(key, value);
return this;
}
public Builder license(License license) {
this.licenses.add(license);
return this;
}
public Builder vulnerability(Vulnerability vulnerability) {
this.vulnerabilities.add(vulnerability);
return this;
}
public Builder hash(Hash hash) {
this.hashes.add(hash);
return this;
}
public Builder supplier(String supplier) {
this.supplier = supplier;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Component build() {
return new Component(id, name, version, type, purl, cpe, properties, 
licenses, vulnerabilities, hashes, supplier, description);
}
}
}
// Supporting enums and classes
package com.sbom.core;
public enum ComponentType {
APPLICATION,
FRAMEWORK,
LIBRARY,
CONTAINER,
OPERATING_SYSTEM,
DEVICE,
FIRMWARE,
FILE,
PLATFORM
}
public class License {
private final String id;
private final String name;
private final String url;
private final boolean osiApproved;
public License(String id, String name, String url, boolean osiApproved) {
this.id = id;
this.name = name;
this.url = url;
this.osiApproved = osiApproved;
}
// Getters
public String getId() { return id; }
public String getName() { return name; }
public String getUrl() { return url; }
public boolean isOsiApproved() { return osiApproved; }
}
public class Vulnerability {
private final String id;
private final String source;
private final Severity severity;
private final String description;
private final List<String> references;
private final Double score;
private final String vector;
public Vulnerability(String id, String source, Severity severity, String description,
List<String> references, Double score, String vector) {
this.id = id;
this.source = source;
this.severity = severity;
this.description = description;
this.references = references != null ? new ArrayList<>(references) : new ArrayList<>();
this.score = score;
this.vector = vector;
}
public boolean isCritical() {
return severity == Severity.CRITICAL || severity == Severity.HIGH;
}
// Getters
public String getId() { return id; }
public String getSource() { return source; }
public Severity getSeverity() { return severity; }
public String getDescription() { return description; }
public List<String> getReferences() { return Collections.unmodifiableList(references); }
public Double getScore() { return score; }
public String getVector() { return vector; }
}
public enum Severity {
CRITICAL,
HIGH,
MEDIUM,
LOW,
INFO,
UNKNOWN
}
public class Hash {
private final String algorithm;
private final String value;
public Hash(String algorithm, String value) {
this.algorithm = algorithm;
this.value = value;
}
// Getters
public String getAlgorithm() { return algorithm; }
public String getValue() { return value; }
}
public class Relationship {
private final String sourceId;
private final String targetId;
private final RelationshipType type;
public Relationship(String sourceId, String targetId, RelationshipType type) {
this.sourceId = sourceId;
this.targetId = targetId;
this.type = type;
}
// Getters
public String getSourceId() { return sourceId; }
public String getTargetId() { return targetId; }
public RelationshipType getType() { return type; }
}
public enum RelationshipType {
CONTAINS,
DEPENDS_ON,
DERIVED_FROM,
GENERATED_FROM,
COPY_OF,
PATCH_FOR
}
public class SbomMetadata {
private final String toolName;
private final String toolVersion;
private final String authors;
private final String timestamp;
private final Map<String, String> properties;
public SbomMetadata(String toolName, String toolVersion, String authors, 
String timestamp, Map<String, String> properties) {
this.toolName = toolName;
this.toolVersion = toolVersion;
this.authors = authors;
this.timestamp = timestamp;
this.properties = properties != null ? new HashMap<>(properties) : new HashMap<>();
}
// Getters
public String getToolName() { return toolName; }
public String getToolVersion() { return toolVersion; }
public String getAuthors() { return authors; }
public String getTimestamp() { return timestamp; }
public Map<String, String> getProperties() { return Collections.unmodifiableMap(properties); }
}

2. Syft Integration Service

// SyftIntegrationService.java
package com.sbom.syft;
import com.sbom.core.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.TimeUnit;
public class SyftIntegrationService {
private static final Logger logger = LoggerFactory.getLogger(SyftIntegrationService.class);
private final String syftBinaryPath;
private final String outputFormat;
private final long timeoutSeconds;
public SyftIntegrationService() {
this("syft", "json", 300);
}
public SyftIntegrationService(String syftBinaryPath, String outputFormat, long timeoutSeconds) {
this.syftBinaryPath = syftBinaryPath;
this.outputFormat = outputFormat;
this.timeoutSeconds = timeoutSeconds;
}
public SbomDocument generateSbom(String targetPath) throws SbomGenerationException {
return generateSbom(targetPath, Collections.emptyList());
}
public SbomDocument generateSbom(String targetPath, List<String> additionalArgs) 
throws SbomGenerationException {
try {
// Execute Syft command
String syftOutput = executeSyftCommand(targetPath, additionalArgs);
// Parse Syft output
return parseSyftOutput(syftOutput, targetPath);
} catch (IOException | InterruptedException e) {
throw new SbomGenerationException("Failed to generate SBOM for: " + targetPath, e);
}
}
public SbomDocument generateSbomForContainer(String containerImage) throws SbomGenerationException {
List<String> additionalArgs = List.of(containerImage);
return generateSbom(containerImage, additionalArgs);
}
public SbomDocument generateSbomForDirectory(String directoryPath) throws SbomGenerationException {
return generateSbom(directoryPath);
}
public SbomDocument generateSbomForFile(String filePath) throws SbomGenerationException {
return generateSbom(filePath);
}
private String executeSyftCommand(String target, List<String> additionalArgs) 
throws IOException, InterruptedException {
List<String> command = new ArrayList<>();
command.add(syftBinaryPath);
command.add(target);
command.add("--output");
command.add(outputFormat);
command.add("--quiet");
// Add additional arguments
command.addAll(additionalArgs);
logger.info("Executing Syft command: {}", String.join(" ", command));
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
// Read output
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
// Wait for process completion
boolean completed = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
if (!completed) {
process.destroyForcibly();
throw new IOException("Syft command timed out after " + timeoutSeconds + " seconds");
}
int exitCode = process.exitValue();
if (exitCode != 0) {
throw new IOException("Syft command failed with exit code: " + exitCode);
}
return output.toString();
}
private SbomDocument parseSyftOutput(String syftOutput, String target) {
// In a real implementation, you would parse the JSON output from Syft
// For this example, we'll create a mock parser
try {
// Parse JSON using Jackson or similar library
// ObjectMapper mapper = new ObjectMapper();
// JsonNode root = mapper.readTree(syftOutput);
// Mock implementation - replace with actual Syft JSON parsing
return createMockSbomDocument(target);
} catch (Exception e) {
logger.error("Failed to parse Syft output", e);
throw new SbomGenerationException("Failed to parse Syft output", e);
}
}
private SbomDocument createMockSbomDocument(String target) {
SbomMetadata metadata = new SbomMetadata(
"syft",
"0.85.0",
"Anchore, Inc.",
java.time.Instant.now().toString(),
Map.of("target", target, "timestamp", String.valueOf(System.currentTimeMillis()))
);
List<Component> components = Arrays.asList(
new Component.Builder()
.id("pkg:maven/org.springframework/[email protected]")
.name("spring-core")
.version("5.3.18")
.type(ComponentType.LIBRARY)
.purl("pkg:maven/org.springframework/[email protected]")
.cpe("cpe:2.3:a:spring:spring_framework:5.3.18:*:*:*:*:*:*:*")
.license(new License("Apache-2.0", "Apache License 2.0", 
"https://www.apache.org/licenses/LICENSE-2.0", true))
.supplier("Spring")
.description("Spring Framework Core")
.build(),
new Component.Builder()
.id("pkg:maven/com.fasterxml.jackson.core/[email protected]")
.name("jackson-databind")
.version("2.13.3")
.type(ComponentType.LIBRARY)
.purl("pkg:maven/com.fasterxml.jackson.core/[email protected]")
.license(new License("Apache-2.0", "Apache License 2.0", 
"https://www.apache.org/licenses/LICENSE-2.0", true))
.supplier("Fasterxml")
.description("Jackson Data Binding")
.build(),
new Component.Builder()
.id("pkg:docker/[email protected]")
.name("alpine")
.version("3.16.0")
.type(ComponentType.OPERATING_SYSTEM)
.purl("pkg:docker/[email protected]")
.description("Alpine Linux Base Image")
.build()
);
List<Relationship> relationships = Arrays.asList(
new Relationship("root-component", "pkg:maven/org.springframework/[email protected]", 
RelationshipType.DEPENDS_ON),
new Relationship("root-component", "pkg:maven/com.fasterxml.jackson.core/[email protected]", 
RelationshipType.DEPENDS_ON),
new Relationship("pkg:docker/[email protected]", "root-component", 
RelationshipType.CONTAINS)
);
return new SbomDocument.Builder()
.id(UUID.randomUUID().toString())
.version("1.3")
.metadata(metadata)
.components(components)
.relationships(relationships)
.build();
}
public boolean isSyftAvailable() {
try {
ProcessBuilder processBuilder = new ProcessBuilder(syftBinaryPath, "--version");
Process process = processBuilder.start();
return process.waitFor(10, TimeUnit.SECONDS) && process.exitValue() == 0;
} catch (IOException | InterruptedException e) {
logger.warn("Syft is not available: {}", e.getMessage());
return false;
}
}
public static class SbomGenerationException extends RuntimeException {
public SbomGenerationException(String message) {
super(message);
}
public SbomGenerationException(String message, Throwable cause) {
super(message, cause);
}
}
}

3. SBOM Analysis and Reporting

// SbomAnalyzer.java
package com.sbom.analysis;
import com.sbom.core.*;
import java.util.*;
import java.util.stream.Collectors;
public class SbomAnalyzer {
public AnalysisResult analyze(SbomDocument sbom) {
List<Component> components = sbom.getComponents();
Map<ComponentType, Long> componentCountByType = components.stream()
.collect(Collectors.groupingBy(Component::getType, Collectors.counting()));
long vulnerableComponents = components.stream()
.filter(Component::hasVulnerabilities)
.count();
long criticalVulnerabilities = components.stream()
.flatMap(component -> component.getVulnerabilities().stream())
.filter(Vulnerability::isCritical)
.count();
Map<String, Long> licenseDistribution = components.stream()
.flatMap(component -> component.getLicenses().stream())
.collect(Collectors.groupingBy(License::getId, Collectors.counting()));
List<Component> outdatedComponents = findOutdatedComponents(components);
return new AnalysisResult(
sbom.getId(),
components.size(),
componentCountByType,
vulnerableComponents,
criticalVulnerabilities,
licenseDistribution,
outdatedComponents,
calculateRiskScore(components)
);
}
public List<Component> findVulnerableComponents(SbomDocument sbom, Severity minSeverity) {
return sbom.getComponents().stream()
.filter(component -> component.getVulnerabilities().stream()
.anyMatch(vuln -> vuln.getSeverity().ordinal() <= minSeverity.ordinal()))
.toList();
}
public List<Component> findComponentsByLicense(SbomDocument sbom, String licenseId) {
return sbom.getComponents().stream()
.filter(component -> component.getLicenses().stream()
.anyMatch(license -> licenseId.equals(license.getId())))
.toList();
}
public Map<Severity, Long> getVulnerabilitySummary(SbomDocument sbom) {
return sbom.getComponents().stream()
.flatMap(component -> component.getVulnerabilities().stream())
.collect(Collectors.groupingBy(Vulnerability::getSeverity, Collectors.counting()));
}
public List<Component> findDuplicateComponents(SbomDocument sbom) {
Map<String, List<Component>> componentsByPurl = sbom.getComponents().stream()
.filter(component -> component.getPurl() != null)
.collect(Collectors.groupingBy(Component::getPurl));
return componentsByPurl.values().stream()
.filter(list -> list.size() > 1)
.flatMap(List::stream)
.toList();
}
private List<Component> findOutdatedComponents(List<Component> components) {
// This is a simplified implementation
// In reality, you would check against known vulnerability databases
// or version tracking services
return components.stream()
.filter(component -> {
String version = component.getVersion();
// Simple heuristic: if version contains certain patterns, consider outdated
return version != null && 
(version.contains("alpha") || 
version.contains("beta") || 
version.contains("rc") ||
isOldVersion(version));
})
.toList();
}
private boolean isOldVersion(String version) {
// Simplified version age check
try {
// Extract major version number
String majorStr = version.split("\\.")[0];
int major = Integer.parseInt(majorStr);
return major < 2; // Consider versions < 2.0.0 as potentially old
} catch (Exception e) {
return false;
}
}
private double calculateRiskScore(List<Component> components) {
if (components.isEmpty()) {
return 0.0;
}
double totalScore = 0.0;
int weight = 0;
for (Component component : components) {
double componentScore = calculateComponentRisk(component);
totalScore += componentScore;
weight++;
}
return totalScore / weight;
}
private double calculateComponentRisk(Component component) {
double risk = 0.0;
// Base risk based on component type
switch (component.getType()) {
case APPLICATION -> risk += 0.3;
case LIBRARY -> risk += 0.2;
case FRAMEWORK -> risk += 0.4;
case CONTAINER -> risk += 0.5;
case OPERATING_SYSTEM -> risk += 0.8;
}
// Increase risk based on vulnerabilities
for (Vulnerability vuln : component.getVulnerabilities()) {
switch (vuln.getSeverity()) {
case CRITICAL -> risk += 0.5;
case HIGH -> risk += 0.3;
case MEDIUM -> risk += 0.1;
case LOW -> risk += 0.05;
}
}
// Cap risk at 1.0
return Math.min(risk, 1.0);
}
public static class AnalysisResult {
private final String sbomId;
private final int totalComponents;
private final Map<ComponentType, Long> componentCountByType;
private final long vulnerableComponents;
private final long criticalVulnerabilities;
private final Map<String, Long> licenseDistribution;
private final List<Component> outdatedComponents;
private final double riskScore;
public AnalysisResult(String sbomId, int totalComponents, 
Map<ComponentType, Long> componentCountByType,
long vulnerableComponents, long criticalVulnerabilities,
Map<String, Long> licenseDistribution,
List<Component> outdatedComponents, double riskScore) {
this.sbomId = sbomId;
this.totalComponents = totalComponents;
this.componentCountByType = componentCountByType;
this.vulnerableComponents = vulnerableComponents;
this.criticalVulnerabilities = criticalVulnerabilities;
this.licenseDistribution = licenseDistribution;
this.outdatedComponents = outdatedComponents;
this.riskScore = riskScore;
}
// Getters
public String getSbomId() { return sbomId; }
public int getTotalComponents() { return totalComponents; }
public Map<ComponentType, Long> getComponentCountByType() { return componentCountByType; }
public long getVulnerableComponents() { return vulnerableComponents; }
public long getCriticalVulnerabilities() { return criticalVulnerabilities; }
public Map<String, Long> getLicenseDistribution() { return licenseDistribution; }
public List<Component> getOutdatedComponents() { return outdatedComponents; }
public double getRiskScore() { return riskScore; }
public String getRiskLevel() {
if (riskScore >= 0.8) return "CRITICAL";
if (riskScore >= 0.6) return "HIGH";
if (riskScore >= 0.4) return "MEDIUM";
if (riskScore >= 0.2) return "LOW";
return "VERY_LOW";
}
@Override
public String toString() {
return String.format(
"AnalysisResult{components=%d, vulnerable=%d, criticalVuln=%d, risk=%.2f (%s)}",
totalComponents, vulnerableComponents, criticalVulnerabilities, riskScore, getRiskLevel()
);
}
}
}
// SbomReporter.java
package com.sbom.reporting;
import com.sbom.core.*;
import com.sbom.analysis.SbomAnalyzer.AnalysisResult;
import java.util.List;
import java.util.Map;
public class SbomReporter {
public void generateConsoleReport(SbomDocument sbom, AnalysisResult analysis) {
System.out.println("=== SBOM Analysis Report ===");
System.out.printf("SBOM ID: %s%n", sbom.getId());
System.out.printf("Generated: %s%n", sbom.getCreatedAt());
System.out.printf("Total Components: %d%n", analysis.getTotalComponents());
System.out.printf("Vulnerable Components: %d%n", analysis.getVulnerableComponents());
System.out.printf("Critical Vulnerabilities: %d%n", analysis.getCriticalVulnerabilities());
System.out.printf("Risk Score: %.2f (%s)%n", analysis.getRiskScore(), analysis.getRiskLevel());
System.out.println("\nComponent Types:");
analysis.getComponentCountByType().forEach((type, count) -> 
System.out.printf("  %s: %d%n", type, count));
System.out.println("\nLicense Distribution:");
analysis.getLicenseDistribution().forEach((license, count) -> 
System.out.printf("  %s: %d%n", license, count));
if (!analysis.getOutdatedComponents().isEmpty()) {
System.out.println("\nOutdated Components:");
analysis.getOutdatedComponents().forEach(component -> 
System.out.printf("  %s %s%n", component.getName(), component.getVersion()));
}
// Show vulnerable components
List<Component> vulnerable = sbom.findVulnerableComponents();
if (!vulnerable.isEmpty()) {
System.out.println("\nVulnerable Components:");
vulnerable.forEach(component -> {
System.out.printf("  %s %s%n", component.getName(), component.getVersion());
component.getVulnerabilities().forEach(vuln -> 
System.out.printf("    - %s: %s (Severity: %s)%n", 
vuln.getId(), vuln.getDescription(), vuln.getSeverity()));
});
}
}
public String generateMarkdownReport(SbomDocument sbom, AnalysisResult analysis) {
StringBuilder markdown = new StringBuilder();
markdown.append("# SBOM Analysis Report\n\n");
markdown.append("## Summary\n\n");
markdown.append(String.format("- **SBOM ID**: %s\n", sbom.getId()));
markdown.append(String.format("- **Total Components**: %d\n", analysis.getTotalComponents()));
markdown.append(String.format("- **Vulnerable Components**: %d\n", analysis.getVulnerableComponents()));
markdown.append(String.format("- **Critical Vulnerabilities**: %d\n", analysis.getCriticalVulnerabilities()));
markdown.append(String.format("- **Risk Score**: %.2f (%s)\n\n", analysis.getRiskScore(), analysis.getRiskLevel()));
markdown.append("## Component Types\n\n");
analysis.getComponentCountByType().forEach((type, count) -> 
markdown.append(String.format("- %s: %d\n", type, count)));
markdown.append("\n## License Distribution\n\n");
analysis.getLicenseDistribution().forEach((license, count) -> 
markdown.append(String.format("- %s: %d\n", license, count)));
if (!analysis.getOutdatedComponents().isEmpty()) {
markdown.append("\n## Outdated Components\n\n");
analysis.getOutdatedComponents().forEach(component -> 
markdown.append(String.format("- %s %s\n", component.getName(), component.getVersion())));
}
List<Component> vulnerable = sbom.findVulnerableComponents();
if (!vulnerable.isEmpty()) {
markdown.append("\n## Vulnerable Components\n\n");
vulnerable.forEach(component -> {
markdown.append(String.format("### %s %s\n\n", component.getName(), component.getVersion()));
component.getVulnerabilities().forEach(vuln -> 
markdown.append(String.format("- **%s**: %s (Severity: %s)\n", 
vuln.getId(), vuln.getDescription(), vuln.getSeverity())));
markdown.append("\n");
});
}
return markdown.toString();
}
public void generateJsonReport(SbomDocument sbom, AnalysisResult analysis) {
// Generate JSON report using Jackson or similar
// This is a simplified version
String json = String.format(
"{\"sbomId\": \"%s\", \"totalComponents\": %d, \"vulnerableComponents\": %d, " +
"\"criticalVulnerabilities\": %d, \"riskScore\": %.2f}",
sbom.getId(), analysis.getTotalComponents(), analysis.getVulnerableComponents(),
analysis.getCriticalVulnerabilities(), analysis.getRiskScore()
);
System.out.println(json);
}
}

4. Spring Boot Integration

// SbomController.java
package com.sbom.web;
import com.sbom.core.SbomDocument;
import com.sbom.syft.SyftIntegrationService;
import com.sbom.analysis.SbomAnalyzer;
import com.sbom.analysis.SbomAnalyzer.AnalysisResult;
import com.sbom.reporting.SbomReporter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/sbom")
public class SbomController {
@Autowired
private SyftIntegrationService syftService;
@Autowired
private SbomAnalyzer sbomAnalyzer;
@Autowired
private SbomReporter sbomReporter;
@PostMapping("/generate")
public ResponseEntity<Map<String, Object>> generateSbom(@RequestBody SbomGenerationRequest request) {
try {
SbomDocument sbom = syftService.generateSbom(request.getTargetPath());
AnalysisResult analysis = sbomAnalyzer.analyze(sbom);
return ResponseEntity.ok(Map.of(
"sbom", sbom,
"analysis", analysis,
"status", "success"
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"error", e.getMessage(),
"status", "error"
));
}
}
@PostMapping("/generate/container")
public ResponseEntity<Map<String, Object>> generateContainerSbom(@RequestBody ContainerSbomRequest request) {
try {
SbomDocument sbom = syftService.generateSbomForContainer(request.getImageName());
AnalysisResult analysis = sbomAnalyzer.analyze(sbom);
return ResponseEntity.ok(Map.of(
"sbom", sbom,
"analysis", analysis,
"status", "success"
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"error", e.getMessage(),
"status", "error"
));
}
}
@GetMapping("/report/{format}")
public ResponseEntity<String> generateReport(@RequestBody SbomDocument sbom, 
@PathVariable String format) {
AnalysisResult analysis = sbomAnalyzer.analyze(sbom);
switch (format.toLowerCase()) {
case "console":
sbomReporter.generateConsoleReport(sbom, analysis);
return ResponseEntity.ok("Report generated to console");
case "markdown":
String markdown = sbomReporter.generateMarkdownReport(sbom, analysis);
return ResponseEntity.ok(markdown);
case "json":
sbomReporter.generateJsonReport(sbom, analysis);
return ResponseEntity.ok("JSON report generated");
default:
return ResponseEntity.badRequest().body("Unsupported format: " + format);
}
}
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
boolean syftAvailable = syftService.isSyftAvailable();
return ResponseEntity.ok(Map.of(
"status", syftAvailable ? "healthy" : "degraded",
"syftAvailable", syftAvailable,
"timestamp", System.currentTimeMillis()
));
}
public static class SbomGenerationRequest {
private String targetPath;
private String outputFormat;
// Getters and setters
public String getTargetPath() { return targetPath; }
public void setTargetPath(String targetPath) { this.targetPath = targetPath; }
public String getOutputFormat() { return outputFormat; }
public void setOutputFormat(String outputFormat) { this.outputFormat = outputFormat; }
}
public static class ContainerSbomRequest {
private String imageName;
private String tag;
// Getters and setters
public String getImageName() { return imageName; }
public void setImageName(String imageName) { this.imageName = imageName; }
public String getTag() { return tag; }
public void setTag(String tag) { this.tag = tag; }
}
}
// SbomService.java
package com.sbom.service;
import com.sbom.core.SbomDocument;
import com.sbom.syft.SyftIntegrationService;
import com.sbom.analysis.SbomAnalyzer;
import com.sbom.analysis.SbomAnalyzer.AnalysisResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class SbomService {
@Autowired
private SyftIntegrationService syftService;
@Autowired
private SbomAnalyzer sbomAnalyzer;
private final Map<String, SbomDocument> sbomCache = new ConcurrentHashMap<>();
private final Map<String, AnalysisResult> analysisCache = new ConcurrentHashMap<>();
public SbomDocument generateAndCacheSbom(String targetPath) {
String cacheKey = generateCacheKey(targetPath);
if (sbomCache.containsKey(cacheKey)) {
return sbomCache.get(cacheKey);
}
SbomDocument sbom = syftService.generateSbom(targetPath);
AnalysisResult analysis = sbomAnalyzer.analyze(sbom);
sbomCache.put(cacheKey, sbom);
analysisCache.put(cacheKey, analysis);
return sbom;
}
public AnalysisResult getAnalysis(String targetPath) {
String cacheKey = generateCacheKey(targetPath);
return analysisCache.get(cacheKey);
}
public List<SbomDocument> getAllCachedSboms() {
return new ArrayList<>(sbomCache.values());
}
public void clearCache() {
sbomCache.clear();
analysisCache.clear();
}
public void removeFromCache(String targetPath) {
String cacheKey = generateCacheKey(targetPath);
sbomCache.remove(cacheKey);
analysisCache.remove(cacheKey);
}
@Scheduled(fixedRate = 3600000) // Clear cache every hour
public void clearExpiredCache() {
// In a real implementation, you might want more sophisticated cache expiration
clearCache();
}
private String generateCacheKey(String targetPath) {
return targetPath + "_" + System.currentTimeMillis() / 3600000; // Change key every hour
}
}

5. Configuration and Application

# application.yml
spring:
application:
name: sbom-generator
sbom:
syft:
binary-path: "syft"
output-format: "json"
timeout-seconds: 300
cache:
enabled: true
ttl-minutes: 60
management:
endpoints:
web:
exposure:
include: health,metrics,info
endpoint:
health:
show-details: always
logging:
level:
com.sbom: DEBUG
// SbomApplication.java
package com.sbom;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class SbomApplication {
public static void main(String[] args) {
SpringApplication.run(SbomApplication.class, args);
}
}
// SbomConfig.java
package com.sbom.config;
import com.sbom.syft.SyftIntegrationService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SbomConfig {
@Value("${sbom.syft.binary-path:syft}")
private String syftBinaryPath;
@Value("${sbom.syft.output-format:json}")
private String outputFormat;
@Value("${sbom.syft.timeout-seconds:300}")
private long timeoutSeconds;
@Bean
public SyftIntegrationService syftIntegrationService() {
return new SyftIntegrationService(syftBinaryPath, outputFormat, timeoutSeconds);
}
}

6. Usage Examples

// SbomGeneratorDemo.java
package com.sbom.demo;
import com.sbom.core.SbomDocument;
import com.sbom.syft.SyftIntegrationService;
import com.sbom.analysis.SbomAnalyzer;
import com.sbom.analysis.SbomAnalyzer.AnalysisResult;
import com.sbom.reporting.SbomReporter;
public class SbomGeneratorDemo {
public static void main(String[] args) {
try {
// Initialize services
SyftIntegrationService syftService = new SyftIntegrationService();
SbomAnalyzer analyzer = new SbomAnalyzer();
SbomReporter reporter = new SbomReporter();
// Check if Syft is available
if (!syftService.isSyftAvailable()) {
System.err.println("Syft is not available. Please install it first.");
return;
}
// Generate SBOM for different targets
System.out.println("Generating SBOM for current directory...");
SbomDocument directorySbom = syftService.generateSbom(".");
analyzeAndReport(directorySbom, analyzer, reporter);
System.out.println("\nGenerating SBOM for container image...");
SbomDocument containerSbom = syftService.generateSbomForContainer("nginx:latest");
analyzeAndReport(containerSbom, analyzer, reporter);
// Generate markdown report
String markdownReport = reporter.generateMarkdownReport(directorySbom, 
analyzer.analyze(directorySbom));
// Save report to file
java.nio.file.Files.writeString(
java.nio.file.Path.of("sbom-report.md"),
markdownReport
);
System.out.println("SBOM report saved to sbom-report.md");
} catch (Exception e) {
System.err.println("Error generating SBOM: " + e.getMessage());
e.printStackTrace();
}
}
private static void analyzeAndReport(SbomDocument sbom, SbomAnalyzer analyzer, 
SbomReporter reporter) {
AnalysisResult analysis = analyzer.analyze(sbom);
reporter.generateConsoleReport(sbom, analysis);
// Show additional analysis
System.out.println("\nVulnerability Summary:");
analyzer.getVulnerabilitySummary(sbom).forEach((severity, count) -> 
System.out.printf("  %s: %d%n", severity, count));
List<com.sbom.core.Component> duplicates = analyzer.findDuplicateComponents(sbom);
if (!duplicates.isEmpty()) {
System.out.println("\nDuplicate Components:");
duplicates.forEach(component -> 
System.out.printf("  %s %s%n", component.getName(), component.getVersion()));
}
}
}
// CI/CD Integration Example
package com.sbom.cicd;
import com.sbom.core.SbomDocument;
import com.sbom.syft.SyftIntegrationService;
import com.sbom.analysis.SbomAnalyzer;
import com.sbom.analysis.SbomAnalyzer.AnalysisResult;
public class CiCdIntegration {
public boolean validateSbom(String imageName, double maxRiskScore) {
try {
SyftIntegrationService syftService = new SyftIntegrationService();
SbomAnalyzer analyzer = new SbomAnalyzer();
// Generate SBOM for container image
SbomDocument sbom = syftService.generateSbomForContainer(imageName);
AnalysisResult analysis = analyzer.analyze(sbom);
System.out.printf("SBOM Analysis for %s:%n", imageName);
System.out.printf("  Risk Score: %.2f%n", analysis.getRiskScore());
System.out.printf("  Vulnerable Components: %d%n", analysis.getVulnerableComponents());
System.out.printf("  Critical Vulnerabilities: %d%n", analysis.getCriticalVulnerabilities());
// Check if risk is within acceptable limits
if (analysis.getRiskScore() > maxRiskScore) {
System.err.printf("❌ SBOM validation failed: Risk score %.2f exceeds maximum %.2f%n", 
analysis.getRiskScore(), maxRiskScore);
return false;
}
if (analysis.getCriticalVulnerabilities() > 0) {
System.err.printf("❌ SBOM validation failed: %d critical vulnerabilities found%n", 
analysis.getCriticalVulnerabilities());
return false;
}
System.out.println("✅ SBOM validation passed");
return true;
} catch (Exception e) {
System.err.println("❌ SBOM generation failed: " + e.getMessage());
return false;
}
}
}

Key Features:

  1. Syft Integration: Seamless integration with Syft CLI tool
  2. Comprehensive SBOM Models: Full SPDX/CycloneDX compatible data structures
  3. Advanced Analysis: Vulnerability detection, license compliance, risk scoring
  4. Multiple Report Formats: Console, Markdown, JSON reporting
  5. Spring Boot Integration: REST API and service layer
  6. Caching: Performance optimization for repeated analysis
  7. CI/CD Ready: Validation and integration for pipelines
  8. Extensible: Easy to extend with additional analyzers and reporters

This implementation provides a complete SBOM generation and analysis system that can be integrated into development workflows, CI/CD pipelines, and security scanning processes.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper