ECR Image Scanning in Java: Complete Guide

Introduction to ECR Image Scanning

Amazon ECR (Elastic Container Registry) provides built-in image scanning that identifies software vulnerabilities in container images. When combined with Java applications, it helps maintain secure container deployments by scanning for CVEs and providing detailed vulnerability reports.


System Architecture Overview

ECR Image Scanning Pipeline
├── Image Building
│   ├ - Secure Dockerfile
│   ├ - Multi-stage Builds
│   ├ - Base Image Security
│   └ - Dependency Management
├── ECR Integration
│   ├ - Image Push
│   ├ - Automated Scanning
│   ├ - Vulnerability Assessment
│   └ - Scan Results
├── Security Gates
│   ├ - CVSS Thresholds
│   ├ - Policy Enforcement
│   ├ - Manual Approval
│   └ - Break-glass Procedures
└── Results & Actions
├ - EventBridge Events
├ - SNS Notifications
├ - Lambda Remediation
└ - Security Dashboards

Core Implementation

1. AWS SDK Dependencies & Configuration

Maven Configuration

<properties>
<aws.java.sdk.version>2.20.0</aws.java.sdk.version>
<spring.cloud.aws.version>2.4.4</spring.cloud.aws.version>
</properties>
<dependencies>
<!-- AWS SDK v2 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>ecr</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>ecrpublic</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>lambda</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>eventbridge</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sns</artifactId>
<version>${aws.java.sdk.version}</version>
</dependency>
<!-- Spring Cloud AWS -->
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter</artifactId>
<version>${spring.cloud.aws.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>

AWS Configuration Class

@Configuration
@EnableConfigurationProperties(AwsProperties.class)
public class AwsConfig {
@Value("${aws.region:us-east-1}")
private String region;
@Bean
@Primary
public AwsCredentialsProvider awsCredentialsProvider() {
return DefaultCredentialsProvider.create();
}
@Bean
public EcrClient ecrClient(AwsCredentialsProvider credentialsProvider) {
return EcrClient.builder()
.region(Region.of(region))
.credentialsProvider(credentialsProvider)
.build();
}
@Bean
public EventBridgeClient eventBridgeClient(AwsCredentialsProvider credentialsProvider) {
return EventBridgeClient.builder()
.region(Region.of(region))
.credentialsProvider(credentialsProvider)
.build();
}
@Bean
public LambdaClient lambdaClient(AwsCredentialsProvider credentialsProvider) {
return LambdaClient.builder()
.region(Region.of(region))
.credentialsProvider(credentialsProvider)
.build();
}
@Bean
public SnsClient snsClient(AwsCredentialsProvider credentialsProvider) {
return SnsClient.builder()
.region(Region.of(region))
.credentialsProvider(credentialsProvider)
.build();
}
}
@ConfigurationProperties(prefix = "aws")
@Data
public class AwsProperties {
private String region = "us-east-1";
private String accountId;
private Ecr ecr = new Ecr();
@Data
public static class Ecr {
private String repositoryName = "my-java-app";
private String scanOnPush = "true";
private String scanFrequency = "SCAN_ON_PUSH";
private List<String> scanFilters = List.of("CRITICAL", "HIGH");
}
}

2. ECR Image Scanning Service

Core Scanning Service

@Service
@Slf4j
public class EcrScanService {
private final EcrClient ecrClient;
private final AwsProperties awsProperties;
private final ObjectMapper objectMapper;
public EcrScanService(EcrClient ecrClient, AwsProperties awsProperties, ObjectMapper objectMapper) {
this.ecrClient = ecrClient;
this.awsProperties = awsProperties;
this.objectMapper = objectMapper;
}
/**
* Trigger ECR image scan for a specific image tag
*/
public StartImageScanResponse triggerImageScan(String repositoryName, String imageTag) {
try {
StartImageScanRequest request = StartImageScanRequest.builder()
.repositoryName(repositoryName)
.imageId(ImageIdentifier.builder().imageTag(imageTag).build())
.build();
StartImageScanResponse response = ecrClient.startImageScan(request);
log.info("Started ECR image scan for {}/{}", repositoryName, imageTag);
return response;
} catch (EcrException e) {
log.error("Failed to start ECR image scan for {}/{}: {}", 
repositoryName, imageTag, e.getMessage());
throw new EcrScanException("Failed to start image scan", e);
}
}
/**
* Get detailed scan findings for an image
*/
public DescribeImageScanFindingsResponse getScanFindings(String repositoryName, String imageTag) {
try {
DescribeImageScanFindingsRequest request = DescribeImageScanFindingsRequest.builder()
.repositoryName(repositoryName)
.imageId(ImageIdentifier.builder().imageTag(imageTag).build())
.maxResults(100)
.build();
return ecrClient.describeImageScanFindings(request);
} catch (EcrException e) {
log.error("Failed to get scan findings for {}/{}: {}", 
repositoryName, imageTag, e.getMessage());
throw new EcrScanException("Failed to get scan findings", e);
}
}
/**
* Wait for scan completion and return results
*/
public ImageScanResult waitForScanCompletion(String repositoryName, String imageTag, 
Duration timeout) {
Instant startTime = Instant.now();
Duration pollInterval = Duration.ofSeconds(10);
while (Duration.between(startTime, Instant.now()).compareTo(timeout) < 0) {
try {
DescribeImageScanFindingsResponse response = getScanFindings(repositoryName, imageTag);
ImageScanFindings findings = response.imageScanFindings();
if (findings != null && findings.imageScanStatus() != null) {
ImageScanStatus status = findings.imageScanStatus();
if (status.status() == ScanStatus.COMPLETE) {
return parseScanResults(findings, repositoryName, imageTag);
} else if (status.status() == ScanStatus.FAILED) {
throw new EcrScanException("Image scan failed: " + status.description());
}
// If scan is in progress, continue waiting
}
Thread.sleep(pollInterval.toMillis());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new EcrScanException("Scan wait interrupted", e);
}
}
throw new EcrScanException("Scan timeout after " + timeout);
}
/**
* Parse and analyze scan results
*/
private ImageScanResult parseScanResults(ImageScanFindings findings, 
String repositoryName, String imageTag) {
ImageScanResult result = new ImageScanResult();
result.setRepositoryName(repositoryName);
result.setImageTag(imageTag);
result.setScanCompletedAt(findings.imageScanCompletedAt());
result.setVulnerabilitySourceUpdatedAt(findings.vulnerabilitySourceUpdatedAt());
// Analyze findings by severity
if (findings.findings() != null) {
Map<String, List<Vulnerability>> vulnerabilitiesBySeverity = 
findings.findings().stream()
.collect(Collectors.groupingBy(
Finding::getSeverity,
Collectors.mapping(this::convertToVulnerability, Collectors.toList())
));
result.setVulnerabilitiesBySeverity(vulnerabilitiesBySeverity);
result.setTotalFindings(findings.findings().size());
// Calculate risk score
result.setRiskScore(calculateRiskScore(vulnerabilitiesBySeverity));
}
// Check enhanced findings if available
if (findings.enhancedFindings() != null) {
result.setEnhancedFindings(findings.enhancedFindings().stream()
.map(this::convertToEnhancedFinding)
.collect(Collectors.toList()));
}
return result;
}
/**
* Calculate risk score based on vulnerability severity
*/
private double calculateRiskScore(Map<String, List<Vulnerability>> vulnerabilitiesBySeverity) {
double score = 0.0;
score += vulnerabilitiesBySeverity.getOrDefault("CRITICAL", List.of()).size() * 10.0;
score += vulnerabilitiesBySeverity.getOrDefault("HIGH", List.of()).size() * 5.0;
score += vulnerabilitiesBySeverity.getOrDefault("MEDIUM", List.of()).size() * 2.0;
score += vulnerabilitiesBySeverity.getOrDefault("LOW", List.of()).size() * 0.5;
score += vulnerabilitiesBySeverity.getOrDefault("INFORMATIONAL", List.of()).size() * 0.1;
return score;
}
/**
* Check if image passes security gates
*/
public SecurityGateResult checkSecurityGates(ImageScanResult scanResult, 
SecurityGateConfig config) {
SecurityGateResult result = new SecurityGateResult();
result.setScanResult(scanResult);
result.setConfig(config);
Map<String, List<Vulnerability>> vulnerabilities = 
scanResult.getVulnerabilitiesBySeverity();
// Check critical vulnerabilities
int criticalCount = vulnerabilities.getOrDefault("CRITICAL", List.of()).size();
if (criticalCount > config.getMaxCritical()) {
result.addFailure("CRITICAL", 
String.format("Found %d critical vulnerabilities, maximum allowed: %d", 
criticalCount, config.getMaxCritical()));
}
// Check high vulnerabilities
int highCount = vulnerabilities.getOrDefault("HIGH", List.of()).size();
if (highCount > config.getMaxHigh()) {
result.addFailure("HIGH", 
String.format("Found %d high vulnerabilities, maximum allowed: %d", 
highCount, config.getMaxHigh()));
}
// Check risk score
if (scanResult.getRiskScore() > config.getMaxRiskScore()) {
result.addFailure("RISK_SCORE", 
String.format("Risk score %.2f exceeds maximum allowed: %.2f", 
scanResult.getRiskScore(), config.getMaxRiskScore()));
}
result.setPassed(result.getFailures().isEmpty());
return result;
}
/**
* Get scan findings with pagination
*/
public List<Finding> getAllScanFindings(String repositoryName, String imageTag) {
List<Finding> allFindings = new ArrayList<>();
String nextToken = null;
do {
DescribeImageScanFindingsRequest request = DescribeImageScanFindingsRequest.builder()
.repositoryName(repositoryName)
.imageId(ImageIdentifier.builder().imageTag(imageTag).build())
.nextToken(nextToken)
.maxResults(100)
.build();
DescribeImageScanFindingsResponse response = ecrClient.describeImageScanFindings(request);
if (response.imageScanFindings() != null && response.imageScanFindings().findings() != null) {
allFindings.addAll(response.imageScanFindings().findings());
}
nextToken = response.nextToken();
} while (nextToken != null);
return allFindings;
}
/**
* Convert AWS Finding to domain Vulnerability
*/
private Vulnerability convertToVulnerability(Finding finding) {
Vulnerability vulnerability = new Vulnerability();
vulnerability.setName(finding.name());
vulnerability.setSeverity(finding.severity());
vulnerability.setUri(finding.uri());
vulnerability.setAttributes(finding.attributes());
vulnerability.setDescription(getDescriptionFromAttributes(finding.attributes()));
// Extract CVSS score if available
finding.attributes().stream()
.filter(attr -> "CVSS2_VECTOR".equals(attr.key()) || "CVSS2_SCORE".equals(attr.key()))
.forEach(attr -> {
if ("CVSS2_SCORE".equals(attr.key())) {
try {
vulnerability.setCvssScore(Double.parseDouble(attr.value()));
} catch (NumberFormatException e) {
log.warn("Invalid CVSS score: {}", attr.value());
}
}
});
return vulnerability;
}
/**
* Convert enhanced finding
*/
private EnhancedFinding convertToEnhancedFinding(software.amazon.awssdk.services.ecr.model.EnhancedFinding enhancedFinding) {
return EnhancedFinding.builder()
.packageVulnerabilityDetails(enhancedFinding.packageVulnerabilityDetails())
.score(enhancedFinding.score())
.scoreDetails(enhancedFinding.scoreDetails())
.severity(enhancedFinding.severity())
.status(enhancedFinding.status())
.title(enhancedFinding.title())
.type(enhancedFinding.type())
.updatedAt(enhancedFinding.updatedAt())
.build();
}
private String getDescriptionFromAttributes(List<Attribute> attributes) {
return attributes.stream()
.filter(attr -> "description".equals(attr.key()))
.map(Attribute::value)
.findFirst()
.orElse("No description available");
}
}

3. Domain Models

Scan Result Models

@Data
@Builder
public class ImageScanResult {
private String repositoryName;
private String imageTag;
private Instant scanCompletedAt;
private Instant vulnerabilitySourceUpdatedAt;
private Map<String, List<Vulnerability>> vulnerabilitiesBySeverity;
private List<EnhancedFinding> enhancedFindings;
private int totalFindings;
private double riskScore;
private String scanStatus;
}
@Data
@Builder
public class Vulnerability {
private String name;
private String severity;
private String uri;
private String description;
private Double cvssScore;
private List<Attribute> attributes;
private String packageName;
private String packageVersion;
private String fixedInVersion;
}
@Data
@Builder
public class EnhancedFinding {
private PackageVulnerabilityDetails packageVulnerabilityDetails;
private Double score;
private ScoreDetails scoreDetails;
private String severity;
private String status;
private String title;
private String type;
private Instant updatedAt;
}
@Data
public class SecurityGateResult {
private ImageScanResult scanResult;
private SecurityGateConfig config;
private boolean passed;
private List<String> failures = new ArrayList<>();
public void addFailure(String type, String message) {
failures.add(String.format("[%s] %s", type, message));
}
}
@Data
@Builder
public class SecurityGateConfig {
@Builder.Default
private int maxCritical = 0;
@Builder.Default
private int maxHigh = 5;
@Builder.Default
private int maxMedium = 20;
@Builder.Default
private int maxLow = 50;
@Builder.Default
private double maxRiskScore = 15.0;
@Builder.Default
private List<String> allowedCves = List.of();
@Builder.Default
private boolean blockOnCritical = true;
}
public class EcrScanException extends RuntimeException {
public EcrScanException(String message) {
super(message);
}
public EcrScanException(String message, Throwable cause) {
super(message, cause);
}
}

4. Event-Driven Scanning with EventBridge

EventBridge Listener

@Component
@Slf4j
public class EcrScanEventListener {
private final EcrScanService ecrScanService;
private final SecurityNotificationService notificationService;
private final ObjectMapper objectMapper;
public EcrScanEventListener(EcrScanService ecrScanService, 
SecurityNotificationService notificationService,
ObjectMapper objectMapper) {
this.ecrScanService = ecrScanService;
this.notificationService = notificationService;
this.objectMapper = objectMapper;
}
/**
* Process ECR scan completion events
*/
@EventListener
public void handleEcrScanEvent(String eventJson) {
try {
EcrScanEvent event = objectMapper.readValue(eventJson, EcrScanEvent.class);
if ("ECR Image Scan".equals(event.getSource()) && 
"aws.ecr".equals(event.getSource())) {
processScanEvent(event);
}
} catch (Exception e) {
log.error("Failed to process ECR scan event: {}", e.getMessage(), e);
}
}
private void processScanEvent(EcrScanEvent event) {
String detailType = event.getDetailType();
if ("ECR Image Scan".equals(detailType)) {
EcrScanDetail detail = event.getDetail();
String repositoryName = detail.getRepositoryName();
String imageDigest = detail.getImageDigest();
log.info("Processing ECR scan event for repository: {}, digest: {}", 
repositoryName, imageDigest);
// Get scan findings
ImageScanResult scanResult = ecrScanService.waitForScanCompletion(
repositoryName, getImageTagFromDigest(imageDigest), Duration.ofMinutes(5));
// Check security gates
SecurityGateConfig config = getSecurityGateConfig();
SecurityGateResult gateResult = ecrScanService.checkSecurityGates(scanResult, config);
// Take action based on result
if (gateResult.isPassed()) {
handleScanPassed(scanResult);
} else {
handleScanFailed(gateResult);
}
}
}
private void handleScanPassed(ImageScanResult scanResult) {
log.info("ECR scan passed for {}/{} with risk score: {}", 
scanResult.getRepositoryName(), scanResult.getImageTag(), scanResult.getRiskScore());
// Notify success
notificationService.sendScanSuccessNotification(scanResult);
// Trigger deployment or next steps
triggerDeploymentIfNeeded(scanResult);
}
private void handleScanFailed(SecurityGateResult gateResult) {
ImageScanResult scanResult = gateResult.getScanResult();
log.warn("ECR scan failed for {}/{}: {}", 
scanResult.getRepositoryName(), scanResult.getImageTag(), 
String.join(", ", gateResult.getFailures()));
// Send failure notification
notificationService.sendScanFailureNotification(gateResult);
// Block deployment if configured
if (gateResult.getConfig().isBlockOnCritical()) {
blockDeployment(scanResult);
}
}
private SecurityGateConfig getSecurityGateConfig() {
return SecurityGateConfig.builder()
.maxCritical(0)
.maxHigh(3)
.maxMedium(10)
.maxLow(25)
.maxRiskScore(20.0)
.blockOnCritical(true)
.build();
}
private String getImageTagFromDigest(String imageDigest) {
// Extract image tag from digest
// This is a simplified implementation
return "latest"; // In practice, you'd need to map digest to tag
}
private void triggerDeploymentIfNeeded(ImageScanResult scanResult) {
// Implement deployment triggering logic
log.info("Triggering deployment for secure image: {}/{}", 
scanResult.getRepositoryName(), scanResult.getImageTag());
}
private void blockDeployment(ImageScanResult scanResult) {
// Implement deployment blocking logic
log.warn("Blocking deployment for insecure image: {}/{}", 
scanResult.getRepositoryName(), scanResult.getImageTag());
}
}
@Data
class EcrScanEvent {
private String version;
private String id;
private String detailType;
private String source;
private String account;
private String time;
private String region;
private List<String> resources;
private EcrScanDetail detail;
}
@Data
class EcrScanDetail {
private String scanStatus;
private String repositoryName;
private Map<String, String> findingSeverityCounts;
private String imageDigest;
private Map<String, Object> imageTags;
}

5. Security Notification Service

Notification Service

@Service
@Slf4j
public class SecurityNotificationService {
private final SnsClient snsClient;
private final ObjectMapper objectMapper;
@Value("${aws.sns.security.topic.arn}")
private String securityTopicArn;
public SecurityNotificationService(SnsClient snsClient, ObjectMapper objectMapper) {
this.snsClient = snsClient;
this.objectMapper = objectMapper;
}
/**
* Send scan success notification
*/
public void sendScanSuccessNotification(ImageScanResult scanResult) {
try {
SecurityNotification notification = SecurityNotification.builder()
.type("SCAN_SUCCESS")
.title("ECR Image Scan Passed")
.message(buildSuccessMessage(scanResult))
.repositoryName(scanResult.getRepositoryName())
.imageTag(scanResult.getImageTag())
.riskScore(scanResult.getRiskScore())
.totalFindings(scanResult.getTotalFindings())
.timestamp(Instant.now())
.build();
sendNotification(notification);
} catch (Exception e) {
log.error("Failed to send scan success notification: {}", e.getMessage(), e);
}
}
/**
* Send scan failure notification
*/
public void sendScanFailureNotification(SecurityGateResult gateResult) {
try {
SecurityNotification notification = SecurityNotification.builder()
.type("SCAN_FAILURE")
.title("ECR Image Scan Failed Security Gates")
.message(buildFailureMessage(gateResult))
.repositoryName(gateResult.getScanResult().getRepositoryName())
.imageTag(gateResult.getScanResult().getImageTag())
.riskScore(gateResult.getScanResult().getRiskScore())
.totalFindings(gateResult.getScanResult().getTotalFindings())
.failures(gateResult.getFailures())
.timestamp(Instant.now())
.build();
sendNotification(notification);
} catch (Exception e) {
log.error("Failed to send scan failure notification: {}", e.getMessage(), e);
}
}
/**
* Send critical vulnerability alert
*/
public void sendCriticalVulnerabilityAlert(ImageScanResult scanResult, 
List<Vulnerability> criticalVulns) {
try {
SecurityNotification notification = SecurityNotification.builder()
.type("CRITICAL_VULNERABILITY")
.title("Critical Vulnerabilities Detected")
.message(buildCriticalAlertMessage(scanResult, criticalVulns))
.repositoryName(scanResult.getRepositoryName())
.imageTag(scanResult.getImageTag())
.riskScore(scanResult.getRiskScore())
.criticalVulnerabilities(criticalVulns)
.timestamp(Instant.now())
.build();
sendNotification(notification);
} catch (Exception e) {
log.error("Failed to send critical vulnerability alert: {}", e.getMessage(), e);
}
}
private void sendNotification(SecurityNotification notification) {
try {
String message = objectMapper.writeValueAsString(notification);
PublishRequest request = PublishRequest.builder()
.topicArn(securityTopicArn)
.message(message)
.subject(notification.getTitle())
.build();
snsClient.publish(request);
log.info("Sent security notification: {}", notification.getType());
} catch (Exception e) {
log.error("Failed to publish SNS notification: {}", e.getMessage(), e);
throw new RuntimeException("Notification failed", e);
}
}
private String buildSuccessMessage(ImageScanResult scanResult) {
return String.format(
"ECR image scan passed for %s/%s\n" +
"Risk Score: %.2f\n" +
"Total Findings: %d\n" +
"Scan Completed: %s",
scanResult.getRepositoryName(),
scanResult.getImageTag(),
scanResult.getRiskScore(),
scanResult.getTotalFindings(),
scanResult.getScanCompletedAt()
);
}
private String buildFailureMessage(SecurityGateResult gateResult) {
ImageScanResult scanResult = gateResult.getScanResult();
StringBuilder message = new StringBuilder();
message.append(String.format(
"ECR image scan FAILED security gates for %s/%s\n" +
"Risk Score: %.2f (Max: %.2f)\n" +
"Total Findings: %d\n\n" +
"Failures:\n",
scanResult.getRepositoryName(),
scanResult.getImageTag(),
scanResult.getRiskScore(),
gateResult.getConfig().getMaxRiskScore(),
scanResult.getTotalFindings()
));
for (String failure : gateResult.getFailures()) {
message.append("• ").append(failure).append("\n");
}
return message.toString();
}
private String buildCriticalAlertMessage(ImageScanResult scanResult, 
List<Vulnerability> criticalVulns) {
StringBuilder message = new StringBuilder();
message.append(String.format(
"CRITICAL VULNERABILITIES DETECTED in %s/%s\n\n" +
"Critical Vulnerabilities Found: %d\n\n",
scanResult.getRepositoryName(),
scanResult.getImageTag(),
criticalVulns.size()
));
for (int i = 0; i < Math.min(criticalVulns.size(), 5); i++) {
Vulnerability vuln = criticalVulns.get(i);
message.append(String.format("%d. %s\n", i + 1, vuln.getName()));
if (vuln.getDescription() != null) {
message.append("   Description: ").append(
vuln.getDescription().length() > 100 ? 
vuln.getDescription().substring(0, 100) + "..." : 
vuln.getDescription()
).append("\n");
}
message.append("\n");
}
if (criticalVulns.size() > 5) {
message.append(String.format("... and %d more critical vulnerabilities\n", 
criticalVulns.size() - 5));
}
return message.toString();
}
}
@Data
@Builder
class SecurityNotification {
private String type;
private String title;
private String message;
private String repositoryName;
private String imageTag;
private double riskScore;
private int totalFindings;
private List<String> failures;
private List<Vulnerability> criticalVulnerabilities;
private Instant timestamp;
}

6. GitHub Actions Integration

ECR Scan Workflow

name: "ECR Image Security Scan"
on:
push:
branches: [ main, develop ]
tags: [ 'v*' ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: my-java-app
IMAGE_TAG: ${{ github.sha }}
permissions:
id-token: write
contents: read
jobs:
build-scan-deploy:
name: Build, Scan, and Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build with Maven
run: mvn clean package -DskipTests
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build Docker image
run: |
docker build \
--tag ${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} \
--file Dockerfile \
.
- name: Push image to ECR
run: |
docker push ${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
- name: Wait for ECR scan completion
run: |
# Wait for ECR scan to complete
echo "Waiting for ECR scan to complete..."
sleep 60
# Check scan status using AWS CLI
SCAN_STATUS=$(aws ecr describe-image-scan-findings \
--repository-name ${{ env.ECR_REPOSITORY }} \
--image-id imageTag=${{ env.IMAGE_TAG }} \
--query 'imageScanFindings.imageScanStatus.status' \
--output text)
echo "ECR Scan Status: $SCAN_STATUS"
# Wait up to 5 minutes for scan completion
COUNTER=0
while [ "$SCAN_STATUS" != "COMPLETE" ] && [ $COUNTER -lt 30 ]; do
sleep 10
SCAN_STATUS=$(aws ecr describe-image-scan-findings \
--repository-name ${{ env.ECR_REPOSITORY }} \
--image-id imageTag=${{ env.IMAGE_TAG }} \
--query 'imageScanFindings.imageScanStatus.status' \
--output text)
echo "ECR Scan Status: $SCAN_STATUS (Attempt $((COUNTER + 1)))"
COUNTER=$((COUNTER + 1))
done
if [ "$SCAN_STATUS" != "COMPLETE" ]; then
echo "ECR scan did not complete in time"
exit 1
fi
- name: Check ECR scan results
run: |
# Get scan findings
aws ecr describe-image-scan-findings \
--repository-name ${{ env.ECR_REPOSITORY }} \
--image-id imageTag=${{ env.IMAGE_TAG }} \
--query 'imageScanFindings.findings[?severity==`CRITICAL`]' \
--output table
# Check for critical vulnerabilities
CRITICAL_COUNT=$(aws ecr describe-image-scan-findings \
--repository-name ${{ env.ECR_REPOSITORY }} \
--image-id imageTag=${{ env.IMAGE_TAG }} \
--query 'length(imageScanFindings.findings[?severity==`CRITICAL`])' \
--output text)
echo "Critical vulnerabilities found: $CRITICAL_COUNT"
if [ "$CRITICAL_COUNT" -gt 0 ]; then
echo "❌ Critical vulnerabilities detected. Failing build."
exit 1
fi
# Check for high vulnerabilities
HIGH_COUNT=$(aws ecr describe-image-scan-findings \
--repository-name ${{ env.ECR_REPOSITORY }} \
--image-id imageTag=${{ env.IMAGE_TAG }} \
--query 'length(imageScanFindings.findings[?severity==`HIGH`])' \
--output text)
echo "High vulnerabilities found: $HIGH_COUNT"
if [ "$HIGH_COUNT" -gt 5 ]; then
echo "❌ Too many high vulnerabilities detected. Failing build."
exit 1
fi
- name: Trigger deployment on success
if: success()
run: |
echo "ECR scan passed. Triggering deployment..."
# Add deployment logic here
- name: Notify on failure
if: failure()
run: |
echo "ECR scan failed. Sending notifications..."
# Add failure notification logic here

7. Secure Dockerfile for Java Applications

Security-Hardened Dockerfile

# Multi-stage build for security
FROM eclipse-temurin:17-jdk-jammy as builder
# Security: Install security updates
RUN apt-get update && \
apt-get upgrade -y && \
rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -r spring && useradd -r -g spring spring
USER spring:spring
WORKDIR /app
# Copy dependency files
COPY --chown=spring:spring mvnw .
COPY --chown=spring:spring .mvn .mvn
COPY --chown=spring:spring pom.xml .
# Download dependencies
RUN ./mvnw dependency:go-offline -B
# Copy source code
COPY --chown=spring:spring src src
# Build application
RUN ./mvnw clean package -DskipTests
# Runtime stage
FROM eclipse-temurin:17-jre-jammy as runtime
# Security: Install security updates and minimal packages
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
curl \
ca-certificates && \
rm -rf /var/lib/apt/lists/* && \
apt-get clean
# Security: Create non-root user
RUN groupadd -r spring && useradd -r -g spring spring
# Security: Create app directory with proper permissions
RUN mkdir -p /app && chown spring:spring /app
WORKDIR /app
USER spring:spring
# Copy JAR from builder stage
COPY --from=builder --chown=spring:spring /app/target/*.jar app.jar
# Security: Use secure JVM options
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom -Djava.awt.headless=true -Dfile.encoding=UTF-8"
# Security: Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# Security: Expose non-privileged port
EXPOSE 8080
# Security: Use exec form
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

8. Application Configuration

application.yml

# Application Configuration
spring:
application:
name: ecr-scan-service
security:
scan:
enabled: true
timeout-minutes: 10
max-critical: 0
max-high: 5
max-risk-score: 15.0
# AWS Configuration
aws:
region: ${AWS_REGION:us-east-1}
account-id: ${AWS_ACCOUNT_ID:}
ecr:
repository-name: ${ECR_REPOSITORY:my-java-app}
scan-on-push: true
scan-frequency: SCAN_ON_PUSH
sns:
security-topic-arn: ${SNS_SECURITY_TOPIC_ARN:}
# Security Configuration
security:
notifications:
enabled: true
slack-webhook: ${SLACK_SECURITY_WEBHOOK:}
email: ${SECURITY_TEAM_EMAIL:}
gates:
block-on-critical: true
require-approval-for-high: true
auto-remediate-low: false
# Logging
logging:
level:
com.example.ecr: DEBUG
software.amazon.awssdk: INFO
pattern:
level: "%5p [%X{traceId:-},%X{spanId:-}]"

9. Security Dashboard and Reporting

Security Dashboard Service

@Service
@Slf4j
public class SecurityDashboardService {
private final EcrScanService ecrScanService;
private final ObjectMapper objectMapper;
public SecurityDashboardService(EcrScanService ecrScanService, ObjectMapper objectMapper) {
this.ecrScanService = ecrScanService;
this.objectMapper = objectMapper;
}
/**
* Generate security dashboard data
*/
public SecurityDashboard generateDashboard(String repositoryName, Duration timeRange) {
SecurityDashboard dashboard = new SecurityDashboard();
dashboard.setGeneratedAt(Instant.now());
dashboard.setTimeRange(timeRange);
// Get recent scan results
List<ImageScanResult> recentScans = getRecentScanResults(repositoryName, timeRange);
dashboard.setRecentScans(recentScans);
// Calculate metrics
calculateMetrics(dashboard, recentScans);
// Generate trends
generateTrends(dashboard, recentScans);
// Identify top vulnerabilities
identifyTopVulnerabilities(dashboard, recentScans);
return dashboard;
}
/**
* Generate compliance report
*/
public ComplianceReport generateComplianceReport(String repositoryName, 
ComplianceStandard standard) {
ComplianceReport report = new ComplianceReport();
report.setGeneratedAt(Instant.now());
report.setStandard(standard);
report.setRepositoryName(repositoryName);
// Get current scan results
ImageScanResult currentScan = ecrScanService.waitForScanCompletion(
repositoryName, "latest", Duration.ofMinutes(5));
// Check compliance against standard
checkCompliance(report, currentScan, standard);
return report;
}
private List<ImageScanResult> getRecentScanResults(String repositoryName, Duration timeRange) {
// This would typically query a database of previous scan results
// For now, return empty list - implement based on your storage
return List.of();
}
private void calculateMetrics(SecurityDashboard dashboard, List<ImageScanResult> scans) {
SecurityMetrics metrics = new SecurityMetrics();
if (!scans.isEmpty()) {
ImageScanResult latestScan = scans.get(0);
metrics.setTotalImagesScanned(scans.size());
metrics.setCurrentRiskScore(latestScan.getRiskScore());
metrics.setCriticalVulnerabilitiesCount(
latestScan.getVulnerabilitiesBySeverity()
.getOrDefault("CRITICAL", List.of()).size());
metrics.setHighVulnerabilitiesCount(
latestScan.getVulnerabilitiesBySeverity()
.getOrDefault("HIGH", List.of()).size());
// Calculate averages
double avgRiskScore = scans.stream()
.mapToDouble(ImageScanResult::getRiskScore)
.average()
.orElse(0.0);
metrics.setAverageRiskScore(avgRiskScore);
}
dashboard.setMetrics(metrics);
}
private void generateTrends(SecurityDashboard dashboard, List<ImageScanResult> scans) {
List<SecurityTrend> trends = new ArrayList<>();
// Analyze risk score trend
if (scans.size() >= 2) {
ImageScanResult latest = scans.get(0);
ImageScanResult previous = scans.get(1);
double riskChange = latest.getRiskScore() - previous.getRiskScore();
trends.add(SecurityTrend.builder()
.type("RISK_SCORE")
.direction(riskChange > 0 ? "INCREASING" : "DECREASING")
.magnitude(Math.abs(riskChange))
.build());
}
dashboard.setTrends(trends);
}
private void identifyTopVulnerabilities(SecurityDashboard dashboard, 
List<ImageScanResult> scans) {
if (!scans.isEmpty()) {
ImageScanResult latestScan = scans.get(0);
Map<String, List<Vulnerability>> vulns = latestScan.getVulnerabilitiesBySeverity();
// Get top critical/high vulnerabilities
List<Vulnerability> topVulns = Stream.concat(
vulns.getOrDefault("CRITICAL", List.of()).stream(),
vulns.getOrDefault("HIGH", List.of()).stream()
)
.sorted(Comparator.comparing(Vulnerability::getCvssScore).reversed())
.limit(10)
.collect(Collectors.toList());
dashboard.setTopVulnerabilities(topVulns);
}
}
private void checkCompliance(ComplianceReport report, ImageScanResult scan, 
ComplianceStandard standard) {
List<ComplianceCheck> checks = new ArrayList<>();
// CIS Benchmark checks
if (standard == ComplianceStandard.CIS) {
checks.add(checkNonRootUser(scan));
checks.add(checkNoCriticalVulnerabilities(scan));
checks.add(checkSecurityUpdates(scan));
}
// HIPAA checks
if (standard == ComplianceStandard.HIPAA) {
checks.add(checkEncryptionRequirements(scan));
checks.add(checkAccessControls(scan));
}
report.setChecks(checks);
report.setCompliant(checks.stream().allMatch(ComplianceCheck::isPassed));
}
private ComplianceCheck checkNonRootUser(ImageScanResult scan) {
// Check if image runs as non-root user
// This would require additional metadata about the image
return ComplianceCheck.builder()
.name("Non-root User")
.description("Container should not run as root user")
.passed(true) // Simplified
.build();
}
private ComplianceCheck checkNoCriticalVulnerabilities(ImageScanResult scan) {
int criticalCount = scan.getVulnerabilitiesBySeverity()
.getOrDefault("CRITICAL", List.of()).size();
return ComplianceCheck.builder()
.name("No Critical Vulnerabilities")
.description("Image should have no critical severity vulnerabilities")
.passed(criticalCount == 0)
.details("Found " + criticalCount + " critical vulnerabilities")
.build();
}
// Additional compliance check methods...
}
@Data
class SecurityDashboard {
private Instant generatedAt;
private Duration timeRange;
private SecurityMetrics metrics;
private List<SecurityTrend> trends;
private List<ImageScanResult> recentScans;
private List<Vulnerability> topVulnerabilities;
}
@Data
class SecurityMetrics {
private int totalImagesScanned;
private double currentRiskScore;
private double averageRiskScore;
private int criticalVulnerabilitiesCount;
private int highVulnerabilitiesCount;
private int complianceViolations;
}
@Data
@Builder
class SecurityTrend {
private String type;
private String direction;
private double magnitude;
private String description;
}
@Data
class ComplianceReport {
private Instant generatedAt;
private ComplianceStandard standard;
private String repositoryName;
private boolean compliant;
private List<ComplianceCheck> checks;
}
@Data
@Builder
class ComplianceCheck {
private String name;
private String description;
private boolean passed;
private String details;
}
enum ComplianceStandard {
CIS, HIPAA, PCI_DSS, NIST
}

Best Practices

1. Secure Image Building

# Always use specific base image versions
FROM eclipse-temurin:17.0.7_7-jre-jammy
# Regularly update base images
# Use multi-stage builds to minimize attack surface
# Run as non-root user
# Remove unnecessary packages

2. Scanning Automation

// Integrate scanning into CI/CD pipeline
// Fail builds on critical vulnerabilities
// Implement security gates with configurable thresholds
// Automate remediation for low-risk issues

3. Continuous Monitoring

# Monitor for new CVEs
# Track vulnerability trends
# Implement automated alerts
# Maintain compliance dashboards

Conclusion

This comprehensive ECR Image Scanning implementation provides:

  • Automated vulnerability scanning integrated with Java applications
  • Security gate enforcement with configurable thresholds
  • Event-driven notifications for security findings
  • Compliance reporting for various standards
  • Dashboard and monitoring for security metrics

Key benefits:

  • Early vulnerability detection in container images
  • Automated security enforcement in CI/CD pipelines
  • Comprehensive reporting for audit and compliance
  • Real-time alerts for critical security issues
  • Risk-based decision making with calculated risk scores

The setup enables organizations to maintain secure containerized Java applications while meeting compliance requirements through automated security controls and continuous monitoring.

Leave a Reply

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


Macro Nepal Helper