ClairCore in Java: Comprehensive Container Security Scanning

ClairCore is a open-source container vulnerability scanner that analyzes container images for security vulnerabilities. This guide covers Java integration, custom analyzers, and comprehensive security scanning implementation.


Dependencies and Setup

1. Maven Dependencies
<properties>
<claircore.version>1.4.0</claircore.version>
<quay.client.version>1.0.0</quay.client.version>
<jackson.version>2.15.2</jackson.version>
<okhttp.version>4.11.0</okhttp.version>
</properties>
<dependencies>
<!-- ClairCore Client -->
<dependency>
<groupId>com.github.quay</groupId>
<artifactId>claircore</artifactId>
<version>${claircore.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- Test Containers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.18.3</version>
<scope>test</scope>
</dependency>
</dependencies>
2. Gradle Configuration
dependencies {
implementation 'com.github.quay:claircore:1.4.0'
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation 'org.slf4j:slf4j-api:2.0.7'
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
testImplementation 'org.testcontainers:junit-jupiter:1.18.3'
}

Core ClairCore Integration

1. ClairCore Client Configuration
package com.example.clair.core;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.OkHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class ClairCoreConfig {
private static final Logger logger = LoggerFactory.getLogger(ClairCoreConfig.class);
@Value("${claircore.url:http://localhost:6060}")
private String clairCoreUrl;
@Value("${claircore.timeout.seconds:30}")
private int timeoutSeconds;
@Bean
public OkHttpClient clairHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(timeoutSeconds))
.readTimeout(Duration.ofSeconds(timeoutSeconds))
.writeTimeout(Duration.ofSeconds(timeoutSeconds))
.addInterceptor(chain -> {
logger.debug("Making request to ClairCore: {}", chain.request().url());
return chain.proceed(chain.request());
})
.build();
}
@Bean
public ObjectMapper clairObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
// Configure Jackson for ClairCore API
return mapper;
}
@Bean
public ClairCoreClient clairCoreClient(OkHttpClient httpClient, 
ObjectMapper objectMapper) {
return new ClairCoreClient(httpClient, objectMapper, clairCoreUrl);
}
}
2. ClairCore Client Implementation
package com.example.clair.core;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Component
public class ClairCoreClient {
private static final Logger logger = LoggerFactory.getLogger(ClairCoreClient.class);
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
private final String baseUrl;
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
public ClairCoreClient(OkHttpClient httpClient, ObjectMapper objectMapper, String baseUrl) {
this.httpClient = httpClient;
this.objectMapper = objectMapper;
this.baseUrl = baseUrl;
}
public IndexReport indexImage(String imageName, String imageTag) throws ClairCoreException {
try {
IndexRequest request = new IndexRequest(imageName, imageTag);
String jsonBody = objectMapper.writeValueAsString(request);
Request httpRequest = new Request.Builder()
.url(baseUrl + "/indexer/api/v1/index_report")
.post(RequestBody.create(jsonBody, JSON))
.build();
try (Response response = httpClient.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
throw new ClairCoreException("Failed to index image: " + response.code() + " - " + response.message());
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, IndexReport.class);
}
} catch (IOException e) {
throw new ClairCoreException("Error indexing image: " + imageName + ":" + imageTag, e);
}
}
public VulnerabilityReport getVulnerabilityReport(String manifestHash) throws ClairCoreException {
try {
Request request = new Request.Builder()
.url(baseUrl + "/matcher/api/v1/vulnerability_report/" + manifestHash)
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new ClairCoreException("Failed to get vulnerability report: " + response.code());
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, VulnerabilityReport.class);
}
} catch (IOException e) {
throw new ClairCoreException("Error getting vulnerability report for manifest: " + manifestHash, e);
}
}
public boolean isReady() {
try {
Request request = new Request.Builder()
.url(baseUrl + "/healthz")
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
return response.isSuccessful();
}
} catch (IOException e) {
logger.warn("ClairCore health check failed", e);
return false;
}
}
public List<String> getKnownUpdaters() throws ClairCoreException {
try {
Request request = new Request.Builder()
.url(baseUrl + "/matcher/api/v1/updaters")
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new ClairCoreException("Failed to get updaters: " + response.code());
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, new TypeReference<List<String>>() {});
}
} catch (IOException e) {
throw new ClairCoreException("Error getting updaters", e);
}
}
public void updateVulnerabilityDatabase() throws ClairCoreException {
try {
Request request = new Request.Builder()
.url(baseUrl + "/matcher/api/v1/update_operation")
.post(RequestBody.create("{}", JSON))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new ClairCoreException("Failed to update vulnerability database: " + response.code());
}
logger.info("Vulnerability database update triggered");
}
} catch (IOException e) {
throw new ClairCoreException("Error updating vulnerability database", e);
}
}
}
3. Data Models
package com.example.clair.core.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
@JsonIgnoreProperties(ignoreUnknown = true)
public class IndexRequest {
@JsonProperty("hash")
private String hash;
@JsonProperty("layers")
private List<Layer> layers;
public IndexRequest(String imageName, String imageTag) {
this.hash = generateManifestHash(imageName, imageTag);
// Layers would be populated from container registry
}
private String generateManifestHash(String imageName, String imageTag) {
return String.format("%s:%s", imageName, imageTag).hashCode() + "";
}
// Getters and setters
public String getHash() { return hash; }
public void setHash(String hash) { this.hash = hash; }
public List<Layer> getLayers() { return layers; }
public void setLayers(List<Layer> layers) { this.layers = layers; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public class IndexReport {
@JsonProperty("manifest_hash")
private String manifestHash;
@JsonProperty("state")
private String state;
@JsonProperty("layers")
private List<IndexedLayer> layers;
@JsonProperty("packages")
private Map<String, Package> packages;
@JsonProperty("distributions")
private Map<String, Distribution> distributions;
@JsonProperty("repositories")
private Map<String, Repository> repositories;
@JsonProperty("environments")
private Map<String, Environment> environments;
// Getters and setters
public String getManifestHash() { return manifestHash; }
public void setManifestHash(String manifestHash) { this.manifestHash = manifestHash; }
public String getState() { return state; }
public void setState(String state) { this.state = state; }
public List<IndexedLayer> getLayers() { return layers; }
public void setLayers(List<IndexedLayer> layers) { this.layers = layers; }
public Map<String, Package> getPackages() { return packages; }
public void setPackages(Map<String, Package> packages) { this.packages = packages; }
public Map<String, Distribution> getDistributions() { return distributions; }
public void setDistributions(Map<String, Distribution> distributions) { this.distributions = distributions; }
public Map<String, Repository> getRepositories() { return repositories; }
public void setRepositories(Map<String, Repository> repositories) { this.repositories = repositories; }
public Map<String, Environment> getEnvironments() { return environments; }
public void setEnvironments(Map<String, Environment> environments) { this.environments = environments; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public class VulnerabilityReport {
@JsonProperty("manifest_hash")
private String manifestHash;
@JsonProperty("vulnerabilities")
private List<Vulnerability> vulnerabilities;
@JsonProperty("packages")
private Map<String, Package> packages;
@JsonProperty("distributions")
private Map<String, Distribution> distributions;
@JsonProperty("repositories")
private Map<String, Repository> repositories;
@JsonProperty("environments")
private Map<String, Environment> environments;
// Getters and setters
public String getManifestHash() { return manifestHash; }
public void setManifestHash(String manifestHash) { this.manifestHash = manifestHash; }
public List<Vulnerability> getVulnerabilities() { return vulnerabilities; }
public void setVulnerabilities(List<Vulnerability> vulnerabilities) { this.vulnerabilities = vulnerabilities; }
public Map<String, Package> getPackages() { return packages; }
public void setPackages(Map<String, Package> packages) { this.packages = packages; }
public Map<String, Distribution> getDistributions() { return distributions; }
public void setDistributions(Map<String, Distribution> distributions) { this.distributions = distributions; }
public Map<String, Repository> getRepositories() { return repositories; }
public void setRepositories(Map<String, Repository> repositories) { this.repositories = repositories; }
public Map<String, Environment> getEnvironments() { return environments; }
public void setEnvironments(Map<String, Environment> environments) { this.environments = environments; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public class Vulnerability {
@JsonProperty("id")
private String id;
@JsonProperty("name")
private String name;
@JsonProperty("description")
private String description;
@JsonProperty("links")
private String links;
@JsonProperty("severity")
private String severity;
@JsonProperty("normalized_severity")
private String normalizedSeverity;
@JsonProperty("package")
private VulnerabilityPackage vulnerabilityPackage;
@JsonProperty("fixed_in_version")
private String fixedInVersion;
@JsonProperty("distro")
private String distro;
@JsonProperty("repo")
private String repo;
@JsonProperty("layer")
private String layer;
// Getters and setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getLinks() { return links; }
public void setLinks(String links) { this.links = links; }
public String getSeverity() { return severity; }
public void setSeverity(String severity) { this.severity = severity; }
public String getNormalizedSeverity() { return normalizedSeverity; }
public void setNormalizedSeverity(String normalizedSeverity) { this.normalizedSeverity = normalizedSeverity; }
public VulnerabilityPackage getVulnerabilityPackage() { return vulnerabilityPackage; }
public void setVulnerabilityPackage(VulnerabilityPackage vulnerabilityPackage) { this.vulnerabilityPackage = vulnerabilityPackage; }
public String getFixedInVersion() { return fixedInVersion; }
public void setFixedInVersion(String fixedInVersion) { this.fixedInVersion = fixedInVersion; }
public String getDistro() { return distro; }
public void setDistro(String distro) { this.distro = distro; }
public String getRepo() { return repo; }
public void setRepo(String repo) { this.repo = repo; }
public String getLayer() { return layer; }
public void setLayer(String layer) { this.layer = layer; }
}
@JsonIgnoreProperties(ignoreUnknown = true)
public class VulnerabilityPackage {
@JsonProperty("name")
private String name;
@JsonProperty("version")
private String version;
@JsonProperty("module")
private String module;
@JsonProperty("arch")
private String arch;
// 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 getModule() { return module; }
public void setModule(String module) { this.module = module; }
public String getArch() { return arch; }
public void setArch(String arch) { this.arch = arch; }
}
// Additional model classes
@JsonIgnoreProperties(ignoreUnknown = true)
public class Layer {
private String hash;
private String uri;
private Map<String, String> headers;
// Getters and setters
}
@JsonIgnoreProperties(ignoreUnknown = true)
public class IndexedLayer {
private String hash;
private List<String> packages;
private List<String> distributions;
private List<String> repositories;
// Getters and setters
}
@JsonIgnoreProperties(ignoreUnknown = true)
public class Package {
private String name;
private String version;
private String arch;
private String source;
// Getters and setters
}
@JsonIgnoreProperties(ignoreUnknown = true)
public class Distribution {
private String name;
private String version;
private String versionCodeName;
private String versionId;
private String arch;
// Getters and setters
}
@JsonIgnoreProperties(ignoreUnknown = true)
public class Repository {
private String name;
private String key;
private String uri;
// Getters and setters
}
@JsonIgnoreProperties(ignoreUnknown = true)
public class Environment {
private String packageDB;
private List<String> introducedIn;
// Getters and setters
}
4. Custom Exception
package com.example.clair.core;
public class ClairCoreException extends Exception {
public ClairCoreException(String message) {
super(message);
}
public ClairCoreException(String message, Throwable cause) {
super(message, cause);
}
}

Container Security Service

1. Main Security Service
package com.example.clair.core.service;
import com.example.clair.core.ClairCoreClient;
import com.example.clair.core.ClairCoreException;
import com.example.clair.core.model.IndexReport;
import com.example.clair.core.model.Vulnerability;
import com.example.clair.core.model.VulnerabilityReport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class ContainerSecurityService {
private static final Logger logger = LoggerFactory.getLogger(ContainerSecurityService.class);
private final ClairCoreClient clairCoreClient;
public ContainerSecurityService(ClairCoreClient clairCoreClient) {
this.clairCoreClient = clairCoreClient;
}
public SecurityScanResult scanImage(String imageName, String imageTag) {
try {
logger.info("Starting security scan for image: {}:{}", imageName, imageTag);
// Step 1: Index the image
IndexReport indexReport = clairCoreClient.indexImage(imageName, imageTag);
logger.debug("Image indexed successfully: {}", indexReport.getManifestHash());
// Step 2: Wait for indexing to complete (polling)
waitForIndexing(indexReport.getManifestHash());
// Step 3: Get vulnerability report
VulnerabilityReport vulnerabilityReport = clairCoreClient.getVulnerabilityReport(
indexReport.getManifestHash());
// Step 4: Analyze results
return analyzeVulnerabilities(vulnerabilityReport, imageName, imageTag);
} catch (ClairCoreException e) {
logger.error("Security scan failed for image: {}:{}", imageName, imageTag, e);
return SecurityScanResult.error(imageName, imageTag, e.getMessage());
}
}
public BatchScanResult scanMultipleImages(List<ContainerImage> images) {
BatchScanResult batchResult = new BatchScanResult();
for (ContainerImage image : images) {
try {
SecurityScanResult result = scanImage(image.getName(), image.getTag());
batchResult.addResult(result);
} catch (Exception e) {
logger.error("Failed to scan image: {}:{}", image.getName(), image.getTag(), e);
batchResult.addError(image.getName(), image.getTag(), e.getMessage());
}
}
return batchResult;
}
public SecurityReport generateSecurityReport(String imageName, String imageTag, 
SecurityPolicy policy) {
SecurityScanResult scanResult = scanImage(imageName, imageTag);
return SecurityReport.fromScanResult(scanResult, policy);
}
public boolean meetsSecurityStandards(String imageName, String imageTag, 
SecurityPolicy policy) {
SecurityScanResult result = scanImage(imageName, imageTag);
return result.meetsPolicy(policy);
}
private void waitForIndexing(String manifestHash) throws ClairCoreException {
int maxAttempts = 30;
int attempt = 0;
while (attempt < maxAttempts) {
try {
VulnerabilityReport report = clairCoreClient.getVulnerabilityReport(manifestHash);
if (report.getVulnerabilities() != null) {
return; // Indexing complete
}
Thread.sleep(2000); // Wait 2 seconds before next check
attempt++;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ClairCoreException("Indexing wait interrupted", e);
}
}
throw new ClairCoreException("Indexing timeout for manifest: " + manifestHash);
}
private SecurityScanResult analyzeVulnerabilities(VulnerabilityReport report, 
String imageName, String imageTag) {
List<Vulnerability> vulnerabilities = report.getVulnerabilities();
if (vulnerabilities == null || vulnerabilities.isEmpty()) {
return SecurityScanResult.clean(imageName, imageTag);
}
// Categorize vulnerabilities by severity
Map<String, Long> severityCount = vulnerabilities.stream()
.collect(Collectors.groupingBy(
Vulnerability::getNormalizedSeverity,
Collectors.counting()
));
// Find critical vulnerabilities
List<Vulnerability> criticalVulnerabilities = vulnerabilities.stream()
.filter(v -> "Critical".equalsIgnoreCase(v.getNormalizedSeverity()))
.collect(Collectors.toList());
// Find high vulnerabilities
List<Vulnerability> highVulnerabilities = vulnerabilities.stream()
.filter(v -> "High".equalsIgnoreCase(v.getNormalizedSeverity()))
.collect(Collectors.toList());
return SecurityScanResult.withVulnerabilities(
imageName, imageTag, vulnerabilities, severityCount,
criticalVulnerabilities, highVulnerabilities
);
}
public void updateVulnerabilityDatabase() {
try {
clairCoreClient.updateVulnerabilityDatabase();
logger.info("Vulnerability database update initiated");
} catch (ClairCoreException e) {
logger.error("Failed to update vulnerability database", e);
}
}
public boolean isClairCoreReady() {
return clairCoreClient.isReady();
}
}
2. Security Models and Results
package com.example.clair.core.service;
import com.example.clair.core.model.Vulnerability;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
public class SecurityScanResult {
private final String imageName;
private final String imageTag;
private final LocalDateTime scanTime;
private final boolean successful;
private final String errorMessage;
private final List<Vulnerability> vulnerabilities;
private final Map<String, Long> severityCount;
private final List<Vulnerability> criticalVulnerabilities;
private final List<Vulnerability> highVulnerabilities;
private final int totalVulnerabilities;
private SecurityScanResult(Builder builder) {
this.imageName = builder.imageName;
this.imageTag = builder.imageTag;
this.scanTime = builder.scanTime;
this.successful = builder.successful;
this.errorMessage = builder.errorMessage;
this.vulnerabilities = builder.vulnerabilities;
this.severityCount = builder.severityCount;
this.criticalVulnerabilities = builder.criticalVulnerabilities;
this.highVulnerabilities = builder.highVulnerabilities;
this.totalVulnerabilities = builder.totalVulnerabilities;
}
public static SecurityScanResult clean(String imageName, String imageTag) {
return new Builder()
.imageName(imageName)
.imageTag(imageTag)
.successful(true)
.scanTime(LocalDateTime.now())
.build();
}
public static SecurityScanResult error(String imageName, String imageTag, String errorMessage) {
return new Builder()
.imageName(imageName)
.imageTag(imageTag)
.successful(false)
.errorMessage(errorMessage)
.scanTime(LocalDateTime.now())
.build();
}
public static SecurityScanResult withVulnerabilities(String imageName, String imageTag,
List<Vulnerability> vulnerabilities,
Map<String, Long> severityCount,
List<Vulnerability> criticalVulnerabilities,
List<Vulnerability> highVulnerabilities) {
return new Builder()
.imageName(imageName)
.imageTag(imageTag)
.successful(true)
.vulnerabilities(vulnerabilities)
.severityCount(severityCount)
.criticalVulnerabilities(criticalVulnerabilities)
.highVulnerabilities(highVulnerabilities)
.totalVulnerabilities(vulnerabilities.size())
.scanTime(LocalDateTime.now())
.build();
}
public boolean meetsPolicy(SecurityPolicy policy) {
if (!successful) {
return false;
}
if (vulnerabilities == null || vulnerabilities.isEmpty()) {
return true;
}
// Check critical vulnerabilities
if (criticalVulnerabilities.size() > policy.getMaxCriticalVulnerabilities()) {
return false;
}
// Check high vulnerabilities
if (highVulnerabilities.size() > policy.getMaxHighVulnerabilities()) {
return false;
}
// Check total vulnerabilities
if (totalVulnerabilities > policy.getMaxTotalVulnerabilities()) {
return false;
}
return true;
}
// Getters
public String getImageName() { return imageName; }
public String getImageTag() { return imageTag; }
public LocalDateTime getScanTime() { return scanTime; }
public boolean isSuccessful() { return successful; }
public String getErrorMessage() { return errorMessage; }
public List<Vulnerability> getVulnerabilities() { return vulnerabilities; }
public Map<String, Long> getSeverityCount() { return severityCount; }
public List<Vulnerability> getCriticalVulnerabilities() { return criticalVulnerabilities; }
public List<Vulnerability> getHighVulnerabilities() { return highVulnerabilities; }
public int getTotalVulnerabilities() { return totalVulnerabilities; }
public boolean hasVulnerabilities() { return vulnerabilities != null && !vulnerabilities.isEmpty(); }
public static class Builder {
private String imageName;
private String imageTag;
private LocalDateTime scanTime;
private boolean successful;
private String errorMessage;
private List<Vulnerability> vulnerabilities;
private Map<String, Long> severityCount;
private List<Vulnerability> criticalVulnerabilities;
private List<Vulnerability> highVulnerabilities;
private int totalVulnerabilities;
public Builder imageName(String imageName) {
this.imageName = imageName;
return this;
}
public Builder imageTag(String imageTag) {
this.imageTag = imageTag;
return this;
}
public Builder scanTime(LocalDateTime scanTime) {
this.scanTime = scanTime;
return this;
}
public Builder successful(boolean successful) {
this.successful = successful;
return this;
}
public Builder errorMessage(String errorMessage) {
this.errorMessage = errorMessage;
return this;
}
public Builder vulnerabilities(List<Vulnerability> vulnerabilities) {
this.vulnerabilities = vulnerabilities;
return this;
}
public Builder severityCount(Map<String, Long> severityCount) {
this.severityCount = severityCount;
return this;
}
public Builder criticalVulnerabilities(List<Vulnerability> criticalVulnerabilities) {
this.criticalVulnerabilities = criticalVulnerabilities;
return this;
}
public Builder highVulnerabilities(List<Vulnerability> highVulnerabilities) {
this.highVulnerabilities = highVulnerabilities;
return this;
}
public Builder totalVulnerabilities(int totalVulnerabilities) {
this.totalVulnerabilities = totalVulnerabilities;
return this;
}
public SecurityScanResult build() {
return new SecurityScanResult(this);
}
}
}
public class SecurityPolicy {
private final int maxCriticalVulnerabilities;
private final int maxHighVulnerabilities;
private final int maxMediumVulnerabilities;
private final int maxTotalVulnerabilities;
private final List<String> allowedBaseImages;
private final List<String> bannedPackages;
private final boolean allowUnfixedVulnerabilities;
public SecurityPolicy(int maxCriticalVulnerabilities, int maxHighVulnerabilities,
int maxMediumVulnerabilities, int maxTotalVulnerabilities,
List<String> allowedBaseImages, List<String> bannedPackages,
boolean allowUnfixedVulnerabilities) {
this.maxCriticalVulnerabilities = maxCriticalVulnerabilities;
this.maxHighVulnerabilities = maxHighVulnerabilities;
this.maxMediumVulnerabilities = maxMediumVulnerabilities;
this.maxTotalVulnerabilities = maxTotalVulnerabilities;
this.allowedBaseImages = allowedBaseImages;
this.bannedPackages = bannedPackages;
this.allowUnfixedVulnerabilities = allowUnfixedVulnerabilities;
}
// Getters
public int getMaxCriticalVulnerabilities() { return maxCriticalVulnerabilities; }
public int getMaxHighVulnerabilities() { return maxHighVulnerabilities; }
public int getMaxMediumVulnerabilities() { return maxMediumVulnerabilities; }
public int getMaxTotalVulnerabilities() { return maxTotalVulnerabilities; }
public List<String> getAllowedBaseImages() { return allowedBaseImages; }
public List<String> getBannedPackages() { return bannedPackages; }
public boolean isAllowUnfixedVulnerabilities() { return allowUnfixedVulnerabilities; }
public static SecurityPolicy strict() {
return new SecurityPolicy(0, 0, 5, 10, 
List.of("ubuntu:20.04", "alpine:3.16"), 
List.of("telnet", "ftp"), false);
}
public static SecurityPolicy lenient() {
return new SecurityPolicy(1, 5, 20, 50,
List.of(), List.of(), true);
}
}
public class SecurityReport {
private final SecurityScanResult scanResult;
private final SecurityPolicy policy;
private final boolean compliant;
private final String summary;
private final List<String> recommendations;
public SecurityReport(SecurityScanResult scanResult, SecurityPolicy policy, 
boolean compliant, String summary, List<String> recommendations) {
this.scanResult = scanResult;
this.policy = policy;
this.compliant = compliant;
this.summary = summary;
this.recommendations = recommendations;
}
public static SecurityReport fromScanResult(SecurityScanResult scanResult, SecurityPolicy policy) {
boolean compliant = scanResult.meetsPolicy(policy);
String summary = generateSummary(scanResult, policy, compliant);
List<String> recommendations = generateRecommendations(scanResult, policy);
return new SecurityReport(scanResult, policy, compliant, summary, recommendations);
}
private static String generateSummary(SecurityScanResult scanResult, SecurityPolicy policy, boolean compliant) {
if (!scanResult.isSuccessful()) {
return "Scan failed: " + scanResult.getErrorMessage();
}
if (!scanResult.hasVulnerabilities()) {
return "No vulnerabilities found. Image is secure.";
}
if (compliant) {
return String.format("Image has %d vulnerabilities but meets policy standards.", 
scanResult.getTotalVulnerabilities());
} else {
return String.format("Image has %d vulnerabilities and does NOT meet policy standards.", 
scanResult.getTotalVulnerabilities());
}
}
private static List<String> generateRecommendations(SecurityScanResult scanResult, SecurityPolicy policy) {
List<String> recommendations = new ArrayList<>();
if (!scanResult.isSuccessful()) {
recommendations.add("Fix the scanning error: " + scanResult.getErrorMessage());
return recommendations;
}
if (scanResult.hasVulnerabilities()) {
if (scanResult.getCriticalVulnerabilities().size() > policy.getMaxCriticalVulnerabilities()) {
recommendations.add("Address critical vulnerabilities: " + 
scanResult.getCriticalVulnerabilities().size() + " found");
}
if (scanResult.getHighVulnerabilities().size() > policy.getMaxHighVulnerabilities()) {
recommendations.add("Address high severity vulnerabilities: " + 
scanResult.getHighVulnerabilities().size() + " found");
}
recommendations.add("Update base image to a more recent version");
recommendations.add("Remove unused packages to reduce attack surface");
}
return recommendations;
}
// Getters
public SecurityScanResult getScanResult() { return scanResult; }
public SecurityPolicy getPolicy() { return policy; }
public boolean isCompliant() { return compliant; }
public String getSummary() { return summary; }
public List<String> getRecommendations() { return recommendations; }
}
public class BatchScanResult {
private final List<SecurityScanResult> results;
private final List<ImageScanError> errors;
private final LocalDateTime scanTime;
public BatchScanResult() {
this.results = new ArrayList<>();
this.errors = new ArrayList<>();
this.scanTime = LocalDateTime.now();
}
public void addResult(SecurityScanResult result) {
results.add(result);
}
public void addError(String imageName, String imageTag, String error) {
errors.add(new ImageScanError(imageName, imageTag, error));
}
public int getTotalScanned() {
return results.size() + errors.size();
}
public int getSuccessfulScans() {
return (int) results.stream().filter(SecurityScanResult::isSuccessful).count();
}
public int getFailedScans() {
return errors.size() + (int) results.stream().filter(r -> !r.isSuccessful()).count();
}
public List<SecurityScanResult> getVulnerableImages() {
return results.stream()
.filter(SecurityScanResult::isSuccessful)
.filter(SecurityScanResult::hasVulnerabilities)
.collect(Collectors.toList());
}
// Getters
public List<SecurityScanResult> getResults() { return results; }
public List<ImageScanError> getErrors() { return errors; }
public LocalDateTime getScanTime() { return scanTime; }
}
public class ImageScanError {
private final String imageName;
private final String imageTag;
private final String error;
private final LocalDateTime timestamp;
public ImageScanError(String imageName, String imageTag, String error) {
this.imageName = imageName;
this.imageTag = imageTag;
this.error = error;
this.timestamp = LocalDateTime.now();
}
// Getters
public String getImageName() { return imageName; }
public String getImageTag() { return imageTag; }
public String getError() { return error; }
public LocalDateTime getTimestamp() { return timestamp; }
}
public class ContainerImage {
private final String name;
private final String tag;
public ContainerImage(String name, String tag) {
this.name = name;
this.tag = tag;
}
// Getters
public String getName() { return name; }
public String getTag() { return tag; }
}

REST API Controllers

1. Security Scan Controller
package com.example.clair.core.controller;
import com.example.clair.core.service.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/security")
public class SecurityScanController {
private static final Logger logger = LoggerFactory.getLogger(SecurityScanController.class);
private final ContainerSecurityService securityService;
public SecurityScanController(ContainerSecurityService securityService) {
this.securityService = securityService;
}
@PostMapping("/scan")
public ResponseEntity<SecurityScanResult> scanImage(
@RequestBody ScanRequest request) {
logger.info("Received scan request for image: {}:{}", 
request.getImageName(), request.getImageTag());
SecurityScanResult result = securityService.scanImage(
request.getImageName(), request.getImageTag());
return ResponseEntity.ok(result);
}
@PostMapping("/scan/batch")
public ResponseEntity<BatchScanResult> scanMultipleImages(
@RequestBody BatchScanRequest request) {
logger.info("Received batch scan request for {} images", 
request.getImages().size());
List<ContainerImage> images = request.getImages().stream()
.map(img -> new ContainerImage(img.getName(), img.getTag()))
.collect(Collectors.toList());
BatchScanResult result = securityService.scanMultipleImages(images);
return ResponseEntity.ok(result);
}
@PostMapping("/scan/with-policy")
public ResponseEntity<SecurityReport> scanWithPolicy(
@RequestBody PolicyScanRequest request) {
SecurityPolicy policy = createPolicyFromRequest(request.getPolicy());
SecurityReport report = securityService.generateSecurityReport(
request.getImageName(), request.getImageTag(), policy);
return ResponseEntity.ok(report);
}
@GetMapping("/compliance/{imageName}/{imageTag}")
public ResponseEntity<ComplianceResult> checkCompliance(
@PathVariable String imageName,
@PathVariable String imageTag,
@RequestParam(defaultValue = "strict") String policyType) {
SecurityPolicy policy = "strict".equals(policyType) ? 
SecurityPolicy.strict() : SecurityPolicy.lenient();
boolean compliant = securityService.meetsSecurityStandards(
imageName, imageTag, policy);
ComplianceResult result = new ComplianceResult(imageName, imageTag, policyType, compliant);
return ResponseEntity.ok(result);
}
@PostMapping("/database/update")
public ResponseEntity<Void> updateVulnerabilityDatabase() {
securityService.updateVulnerabilityDatabase();
return ResponseEntity.accepted().build();
}
@GetMapping("/health")
public ResponseEntity<HealthStatus> healthCheck() {
boolean clairReady = securityService.isClairCoreReady();
HealthStatus health = new HealthStatus(clairReady, "ClairCore integration");
return ResponseEntity.ok(health);
}
private SecurityPolicy createPolicyFromRequest(SecurityPolicyRequest policyRequest) {
return new SecurityPolicy(
policyRequest.getMaxCriticalVulnerabilities(),
policyRequest.getMaxHighVulnerabilities(),
policyRequest.getMaxMediumVulnerabilities(),
policyRequest.getMaxTotalVulnerabilities(),
policyRequest.getAllowedBaseImages(),
policyRequest.getBannedPackages(),
policyRequest.isAllowUnfixedVulnerabilities()
);
}
}
// Request/Response DTOs
class ScanRequest {
private String imageName;
private String imageTag;
// Getters and setters
public String getImageName() { return imageName; }
public void setImageName(String imageName) { this.imageName = imageName; }
public String getImageTag() { return imageTag; }
public void setImageTag(String imageTag) { this.imageTag = imageTag; }
}
class BatchScanRequest {
private List<ImageRequest> images;
// Getters and setters
public List<ImageRequest> getImages() { return images; }
public void setImages(List<ImageRequest> images) { this.images = images; }
}
class ImageRequest {
private String name;
private String tag;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getTag() { return tag; }
public void setTag(String tag) { this.tag = tag; }
}
class PolicyScanRequest {
private String imageName;
private String imageTag;
private SecurityPolicyRequest policy;
// Getters and setters
public String getImageName() { return imageName; }
public void setImageName(String imageName) { this.imageName = imageName; }
public String getImageTag() { return imageTag; }
public void setImageTag(String imageTag) { this.imageTag = imageTag; }
public SecurityPolicyRequest getPolicy() { return policy; }
public void setPolicy(SecurityPolicyRequest policy) { this.policy = policy; }
}
class SecurityPolicyRequest {
private int maxCriticalVulnerabilities = 0;
private int maxHighVulnerabilities = 5;
private int maxMediumVulnerabilities = 10;
private int maxTotalVulnerabilities = 50;
private List<String> allowedBaseImages = List.of();
private List<String> bannedPackages = List.of();
private boolean allowUnfixedVulnerabilities = false;
// Getters and setters
public int getMaxCriticalVulnerabilities() { return maxCriticalVulnerabilities; }
public void setMaxCriticalVulnerabilities(int maxCriticalVulnerabilities) { this.maxCriticalVulnerabilities = maxCriticalVulnerabilities; }
public int getMaxHighVulnerabilities() { return maxHighVulnerabilities; }
public void setMaxHighVulnerabilities(int maxHighVulnerabilities) { this.maxHighVulnerabilities = maxHighVulnerabilities; }
public int getMaxMediumVulnerabilities() { return maxMediumVulnerabilities; }
public void setMaxMediumVulnerabilities(int maxMediumVulnerabilities) { this.maxMediumVulnerabilities = maxMediumVulnerabilities; }
public int getMaxTotalVulnerabilities() { return maxTotalVulnerabilities; }
public void setMaxTotalVulnerabilities(int maxTotalVulnerabilities) { this.maxTotalVulnerabilities = maxTotalVulnerabilities; }
public List<String> getAllowedBaseImages() { return allowedBaseImages; }
public void setAllowedBaseImages(List<String> allowedBaseImages) { this.allowedBaseImages = allowedBaseImages; }
public List<String> getBannedPackages() { return bannedPackages; }
public void setBannedPackages(List<String> bannedPackages) { this.bannedPackages = bannedPackages; }
public boolean isAllowUnfixedVulnerabilities() { return allowUnfixedVulnerabilities; }
public void setAllowUnfixedVulnerabilities(boolean allowUnfixedVulnerabilities) { this.allowUnfixedVulnerabilities = allowUnfixedVulnerabilities; }
}
class ComplianceResult {
private final String imageName;
private final String imageTag;
private final String policyType;
private final boolean compliant;
private final String timestamp;
public ComplianceResult(String imageName, String imageTag, String policyType, boolean compliant) {
this.imageName = imageName;
this.imageTag = imageTag;
this.policyType = policyType;
this.compliant = compliant;
this.timestamp = java.time.LocalDateTime.now().toString();
}
// Getters
public String getImageName() { return imageName; }
public String getImageTag() { return imageTag; }
public String getPolicyType() { return policyType; }
public boolean isCompliant() { return compliant; }
public String getTimestamp() { return timestamp; }
}
class HealthStatus {
private final boolean healthy;
private final String service;
private final String timestamp;
public HealthStatus(boolean healthy, String service) {
this.healthy = healthy;
this.service = service;
this.timestamp = java.time.LocalDateTime.now().toString();
}
// Getters
public boolean isHealthy() { return healthy; }
public String getService() { return service; }
public String getTimestamp() { return timestamp; }
}

Custom Analyzers and Extensions

1. Custom Vulnerability Analyzer
package com.example.clair.core.analyzer;
import com.example.clair.core.model.Vulnerability;
import com.example.clair.core.model.VulnerabilityReport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Component
public class CustomVulnerabilityAnalyzer {
private static final Logger logger = LoggerFactory.getLogger(CustomVulnerabilityAnalyzer.class);
private static final Pattern CRITICAL_PATTERN = Pattern.compile(
"remote.*code.*execution|RCE|privilege.*escalation", Pattern.CASE_INSENSITIVE);
private static final List<String> IGNORED_VULNERABILITIES = List.of(
"CVE-2014-0160", // Heartbleed (example)
"CVE-2017-5638"  // Struts2 (example)
);
public AnalyzedVulnerabilities analyze(VulnerabilityReport report) {
List<Vulnerability> vulnerabilities = report.getVulnerabilities();
if (vulnerabilities == null || vulnerabilities.isEmpty()) {
return AnalyzedVulnerabilities.empty();
}
// Filter out ignored vulnerabilities
List<Vulnerability> filtered = vulnerabilities.stream()
.filter(vuln -> !IGNORED_VULNERABILITIES.contains(vuln.getId()))
.collect(Collectors.toList());
// Enhance severity based on custom rules
List<EnhancedVulnerability> enhanced = filtered.stream()
.map(this::enhanceVulnerability)
.collect(Collectors.toList());
// Categorize by risk level
List<EnhancedVulnerability> critical = enhanced.stream()
.filter(v -> v.getRiskLevel() == RiskLevel.CRITICAL)
.collect(Collectors.toList());
List<EnhancedVulnerability> high = enhanced.stream()
.filter(v -> v.getRiskLevel() == RiskLevel.HIGH)
.collect(Collectors.toList());
List<EnhancedVulnerability> medium = enhanced.stream()
.filter(v -> v.getRiskLevel() == RiskLevel.MEDIUM)
.collect(Collectors.toList());
List<EnhancedVulnerability> low = enhanced.stream()
.filter(v -> v.getRiskLevel() == RiskLevel.LOW)
.collect(Collectors.toList());
return new AnalyzedVulnerabilities(enhanced, critical, high, medium, low);
}
private EnhancedVulnerability enhanceVulnerability(Vulnerability vulnerability) {
RiskLevel riskLevel = calculateRiskLevel(vulnerability);
String customDescription = enhanceDescription(vulnerability);
List<String> mitigationSteps = suggestMitigation(vulnerability);
return new EnhancedVulnerability(vulnerability, riskLevel, customDescription, mitigationSteps);
}
private RiskLevel calculateRiskLevel(Vulnerability vulnerability) {
String severity = vulnerability.getNormalizedSeverity();
// Base risk level from Clair
RiskLevel baseLevel = switch (severity.toLowerCase()) {
case "critical" -> RiskLevel.CRITICAL;
case "high" -> RiskLevel.HIGH;
case "medium" -> RiskLevel.MEDIUM;
case "low" -> RiskLevel.LOW;
default -> RiskLevel.UNKNOWN;
};
// Enhance based on custom rules
if (isCriticalByDescription(vulnerability.getDescription())) {
return RiskLevel.CRITICAL;
}
if (affectsCriticalPackage(vulnerability)) {
return baseLevel.increase();
}
return baseLevel;
}
private boolean isCriticalByDescription(String description) {
return description != null && CRITICAL_PATTERN.matcher(description).find();
}
private boolean affectsCriticalPackage(Vulnerability vulnerability) {
String packageName = vulnerability.getVulnerabilityPackage().getName();
// Define critical packages
List<String> criticalPackages = List.of(
"openssl", "libssl", "glibc", "bash", "sudo",
"kernel", "linux-image", "docker", "containerd"
);
return criticalPackages.stream()
.anyMatch(critical -> packageName.toLowerCase().contains(critical));
}
private String enhanceDescription(Vulnerability vulnerability) {
String baseDescription = vulnerability.getDescription();
StringBuilder enhanced = new StringBuilder(baseDescription);
if (vulnerability.getFixedInVersion() != null && 
!vulnerability.getFixedInVersion().isEmpty()) {
enhanced.append("\n\nFixed in version: ").append(vulnerability.getFixedInVersion());
}
if (vulnerability.getLinks() != null && !vulnerability.getLinks().isEmpty()) {
enhanced.append("\nReferences: ").append(vulnerability.getLinks());
}
return enhanced.toString();
}
private List<String> suggestMitigation(Vulnerability vulnerability) {
List<String> mitigations = new ArrayList<>();
if (vulnerability.getFixedInVersion() != null && 
!vulnerability.getFixedInVersion().isEmpty()) {
mitigations.add("Update package to version: " + vulnerability.getFixedInVersion());
} else {
mitigations.add("No fixed version available. Consider alternative packages.");
}
if (affectsCriticalPackage(vulnerability)) {
mitigations.add("This affects a critical system package. Prioritize remediation.");
}
if (vulnerability.getNormalizedSeverity().equalsIgnoreCase("critical")) {
mitigations.add("Critical vulnerability - immediate action required.");
}
return mitigations;
}
}
enum RiskLevel {
CRITICAL(4),
HIGH(3),
MEDIUM(2),
LOW(1),
UNKNOWN(0);
private final int level;
RiskLevel(int level) {
this.level = level;
}
public int getLevel() {
return level;
}
public RiskLevel increase() {
return switch (this) {
case LOW -> MEDIUM;
case MEDIUM -> HIGH;
case HIGH, CRITICAL -> CRITICAL;
default -> this;
};
}
public RiskLevel decrease() {
return switch (this) {
case CRITICAL -> HIGH;
case HIGH -> MEDIUM;
case MEDIUM -> LOW;
default -> this;
};
}
}
class EnhancedVulnerability {
private final Vulnerability original;
private final RiskLevel riskLevel;
private final String enhancedDescription;
private final List<String> mitigationSteps;
public EnhancedVulnerability(Vulnerability original, RiskLevel riskLevel,
String enhancedDescription, List<String> mitigationSteps) {
this.original = original;
this.riskLevel = riskLevel;
this.enhancedDescription = enhancedDescription;
this.mitigationSteps = mitigationSteps;
}
// Getters
public Vulnerability getOriginal() { return original; }
public RiskLevel getRiskLevel() { return riskLevel; }
public String getEnhancedDescription() { return enhancedDescription; }
public List<String> getMitigationSteps() { return mitigationSteps; }
}
class AnalyzedVulnerabilities {
private final List<EnhancedVulnerability> allVulnerabilities;
private final List<EnhancedVulnerability> critical;
private final List<EnhancedVulnerability> high;
private final List<EnhancedVulnerability> medium;
private final List<EnhancedVulnerability> low;
public AnalyzedVulnerabilities(List<EnhancedVulnerability> allVulnerabilities,
List<EnhancedVulnerability> critical,
List<EnhancedVulnerability> high,
List<EnhancedVulnerability> medium,
List<EnhancedVulnerability> low) {
this.allVulnerabilities = allVulnerabilities;
this.critical = critical;
this.high = high;
this.medium = medium;
this.low = low;
}
public static AnalyzedVulnerabilities empty() {
return new AnalyzedVulnerabilities(
List.of(), List.of(), List.of(), List.of(), List.of());
}
// Getters
public List<EnhancedVulnerability> getAllVulnerabilities() { return allVulnerabilities; }
public List<EnhancedVulnerability> getCritical() { return critical; }
public List<EnhancedVulnerability> getHigh() { return high; }
public List<EnhancedVulnerability> getMedium() { return medium; }
public List<EnhancedVulnerability> getLow() { return low; }
public int getTotalCount() { return allVulnerabilities.size(); }
public boolean hasCritical() { return !critical.isEmpty(); }
public boolean hasHighOrCritical() { return !critical.isEmpty() || !high.isEmpty(); }
}

Testing

1. Unit Tests
package com.example.clair.core.service;
import com.example.clair.core.ClairCoreClient;
import com.example.clair.core.ClairCoreException;
import com.example.clair.core.model.Vulnerability;
import com.example.clair.core.model.VulnerabilityReport;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ContainerSecurityServiceTest {
@Mock
private ClairCoreClient clairCoreClient;
private ContainerSecurityService securityService;
@BeforeEach
void setUp() {
securityService = new ContainerSecurityService(clairCoreClient);
}
@Test
void testScanImage_Successful() throws Exception {
// Given
String imageName = "nginx";
String imageTag = "1.21";
String manifestHash = "abc123";
VulnerabilityReport vulnerabilityReport = new VulnerabilityReport();
vulnerabilityReport.setManifestHash(manifestHash);
when(clairCoreClient.indexImage(imageName, imageTag))
.thenReturn(createIndexReport(manifestHash));
when(clairCoreClient.getVulnerabilityReport(manifestHash))
.thenReturn(vulnerabilityReport);
// When
SecurityScanResult result = securityService.scanImage(imageName, imageTag);
// Then
assertTrue(result.isSuccessful());
assertEquals(imageName, result.getImageName());
assertEquals(imageTag, result.getImageTag());
assertFalse(result.hasVulnerabilities());
verify(clairCoreClient).indexImage(imageName, imageTag);
verify(clairCoreClient).getVulnerabilityReport(manifestHash);
}
@Test
void testScanImage_WithVulnerabilities() throws Exception {
// Given
String imageName = "vulnerable-app";
String imageTag = "latest";
String manifestHash = "def456";
VulnerabilityReport vulnerabilityReport = createVulnerabilityReport(manifestHash);
when(clairCoreClient.indexImage(imageName, imageTag))
.thenReturn(createIndexReport(manifestHash));
when(clairCoreClient.getVulnerabilityReport(manifestHash))
.thenReturn(vulnerabilityReport);
// When
SecurityScanResult result = securityService.scanImage(imageName, imageTag);
// Then
assertTrue(result.isSuccessful());
assertTrue(result.hasVulnerabilities());
assertEquals(2, result.getTotalVulnerabilities());
assertEquals(1, result.getCriticalVulnerabilities().size());
assertEquals(1, result.getHighVulnerabilities().size());
}
@Test
void testScanImage_ClairCoreException() throws Exception {
// Given
String imageName = "test-image";
String imageTag = "1.0";
when(clairCoreClient.indexImage(imageName, imageTag))
.thenThrow(new ClairCoreException("ClairCore unavailable"));
// When
SecurityScanResult result = securityService.scanImage(imageName, imageTag);
// Then
assertFalse(result.isSuccessful());
assertNotNull(result.getErrorMessage());
assertTrue(result.getErrorMessage().contains("ClairCore unavailable"));
}
@Test
void testMeetsSecurityStandards_Compliant() throws Exception {
// Given
String imageName = "compliant-image";
String imageTag = "1.0";
SecurityPolicy policy = SecurityPolicy.lenient();
VulnerabilityReport vulnerabilityReport = createVulnerabilityReport("hash123");
when(clairCoreClient.indexImage(imageName, imageTag))
.thenReturn(createIndexReport("hash123"));
when(clairCoreClient.getVulnerabilityReport("hash123"))
.thenReturn(vulnerabilityReport);
// When
boolean compliant = securityService.meetsSecurityStandards(imageName, imageTag, policy);
// Then
assertTrue(compliant);
}
@Test
void testMeetsSecurityStandards_NotCompliant() throws Exception {
// Given
String imageName = "non-compliant-image";
String imageTag = "1.0";
SecurityPolicy policy = SecurityPolicy.strict(); // No critical vulnerabilities allowed
VulnerabilityReport vulnerabilityReport = createVulnerabilityReport("hash456");
when(clairCoreClient.indexImage(imageName, imageTag))
.thenReturn(createIndexReport("hash456"));
when(clairCoreClient.getVulnerabilityReport("hash456"))
.thenReturn(vulnerabilityReport);
// When
boolean compliant = securityService.meetsSecurityStandards(imageName, imageTag, policy);
// Then
assertFalse(compliant);
}
private com.example.clair.core.model.IndexReport createIndexReport(String manifestHash) {
com.example.clair.core.model.IndexReport report = new com.example.clair.core.model.IndexReport();
report.setManifestHash(manifestHash);
report.setState("Indexed");
return report;
}
private VulnerabilityReport createVulnerabilityReport(String manifestHash) {
VulnerabilityReport report = new VulnerabilityReport();
report.setManifestHash(manifestHash);
// Create critical vulnerability
Vulnerability criticalVuln = new Vulnerability();
criticalVuln.setId("CVE-2023-1234");
criticalVuln.setName("Critical Remote Code Execution");
criticalVuln.setSeverity("Critical");
criticalVuln.setNormalizedSeverity("Critical");
Vulnerability highVuln = new Vulnerability();
highVuln.setId("CVE-2023-5678");
highVuln.setName("High Severity Vulnerability");
highVuln.setSeverity("High");
highVuln.setNormalizedSeverity("High");
report.setVulnerabilities(List.of(criticalVuln, highVuln));
return report;
}
}

Configuration and Deployment

1. Application Properties
# application.yml
claircore:
url: ${CLAIRCORE_URL:http://localhost:6060}
timeout:
seconds: 30
security:
policy:
default: strict
strict:
max-critical: 0
max-high: 0
max-medium: 5
max-total: 10
lenient:
max-critical: 1
max-high: 5
max-medium: 20
max-total: 50
logging:
level:
com.example.clair.core: DEBUG
2. Docker Compose for Development
# docker-compose.yml
version: '3.8'
services:
claircore:
image: quay.io/projectquay/clair-core:latest
ports:
- "6060:6060"
environment:
- CLAIR_CONF=/config/config.yaml
- CLAIR_MODE=combo
volumes:
- ./clair-config:/config
networks:
- clair-network
postgres:
image: postgres:13
environment:
- POSTGRES_DB=clair
- POSTGRES_USER=clair
- POSTGRES_PASSWORD=clair
networks:
- clair-network
clair-ui:
image: quay.io/projectquay/clair-ui:latest
ports:
- "8080:80"
environment:
- CLAIR_API_ENDPOINT=http://claircore:6060
networks:
- clair-network
depends_on:
- claircore
networks:
clair-network:
driver: bridge

Best Practices

  1. Regular Updates: Keep ClairCore and vulnerability databases updated
  2. Policy Enforcement: Implement strict security policies in CI/CD pipelines
  3. Monitoring: Monitor scan results and track vulnerability trends
  4. Integration: Integrate with container registries and orchestration platforms
  5. Customization: Develop custom analyzers for organization-specific requirements
// Example integration with CI/CD pipeline
public class CICDIntegration {
public boolean shouldDeploy(String imageName, String imageTag) {
SecurityPolicy policy = SecurityPolicy.strict();
return securityService.meetsSecurityStandards(imageName, imageTag, policy);
}
}

Conclusion

ClairCore Java integration provides:

  • Comprehensive container security scanning
  • Vulnerability analysis and risk assessment
  • Custom security policies and compliance checking
  • REST API for integration with other systems
  • Batch scanning capabilities
  • Extensible analyzer framework

By implementing ClairCore with custom analyzers and security policies, you can ensure container images meet security standards before deployment, reducing the attack surface and maintaining compliance with organizational security requirements.

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.

Leave a Reply

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


Macro Nepal Helper