From 7c45cb45ae3da46211fc5161c572d2e323d9f644 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Fri, 18 Aug 2023 09:17:02 +0200 Subject: [PATCH] feat(ecr_repositories_scan_vulnerabilities_in_latest_image): Minimum severity is configurable (#2736) --- prowler/config/config.yaml | 7 + ...es_scan_vulnerabilities_in_latest_image.py | 25 +- ...an_vulnerabilities_in_latest_image_test.py | 259 +++++++++++++++++- 3 files changed, 279 insertions(+), 12 deletions(-) diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 246731004a..87b88b754d 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -54,6 +54,13 @@ aws: organizations_enabled_regions: [] organizations_trusted_delegated_administrators: [] + # AWS ECR + # ecr_repositories_scan_vulnerabilities_in_latest_image + # CRITICAL + # HIGH + # MEDIUM + ecr_repository_vulnerability_minimum_severity: "MEDIUM" + # Azure Configuration azure: diff --git a/prowler/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image.py b/prowler/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image.py index 0e07adba90..cba9f4ae3a 100644 --- a/prowler/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image.py +++ b/prowler/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image.py @@ -5,6 +5,12 @@ from prowler.providers.aws.services.ecr.ecr_client import ecr_client class ecr_repositories_scan_vulnerabilities_in_latest_image(Check): def execute(self): findings = [] + + # Get minimun severity to report + minimum_severity = ecr_client.audit_config.get( + "ecr_repository_vulnerability_minimum_severity", "MEDIUM" + ) + for registry in ecr_client.registries.values(): for repository in registry.repositories: # First check if the repository has images @@ -27,8 +33,23 @@ class ecr_repositories_scan_vulnerabilities_in_latest_image(Check): report.status_extended = ( f"ECR repository {repository.name} with scan status FAILED." ) - elif image.scan_findings_status != "FAILED": - if image.scan_findings_severity_count and ( + elif ( + image.scan_findings_status != "FAILED" + and image.scan_findings_severity_count + ): + if ( + minimum_severity == "CRITICAL" + and image.scan_findings_severity_count.critical + ): + report.status = "FAIL" + report.status_extended = f"ECR repository {repository.name} has imageTag {image.latest_tag} scanned with findings: CRITICAL->{image.scan_findings_severity_count.critical}." + elif minimum_severity == "HIGH" and ( + image.scan_findings_severity_count.critical + or image.scan_findings_severity_count.high + ): + report.status = "FAIL" + report.status_extended = f"ECR repository {repository.name} has imageTag {image.latest_tag} scanned with findings: CRITICAL->{image.scan_findings_severity_count.critical}, HIGH->{image.scan_findings_severity_count.high}." + elif minimum_severity == "MEDIUM" and ( image.scan_findings_severity_count.critical or image.scan_findings_severity_count.high or image.scan_findings_severity_count.medium diff --git a/tests/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image_test.py b/tests/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image_test.py index ecda04827c..a089ab9f28 100644 --- a/tests/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image_test.py +++ b/tests/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image_test.py @@ -1,5 +1,4 @@ from datetime import datetime -from re import search from unittest import mock from prowler.providers.aws.services.ecr.ecr_service import ( @@ -16,6 +15,7 @@ repository_name = "test_repo" repository_arn = ( f"arn:aws:ecr:eu-west-1:{AWS_ACCOUNT_NUMBER}:repository/{repository_name}" ) +latest_tag = "test-tag" repo_policy_public = { "Version": "2012-10-17", "Statement": [ @@ -118,7 +118,7 @@ class Test_ecr_repositories_scan_vulnerabilities_in_latest_image: policy=repo_policy_public, images_details=[ ImageDetails( - latest_tag="test-tag", + latest_tag=latest_tag, latest_digest="test-digest", image_pushed_at=datetime(2023, 1, 1), scan_findings_status="COMPLETE", @@ -145,11 +145,14 @@ class Test_ecr_repositories_scan_vulnerabilities_in_latest_image: result = check.execute() assert len(result) == 1 assert result[0].status == "PASS" - assert search("scanned without findings", result[0].status_extended) + assert ( + result[0].status_extended + == f"ECR repository {repository_name} has imageTag {latest_tag} scanned without findings." + ) assert result[0].resource_id == repository_name assert result[0].resource_arn == repository_arn - def test_image_scanned_with_findings(self): + def test_image_scanned_with_findings_default_severity_MEDIUM(self): ecr_client = mock.MagicMock ecr_client.registries = {} ecr_client.registries[AWS_REGION] = Registry( @@ -165,7 +168,7 @@ class Test_ecr_repositories_scan_vulnerabilities_in_latest_image: policy=repo_policy_public, images_details=[ ImageDetails( - latest_tag="test-tag", + latest_tag=latest_tag, latest_digest="test-digest", image_pushed_at=datetime(2023, 1, 1), scan_findings_status="COMPLETE", @@ -180,6 +183,11 @@ class Test_ecr_repositories_scan_vulnerabilities_in_latest_image: rules=[], ) + # Set audit_config + ecr_client.audit_config = { + "ecr_repository_vulnerability_minimum_severity": "MEDIUM" + } + with mock.patch( "prowler.providers.aws.services.ecr.ecr_service.ECR", ecr_client, @@ -192,7 +200,232 @@ class Test_ecr_repositories_scan_vulnerabilities_in_latest_image: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert search("scanned with findings:", result[0].status_extended) + assert ( + result[0].status_extended + == f"ECR repository {repository_name} has imageTag {latest_tag} scanned with findings: CRITICAL->{12}, HIGH->{34}, MEDIUM->{7}." + ) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + + def test_image_scanned_with_findings_default_severity_HIGH(self): + ecr_client = mock.MagicMock + ecr_client.registries = {} + ecr_client.registries[AWS_REGION] = Registry( + id=AWS_ACCOUNT_NUMBER, + region=AWS_REGION, + scan_type="BASIC", + repositories=[ + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_public, + images_details=[ + ImageDetails( + latest_tag=latest_tag, + latest_digest="test-digest", + image_pushed_at=datetime(2023, 1, 1), + scan_findings_status="COMPLETE", + scan_findings_severity_count=FindingSeverityCounts( + critical=12, high=34, medium=7 + ), + ) + ], + lifecycle_policy=None, + ) + ], + rules=[], + ) + + # Set audit_config + ecr_client.audit_config = { + "ecr_repository_vulnerability_minimum_severity": "HIGH" + } + + with mock.patch( + "prowler.providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from prowler.providers.aws.services.ecr.ecr_repositories_scan_vulnerabilities_in_latest_image.ecr_repositories_scan_vulnerabilities_in_latest_image import ( + ecr_repositories_scan_vulnerabilities_in_latest_image, + ) + + check = ecr_repositories_scan_vulnerabilities_in_latest_image() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECR repository {repository_name} has imageTag {latest_tag} scanned with findings: CRITICAL->{12}, HIGH->{34}." + ) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + + def test_image_scanned_with_findings_default_severity_CRITICAL(self): + ecr_client = mock.MagicMock + ecr_client.registries = {} + ecr_client.registries[AWS_REGION] = Registry( + id=AWS_ACCOUNT_NUMBER, + region=AWS_REGION, + scan_type="BASIC", + repositories=[ + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_public, + images_details=[ + ImageDetails( + latest_tag=latest_tag, + latest_digest="test-digest", + image_pushed_at=datetime(2023, 1, 1), + scan_findings_status="COMPLETE", + scan_findings_severity_count=FindingSeverityCounts( + critical=12, high=34, medium=7 + ), + ) + ], + lifecycle_policy=None, + ) + ], + rules=[], + ) + + # Set audit_config + ecr_client.audit_config = { + "ecr_repository_vulnerability_minimum_severity": "CRITICAL" + } + + with mock.patch( + "prowler.providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from prowler.providers.aws.services.ecr.ecr_repositories_scan_vulnerabilities_in_latest_image.ecr_repositories_scan_vulnerabilities_in_latest_image import ( + ecr_repositories_scan_vulnerabilities_in_latest_image, + ) + + check = ecr_repositories_scan_vulnerabilities_in_latest_image() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECR repository {repository_name} has imageTag {latest_tag} scanned with findings: CRITICAL->{12}." + ) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + + def test_image_scanned_without_CRITICAL_findings_default_severity_CRITICAL(self): + ecr_client = mock.MagicMock + ecr_client.registries = {} + ecr_client.registries[AWS_REGION] = Registry( + id=AWS_ACCOUNT_NUMBER, + region=AWS_REGION, + scan_type="BASIC", + repositories=[ + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_public, + images_details=[ + ImageDetails( + latest_tag=latest_tag, + latest_digest="test-digest", + image_pushed_at=datetime(2023, 1, 1), + scan_findings_status="COMPLETE", + scan_findings_severity_count=FindingSeverityCounts( + critical=0, high=34, medium=7 + ), + ) + ], + lifecycle_policy=None, + ) + ], + rules=[], + ) + + # Set audit_config + ecr_client.audit_config = { + "ecr_repository_vulnerability_minimum_severity": "CRITICAL" + } + + with mock.patch( + "prowler.providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from prowler.providers.aws.services.ecr.ecr_repositories_scan_vulnerabilities_in_latest_image.ecr_repositories_scan_vulnerabilities_in_latest_image import ( + ecr_repositories_scan_vulnerabilities_in_latest_image, + ) + + check = ecr_repositories_scan_vulnerabilities_in_latest_image() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECR repository {repository_name} has imageTag {latest_tag} scanned without findings." + ) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + + def test_image_scanned_without_CRITICAL_and_HIGH_findings_default_severity_HIGH( + self, + ): + ecr_client = mock.MagicMock + ecr_client.registries = {} + ecr_client.registries[AWS_REGION] = Registry( + id=AWS_ACCOUNT_NUMBER, + region=AWS_REGION, + scan_type="BASIC", + repositories=[ + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_public, + images_details=[ + ImageDetails( + latest_tag=latest_tag, + latest_digest="test-digest", + image_pushed_at=datetime(2023, 1, 1), + scan_findings_status="COMPLETE", + scan_findings_severity_count=FindingSeverityCounts( + critical=0, high=0, medium=7 + ), + ) + ], + lifecycle_policy=None, + ) + ], + rules=[], + ) + + # Set audit_config + ecr_client.audit_config = { + "ecr_repository_vulnerability_minimum_severity": "HIGH" + } + + with mock.patch( + "prowler.providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from prowler.providers.aws.services.ecr.ecr_repositories_scan_vulnerabilities_in_latest_image.ecr_repositories_scan_vulnerabilities_in_latest_image import ( + ecr_repositories_scan_vulnerabilities_in_latest_image, + ) + + check = ecr_repositories_scan_vulnerabilities_in_latest_image() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECR repository {repository_name} has imageTag {latest_tag} scanned without findings." + ) assert result[0].resource_id == repository_name assert result[0].resource_arn == repository_arn @@ -212,7 +445,7 @@ class Test_ecr_repositories_scan_vulnerabilities_in_latest_image: policy=repo_policy_public, images_details=[ ImageDetails( - latest_tag="test-tag", + latest_tag=latest_tag, latest_digest="test-digest", image_pushed_at=datetime(2023, 1, 1), scan_findings_status="FAILED", @@ -239,7 +472,10 @@ class Test_ecr_repositories_scan_vulnerabilities_in_latest_image: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert search("with scan status FAILED", result[0].status_extended) + assert ( + result[0].status_extended + == f"ECR repository {repository_name} with scan status FAILED." + ) assert result[0].resource_id == repository_name assert result[0].resource_arn == repository_arn @@ -259,7 +495,7 @@ class Test_ecr_repositories_scan_vulnerabilities_in_latest_image: policy=repo_policy_public, images_details=[ ImageDetails( - latest_tag="test-tag", + latest_tag=latest_tag, latest_digest="test-digest", image_pushed_at=datetime(2023, 1, 1), scan_findings_status="", @@ -286,6 +522,9 @@ class Test_ecr_repositories_scan_vulnerabilities_in_latest_image: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert search("without a scan", result[0].status_extended) + assert ( + result[0].status_extended + == f"ECR repository {repository_name} has imageTag {latest_tag} without a scan." + ) assert result[0].resource_id == repository_name assert result[0].resource_arn == repository_arn