Introduction to Harbor Vulnerability Scanning
Harbor is an open-source container registry that provides vulnerability scanning through integration with scanners like Trivy, Clair, and Anchore. This enables automated security scanning of Docker images as part of the CI/CD pipeline.
Docker Image Configuration
Secure Dockerfile for Java Applications
# Multi-stage build for security and minimal footprint FROM eclipse-temurin:17-jdk-jammy as builder # Security: Use non-root user RUN groupadd -r javaapp && useradd -r -g javaapp javaapp # Install security updates RUN apt-get update && \ apt-get upgrade -y && \ rm -rf /var/lib/apt/lists/* WORKDIR /app # Copy dependency files first for better caching COPY mvnw . COPY .mvn .mvn COPY pom.xml . # Download dependencies RUN ./mvnw dependency:go-offline -B # Copy source code COPY src src # Build application RUN ./mvnw package -DskipTests # Runtime stage FROM eclipse-temurin:17-jre-jammy as runtime # Security hardening RUN groupadd -r javaapp && useradd -r -g javaapp javaapp && \ apt-get update && \ apt-get upgrade -y && \ rm -rf /var/lib/apt/lists/* && \ # Remove unnecessary packages apt-get autoremove -y && \ # Clean up rm -rf /var/lib/apt/lists/* # Security: Use non-root user USER javaapp WORKDIR /app # Copy built application from builder stage COPY --from=builder --chown=javaapp:javaapp /app/target/*.jar app.jar # Security: Use non-standard port EXPOSE 8080 # Security: Run as non-root USER javaapp # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 # Security: Use exec form ENTRYPOINT ["java", "-jar", "app.jar"] # Security: Add security-related JVM options CMD ["-Djava.security.egd=file:/dev/./urandom", \ "-Dspring.profiles.active=prod", \ "-XX:+UseContainerSupport", \ "-XX:MaxRAMPercentage=75.0"]
Docker Compose with Security Configuration
# docker-compose.security.yml
version: '3.8'
services:
java-app:
build:
context: .
dockerfile: Dockerfile
args:
- BUILDKIT_INLINE_CACHE=1
image: harbor.company.com/java-team/my-java-app:${TAG:-latest}
container_name: java-app
restart: unless-stopped
# Security configurations
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /var/tmp
# Resource limits
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
reservations:
memory: 256M
cpus: '0.5'
# Environment variables
environment:
- SPRING_PROFILES_ACTIVE=prod
- JAVA_OPTS=-Djava.security.egd=file:/dev/./urandom -XX:+UseContainerSupport
- SPRING_SECURITY_USER_PASSWORD=${APP_PASSWORD}
# Health check
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Network configuration
networks:
- app-network
# Vulnerability scanner service
trivy-scanner:
image: aquasec/trivy:0.45.1
container_name: trivy-scanner
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./trivy-cache:/root/.cache/trivy
environment:
- TRIVY_CACHE_DIR=/root/.cache/trivy
profiles: ["scan"]
networks:
app-network:
driver: bridge
internal: false
Harbor API Integration
Harbor Java Client
/**
* Harbor API Client for vulnerability scanning operations.
*/
@Component
@Slf4j
public class HarborApiClient {
private final RestTemplate restTemplate;
private final String harborUrl;
private final String username;
private final String password;
@Value("${harbor.url}")
private String harborUrl;
@Value("${harbor.username}")
private String harborUsername;
@Value("${harbor.password}")
private String harborPassword;
public HarborApiClient(RestTemplateBuilder restTemplateBuilder) {
this.harborUrl = harborUrl;
this.username = harborUsername;
this.password = harborPassword;
this.restTemplate = restTemplateBuilder
.basicAuthentication(username, password)
.defaultHeader("Content-Type", "application/json")
.build();
}
/**
* Triggers vulnerability scan for an image.
*/
public ScanResponse triggerScan(String projectName, String repositoryName, String tag) {
try {
String scanUrl = String.format("%s/api/v2.0/projects/%s/repositories/%s/artifacts/%s/scan",
harborUrl, projectName, encodeRepository(repositoryName), tag);
ResponseEntity<ScanResponse> response = restTemplate.postForEntity(
scanUrl, null, ScanResponse.class);
if (response.getStatusCode().is2xxSuccessful()) {
log.info("Triggered vulnerability scan for {}/{}:{}",
projectName, repositoryName, tag);
return response.getBody();
} else {
throw new HarborException("Failed to trigger scan: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("Failed to trigger vulnerability scan for {}/{}:{}",
projectName, repositoryName, tag, e);
throw new HarborException("Scan trigger failed", e);
}
}
/**
* Gets vulnerability report for an image.
*/
public VulnerabilityReport getVulnerabilityReport(String projectName, String repositoryName, String tag) {
try {
String reportUrl = String.format("%s/api/v2.0/projects/%s/repositories/%s/artifacts/%s/additions/vulnerabilities",
harborUrl, projectName, encodeRepository(repositoryName), tag);
ResponseEntity<VulnerabilityReport> response = restTemplate.getForEntity(
reportUrl, VulnerabilityReport.class);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
} else {
throw new HarborException("Failed to get vulnerability report: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("Failed to get vulnerability report for {}/{}:{}",
projectName, repositoryName, tag, e);
throw new HarborException("Failed to get vulnerability report", e);
}
}
/**
* Checks scan status for an image.
*/
public ScanStatus getScanStatus(String projectName, String repositoryName, String tag) {
try {
String statusUrl = String.format("%s/api/v2.0/projects/%s/repositories/%s/artifacts/%s",
harborUrl, projectName, encodeRepository(repositoryName), tag);
ResponseEntity<Artifact> response = restTemplate.getForEntity(
statusUrl, Artifact.class);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody().getScanStatus();
} else {
throw new HarborException("Failed to get scan status: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("Failed to get scan status for {}/{}:{}",
projectName, repositoryName, tag, e);
throw new HarborException("Failed to get scan status", e);
}
}
/**
* Lists all projects in Harbor.
*/
public List<Project> listProjects() {
try {
String projectsUrl = harborUrl + "/api/v2.0/projects";
ResponseEntity<Project[]> response = restTemplate.getForEntity(
projectsUrl, Project[].class);
if (response.getStatusCode().is2xxSuccessful()) {
return Arrays.asList(response.getBody());
} else {
throw new HarborException("Failed to list projects: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("Failed to list Harbor projects", e);
throw new HarborException("Failed to list projects", e);
}
}
/**
* Gets project vulnerability summary.
*/
public ProjectVulnerabilitySummary getProjectVulnerabilitySummary(String projectName) {
try {
String summaryUrl = String.format("%s/api/v2.0/projects/%s/summary",
harborUrl, projectName);
ResponseEntity<ProjectVulnerabilitySummary> response = restTemplate.getForEntity(
summaryUrl, ProjectVulnerabilitySummary.class);
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
} else {
throw new HarborException("Failed to get project summary: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("Failed to get vulnerability summary for project: {}", projectName, e);
throw new HarborException("Failed to get project summary", e);
}
}
/**
* Deletes an artifact (image) from Harbor.
*/
public void deleteArtifact(String projectName, String repositoryName, String tag) {
try {
String deleteUrl = String.format("%s/api/v2.0/projects/%s/repositories/%s/artifacts/%s",
harborUrl, projectName, encodeRepository(repositoryName), tag);
restTemplate.delete(deleteUrl);
log.info("Deleted artifact {}/{}:{}", projectName, repositoryName, tag);
} catch (Exception e) {
log.error("Failed to delete artifact {}/{}:{}",
projectName, repositoryName, tag, e);
throw new HarborException("Failed to delete artifact", e);
}
}
private String encodeRepository(String repositoryName) {
return URLEncoder.encode(repositoryName, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
}
}
/**
* Harbor API data models.
*/
public class ScanResponse {
private String scanId;
private LocalDateTime triggeredAt;
private String status;
// Getters and setters
public String getScanId() { return scanId; }
public void setScanId(String scanId) { this.scanId = scanId; }
public LocalDateTime getTriggeredAt() { return triggeredAt; }
public void setTriggeredAt(LocalDateTime triggeredAt) { this.triggeredAt = triggeredAt; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
public class VulnerabilityReport {
private String scanId;
private LocalDateTime generatedAt;
private String scanner;
private String severity;
private List<Vulnerability> vulnerabilities;
private Summary summary;
// Getters and setters
public String getScanId() { return scanId; }
public void setScanId(String scanId) { this.scanId = scanId; }
public LocalDateTime getGeneratedAt() { return generatedAt; }
public void setGeneratedAt(LocalDateTime generatedAt) { this.generatedAt = generatedAt; }
public String getScanner() { return scanner; }
public void setScanner(String scanner) { this.scanner = scanner; }
public String getSeverity() { return severity; }
public void setSeverity(String severity) { this.severity = severity; }
public List<Vulnerability> getVulnerabilities() { return vulnerabilities; }
public void setVulnerabilities(List<Vulnerability> vulnerabilities) { this.vulnerabilities = vulnerabilities; }
public Summary getSummary() { return summary; }
public void setSummary(Summary summary) { this.summary = summary; }
public static class Summary {
private int total;
private int fixable;
private int critical;
private int high;
private int medium;
private int low;
private int unknown;
// Getters and setters
public int getTotal() { return total; }
public void setTotal(int total) { this.total = total; }
public int getFixable() { return fixable; }
public void setFixable(int fixable) { this.fixable = fixable; }
public int getCritical() { return critical; }
public void setCritical(int critical) { this.critical = critical; }
public int getHigh() { return high; }
public void setHigh(int high) { this.high = high; }
public int getMedium() { return medium; }
public void setMedium(int medium) { this.medium = medium; }
public int getLow() { return low; }
public void setLow(int low) { this.low = low; }
public int getUnknown() { return unknown; }
public void setUnknown(int unknown) { this.unknown = unknown; }
}
}
public class Vulnerability {
private String id;
private String packageName;
private String version;
private String fixVersion;
private String severity;
private String description;
private List<String> links;
private LocalDateTime publishedDate;
private LocalDateTime lastModifiedDate;
// Getters and setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getPackageName() { return packageName; }
public void setPackageName(String packageName) { this.packageName = packageName; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public String getFixVersion() { return fixVersion; }
public void setFixVersion(String fixVersion) { this.fixVersion = fixVersion; }
public String getSeverity() { return severity; }
public void setSeverity(String severity) { this.severity = severity; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public List<String> getLinks() { return links; }
public void setLinks(List<String> links) { this.links = links; }
public LocalDateTime getPublishedDate() { return publishedDate; }
public void setPublishedDate(LocalDateTime publishedDate) { this.publishedDate = publishedDate; }
public LocalDateTime getLastModifiedDate() { return lastModifiedDate; }
public void setLastModifiedDate(LocalDateTime lastModifiedDate) { this.lastModifiedDate = lastModifiedDate; }
}
public class ScanStatus {
private String status;
private LocalDateTime startTime;
private LocalDateTime endTime;
private String scanner;
private String severity;
// Getters and setters
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public LocalDateTime getStartTime() { return startTime; }
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
public LocalDateTime getEndTime() { return endTime; }
public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; }
public String getScanner() { return scanner; }
public void setScanner(String scanner) { this.scanner = scanner; }
public String getSeverity() { return severity; }
public void setSeverity(String severity) { this.severity = severity; }
}
public class HarborException extends RuntimeException {
public HarborException(String message) {
super(message);
}
public HarborException(String message, Throwable cause) {
super(message, cause);
}
}
Vulnerability Scanning Service
Automated Vulnerability Management
/**
* Service for managing Harbor vulnerability scans.
*/
@Service
@Slf4j
public class VulnerabilityScanService {
private final HarborApiClient harborClient;
private final ImageRepository imageRepository;
private final NotificationService notificationService;
private final SecurityConfig securityConfig;
public VulnerabilityScanService(HarborApiClient harborClient,
ImageRepository imageRepository,
NotificationService notificationService,
SecurityConfig securityConfig) {
this.harborClient = harborClient;
this.imageRepository = imageRepository;
this.notificationService = notificationService;
this.securityConfig = securityConfig;
}
/**
* Scans an image and returns the vulnerability report.
*/
public ScanResult scanImage(String imageRef) {
ImageReference ref = parseImageReference(imageRef);
try {
log.info("Starting vulnerability scan for image: {}", imageRef);
// Trigger scan
ScanResponse scanResponse = harborClient.triggerScan(
ref.getProject(), ref.getRepository(), ref.getTag());
// Wait for scan completion
VulnerabilityReport report = waitForScanCompletion(ref);
// Analyze results
ScanResult result = analyzeScanResults(ref, report);
// Save results
saveScanResults(ref, result);
// Send notifications if needed
sendNotifications(ref, result);
log.info("Vulnerability scan completed for image: {}. Severity: {}",
imageRef, result.getHighestSeverity());
return result;
} catch (Exception e) {
log.error("Vulnerability scan failed for image: {}", imageRef, e);
throw new ScanException("Vulnerability scan failed for image: " + imageRef, e);
}
}
/**
* Scans multiple images in parallel.
*/
public List<ScanResult> scanImages(List<String> imageRefs) {
return imageRefs.parallelStream()
.map(this::scanImage)
.collect(Collectors.toList());
}
/**
* Checks if an image meets security policies.
*/
public PolicyCompliance checkPolicyCompliance(String imageRef) {
VulnerabilityReport report = harborClient.getVulnerabilityReport(
parseImageReference(imageRef).getProject(),
parseImageReference(imageRef).getRepository(),
parseImageReference(imageRef).getTag()
);
return evaluatePolicyCompliance(report);
}
/**
* Gets vulnerability trends for a project.
*/
public VulnerabilityTrend getVulnerabilityTrend(String projectName, Duration period) {
LocalDateTime endDate = LocalDateTime.now();
LocalDateTime startDate = endDate.minus(period);
List<ScanResult> historicalScans = imageRepository.findScansByProjectAndDateRange(
projectName, startDate, endDate);
return analyzeVulnerabilityTrend(historicalScans, period);
}
/**
* Automatically scans new images in a project.
*/
@Scheduled(fixedRate = 300000) // 5 minutes
public void autoScanNewImages() {
log.info("Starting automatic scan of new images");
List<Project> projects = harborClient.listProjects();
for (Project project : projects) {
try {
scanNewImagesInProject(project.getName());
} catch (Exception e) {
log.error("Failed to scan new images in project: {}", project.getName(), e);
}
}
}
/**
* Waits for scan completion with timeout.
*/
private VulnerabilityReport waitForScanCompletion(ImageReference ref) {
int maxAttempts = 30; // 5 minutes with 10-second intervals
int attempt = 0;
while (attempt < maxAttempts) {
try {
ScanStatus status = harborClient.getScanStatus(
ref.getProject(), ref.getRepository(), ref.getTag());
if ("Completed".equals(status.getStatus())) {
return harborClient.getVulnerabilityReport(
ref.getProject(), ref.getRepository(), ref.getTag());
} else if ("Error".equals(status.getStatus())) {
throw new ScanException("Scan failed for image: " + ref.toString());
}
// Wait before next check
Thread.sleep(10000);
attempt++;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ScanException("Scan wait interrupted", e);
} catch (Exception e) {
log.warn("Failed to check scan status, attempt {}/{}", attempt, maxAttempts, e);
attempt++;
}
}
throw new ScanException("Scan timeout for image: " + ref.toString());
}
/**
* Analyzes scan results against security policies.
*/
private ScanResult analyzeScanResults(ImageReference ref, VulnerabilityReport report) {
ScanResult result = new ScanResult(ref, report);
// Check against severity thresholds
if (report.getSummary().getCritical() > securityConfig.getMaxCriticalVulnerabilities()) {
result.setCompliant(false);
result.setBlockingIssue("Critical vulnerabilities exceed threshold");
}
if (report.getSummary().getHigh() > securityConfig.getMaxHighVulnerabilities()) {
result.setCompliant(false);
result.setBlockingIssue("High vulnerabilities exceed threshold");
}
// Check for specific banned vulnerabilities
List<Vulnerability> bannedVulnerabilities = findBannedVulnerabilities(report.getVulnerabilities());
if (!bannedVulnerabilities.isEmpty()) {
result.setCompliant(false);
result.setBlockingIssue("Contains banned vulnerabilities: " +
bannedVulnerabilities.stream()
.map(Vulnerability::getId)
.collect(Collectors.joining(", ")));
}
return result;
}
/**
* Evaluates policy compliance.
*/
private PolicyCompliance evaluatePolicyCompliance(VulnerabilityReport report) {
PolicyCompliance compliance = new PolicyCompliance();
// Check severity thresholds
if (report.getSummary().getCritical() > 0) {
compliance.addViolation("CRITICAL",
"Critical vulnerabilities found: " + report.getSummary().getCritical());
}
if (report.getSummary().getHigh() > securityConfig.getMaxHighVulnerabilities()) {
compliance.addViolation("HIGH",
"High vulnerabilities exceed threshold: " + report.getSummary().getHigh());
}
// Check for fixable vulnerabilities
if (report.getSummary().getFixable() > securityConfig.getMaxFixableVulnerabilities()) {
compliance.addViolation("FIXABLE",
"Fixable vulnerabilities exceed threshold: " + report.getSummary().getFixable());
}
compliance.setCompliant(compliance.getViolations().isEmpty());
return compliance;
}
private void scanNewImagesInProject(String projectName) {
// Implementation to find and scan new images
log.debug("Scanning new images in project: {}", projectName);
}
private ImageReference parseImageReference(String imageRef) {
// Parse image reference (e.g., "harbor.company.com/project/repo:tag")
return new ImageReference(imageRef);
}
private List<Vulnerability> findBannedVulnerabilities(List<Vulnerability> vulnerabilities) {
return vulnerabilities.stream()
.filter(v -> securityConfig.getBannedVulnerabilities().contains(v.getId()))
.collect(Collectors.toList());
}
private void saveScanResults(ImageReference ref, ScanResult result) {
imageRepository.save(new ImageScan(ref, result));
}
private void sendNotifications(ImageReference ref, ScanResult result) {
if (!result.isCompliant()) {
notificationService.sendSecurityAlert(ref, result);
}
if (result.getHighestSeverity() == Severity.CRITICAL) {
notificationService.sendCriticalAlert(ref, result);
}
}
private VulnerabilityTrend analyzeVulnerabilityTrend(List<ScanResult> historicalScans, Duration period) {
// Analyze trends over time
return new VulnerabilityTrend(historicalScans, period);
}
}
/**
* Data models for vulnerability scanning.
*/
public class ScanResult {
private ImageReference imageRef;
private VulnerabilityReport report;
private boolean compliant;
private String blockingIssue;
private LocalDateTime scannedAt;
private Severity highestSeverity;
public ScanResult(ImageReference imageRef, VulnerabilityReport report) {
this.imageRef = imageRef;
this.report = report;
this.scannedAt = LocalDateTime.now();
this.highestSeverity = calculateHighestSeverity(report);
}
private Severity calculateHighestSeverity(VulnerabilityReport report) {
if (report.getSummary().getCritical() > 0) return Severity.CRITICAL;
if (report.getSummary().getHigh() > 0) return Severity.HIGH;
if (report.getSummary().getMedium() > 0) return Severity.MEDIUM;
if (report.getSummary().getLow() > 0) return Severity.LOW;
return Severity.NONE;
}
// Getters and setters
public ImageReference getImageRef() { return imageRef; }
public void setImageRef(ImageReference imageRef) { this.imageRef = imageRef; }
public VulnerabilityReport getReport() { return report; }
public void setReport(VulnerabilityReport report) { this.report = report; }
public boolean isCompliant() { return compliant; }
public void setCompliant(boolean compliant) { this.compliant = compliant; }
public String getBlockingIssue() { return blockingIssue; }
public void setBlockingIssue(String blockingIssue) { this.blockingIssue = blockingIssue; }
public LocalDateTime getScannedAt() { return scannedAt; }
public void setScannedAt(LocalDateTime scannedAt) { this.scannedAt = scannedAt; }
public Severity getHighestSeverity() { return highestSeverity; }
public void setHighestSeverity(Severity highestSeverity) { this.highestSeverity = highestSeverity; }
}
public class ImageReference {
private String registry;
private String project;
private String repository;
private String tag;
private String digest;
public ImageReference(String imageRef) {
// Parse image reference
// Format: [registry/]project/repository:tag[@digest]
String[] parts = imageRef.split("/");
if (parts.length == 3) {
this.registry = parts[0];
this.project = parts[1];
String[] repoTag = parts[2].split("[:@]");
this.repository = repoTag[0];
if (repoTag.length > 1) {
this.tag = repoTag[1];
}
if (repoTag.length > 2) {
this.digest = repoTag[2];
}
}
}
// Getters
public String getRegistry() { return registry; }
public String getProject() { return project; }
public String getRepository() { return repository; }
public String getTag() { return tag; }
public String getDigest() { return digest; }
@Override
public String toString() {
return String.format("%s/%s/%s:%s", registry, project, repository, tag);
}
}
public enum Severity {
CRITICAL, HIGH, MEDIUM, LOW, NONE
}
public class PolicyCompliance {
private boolean compliant;
private List<PolicyViolation> violations;
public PolicyCompliance() {
this.violations = new ArrayList<>();
}
public void addViolation(String type, String description) {
violations.add(new PolicyViolation(type, description));
}
// Getters and setters
public boolean isCompliant() { return compliant; }
public void setCompliant(boolean compliant) { this.compliant = compliant; }
public List<PolicyViolation> getViolations() { return violations; }
public void setViolations(List<PolicyViolation> violations) { this.violations = violations; }
}
public class PolicyViolation {
private String type;
private String description;
private LocalDateTime detectedAt;
public PolicyViolation(String type, String description) {
this.type = type;
this.description = description;
this.detectedAt = LocalDateTime.now();
}
// Getters
public String getType() { return type; }
public String getDescription() { return description; }
public LocalDateTime getDetectedAt() { return detectedAt; }
}
public class ScanException extends RuntimeException {
public ScanException(String message) {
super(message);
}
public ScanException(String message, Throwable cause) {
super(message, cause);
}
}
Security Configuration
Security Policy Configuration
/**
* Security configuration for vulnerability scanning.
*/
@Configuration
@ConfigurationProperties(prefix = "security.vulnerability")
@Data
public class SecurityConfig {
// Severity thresholds
private int maxCriticalVulnerabilities = 0;
private int maxHighVulnerabilities = 5;
private int maxMediumVulnerabilities = 20;
private int maxFixableVulnerabilities = 10;
// Scan configuration
private boolean autoScanEnabled = true;
private Duration scanTimeout = Duration.ofMinutes(5);
private int maxScanRetries = 3;
// Notification thresholds
private boolean notifyOnCritical = true;
private boolean notifyOnHigh = true;
private boolean notifyOnPolicyViolation = true;
// Banned vulnerabilities
private Set<String> bannedVulnerabilities = new HashSet<>();
// Allowed base images
private Set<String> allowedBaseImages = Set.of(
"eclipse-temurin:17-jre-jammy",
"eclipse-temurin:11-jre-jammy",
"openjdk:17-jre-slim",
"openjdk:11-jre-slim"
);
// Auto-remediation
private boolean autoUpdateBaseImages = true;
private Duration baseImageUpdateCheck = Duration.ofDays(7);
public SecurityConfig() {
// Initialize with commonly banned vulnerabilities
bannedVulnerabilities.addAll(Set.of(
"CVE-2021-44228", // Log4Shell
"CVE-2021-45046", // Log4Shell
"CVE-2022-22965" // Spring4Shell
));
}
}
/**
* Security policy enforcement service.
*/
@Service
@Slf4j
public class SecurityPolicyService {
private final SecurityConfig securityConfig;
private final HarborApiClient harborClient;
private final NotificationService notificationService;
public SecurityPolicyService(SecurityConfig securityConfig,
HarborApiClient harborClient,
NotificationService notificationService) {
this.securityConfig = securityConfig;
this.harborClient = harborClient;
this.notificationService = notificationService;
}
/**
* Enforces security policies for an image.
*/
public PolicyEnforcementResult enforcePolicies(String imageRef) {
try {
VulnerabilityReport report = harborClient.getVulnerabilityReport(
parseImageReference(imageRef).getProject(),
parseImageReference(imageRef).getRepository(),
parseImageReference(imageRef).getTag()
);
return evaluatePolicies(imageRef, report);
} catch (Exception e) {
log.error("Failed to enforce policies for image: {}", imageRef, e);
return PolicyEnforcementResult.failed(imageRef, e.getMessage());
}
}
/**
* Evaluates all security policies against a vulnerability report.
*/
private PolicyEnforcementResult evaluatePolicies(String imageRef, VulnerabilityReport report) {
PolicyEnforcementResult result = new PolicyEnforcementResult(imageRef);
// Check severity thresholds
evaluateSeverityPolicies(report, result);
// Check banned vulnerabilities
evaluateBannedVulnerabilities(report, result);
// Check base image compliance
evaluateBaseImagePolicy(imageRef, result);
result.setCompliant(result.getViolations().isEmpty());
if (!result.isCompliant()) {
log.warn("Image {} failed policy enforcement. Violations: {}",
imageRef, result.getViolations().size());
notificationService.sendPolicyViolationAlert(imageRef, result);
}
return result;
}
private void evaluateSeverityPolicies(VulnerabilityReport report, PolicyEnforcementResult result) {
if (report.getSummary().getCritical() > securityConfig.getMaxCriticalVulnerabilities()) {
result.addViolation("CRITICAL_SEVERITY",
String.format("Critical vulnerabilities exceed threshold: %d > %d",
report.getSummary().getCritical(), securityConfig.getMaxCriticalVulnerabilities()));
}
if (report.getSummary().getHigh() > securityConfig.getMaxHighVulnerabilities()) {
result.addViolation("HIGH_SEVERITY",
String.format("High vulnerabilities exceed threshold: %d > %d",
report.getSummary().getHigh(), securityConfig.getMaxHighVulnerabilities()));
}
}
private void evaluateBannedVulnerabilities(VulnerabilityReport report, PolicyEnforcementResult result) {
List<Vulnerability> bannedVulns = report.getVulnerabilities().stream()
.filter(v -> securityConfig.getBannedVulnerabilities().contains(v.getId()))
.collect(Collectors.toList());
if (!bannedVulns.isEmpty()) {
result.addViolation("BANNED_VULNERABILITIES",
"Contains banned vulnerabilities: " +
bannedVulns.stream().map(Vulnerability::getId).collect(Collectors.joining(", ")));
}
}
private void evaluateBaseImagePolicy(String imageRef, PolicyEnforcementResult result) {
// Check if base image is from allowed list
// This would require additional metadata or image analysis
if (!isBaseImageAllowed(imageRef)) {
result.addViolation("BASE_IMAGE",
"Base image is not from allowed list");
}
}
private boolean isBaseImageAllowed(String imageRef) {
// Implementation to check base image against allowed list
// This might require Dockerfile analysis or image metadata inspection
return true; // Simplified
}
private ImageReference parseImageReference(String imageRef) {
return new ImageReference(imageRef);
}
}
public class PolicyEnforcementResult {
private String imageRef;
private boolean compliant;
private List<PolicyViolation> violations;
private LocalDateTime evaluatedAt;
public PolicyEnforcementResult(String imageRef) {
this.imageRef = imageRef;
this.violations = new ArrayList<>();
this.evaluatedAt = LocalDateTime.now();
}
public void addViolation(String type, String description) {
violations.add(new PolicyViolation(type, description));
}
public static PolicyEnforcementResult failed(String imageRef, String error) {
PolicyEnforcementResult result = new PolicyEnforcementResult(imageRef);
result.addViolation("ENFORCEMENT_ERROR", error);
result.setCompliant(false);
return result;
}
// Getters and setters
public String getImageRef() { return imageRef; }
public void setImageRef(String imageRef) { this.imageRef = imageRef; }
public boolean isCompliant() { return compliant; }
public void setCompliant(boolean compliant) { this.compliant = compliant; }
public List<PolicyViolation> getViolations() { return violations; }
public void setViolations(List<PolicyViolation> violations) { this.violations = violations; }
public LocalDateTime getEvaluatedAt() { return evaluatedAt; }
public void setEvaluatedAt(LocalDateTime evaluatedAt) { this.evaluatedAt = evaluatedAt; }
}
GitHub Actions Integration
CI/CD Pipeline with Harbor Scanning
# .github/workflows/harbor-scan.yml
name: Harbor Vulnerability Scan
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 6 AM
env:
REGISTRY: harbor.company.com
PROJECT: java-team
IMAGE_NAME: my-java-app
jobs:
build-and-scan:
name: Build and Scan Image
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build application
run: |
mvn clean package -DskipTests
- name: Build Docker image
run: |
docker build -t $REGISTRY/$PROJECT/$IMAGE_NAME:$GITHUB_SHA .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: '$REGISTRY/$PROJECT/$IMAGE_NAME:$GITHUB_SHA'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
- name: Push to Harbor
run: |
echo ${{ secrets.HARBOR_PASSWORD }} | docker login $REGISTRY -u ${{ secrets.HARBOR_USERNAME }} --password-stdin
docker push $REGISTRY/$PROJECT/$IMAGE_NAME:$GITHUB_SHA
- name: Trigger Harbor Scan
run: |
curl -X POST \
-u "${{ secrets.HARBOR_USERNAME }}:${{ secrets.HARBOR_PASSWORD }}" \
"$REGISTRY/api/v2.0/projects/$PROJECT/repositories/${IMAGE_NAME//\//%2F}/artifacts/$GITHUB_SHA/scan"
- name: Wait for Scan Completion
run: |
# Wait for Harbor scan to complete
until curl -s -u "${{ secrets.HARBOR_USERNAME }}:${{ secrets.HARBOR_PASSWORD }}" \
"$REGISTRY/api/v2.0/projects/$PROJECT/repositories/${IMAGE_NAME//\//%2F}/artifacts/$GITHUB_SHA" \
| grep -q '"scan_status": "Completed"'; do
echo "Waiting for scan completion..."
sleep 10
done
- name: Check Scan Results
run: |
SCAN_RESULT=$(curl -s -u "${{ secrets.HARBOR_USERNAME }}:${{ secrets.HARBOR_PASSWORD }}" \
"$REGISTRY/api/v2.0/projects/$PROJECT/repositories/${IMAGE_NAME//\//%2F}/artifacts/$GITHUB_SHA/additions/vulnerabilities")
CRITICAL_COUNT=$(echo "$SCAN_RESULT" | jq '.summary.critical // 0')
HIGH_COUNT=$(echo "$SCAN_RESULT" | jq '.summary.high // 0')
if [ "$CRITICAL_COUNT" -gt 0 ] || [ "$HIGH_COUNT" -gt 5 ]; then
echo "❌ Critical or high vulnerabilities found"
echo "Critical: $CRITICAL_COUNT, High: $HIGH_COUNT"
exit 1
else
echo "✅ Scan passed - no critical vulnerabilities"
fi
security-gate:
name: Security Policy Gate
runs-on: ubuntu-latest
needs: build-and-scan
steps:
- name: Check Security Compliance
run: |
# Use custom Java application to check policies
java -jar security-checker.jar \
--image $REGISTRY/$PROJECT/$IMAGE_NAME:$GITHUB_SHA \
--policy security-policy.json
- name: Fail on Policy Violation
if: failure()
run: |
echo "❌ Security policy violation detected"
exit 1
Spring Boot Configuration
Auto-Configuration for Harbor
/**
* Spring Boot Auto-Configuration for Harbor integration.
*/
@Configuration
@EnableConfigurationProperties(HarborProperties.class)
@ConditionalOnProperty(name = "harbor.enabled", havingValue = "true")
public class HarborAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public HarborApiClient harborApiClient(RestTemplateBuilder restTemplateBuilder,
HarborProperties properties) {
return new HarborApiClient(restTemplateBuilder, properties);
}
@Bean
public VulnerabilityScanService vulnerabilityScanService(HarborApiClient harborClient,
ImageRepository imageRepository,
NotificationService notificationService,
SecurityConfig securityConfig) {
return new VulnerabilityScanService(harborClient, imageRepository, notificationService, securityConfig);
}
@Bean
public SecurityPolicyService securityPolicyService(SecurityConfig securityConfig,
HarborApiClient harborClient,
NotificationService notificationService) {
return new SecurityPolicyService(securityConfig, harborClient, notificationService);
}
@Bean
public HarborHealthIndicator harborHealthIndicator(HarborApiClient harborClient) {
return new HarborHealthIndicator(harborClient);
}
}
/**
* Harbor configuration properties.
*/
@ConfigurationProperties(prefix = "harbor")
@Data
public class HarborProperties {
private boolean enabled = true;
private String url;
private String username;
private String password;
private String defaultProject;
private Scan scan = new Scan();
private Notification notification = new Notification();
@Data
public static class Scan {
private boolean autoTrigger = true;
private Duration timeout = Duration.ofMinutes(5);
private int maxRetries = 3;
private String scanner = "trivy";
private List<String> excludedProjects = List.of("library", "infrastructure");
}
@Data
public static class Notification {
private boolean enabled = true;
private List<String> criticalChannels = List.of("slack-security", "email-security");
private List<String> warningChannels = List.of("slack-dev");
private Duration reminderInterval = Duration.ofDays(1);
}
}
/**
* Health indicator for Harbor integration.
*/
@Component
public class HarborHealthIndicator implements HealthIndicator {
private final HarborApiClient harborClient;
public HarborHealthIndicator(HarborApiClient harborClient) {
this.harborClient = harborClient;
}
@Override
public Health health() {
try {
// Test Harbor connectivity by listing projects
List<Project> projects = harborClient.listProjects();
return Health.up()
.withDetail("service", "harbor")
.withDetail("status", "connected")
.withDetail("projects", projects.size())
.build();
} catch (Exception e) {
return Health.down()
.withDetail("service", "harbor")
.withDetail("status", "disconnected")
.withDetail("error", e.getMessage())
.build();
}
}
}
Best Practices Summary
/**
* HARBOR VULNERABILITY SCANNING BEST PRACTICES
*
* 1. Use multi-stage Docker builds to minimize attack surface
* 2. Regularly update base images with security patches
* 3. Implement security gates in CI/CD pipelines
* 4. Use automated scanning for all images
* 5. Enforce severity-based policies
* 6. Monitor for banned vulnerabilities
* 7. Implement automatic notifications for critical issues
* 8. Maintain vulnerability trends and reports
* 9. Use non-root users in containers
* 10. Regularly review and update security policies
*/
/**
* Utility class with Harbor best practices.
*/
public final class HarborBestPractices {
private HarborBestPractices() {
// Utility class
}
/**
* Validates Dockerfile security practices.
*/
public static List<String> validateDockerfile(File dockerfile) {
List<String> issues = new ArrayList<>();
try {
List<String> lines = Files.readAllLines(dockerfile.toPath());
// Check for security best practices
if (!containsNonRootUser(lines)) {
issues.add("Dockerfile should use non-root user");
}
if (containsSensitiveData(lines)) {
issues.add("Dockerfile may contain sensitive data");
}
if (!usesMultiStageBuild(lines)) {
issues.add("Consider using multi-stage build for smaller images");
}
if (usesLatestTag(lines)) {
issues.add("Avoid using 'latest' tag for base images");
}
} catch (IOException e) {
issues.add("Failed to read Dockerfile: " + e.getMessage());
}
return issues;
}
/**
* Generates security policy template.
*/
public static String generateSecurityPolicy() {
return """
{
"version": "1.0",
"policies": {
"severity": {
"critical": 0,
"high": 5,
"medium": 20
},
"banned_vulnerabilities": [
"CVE-2021-44228",
"CVE-2021-45046"
],
"allowed_base_images": [
"eclipse-temurin:17-jre-jammy",
"eclipse-temurin:11-jre-jammy"
],
"auto_remediation": {
"enabled": true,
"base_image_check_interval": "7d"
}
}
}
""";
}
/**
* Creates integration checklist.
*/
public static List<String> getIntegrationChecklist() {
return Arrays.asList(
"Configure Harbor registry with vulnerability scanning",
"Set up automated image builds with security scanning",
"Implement security gates in CI/CD pipeline",
"Configure severity-based policies",
"Set up notifications for critical vulnerabilities",
"Establish vulnerability remediation process",
"Monitor vulnerability trends over time",
"Regularly update base images",
"Train team on container security best practices",
"Conduct regular security reviews"
);
}
private static boolean containsNonRootUser(List<String> lines) {
return lines.stream().anyMatch(line ->
line.trim().toLowerCase().startsWith("user ") &&
!line.contains("root"));
}
private static boolean containsSensitiveData(List<String> lines) {
return lines.stream().anyMatch(line ->
line.toLowerCase().contains("password") ||
line.toLowerCase().contains("secret") ||
line.toLowerCase().contains("key"));
}
private static boolean usesMultiStageBuild(List<String> lines) {
return lines.stream().anyMatch(line ->
line.trim().toLowerCase().startsWith("from ") &&
line.toLowerCase().contains(" as "));
}
private static boolean usesLatestTag(List<String> lines) {
return lines.stream()
.filter(line -> line.trim().toLowerCase().startsWith("from "))
.anyMatch(line -> line.toLowerCase().contains(":latest"));
}
}
Conclusion
Harbor vulnerability scanning for Java provides comprehensive container security:
- Automated Scanning - Integrated vulnerability scanning in CI/CD pipelines
- Policy Enforcement - Configurable security policies and gates
- Comprehensive Reporting - Detailed vulnerability reports and trends
- Remediation Guidance - Fix versions and remediation advice
- Integration Flexibility - REST API for custom integrations
- Security Hardening - Best practices for Docker image security
- Compliance Monitoring - Continuous compliance checking
- Alerting and Notification - Real-time security alerts
By implementing Harbor vulnerability scanning with proper configuration, policies, and integration, Java teams can ensure container security throughout the development lifecycle while maintaining compliance with organizational security standards.
Secure Java Supply Chain, Minimal Containers & Runtime Security (Alpine, Distroless, Signing, SBOM & Kubernetes Controls)
https://macronepal.com/blog/alpine-linux-security-in-java-complete-guide/
Explains how Alpine Linux is used as a lightweight base for Java containers to reduce image size and attack surface, while discussing tradeoffs like musl compatibility, CVE handling, and additional hardening requirements for production security.
https://macronepal.com/blog/the-minimalists-approach-building-ultra-secure-java-applications-with-scratch-base-images/
Explains using scratch base images for Java applications to create extremely minimal containers with almost zero attack surface, where only the compiled Java application and runtime dependencies exist.
https://macronepal.com/blog/distroless-containers-in-java-minimal-secure-containers-for-jvm-applications/
Explains distroless Java containers that remove shells, package managers, and unnecessary OS tools, significantly reducing vulnerabilities while improving security posture for JVM workloads.
https://macronepal.com/blog/revolutionizing-container-security-implementing-chainguard-images-for-java-applications/
Explains Chainguard images for Java, which are secure-by-default, CVE-minimized container images with SBOMs and cryptographic signing, designed for modern supply-chain security.
https://macronepal.com/blog/seccomp-filtering-in-java-comprehensive-security-sandboxing/
Explains seccomp syscall filtering in Linux to restrict what system calls Java applications can make, reducing the impact of exploits by limiting kernel-level access.
https://macronepal.com/blog/in-toto-attestations-in-java/
Explains in-toto framework integration in Java to create cryptographically verifiable attestations across the software supply chain, ensuring every build step is trusted and auditable.
https://macronepal.com/blog/fulcio-integration-in-java-code-signing-certificate-infrastructure/
Explains Fulcio integration for Java, which issues short-lived certificates for code signing in a zero-trust supply chain, enabling secure identity-based signing of artifacts.
https://macronepal.com/blog/tekton-supply-chain-in-java-comprehensive-ci-cd-pipeline-implementation/
Explains using Tekton CI/CD pipelines for Java applications to automate secure builds, testing, signing, and deployment with supply-chain security controls built in.
https://macronepal.com/blog/slsa-provenance-in-java-complete-guide-to-supply-chain-security-2/
Explains SLSA (Supply-chain Levels for Software Artifacts) provenance in Java builds, ensuring traceability of how software is built, from source code to final container image.
https://macronepal.com/blog/notary-project-in-java-complete-implementation-guide/
Explains the Notary Project for Java container security, enabling cryptographic signing and verification of container images and artifacts to prevent tampering in deployment pipelines.