mirror of
https://github.com/ghndrx/prowler.git
synced 2026-02-10 06:45:08 +00:00
feat(): ECR service and checks (#1475)
Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com> Co-authored-by: sergargar <sergio@verica.io>
This commit is contained in:
0
providers/aws/services/ecr/__init__.py
Normal file
0
providers/aws/services/ecr/__init__.py
Normal file
@@ -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 <value> \
|
||||
# --repository-name <value> \
|
||||
# --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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 <value> \
|
||||
# --repository-name <value> \
|
||||
# --image-scanning-configuration scanOnPush=true
|
||||
#
|
||||
# aws ecr describe-image-scan-findings \
|
||||
# --region <value> \
|
||||
# --repository-name <value>
|
||||
# --image-id imageTag=<value>
|
||||
|
||||
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
|
||||
}
|
||||
4
providers/aws/services/ecr/ecr_client.py
Normal file
4
providers/aws/services/ecr/ecr_client.py
Normal file
@@ -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)
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 <repo_name> --image-scanning-configuration scanOnPush=true--region <region_name>",
|
||||
"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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
196
providers/aws/services/ecr/ecr_service.py
Normal file
196
providers/aws/services/ecr/ecr_service.py
Normal file
@@ -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
|
||||
212
providers/aws/services/ecr/ecr_service_test.py
Normal file
212
providers/aws/services/ecr/ecr_service_test.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user