Secure by Default: Integrating Trivy Vulnerability Scanning into Java CI Pipelines

In modern software development, security can't be an afterthought. Trivy has emerged as a comprehensive, easy-to-use vulnerability scanner that fits perfectly into CI/CD pipelines. For Java teams, integrating Trivy ensures that vulnerabilities in dependencies, container images, and infrastructure code are caught early, before they reach production.

Why Trivy for Java Applications?

Trivy's advantages for Java teams:

  • Comprehensive scanning - JAR files, Maven/Gradle dependencies, Docker images, Kubernetes manifests
  • Accuracy - Minimal false positives compared to other scanners
  • Speed - Fast scanning suitable for CI pipeline constraints
  • Zero-config - Works out of the box with sensible defaults
  • Open source - No licensing costs or limitations

Trivy Scanning Targets for Java

  1. Dependency Scanning - Maven/Gradle dependencies for known CVEs
  2. Container Scanning - Docker images for OS packages and library vulnerabilities
  3. Infrastructure Scanning - Kubernetes manifests, Dockerfiles, and Helm charts
  4. SBOM Generation - Software Bill of Materials for compliance and audit

GitHub Actions Integration

1. Complete Java CI Pipeline with Trivy

# .github/workflows/ci-security.yml
name: Java CI with Trivy Security Scanning
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Run tests
run: mvn test
trivy-dependency-scan:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build application
run: mvn compile -DskipTests
- name: Run Trivy vulnerability scanner on dependencies
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results-dependency.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results-dependency.sarif'
trivy-container-scan:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build Docker image
run: |
docker build -t my-java-app:${{ github.sha }} .
docker tag my-java-app:${{ github.sha }} my-java-app:latest
- name: Run Trivy container scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'image'
scan-ref: 'my-java-app:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results-container.sarif'
severity: 'CRITICAL,HIGH,MEDIUM'
- name: Upload container scan results
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results-container.sarif'
security-gate:
runs-on: ubuntu-latest
needs: [trivy-dependency-scan, trivy-container-scan]
steps:
- name: Check Trivy results
run: |
echo "Security scans completed"
echo "Check GitHub Security tab for detailed results"

2. Advanced Security Pipeline with Quality Gates

# .github/workflows/security-scan.yml
name: Advanced Security Scanning
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday
workflow_dispatch: # Manual trigger
jobs:
comprehensive-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build and test
run: mvn clean compile test -DskipIntegrationTests
- name: Download Trivy
run: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.45.0
- name: FS Scan with custom config
run: |
trivy filesystem . \
--format json \
--output trivy-fs-report.json \
--severity CRITICAL,HIGH,MEDIUM \
--exit-code 0 \
--ignorefile .trivyignore
- name: SBOM Generation
run: |
trivy filesystem . \
--format cyclonedx \
--output sbom.json
- name: Build container image
run: docker build -t my-java-app:security-scan .
- name: Container Scan with threshold
run: |
trivy image \
--exit-code 1 \
--severity CRITICAL,HIGH \
--ignore-unfixed \
my-java-app:security-scan
- name: Upload SBOM
uses: actions/upload-artifact@v3
with:
name: sbom
path: sbom.json
- name: Generate Security Report
run: |
echo "# Security Scan Report" > security-report.md
echo "## Dependency Vulnerabilities" >> security-report.md
trivy filesystem . --format template --template "@contrib/gitlab.tpl" --output security-report.md
cat security-report.md

Jenkins Pipeline Integration

1. Jenkinsfile with Trivy Scanning

