Introduction to Grype
Grype is a powerful vulnerability scanner for container images and filesystems that detects security vulnerabilities in software dependencies. It works by generating a Software Bill of Materials (SBOM) using Syft and then scanning it against multiple vulnerability databases.
Key Features
- Comprehensive Scanning: Supports containers, filesystems, and archives
- Multiple Data Sources: Uses NVD, GitHub Security Advisories, and other sources
- SBOM Integration: Works with Syft-generated SBOMs
- CI/CD Ready: Easy integration into build pipelines
- Custom Matchers: Extensible vulnerability matching
- Multiple Output Formats: JSON, table, cyclonedx, etc.
Implementation Guide
Dependencies and Setup
Maven Configuration (pom.xml)
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>grype-scan-demo</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- Security Scanning Versions --> <cyclonedx.maven.version>2.7.9</cyclonedx.maven.version> <owasp.dependency.check.version>8.4.0</owasp.dependency.check.version> <junit.version>5.9.2</junit.version> <!-- Application Dependencies --> <spring.boot.version>3.0.6</spring.boot.version> <jackson.version>2.15.2</jackson.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- Spring Boot Starters --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- Database --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>${jackson.version}</version> </dependency> <!-- Commonly vulnerable dependencies for testing --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.20.0</version> <!-- Fixed version post Log4Shell --> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.10.0</version> <!-- Fixed version post CVE-2022-42889 --> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> <!-- Fixed version with security patches --> </dependency> <!-- Test Dependencies --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- Spring Boot Maven Plugin --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> <!-- CycloneDX SBOM Generation --> <plugin> <groupId>org.cyclonedx</groupId> <artifactId>cyclonedx-maven-plugin</artifactId> <version>${cyclonedx.maven.version}</version> <executions> <execution> <phase>package</phase> <goals> <goal>makeAggregateBom</goal> </goals> </execution> </executions> <configuration> <projectType>application</projectType> <schemaVersion>1.4</schemaVersion> <includeBomSerialNumber>true</includeBomSerialNumber> <includeCompileScope>true</includeCompileScope> <includeProvidedScope>true</includeProvidedScope> <includeRuntimeScope>true</includeRuntimeScope> <includeSystemScope>true</includeSystemScope> <includeTestScope>false</includeTestScope> <outputFormat>all</outputFormat> <outputName>sbom</outputName> </configuration> </plugin> <!-- OWASP Dependency Check --> <plugin> <groupId>org.owasp</groupId> <artifactId>dependency-check-maven</artifactId> <version>${owasp.dependency.check.version}</version> <executions> <execution> <goals> <goal>check</goal> </goals> </execution> </executions> <configuration> <failBuildOnAnyVulnerability>false</failBuildOnAnyVulnerability> <failBuildOnCVSS>7</failBuildOnCVSS> <suppressionFile>${project.basedir}/security/dependency-check-suppressions.xml</suppressionFile> <format>HTML</format> <formats>HTML,JSON,XML</formats> <cveValidForHours>12</cveValidForHours> </configuration> </plugin> <!-- Maven Surefire Plugin --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0</version> <configuration> <includes> <include>**/*Test.java</include> </includes> </configuration> </plugin> </plugins> </build> </project> Grype Configuration Files
Dockerfile for Application
# Multi-stage build for secure Java application FROM eclipse-temurin:11-jdk as builder WORKDIR /app # Copy Maven wrapper and pom.xml COPY mvnw . COPY .mvn .mvn COPY pom.xml . # Copy source code COPY src ./src # Build the application RUN ./mvnw clean package -DskipTests # Runtime stage FROM eclipse-temurin:11-jre as runtime # Install security tools RUN apt-get update && apt-get install -y \ curl \ ca-certificates \ && rm -rf /var/lib/apt/lists/* # Install Grype RUN curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin # Create non-root user RUN groupadd -r spring && useradd -r -g spring spring USER spring:spring WORKDIR /app # Copy built application COPY --from=builder /app/target/*.jar app.jar # Create directory for security reports RUN mkdir -p /app/security-reports # Expose application 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 # Run the application ENTRYPOINT ["java", "-jar", "app.jar"]
Grype Configuration (grype.yaml)
# Grype configuration file check-for-app-update: false fail-on-severity: high db: auto-update: true cache-dir: "/tmp/grype-db" update-url: "https://toolbox-data.anchore.io/grype/databases/" output: "table" file: # Security scanning configurations catalogers: enabled: - "java" - "apk" - "dpkg" - "rpm" - "ruby" - "python" - "javascript" - "dotnet" # Vulnerability matching configuration match: # Java-specific matchers java: enabled: true use-cpes: true # Distro-specific matchers distro: enabled: true # Language-specific matchers language: enabled: true # Ignore rules for false positives ignore: - vulnerability: "CVE-2018-1258" reason: "Spring Framework false positive - doesn't affect our usage" - vulnerability: "CVE-2022-22965" reason: "Spring4Shell - patched in our Spring Boot version" - vulnerability: "CVE-2020-36518" reason: "Jackson false positive - doesn't affect our usage pattern" # Severity thresholds severity: - critical - high - medium - low # Package exclusion patterns exclude: - path: "**/test/**" - path: "**/target/classes/**" - path: "**/node_modules/**" # Output configurations outputs: - format: "json" file: "security-reports/grype-report.json" - format: "table" file: "security-reports/grype-report.txt" - format: "cyclonedx" file: "security-reports/grype-sbom.xml" - format: "sarif" file: "security-reports/grype-report.sarif" log: level: "warn" structured: true
Security Suppression File (security/dependency-check-suppressions.xml)
<?xml version="1.0" encoding="UTF-8"?> <suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd"> <!-- Suppress Spring Framework false positives --> <suppress> <notes><![CDATA[ False positive for Spring Framework - CVE doesn't affect our usage pattern ]]></notes> <cve>CVE-2018-1258</cve> </suppress> <suppress> <notes><![CDATA[ Spring4Shell - Patched in Spring Boot 2.6.6+ ]]></notes> <cve>CVE-2022-22965</cve> </suppress> <!-- Suppress Jackson false positives --> <suppress> <notes><![CDATA[ Jackson CVE - Doesn't affect our usage pattern with trusted data ]]></notes> <cve>CVE-2020-36518</cve> </suppress> <!-- Suppress test dependencies --> <suppress> <notes><![CDATA[ Suppress vulnerabilities in test dependencies ]]></notes> <filePath regex="true">.*test.*\.jar$</filePath> </suppress> <!-- Suppress H2 database in development --> <suppress> <notes><![CDATA[ H2 database used only in development and testing ]]></notes> <filePath regex="true">.*h2.*\.jar$</filePath> </suppress> <!-- Suppress low severity vulnerabilities --> <suppress base="true"> <notes><![CDATA[ Suppress low severity vulnerabilities for now ]]></notes> <severity>LOW</severity> </suppress> </suppressions>
Java Security Scanner Integration
Grype Scanner Service
package com.example.grype.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.scheduling.annotation.Scheduled; 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.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * Service for running Grype vulnerability scans */ @Service public class GrypeScannerService { private static final Logger logger = LoggerFactory.getLogger(GrypeScannerService.class); @Value("${grype.scan.enabled:true}") private boolean grypeScanEnabled; @Value("${grype.fail.on.high:true}") private boolean failOnHighSeverity; @Value("${grype.reports.directory:target/security-reports}") private String reportsDirectory; @Autowired private ResourceLoader resourceLoader; @Autowired private ObjectMapper objectMapper; private static final String GRYPE_CONFIG = "classpath:grype.yaml"; /** * Run Grype scan on the current application */ public GrypeScanResult runScan() { if (!grypeScanEnabled) { logger.info("Grype scanning is disabled"); return new GrypeScanResult(false, "Scanning disabled"); } try { logger.info("Starting Grype vulnerability scan..."); // Ensure reports directory exists Path reportsPath = Paths.get(reportsDirectory); Files.createDirectories(reportsPath); // Build Grype command List<String> command = buildGrypeCommand(); // Execute Grype scan ProcessBuilder processBuilder = new ProcessBuilder(command); processBuilder.directory(new File(".")); processBuilder.redirectErrorStream(true); Process process = processBuilder.start(); String output = readProcessOutput(process.getInputStream()); int exitCode = process.waitFor(); if (exitCode == 0) { logger.info("Grype scan completed successfully"); return parseScanResults(); } else { logger.error("Grype scan failed with exit code: {}", exitCode); logger.error("Grype output: {}", output); return new GrypeScanResult(true, "Scan failed with exit code: " + exitCode); } } catch (Exception e) { logger.error("Error running Grype scan", e); return new GrypeScanResult(true, "Scan error: " + e.getMessage()); } } /** * Build Grype command with all parameters */ private List<String> buildGrypeCommand() throws IOException { List<String> command = new ArrayList<>(); command.add("grype"); // Add config file if exists Resource configResource = resourceLoader.getResource(GRYPE_CONFIG); if (configResource.exists()) { command.add("--config"); command.add(configResource.getFile().getAbsolutePath()); } // Scan current directory (where the JAR is built) command.add("dir:."); // Output formats command.add("-o"); command.add("json"); command.add("--file"); command.add(reportsDirectory + "/grype-report.json"); // Fail on high severity if configured if (failOnHighSeverity) { command.add("--fail-on"); command.add("high"); } return command; } /** * Parse Grype scan results from JSON report */ private GrypeScanResult parseScanResults() { try { Path reportPath = Paths.get(reportsDirectory, "grype-report.json"); if (!Files.exists(reportPath)) { return new GrypeScanResult(false, "No scan report generated"); } JsonNode report = objectMapper.readTree(reportPath.toFile()); JsonNode matches = report.path("matches"); List<Vulnerability> vulnerabilities = new ArrayList<>(); int criticalCount = 0; int highCount = 0; int mediumCount = 0; int lowCount = 0; for (JsonNode match : matches) { Vulnerability vuln = parseVulnerability(match); vulnerabilities.add(vuln); switch (vuln.getSeverity().toLowerCase()) { case "critical": criticalCount++; break; case "high": highCount++; break; case "medium": mediumCount++; break; case "low": lowCount++; break; } } GrypeScanResult result = new GrypeScanResult( vulnerabilities.isEmpty(), String.format("Found %d vulnerabilities (Critical: %d, High: %d, Medium: %d, Low: %d)", vulnerabilities.size(), criticalCount, highCount, mediumCount, lowCount) ); result.setVulnerabilities(vulnerabilities); result.setCriticalCount(criticalCount); result.setHighCount(highCount); result.setMediumCount(mediumCount); result.setLowCount(lowCount); // Log summary if (criticalCount > 0 || highCount > 0) { logger.warn("CRITICAL VULNERABILITIES DETECTED: {} critical, {} high", criticalCount, highCount); } else if (mediumCount > 0) { logger.info("Scan completed: {} medium severity vulnerabilities found", mediumCount); } else { logger.info("Scan completed: No vulnerabilities found"); } return result; } catch (Exception e) { logger.error("Error parsing Grype scan results", e); return new GrypeScanResult(true, "Error parsing results: " + e.getMessage()); } } /** * Parse individual vulnerability from Grype match */ private Vulnerability parseVulnerability(JsonNode match) { Vulnerability vuln = new Vulnerability(); vuln.setId(match.path("vulnerability").path("id").asText()); vuln.setSeverity(match.path("vulnerability").path("severity").asText()); vuln.setDescription(match.path("vulnerability").path("description").asText()); vuln.setPackageName(match.path("artifact").path("name").asText()); vuln.setPackageVersion(match.path("artifact").path("version").asText()); vuln.setPackageType(match.path("artifact").path("type").asText()); vuln.setFixedInVersion(match.path("vulnerability").path("fix").path("versions").get(0).asText()); vuln.setDataSource(match.path("vulnerability").path("dataSource").asText()); return vuln; } /** * Read process output stream */ private String readProcessOutput(InputStream inputStream) throws IOException { StringBuilder output = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } } return output.toString(); } /** * Scheduled daily vulnerability scan */ @Scheduled(fixedRate = 24, timeUnit = TimeUnit.HOURS) public void scheduledScan() { if (grypeScanEnabled) { logger.info("Running scheduled Grype vulnerability scan..."); GrypeScanResult result = runScan(); if (result.hasVulnerabilities()) { logger.warn("Scheduled scan found vulnerabilities: {}", result.getMessage()); // Could send notification here } } } /** * Scan result data class */ public static class GrypeScanResult { private boolean hasVulnerabilities; private String message; private List<Vulnerability> vulnerabilities; private int criticalCount; private int highCount; private int mediumCount; private int lowCount; public GrypeScanResult(boolean hasVulnerabilities, String message) { this.hasVulnerabilities = hasVulnerabilities; this.message = message; this.vulnerabilities = new ArrayList<>(); } // Getters and setters public boolean hasVulnerabilities() { return hasVulnerabilities; } public void setHasVulnerabilities(boolean hasVulnerabilities) { this.hasVulnerabilities = hasVulnerabilities; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public List<Vulnerability> getVulnerabilities() { return vulnerabilities; } public void setVulnerabilities(List<Vulnerability> vulnerabilities) { this.vulnerabilities = vulnerabilities; } public int getCriticalCount() { return criticalCount; } public void setCriticalCount(int criticalCount) { this.criticalCount = criticalCount; } public int getHighCount() { return highCount; } public void setHighCount(int highCount) { this.highCount = highCount; } public int getMediumCount() { return mediumCount; } public void setMediumCount(int mediumCount) { this.mediumCount = mediumCount; } public int getLowCount() { return lowCount; } public void setLowCount(int lowCount) { this.lowCount = lowCount; } } /** * Vulnerability data class */ public static class Vulnerability { private String id; private String severity; private String description; private String packageName; private String packageVersion; private String packageType; private String fixedInVersion; private String dataSource; // Getters and setters public String getId() { return id; } public void setId(String id) { this.id = id; } public String getSeverity() { return severity; } public void setSeverity(String severity) { this.severity = severity; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getPackageName() { return packageName; } public void setPackageName(String packageName) { this.packageName = packageName; } public String getPackageVersion() { return packageVersion; } public void setPackageVersion(String packageVersion) { this.packageVersion = packageVersion; } public String getPackageType() { return packageType; } public void setPackageType(String packageType) { this.packageType = packageType; } public String getFixedInVersion() { return fixedInVersion; } public void setFixedInVersion(String fixedInVersion) { this.fixedInVersion = fixedInVersion; } public String getDataSource() { return dataSource; } public void setDataSource(String dataSource) { this.dataSource = dataSource; } @Override public String toString() { return String.format("%s: %s (%s) in %s:%s - Fixed in: %s", severity.toUpperCase(), id, description, packageName, packageVersion, fixedInVersion); } } } Security Dashboard Controller
package com.example.grype.controller; import com.example.grype.service.GrypeScannerService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; /** * REST controller for security scanning operations */ @RestController @RequestMapping("/api/security") @PreAuthorize("hasRole('ADMIN')") public class SecurityDashboardController { private static final Logger logger = LoggerFactory.getLogger(SecurityDashboardController.class); @Autowired private GrypeScannerService grypeScannerService; /** * Trigger manual Grype scan */ @PostMapping("/scan") public ResponseEntity<Map<String, Object>> triggerScan() { logger.info("Manual Grype scan triggered via API"); GrypeScannerService.GrypeScanResult result = grypeScannerService.runScan(); Map<String, Object> response = new HashMap<>(); response.put("success", !result.hasVulnerabilities()); response.put("message", result.getMessage()); response.put("vulnerabilities", result.getVulnerabilities()); response.put("summary", Map.of( "critical", result.getCriticalCount(), "high", result.getHighCount(), "medium", result.getMediumCount(), "low", result.getLowCount() )); if (result.hasVulnerabilities()) { return ResponseEntity.ok(response); } else { return ResponseEntity.ok(response); } } /** * Get scan status */ @GetMapping("/status") public ResponseEntity<Map<String, Object>> getScanStatus() { Map<String, Object> status = new HashMap<>(); status.put("scanner", "Grype"); status.put("enabled", true); status.put("lastScan", "2024-01-15T10:30:00Z"); // Would be dynamic in real implementation status.put("databaseUpdated", true); return ResponseEntity.ok(status); } /** * Get vulnerability statistics */ @GetMapping("/statistics") public ResponseEntity<Map<String, Object>> getVulnerabilityStats() { // This would typically query a database of previous scans Map<String, Object> stats = new HashMap<>(); stats.put("totalScans", 42); stats.put("criticalVulnerabilities", 2); stats.put("highVulnerabilities", 15); stats.put("mediumVulnerabilities", 87); stats.put("fixedVulnerabilities", 94); stats.put("scanSuccessRate", 95.2); return ResponseEntity.ok(stats); } } SBOM Management Service
package com.example.grype.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; /** * Service for managing Software Bill of Materials (SBOM) */ @Service public class SBOMService { private static final Logger logger = LoggerFactory.getLogger(SBOMService.class); @Value("${sbom.directory:target}") private String sbomDirectory; @Autowired private ObjectMapper objectMapper; /** * Generate SBOM using CycloneDX Maven plugin */ public boolean generateSBOM() { try { logger.info("Generating Software Bill of Materials..."); // This would typically execute Maven command: mvn cyclonedx:makeAggregateBom // For this example, we'll assume the SBOM is generated during build Path sbomPath = Paths.get(sbomDirectory, "sbom.json"); if (Files.exists(sbomPath)) { logger.info("SBOM generated successfully: {}", sbomPath); return true; } else { logger.warn("SBOM file not found: {}", sbomPath); return false; } } catch (Exception e) { logger.error("Error generating SBOM", e); return false; } } /** * Analyze SBOM for security insights */ public SBOMAnalysis analyzeSBOM() { try { Path sbomPath = Paths.get(sbomDirectory, "sbom.json"); if (!Files.exists(sbomPath)) { logger.warn("SBOM file not found for analysis: {}", sbomPath); return new SBOMAnalysis(false, "SBOM file not found"); } JsonNode sbom = objectMapper.readTree(sbomPath.toFile()); JsonNode components = sbom.path("components"); SBOMAnalysis analysis = new SBOMAnalysis(); analysis.setTotalComponents(components.size()); analysis.setGeneratedDate(LocalDateTime.now()); // Analyze components List<ComponentInfo> componentInfos = new ArrayList<>(); for (JsonNode component : components) { ComponentInfo info = parseComponent(component); componentInfos.add(info); // Count by type analysis.getComponentCounts().merge(info.getType(), 1, Integer::sum); } analysis.setComponents(componentInfos); analysis.setSuccess(true); analysis.setMessage(String.format("Analyzed %d components", components.size())); logger.info("SBOM analysis completed: {} components found", components.size()); return analysis; } catch (Exception e) { logger.error("Error analyzing SBOM", e); return new SBOMAnalysis(false, "Analysis failed: " + e.getMessage()); } } /** * Parse component information from SBOM */ private ComponentInfo parseComponent(JsonNode component) { ComponentInfo info = new ComponentInfo(); info.setName(component.path("name").asText()); info.setVersion(component.path("version").asText()); info.setType(component.path("type").asText()); info.setGroup(component.path("group").asText()); info.setPublisher(component.path("publisher").asText()); info.setDescription(component.path("description").asText()); info.setLicense(component.path("licenses").path(0).path("license").path("id").asText()); // Parse PURL (Package URL) if (component.has("purl")) { info.setPurl(component.path("purl").asText()); } return info; } /** * Compare two SBOMs for changes */ public SBOMComparison compareSBOMs(File oldSBOM, File newSBOM) { try { JsonNode oldComponents = objectMapper.readTree(oldSBOM).path("components"); JsonNode newComponents = objectMapper.readTree(newSBOM).path("components"); SBOMComparison comparison = new SBOMComparison(); // This would implement detailed comparison logic // For now, return basic comparison comparison.setOldComponentCount(oldComponents.size()); comparison.setNewComponentCount(newComponents.size()); comparison.setComponentsAdded(newComponents.size() - oldComponents.size()); // Simplified comparison.setComparisonTime(LocalDateTime.now()); return comparison; } catch (Exception e) { logger.error("Error comparing SBOMs", e); return new SBOMComparison("Comparison failed: " + e.getMessage()); } } /** * SBOM Analysis data class */ public static class SBOMAnalysis { private boolean success; private String message; private int totalComponents; private LocalDateTime generatedDate; private List<ComponentInfo> components; private Map<String, Integer> componentCounts; public SBOMAnalysis() { this.components = new ArrayList<>(); this.componentCounts = new HashMap<>(); } public SBOMAnalysis(boolean success, String message) { this(); this.success = success; this.message = message; } // Getters and setters public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public int getTotalComponents() { return totalComponents; } public void setTotalComponents(int totalComponents) { this.totalComponents = totalComponents; } public LocalDateTime getGeneratedDate() { return generatedDate; } public void setGeneratedDate(LocalDateTime generatedDate) { this.generatedDate = generatedDate; } public List<ComponentInfo> getComponents() { return components; } public void setComponents(List<ComponentInfo> components) { this.components = components; } public Map<String, Integer> getComponentCounts() { return componentCounts; } public void setComponentCounts(Map<String, Integer> componentCounts) { this.componentCounts = componentCounts; } } /** * Component information data class */ public static class ComponentInfo { private String name; private String version; private String type; private String group; private String publisher; private String description; private String license; private String purl; // Getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getGroup() { return group; } public void setGroup(String group) { this.group = group; } public String getPublisher() { return publisher; } public void setPublisher(String publisher) { this.publisher = publisher; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getLicense() { return license; } public void setLicense(String license) { this.license = license; } public String getPurl() { return purl; } public void setPurl(String purl) { this.purl = purl; } @Override public String toString() { return String.format("%s:%s (%s)", name, version, type); } } /** * SBOM Comparison data class */ public static class SBOMComparison { private int oldComponentCount; private int newComponentCount; private int componentsAdded; private int componentsRemoved; private int componentsUpdated; private LocalDateTime comparisonTime; private String error; public SBOMComparison() {} public SBOMComparison(String error) { this.error = error; } // Getters and setters public int getOldComponentCount() { return oldComponentCount; } public void setOldComponentCount(int oldComponentCount) { this.oldComponentCount = oldComponentCount; } public int getNewComponentCount() { return newComponentCount; } public void setNewComponentCount(int newComponentCount) { this.newComponentCount = newComponentCount; } public int getComponentsAdded() { return componentsAdded; } public void setComponentsAdded(int componentsAdded) { this.componentsAdded = componentsAdded; } public int getComponentsRemoved() { return componentsRemoved; } public void setComponentsRemoved(int componentsRemoved) { this.componentsRemoved = componentsRemoved; } public int getComponentsUpdated() { return componentsUpdated; } public void setComponentsUpdated(int componentsUpdated) { this.componentsUpdated = componentsUpdated; } public LocalDateTime getComparisonTime() { return comparisonTime; } public void setComparisonTime(LocalDateTime comparisonTime) { this.comparisonTime = comparisonTime; } public String getError() { return error; } public void setError(String error) { this.error = error; } public boolean hasChanges() { return componentsAdded > 0 || componentsRemoved > 0 || componentsUpdated > 0; } } } CI/CD Integration
GitHub Actions Workflow (.github/workflows/grype-scan.yml)
name: Grype Vulnerability Scan on: push: branches: [ main, develop ] pull_request: branches: [ main ] schedule: - cron: '0 2 * * 1' # Weekly on Monday at 2 AM jobs: security-scan: name: Grype Security Scan runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Java uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' cache: 'maven' - name: Cache Maven packages uses: actions/cache@v3 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - name: Build application run: mvn clean package -DskipTests - name: Install Grype run: | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin grype version - name: Generate SBOM with CycloneDX run: mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom - name: Run Grype Scan run: | mkdir -p security-reports grype dir:. \ --output json \ --file security-reports/grype-report.json \ --fail-on high - name: Run OWASP Dependency Check run: mvn org.owasp:dependency-check-maven:check - name: Analyze Scan Results id: scan-analysis run: | if [ -f "security-reports/grype-report.json" ]; then CRITICAL_COUNT=$(jq '.matches | map(select(.vulnerability.severity == "Critical")) | length' security-reports/grype-report.json) HIGH_COUNT=$(jq '.matches | map(select(.vulnerability.severity == "High")) | length' security-reports/grype-report.json) echo "critical_count=$CRITICAL_COUNT" >> $GITHUB_OUTPUT echo "high_count=$HIGH_COUNT" >> $GITHUB_OUTPUT if [ $CRITICAL_COUNT -gt 0 ] || [ $HIGH_COUNT -gt 0 ]; then echo "has_critical_vulnerabilities=true" >> $GITHUB_OUTPUT else echo "has_critical_vulnerabilities=false" >> $GITHUB_OUTPUT fi else echo "critical_count=0" >> $GITHUB_OUTPUT echo "high_count=0" >> $GITHUB_OUTPUT echo "has_critical_vulnerabilities=false" >> $GITHUB_OUTPUT fi - name: Upload Security Reports uses: actions/upload-artifact@v3 with: name: security-reports path: | security-reports/ target/sbom.* target/dependency-check-report.html - name: Security Scan Summary if: always() run: | echo "## Grype Vulnerability Scan Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Scan Summary" >> $GITHUB_STEP_SUMMARY echo "- **Critical Vulnerabilities:** ${{ steps.scan-analysis.outputs.critical_count }}" >> $GITHUB_STEP_SUMMARY echo "- **High Vulnerabilities:** ${{ steps.scan-analysis.outputs.high_count }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ steps.scan-analysis.outputs.has_critical_vulnerabilities }}" = "true" ]; then echo "### 🚨 Critical Vulnerabilities Detected" >> $GITHUB_STEP_SUMMARY echo "Immediate action required! Review the security reports and update vulnerable dependencies." >> $GITHUB_STEP_SUMMARY else echo "### ✅ No Critical Vulnerabilities" >> $GITHUB_STEP_SUMMARY echo "Security scan passed. No critical or high severity vulnerabilities detected." >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY echo "### Reports Generated" >> $GITHUB_STEP_SUMMARY echo "- Grype Vulnerability Report" >> $GITHUB_STEP_SUMMARY echo "- OWASP Dependency Check Report" >> $GITHUB_STEP_SUMMARY echo "- CycloneDX SBOM" >> $GITHUB_STEP_SUMMARY container-scan: name: Container Image Scan runs-on: ubuntu-latest needs: security-scan steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Build container image run: | docker build -t grype-demo-app:latest . - name: Scan container image with Grype run: | grype grype-demo-app:latest \ --output table \ --fail-on high Jenkins Pipeline (Jenkinsfile)
pipeline { agent any environment { JAVA_HOME = '/usr/lib/jvm/java-11-openjdk' GRYPE_VERSION = '0.70.0' } stages { stage('Checkout') { steps { checkout scm } } stage('Build') { steps { sh 'mvn clean compile -DskipTests' } } stage('Generate SBOM') { steps { sh 'mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom' } post { always { archiveArtifacts artifacts: 'target/sbom.*', fingerprint: true } } } stage('Security Scan') { parallel { stage('Grype Scan') { steps { script { // Install Grype if not present sh ''' if ! command -v grype &> /dev/null; then curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin fi ''' // Run Grype scan sh ''' mkdir -p security-reports grype dir:. \ --output json \ --file security-reports/grype-report.json \ --fail-on high \ || true # Continue even if vulnerabilities found ''' } } post { always { publishHTML([ allowMissing: false, alwaysLinkToLastBuild: true, keepAll: true, reportDir: 'security-reports', reportFiles: 'grype-report.json', reportName: 'Grype Vulnerability Report' ]) } } } stage('OWASP Dependency Check') { steps { sh 'mvn org.owasp:dependency-check-maven:check' } post { always { publishHTML([ allowMissing: false, alwaysLinkToLastBuild: true, keepAll: true, reportDir: 'target', reportFiles: 'dependency-check-report.html', reportName: 'OWASP Dependency Check' ]) } } } } } stage('Analyze Results') { steps { script { // Analyze Grype results def grypeReport = readJSON file: 'security-reports/grype-report.json' def criticalVulns = grypeReport.matches.findAll { it.vulnerability.severity == 'Critical' } def highVulns = grypeReport.matches.findAll { it.vulnerability.severity == 'High' } echo "Security Scan Results:" echo "Critical Vulnerabilities: ${criticalVulns.size()}" echo "High Vulnerabilities: ${highVulns.size()}" // Set build result based on findings if (criticalVulns.size() > 0) { currentBuild.result = 'UNSTABLE' echo "🚨 CRITICAL VULNERABILITIES DETECTED - Immediate action required!" } else if (highVulns.size() > 0) { currentBuild.result = 'UNSTABLE' echo "⚠️ HIGH SEVERITY VULNERABILITIES DETECTED - Review required!" } else { echo "✅ No critical or high severity vulnerabilities detected" } } } } stage('Package') { when { expression { currentBuild.result != 'FAILURE' } } steps { sh 'mvn package -DskipTests' } post { always { archiveArtifacts artifacts: 'target/*.jar', fingerprint: true } } } } post { always { // Clean up workspace cleanWs() } success { emailext ( subject: "SUCCESS: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'", body: """ Security scan completed successfully. Reports: - Grype Vulnerability Scan: ${env.BUILD_URL}grype-vulnerability-report/ - OWASP Dependency Check: ${env.BUILD_URL}owasp-dependency-check/ Check console output for details: ${env.BUILD_URL}console """, to: "${env.BUILD_USER_EMAIL}" ) } unstable { emailext ( subject: "UNSTABLE: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' - Vulnerabilities Detected", body: """ Security scan completed with vulnerabilities detected. High or critical severity vulnerabilities require immediate attention. Check security reports and console output: ${env.BUILD_URL}console Required Actions: 1. Review vulnerability reports 2. Update vulnerable dependencies 3. Re-run security scan """, to: "${env.BUILD_USER_EMAIL}" ) } failure { emailext ( subject: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'", body: """ Security scan failed due to errors. Check console output for error details: ${env.BUILD_URL}console """, to: "${env.BUILD_USER_EMAIL}" ) } } } Security Test Cases
package com.example.grype.security; import com.example.grype.service.GrypeScannerService; import com.example.grype.service.SBOMService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import static org.junit.jupiter.api.Assertions.*; /** * Security scanning integration tests */ @SpringBootTest @TestPropertySource(properties = { "grype.scan.enabled=true", "grype.fail.on.high=false", "sbom.directory=src/test/resources" }) public class SecurityScanningTests { @Autowired private GrypeScannerService grypeScannerService; @Autowired private SBOMService sbomService; @Test public void testGrypeScanExecution() { GrypeScannerService.GrypeScanResult result = grypeScannerService.runScan(); assertNotNull(result); assertNotNull(result.getMessage()); assertNotNull(result.getVulnerabilities()); // Scan should complete (though it might find vulnerabilities) System.out.println("Scan result: " + result.getMessage()); } @Test public void testSBOMGeneration() { boolean sbomGenerated = sbomService.generateSBOM(); // In test environment, this might not generate actual SBOM // but the method should execute without errors assertDoesNotThrow(() -> sbomService.generateSBOM()); } @Test public void testSBOMAnalysis() { SBOMService.SBOMAnalysis analysis = sbomService.analyzeSBOM(); // Analysis should complete without errors assertNotNull(analysis); assertNotNull(analysis.getMessage()); } @Test public void testVulnerabilityParsing() { // Test with sample vulnerability data String sampleVulnerability = """ { "vulnerability": { "id": "CVE-2021-44228", "severity": "Critical", "description": "Log4Shell vulnerability", "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228", "fix": { "versions": ["2.17.0"] } }, "artifact": { "name": "log4j-core", "version": "2.14.1", "type": "java-archive" } } """; // This would test the parsing logic in a real implementation assertTrue(true, "Vulnerability parsing test placeholder"); } @Test public void testSecurityReportsDirectory() { Path reportsDir = Paths.get("security-reports"); // Ensure reports directory can be created assertDoesNotThrow(() -> Files.createDirectories(reportsDir)); // Clean up assertDoesNotThrow(() -> Files.deleteIfExists(reportsDir)); } } Security Monitoring Script
#!/bin/bash # security-monitor.sh set -e echo "🔍 Grype Security Monitor" echo "=========================" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Print colored output print_status() { local color=$1 local message=$2 echo -e "${color}${message}${NC}" } # Configuration REPORTS_DIR="security-reports" SBOM_FILE="target/sbom.json" GRYPE_CONFIG="grype.yaml" # Check prerequisites check_prerequisites() { print_status $BLUE "Checking prerequisites..." # Check if Maven is available if ! command -v mvn &> /dev/null; then print_status $RED "❌ Maven is not installed or not in PATH" exit 1 fi # Check if Grype is available if ! command -v grype &> /dev/null; then print_status $YELLOW "⚠️ Grype not found, installing..." curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin fi # Check if jq is available for JSON parsing if ! command -v jq &> /dev/null; then print_status $RED "❌ jq is required for JSON parsing. Please install jq." exit 1 fi print_status $GREEN "✅ Prerequisites satisfied" } # Generate SBOM generate_sbom() { print_status $BLUE "Generating Software Bill of Materials..." if mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom -q; then print_status $GREEN "✅ SBOM generated successfully" else print_status $RED "❌ Failed to generate SBOM" exit 1 fi } # Run Grype scan run_grype_scan() { print_status $BLUE "Running Grype vulnerability scan..." # Create reports directory mkdir -p "$REPORTS_DIR" # Run Grype scan local grype_cmd="grype dir:. --output json --file $REPORTS_DIR/grype-report.json" if [ -f "$GRYPE_CONFIG" ]; then grype_cmd="$grype_cmd --config $GRYPE_CONFIG" fi if $grype_cmd; then print_status $GREEN "✅ Grype scan completed successfully" else print_status $YELLOW "⚠️ Grype scan completed with findings" fi } # Analyze results analyze_results() { print_status $BLUE "Analyzing scan results..." if [ ! -f "$REPORTS_DIR/grype-report.json" ]; then print_status $RED "❌ Grype report not found" return 1 fi # Parse vulnerability counts local critical_count=$(jq '.matches | map(select(.vulnerability.severity == "Critical")) | length' "$REPORTS_DIR/grype-report.json") local high_count=$(jq '.matches | map(select(.vulnerability.severity == "High")) | length' "$REPORTS_DIR/grype-report.json") local medium_count=$(jq '.matches | map(select(.vulnerability.severity == "Medium")) | length' "$REPORTS_DIR/grype-report.json") local low_count=$(jq '.matches | map(select(.vulnerability.severity == "Low")) | length' "$REPORTS_DIR/grype-report.json") local total_count=$(jq '.matches | length' "$REPORTS_DIR/grype-report.json") # Print summary echo "" print_status $BLUE "📊 VULNERABILITY SUMMARY" echo "=================================" echo -e "Critical: ${RED}$critical_count${NC}" echo -e "High: ${RED}$high_count${NC}" echo -e "Medium: ${YELLOW}$medium_count${NC}" echo -e "Low: ${GREEN}$low_count${NC}" echo -e "Total: $total_count" echo "" # Check for critical/high vulnerabilities if [ "$critical_count" -gt 0 ] || [ "$high_count" -gt 0 ]; then print_status $RED "🚨 CRITICAL OR HIGH SEVERITY VULNERABILITIES DETECTED" echo "" echo "Affected packages:" jq -r '.matches[] | select(.vulnerability.severity == "Critical" or .vulnerability.severity == "High") | "\(.vulnerability.severity): \(.artifact.name)@\(.artifact.version) - \(.vulnerability.id)"' "$REPORTS_DIR/grype-report.json" return 1 else print_status $GREEN "✅ No critical or high severity vulnerabilities detected" return 0 fi } # Generate report generate_report() { print_status $BLUE "Generating security report..." # Generate additional report formats grype dir:. --output table --file "$REPORTS_DIR/grype-report.txt" grype dir:. --output cyclonedx --file "$REPORTS_DIR/grype-sbom.xml" print_status $GREEN "📁 Reports generated in: $REPORTS_DIR/" echo " - grype-report.json (JSON format)" echo " - grype-report.txt (Table format)" echo " - grype-sbom.xml (CycloneDX format)" } # Main execution main() { echo "Starting security monitoring..." echo "" check_prerequisites echo "" generate_sbom echo "" run_grype_scan echo "" if analyze_results; then print_status $GREEN "🎉 Security scan passed!" else print_status $RED "❌ Security scan failed due to critical/high vulnerabilities" echo "" echo "Recommended actions:" echo "1. Review the detailed report: $REPORTS_DIR/grype-report.json" echo "2. Update vulnerable dependencies to patched versions" echo "3. Consider alternative libraries for frequently vulnerable dependencies" echo "4. Re-run the security scan after updates" exit 1 fi echo "" generate_report echo "" print_status $GREEN "🎊 Security monitoring completed successfully!" } # Run main function main "$@" Best Practices
1. Regular Scanning Schedule
/** * Scheduled security scanning configuration */ @Configuration @EnableScheduling public class SecurityScanningConfig { @Bean public TaskScheduler securityTaskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(2); scheduler.setThreadNamePrefix("security-scan-"); return scheduler; } /** * Daily vulnerability database update */ @Scheduled(cron = "0 0 2 * * ?") // 2 AM daily public void updateVulnerabilityDatabase() { // This would update Grype's vulnerability database logger.info("Updating vulnerability database..."); } /** * Weekly comprehensive scan */ @Scheduled(cron = "0 0 3 * * MON") // 3 AM every Monday public void weeklyComprehensiveScan() { logger.info("Starting weekly comprehensive security scan..."); // Run full Grype scan and generate reports } } 2. Security Policy Enforcement
/** * Security policy enforcement */ @Component public class SecurityPolicyEnforcer { /** * Enforce security policies based on scan results */ public boolean enforcePolicies(GrypeScannerService.GrypeScanResult result) { PolicyValidation validation = new PolicyValidation(); // Check for critical vulnerabilities if (result.getCriticalCount() > 0) { validation.addViolation("CRITICAL_VULNERABILITIES", "Critical vulnerabilities are not allowed"); } // Check for high severity vulnerabilities exceeding threshold if (result.getHighCount() > 5) { validation.addViolation("HIGH_VULNERABILITY_THRESHOLD", "Too many high severity vulnerabilities: " + result.getHighCount()); } // Check for forbidden licenses if (hasForbiddenLicenses()) { validation.addViolation("FORBIDDEN_LICENSES", "Dependencies with forbidden licenses detected"); } return validation.isValid(); } private boolean hasForbiddenLicenses() { // Check SBOM for forbidden licenses (GPL, AGPL, etc.) return false; // Implementation would check actual licenses } /** * Policy validation result */ public static class PolicyValidation { private List<String> violations = new ArrayList<>(); private boolean valid = true; public void addViolation(String code, String message) { violations.add(code + ": " + message); valid = false; } public boolean isValid() { return valid; } public List<String> getViolations() { return violations; } } } Conclusion
This comprehensive Grype vulnerability scanning implementation provides:
- Complete Grype integration for Java applications
- Automated SBOM generation with CycloneDX
- CI/CD pipeline integration with GitHub Actions and Jenkins
- Comprehensive reporting with multiple output formats
- Security policy enforcement with customizable rules
- Scheduled scanning for continuous monitoring
Key benefits include early detection of vulnerable dependencies, automated security enforcement, comprehensive dependency visibility, and seamless integration with development workflows.
Remember to:
- Run Grype scans regularly in your CI/CD pipeline
- Address critical and high severity vulnerabilities immediately
- Maintain an up-to-date SBOM for dependency transparency
- Enforce security policies consistently across projects
- Monitor for new vulnerabilities in your dependencies
- Integrate security scanning into your development workflow
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.