Introduction to Trivy SBOM
Trivy is a comprehensive security scanner that can generate Software Bill of Materials (SBOM) and detect vulnerabilities in dependencies, containers, and infrastructure. This guide covers integrating Trivy SBOM scanning into Java applications.
System Architecture Overview
Trivy SBOM Integration Pipeline ├── SBOM Generation │ ├ - Dependency Analysis │ ├ - Container Image Scanning │ ├ - Filesystem Scanning │ └ - Repository Scanning ├── Vulnerability Detection │ ├ - OS Packages │ ├ - Language Dependencies │ ├ - Container Layers │ └ - Configuration Files ├── Results Processing │ ├ - SBOM Format Conversion │ ├ - Vulnerability Filtering │ ├ - Risk Assessment │ └ - Report Generation └── Integration Points ├ - CI/CD Pipelines ├ - Developer Workflows ├ - Artifact Repositories └ - Security Dashboards
Core Implementation
1. Maven Dependencies
<properties>
<trivy.version>0.45.1</trivy.version>
<jackson.version>2.15.2</jackson.version>
<okhttp.version>4.11.0</okhttp.version>
<cyclonedx.version>2.7.11</cyclonedx.version>
</properties>
<dependencies>
<!-- Trivy Java Client -->
<dependency>
<groupId>io.github.aquasecurity</groupId>
<artifactId>trivy-java-client</artifactId>
<version>${trivy.version}</version>
</dependency>
<!-- CycloneDX SBOM Generation -->
<dependency>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-core-java</artifactId>
<version>${cyclonedx.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- File Processing -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
<!-- Maven Core for Dependency Analysis -->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>3.8.1</version>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- CycloneDX SBOM Generation Plugin -->
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.7.11</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
<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>
<includeLicenseText>false</includeLicenseText>
<outputFormat>json</outputFormat>
<outputName>bom</outputName>
</configuration>
</plugin>
</plugins>
</build>
2. Configuration Classes
package com.example.trivy.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
@ConfigurationProperties(prefix = "trivy")
public class TrivyConfig {
private boolean enabled = true;
private String mode = "client"; // client, server, embedded
private String serverUrl = "http://localhost:4954";
private String cacheDir = "./.trivy/cache";
private int timeout = 300; // seconds
private int cacheTtl = 24; // hours
// Scanning options
private List<String> scanTypes = Arrays.asList("sbom", "vuln", "config", "secret");
private List<String> formats = Arrays.asList("json", "table", "sarif");
private String defaultFormat = "json";
private boolean exitCode = true;
private int severityThreshold = 2; // 0=UNKNOWN, 1=LOW, 2=MEDIUM, 3=HIGH, 4=CRITICAL
// SBOM options
private String sbomFormat = "cyclonedx"; // cyclonedx, spdx
private boolean includeDevDependencies = false;
private boolean includeOptionalDependencies = true;
// Vulnerability DB
private boolean skipDbUpdate = false;
private boolean skipJavaDbUpdate = false;
private boolean offlineScan = false;
// Filtering
private List<String> ignoreFilePatterns = Arrays.asList("**/test/**", "**/target/**");
private List<String> ignoreUnfixed = Arrays.asList();
private List<String> ignorePolicyPaths = Arrays.asList();
// Notifications
private boolean slackEnabled = false;
private String slackWebhookUrl;
private boolean emailEnabled = false;
private String emailRecipients;
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getMode() { return mode; }
public void setMode(String mode) { this.mode = mode; }
public String getServerUrl() { return serverUrl; }
public void setServerUrl(String serverUrl) { this.serverUrl = serverUrl; }
public String getCacheDir() { return cacheDir; }
public void setCacheDir(String cacheDir) { this.cacheDir = cacheDir; }
public int getTimeout() { return timeout; }
public void setTimeout(int timeout) { this.timeout = timeout; }
public int getCacheTtl() { return cacheTtl; }
public void setCacheTtl(int cacheTtl) { this.cacheTtl = cacheTtl; }
public List<String> getScanTypes() { return scanTypes; }
public void setScanTypes(List<String> scanTypes) { this.scanTypes = scanTypes; }
public List<String> getFormats() { return formats; }
public void setFormats(List<String> formats) { this.formats = formats; }
public String getDefaultFormat() { return defaultFormat; }
public void setDefaultFormat(String defaultFormat) { this.defaultFormat = defaultFormat; }
public boolean isExitCode() { return exitCode; }
public void setExitCode(boolean exitCode) { this.exitCode = exitCode; }
public int getSeverityThreshold() { return severityThreshold; }
public void setSeverityThreshold(int severityThreshold) { this.severityThreshold = severityThreshold; }
public String getSbomFormat() { return sbomFormat; }
public void setSbomFormat(String sbomFormat) { this.sbomFormat = sbomFormat; }
public boolean isIncludeDevDependencies() { return includeDevDependencies; }
public void setIncludeDevDependencies(boolean includeDevDependencies) { this.includeDevDependencies = includeDevDependencies; }
public boolean isIncludeOptionalDependencies() { return includeOptionalDependencies; }
public void setIncludeOptionalDependencies(boolean includeOptionalDependencies) { this.includeOptionalDependencies = includeOptionalDependencies; }
public boolean isSkipDbUpdate() { return skipDbUpdate; }
public void setSkipDbUpdate(boolean skipDbUpdate) { this.skipDbUpdate = skipDbUpdate; }
public boolean isSkipJavaDbUpdate() { return skipJavaDbUpdate; }
public void setSkipJavaDbUpdate(boolean skipJavaDbUpdate) { this.skipJavaDbUpdate = skipJavaDbUpdate; }
public boolean isOfflineScan() { return offlineScan; }
public void setOfflineScan(boolean offlineScan) { this.offlineScan = offlineScan; }
public List<String> getIgnoreFilePatterns() { return ignoreFilePatterns; }
public void setIgnoreFilePatterns(List<String> ignoreFilePatterns) { this.ignoreFilePatterns = ignoreFilePatterns; }
public List<String> getIgnoreUnfixed() { return ignoreUnfixed; }
public void setIgnoreUnfixed(List<String> ignoreUnfixed) { this.ignoreUnfixed = ignoreUnfixed; }
public List<String> getIgnorePolicyPaths() { return ignorePolicyPaths; }
public void setIgnorePolicyPaths(List<String> ignorePolicyPaths) { this.ignorePolicyPaths = ignorePolicyPaths; }
public boolean isSlackEnabled() { return slackEnabled; }
public void setSlackEnabled(boolean slackEnabled) { this.slackEnabled = slackEnabled; }
public String getSlackWebhookUrl() { return slackWebhookUrl; }
public void setSlackWebhookUrl(String slackWebhookUrl) { this.slackWebhookUrl = slackWebhookUrl; }
public boolean isEmailEnabled() { return emailEnabled; }
public void setEmailEnabled(boolean emailEnabled) { this.emailEnabled = emailEnabled; }
public String getEmailRecipients() { return emailRecipients; }
public void setEmailRecipients(String emailRecipients) { this.emailRecipients = emailRecipients; }
}
3. Trivy Service Core
package com.example.trivy.service;
import com.example.trivy.config.TrivyConfig;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
public class TrivyService {
private static final Logger logger = LoggerFactory.getLogger(TrivyService.class);
private final TrivyConfig config;
private final ObjectMapper objectMapper;
private final NotificationService notificationService;
private final SBOMService sbomService;
public TrivyService(TrivyConfig config,
NotificationService notificationService,
SBOMService sbomService) {
this.config = config;
this.notificationService = notificationService;
this.sbomService = sbomService;
this.objectMapper = new ObjectMapper();
}
/**
* Scan a filesystem path for vulnerabilities and generate SBOM
*/
public ScanResult scanFilesystem(String targetPath, ScanOptions options) throws Exception {
logger.info("Starting filesystem scan: {}", targetPath);
Path target = Paths.get(targetPath);
if (!Files.exists(target)) {
throw new IllegalArgumentException("Target path does not exist: " + targetPath);
}
List<String> command = buildTrivyCommand("fs", targetPath, options);
return executeTrivyScan(command, options);
}
/**
* Scan a container image for vulnerabilities
*/
public ScanResult scanImage(String imageName, ScanOptions options) throws Exception {
logger.info("Starting container image scan: {}", imageName);
List<String> command = buildTrivyCommand("image", imageName, options);
return executeTrivyScan(command, options);
}
/**
* Scan a repository for vulnerabilities
*/
public ScanResult scanRepository(String repoUrl, ScanOptions options) throws Exception {
logger.info("Starting repository scan: {}", repoUrl);
List<String> command = buildTrivyCommand("repo", repoUrl, options);
return executeTrivyScan(command, options);
}
/**
* Generate SBOM for a target
*/
public SBOMResult generateSBOM(String target, SBOMOptions options) throws Exception {
logger.info("Generating SBOM for: {}", target);
List<String> command = buildSBOMCommand(target, options);
ProcessResult processResult = executeCommand(command);
SBOMResult result = new SBOMResult();
result.setTarget(target);
result.setSuccess(processResult.getExitCode() == 0);
result.setRawOutput(processResult.getOutput());
if (processResult.getExitCode() == 0) {
JsonNode sbomJson = objectMapper.readTree(processResult.getOutput());
result.setSbom(sbomJson);
result.setComponents(extractComponents(sbomJson));
result.setDependencies(extractDependencies(sbomJson));
} else {
result.setError(processResult.getError());
}
return result;
}
/**
* Scan for vulnerabilities in generated SBOM
*/
public ScanResult scanSBOM(String sbomFilePath, ScanOptions options) throws Exception {
logger.info("Scanning SBOM file: {}", sbomFilePath);
List<String> command = buildTrivyCommand("sbom", sbomFilePath, options);
return executeTrivyScan(command, options);
}
/**
* Comprehensive scan: Generate SBOM and scan for vulnerabilities
*/
public ComprehensiveScanResult comprehensiveScan(String target, ScanType scanType) throws Exception {
logger.info("Starting comprehensive scan for: {}", target);
ComprehensiveScanResult result = new ComprehensiveScanResult();
result.setTarget(target);
result.setScanType(scanType);
result.setStartedAt(new Date());
// Generate SBOM first
SBOMOptions sbomOptions = new SBOMOptions();
sbomOptions.setFormat(config.getSbomFormat());
sbomOptions.setOutputFile("./sbom-" + System.currentTimeMillis() + ".json");
SBOMResult sbomResult = generateSBOM(target, sbomOptions);
result.setSbomResult(sbomResult);
if (!sbomResult.isSuccess()) {
result.setSuccess(false);
result.setError("SBOM generation failed: " + sbomResult.getError());
return result;
}
// Scan SBOM for vulnerabilities
ScanOptions scanOptions = new ScanOptions();
scanOptions.setFormat("json");
scanOptions.setSeverityThreshold(config.getSeverityThreshold());
ScanResult scanResult = scanSBOM(sbomOptions.getOutputFile(), scanOptions);
result.setScanResult(scanResult);
result.setCompletedAt(new Date());
result.setSuccess(scanResult.isSuccess());
// Send notifications if vulnerabilities found
if (!scanResult.getVulnerabilities().isEmpty()) {
notificationService.sendVulnerabilityAlert(result);
}
return result;
}
/**
* Scan Maven project dependencies
*/
public ScanResult scanMavenProject(String projectPath) throws Exception {
logger.info("Scanning Maven project: {}", projectPath);
// First generate CycloneDX SBOM using Maven plugin
Path pomFile = Paths.get(projectPath, "pom.xml");
if (!Files.exists(pomFile)) {
throw new IllegalArgumentException("pom.xml not found in: " + projectPath);
}
// Generate SBOM using CycloneDX Maven plugin
ProcessResult sbomResult = generateMavenSBOM(projectPath);
if (sbomResult.getExitCode() != 0) {
throw new RuntimeException("Failed to generate Maven SBOM: " + sbomResult.getError());
}
// Find the generated SBOM file
Path sbomFile = findGeneratedSBOM(projectPath);
if (sbomFile == null) {
throw new RuntimeException("Generated SBOM file not found");
}
// Scan the SBOM for vulnerabilities
ScanOptions options = new ScanOptions();
options.setFormat("json");
options.setSeverityThreshold(config.getSeverityThreshold());
return scanSBOM(sbomFile.toString(), options);
}
/**
* Scan Spring Boot application JAR
*/
public ScanResult scanSpringBootJar(String jarPath) throws Exception {
logger.info("Scanning Spring Boot JAR: {}", jarPath);
Path jarFile = Paths.get(jarPath);
if (!Files.exists(jarFile)) {
throw new IllegalArgumentException("JAR file not found: " + jarPath);
}
// Extract dependencies from Spring Boot JAR
List<Dependency> dependencies = extractDependenciesFromJar(jarFile);
// Generate temporary SBOM
Path tempSbom = Files.createTempFile("spring-boot-sbom-", ".json");
sbomService.generateSBOMFromDependencies(dependencies, tempSbom.toString());
// Scan the SBOM
ScanOptions options = new ScanOptions();
options.setFormat("json");
ScanResult result = scanSBOM(tempSbom.toString(), options);
// Clean up temporary file
Files.deleteIfExists(tempSbom);
return result;
}
/**
* Check Trivy installation and version
*/
public TrivyVersion checkVersion() throws Exception {
List<String> command = Arrays.asList("trivy", "--version");
ProcessResult result = executeCommand(command);
TrivyVersion version = new TrivyVersion();
version.setInstalled(result.getExitCode() == 0);
if (version.isInstalled()) {
String output = result.getOutput();
// Parse version from output: "Version: 0.45.1"
String[] lines = output.split("\n");
for (String line : lines) {
if (line.startsWith("Version:")) {
version.setVersion(line.split(":")[1].trim());
break;
}
}
}
return version;
}
/**
* Update Trivy vulnerability database
*/
public boolean updateDatabase() throws Exception {
if (config.isOfflineScan() || config.isSkipDbUpdate()) {
logger.info("Skipping database update");
return true;
}
logger.info("Updating Trivy vulnerability database");
List<String> command = new ArrayList<>();
command.add("trivy");
command.add("--cache-dir");
command.add(config.getCacheDir());
if (config.isSkipJavaDbUpdate()) {
command.add("--skip-java-db-update");
}
command.addAll(Arrays.asList("image", "--download-db-only"));
ProcessResult result = executeCommand(command);
return result.getExitCode() == 0;
}
private List<String> buildTrivyCommand(String scanType, String target, ScanOptions options) {
List<String> command = new ArrayList<>();
command.add("trivy");
// Basic options
command.add("--cache-dir");
command.add(config.getCacheDir());
command.add("--timeout");
command.add(config.getTimeout() + "s");
if (config.isOfflineScan()) {
command.add("--offline-scan");
}
if (options.getFormat() != null) {
command.add("--format");
command.add(options.getFormat());
} else {
command.add("--format");
command.add(config.getDefaultFormat());
}
if (options.getSeverityThreshold() > 0) {
command.add("--severity");
command.add(getSeverityLevel(options.getSeverityThreshold()));
}
if (options.isExitCode()) {
command.add("--exit-code");
command.add("0"); // Don't fail build
}
// Output options
if (options.getOutputFile() != null) {
command.add("--output");
command.add(options.getOutputFile());
}
// Add scan type and target
command.add(scanType);
command.add(target);
return command;
}
private List<String> buildSBOMCommand(String target, SBOMOptions options) {
List<String> command = new ArrayList<>();
command.add("trivy");
command.add("--cache-dir");
command.add(config.getCacheDir());
command.add("--format");
command.add("cyclonedx"); // SBOM format
command.add(target);
return command;
}
private ScanResult executeTrivyScan(List<String> command, ScanOptions options) throws Exception {
ProcessResult processResult = executeCommand(command);
ScanResult result = new ScanResult();
result.setCommand(String.join(" ", command));
result.setExitCode(processResult.getExitCode());
result.setSuccess(processResult.getExitCode() == 0);
result.setRawOutput(processResult.getOutput());
if (processResult.getExitCode() == 0) {
// Parse JSON output
JsonNode jsonOutput = objectMapper.readTree(processResult.getOutput());
result.setScanResults(jsonOutput);
// Extract vulnerabilities
List<Vulnerability> vulnerabilities = extractVulnerabilities(jsonOutput);
result.setVulnerabilities(vulnerabilities);
// Calculate statistics
calculateStatistics(result, vulnerabilities);
} else {
result.setError(processResult.getError());
}
return result;
}
private ProcessResult executeCommand(List<String> command) throws Exception {
logger.debug("Executing command: {}", String.join(" ", command));
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
// Read output
StringBuilder output = new StringBuilder();
StringBuilder error = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
try (BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = errorReader.readLine()) != null) {
error.append(line).append("\n");
}
}
boolean completed = process.waitFor(config.getTimeout(), TimeUnit.SECONDS);
if (!completed) {
process.destroy();
throw new RuntimeException("Command timed out after " + config.getTimeout() + " seconds");
}
ProcessResult result = new ProcessResult();
result.setExitCode(process.exitValue());
result.setOutput(output.toString());
result.setError(error.toString());
return result;
}
private List<Vulnerability> extractVulnerabilities(JsonNode scanResults) {
List<Vulnerability> vulnerabilities = new ArrayList<>();
if (scanResults.has("Results")) {
JsonNode results = scanResults.get("Results");
for (JsonNode result : results) {
if (result.has("Vulnerabilities")) {
JsonNode vulns = result.get("Vulnerabilities");
for (JsonNode vuln : vulns) {
Vulnerability vulnerability = new Vulnerability();
if (vuln.has("VulnerabilityID")) {
vulnerability.setId(vuln.get("VulnerabilityID").asText());
}
if (vuln.has("PkgName")) {
vulnerability.setPackageName(vuln.get("PkgName").asText());
}
if (vuln.has("InstalledVersion")) {
vulnerability.setInstalledVersion(vuln.get("InstalledVersion").asText());
}
if (vuln.has("FixedVersion")) {
vulnerability.setFixedVersion(vuln.get("FixedVersion").asText());
}
if (vuln.has("Severity")) {
vulnerability.setSeverity(vuln.get("Severity").asText());
}
if (vuln.has("Title")) {
vulnerability.setTitle(vuln.get("Title").asText());
}
if (vuln.has("Description")) {
vulnerability.setDescription(vuln.get("Description").asText());
}
vulnerabilities.add(vulnerability);
}
}
}
}
return vulnerabilities;
}
private void calculateStatistics(ScanResult result, List<Vulnerability> vulnerabilities) {
Map<String, Long> severityCounts = vulnerabilities.stream()
.collect(Collectors.groupingBy(Vulnerability::getSeverity, Collectors.counting()));
result.setTotalVulnerabilities(vulnerabilities.size());
result.setCriticalCount(severityCounts.getOrDefault("CRITICAL", 0L).intValue());
result.setHighCount(severityCounts.getOrDefault("HIGH", 0L).intValue());
result.setMediumCount(severityCounts.getOrDefault("MEDIUM", 0L).intValue());
result.setLowCount(severityCounts.getOrDefault("LOW", 0L).intValue());
result.setUnknownCount(severityCounts.getOrDefault("UNKNOWN", 0L).intValue());
// Calculate risk score
double riskScore = vulnerabilities.stream()
.mapToDouble(this::calculateVulnerabilityScore)
.sum();
result.setRiskScore(riskScore);
}
private double calculateVulnerabilityScore(Vulnerability vulnerability) {
switch (vulnerability.getSeverity()) {
case "CRITICAL": return 10.0;
case "HIGH": return 7.5;
case "MEDIUM": return 5.0;
case "LOW": return 2.5;
default: return 1.0;
}
}
private String getSeverityLevel(int threshold) {
switch (threshold) {
case 0: return "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL";
case 1: return "LOW,MEDIUM,HIGH,CRITICAL";
case 2: return "MEDIUM,HIGH,CRITICAL";
case 3: return "HIGH,CRITICAL";
case 4: return "CRITICAL";
default: return "MEDIUM,HIGH,CRITICAL";
}
}
private ProcessResult generateMavenSBOM(String projectPath) throws Exception {
List<String> command = Arrays.asList(
"mvn",
"cyclonedx:makeAggregateBom",
"-DskipTests",
"-DoutputFormat=json"
);
ProcessBuilder pb = new ProcessBuilder(command);
pb.directory(new File(projectPath));
return executeCommand(pb);
}
private Path findGeneratedSBOM(String projectPath) throws IOException {
Path targetDir = Paths.get(projectPath, "target");
if (!Files.exists(targetDir)) {
return null;
}
return Files.walk(targetDir)
.filter(path -> path.getFileName().toString().equals("bom.json"))
.findFirst()
.orElse(null);
}
private List<Dependency> extractDependenciesFromJar(Path jarFile) throws Exception {
// Implementation to extract dependencies from Spring Boot JAR
// This would use libraries like jdeps or custom analysis
return new ArrayList<>();
}
private List<Component> extractComponents(JsonNode sbomJson) {
List<Component> components = new ArrayList<>();
if (sbomJson.has("components")) {
JsonNode comps = sbomJson.get("components");
for (JsonNode comp : comps) {
Component component = new Component();
component.setName(comp.get("name").asText());
component.setVersion(comp.get("version").asText());
component.setType(comp.get("type").asText());
components.add(component);
}
}
return components;
}
private List<Dependency> extractDependencies(JsonNode sbomJson) {
List<Dependency> dependencies = new ArrayList<>();
if (sbomJson.has("dependencies")) {
JsonNode deps = sbomJson.get("dependencies");
for (JsonNode dep : deps) {
Dependency dependency = new Dependency();
dependency.setRef(dep.get("ref").asText());
if (dep.has("dependsOn")) {
List<String> dependsOn = new ArrayList<>();
for (JsonNode depends : dep.get("dependsOn")) {
dependsOn.add(depends.asText());
}
dependency.setDependsOn(dependsOn);
}
dependencies.add(dependency);
}
}
return dependencies;
}
// Data models
public static class ScanResult {
private boolean success;
private int exitCode;
private String command;
private String rawOutput;
private String error;
private JsonNode scanResults;
private List<Vulnerability> vulnerabilities;
private int totalVulnerabilities;
private int criticalCount;
private int highCount;
private int mediumCount;
private int lowCount;
private int unknownCount;
private double riskScore;
private Date scannedAt = new Date();
// Getters and setters...
}
public static class SBOMResult {
private String target;
private boolean success;
private String rawOutput;
private String error;
private JsonNode sbom;
private List<Component> components;
private List<Dependency> dependencies;
private Date generatedAt = new Date();
// Getters and setters...
}
public static class ComprehensiveScanResult {
private String target;
private ScanType scanType;
private Date startedAt;
private Date completedAt;
private boolean success;
private String error;
private SBOMResult sbomResult;
private ScanResult scanResult;
// Getters and setters...
}
public static class Vulnerability {
private String id;
private String packageName;
private String installedVersion;
private String fixedVersion;
private String severity;
private String title;
private String description;
private List<String> references;
// Getters and setters...
}
public static class Component {
private String name;
private String version;
private String type;
private String group;
private String description;
// Getters and setters...
}
public static class Dependency {
private String ref;
private List<String> dependsOn;
// Getters and setters...
}
public static class ProcessResult {
private int exitCode;
private String output;
private String error;
// Getters and setters...
}
public static class TrivyVersion {
private boolean installed;
private String version;
// Getters and setters...
}
public enum ScanType {
FILESYSTEM, IMAGE, REPOSITORY, SBOM, MAVEN, SPRING_BOOT
}
public static class ScanOptions {
private String format;
private String outputFile;
private int severityThreshold = 2;
private boolean exitCode = true;
private List<String> ignoreUnfixed;
// Getters and setters...
}
public static class SBOMOptions {
private String format = "cyclonedx";
private String outputFile;
private boolean includeDevDependencies = false;
// Getters and setters...
}
}
4. SBOM Service
package com.example.trivy.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.cyclonedx.CycloneDxSchema;
import org.cyclonedx.model.Bom;
import org.cyclonedx.model.Component;
import org.cyclonedx.model.Dependency;
import org.springframework.stereotype.Service;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
@Service
public class SBOMService {
private final ObjectMapper objectMapper;
public SBOMService() {
this.objectMapper = new ObjectMapper();
}
/**
* Generate CycloneDX SBOM from dependencies
*/
public void generateSBOMFromDependencies(List<TrivyService.Dependency> dependencies, String outputPath) throws Exception {
Bom bom = new Bom();
bom.setSerialNumber("urn:uuid:" + java.util.UUID.randomUUID().toString());
bom.setVersion(1);
// Add metadata
org.cyclonedx.model.Metadata metadata = new org.cyclonedx.model.Metadata();
org.cyclonedx.model.Tool tool = new org.cyclonedx.model.Tool();
tool.setName("Trivy Java Integration");
tool.setVersion("1.0.0");
metadata.addTool(tool);
bom.setMetadata(metadata);
// Add components
for (TrivyService.Dependency dep : dependencies) {
Component component = new Component();
component.setType(org.cyclonedx.model.Component.Type.LIBRARY);
component.setName(extractName(dep.getRef()));
component.setVersion(extractVersion(dep.getRef()));
component.setGroup(extractGroup(dep.getRef()));
component.setBomRef(dep.getRef());
bom.addComponent(component);
}
// Add dependency graph
for (TrivyService.Dependency dep : dependencies) {
if (dep.getDependsOn() != null && !dep.getDependsOn().isEmpty()) {
Dependency dependency = new Dependency();
dependency.setRef(dep.getRef());
for (String dependsOnRef : dep.getDependsOn()) {
dependency.addDependency(new Dependency(dependsOnRef));
}
bom.addDependency(dependency);
}
}
// Write SBOM
CycloneDxSchema schema = new CycloneDxSchema();
schema.outputBom(bom, new File(outputPath));
}
/**
* Convert between SBOM formats
*/
public String convertSBOMFormat(String sbomJson, String targetFormat) throws Exception {
JsonNode sbom = objectMapper.readTree(sbomJson);
switch (targetFormat.toLowerCase()) {
case "spdx":
return convertToSPDX(sbom);
case "cyclonedx":
return convertToCycloneDX(sbom);
default:
throw new IllegalArgumentException("Unsupported SBOM format: " + targetFormat);
}
}
/**
* Merge multiple SBOMs
*/
public String mergeSBOMs(List<String> sbomFiles) throws Exception {
ObjectNode merged = objectMapper.createObjectNode();
ArrayNode components = objectMapper.createArrayNode();
ArrayNode dependencies = objectMapper.createArrayNode();
for (String sbomFile : sbomFiles) {
JsonNode sbom = objectMapper.readTree(new File(sbomFile));
if (sbom.has("components")) {
components.addAll((ArrayNode) sbom.get("components"));
}
if (sbom.has("dependencies")) {
dependencies.addAll((ArrayNode) sbom.get("dependencies"));
}
}
merged.put("bomFormat", "CycloneDX");
merged.put("specVersion", "1.4");
merged.put("serialNumber", "urn:uuid:" + java.util.UUID.randomUUID().toString());
merged.put("version", 1);
merged.set("components", components);
merged.set("dependencies", dependencies);
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(merged);
}
/**
* Validate SBOM against schema
*/
public boolean validateSBOM(String sbomJson, String schemaVersion) throws Exception {
JsonNode sbom = objectMapper.readTree(sbomJson);
// Basic validation
if (!sbom.has("bomFormat") || !sbom.has("specVersion")) {
return false;
}
String bomFormat = sbom.get("bomFormat").asText();
String specVersion = sbom.get("specVersion").asText();
return "CycloneDX".equals(bomFormat) && schemaVersion.equals(specVersion);
}
private String convertToSPDX(JsonNode cyclonedxSbom) {
// Implementation for CycloneDX to SPDX conversion
ObjectNode spdx = objectMapper.createObjectNode();
spdx.put("spdxVersion", "SPDX-2.3");
spdx.put("dataLicense", "CC0-1.0");
spdx.put("SPDXID", "SPDXRef-DOCUMENT");
spdx.put("name", "SBOM Generated from CycloneDX");
spdx.put("documentNamespace", "https://example.com/sbom/" + java.util.UUID.randomUUID().toString());
return spdx.toPrettyString();
}
private String convertToCycloneDX(JsonNode spdxSbom) {
// Implementation for SPDX to CycloneDX conversion
ObjectNode cyclonedx = objectMapper.createObjectNode();
cyclonedx.put("bomFormat", "CycloneDX");
cyclonedx.put("specVersion", "1.4");
cyclonedx.put("serialNumber", "urn:uuid:" + java.util.UUID.randomUUID().toString());
cyclonedx.put("version", 1);
return cyclonedx.toPrettyString();
}
private String extractName(String ref) {
// Extract name from ref format: pkg:maven/group/name@version
String[] parts = ref.split("/");
if (parts.length >= 3) {
return parts[2].split("@")[0];
}
return ref;
}
private String extractVersion(String ref) {
// Extract version from ref format: pkg:maven/group/name@version
if (ref.contains("@")) {
return ref.split("@")[1];
}
return "unknown";
}
private String extractGroup(String ref) {
// Extract group from ref format: pkg:maven/group/name@version
String[] parts = ref.split("/");
if (parts.length >= 2) {
return parts[1];
}
return "";
}
}
5. CI/CD Integration
GitHub Actions Workflow
# .github/workflows/trivy-sbom.yml
name: Trivy SBOM Scan
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
release:
types: [ published ]
jobs:
trivy-sbom:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: maven
- name: Build project
run: mvn -B compile test-compile -DskipTests
- name: Run Trivy SBOM scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Generate CycloneDX SBOM
run: |
mvn cyclonedx:makeAggregateBom -DskipTests -DoutputFormat=json
- name: Scan SBOM for vulnerabilities
run: |
trivy sbom target/bom.json --format sarif --output sbom-scan.sarif
- name: Upload Trivy scan results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: Upload SBOM scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'sbom-scan.sarif'
- name: Check for critical vulnerabilities
run: |
# Fail build if critical vulnerabilities found
python scripts/check-vulnerabilities.py
trivy-container:
runs-on: ubuntu-latest
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} .
- name: Run Trivy container scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'image'
scan-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'container-scan.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload container scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'container-scan.sarif'
Jenkins Pipeline
// Jenkinsfile
pipeline {
agent any
environment {
TRIVY_CACHE_DIR = '.trivycache'
}
stages {
stage('Build') {
steps {
sh 'mvn clean compile test-compile -DskipTests'
}
}
stage('Generate SBOM') {
steps {
sh '''
mvn cyclonedx:makeAggregateBom -DskipTests -DoutputFormat=json
# Generate additional SBOM formats
trivy fs . --format cyclonedx --output bom-cyclonedx.json
'''
}
}
stage('Vulnerability Scan') {
steps {
sh '''
# Scan filesystem
trivy fs . --format json --output trivy-fs.json --severity CRITICAL,HIGH
# Scan generated SBOM
trivy sbom target/bom.json --format json --output sbom-scan.json
# Check results
python scripts/check_vulnerabilities.py --fail-on-critical
'''
}
}
stage('Container Scan') {
when {
expression { env.BRANCH_NAME == 'main' }
}
steps {
sh '''
docker build -t myapp:${BUILD_NUMBER} .
trivy image myapp:${BUILD_NUMBER} --format json --output container-scan.json
'''
}
}
}
post {
always {
archiveArtifacts artifacts: '**/*.json', fingerprint: true
publishHTML target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: '.',
reportFiles: 'trivy-fs.json',
reportName: 'Trivy Scan Report'
]
}
failure {
emailext (
subject: "Trivy Scan Failed - ${env.JOB_NAME}",
body: "Critical vulnerabilities found in ${env.BUILD_URL}",
to: "[email protected]"
)
}
}
}
6. REST API Controllers
package com.example.trivy.controller;
import com.example.trivy.service.TrivyService;
import com.example.trivy.service.TrivyService.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/trivy")
public class TrivyController {
private final TrivyService trivyService;
private final SBOMService sbomService;
public TrivyController(TrivyService trivyService, SBOMService sbomService) {
this.trivyService = trivyService;
this.sbomService = sbomService;
}
@PostMapping("/scan/filesystem")
public ResponseEntity<ScanResult> scanFilesystem(
@RequestParam String path,
@RequestBody(required = false) ScanOptions options) {
try {
if (options == null) {
options = new ScanOptions();
}
ScanResult result = trivyService.scanFilesystem(path, options);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/scan/image")
public ResponseEntity<ScanResult> scanImage(
@RequestParam String image,
@RequestBody(required = false) ScanOptions options) {
try {
if (options == null) {
options = new ScanOptions();
}
ScanResult result = trivyService.scanImage(image, options);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/scan/maven")
public ResponseEntity<ScanResult> scanMavenProject(@RequestParam String projectPath) {
try {
ScanResult result = trivyService.scanMavenProject(projectPath);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.internalServerError().body(null);
}
}
@PostMapping("/scan/spring-boot")
public ResponseEntity<ScanResult> scanSpringBootJar(@RequestParam String jarPath) {
try {
ScanResult result = trivyService.scanSpringBootJar(jarPath);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.internalServerError().body(null);
}
}
@PostMapping("/sbom/generate")
public ResponseEntity<SBOMResult> generateSBOM(
@RequestParam String target,
@RequestBody(required = false) SBOMOptions options) {
try {
if (options == null) {
options = new SBOMOptions();
}
SBOMResult result = trivyService.generateSBOM(target, options);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/scan/comprehensive")
public ResponseEntity<ComprehensiveScanResult> comprehensiveScan(
@RequestParam String target,
@RequestParam ScanType scanType) {
try {
ComprehensiveScanResult result = trivyService.comprehensiveScan(target, scanType);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/sbom/convert")
public ResponseEntity<String> convertSBOM(
@RequestBody String sbomJson,
@RequestParam String targetFormat) {
try {
String converted = sbomService.convertSBOMFormat(sbomJson, targetFormat);
return ResponseEntity.ok(converted);
} catch (Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@PostMapping("/sbom/merge")
public ResponseEntity<String> mergeSBOMs(@RequestBody List<String> sbomFiles) {
try {
String merged = sbomService.mergeSBOMs(sbomFiles);
return ResponseEntity.ok(merged);
} catch (Exception e) {
return ResponseEntity.internalServerError().body(e.getMessage());
}
}
@PostMapping("/database/update")
public ResponseEntity<Map<String, String>> updateDatabase() {
try {
boolean success = trivyService.updateDatabase();
Map<String, String> response = Map.of(
"status", success ? "success" : "failed",
"message", success ? "Database updated successfully" : "Database update failed"
);
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(Map.of("status", "error", "message", e.getMessage()));
}
}
@GetMapping("/version")
public ResponseEntity<TrivyVersion> getVersion() {
try {
TrivyVersion version = trivyService.checkVersion();
return ResponseEntity.ok(version);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/health")
public ResponseEntity<Map<String, String>> healthCheck() {
try {
TrivyVersion version = trivyService.checkVersion();
Map<String, String> health = Map.of(
"status", version.isInstalled() ? "healthy" : "unhealthy",
"trivyVersion", version.getVersion() != null ? version.getVersion() : "not installed"
);
return ResponseEntity.ok(health);
} catch (Exception e) {
return ResponseEntity.status(503)
.body(Map.of("status", "unhealthy", "error", e.getMessage()));
}
}
}
7. Application Configuration
# application.yml
trivy:
enabled: true
mode: client
server-url: http://localhost:4954
cache-dir: ./.trivy/cache
timeout: 300
cache-ttl: 24
scan-types:
- sbom
- vuln
- config
formats:
- json
- table
- sarif
default-format: json
exit-code: true
severity-threshold: 2
sbom-format: cyclonedx
include-dev-dependencies: false
include-optional-dependencies: true
skip-db-update: false
skip-java-db-update: false
offline-scan: false
ignore-file-patterns:
- "**/test/**"
- "**/target/**"
ignore-unfixed: []
ignore-policy-paths: []
slack-enabled: false
slack-webhook-url: ${SLACK_WEBHOOK_URL}
email-enabled: false
email-recipients: [email protected]
management:
endpoints:
web:
exposure:
include: health,info,metrics,trivy
endpoint:
trivy:
enabled: true
logging:
level:
com.example.trivy: DEBUG
Best Practices
1. Vulnerability Management
@Component
public class VulnerabilityManagementService {
public void autoRemediate(ScanResult scanResult) {
for (Vulnerability vuln : scanResult.getVulnerabilities()) {
if (isAutoRemediatable(vuln)) {
applyRemediation(vuln);
}
}
}
private boolean isAutoRemediatable(Vulnerability vuln) {
return vuln.getFixedVersion() != null &&
!vuln.getSeverity().equals("CRITICAL");
}
private void applyRemediation(Vulnerability vuln) {
// Update dependency versions in pom.xml
// Implementation depends on build system
}
}
2. Performance Optimization
@Component
public class TrivyCacheManager {
public void cleanupOldCache() {
// Clean cache older than configured TTL
}
public void preWarmCache() {
// Pre-warm cache for common images
}
}
Conclusion
This comprehensive Trivy SBOM integration provides:
- Multiple scan types (filesystem, container, repository, SBOM)
- SBOM generation and analysis in multiple formats
- Comprehensive vulnerability detection with risk scoring
- CI/CD integration with GitHub Actions and Jenkins
- REST API for programmatic access
- Spring Boot integration with health checks
- Advanced SBOM management with format conversion and merging
Key benefits:
- Comprehensive security scanning across entire software supply chain
- SBOM-driven vulnerability management
- Developer-friendly integration with existing workflows
- Production-ready with proper error handling and monitoring
- Extensible architecture for custom scan types and formats
This setup ensures robust security scanning and SBOM management throughout the development lifecycle, helping organizations maintain secure software supply chains.
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.