diff --git a/providers/aws/services/ssm/__init__.py b/providers/aws/services/ssm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/ssm/check_extra7124 b/providers/aws/services/ssm/check_extra7124 deleted file mode 100644 index 0e1df008..00000000 --- a/providers/aws/services/ssm/check_extra7124 +++ /dev/null @@ -1,52 +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_extra7124="7.124" -CHECK_TITLE_extra7124="[extra7124] Check if EC2 instances are managed by Systems Manager." -CHECK_SCORED_extra7124="NOT_SCORED" -CHECK_CIS_LEVEL_extra7124="EXTRA" -CHECK_SEVERITY_extra7124="Medium" -CHECK_ASFF_RESOURCE_TYPE_extra7124="AwsEc2Instance" -CHECK_ALTERNATE_check7124="extra7124" -CHECK_ASFF_COMPLIANCE_TYPE_extra7124="ens-op.exp.1.aws.sys.1 ens-op.acc.4.aws.sys.1" -CHECK_SERVICENAME_extra7124="ssm" -CHECK_RISK_extra7124='AWS Config provides AWS Managed Rules; which are predefined; customizable rules that AWS Config uses to evaluate whether your AWS resource configurations comply with common best practices.' -CHECK_REMEDIATION_extra7124='Verify and apply Systems Manager Prerequisites.' -CHECK_DOC_extra7124='https://docs.aws.amazon.com/systems-manager/latest/userguide/managed_instances.html' -CHECK_CAF_EPIC_extra7124='Infrastructure Security' - -extra7124(){ - for regx in $REGIONS; do - # Filters running instances only - LIST_EC2_INSTANCES=$($AWSCLI ec2 describe-instances $PROFILE_OPT --query 'Reservations[*].Instances[*].[InstanceId]' --filters Name=instance-state-name,Values=running --region $regx --output text 2>&1) - if [[ $(echo "$LIST_EC2_INSTANCES" | grep -E 'AccessDenied|UnauthorizedOperation|AuthorizationError') ]]; then - textInfo "$regx: Access Denied trying to describe instances" "$regx" - continue - fi - if [[ $LIST_EC2_INSTANCES ]]; then - LIST_SSM_MANAGED_INSTANCES=$($AWSCLI ssm describe-instance-information $PROFILE_OPT --query "InstanceInformationList[].InstanceId" --region $regx | jq -r '.[]') - LIST_EC2_UNMANAGED=$(echo ${LIST_SSM_MANAGED_INSTANCES[@]} ${LIST_EC2_INSTANCES[@]} | tr ' ' '\n' | sort | uniq -u) - if [[ $LIST_EC2_UNMANAGED ]]; then - for instance in $LIST_EC2_UNMANAGED; do - textFail "$regx: EC2 instance $instance is not managed by Systems Manager" "$regx" "$instance" - done - fi - if [[ $LIST_SSM_MANAGED_INSTANCES ]]; then - for instance in $LIST_SSM_MANAGED_INSTANCES; do - textPass "$regx: EC2 instance $instance is managed by Systems Manager" "$regx" "$instance" - done - fi - else - textInfo "$regx: No EC2 instances running found" "$regx" - fi - done -} diff --git a/providers/aws/services/ssm/check_extra7127 b/providers/aws/services/ssm/check_extra7127 deleted file mode 100644 index c448f421..00000000 --- a/providers/aws/services/ssm/check_extra7127 +++ /dev/null @@ -1,51 +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_extra7127="7.127" -CHECK_TITLE_extra7127="[extra7127] Check if EC2 instances managed by Systems Manager are compliant with patching requirements" -CHECK_SCORED_extra7127="NOT_SCORED" -CHECK_CIS_LEVEL_extra7127="EXTRA" -CHECK_SEVERITY_extra7127="High" -CHECK_ASFF_RESOURCE_TYPE_extra7127="AwsEc2Instance" -CHECK_ASFF_TYPE_extra7127="Software and Configuration Checks/ENS op.exp.4.aws.sys.1" -CHECK_ALTERNATE_check7127="extra7127" -CHECK_ASFF_COMPLIANCE_TYPE_extra7127="ens-op.exp.1.aws.sys.1 ens-op.exp.4.aws.sys.1" -CHECK_SERVICENAME_extra7127="ssm" -CHECK_RISK_extra7127='Without the most recent security patches your system is potentially vulnerable to cyberattacks. Even the best-designed software can not anticipate every future threat to cybersecurity. Poor patch management can leave an organizations data exposed subjecting them to malware and ransomware attacks.' -CHECK_REMEDIATION_extra7127='Consider using SSM in all accounts and services to at least monitor for missing patches on servers. Use a robust process to apply security fixes as soon as they are made available. Patch compliance data from Patch Manager can be sent to AWS Security Hub to centralize security issues.' -CHECK_DOC_extra7127='https://docs.aws.amazon.com/systems-manager/latest/userguide/patch-compliance-identify.html' -CHECK_CAF_EPIC_extra7127='Infrastructure Security' - -extra7127(){ - for regx in $REGIONS; do - NON_COMPLIANT_SSM_MANAGED_INSTANCES=$($AWSCLI ssm list-resource-compliance-summaries $PROFILE_OPT --region $regx --filters Key=Status,Values=NON_COMPLIANT --query ResourceComplianceSummaryItems[].ResourceId --output text 2>&1) - if [[ $(echo "$NON_COMPLIANT_SSM_MANAGED_INSTANCES" | grep -E 'AccessDenied|UnauthorizedOperation|AuthorizationError') ]]; then - textInfo "$regx: Access Denied trying to list resource compliance summaries" "$regx" - continue - fi - COMPLIANT_SSM_MANAGED_INSTANCES=$($AWSCLI ssm list-resource-compliance-summaries $PROFILE_OPT --region $regx --filters Key=Status,Values=COMPLIANT --query ResourceComplianceSummaryItems[].ResourceId --output text) - if [[ $NON_COMPLIANT_SSM_MANAGED_INSTANCES || $COMPLIANT_SSM_MANAGED_INSTANCES ]]; then - if [[ $NON_COMPLIANT_SSM_MANAGED_INSTANCES ]]; then - for instance in $NON_COMPLIANT_SSM_MANAGED_INSTANCES; do - textFail "$regx: EC2 managed instance $instance is non-compliant" "$regx" "$instance" - done - fi - if [[ $COMPLIANT_SSM_MANAGED_INSTANCES ]]; then - for instance in $COMPLIANT_SSM_MANAGED_INSTANCES; do - textPass "$regx: EC2 managed instance $instance is compliant" "$regx" "$instance" - done - fi - else - textInfo "$regx: No EC2 managed instances found" "$regx" - fi - done -} diff --git a/providers/aws/services/ssm/check_extra7140 b/providers/aws/services/ssm/check_extra7140 deleted file mode 100644 index ad1efec6..00000000 --- a/providers/aws/services/ssm/check_extra7140 +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -# Prowler - the handy cloud security tool (copyright 2019) 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_extra7140="7.140" -CHECK_TITLE_extra7140="[extra7140] Check if there are SSM Documents set as public" -CHECK_SCORED_extra7140="NOT_SCORED" -CHECK_CIS_LEVEL_extra7140="EXTRA" -CHECK_SEVERITY_extra7140="High" -CHECK_ASFF_RESOURCE_TYPE_extra7140="AwsSsmDocument" -CHECK_ALTERNATE_check7140="extra7140" -CHECK_SERVICENAME_extra7140="ssm" -CHECK_RISK_extra7140='SSM Documents may contain private information or even secrets and tokens.' -CHECK_REMEDIATION_extra7140='Carefully review the contents of the document before is shared. Enable SSM Block public sharing for documents.' -CHECK_DOC_extra7140='https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-before-you-share.html' -CHECK_CAF_EPIC_extra7140='Data Protection' -extra7140(){ - - for regx in $REGIONS; do - SSM_DOCS=$($AWSCLI $PROFILE_OPT --region $regx ssm list-documents --filters Key=Owner,Values=Self --query DocumentIdentifiers[].Name --output text 2>&1) - if [[ $(echo "$SSM_DOCS" | grep -E 'AccessDenied|UnauthorizedOperation|AuthorizationError') ]]; then - textInfo "$regx: Access Denied trying to list documents" "$regx" - continue - fi - if [[ $SSM_DOCS ]];then - for ssmdoc in $SSM_DOCS; do - SSM_DOC_SHARED_ALL=$($AWSCLI $PROFILE_OPT --region $regx ssm describe-document-permission --name "$ssmdoc" --permission-type "Share" --query AccountIds[] --output text | grep all) - if [[ $SSM_DOC_SHARED_ALL ]];then - textFail "$regx: SSM Document $ssmdoc is public." "$regx" "$ssmdoc" - else - textPass "$regx: SSM Document $ssmdoc is not public." "$regx" "$ssmdoc" - fi - done - else - textInfo "$regx: No SSM Document found." "$regx" - fi - done -} diff --git a/providers/aws/services/ssm/check_extra7141 b/providers/aws/services/ssm/check_extra7141 deleted file mode 100644 index e664a5a8..00000000 --- a/providers/aws/services/ssm/check_extra7141 +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash - -# Prowler - the handy cloud security tool (copyright 2019) 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_extra7141="7.141" -CHECK_TITLE_extra7141="[extra7141] Find secrets in SSM Documents" -CHECK_SCORED_extra7141="NOT_SCORED" -CHECK_CIS_LEVEL_extra7141="EXTRA" -CHECK_SEVERITY_extra7141="Critical" -CHECK_ASFF_RESOURCE_TYPE_extra7141="AwsSsmDocument" -CHECK_ALTERNATE_check7141="extra7141" -CHECK_SERVICENAME_extra7141="ssm" -CHECK_RISK_extra7141='Secrets hardcoded into SSM Documents by malware and bad actors to gain lateral access to other services.' -CHECK_REMEDIATION_extra7141='Implement automated detective control (e.g. using tools like Prowler) to scan accounts for passwords and secrets. Use Secrets Manager service to store and retrieve passwords and secrets.' -CHECK_DOC_extra7141='https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-secret-generatesecretstring.html' -CHECK_CAF_EPIC_extra7141='IAM' - -extra7141(){ - SECRETS_TEMP_FOLDER="$PROWLER_DIR/secrets-$ACCOUNT_NUM-$PROWLER_START_TIME" - if [[ ! -d "${SECRETS_TEMP_FOLDER}" ]]; then - # this folder is deleted once this check is finished - mkdir "${SECRETS_TEMP_FOLDER}" - fi - - for regx in ${REGIONS}; do - CHECK_DETECT_SECRETS_INSTALLATION=$(secretsDetector) - if [[ $? -eq 241 ]]; then - textInfo "$regx: python library detect-secrets not found. Make sure it is installed correctly." "$regx" - else - SSM_DOCS=$("${AWSCLI}" ${PROFILE_OPT} --region "${regx}" ssm list-documents --filters 'Key=Owner,Values=Self' --query 'DocumentIdentifiers[].Name' --output text 2>&1) - if [[ $(echo "${SSM_DOCS}" | grep -E 'AccessDenied|UnauthorizedOperation|AuthorizationError') ]]; then - textInfo "${regx}: Access Denied trying to list documents" "${regx}" - continue - fi - if [[ ${SSM_DOCS} ]];then - for ssmdoc in ${SSM_DOCS}; do - SSM_DOC_FILE="${SECRETS_TEMP_FOLDER}/extra7141-${ssmdoc}-${regx}-content.txt" - "${AWSCLI}" ${PROFILE_OPT} --region "${regx}" ssm get-document --name "${ssmdoc}" --output text --document-format JSON > "${SSM_DOC_FILE}" 2>&1 - if [[ $(grep -E 'AccessDenied|UnauthorizedOperation|AuthorizationError' "${SSM_DOC_FILE}") ]]; then - textInfo "${regx}: Access Denied trying to get document" "${regx}" - continue - fi - FINDINGS=$(secretsDetector file "${SSM_DOC_FILE}") - if [[ "${FINDINGS}" -eq 0 ]]; then - textPass "${regx}: No secrets found in SSM Document ${ssmdoc}" "${regx}" "${ssmdoc}" - # delete file if nothing interesting is there - rm -f "${SSM_DOC_FILE}" - else - textFail "${regx}: Potential secret found SSM Document ${ssmdoc}" "${regx}" "${ssmdoc}" - # delete file to not leave trace, user must look at the CFN Stack - rm -f "${SSM_DOC_FILE}" - fi - done - else - textInfo "${regx}: No SSM Document found." "${regx}" - fi - fi - done - rm -rf "${SECRETS_TEMP_FOLDER}" -} diff --git a/providers/aws/services/ssm/ssm_client.py b/providers/aws/services/ssm/ssm_client.py new file mode 100644 index 00000000..2b70ee18 --- /dev/null +++ b/providers/aws/services/ssm/ssm_client.py @@ -0,0 +1,4 @@ +from providers.aws.lib.audit_info.audit_info import current_audit_info +from providers.aws.services.ssm.ssm_service import SSM + +ssm_client = SSM(current_audit_info) diff --git a/providers/aws/services/ssm/ssm_document_secrets/__init__.py b/providers/aws/services/ssm/ssm_document_secrets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/ssm/ssm_document_secrets/ssm_document_secrets.metadata.json b/providers/aws/services/ssm/ssm_document_secrets/ssm_document_secrets.metadata.json new file mode 100644 index 00000000..a0759c87 --- /dev/null +++ b/providers/aws/services/ssm/ssm_document_secrets/ssm_document_secrets.metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "aws", + "CheckID": "ssm_document_secrets", + "CheckTitle": "Find secrets in SSM Documents.", + "CheckType": [], + "ServiceName": "ssm", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:ssm:region:account-id:document/document-name", + "Severity": "critical", + "ResourceType": "AwsSsmDocument", + "Description": "Find secrets in SSM Documents.", + "Risk": "Secrets hardcoded into SSM Documents by malware and bad actors to gain lateral access to other services.", + "RelatedUrl": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-secret-generatesecretstring.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Implement automated detective control (e.g. using tools like Prowler) to scan accounts for passwords and secrets. Use Secrets Manager service to store and retrieve passwords and secrets.", + "Url": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-secret-generatesecretstring.html" + } + }, + "Categories": [], + "Tags": { + "Tag1Key": "value", + "Tag2Key": "value" + }, + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "Compliance": [] +} diff --git a/providers/aws/services/ssm/ssm_document_secrets/ssm_document_secrets.py b/providers/aws/services/ssm/ssm_document_secrets/ssm_document_secrets.py new file mode 100644 index 00000000..ee1cd76b --- /dev/null +++ b/providers/aws/services/ssm/ssm_document_secrets/ssm_document_secrets.py @@ -0,0 +1,44 @@ +import json +import os +import tempfile + +from detect_secrets import SecretsCollection +from detect_secrets.settings import default_settings + +from lib.check.models import Check, Check_Report +from providers.aws.services.ssm.ssm_client import ssm_client + + +class ssm_document_secrets(Check): + def execute(self): + findings = [] + for document in ssm_client.documents.values(): + report = Check_Report(self.metadata) + report.region = document.region + report.resource_arn = f"arn:aws:ssm:{document.region}:{ssm_client.audited_account}:document/{document.name}" + report.resource_id = document.name + + report.status = "PASS" + report.status_extended = f"No secrets found in SSM Document {document.name}" + + if document.content: + temp_env_data_file = tempfile.NamedTemporaryFile(delete=False) + temp_env_data_file.write( + bytes(json.dumps(document.content), encoding="raw_unicode_escape") + ) + temp_env_data_file.close() + secrets = SecretsCollection() + with default_settings(): + secrets.scan_file(temp_env_data_file.name) + + if secrets.json(): + report.status = "FAIL" + report.status_extended = ( + f"Potential secret found in SSM Document {document.name}" + ) + + os.remove(temp_env_data_file.name) + + findings.append(report) + + return findings diff --git a/providers/aws/services/ssm/ssm_document_secrets/ssm_document_secrets_test.py b/providers/aws/services/ssm/ssm_document_secrets/ssm_document_secrets_test.py new file mode 100644 index 00000000..f1b4a012 --- /dev/null +++ b/providers/aws/services/ssm/ssm_document_secrets/ssm_document_secrets_test.py @@ -0,0 +1,100 @@ +from unittest import mock + +from moto.core import DEFAULT_ACCOUNT_ID + +from providers.aws.services.ssm.ssm_service import Document + +AWS_REGION = "eu-west-1" + + +class Test_ssm_documents_secrets: + def test_no_documents(self): + ssm_client = mock.MagicMock + ssm_client.documents = {} + with mock.patch( + "providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from providers.aws.services.ssm.ssm_document_secrets.ssm_document_secrets import ( + ssm_document_secrets, + ) + + check = ssm_document_secrets() + result = check.execute() + + assert len(result) == 0 + + def test_document_with_secrets(self): + ssm_client = mock.MagicMock + document_name = "test-document" + document_arn = ( + f"arn:aws:ssm:{AWS_REGION}:{DEFAULT_ACCOUNT_ID}:document/{document_name}" + ) + ssm_client.audited_account = DEFAULT_ACCOUNT_ID + ssm_client.documents = { + document_name: Document( + name=document_name, + region=AWS_REGION, + content={"db_password": "test-password"}, + account_owners=[], + ) + } + with mock.patch( + "providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from providers.aws.services.ssm.ssm_document_secrets.ssm_document_secrets import ( + ssm_document_secrets, + ) + + check = ssm_document_secrets() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION + assert result[0].resource_id == document_name + assert result[0].resource_arn == document_arn + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Potential secret found in SSM Document {document_name}" + ) + + def test_document_no_secrets(self): + ssm_client = mock.MagicMock + document_name = "test-document" + document_arn = ( + f"arn:aws:ssm:{AWS_REGION}:{DEFAULT_ACCOUNT_ID}:document/{document_name}" + ) + ssm_client.audited_account = DEFAULT_ACCOUNT_ID + ssm_client.documents = { + document_name: Document( + name=document_name, + region=AWS_REGION, + content={"profile": "test"}, + account_owners=[], + ) + } + with mock.patch( + "providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from providers.aws.services.ssm.ssm_document_secrets.ssm_document_secrets import ( + ssm_document_secrets, + ) + + check = ssm_document_secrets() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION + assert result[0].resource_id == document_name + assert result[0].resource_arn == document_arn + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"No secrets found in SSM Document {document_name}" + ) diff --git a/providers/aws/services/ssm/ssm_documents_set_as_public/__init__.py b/providers/aws/services/ssm/ssm_documents_set_as_public/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public.metadata.json b/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public.metadata.json new file mode 100644 index 00000000..5fd61486 --- /dev/null +++ b/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public.metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "aws", + "CheckID": "ssm_documents_set_as_public", + "CheckTitle": "Check if there are SSM Documents set as public.", + "CheckType": [], + "ServiceName": "ssm", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:ssm:region:account-id:document/document-name", + "Severity": "high", + "ResourceType": "AwsSsmDocument", + "Description": "Check if there are SSM Documents set as public.", + "Risk": "SSM Documents may contain private information or even secrets and tokens.", + "RelatedUrl": "https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-before-you-share.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://github.com/cloudmatos/matos/tree/master/remediations/aws/ssm/ssm-doc-block", + "Terraform": "" + }, + "Recommendation": { + "Text": "Carefully review the contents of the document before is shared. Enable SSM Block public sharing for documents.", + "Url": "https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-before-you-share.html" + } + }, + "Categories": [], + "Tags": { + "Tag1Key": "value", + "Tag2Key": "value" + }, + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "Compliance": [] +} diff --git a/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public.py b/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public.py new file mode 100644 index 00000000..36223df5 --- /dev/null +++ b/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public.py @@ -0,0 +1,23 @@ +from lib.check.models import Check, Check_Report +from providers.aws.services.ssm.ssm_client import ssm_client + + +class ssm_documents_set_as_public(Check): + def execute(self): + findings = [] + for document in ssm_client.documents.values(): + report = Check_Report(self.metadata) + report.region = document.region + report.resource_arn = f"arn:aws:ssm:{document.region}:{ssm_client.audited_account}:document/{document.name}" + report.resource_id = document.name + + if document.account_owners: + report.status = "FAIL" + report.status_extended = f"SSM Document {document.name} is public" + else: + report.status = "PASS" + report.status_extended = f"SSM Document {document.name} is not public" + + findings.append(report) + + return findings diff --git a/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public_test.py b/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public_test.py new file mode 100644 index 00000000..1979be7f --- /dev/null +++ b/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public_test.py @@ -0,0 +1,99 @@ +from unittest import mock + +from moto.core import DEFAULT_ACCOUNT_ID + +from providers.aws.services.ssm.ssm_service import Document + +AWS_REGION = "eu-west-1" + + +class Test_ssm_documents_set_as_public: + def test_no_documents(self): + ssm_client = mock.MagicMock + ssm_client.documents = {} + with mock.patch( + "providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from providers.aws.services.ssm.ssm_documents_set_as_public.ssm_documents_set_as_public import ( + ssm_documents_set_as_public, + ) + + check = ssm_documents_set_as_public() + result = check.execute() + + assert len(result) == 0 + + def test_document_public(self): + ssm_client = mock.MagicMock + document_name = "test-document" + document_arn = ( + f"arn:aws:ssm:{AWS_REGION}:{DEFAULT_ACCOUNT_ID}:document/{document_name}" + ) + ssm_client.audited_account = DEFAULT_ACCOUNT_ID + ssm_client.documents = { + document_name: Document( + name=document_name, + region=AWS_REGION, + content="", + account_owners=["111111111111", "111111222222"], + ) + } + with mock.patch( + "providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from providers.aws.services.ssm.ssm_documents_set_as_public.ssm_documents_set_as_public import ( + ssm_documents_set_as_public, + ) + + check = ssm_documents_set_as_public() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION + assert result[0].resource_id == document_name + assert result[0].resource_arn == document_arn + assert result[0].status == "FAIL" + assert ( + result[0].status_extended == f"SSM Document {document_name} is public" + ) + + def test_document_not_public(self): + ssm_client = mock.MagicMock + document_name = "test-document" + document_arn = ( + f"arn:aws:ssm:{AWS_REGION}:{DEFAULT_ACCOUNT_ID}:document/{document_name}" + ) + ssm_client.audited_account = DEFAULT_ACCOUNT_ID + ssm_client.documents = { + document_name: Document( + name=document_name, + region=AWS_REGION, + content="", + account_owners=[], + ) + } + with mock.patch( + "providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from providers.aws.services.ssm.ssm_documents_set_as_public.ssm_documents_set_as_public import ( + ssm_documents_set_as_public, + ) + + check = ssm_documents_set_as_public() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION + assert result[0].resource_id == document_name + assert result[0].resource_arn == document_arn + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"SSM Document {document_name} is not public" + ) diff --git a/providers/aws/services/ssm/ssm_managed_compliant_patching/__init__.py b/providers/aws/services/ssm/ssm_managed_compliant_patching/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.metadata.json b/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.metadata.json new file mode 100644 index 00000000..2ac20f45 --- /dev/null +++ b/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.metadata.json @@ -0,0 +1,35 @@ +{ + "Provider": "aws", + "CheckID": "ssm_managed_compliant_patching", + "CheckTitle": "Check if EC2 instances managed by Systems Manager are compliant with patching requirements.", + "CheckType": [], + "ServiceName": "ssm", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:ec2:region:account-id:instance/instance-id", + "Severity": "high", + "ResourceType": "AwsEc2Instance", + "Description": "Check if EC2 instances managed by Systems Manager are compliant with patching requirements.", + "Risk": "Without the most recent security patches your system is potentially vulnerable to cyberattacks. Even the best-designed software can not anticipate every future threat to cybersecurity. Poor patch management can leave an organizations data exposed subjecting them to malware and ransomware attacks.", + "RelatedUrl": "https://docs.aws.amazon.com/systems-manager/latest/userguide/patch-compliance-identify.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Consider using SSM in all accounts and services to at least monitor for missing patches on servers. Use a robust process to apply security fixes as soon as they are made available. Patch compliance data from Patch Manager can be sent to AWS Security Hub to centralize security issues.", + "Url": "https://docs.aws.amazon.com/systems-manager/latest/userguide/patch-compliance-identify.html" + } + }, + "Categories": [], + "Tags": { + "Tag1Key": "value", + "Tag2Key": "value" + }, + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "Compliance": [] +} diff --git a/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.py b/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.py new file mode 100644 index 00000000..e865ac8b --- /dev/null +++ b/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.py @@ -0,0 +1,28 @@ +from lib.check.models import Check, Check_Report +from providers.aws.services.ssm.ssm_client import ssm_client +from providers.aws.services.ssm.ssm_service import ResourceStatus + + +class ssm_managed_compliant_patching(Check): + def execute(self): + findings = [] + for resource in ssm_client.compliance_resources.values(): + report = Check_Report(self.metadata) + report.region = resource.region + report.resource_arn = f"arn:aws:ec2:{resource.region}:{ssm_client.audited_account}:instance/{resource.id}" + report.resource_id = resource.id + + if resource.status == ResourceStatus.COMPLIANT: + report.status = "PASS" + report.status_extended = ( + f"EC2 managed instance {resource.id} is compliant" + ) + else: + report.status = "FAIL" + report.status_extended = ( + f"EC2 managed instance {resource.id} is non-compliant" + ) + + findings.append(report) + + return findings diff --git a/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching_test.py b/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching_test.py new file mode 100644 index 00000000..7ea0a6c1 --- /dev/null +++ b/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching_test.py @@ -0,0 +1,100 @@ +from unittest import mock + +from moto.core import DEFAULT_ACCOUNT_ID + +from providers.aws.services.ssm.ssm_service import ComplianceResource, ResourceStatus + +AWS_REGION = "eu-west-1" + + +class Test_ssm_managed_compliant_patching: + def test_no_compliance_resources(self): + ssm_client = mock.MagicMock + ssm_client.compliance_resources = {} + with mock.patch( + "providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from providers.aws.services.ssm.ssm_managed_compliant_patching.ssm_managed_compliant_patching import ( + ssm_managed_compliant_patching, + ) + + check = ssm_managed_compliant_patching() + result = check.execute() + + assert len(result) == 0 + + def test_compliance_resources_compliant(self): + ssm_client = mock.MagicMock + instance_id = "i-1234567890abcdef0" + instance_arn = ( + f"arn:aws:ec2:{AWS_REGION}:{DEFAULT_ACCOUNT_ID}:instance/{instance_id}" + ) + ssm_client.audited_account = DEFAULT_ACCOUNT_ID + ssm_client.compliance_resources = { + instance_id: ComplianceResource( + id="i-1234567890abcdef0", + region=AWS_REGION, + status=ResourceStatus.COMPLIANT, + ) + } + + with mock.patch( + "providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from providers.aws.services.ssm.ssm_managed_compliant_patching.ssm_managed_compliant_patching import ( + ssm_managed_compliant_patching, + ) + + check = ssm_managed_compliant_patching() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION + assert result[0].resource_id == instance_id + assert result[0].resource_arn == instance_arn + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"EC2 managed instance {instance_id} is compliant" + ) + + def test_compliance_resources_non_compliant(self): + ssm_client = mock.MagicMock + instance_id = "i-1234567890abcdef0" + instance_arn = ( + f"arn:aws:ec2:{AWS_REGION}:{DEFAULT_ACCOUNT_ID}:instance/{instance_id}" + ) + ssm_client.audited_account = DEFAULT_ACCOUNT_ID + ssm_client.compliance_resources = { + instance_id: ComplianceResource( + id="i-1234567890abcdef0", + region=AWS_REGION, + status=ResourceStatus.NON_COMPLIANT, + ) + } + + with mock.patch( + "providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from providers.aws.services.ssm.ssm_managed_compliant_patching.ssm_managed_compliant_patching import ( + ssm_managed_compliant_patching, + ) + + check = ssm_managed_compliant_patching() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION + assert result[0].resource_id == instance_id + assert result[0].resource_arn == instance_arn + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"EC2 managed instance {instance_id} is non-compliant" + ) diff --git a/providers/aws/services/ssm/ssm_service.py b/providers/aws/services/ssm/ssm_service.py new file mode 100644 index 00000000..ba8de6c6 --- /dev/null +++ b/providers/aws/services/ssm/ssm_service.py @@ -0,0 +1,144 @@ +import json +import threading +from enum import Enum + +from pydantic import BaseModel + +from lib.logger import logger +from providers.aws.aws_provider import generate_regional_clients + + +################## SSM +class SSM: + def __init__(self, audit_info): + self.service = "ssm" + self.session = audit_info.audit_session + self.audited_account = audit_info.audited_account + self.regional_clients = generate_regional_clients(self.service, audit_info) + self.documents = {} + self.compliance_resources = {} + self.__threading_call__(self.__list_documents__) + self.__threading_call__(self.__get_document__) + self.__threading_call__(self.__describe_document_permission__) + self.__threading_call__(self.__list_resource_compliance_summaries__) + + 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 __list_documents__(self, regional_client): + logger.info("SSM - Listing Documents...") + try: + # To retrieve only the documents owned by the account + list_documents_parameters = { + "Filters": [ + { + "Key": "Owner", + "Values": [ + "Self", + ], + }, + ], + } + list_documents_paginator = regional_client.get_paginator("list_documents") + for page in list_documents_paginator.paginate(**list_documents_parameters): + for document in page["DocumentIdentifiers"]: + document_name = document["Name"] + + self.documents[document_name] = Document( + name=document_name, + region=regional_client.region, + ) + + except Exception as error: + logger.error( + f"{regional_client.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) + + def __get_document__(self, regional_client): + logger.info("SSM - Getting Document...") + try: + for document in self.documents.values(): + if document.region == regional_client.region: + document_info = regional_client.get_document(Name=document.name) + self.documents[document.name].content = json.loads( + document_info["Content"] + ) + + except Exception as error: + logger.error( + f"{regional_client.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) + + def __describe_document_permission__(self, regional_client): + logger.info("SSM - Describing Document Permission...") + try: + for document in self.documents.values(): + if document.region == regional_client.region: + document_permissions = regional_client.describe_document_permission( + Name=document.name, PermissionType="Share" + ) + self.documents[document.name].account_owners = document_permissions[ + "AccountIds" + ] + + except Exception as error: + logger.error( + f"{regional_client.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) + + def __list_resource_compliance_summaries__(self, regional_client): + logger.info("SSM - List Resources Compliance Summaries...") + try: + list_resource_compliance_summaries_paginator = ( + regional_client.get_paginator("list_resource_compliance_summaries") + ) + for page in list_resource_compliance_summaries_paginator.paginate(): + for item in page["ResourceComplianceSummaryItems"]: + resource_id = item["ResourceId"] + resource_status = item["Status"] + + self.compliance_resources[resource_id] = ComplianceResource( + id=resource_id, + status=resource_status, + region=regional_client.region, + ) + + except Exception as error: + logger.error( + f"{regional_client.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) + + +class ResourceStatus(Enum): + COMPLIANT = "COMPLIANT" + NON_COMPLIANT = "NON_COMPLIANT" + + +class ComplianceResource(BaseModel): + id: str + region: str + status: ResourceStatus + + +class Document(BaseModel): + name: str + region: str + content: dict = None + account_owners: list[str] = None diff --git a/providers/aws/services/ssm/ssm_service_test.py b/providers/aws/services/ssm/ssm_service_test.py new file mode 100644 index 00000000..ebf899f9 --- /dev/null +++ b/providers/aws/services/ssm/ssm_service_test.py @@ -0,0 +1,204 @@ +from unittest.mock import patch + +import botocore +import yaml +from boto3 import client, session +from moto import mock_ssm +from moto.core import DEFAULT_ACCOUNT_ID + +from providers.aws.lib.audit_info.audit_info import AWS_Audit_Info +from providers.aws.services.ssm.ssm_service import SSM, ResourceStatus + +# Mock Test Region +AWS_REGION = "eu-west-1" + +# Mocking Access Analyzer Calls +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + """We have to mock every AWS API call using Boto3""" + if operation_name == "ListResourceComplianceSummaries": + return { + "ResourceComplianceSummaryItems": [ + { + "ComplianceType": "Association", + "ResourceType": "ManagedInstance", + "ResourceId": "i-1234567890abcdef0", + "Status": "COMPLIANT", + "OverallSeverity": "UNSPECIFIED", + "ExecutionSummary": {"ExecutionTime": 1550509273.0}, + "CompliantSummary": { + "CompliantCount": 2, + "SeveritySummary": { + "CriticalCount": 0, + "HighCount": 0, + "MediumCount": 0, + "LowCount": 0, + "InformationalCount": 0, + "UnspecifiedCount": 2, + }, + }, + "NonCompliantSummary": { + "NonCompliantCount": 0, + "SeveritySummary": { + "CriticalCount": 0, + "HighCount": 0, + "MediumCount": 0, + "LowCount": 0, + "InformationalCount": 0, + "UnspecifiedCount": 0, + }, + }, + }, + ], + } + + return make_api_call(self, operation_name, kwarg) + + +# Mock generate_regional_clients() +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} + + +# SSM Document YAML Template +ssm_document_yaml = """ +schemaVersion: "2.2" +description: "Sample Yaml" +parameters: + Parameter1: + type: "Integer" + default: 3 + description: "Command Duration." + allowedValues: [1,2,3,4] + Parameter2: + type: "String" + default: "def" + description: + allowedValues: ["abc", "def", "ghi"] + allowedPattern: r"^[a-zA-Z0-9_-.]{3,128}$" + Parameter3: + type: "Boolean" + default: false + description: "A boolean" + allowedValues: [True, False] + Parameter4: + type: "StringList" + default: ["abc", "def"] + description: "A string list" + Parameter5: + type: "StringMap" + default: + NotificationType: Command + NotificationEvents: + - Failed + NotificationArn: "$dependency.topicArn" + description: + Parameter6: + type: "MapList" + default: + - DeviceName: "/dev/sda1" + Ebs: + VolumeSize: '50' + - DeviceName: "/dev/sdm" + Ebs: + VolumeSize: '100' + description: +mainSteps: + - action: "aws:runShellScript" + name: "sampleCommand" + inputs: + runCommand: + - "echo hi" +""" + + +# 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.ssm.ssm_service.generate_regional_clients", + new=mock_generate_regional_clients, +) +class Test_SSM_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=None, + audited_user_id=None, + audited_partition="aws", + audited_identity_arn=None, + profile=None, + profile_region=AWS_REGION, + credentials=None, + assumed_role_info=None, + audited_regions=None, + organizations_metadata=None, + ) + return audit_info + + # Test SSM Client + @mock_ssm + def test__get_client__(self): + ssm = SSM(self.set_mocked_audit_info()) + assert ssm.regional_clients[AWS_REGION].__class__.__name__ == "SSM" + + # Test SSM Session + @mock_ssm + def test__get_session__(self): + ssm = SSM(self.set_mocked_audit_info()) + assert ssm.session.__class__.__name__ == "Session" + + # Test SSM Service + @mock_ssm + def test__get_service__(self): + ssm = SSM(self.set_mocked_audit_info()) + assert ssm.service == "ssm" + + @mock_ssm + def test__list_documents__(self): + # Create SSM Document + ssm_client = client("ssm", region_name=AWS_REGION) + ssm_document_name = "test-document" + _ = ssm_client.create_document( + Content=ssm_document_yaml, + Name=ssm_document_name, + DocumentType="Command", + DocumentFormat="YAML", + ) + # Add permissions + ssm_client.modify_document_permission( + Name=ssm_document_name, + PermissionType="Share", + AccountIdsToAdd=[DEFAULT_ACCOUNT_ID], + ) + + ssm = SSM(self.set_mocked_audit_info()) + + assert len(ssm.documents) == 1 + assert ssm.documents + assert ssm.documents[ssm_document_name] + assert ssm.documents[ssm_document_name].name == ssm_document_name + assert ssm.documents[ssm_document_name].region == AWS_REGION + assert ssm.documents[ssm_document_name].content == yaml.safe_load( + ssm_document_yaml + ) + assert ssm.documents[ssm_document_name].account_owners == [DEFAULT_ACCOUNT_ID] + + @mock_ssm + def test__list_resource_compliance_summaries__(self): + ssm = SSM(self.set_mocked_audit_info()) + instance_id = "i-1234567890abcdef0" + assert len(ssm.compliance_resources) == 1 + assert ssm.compliance_resources + assert ssm.compliance_resources[instance_id] + assert ssm.compliance_resources[instance_id].id == instance_id + assert ssm.compliance_resources[instance_id].region == AWS_REGION + assert ssm.compliance_resources[instance_id].status == ResourceStatus.COMPLIANT