// Jenkinsfile
pipeline {
agent any
tools {
maven 'Maven-3.8'
jdk 'OpenJDK-17'
}
environment {
TRIVY_VERSION = '0.45.0'
DOCKER_IMAGE = "my-company/my-java-app:${env.BUILD_ID}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build and Test') {
steps {
sh 'mvn clean compile test'
}
}
stage('Dependency Security Scan') {
steps {
script {
// Download and install Trivy
sh """
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v${TRIVY_VERSION}
"""
// Scan dependencies
sh """
trivy filesystem . \
--format json \
--output trivy-dependency-report.json \
--severity CRITICAL,HIGH \
--exit-code 0
"""
// Parse results and set quality gate
def scanResults = readJSON file: 'trivy-dependency-report.json'
def criticalVulns = scanResults.Results?.findAll { it.Vulnerabilities?.any { it.Severity == 'CRITICAL' } }?.size() ?: 0
if (criticalVulns > 0) {
unstable "Found ${criticalVulns} critical vulnerabilities in dependencies"
}
}
}
post {
always {
archiveArtifacts artifacts: 'trivy-dependency-report.json', fingerprint: true
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: '.',
reportFiles: 'trivy-dependency-report.json',
reportName: 'Trivy Dependency Report'
])
}
}
}
stage('Build Container') {
steps {
sh "docker build -t ${DOCKER_IMAGE} ."
}
}
stage('Container Security Scan') {
steps {
script {
sh """
trivy image \
--format template \
--template @contrib/html.tpl \
--output trivy-container-report.html \
--exit-code 0 \
--severity CRITICAL,HIGH,MEDIUM \
${DOCKER_IMAGE}
"""
// Check for critical vulnerabilities that should fail build
sh """
trivy image \
--exit-code 1 \
--severity CRITICAL \
--ignore-unfixed \
${DOCKER_IMAGE} || echo "Critical vulnerabilities found - check report"
"""
}
}
post {
always {
archiveArtifacts artifacts: 'trivy-container-report.html', fingerprint: true
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: '.',
reportFiles: 'trivy-container-report.html',
reportName: 'Trivy Container Report'
])
}
}
}
stage('SBOM Generation') {
steps {
sh """
trivy image \
--format cyclonedx \
--output sbom-${env.BUILD_ID}.json \
${DOCKER_IMAGE}
"""
}
post {
always {
archiveArtifacts artifacts: "sbom-${env.BUILD_ID}.json", fingerprint: true
}
}
}
}
post {
always {
emailext (
subject: "Build ${currentBuild.result}: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: """
Security scan completed for ${env.JOB_NAME} build ${env.BUILD_NUMBER}
Build Result: ${currentBuild.result}
Build URL: ${env.BUILD_URL}
Check Trivy reports for vulnerability details.
""",
to: "${env.BUILD_USER_EMAIL}",
attachLog: true
)
}
}
}

GitLab CI Integration

1. .gitlab-ci.yml with Trivy

# .gitlab-ci.yml
image: maven:3.8-openjdk-17
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
stages:
- test
- security
- build
- container-security
cache:
paths:
- .m2/repository
- target/
test:
stage: test
script:
- mvn clean test
trivy-dependency-scan:
stage: security
image: aquasec/trivy:0.45.0
dependencies: []
script:
- trivy filesystem . --format gitlab --output gl-dependency-scanning-report.json
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json
allow_failure: true
trivy-container-scan:
stage: container-security
image: docker:latest
services:
- docker:dind
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCK_IMAGE
- |
apk add --no-cache curl
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.45.0
- trivy image --exit-code 0 --format template --template "@contrib/gitlab.tpl" --output gl-container-scanning-report.json $DOCKER_IMAGE
# Fail build on critical vulnerabilities
- trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed $DOCKER_IMAGE
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
only:
- main
- develop
generate-sbom:
stage: security
image: aquasec/trivy:0.45.0
script:
- trivy filesystem . --format cyclonedx --output sbom.json
artifacts:
paths:
- sbom.json
expire_in: 1 week

Java-Specific Trivy Configuration

1. Custom Trivy Configuration File

# .trivy.yaml
db:
repository: aquasecurity/trivy-db
skip-update: false
cache:
dir: /tmp/trivy
scan:
skip-dirs:
- .git
- node_modules
- tmp
skip-files:
- package-lock.json
- yarn.lock
severity:
- CRITICAL
- HIGH
- MEDIUM
vulnerability:
type:
- os
- library
format: table
ignorefile: .trivyignore
exit-code: 1
ignore-policy: .trivyignorepolicy

2. Trivy Ignore File for Java

# .trivyignore
# Ignore specific vulnerabilities with justification
# Spring Framework vulnerability - mitigated by our usage pattern
CVE-2022-22965
# Log4j 1.x - we use Log4j 2.x
CVE-2021-44228
CVE-2021-45046
# Jackson Databind - false positive in our usage
CVE-2020-36518 until=2024-12-31
# Acceptable risk for legacy library
CVE-2019-12384

3. Custom Ignore Policies

# .trivyignorepolicy
ignoreRules:
- vulnerability: CVE-2023-12345
reason: "Vulnerability in test scope dependency"
expires: "2024-12-31"
- vulnerability: CVE-2023-54321
reason: "Mitigated by network configuration"
expires: "never"

Java Application Security Helper

1. Maven Plugin Integration

// SecurityScanHelper.java
@Component
public class SecurityScanHelper {
private final ProcessBuilder processBuilder;
public SecurityScanHelper() {
this.processBuilder = new ProcessBuilder();
}
public ScanResult runDependencyScan() throws IOException, InterruptedException {
List<String> command = Arrays.asList(
"trivy", "filesystem", ".",
"--format", "json",
"--severity", "CRITICAL,HIGH",
"--exit-code", "0"
);
Process process = processBuilder.command(command).start();
String output = new String(process.getInputStream().readAllBytes());
String error = new String(process.getErrorStream().readAllBytes());
int exitCode = process.waitFor();
if (exitCode != 0 && exitCode != 1) { // Trivy returns 1 when vulnerabilities found
throw new SecurityScanException("Trivy scan failed: " + error);
}
return parseScanResults(output);
}
public ScanResult runContainerScan(String imageName) throws IOException, InterruptedException {
List<String> command = Arrays.asList(
"trivy", "image",
"--format", "json",
"--severity", "CRITICAL,HIGH,MEDIUM",
"--exit-code", "0",
imageName
);
Process process = processBuilder.command(command).start();
String output = new String(process.getInputStream().readAllBytes());
int exitCode = process.waitFor();
if (exitCode != 0 && exitCode != 1) {
throw new SecurityScanException("Container scan failed");
}
return parseScanResults(output);
}
public void generateSBOM(String outputPath) throws IOException, InterruptedException {
List<String> command = Arrays.asList(
"trivy", "filesystem", ".",
"--format", "cyclonedx",
"--output", outputPath
);
Process process = processBuilder.command(command).start();
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new SecurityScanException("SBOM generation failed");
}
}
private ScanResult parseScanResults(String jsonOutput) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(jsonOutput, ScanResult.class);
} catch (Exception e) {
throw new SecurityScanException("Failed to parse scan results", e);
}
}
public static class ScanResult {
private List< Vulnerability> vulnerabilities;
private int criticalCount;
private int highCount;
private int mediumCount;
// Getters and setters
public List<Vulnerability> getVulnerabilities() { return vulnerabilities; }
public void setVulnerabilities(List<Vulnerability> vulnerabilities) { 
this.vulnerabilities = vulnerabilities;
this.criticalCount = (int) vulnerabilities.stream()
.filter(v -> "CRITICAL".equals(v.getSeverity())).count();
this.highCount = (int) vulnerabilities.stream()
.filter(v -> "HIGH".equals(v.getSeverity())).count();
this.mediumCount = (int) vulnerabilities.stream()
.filter(v -> "MEDIUM".equals(v.getSeverity())).count();
}
public boolean hasCriticalVulnerabilities() {
return criticalCount > 0;
}
public boolean hasHighOrCriticalVulnerabilities() {
return criticalCount > 0 || highCount > 0;
}
}
public static class Vulnerability {
private String vulnerabilityID;
private String pkgName;
private String installedVersion;
private String fixedVersion;
private String severity;
private String title;
private String description;
// Getters and setters
}
}

2. Secure Dockerfile for Java Applications

# Dockerfile
FROM eclipse-temurin:17-jre-alpine@sha256:1234567890abcdef
# Security: Run as non-root user
RUN addgroup -S spring && adduser -S spring -G spring
USER spring
# Security: Copy application with correct permissions
COPY --chown=spring:spring target/*.jar app.jar
# Security: Use secure JVM options
ENV JAVA_OPTS="-Djava.security.egd=file:/dev/./urandom -XX:+UseContainerSupport"
# Security: Expose on non-privileged port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]

Quality Gates and Reporting

1. Security Quality Gate

@Component
public class SecurityQualityGate {
private final SecurityScanHelper scanHelper;
public SecurityQualityGate(SecurityScanHelper scanHelper) {
this.scanHelper = scanHelper;
}
public QualityGateResult evaluateBuild() {
try {
ScanResult dependencyScan = scanHelper.runDependencyScan();
ScanResult containerScan = scanHelper.runContainerScan("my-app:latest");
QualityGateResult result = new QualityGateResult();
result.setDependencyScan(dependencyScan);
result.setContainerScan(containerScan);
result.setPassed(evaluateResults(dependencyScan, containerScan));
return result;
} catch (Exception e) {
throw new QualityGateException("Security evaluation failed", e);
}
}
private boolean evaluateResults(ScanResult dependencyScan, ScanResult containerScan) {
// Fail quality gate if critical vulnerabilities found
if (dependencyScan.hasCriticalVulnerabilities() || 
containerScan.hasCriticalVulnerabilities()) {
return false;
}
// Warn but pass for high vulnerabilities (configurable threshold)
int totalHighVulns = dependencyScan.getHighCount() + containerScan.getHighCount();
if (totalHighVulns > 5) { // Configurable threshold
return false;
}
return true;
}
public String generateSecurityReport(QualityGateResult result) {
StringBuilder report = new StringBuilder();
report.append("# Security Quality Gate Report\n\n");
report.append("## Dependency Scan Results\n");
report.append("- Critical: ").append(result.getDependencyScan().getCriticalCount()).append("\n");
report.append("- High: ").append(result.getDependencyScan().getHighCount()).append("\n");
report.append("- Medium: ").append(result.getDependencyScan().getMediumCount()).append("\n");
report.append("\n## Container Scan Results\n");
report.append("- Critical: ").append(result.getContainerScan().getCriticalCount()).append("\n");
report.append("- High: ").append(result.getContainerScan().getHighCount()).append("\n");
report.append("- Medium: ").append(result.getContainerScan().getMediumCount()).append("\n");
report.append("\n## Quality Gate: ").append(result.isPassed() ? "PASSED ✅" : "FAILED ❌").append("\n");
return report.toString();
}
}

Best Practices for Trivy in Java CI

  1. Scan Early, Scan Often - Integrate scanning in development and CI
  2. Fail Build on Critical - Block deployments with critical vulnerabilities
  3. Use SBOMs - Generate Software Bill of Materials for compliance
  4. Regular Updates - Keep Trivy and vulnerability databases updated
  5. Custom Ignore Policies - Document and justify ignored vulnerabilities
  6. Multi-stage Scanning - Scan dependencies, containers, and infrastructure
  7. Security Gates - Implement quality gates based on vulnerability severity
  8. Reporting - Generate human-readable reports for different stakeholders

Conclusion

Integrating Trivy into Java CI pipelines transforms security from a periodic audit to a continuous, automated process. By catching vulnerabilities at every stage—dependencies, container images, and infrastructure—Java teams can significantly reduce their attack surface and maintain secure software delivery.

The key benefits include:

  • Early Detection - Find vulnerabilities before they reach production
  • Comprehensive Coverage - Scan all components of your Java application
  • Developer-Friendly - Integrates seamlessly with existing CI/CD tools
  • Actionable Results - Clear reporting with severity levels and remediation guidance
  • Compliance Ready - SBOM generation for regulatory requirements

By implementing the patterns and configurations shown here, Java teams can build security into their development process, ensuring that every release meets organizational security standards without slowing down delivery.


Leave a Reply

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


Macro Nepal Helper