diff --git a/providers/aws/services/ecr/__init__.py b/providers/aws/services/ecr/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/ecr/check_extra765 b/providers/aws/services/ecr/check_extra765 deleted file mode 100644 index ad7a409b..00000000 --- a/providers/aws/services/ecr/check_extra765 +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash - -# Prowler - the handy cloud security tool (copyright 2018) by Toni de la Fuente -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -# Remediation: -# -# https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html -# -# aws ecr put-image-scanning-configuration \ -# --region \ -# --repository-name \ -# --image-scanning-configuration scanOnPush=true - -CHECK_ID_extra765="7.65" -CHECK_TITLE_extra765="[extra765] Check if ECR image scan on push is enabled " -CHECK_SCORED_extra765="NOT_SCORED" -CHECK_CIS_LEVEL_extra765="EXTRA" -CHECK_SEVERITY_extra765="Medium" -CHECK_ALTERNATE_check765="extra765" -CHECK_SERVICENAME_extra765="ecr" -CHECK_RISK_extra765='Amazon ECR image scanning helps in identifying software vulnerabilities in your container images. Amazon ECR uses the Common Vulnerabilities and Exposures (CVEs) database from the open-source Clair project and provides a list of scan findings. ' -CHECK_REMEDIATION_extra765='Enable ECR image scanning and review the scan findings for information about the security of the container images that are being deployed.' -CHECK_DOC_extra765='https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html' -CHECK_CAF_EPIC_extra765='Infrastructure Security' - -extra765(){ - for region in $REGIONS; do - LIST_ECR_REPOS=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $region --query "repositories[*].[repositoryName]" --output text 2>&1) - if [[ $(echo "$LIST_ECR_REPOS" | grep AccessDenied) ]]; then - textInfo "$region: Access Denied Trying to describe ECR repositories" "$region" - continue - fi - if [[ ! -z "$LIST_ECR_REPOS" ]]; then - for repo in $LIST_ECR_REPOS; do - SCAN_ENABLED=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $region --query "repositories[?repositoryName==\`$repo\`].[imageScanningConfiguration.scanOnPush]" --output text 2>&1) - case "$SCAN_ENABLED" in - "True") - textPass "$region: ECR repository $repo has scan on push enabled" "$region" "$repo" - ;; - "False") - textFail "$region: ECR repository $repo has scan on push disabled!" "$region" "$repo" - ;; - "None") - textInfo "$region: ECR repository $repo has no scanOnPush status: newer awscli needed" "$region" "$repo" - ;; - "*") - textInfo "$region: ECR repository $repo has unknown scanOnPush status \"$SCAN_ENABLED\"" "$region" "$repo" - ;; - esac - done - else - textInfo "$region: No ECR repositories found" "$region" - fi - done -} diff --git a/providers/aws/services/ecr/check_extra77 b/providers/aws/services/ecr/check_extra77 deleted file mode 100644 index 4971d243..00000000 --- a/providers/aws/services/ecr/check_extra77 +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash - -# Prowler - the handy cloud security tool (copyright 2018) by Toni de la Fuente -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -CHECK_ID_extra77="7.7" -CHECK_TITLE_extra77="[extra77] Ensure there are no ECR repositories set as Public" -CHECK_SCORED_extra77="NOT_SCORED" -CHECK_CIS_LEVEL_extra77="EXTRA" -CHECK_SEVERITY_extra77="Critical" -CHECK_ALTERNATE_extra707="extra77" -CHECK_ALTERNATE_check77="extra77" -CHECK_ALTERNATE_check707="extra77" -CHECK_SERVICENAME_extra77="ecr" -CHECK_RISK_extra77='Policy may allow Anonymous users to perform actions.' -CHECK_REMEDIATION_extra77='Ensure this repository and its contents should be publicly accessible.' -CHECK_DOC_extra77='https://docs.aws.amazon.com/AmazonECR/latest/public/security_iam_service-with-iam.html' -CHECK_CAF_EPIC_extra77='Data Protection' - -extra77(){ - # "Ensure there are no ECR repositories set as Public " - for regx in $REGIONS; do - LIST_ECR_REPOS=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region $regx --query "repositories[*].[repositoryName]" --output text 2>&1) - if [[ $(echo "$LIST_ECR_REPOS" | grep AccessDenied) ]]; then - textInfo "$regx: Access Denied Trying to describe ECR repositories" "$regx" "$repo" - continue - fi - if [[ $(echo "$LIST_ECR_REPOS" | grep SubscriptionRequiredException) ]]; then - textInfo "$regx: Subscription Required Exception trying to describe ECR repositories" "$regx" "$repo" - continue - fi - if [[ ! -z "$LIST_ECR_REPOS" ]]; then - for repo in $LIST_ECR_REPOS; do - TEMP_POLICY_FILE=$(mktemp -t prowler-${ACCOUNT_NUM}-ecr-repo.policy.XXXXXXXXXX) - $AWSCLI ecr get-repository-policy $PROFILE_OPT --region $regx --repository-name $repo --query "policyText" --output text > $TEMP_POLICY_FILE 2>&1 - if [[ $(grep AccessDenied $TEMP_POLICY_FILE) ]]; then - textInfo "$regx: Access Denied to get repository policy for repo $repo" "$regx" "$repo" - rm -f $TEMP_POLICY_FILE - continue - fi - # https://docs.aws.amazon.com/AmazonECR/latest/userguide/repository-policies.html - "By default, only the repository owner has access to a repository." - if [[ $(grep RepositoryPolicyNotFoundException $TEMP_POLICY_FILE) ]]; then - textPass "$regx: $repo is not open" "$regx" "$repo" - rm -f $TEMP_POLICY_FILE - continue - fi - # check if the policy has Principal as * - CHECK_ECR_REPO_ALLUSERS_POLICY=$(cat $TEMP_POLICY_FILE | jq '.Statement[]|select(.Effect=="Allow" and (((.Principal|type == "object") and .Principal.AWS == "*") or ((.Principal|type == "string") and .Principal == "*")))') - if [[ $CHECK_ECR_REPO_ALLUSERS_POLICY ]]; then - textFail "$regx: $repo policy \"may\" allow Anonymous users to perform actions (Principal: \"*\")" "$regx" - else - textPass "$regx: $repo is not open" "$regx" "$repo" - fi - rm -f $TEMP_POLICY_FILE - done - else - textInfo "$regx: No ECR repositories found" "$regx" "$repo" - fi - done -} diff --git a/providers/aws/services/ecr/check_extra776 b/providers/aws/services/ecr/check_extra776 deleted file mode 100644 index e0acef8f..00000000 --- a/providers/aws/services/ecr/check_extra776 +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bash - -# Prowler - the handy cloud security tool (copyright 2018) by Toni de la Fuente -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. - -# Remediation: -# -# https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html -# -# aws ecr put-image-scanning-configuration \ -# --region \ -# --repository-name \ -# --image-scanning-configuration scanOnPush=true -# -# aws ecr describe-image-scan-findings \ -# --region \ -# --repository-name -# --image-id imageTag= - -CHECK_ID_extra776="7.76" -CHECK_TITLE_extra776="[extra776] Check if ECR image scan found vulnerabilities in the newest image version" -CHECK_SCORED_extra776="NOT_SCORED" -CHECK_CIS_LEVEL_extra776="EXTRA" -CHECK_SEVERITY_extra776="Medium" -CHECK_ALTERNATE_check776="extra776" -CHECK_SERVICENAME_extra776="ecr" -CHECK_ASFF_RESOURCE_TYPE_extra776="AwsEcrRepository" -CHECK_RISK_extra776='Amazon ECR image scanning helps in identifying software vulnerabilities in your container images. Amazon ECR uses the Common Vulnerabilities and Exposures (CVEs) database from the open-source Clair project and provides a list of scan findings. ' -CHECK_REMEDIATION_extra776='Open the Amazon ECR console. Then look for vulnerabilities and fix them.' -CHECK_DOC_extra776='https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html#describe-scan-findings' -CHECK_CAF_EPIC_extra776='Logging and Monitoring' - -extra776(){ - for region in ${REGIONS}; do - # List ECR repositories - LIST_ECR_REPOS=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region "${region}" --query "repositories[*].[repositoryName]" --output text 2>&1) - # Handle authorization errors - if grep -q -E 'AccessDenied|UnauthorizedOperation|AuthorizationError' <<< "$LIST_ECR_REPOS"; then - textInfo "$region: Access Denied trying to describe ECR repositories" "$region" - continue - fi - if [[ -n "$LIST_ECR_REPOS" ]]; then - for repo in $LIST_ECR_REPOS; do - # Check if the repository has scanOnPush enabled - SCAN_ENABLED=$($AWSCLI ecr describe-repositories $PROFILE_OPT --region "${region}" --query "repositories[?repositoryName=='${repo}'].[imageScanningConfiguration.scanOnPush]" --output text 2>&1) - if [[ "${SCAN_ENABLED}" == "True" ]]; then - # Recover newest image digest - NEWEST_IMAGE_DIGEST=$($AWSCLI ecr describe-images $PROFILE_OPT --region "${region}" --repository-name "${repo}" --query "sort_by(imageDetails,& imagePushedAt)[-1].imageDigest" --output text | head -n 1 2>&1) - if [[ "${NEWEST_IMAGE_DIGEST}" != *"None"* ]]; then - # Recover newest image tag - NEWEST_IMAGE_TAG=$($AWSCLI ecr describe-images $PROFILE_OPT --region "${region}" --repository-name "${repo}" --query "sort_by(imageDetails,& imagePushedAt)[-1].imageTags[0]" --output text | head -n 1 2>&1) - if [[ -n "${LIST_ECR_REPOS}" ]]; then - # For this newest digest, recover the last scan status - IMAGE_SCAN_STATUS=$($AWSCLI ecr describe-image-scan-findings $PROFILE_OPT --region "${region}" --repository-name "${repo}" --image-id imageDigest="${NEWEST_IMAGE_DIGEST}" --query "imageScanStatus.status" --output text 2>&1) - if [[ "${IMAGE_SCAN_STATUS}" == *"ScanNotFoundException"* ]]; then - textFail "${region}: ECR repository ${repo} has imageTag ${NEWEST_IMAGE_TAG} without a scan" "${region}" "${repo}" - else - if [[ "${IMAGE_SCAN_STATUS}" == *"FAILED"* ]]; then - textFail "${region}: ECR repository ${repo} has imageTag ${NEWEST_IMAGE_TAG} with scan status ${IMAGE_SCAN_STATUS}" "${region}" "${repo}" - else - # For this newest digest, recover the number of findings found - # This search needs a JSON response to match against severity - FINDINGS_COUNT=$($AWSCLI ecr describe-image-scan-findings $PROFILE_OPT --region "${region}" --repository-name "${repo}" --image-id imageDigest="${NEWEST_IMAGE_DIGEST}" --query "imageScanFindings.findingSeverityCounts" --output json 2>&1) - if [[ -n "${FINDINGS_COUNT}" ]]; then - SEVERITY_CRITICAL=$(jq -r '.CRITICAL' <<< "${FINDINGS_COUNT}") - if [[ "${SEVERITY_CRITICAL}" != "null" ]]; then - textFail "${region}: ECR repository ${repo} has imageTag ${NEWEST_IMAGE_TAG} with CRITICAL ($SEVERITY_CRITICAL) findings" "${region}" "${repo}" - fi - SEVERITY_HIGH=$(jq -r '.HIGH' <<< "${FINDINGS_COUNT}") - if [[ "${SEVERITY_HIGH}" != "null" ]]; then - textFail "${region}: ECR repository ${repo} has imageTag ${NEWEST_IMAGE_TAG} with HIGH ($SEVERITY_HIGH) findings" "${region}" "${repo}" - fi - SEVERITY_MEDIUM=$(jq -r '.MEDIUM' <<< "${FINDINGS_COUNT}") - if [[ "${SEVERITY_MEDIUM}" != "null" ]]; then - textFail "${region}: ECR repository ${repo} has imageTag ${NEWEST_IMAGE_TAG} with MEDIUM ($SEVERITY_MEDIUM) findings" "${region}" "${repo}" - fi - SEVERITY_LOW=$(jq -r '.LOW' <<< "${FINDINGS_COUNT}") - if [[ "${SEVERITY_LOW}" != "null" ]]; then - textInfo "${region}: ECR repository ${repo} has imageTag ${NEWEST_IMAGE_TAG} with LOW ($SEVERITY_LOW) findings" "${region}" "${repo}" - fi - SEVERITY_INFORMATIONAL=$(jq -r '.INFORMATIONAL' <<< "${FINDINGS_COUNT}") - if [[ "${SEVERITY_INFORMATIONAL}" != "null" ]]; then - textInfo "${region}: ECR repository ${repo} has imageTag ${NEWEST_IMAGE_TAG} with INFORMATIONAL ($SEVERITY_INFORMATIONAL) findings" "${region}" "${repo}" - fi - SEVERITY_UNDEFINED=$(jq -r '.UNDEFINED' <<< "${FINDINGS_COUNT}") - if [[ "${SEVERITY_UNDEFINED}" != "null" ]]; then - textInfo "${region}: ECR repository ${repo} has imageTag ${NEWEST_IMAGE_TAG} with UNDEFINED ($SEVERITY_UNDEFINED) findings" "${region}" "${repo}" - fi - else - textPass "${region}: ECR repository ${repo} has imageTag ${NEWEST_IMAGE_TAG} without findings" "${region}" "${repo}" - fi - fi - fi - fi - else - textInfo "${region}: ECR repository ${repo} has no images" "${region}" - fi - else - textInfo "${region}: ECR repository ${repo} has scanOnPush disabled" "${region}" "${repo}" - fi - done - else - textInfo "${region}: No ECR repositories found" "${region}" - fi - done -} diff --git a/providers/aws/services/ecr/ecr_client.py b/providers/aws/services/ecr/ecr_client.py new file mode 100644 index 00000000..a50598df --- /dev/null +++ b/providers/aws/services/ecr/ecr_client.py @@ -0,0 +1,4 @@ +from providers.aws.lib.audit_info.audit_info import current_audit_info +from providers.aws.services.ecr.ecr_service import ECR + +ecr_client = ECR(current_audit_info) diff --git a/providers/aws/services/ecr/ecr_repositories_lifecycle_policy_enabled/__init__.py b/providers/aws/services/ecr/ecr_repositories_lifecycle_policy_enabled/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/ecr/ecr_repositories_lifecycle_policy_enabled/ecr_repositories_lifecycle_policy_enabled.metadata.json b/providers/aws/services/ecr/ecr_repositories_lifecycle_policy_enabled/ecr_repositories_lifecycle_policy_enabled.metadata.json new file mode 100644 index 00000000..7d35f3f9 --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_lifecycle_policy_enabled/ecr_repositories_lifecycle_policy_enabled.metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "aws", + "CheckID": "ecr_repositories_lifecycle_policy_enabled", + "CheckTitle": "Check if ECR repositories have lifecycle policies enabled", + "CheckType": ["Identify", "Resource configuration"], + "ServiceName": "ecr", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "low", + "ResourceType": "AwsEcrRepository", + "Description": "Check if ECR repositories have lifecycle policies enabled", + "Risk": "Amazon ECR repositories run the risk of retaining huge volumes of images, increasing unnecessary cost.", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/ECR/lifecycle-policy-in-use.html", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Open the Amazon ECR console. Create an ECR lifecycle policy.", + "Url": "https://docs.aws.amazon.com/AmazonECR/latest/userguide/LifecyclePolicies.html" + } + }, + "Categories": [], + "Tags": { + "Tag1Key": "value", + "Tag2Key": "value" + }, + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "Compliance": [] + } diff --git a/providers/aws/services/ecr/ecr_repositories_lifecycle_policy_enabled/ecr_repositories_lifecycle_policy_enabled.py b/providers/aws/services/ecr/ecr_repositories_lifecycle_policy_enabled/ecr_repositories_lifecycle_policy_enabled.py new file mode 100644 index 00000000..7493f8d3 --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_lifecycle_policy_enabled/ecr_repositories_lifecycle_policy_enabled.py @@ -0,0 +1,25 @@ +from lib.check.models import Check, Check_Report +from providers.aws.services.ecr.ecr_client import ecr_client + + +class ecr_repositories_lifecycle_policy_enabled(Check): + def execute(self): + findings = [] + for repository in ecr_client.repositories: + report = Check_Report(self.metadata) + report.region = repository.region + report.resource_id = repository.name + report.resource_arn = repository.arn + report.status = "FAIL" + report.status_extended = ( + f"Repository {repository.name} has no lifecycle policy" + ) + if repository.lyfecicle_policy: + report.status = "PASS" + report.status_extended = ( + f"Repository {repository.name} has lifecycle policy" + ) + + findings.append(report) + + return findings diff --git a/providers/aws/services/ecr/ecr_repositories_lifecycle_policy_enabled/ecr_repositories_lifecycle_policy_enabled_test.py b/providers/aws/services/ecr/ecr_repositories_lifecycle_policy_enabled/ecr_repositories_lifecycle_policy_enabled_test.py new file mode 100644 index 00000000..4b046fd4 --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_lifecycle_policy_enabled/ecr_repositories_lifecycle_policy_enabled_test.py @@ -0,0 +1,87 @@ +from re import search +from unittest import mock + +from providers.aws.services.ecr.ecr_service import Repository + +# Mock Test Region +AWS_REGION = "eu-west-1" +AWS_ACCOUNT_NUMBER = "123456789012" +repository_name = "test_repo" +repository_arn = ( + f"arn:aws:ecr:eu-west-1:{AWS_ACCOUNT_NUMBER}:repository/{repository_name}" +) +repo_policy_public = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ECRRepositoryPolicy", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/username"}, + "Action": ["ecr:DescribeImages", "ecr:DescribeRepositories"], + } + ], +} + + +class Test_ecr_repositories_lifecycle_policy_enabled: + def test_no_lyfecicle_policy(self): + ecr_client = mock.MagicMock + ecr_client.repositories = [] + ecr_client.repositories.append( + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_public, + images_details=None, + lyfecicle_policy="test-policy", + ) + ) + + with mock.patch( + "providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from providers.aws.services.ecr.ecr_repositories_lifecycle_policy_enabled.ecr_repositories_lifecycle_policy_enabled import ( + ecr_repositories_lifecycle_policy_enabled, + ) + + check = ecr_repositories_lifecycle_policy_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert search("has lifecycle policy", result[0].status_extended) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + + def test_lifecycle_policy(self): + ecr_client = mock.MagicMock + ecr_client.repositories = [] + ecr_client.repositories.append( + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=False, + policy=repo_policy_public, + images_details=None, + lyfecicle_policy=None, + ) + ) + + with mock.patch( + "providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from providers.aws.services.ecr.ecr_repositories_lifecycle_policy_enabled.ecr_repositories_lifecycle_policy_enabled import ( + ecr_repositories_lifecycle_policy_enabled, + ) + + check = ecr_repositories_lifecycle_policy_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert search("has no lifecycle policy", result[0].status_extended) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn diff --git a/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/__init__.py b/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.metadata.json b/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.metadata.json new file mode 100644 index 00000000..30eb3f14 --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "aws", + "CheckID": "ecr_repositories_not_publicly_accessible", + "CheckTitle": "Ensure there are no ECR repositories set as Public", + "CheckType": ["Protect", "Secure Access Management"], + "ServiceName": "ecr", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "critical", + "ResourceType": "AwsEcrRepository", + "Description": "Ensure there are no ECR repositories set as Public", + "Risk": "A repository policy that allows anonymous access may allow anonymous users to perform actions.", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "https://docs.bridgecrew.io/docs/public_1-ecr-repositories-not-public#cloudformation", + "Other": "https://docs.bridgecrew.io/docs/public_1-ecr-repositories-not-public#aws-console", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure the repository and its contents are not publicly accessible", + "Url": "https://docs.aws.amazon.com/AmazonECR/latest/public/security_iam_service-with-iam.html" + } + }, + "Categories": [], + "Tags": { + "Tag1Key": "value", + "Tag2Key": "value" + }, + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "Compliance": [] + } diff --git a/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.py b/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.py new file mode 100644 index 00000000..fcf20d7c --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.py @@ -0,0 +1,28 @@ +from lib.check.models import Check, Check_Report +from providers.aws.services.ecr.ecr_client import ecr_client + + +class ecr_repositories_not_publicly_accessible(Check): + def execute(self): + findings = [] + for repository in ecr_client.repositories: + report = Check_Report(self.metadata) + report.region = repository.region + report.resource_id = repository.name + report.resource_arn = repository.arn + report.status = "PASS" + report.status_extended = f"Repository {repository.name} is not open" + if repository.policy: + for statement in repository.policy["Statement"]: + if statement["Effect"] == "Allow": + if "*" in statement["Principal"] or ( + "AWS" in statement["Principal"] + and "*" in statement["Principal"]["AWS"] + ): + report.status = "FAIL" + report.status_extended = f"Repository {repository.name} policy may allow anonymous users to perform actions (Principal: '*')" + break + + findings.append(report) + + return findings diff --git a/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_test.py b/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_test.py new file mode 100644 index 00000000..29b3b1a2 --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_test.py @@ -0,0 +1,101 @@ +from re import search +from unittest import mock + +from providers.aws.services.ecr.ecr_service import Repository + +# Mock Test Region +AWS_REGION = "eu-west-1" +AWS_ACCOUNT_NUMBER = "123456789012" +repository_name = "test_repo" +repository_arn = ( + f"arn:aws:ecr:eu-west-1:{AWS_ACCOUNT_NUMBER}:repository/{repository_name}" +) +repo_policy_not_public = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ECRRepositoryPolicy", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/username"}, + "Action": ["ecr:DescribeImages", "ecr:DescribeRepositories"], + } + ], +} + +repo_policy_public = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ECRRepositoryPolicy", + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": ["ecr:DescribeImages", "ecr:DescribeRepositories"], + } + ], +} + + +class Test_ecr_repositories_not_publicly_accessible: + def test_repository_not_public(self): + ecr_client = mock.MagicMock + ecr_client.repositories = [] + ecr_client.repositories.append( + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_not_public, + images_details=None, + lyfecicle_policy=None, + ) + ) + + with mock.patch( + "providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible import ( + ecr_repositories_not_publicly_accessible, + ) + + check = ecr_repositories_not_publicly_accessible() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert search("is not open", result[0].status_extended) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + + def test_repository_public(self): + ecr_client = mock.MagicMock + ecr_client.repositories = [] + ecr_client.repositories.append( + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_public, + images_details=None, + lyfecicle_policy=None, + ) + ) + + with mock.patch( + "providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible import ( + ecr_repositories_not_publicly_accessible, + ) + + check = ecr_repositories_not_publicly_accessible() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert search( + "policy may allow anonymous users to", result[0].status_extended + ) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn diff --git a/providers/aws/services/ecr/ecr_repositories_scan_images_on_push_enabled/__init__.py b/providers/aws/services/ecr/ecr_repositories_scan_images_on_push_enabled/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/ecr/ecr_repositories_scan_images_on_push_enabled/ecr_repositories_scan_images_on_push_enabled.metadata.json b/providers/aws/services/ecr/ecr_repositories_scan_images_on_push_enabled/ecr_repositories_scan_images_on_push_enabled.metadata.json new file mode 100644 index 00000000..9bf6cd5f --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_scan_images_on_push_enabled/ecr_repositories_scan_images_on_push_enabled.metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "aws", + "CheckID": "ecr_repositories_scan_images_on_push_enabled", + "CheckTitle": "Check if ECR image scan on push is enabled", + "CheckType": ["Identify", "Vulnerability, patch, and version management"], + "ServiceName": "ecr", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "medium", + "ResourceType": "AwsEcrRepository", + "Description": "Check if ECR image scan on push is enabled", + "Risk": "Amazon ECR image scanning helps in identifying software vulnerabilities in your container images. Amazon ECR uses the Common Vulnerabilities and Exposures (CVEs) database from the open-source Clair project and provides a list of scan findings. ", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "aws ecr create-repository --repository-name --image-scanning-configuration scanOnPush=true--region ", + "NativeIaC": "https://docs.bridgecrew.io/docs/general_8#cli-command", + "Other": "", + "Terraform": "https://docs.bridgecrew.io/docs/general_8#fix---buildtime" + }, + "Recommendation": { + "Text": "Enable ECR image scanning and review the scan findings for information about the security of the container images that are being deployed.", + "Url": "https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html" + } + }, + "Categories": [], + "Tags": { + "Tag1Key": "value", + "Tag2Key": "value" + }, + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "Compliance": [] + } diff --git a/providers/aws/services/ecr/ecr_repositories_scan_images_on_push_enabled/ecr_repositories_scan_images_on_push_enabled.py b/providers/aws/services/ecr/ecr_repositories_scan_images_on_push_enabled/ecr_repositories_scan_images_on_push_enabled.py new file mode 100644 index 00000000..5d28a9e8 --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_scan_images_on_push_enabled/ecr_repositories_scan_images_on_push_enabled.py @@ -0,0 +1,25 @@ +from lib.check.models import Check, Check_Report +from providers.aws.services.ecr.ecr_client import ecr_client + + +class ecr_repositories_scan_images_on_push_enabled(Check): + def execute(self): + findings = [] + for repository in ecr_client.repositories: + report = Check_Report(self.metadata) + report.region = repository.region + report.resource_id = repository.name + report.resource_arn = repository.arn + report.status = "PASS" + report.status_extended = ( + f"ECR repository {repository.name} has scan on push enabled" + ) + if not repository.scan_on_push: + report.status = "FAIL" + report.status_extended = ( + f"ECR repository {repository.name} has scan on push disabled" + ) + + findings.append(report) + + return findings diff --git a/providers/aws/services/ecr/ecr_repositories_scan_images_on_push_enabled/ecr_repositories_scan_images_on_push_enabled_test.py b/providers/aws/services/ecr/ecr_repositories_scan_images_on_push_enabled/ecr_repositories_scan_images_on_push_enabled_test.py new file mode 100644 index 00000000..743effac --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_scan_images_on_push_enabled/ecr_repositories_scan_images_on_push_enabled_test.py @@ -0,0 +1,87 @@ +from re import search +from unittest import mock + +from providers.aws.services.ecr.ecr_service import Repository + +# Mock Test Region +AWS_REGION = "eu-west-1" +AWS_ACCOUNT_NUMBER = "123456789012" +repository_name = "test_repo" +repository_arn = ( + f"arn:aws:ecr:eu-west-1:{AWS_ACCOUNT_NUMBER}:repository/{repository_name}" +) +repo_policy_public = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ECRRepositoryPolicy", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/username"}, + "Action": ["ecr:DescribeImages", "ecr:DescribeRepositories"], + } + ], +} + + +class Test_ecr_repositories_scan_images_on_push_enabled: + def test_scan_on_push_disabled(self): + ecr_client = mock.MagicMock + ecr_client.repositories = [] + ecr_client.repositories.append( + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_public, + images_details=None, + lyfecicle_policy=None, + ) + ) + + with mock.patch( + "providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from providers.aws.services.ecr.ecr_repositories_scan_images_on_push_enabled.ecr_repositories_scan_images_on_push_enabled import ( + ecr_repositories_scan_images_on_push_enabled, + ) + + check = ecr_repositories_scan_images_on_push_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert search("has scan on push enabled", result[0].status_extended) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + + def test_scan_on_push_enabled(self): + ecr_client = mock.MagicMock + ecr_client.repositories = [] + ecr_client.repositories.append( + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=False, + policy=repo_policy_public, + images_details=None, + lyfecicle_policy=None, + ) + ) + + with mock.patch( + "providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from providers.aws.services.ecr.ecr_repositories_scan_images_on_push_enabled.ecr_repositories_scan_images_on_push_enabled import ( + ecr_repositories_scan_images_on_push_enabled, + ) + + check = ecr_repositories_scan_images_on_push_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert search("has scan on push disabled", result[0].status_extended) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn diff --git a/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/__init__.py b/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image.metadata.json b/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image.metadata.json new file mode 100644 index 00000000..7ef75831 --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image.metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "aws", + "CheckID": "ecr_repositories_scan_vulnerabilities_in_latest_image", + "CheckTitle": "Check if ECR image scan found vulnerabilities in the newest image version", + "CheckType": ["Identify", "Vulnerability, patch, and version management"], + "ServiceName": "ecr", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "medium", + "ResourceType": "AwsEcrRepository", + "Description": "Check if ECR image scan found vulnerabilities in the newest image version", + "Risk": "Amazon ECR image scanning helps in identifying software vulnerabilities in your container images. Amazon ECR uses the Common Vulnerabilities and Exposures (CVEs) database from the open-source Clair project and provides a list of scan findings.", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Open the Amazon ECR console. Then look for vulnerabilities and fix them.", + "Url": "https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html#describe-scan-findings" + } + }, + "Categories": [], + "Tags": { + "Tag1Key": "value", + "Tag2Key": "value" + }, + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "Compliance": [] + } diff --git a/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image.py b/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image.py new file mode 100644 index 00000000..af0694fc --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image.py @@ -0,0 +1,35 @@ +from lib.check.models import Check, Check_Report +from providers.aws.services.ecr.ecr_client import ecr_client + + +class ecr_repositories_scan_vulnerabilities_in_latest_image(Check): + def execute(self): + findings = [] + for repository in ecr_client.repositories: + for image in repository.images_details: + report = Check_Report(self.metadata) + report.region = repository.region + report.resource_id = repository.name + report.resource_arn = repository.arn + report.status = "PASS" + report.status_extended = f"ECR repository {repository.name} has imageTag {image.latest_tag} scanned without findings" + if not image.scan_findings_status: + report.status = "FAIL" + report.status_extended = f"ECR repository {repository.name} has imageTag {image.latest_tag} without a scan" + elif image.scan_findings_status == "FAILED": + report.status = "FAIL" + 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 ( + image.scan_findings_severity_count.critical + or image.scan_findings_severity_count.high + or image.scan_findings_severity_count.medium + ): + 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}, MEDIUM->{image.scan_findings_severity_count.medium} " + + findings.append(report) + + return findings diff --git a/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image_test.py b/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image_test.py new file mode 100644 index 00000000..d72d91a6 --- /dev/null +++ b/providers/aws/services/ecr/ecr_repositories_scan_vulnerabilities_in_latest_image/ecr_repositories_scan_vulnerabilities_in_latest_image_test.py @@ -0,0 +1,189 @@ +from re import search +from unittest import mock + +from providers.aws.services.ecr.ecr_service import ( + FindingSeverityCounts, + ImageDetails, + Repository, +) + +# Mock Test Region +AWS_REGION = "eu-west-1" +AWS_ACCOUNT_NUMBER = "123456789012" +repository_name = "test_repo" +repository_arn = ( + f"arn:aws:ecr:eu-west-1:{AWS_ACCOUNT_NUMBER}:repository/{repository_name}" +) +repo_policy_public = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ECRRepositoryPolicy", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/username"}, + "Action": ["ecr:DescribeImages", "ecr:DescribeRepositories"], + } + ], +} + + +class Test_ecr_repositories_scan_vulnerabilities_in_latest_image: + def test_image_scaned_without_findings(self): + ecr_client = mock.MagicMock + ecr_client.repositories = [] + ecr_client.repositories.append( + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_public, + images_details=[], + lyfecicle_policy=None, + ) + ) + ecr_client.repositories[0].images_details.append( + ImageDetails( + latest_tag="test-tag", + latest_digest="test-digest", + scan_findings_status="COMPLETE", + scan_findings_severity_count=FindingSeverityCounts( + critical=0, high=0, medium=0 + ), + ), + ), + with mock.patch( + "providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from 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 search("scanned without findings", result[0].status_extended) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + + def test_image_scanned_with_findings(self): + ecr_client = mock.MagicMock + ecr_client.repositories = [] + ecr_client.repositories.append( + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_public, + images_details=[], + lyfecicle_policy=None, + ) + ) + ecr_client.repositories[0].images_details.append( + ImageDetails( + latest_tag="test-tag", + latest_digest="test-digest", + scan_findings_status="COMPLETE", + scan_findings_severity_count=FindingSeverityCounts( + critical=12, high=34, medium=7 + ), + ), + ), + with mock.patch( + "providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from 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 search("scanned with findings:", result[0].status_extended) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + + def test_image_scanned_fail_scan(self): + ecr_client = mock.MagicMock + ecr_client.repositories = [] + ecr_client.repositories.append( + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_public, + images_details=[], + lyfecicle_policy=None, + ) + ) + ecr_client.repositories[0].images_details.append( + ImageDetails( + latest_tag="test-tag", + latest_digest="test-digest", + scan_findings_status="FAILED", + scan_findings_severity_count=FindingSeverityCounts( + critical=0, high=0, medium=0 + ), + ), + ), + with mock.patch( + "providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from 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 search("with scan status FAILED", result[0].status_extended) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + + def test_image_not_scanned(self): + ecr_client = mock.MagicMock + ecr_client.repositories = [] + ecr_client.repositories.append( + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION, + scan_on_push=True, + policy=repo_policy_public, + images_details=[], + lyfecicle_policy=None, + ) + ) + ecr_client.repositories[0].images_details.append( + ImageDetails( + latest_tag="test-tag", + latest_digest="test-digest", + scan_findings_status="", + scan_findings_severity_count=FindingSeverityCounts( + critical=0, high=0, medium=0 + ), + ), + ), + with mock.patch( + "providers.aws.services.ecr.ecr_service.ECR", + ecr_client, + ): + from 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 search("without a scan", result[0].status_extended) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn diff --git a/providers/aws/services/ecr/ecr_service.py b/providers/aws/services/ecr/ecr_service.py new file mode 100644 index 00000000..da9fe902 --- /dev/null +++ b/providers/aws/services/ecr/ecr_service.py @@ -0,0 +1,196 @@ +import threading +from dataclasses import dataclass +from json import loads + +from lib.logger import logger +from providers.aws.aws_provider import generate_regional_clients + + +################################ ECR +class ECR: + def __init__(self, audit_info): + self.service = "ecr" + self.session = audit_info.audit_session + self.regional_clients = generate_regional_clients(self.service, audit_info) + self.repositories = [] + self.__threading_call__(self.__describe_repositories__) + self.__describe_repository_policies__() + self.__get_image_details__() + self.__get_repository_lifecycle_policy__() + + def __get_session__(self): + return self.session + + def __threading_call__(self, call): + threads = [] + for regional_client in self.regional_clients.values(): + threads.append(threading.Thread(target=call, args=(regional_client,))) + for t in threads: + t.start() + for t in threads: + t.join() + + def __describe_repositories__(self, regional_client): + logger.info("ECR - Describing repositories...") + try: + describe_ecr_paginator = regional_client.get_paginator( + "describe_repositories" + ) + for page in describe_ecr_paginator.paginate(): + for repository in page["repositories"]: + self.repositories.append( + Repository( + name=repository["repositoryName"], + arn=repository["repositoryArn"], + region=regional_client.region, + scan_on_push=repository["imageScanningConfiguration"][ + "scanOnPush" + ], + policy=None, + images_details=[], + lyfecicle_policy=None, + ) + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def __describe_repository_policies__(self): + logger.info("ECR - Describing repository policies...") + try: + for repository in self.repositories: + client = self.regional_clients[repository.region] + policy = client.get_repository_policy(repositoryName=repository.name) + if "policyText" in policy: + repository.policy = loads(policy["policyText"]) + + except Exception as error: + logger.error( + f"-- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def __get_repository_lifecycle_policy__(self): + logger.info("ECR - Getting repository lifecycle policy...") + try: + for repository in self.repositories: + client = self.regional_clients[repository.region] + policy = client.get_lifecycle_policy(repositoryName=repository.name) + if "lifecyclePolicyText" in policy: + repository.lyfecicle_policy = policy["lifecyclePolicyText"] + + except Exception as error: + logger.error( + f"-- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def __get_image_details__(self): + logger.info("ECR - Getting images details...") + try: + for repository in self.repositories: + # if the repo is not scanning pushed images there is nothing to do + if repository.scan_on_push: + client = self.regional_clients[repository.region] + describe_images_paginator = client.get_paginator("describe_images") + for page in describe_images_paginator.paginate( + repositoryName=repository.name + ): + + for image in page["imageDetails"]: + severity_counts = None + last_scan_status = None + if "imageScanStatus" in image: + last_scan_status = image["imageScanStatus"]["status"] + + if "imageScanFindingsSummary" in image: + severity_counts = FindingSeverityCounts( + critical=0, high=0, medium=0 + ) + finding_severity_counts = image[ + "imageScanFindingsSummary" + ]["findingSeverityCounts"] + if "CRITICAL" in finding_severity_counts: + severity_counts.critical = finding_severity_counts[ + "CRITICAL" + ] + if "HIGH" in finding_severity_counts: + severity_counts.high = finding_severity_counts[ + "HIGH" + ] + if "MEDIUM" in finding_severity_counts: + severity_counts.medium = finding_severity_counts[ + "MEDIUM" + ] + + repository.images_details.append( + ImageDetails( + latest_tag=image["imageTags"][0], + latest_digest=image["imageDigest"], + scan_findings_status=last_scan_status, + scan_findings_severity_count=severity_counts, + ) + ) + + except Exception as error: + logger.error( + f"-- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + +@dataclass +class FindingSeverityCounts: + critical: int + high: int + medium: int + + def __init__( + self, + critical, + high, + medium, + ): + self.critical = critical + self.high = high + self.medium = medium + + +@dataclass +class ImageDetails: + latest_tag: str + latest_digest: str + scan_findings_status: str + scan_findings_severity_count: FindingSeverityCounts + + def __init__( + self, + latest_tag, + latest_digest, + scan_findings_status, + scan_findings_severity_count, + ): + self.latest_tag = latest_tag + self.latest_digest = latest_digest + self.scan_findings_status = scan_findings_status + self.scan_findings_severity_count = scan_findings_severity_count + + +@dataclass +class Repository: + name: str + arn: str + region: str + scan_on_push: bool + policy: dict + images_details: list[ImageDetails] + lyfecicle_policy: str + + def __init__( + self, name, arn, region, scan_on_push, policy, images_details, lyfecicle_policy + ): + self.name = name + self.arn = arn + self.region = region + self.scan_on_push = scan_on_push + self.policy = policy + self.images_details = images_details + self.lyfecicle_policy = lyfecicle_policy diff --git a/providers/aws/services/ecr/ecr_service_test.py b/providers/aws/services/ecr/ecr_service_test.py new file mode 100644 index 00000000..eb5b1cd9 --- /dev/null +++ b/providers/aws/services/ecr/ecr_service_test.py @@ -0,0 +1,212 @@ +from unittest.mock import patch + +import botocore +from boto3 import client, session +from moto import mock_ecr + +from providers.aws.lib.audit_info.models import AWS_Audit_Info +from providers.aws.services.ecr.ecr_service import ECR + +AWS_ACCOUNT_NUMBER = 123456789012 +AWS_REGION = "eu-west-1" + +repo_arn = f"arn:aws:ecr:eu-west-1:{AWS_ACCOUNT_NUMBER}:repository/test-repo" +repo_name = "test-repo" + +# Mocking Access Analyzer Calls +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "DescribeImages": + return { + "imageDetails": [ + { + "imageDigest": "sha256:d8868e50ac4c7104d2200d42f432b661b2da8c1e417ccfae217e6a1e04bb9295", + "imageTags": [ + "test-tag", + ], + "imageScanStatus": { + "status": "COMPLETE", + }, + "imageScanFindingsSummary": { + "findingSeverityCounts": {"CRITICAL": 1, "HIGH": 2, "MEDIUM": 3} + }, + }, + { + "imageDigest": "sha256:83251ac64627fc331584f6c498b3aba5badc01574e2c70b2499af3af16630eed", + "imageTags": [ + "test-tag2", + ], + }, + ], + } + if operation_name == "GetRepositoryPolicy": + return { + "registryId": "string", + "repositoryName": "string", + "policyText": '{\n "Version" : "2012-10-17",\n "Statement" : [ {\n "Sid" : "Allow Describe Images",\n "Effect" : "Allow",\n "Principal" : {\n "AWS" : [ "arn:aws:iam::123456789012:root" ]\n },\n "Action" : [ "ecr:DescribeImages", "ecr:DescribeRepositories" ]\n } ]\n}', + } + if operation_name == "GetLifecyclePolicy": + return { + "registryId": "string", + "repositoryName": "string", + "lifecyclePolicyText": "test-policy", + } + return make_api_call(self, operation_name, kwarg) + + +def mock_generate_regional_clients(service, audit_info): + regional_client = audit_info.audit_session.client(service, region_name=AWS_REGION) + regional_client.region = AWS_REGION + return {AWS_REGION: regional_client} + + +# Patch every AWS call using Boto3 and generate_regional_clients to have 1 client +@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) +@patch( + "providers.aws.services.ecr.ecr_service.generate_regional_clients", + new=mock_generate_regional_clients, +) +class Test_ECR_Service: + # Mocked Audit Info + def set_mocked_audit_info(self): + audit_info = AWS_Audit_Info( + original_session=None, + audit_session=session.Session( + profile_name=None, + botocore_session=None, + ), + audited_account=AWS_ACCOUNT_NUMBER, + audited_user_id=None, + audited_partition="aws", + audited_identity_arn=None, + profile=None, + profile_region=None, + credentials=None, + assumed_role_info=None, + audited_regions=None, + organizations_metadata=None, + ) + return audit_info + + # Test ECR Service + def test_service(self): + audit_info = self.set_mocked_audit_info() + ecr = ECR(audit_info) + assert ecr.service == "ecr" + + # Test ECR client + def test_client(self): + audit_info = self.set_mocked_audit_info() + ecr = ECR(audit_info) + for regional_client in ecr.regional_clients.values(): + assert regional_client.__class__.__name__ == "ECR" + + # Test ECR session + def test__get_session__(self): + audit_info = self.set_mocked_audit_info() + ecr = ECR(audit_info) + assert ecr.session.__class__.__name__ == "Session" + + # Test describe ECR repositories + @mock_ecr + def test__describe_repositories__(self): + ecr_client = client("ecr", region_name=AWS_REGION) + ecr_client.create_repository( + repositoryName=repo_name, + imageScanningConfiguration={"scanOnPush": True}, + ) + audit_info = self.set_mocked_audit_info() + ecr = ECR(audit_info) + assert len(ecr.repositories) == 1 + assert ecr.repositories[0].name == repo_name + assert ecr.repositories[0].arn == repo_arn + assert ecr.repositories[0].scan_on_push + + # Test describe ECR repository policies + @mock_ecr + def test__describe_repository_policies__(self): + ecr_client = client("ecr", region_name=AWS_REGION) + ecr_client.create_repository( + repositoryName=repo_name, + imageScanningConfiguration={"scanOnPush": True}, + ) + audit_info = self.set_mocked_audit_info() + ecr = ECR(audit_info) + assert len(ecr.repositories) == 1 + assert ecr.repositories[0].name == repo_name + assert ecr.repositories[0].arn == repo_arn + assert ecr.repositories[0].scan_on_push + assert ( + ecr.repositories[0].policy["Statement"][0]["Sid"] == "Allow Describe Images" + ) + assert ecr.repositories[0].policy["Statement"][0]["Effect"] == "Allow" + assert ( + ecr.repositories[0].policy["Statement"][0]["Principal"]["AWS"][0] + == f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + assert ( + ecr.repositories[0].policy["Statement"][0]["Action"][0] + == "ecr:DescribeImages" + ) + assert ( + ecr.repositories[0].policy["Statement"][0]["Action"][1] + == "ecr:DescribeRepositories" + ) + + # Test describe ECR repository policies + @mock_ecr + def test__get_lifecycle_policies__(self): + ecr_client = client("ecr", region_name=AWS_REGION) + ecr_client.create_repository( + repositoryName=repo_name, + imageScanningConfiguration={"scanOnPush": True}, + ) + audit_info = self.set_mocked_audit_info() + ecr = ECR(audit_info) + assert len(ecr.repositories) == 1 + assert ecr.repositories[0].name == repo_name + assert ecr.repositories[0].arn == repo_arn + assert ecr.repositories[0].scan_on_push + assert ecr.repositories[0].lyfecicle_policy + + # Test get image details + @mock_ecr + def test__get_image_details__(self): + ecr_client = client("ecr", region_name=AWS_REGION) + ecr_client.create_repository( + repositoryName=repo_name, + imageScanningConfiguration={"scanOnPush": True}, + ) + audit_info = self.set_mocked_audit_info() + ecr = ECR(audit_info) + assert len(ecr.repositories) == 1 + assert ecr.repositories[0].name == repo_name + assert ecr.repositories[0].arn == repo_arn + assert ecr.repositories[0].scan_on_push + assert len(ecr.repositories[0].images_details) == 2 + assert ecr.repositories[0].images_details[0].latest_tag == "test-tag" + assert ( + ecr.repositories[0].images_details[0].latest_digest + == "sha256:d8868e50ac4c7104d2200d42f432b661b2da8c1e417ccfae217e6a1e04bb9295" + ) + assert ecr.repositories[0].images_details[0].scan_findings_status == "COMPLETE" + assert ( + ecr.repositories[0].images_details[0].scan_findings_severity_count.critical + == 1 + ) + assert ( + ecr.repositories[0].images_details[0].scan_findings_severity_count.high == 2 + ) + assert ( + ecr.repositories[0].images_details[0].scan_findings_severity_count.medium + == 3 + ) + assert ecr.repositories[0].images_details[1].latest_tag == "test-tag2" + assert ( + ecr.repositories[0].images_details[1].latest_digest + == "sha256:83251ac64627fc331584f6c498b3aba5badc01574e2c70b2499af3af16630eed" + ) + assert not ecr.repositories[0].images_details[1].scan_findings_status + assert not ecr.repositories[0].images_details[1].scan_findings_severity_count