ACR Scan provides native vulnerability scanning for container images stored in Azure Container Registry, integrating security directly into your container workflow.
Understanding ACR Scan
What is ACR Scan?
- Native vulnerability assessment for Azure Container Registry
- Powered by Qualys and Microsoft Security
- Scans Linux and Windows containers
- Integrates with Azure Security Center
Key Features:
- Automated Scanning: On-push and on-demand scanning
- Comprehensive Database: CVE vulnerabilities from multiple sources
- CI/CD Integration: Azure Pipelines and GitHub Actions
- Security Policies: Define and enforce compliance
- Remediation Guidance: Fix recommendations
Setup and Dependencies
1. Azure SDK Dependencies
<properties>
<azure-sdk.version>1.0.0</azure-sdk.version>
<azure-identity.version>1.8.0</azure-identity.version>
<azure-containerregistry.version>1.0.0</azure-containerregistry.version>
</properties>
<dependencies>
<!-- Azure Container Registry -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-containers-containerregistry</artifactId>
<version>${azure-containerregistry.version}</version>
</dependency>
<!-- Azure Identity -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>${azure-identity.version}</version>
</dependency>
<!-- Azure Security Center -->
<dependency>
<groupId>com.azure.resourcemanager</groupId>
<artifactId>azure-resourcemanager-security</artifactId>
<version>${azure-sdk.version}</version>
</dependency>
<!-- Azure Management Client -->
<dependency>
<groupId>com.azure.resourcemanager</groupId>
<artifactId>azure-resourcemanager-containerregistry</artifactId>
<version>${azure-sdk.version}</version>
</dependency>
</dependencies>
2. Azure Configuration
# application.yml
azure:
container-registry:
endpoint: https://yourregistry.azurecr.io
subscription-id: ${AZURE_SUBSCRIPTION_ID}
resource-group: your-resource-group
registry-name: yourregistry
security:
enabled: true
fail-on-critical: true
fail-on-high: false
scan-on-push: true
identity:
client-id: ${AZURE_CLIENT_ID}
client-secret: ${AZURE_CLIENT_SECRET}
tenant-id: ${AZURE_TENANT_ID}
ACR Scan Integration
1. ACR Scan Service
package com.yourapp.acr;
import com.azure.containers.containerregistry.*;
import com.azure.containers.containerregistry.models.*;
import com.azure.core.credential.TokenCredential;
import com.azure.identity.DefaultAzureCredential;
import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.resourcemanager.containerregistry.ContainerRegistryManager;
import com.azure.resourcemanager.containerregistry.models.Registry;
import com.azure.resourcemanager.security.SecurityManager;
import com.azure.resourcemanager.security.models.Scan;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.*;
@Service
public class AcrScanService {
private static final Logger logger = LoggerFactory.getLogger(AcrScanService.class);
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${azure.container-registry.endpoint}")
private String registryEndpoint;
@Value("${azure.container-registry.registry-name}")
private String registryName;
@Value("${azure.container-registry.resource-group}")
private String resourceGroup;
private final ContainerRegistryClient registryClient;
private final ContainerRegistryManager registryManager;
private final SecurityManager securityManager;
public AcrScanService() {
TokenCredential credential = new DefaultAzureCredentialBuilder().build();
this.registryClient = new ContainerRegistryClientBuilder()
.endpoint(registryEndpoint)
.credential(credential)
.buildClient();
this.registryManager = ContainerRegistryManager.authenticate(credential,
com.azure.core.management.profile.AzureProfile.fromEnvironment(
new com.azure.core.management.AzureEnvironment(null)));
this.securityManager = SecurityManager.authenticate(credential,
com.azure.core.management.profile.AzureProfile.fromEnvironment(
new com.azure.core.management.AzureEnvironment(null)));
}
public ScanResult scanImage(String repository, String tag) throws Exception {
return scanImage(repository, tag, new ScanOptions());
}
public ScanResult scanImage(String repository, String tag, ScanOptions options)
throws Exception {
String imageReference = String.format("%s/%s:%s", registryName, repository, tag);
logger.info("Starting ACR scan for image: {}", imageReference);
// Trigger scan
triggerScan(repository, tag);
// Wait for scan completion
ScanStatus scanStatus = waitForScanCompletion(repository, tag, options.getTimeout());
if (scanStatus != ScanStatus.COMPLETED) {
throw new AcrScanException("Scan failed or timed out. Status: " + scanStatus);
}
// Get scan results
return getScanResults(repository, tag);
}
private void triggerScan(String repository, String tag) {
try {
// Using Azure CLI command as Java SDK doesn't directly expose scan trigger
ProcessBuilder processBuilder = new ProcessBuilder(
"az", "acr", "scan",
"--name", registryName,
"--repository", repository,
"--image", tag,
"--resource-group", resourceGroup
);
Process process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new AcrScanException("Failed to trigger ACR scan");
}
logger.info("ACR scan triggered successfully for {}/{}", repository, tag);
} catch (Exception e) {
throw new AcrScanException("Error triggering ACR scan", e);
}
}
private ScanStatus waitForScanCompletion(String repository, String tag, Duration timeout)
throws InterruptedException {
long startTime = System.currentTimeMillis();
long timeoutMs = timeout.toMillis();
while (System.currentTimeMillis() - startTime < timeoutMs) {
ScanStatus status = getScanStatus(repository, tag);
if (status == ScanStatus.COMPLETED || status == ScanStatus.FAILED) {
return status;
}
logger.debug("Scan status for {}/{}: {}", repository, tag, status);
Thread.sleep(5000); // Wait 5 seconds between checks
}
return ScanStatus.TIMEOUT;
}
private ScanStatus getScanStatus(String repository, String tag) {
try {
ProcessBuilder processBuilder = new ProcessBuilder(
"az", "acr", "repository", "show",
"--name", registryName,
"--repository", repository,
"--image", tag,
"--query", "scanStatus",
"--output", "tsv"
);
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String status = reader.readLine();
int exitCode = process.waitFor();
if (exitCode == 0 && status != null) {
return ScanStatus.fromString(status.trim());
}
} catch (Exception e) {
logger.warn("Failed to get scan status for {}/{}", repository, tag, e);
}
return ScanStatus.UNKNOWN;
}
private ScanResult getScanResults(String repository, String tag) throws Exception {
try {
ProcessBuilder processBuilder = new ProcessBuilder(
"az", "acr", "repository", "show",
"--name", registryName,
"--repository", repository,
"--image", tag,
"--query", "scanResult",
"--output", "json"
);
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
StringBuilder jsonResult = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
jsonResult.append(line);
}
int exitCode = process.waitFor();
if (exitCode == 0) {
return parseScanResult(jsonResult.toString(), repository, tag);
}
} catch (Exception e) {
logger.error("Failed to get scan results for {}/{}", repository, tag, e);
}
throw new AcrScanException("Failed to retrieve scan results");
}
private ScanResult parseScanResult(String jsonResult, String repository, String tag)
throws Exception {
JsonNode root = objectMapper.readTree(jsonResult);
ScanResult result = new ScanResult();
result.setRepository(repository);
result.setTag(tag);
result.setScanTimestamp(root.path("scanTimestamp").asText());
result.setScanStatus(ScanStatus.fromString(root.path("status").asText()));
result.setVulnerabilities(new ArrayList<>());
JsonNode vulnerabilities = root.path("vulnerabilities");
if (vulnerabilities.isArray()) {
for (JsonNode vuln : vulnerabilities) {
Vulnerability vulnerability = parseVulnerability(vuln);
result.getVulnerabilities().add(vulnerability);
}
}
// Calculate summary
calculateSummary(result);
return result;
}
private Vulnerability parseVulnerability(JsonNode vuln) {
Vulnerability vulnerability = new Vulnerability();
vulnerability.setId(vuln.path("id").asText());
vulnerability.setTitle(vuln.path("title").asText());
vulnerability.setDescription(vuln.path("description").asText());
vulnerability.setSeverity(vuln.path("severity").asText());
vulnerability.setPackageName(vuln.path("packageName").asText());
vulnerability.setVersion(vuln.path("version").asText());
vulnerability.setFixedIn(vuln.path("fixedIn").asText());
vulnerability.setCvssScore(vuln.path("cvssScore").asDouble());
vulnerability.setCve(vuln.path("cve").asText());
vulnerability.setLink(vuln.path("link").asText());
vulnerability.setAssessment(vuln.path("assessment").asText());
return vulnerability;
}
private void calculateSummary(ScanResult result) {
Summary summary = new Summary();
List<Vulnerability> vulnerabilities = result.getVulnerabilities();
summary.setTotal(vulnerabilities.size());
summary.setCritical((int) vulnerabilities.stream()
.filter(v -> "Critical".equalsIgnoreCase(v.getSeverity()))
.count());
summary.setHigh((int) vulnerabilities.stream()
.filter(v -> "High".equalsIgnoreCase(v.getSeverity()))
.count());
summary.setMedium((int) vulnerabilities.stream()
.filter(v -> "Medium".equalsIgnoreCase(v.getSeverity()))
.count());
summary.setLow((int) vulnerabilities.stream()
.filter(v -> "Low".equalsIgnoreCase(v.getSeverity()))
.count());
result.setSummary(summary);
}
// Additional methods for repository management
public List<String> listRepositories() {
List<String> repositories = new ArrayList<>();
registryClient.listRepositoryNames().forEach(repositories::add);
return repositories;
}
public List<String> listTags(String repository) {
List<String> tags = new ArrayList<>();
ContainerRepository containerRepository = registryClient.getRepository(repository);
containerRepository.listTagProperties().forEach(tag -> tags.add(tag.getName()));
return tags;
}
public void enableScanOnPush(String repository) {
try {
ProcessBuilder processBuilder = new ProcessBuilder(
"az", "acr", "config", "scan", "update",
"--name", registryName,
"--repository", repository,
"--scan-on-push", "true",
"--resource-group", resourceGroup
);
Process process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode == 0) {
logger.info("Enabled scan-on-push for repository: {}", repository);
} else {
throw new AcrScanException("Failed to enable scan-on-push");
}
} catch (Exception e) {
throw new AcrScanException("Error enabling scan-on-push", e);
}
}
}
@Data
public class ScanResult {
private String repository;
private String tag;
private String scanTimestamp;
private ScanStatus scanStatus;
private List<Vulnerability> vulnerabilities;
private Summary summary;
}
@Data
public class Vulnerability {
private String id;
private String title;
private String description;
private String severity;
private String packageName;
private String version;
private String fixedIn;
private double cvssScore;
private String cve;
private String link;
private String assessment;
}
@Data
public class Summary {
private int total;
private int critical;
private int high;
private int medium;
private int low;
}
@Data
public class ScanOptions {
private Duration timeout = Duration.ofMinutes(10);
private boolean waitForCompletion = true;
public static ScanOptions defaultOptions() {
return new ScanOptions();
}
public static ScanOptions quickScan() {
ScanOptions options = new ScanOptions();
options.setTimeout(Duration.ofMinutes(5));
return options;
}
}
public enum ScanStatus {
COMPLETED("Completed"),
FAILED("Failed"),
RUNNING("Running"),
QUEUED("Queued"),
TIMEOUT("Timeout"),
UNKNOWN("Unknown");
private final String value;
ScanStatus(String value) {
this.value = value;
}
public static ScanStatus fromString(String value) {
for (ScanStatus status : values()) {
if (status.value.equalsIgnoreCase(value)) {
return status;
}
}
return UNKNOWN;
}
}
public class AcrScanException extends RuntimeException {
public AcrScanException(String message) {
super(message);
}
public AcrScanException(String message, Throwable cause) {
super(message, cause);
}
}
2. Security Policy Engine for ACR
@Service
public class AcrSecurityPolicyEngine {
private final AcrSecurityPolicy policy;
private final AcrScanService scanService;
public PolicyEvaluation evaluateImage(String repository, String tag) throws Exception {
ScanResult scanResult = scanService.scanImage(repository, tag);
return evaluateScanResult(scanResult);
}
public PolicyEvaluation evaluateScanResult(ScanResult scanResult) {
PolicyEvaluation evaluation = new PolicyEvaluation();
evaluation.setScanResult(scanResult);
evaluation.setViolations(new ArrayList<>());
// Check critical vulnerabilities
if (policy.getMaxCritical() >= 0 &&
scanResult.getSummary().getCritical() > policy.getMaxCritical()) {
evaluation.getViolations().add(
new PolicyViolation(
"CRITICAL_VULNERABILITIES",
"Exceeded maximum critical vulnerabilities: " +
scanResult.getSummary().getCritical() + " > " + policy.getMaxCritical(),
ViolationSeverity.CRITICAL
)
);
}
// Check high vulnerabilities
if (policy.getMaxHigh() >= 0 &&
scanResult.getSummary().getHigh() > policy.getMaxHigh()) {
evaluation.getViolations().add(
new PolicyViolation(
"HIGH_VULNERABILITIES",
"Exceeded maximum high vulnerabilities: " +
scanResult.getSummary().getHigh() + " > " + policy.getMaxHigh(),
ViolationSeverity.HIGH
)
);
}
// Check CVSS score threshold
checkCvssThreshold(scanResult, evaluation);
// Check prohibited packages
checkProhibitedPackages(scanResult, evaluation);
// Check base image vulnerabilities
checkBaseImageVulnerabilities(scanResult, evaluation);
evaluation.setCompliant(evaluation.getViolations().isEmpty());
evaluation.setEvaluationTime(LocalDateTime.now());
return evaluation;
}
private void checkCvssThreshold(ScanResult scanResult, PolicyEvaluation evaluation) {
for (Vulnerability vuln : scanResult.getVulnerabilities()) {
if (vuln.getCvssScore() >= policy.getMaxCvssScore()) {
evaluation.getViolations().add(
new PolicyViolation(
"CVSS_THRESHOLD",
"Vulnerability exceeds CVSS threshold: " +
vuln.getCve() + " (" + vuln.getCvssScore() + ")",
ViolationSeverity.HIGH
)
);
}
}
}
private void checkProhibitedPackages(ScanResult scanResult, PolicyEvaluation evaluation) {
for (Vulnerability vuln : scanResult.getVulnerabilities()) {
if (policy.getProhibitedPackages().contains(vuln.getPackageName())) {
evaluation.getViolations().add(
new PolicyViolation(
"PROHIBITED_PACKAGE",
"Prohibited package found: " + vuln.getPackageName(),
ViolationSeverity.HIGH
)
);
}
}
}
private void checkBaseImageVulnerabilities(ScanResult scanResult, PolicyEvaluation evaluation) {
long baseImageVulns = scanResult.getVulnerabilities().stream()
.filter(v -> v.getPackageName().startsWith("base-image"))
.filter(v -> "Critical".equals(v.getSeverity()) || "High".equals(v.getSeverity()))
.count();
if (baseImageVulns > policy.getMaxBaseImageVulnerabilities()) {
evaluation.getViolations().add(
new PolicyViolation(
"BASE_IMAGE_VULNERABILITIES",
"Too many base image vulnerabilities: " + baseImageVulns,
ViolationSeverity.MEDIUM
)
);
}
}
public boolean canDeploy(PolicyEvaluation evaluation) {
if (evaluation.isCompliant()) {
return true;
}
// Check if violations are overridable
return evaluation.getViolations().stream()
.allMatch(v -> policy.isOverrideAllowed(v.getType()));
}
}
@Data
public class AcrSecurityPolicy {
private String name;
private String version;
private int maxCritical = 0;
private int maxHigh = 5;
private int maxMedium = 20;
private int maxLow = -1; // -1 means no limit
private double maxCvssScore = 8.0;
private int maxBaseImageVulnerabilities = 3;
private Set<String> prohibitedPackages = new HashSet<>();
private Map<String, Boolean> overrideRules = new HashMap<>();
private List<String> allowedExceptions = new ArrayList<>();
public AcrSecurityPolicy() {
// Default override rules
overrideRules.put("CRITICAL_VULNERABILITIES", false);
overrideRules.put("HIGH_VULNERABILITIES", true);
overrideRules.put("CVSS_THRESHOLD", true);
overrideRules.put("PROHIBITED_PACKAGE", false);
overrideRules.put("BASE_IMAGE_VULNERABILITIES", true);
}
public boolean isOverrideAllowed(String violationType) {
return overrideRules.getOrDefault(violationType, false);
}
public static AcrSecurityPolicy strictPolicy() {
AcrSecurityPolicy policy = new AcrSecurityPolicy();
policy.setName("Strict ACR Security Policy");
policy.setMaxCritical(0);
policy.setMaxHigh(0);
policy.setMaxMedium(5);
policy.setMaxCvssScore(7.0);
policy.setMaxBaseImageVulnerabilities(0);
return policy;
}
public static AcrSecurityPolicy productionPolicy() {
AcrSecurityPolicy policy = new AcrSecurityPolicy();
policy.setName("Production ACR Security Policy");
policy.setMaxCritical(0);
policy.setMaxHigh(2);
policy.setMaxMedium(10);
policy.setMaxCvssScore(8.0);
policy.setMaxBaseImageVulnerabilities(2);
return policy;
}
}
@Data
public class PolicyEvaluation {
private ScanResult scanResult;
private boolean compliant;
private List<PolicyViolation> violations;
private LocalDateTime evaluationTime;
public PolicyEvaluation() {
this.violations = new ArrayList<>();
this.evaluationTime = LocalDateTime.now();
}
}
@Data
public class PolicyViolation {
private String type;
private String message;
private ViolationSeverity severity;
private String remediation;
public PolicyViolation(String type, String message, ViolationSeverity severity) {
this.type = type;
this.message = message;
this.severity = severity;
}
}
public enum ViolationSeverity {
CRITICAL, HIGH, MEDIUM, LOW
}
Azure DevOps Integration
1. Azure Pipeline Configuration
# azure-pipelines.yml trigger: branches: include: - main - develop paths: exclude: - README.md variables: registryName: 'yourregistry' resourceGroup: 'your-resource-group' imageRepository: 'yourapp' dockerfilePath: '**/Dockerfile' tag: '$(Build.BuildId)' stages: - stage: Build displayName: Build and Scan jobs: - job: Build displayName: Build and Push steps: - task: Maven@3 inputs: mavenPomFile: 'pom.xml' goals: 'package' options: '-DskipTests' - task: Docker@2 displayName: Build Docker Image inputs: command: build dockerfile: '$(dockerfilePath)' tags: | $(tag) latest arguments: '--build-arg JAR_FILE=target/*.jar' - task: Docker@2 displayName: Push to ACR inputs: command: push repository: $(imageRepository) tags: | $(tag) latest containerRegistry: 'ACRServiceConnection' - job: SecurityScan displayName: Security Scan dependsOn: Build steps: - task: AzureCLI@2 displayName: Trigger ACR Scan inputs: azureSubscription: 'AzureServiceConnection' scriptType: bash scriptLocation: inlineScript inlineScript: | az acr scan --name $(registryName) \ --repository $(imageRepository) \ --image $(tag) \ --resource-group $(resourceGroup) - task: AzureCLI@2 displayName: Wait for Scan Completion inputs: azureSubscription: 'AzureServiceConnection' scriptType: bash scriptLocation: inlineScript inlineScript: | # Wait for scan completion with timeout timeout=600 interval=10 counter=0 while [ $counter -lt $timeout ]; do status=$(az acr repository show \ --name $(registryName) \ --repository $(imageRepository) \ --image $(tag) \ --query scanStatus \ --output tsv) if [ "$status" == "Completed" ]; then echo "Scan completed successfully" break elif [ "$status" == "Failed" ]; then echo "Scan failed" exit 1 fi echo "Scan status: $status. Waiting..." sleep $interval counter=$((counter + interval)) done if [ $counter -ge $timeout ]; then echo "Scan timed out" exit 1 fi - task: AzureCLI@2 displayName: Check Scan Results inputs: azureSubscription: 'AzureServiceConnection' scriptType: bash scriptLocation: inlineScript inlineScript: | result=$(az acr repository show \ --name $(registryName) \ --repository $(imageRepository) \ --image $(tag) \ --query scanResult \ --output json) # Parse JSON and check for critical vulnerabilities critical_count=$(echo $result | jq '.summary.critical') high_count=$(echo $result | jq '.summary.high') echo "Critical vulnerabilities: $critical_count" echo "High vulnerabilities: $high_count" if [ $critical_count -gt 0 ]; then echo "##vso[task.logissue type=error]Critical vulnerabilities found: $critical_count" exit 1 fi if [ $high_count -gt 5 ]; then echo "##vso[task.logissue type=error]Too many high vulnerabilities: $high_count" exit 1 fi - task: PublishBuildArtifacts@1 displayName: Publish Scan Report inputs: pathToPublish: '$(System.DefaultWorkingDirectory)/scan-results' artifactName: 'SecurityScanReport' - stage: Deploy displayName: Deploy to Production dependsOn: Build condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) jobs: - deployment: Deploy environment: production strategy: runOnce: deploy: steps: - download: current artifact: SecurityScanReport - task: AzureCLI@2 displayName: Final Security Check inputs: azureSubscription: 'AzureServiceConnection' scriptType: bash scriptLocation: inlineScript inlineScript: | # Final security gate before deployment java -cp target/yourapp.jar com.yourapp.acr.SecurityGate \ --registry $(registryName) \ --repository $(imageRepository) \ --tag $(tag) \ --policy production-policy.json
2. Java-based Security Gate
@Component
public class AcrSecurityGate {
private final AcrScanService scanService;
private final AcrSecurityPolicyEngine policyEngine;
public SecurityGateResult checkDeploymentApproval(String repository, String tag,
String policyName) throws Exception {
SecurityGateResult result = new SecurityGateResult();
result.setRepository(repository);
result.setTag(tag);
result.setCheckTime(LocalDateTime.now());
// Get latest scan results
ScanResult scanResult = scanService.scanImage(repository, tag);
result.setScanResult(scanResult);
// Evaluate against policy
PolicyEvaluation evaluation = policyEngine.evaluateScanResult(scanResult);
result.setEvaluation(evaluation);
// Check if deployment can proceed
result.setApproved(policyEngine.canDeploy(evaluation));
// Generate report
result.setReport(generateSecurityReport(result));
return result;
}
public void enforceSecurityGate(String repository, String tag, String policyName)
throws Exception {
SecurityGateResult result = checkDeploymentApproval(repository, tag, policyName);
if (!result.isApproved()) {
String message = String.format(
"Security gate failed for %s/%s. Violations: %d",
repository, tag, result.getEvaluation().getViolations().size()
);
throw new SecurityGateException(message, result);
}
logger.info("Security gate passed for {}/{}", repository, tag);
}
private SecurityReport generateSecurityReport(SecurityGateResult result) {
SecurityReport report = new SecurityReport();
report.setGeneratedAt(LocalDateTime.now());
report.setRepository(result.getRepository());
report.setTag(result.getTag());
report.setApproved(result.isApproved());
ScanResult scanResult = result.getScanResult();
report.setTotalVulnerabilities(scanResult.getSummary().getTotal());
report.setCriticalVulnerabilities(scanResult.getSummary().getCritical());
report.setHighVulnerabilities(scanResult.getSummary().getHigh());
PolicyEvaluation evaluation = result.getEvaluation();
report.setViolations(evaluation.getViolations());
report.setCompliant(evaluation.isCompliant());
// Generate recommendations
report.setRecommendations(generateRecommendations(scanResult, evaluation));
return report;
}
private List<String> generateRecommendations(ScanResult scanResult,
PolicyEvaluation evaluation) {
List<String> recommendations = new ArrayList<>();
if (scanResult.getSummary().getCritical() > 0) {
recommendations.add("Fix critical vulnerabilities before deployment");
}
if (scanResult.getSummary().getHigh() > 5) {
recommendations.add("Reduce high severity vulnerabilities");
}
if (!evaluation.isCompliant()) {
recommendations.add("Review and address policy violations");
}
// Base image recommendations
long baseImageVulns = scanResult.getVulnerabilities().stream()
.filter(v -> v.getPackageName().startsWith("base-image"))
.count();
if (baseImageVulns > 10) {
recommendations.add("Consider updating base image to reduce vulnerabilities");
}
return recommendations;
}
}
@Data
public class SecurityGateResult {
private String repository;
private String tag;
private LocalDateTime checkTime;
private ScanResult scanResult;
private PolicyEvaluation evaluation;
private boolean approved;
private SecurityReport report;
}
@Data
public class SecurityReport {
private LocalDateTime generatedAt;
private String repository;
private String tag;
private boolean approved;
private boolean compliant;
private int totalVulnerabilities;
private int criticalVulnerabilities;
private int highVulnerabilities;
private List<PolicyViolation> violations;
private List<String> recommendations;
}
public class SecurityGateException extends RuntimeException {
private final SecurityGateResult result;
public SecurityGateException(String message, SecurityGateResult result) {
super(message);
this.result = result;
}
public SecurityGateResult getResult() {
return result;
}
}
Advanced ACR Features
1. Continuous Monitoring Service
@Service
public class AcrContinuousMonitoringService {
private final AcrScanService scanService;
private final AcrSecurityPolicyEngine policyEngine;
private final NotificationService notificationService;
@Scheduled(cron = "0 0 6 * * *") // Daily at 6 AM
public void monitorProductionImages() {
List<String> productionRepositories = getProductionRepositories();
for (String repository : productionRepositories) {
try {
List<String> tags = getProductionTags(repository);
for (String tag : tags) {
monitorImage(repository, tag);
}
} catch (Exception e) {
logger.error("Failed to monitor repository: {}", repository, e);
}
}
}
private void monitorImage(String repository, String tag) {
try {
ScanResult scanResult = scanService.scanImage(repository, tag);
PolicyEvaluation evaluation = policyEngine.evaluateScanResult(scanResult);
if (!evaluation.isCompliant()) {
sendSecurityAlert(repository, tag, evaluation);
}
// Store results for trending
storeScanHistory(repository, tag, scanResult, evaluation);
// Check for new critical vulnerabilities
checkNewCriticalVulnerabilities(repository, tag, scanResult);
} catch (Exception e) {
logger.error("Failed to monitor image {}/{}", repository, tag, e);
}
}
private void checkNewCriticalVulnerabilities(String repository, String tag,
ScanResult currentScan) {
ScanResult previousScan = getPreviousScan(repository, tag);
if (previousScan != null) {
Set<String> previousCriticalCves = getCriticalCves(previousScan);
Set<String> currentCriticalCves = getCriticalCves(currentScan);
// Find new critical vulnerabilities
Set<String> newCriticalCves = new HashSet<>(currentCriticalCves);
newCriticalCves.removeAll(previousCriticalCves);
if (!newCriticalCves.isEmpty()) {
sendNewCriticalAlert(repository, tag, newCriticalCves);
}
}
}
private void sendSecurityAlert(String repository, String tag,
PolicyEvaluation evaluation) {
SecurityAlert alert = new SecurityAlert();
alert.setRepository(repository);
alert.setTag(tag);
alert.setEvaluation(evaluation);
alert.setGeneratedAt(LocalDateTime.now());
alert.setSeverity(calculateAlertSeverity(evaluation));
notificationService.sendSecurityAlert(alert);
// Create Azure DevOps work item for critical issues
if (alert.getSeverity() == AlertSeverity.CRITICAL) {
createSecurityWorkItem(alert);
}
}
private Set<String> getCriticalCves(ScanResult scanResult) {
return scanResult.getVulnerabilities().stream()
.filter(v -> "Critical".equals(v.getSeverity()))
.map(Vulnerability::getCve)
.collect(Collectors.toSet());
}
}
@Data
public class SecurityAlert {
private String repository;
private String tag;
private PolicyEvaluation evaluation;
private LocalDateTime generatedAt;
private AlertSeverity severity;
private String alertMessage;
public SecurityAlert() {
this.generatedAt = LocalDateTime.now();
}
}
public enum AlertSeverity {
CRITICAL, HIGH, MEDIUM, LOW
}
2. ACR Task Integration
@Service
public class AcrTaskService {
public void createScanTask(String repository, String schedule) {
try {
String taskName = "scan-" + repository.replace("/", "-");
ProcessBuilder processBuilder = new ProcessBuilder(
"az", "acr", "task", "create",
"--name", taskName,
"--registry", registryName,
"--context", "/dev/null",
"--file", "acr-task.yaml",
"--schedule", schedule,
"--resource-group", resourceGroup,
"--cmd", "acr scan --registry {{.Run.Registry}} --repository " + repository
);
Process process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode == 0) {
logger.info("Created ACR task for repository: {}", repository);
} else {
throw new AcrTaskException("Failed to create ACR task");
}
} catch (Exception e) {
throw new AcrTaskException("Error creating ACR task", e);
}
}
public void triggerScanTask(String taskName) {
try {
ProcessBuilder processBuilder = new ProcessBuilder(
"az", "acr", "task", "run",
"--name", taskName,
"--registry", registryName,
"--resource-group", resourceGroup
);
Process process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode == 0) {
logger.info("Triggered ACR task: {}", taskName);
} else {
throw new AcrTaskException("Failed to trigger ACR task");
}
} catch (Exception e) {
throw new AcrTaskException("Error triggering ACR task", e);
}
}
}
# acr-task.yaml
version: v1.1.0
steps:
- cmd: acr scan --registry {{.Run.Registry}} --repository {{.Values.repository}} --image {{.Run.ID}}
timeout: 3600
when: ["-"]
3. Remediation Automation
@Service
public class AcrRemediationService {
private final AcrScanService scanService;
private final DockerfileService dockerfileService;
private final GitService gitService;
public RemediationPlan generateRemediationPlan(String repository, String tag)
throws Exception {
ScanResult scanResult = scanService.scanImage(repository, tag);
return generateRemediationPlan(scanResult);
}
public RemediationPlan generateRemediationPlan(ScanResult scanResult) {
RemediationPlan plan = new RemediationPlan();
plan.setRepository(scanResult.getRepository());
plan.setTag(scanResult.getTag());
plan.setGeneratedDate(LocalDateTime.now());
plan.setActions(new ArrayList<>());
// Group vulnerabilities by type
Map<String, List<Vulnerability>> vulnerabilitiesByType =
groupVulnerabilitiesByType(scanResult.getVulnerabilities());
// Create remediation actions
for (Map.Entry<String, List<Vulnerability>> entry : vulnerabilitiesByType.entrySet()) {
RemediationAction action = createRemediationAction(entry.getKey(), entry.getValue());
if (action != null) {
plan.getActions().add(action);
}
}
// Sort by priority
plan.getActions().sort(Comparator.comparing(RemediationAction::getPriority).reversed());
plan.setEstimatedEffort(calculateEffort(plan.getActions()));
return plan;
}
private Map<String, List<Vulnerability>> groupVulnerabilitiesByType(
List<Vulnerability> vulnerabilities) {
Map<String, List<Vulnerability>> grouped = new HashMap<>();
for (Vulnerability vuln : vulnerabilities) {
String type = determineVulnerabilityType(vuln);
grouped.computeIfAbsent(type, k -> new ArrayList<>()).add(vuln);
}
return grouped;
}
private String determineVulnerabilityType(Vulnerability vuln) {
if (vuln.getPackageName().startsWith("base-image")) {
return "BASE_IMAGE";
} else if (vuln.getPackageName().contains("java") ||
vuln.getPackageName().contains("maven")) {
return "JAVA_DEPENDENCY";
} else if (vuln.getPackageName().contains("lib") ||
vuln.getPackageName().contains("openssl")) {
return "SYSTEM_LIBRARY";
} else {
return "OTHER";
}
}
private RemediationAction createRemediationAction(String type,
List<Vulnerability> vulnerabilities) {
if (vulnerabilities.isEmpty()) {
return null;
}
RemediationAction action = new RemediationAction();
action.setType(type);
action.setVulnerabilities(vulnerabilities);
action.setPriority(calculatePriority(vulnerabilities));
action.setAutomated(isAutomated(type));
action.setInstructions(generateInstructions(type, vulnerabilities));
return action;
}
public void executeRemediation(RemediationAction action) throws Exception {
switch (action.getType()) {
case "BASE_IMAGE":
updateBaseImage(action);
break;
case "JAVA_DEPENDENCY":
updateJavaDependencies(action);
break;
case "SYSTEM_LIBRARY":
updateSystemLibraries(action);
break;
default:
logger.warn("Manual remediation required for type: {}", action.getType());
}
}
private void updateBaseImage(RemediationAction action) throws Exception {
// Find the most common base image update
String targetBaseImage = findRecommendedBaseImage(action.getVulnerabilities());
if (targetBaseImage != null) {
dockerfileService.updateBaseImage(targetBaseImage);
// Commit and push changes
gitService.commitAndPush(
"Update base image to " + targetBaseImage,
"Security: Fix base image vulnerabilities\n\n" +
"Updated base image to address " + action.getVulnerabilities().size() +
" vulnerabilities"
);
}
}
}
@Data
public class RemediationPlan {
private String repository;
private String tag;
private LocalDateTime generatedDate;
private List<RemediationAction> actions;
private String estimatedEffort;
}
@Data
public class RemediationAction {
private String type;
private List<Vulnerability> vulnerabilities;
private int priority;
private boolean automated;
private String instructions;
private String branchName;
}
Best Practices
1. ACR Security Checklist
public class AcrSecurityChecklist {
public static final List<String> SECURITY_CHECKS = Arrays.asList(
"1. Enable admin user for ACR or use managed identity",
"2. Configure network rules to restrict access",
"3. Enable content trust for image signing",
"4. Implement retention policies for old images",
"5. Enable scan-on-push for all repositories",
"6. Configure security policies for vulnerability thresholds",
"7. Set up continuous monitoring for production images",
"8. Implement automated remediation for common issues",
"9. Use private endpoints for secure connectivity",
"10. Regularly review and update base images"
);
public static AcrSecurityConfiguration secureConfiguration() {
AcrSecurityConfiguration config = new AcrSecurityConfiguration();
config.setScanOnPush(true);
config.setRetentionPolicy(30); // days
config.setContentTrust(true);
config.setNetworkRestriction(true);
config.setPrivateEndpoint(true);
return config;
}
}
@Data
public class AcrSecurityConfiguration {
private boolean scanOnPush;
private int retentionPolicy;
private boolean contentTrust;
private boolean networkRestriction;
private boolean privateEndpoint;
private List<String> allowedIpRanges;
}
2. Monitoring and Alerting
@Service
public class AcrMonitoringService {
@Scheduled(fixedRate = 300000) // 5 minutes
public void monitorScanHealth() {
try {
List<String> repositories = scanService.listRepositories();
for (String repository : repositories) {
monitorRepositoryHealth(repository);
}
} catch (Exception e) {
logger.error("Failed to monitor ACR scan health", e);
}
}
private void monitorRepositoryHealth(String repository) {
try {
List<String> tags = scanService.listTags(repository);
long failedScans = tags.stream()
.map(tag -> getScanStatus(repository, tag))
.filter(status -> status == ScanStatus.FAILED)
.count();
if (failedScans > 0) {
sendHealthAlert(repository, failedScans);
}
} catch (Exception e) {
logger.warn("Failed to monitor repository health: {}", repository, e);
}
}
}
Conclusion
ACR Scan integration provides:
- Native vulnerability scanning for Azure Container Registry
- Policy-based security enforcement with customizable thresholds
- CI/CD pipeline integration for automated security gates
- Continuous monitoring of production container images
- Automated remediation guidance and execution
- Comprehensive reporting and compliance tracking
By implementing the patterns and configurations shown above, you can establish a robust container security practice that leverages Azure's native capabilities while integrating seamlessly with your Java development workflow.
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.