ACR Scan in Java: Comprehensive Azure Container Registry Security Scanning

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.

Leave a Reply

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


Macro Nepal Helper