feat(ssm): Service and checks (#1496)

This commit is contained in:
Pepe Fagoaga
2022-11-17 20:59:55 +01:00
committed by GitHub
parent 025b0547cd
commit 6ff9f30473
20 changed files with 851 additions and 216 deletions

View File

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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}"
}

View File

@@ -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)

View File

@@ -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": []
}

View File

@@ -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

View File

@@ -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}"
)

View File

@@ -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": []
}

View File

@@ -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

View File

@@ -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"
)

View File

@@ -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": []
}

View File

@@ -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

View File

@@ -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"
)

View File

@@ -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

View File

@@ -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