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