From 01cea519440f46b7455514bfef5e5217a2e0c336 Mon Sep 17 00:00:00 2001 From: Patrick Brazil Date: Mon, 15 Jun 2020 09:49:58 -0500 Subject: [PATCH 01/11] made fixes to sns and added test cases --- audit_controller.py | 2 +- auditors/Amazon_SNS_Auditor.py | 267 +++++++++--------- auditors/tests/test_Amazon_SNS_Auditor.py | 47 ++- govcloud-auditors/Amazon_SNS_Auditor.py | 266 ++++++++--------- .../tests/test_Amazon_SNS_Auditor.py | 47 ++- 5 files changed, 357 insertions(+), 272 deletions(-) diff --git a/audit_controller.py b/audit_controller.py index 479be80b..9f558637 100644 --- a/audit_controller.py +++ b/audit_controller.py @@ -116,7 +116,7 @@ def main(argv): continue print(f"Executing check: {check.name}") for finding in check.execute(): - # It would be possible to collect these fidnings and batch them up before sending. + # It would be possible to collect these findings and batch them up before sending. # This current implementation has the advantage of a small memory footprint, but # could be a slight performance improvement to batch and make one securityhub # call per check. diff --git a/auditors/Amazon_SNS_Auditor.py b/auditors/Amazon_SNS_Auditor.py index 6720a5d1..297a4bbc 100644 --- a/auditors/Amazon_SNS_Auditor.py +++ b/auditors/Amazon_SNS_Auditor.py @@ -316,12 +316,12 @@ def execute(self): fail = False # this results in one finding per topic instead of one finding per statement for sid in statement["Statement"]: - access = sid["Principal"]["AWS"] iso8601Time = ( datetime.datetime.utcnow() .replace(tzinfo=datetime.timezone.utc) .isoformat() ) + access = sid["Principal"].get("AWS", None) if access != "*" or (access == "*" and "Condition" in sid): continue else: @@ -454,7 +454,6 @@ def execute(self): } yield finding - class SNSCrossAccountCheck(Auditor): def execute(self): awsAccountId = sts.get_caller_identity()["Account"] @@ -469,143 +468,151 @@ def execute(self): response = sns.get_topic_attributes(TopicArn=topicarn) myPolicy_json = str(response["Attributes"]["Policy"]) myPolicy = json.loads(myPolicy_json) + fail = False for statement in myPolicy["Statement"]: - principal = statement["Principal"]["AWS"] iso8601Time = ( datetime.datetime.utcnow() .replace(tzinfo=datetime.timezone.utc) .isoformat() ) - if principal[0] != "*": - if not principal[0].isdigit(): + principal = statement["Principal"].get("AWS", None) + if principal and principal != "*": + if not principal.isdigit(): + # This assumes if it is not a digit that it must be an arn. + # not sure if this is a safe assumption. principal = principal.split(":")[4] if principal == awsAccountId: - finding = { - "SchemaVersion": "2018-10-08", - "Id": topicarn + "/sns-cross-account-check", - "ProductArn": "arn:aws:securityhub:" - + awsRegion - + ":" - + awsAccountId - + ":product/" - + awsAccountId - + "/default", - "GeneratorId": topicarn, - "AwsAccountId": awsAccountId, - "Types": [ - "Software and Configuration Checks/AWS Security Best Practices", - "Effects/Data Exposure", - ], - "FirstObservedAt": iso8601Time, - "CreatedAt": iso8601Time, - "UpdatedAt": iso8601Time, - "Severity": {"Label": "INFORMATIONAL"}, - "Confidence": 99, - "Title": "[SNS.4] SNS topics should not allow cross-account access", - "Description": "SNS topic " - + topicName - + " does not have cross-account access.", - "Remediation": { - "Recommendation": { - "Text": "For more information on SNS best practices refer to the Amazon SNS security best practices section of the Amazon Simple Notification Service Developer Guide.", - "Url": "https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit", - } - }, - "ProductFields": {"Product Name": "ElectricEye"}, - "Resources": [ - { - "Type": "AwsSnsTopic", - "Id": topicarn, - "Partition": "aws", - "Region": awsRegion, - "Details": { - "AwsSnsTopic": {"TopicName": topicName} - }, - } - ], - "Compliance": { - "Status": "PASSED", - "RelatedRequirements": [ - "NIST CSF PR.AC-3", - "NIST SP 800-53 AC-1", - "NIST SP 800-53 AC-17", - "NIST SP 800-53 AC-19", - "NIST SP 800-53 AC-20", - "NIST SP 800-53 SC-15", - "AICPA TSC CC6.6", - "ISO 27001:2013 A.6.2.1", - "ISO 27001:2013 A.6.2.2", - "ISO 27001:2013 A.11.2.6", - "ISO 27001:2013 A.13.1.1", - "ISO 27001:2013 A.13.2.1", - ], - }, - "Workflow": {"Status": "RESOLVED"}, - "RecordState": "ARCHIVED", - } - yield finding + continue else: - finding = { - "SchemaVersion": "2018-10-08", - "Id": topicarn + "/sns-cross-account-check", - "ProductArn": "arn:aws:securityhub:" - + awsRegion - + ":" - + awsAccountId - + ":product/" - + awsAccountId - + "/default", - "GeneratorId": topicarn, - "AwsAccountId": awsAccountId, - "Types": [ - "Software and Configuration Checks/AWS Security Best Practices", - "Effects/Data Exposure", - ], - "FirstObservedAt": iso8601Time, - "CreatedAt": iso8601Time, - "UpdatedAt": iso8601Time, - "Severity": {"Label": "Low"}, - "Confidence": 99, - "Title": "[SNS.4] SNS topics should not allow cross-account access", - "Description": "SNS topic " - + topicName - + " has cross-account access.", - "Remediation": { - "Recommendation": { - "Text": "For more information on SNS best practices refer to the Amazon SNS security best practices section of the Amazon Simple Notification Service Developer Guide.", - "Url": "https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit", - } + fail = True + break + if not fail: + finding = { + "SchemaVersion": "2018-10-08", + "Id": topicarn + "/sns-cross-account-check", + "ProductArn": "arn:aws:securityhub:" + + awsRegion + + ":" + + awsAccountId + + ":product/" + + awsAccountId + + "/default", + "GeneratorId": topicarn, + "AwsAccountId": awsAccountId, + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Data Exposure", + ], + "FirstObservedAt": iso8601Time, + "CreatedAt": iso8601Time, + "UpdatedAt": iso8601Time, + "Severity": {"Label": "INFORMATIONAL"}, + "Confidence": 99, + "Title": "[SNS.4] SNS topics should not allow cross-account access", + "Description": "SNS topic " + + topicName + + " does not have cross-account access.", + "Remediation": { + "Recommendation": { + "Text": "For more information on SNS best practices refer to the Amazon SNS security best practices section of the Amazon Simple Notification Service Developer Guide.", + "Url": "https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit", + } + }, + "ProductFields": {"Product Name": "ElectricEye"}, + "Resources": [ + { + "Type": "AwsSnsTopic", + "Id": topicarn, + "Partition": "aws", + "Region": awsRegion, + "Details": { + "AwsSnsTopic": {"TopicName": topicName} }, - "ProductFields": {"Product Name": "ElectricEye"}, - "Resources": [ - { - "Type": "AwsSnsTopic", - "Id": topicarn, - "Partition": "aws", - "Region": awsRegion, - "Details": { - "AwsSnsTopic": {"TopicName": topicName} - }, - } - ], - "Compliance": { - "Status": "FAILED", - "RelatedRequirements": [ - "NIST CSF PR.AC-3", - "NIST SP 800-53 AC-1", - "NIST SP 800-53 AC-17", - "NIST SP 800-53 AC-19", - "NIST SP 800-53 AC-20", - "NIST SP 800-53 SC-15", - "AICPA TSC CC6.6", - "ISO 27001:2013 A.6.2.1", - "ISO 27001:2013 A.6.2.2", - "ISO 27001:2013 A.11.2.6", - "ISO 27001:2013 A.13.1.1", - "ISO 27001:2013 A.13.2.1", - ], + } + ], + "Compliance": { + "Status": "PASSED", + "RelatedRequirements": [ + "NIST CSF PR.AC-3", + "NIST SP 800-53 AC-1", + "NIST SP 800-53 AC-17", + "NIST SP 800-53 AC-19", + "NIST SP 800-53 AC-20", + "NIST SP 800-53 SC-15", + "AICPA TSC CC6.6", + "ISO 27001:2013 A.6.2.1", + "ISO 27001:2013 A.6.2.2", + "ISO 27001:2013 A.11.2.6", + "ISO 27001:2013 A.13.1.1", + "ISO 27001:2013 A.13.2.1", + ], + }, + "Workflow": {"Status": "RESOLVED"}, + "RecordState": "ARCHIVED", + } + yield finding + else: + finding = { + "SchemaVersion": "2018-10-08", + "Id": topicarn + "/sns-cross-account-check", + "ProductArn": "arn:aws:securityhub:" + + awsRegion + + ":" + + awsAccountId + + ":product/" + + awsAccountId + + "/default", + "GeneratorId": topicarn, + "AwsAccountId": awsAccountId, + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Data Exposure", + ], + "FirstObservedAt": iso8601Time, + "CreatedAt": iso8601Time, + "UpdatedAt": iso8601Time, + "Severity": {"Label": "Low"}, + "Confidence": 99, + "Title": "[SNS.4] SNS topics should not allow cross-account access", + "Description": "SNS topic " + + topicName + + " has cross-account access.", + "Remediation": { + "Recommendation": { + "Text": "For more information on SNS best practices refer to the Amazon SNS security best practices section of the Amazon Simple Notification Service Developer Guide.", + "Url": "https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit", + } + }, + "ProductFields": {"Product Name": "ElectricEye"}, + "Resources": [ + { + "Type": "AwsSnsTopic", + "Id": topicarn, + "Partition": "aws", + "Region": awsRegion, + "Details": { + "AwsSnsTopic": {"TopicName": topicName} }, - "Workflow": {"Status": "NEW"}, - "RecordState": "ACTIVE", } - yield finding + ], + "Compliance": { + "Status": "FAILED", + "RelatedRequirements": [ + "NIST CSF PR.AC-3", + "NIST SP 800-53 AC-1", + "NIST SP 800-53 AC-17", + "NIST SP 800-53 AC-19", + "NIST SP 800-53 AC-20", + "NIST SP 800-53 SC-15", + "AICPA TSC CC6.6", + "ISO 27001:2013 A.6.2.1", + "ISO 27001:2013 A.6.2.2", + "ISO 27001:2013 A.11.2.6", + "ISO 27001:2013 A.13.1.1", + "ISO 27001:2013 A.13.2.1", + ], + }, + "Workflow": {"Status": "NEW"}, + "RecordState": "ACTIVE", + } + yield finding diff --git a/auditors/tests/test_Amazon_SNS_Auditor.py b/auditors/tests/test_Amazon_SNS_Auditor.py index 430b3db5..a3afbf96 100644 --- a/auditors/tests/test_Amazon_SNS_Auditor.py +++ b/auditors/tests/test_Amazon_SNS_Auditor.py @@ -26,6 +26,12 @@ "Topics": [{"TopicArn": "arn:aws:sns:us-east-1:012345678901:MyTopic"},], } +get_topic_attributes_no_AWS = { + "Attributes": { + "Policy": '{"Version": "2008-10-17","Id": "__default_policy_ID","Statement": [{"Sid": "__default_statement_ID","Effect": "Allow","Principal": {"AWS": "*"},"Action": ["SNS:GetTopicAttributes","SNS:SetTopicAttributes","SNS:AddPermission","SNS:RemovePermission","SNS:DeleteTopic","SNS:Subscribe","SNS:ListSubscriptionsByTopic","SNS:Publish","SNS:Receive"],"Resource": "arn:aws:sns:us-east-1:012345678901:cloudtrail-sns","Condition": {"StringEquals": {"AWS:SourceOwner": "012345678901"}}},{"Sid": "AWSCloudTrailSNSPolicy20150319","Effect": "Allow","Principal": {"Service": "cloudtrail.amazonaws.com"},"Action": "SNS:Publish","Resource": "arn:aws:sns:us-east-1:012345678901:cloudtrail-sns"}]}' + } +} + get_topic_attributes_arn_response = { "Attributes": { "Policy": '{"Statement":[{"Principal":{"AWS":"arn:aws:iam::012345678901:root"},"Condition":{"StringEquals":{"AWS:SourceOwner":"012345678901"}}}]}', @@ -45,19 +51,19 @@ get_topic_attributes_response1 = { "Attributes": { - "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::012345678901:root"},"Action":["SNS:Publish","SNS:RemovePermission","SNS:SetTopicAttributes","SNS:DeleteTopic","SNS:ListSubscriptionsByTopic","SNS:GetTopicAttributes","SNS:Receive","SNS:AddPermission","SNS:Subscribe"],"Resource":"arn:aws:sns:us-east-1:012345678901:Test"}]}' + "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::012345678901:root"},"Action":["SNS:Publish","SNS:RemovePermission","SNS:SetTopicAttributes","SNS:DeleteTopic","SNS:ListSubscriptionsByTopic","SNS:GetTopicAttributes","SNS:Receive","SNS:AddPermission","SNS:Subscribe"],"Resource":"arn:aws:sns:us-east-1:012345678901:MyTopic"}]}' } } get_topic_attributes_response2 = { "Attributes": { - "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:GetTopicAttributes","SNS:SetTopicAttributes","SNS:AddPermission","SNS:RemovePermission","SNS:DeleteTopic","SNS:Subscribe","SNS:ListSubscriptionsByTopic","SNS:Publish","SNS:Receive"],"Resource":"arn:aws:sns:us-east-1:012345678901:Test","Condition":{"StringEquals":{"AWS:SourceOwner":"012345678901"}}}]}' + "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:GetTopicAttributes","SNS:SetTopicAttributes","SNS:AddPermission","SNS:RemovePermission","SNS:DeleteTopic","SNS:Subscribe","SNS:ListSubscriptionsByTopic","SNS:Publish","SNS:Receive"],"Resource":"arn:aws:sns:us-east-1:012345678901:MyTopic","Condition":{"StringEquals":{"AWS:SourceOwner":"012345678901"}}}]}' } } get_topic_attributes_response3 = { "Attributes": { - "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:Publish","SNS:RemovePermission","SNS:SetTopicAttributes","SNS:DeleteTopic","SNS:ListSubscriptionsByTopic","SNS:GetTopicAttributes","SNS:Receive","SNS:AddPermission","SNS:Subscribe"],"Resource":"arn:aws:sns:us-east-1:012345678901:Test","Condition":{"StringEquals":{"AWS:SourceOwner":"012345678901"}}},{"Sid":"__console_pub_0","Effect":"Allow","Principal":{"AWS":"*"},"Action":"SNS:Publish","Resource":"arn:aws:sns:us-east-1:012345678901:Test"},{"Sid":"__console_sub_0","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:Subscribe","SNS:Receive"],"Resource":"arn:aws:sns:us-east-1:012345678901:Test"}]}' + "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:Publish","SNS:RemovePermission","SNS:SetTopicAttributes","SNS:DeleteTopic","SNS:ListSubscriptionsByTopic","SNS:GetTopicAttributes","SNS:Receive","SNS:AddPermission","SNS:Subscribe"],"Resource":"arn:aws:sns:us-east-1:012345678901:MyTopic","Condition":{"StringEquals":{"AWS:SourceOwner":"012345678901"}}},{"Sid":"__console_pub_0","Effect":"Allow","Principal":{"AWS":"*"},"Action":"SNS:Publish","Resource":"arn:aws:sns:us-east-1:012345678901:MyTopic"},{"Sid":"__console_sub_0","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:Subscribe","SNS:Receive"],"Resource":"arn:aws:sns:us-east-1:012345678901:MyTopic"}]}' } } @@ -124,6 +130,20 @@ def test_id_not_principal(sns_stubber, sts_stubber): assert result["RecordState"] == "ACTIVE" sns_stubber.assert_no_pending_responses() +def test_no_AWS(sns_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + sns_stubber.add_response("list_topics", list_topics_response) + sns_stubber.add_response( + "get_topic_attributes", get_topic_attributes_no_AWS + ) + check = SNSCrossAccountCheck() + results = check.execute() + for result in results: + if "MyTopic" in result["Id"]: + assert result["RecordState"] == "ARCHIVED" + else: + assert False + sns_stubber.assert_no_pending_responses() def test_no_access(sts_stubber, sns_stubber): sts_stubber.add_response("get_caller_identity", sts_response) @@ -132,7 +152,7 @@ def test_no_access(sts_stubber, sns_stubber): check = SNSPublicAccessCheck() results = check.execute() for result in results: - if "Test" in result["Id"]: + if "MyTopic" in result["Id"]: assert result["RecordState"] == "ARCHIVED" else: assert False @@ -146,7 +166,7 @@ def test_has_a_condition(sts_stubber, sns_stubber): check = SNSPublicAccessCheck() results = check.execute() for result in results: - if "Test" in result["Id"]: + if "MyTopic" in result["Id"]: assert result["RecordState"] == "ARCHIVED" else: assert False @@ -160,7 +180,22 @@ def test_has_public_access(sts_stubber, sns_stubber): check = SNSPublicAccessCheck() results = check.execute() for result in results: - if "Test" in result["Id"]: + if "MyTopic" in result["Id"]: + assert result["RecordState"] == "ACTIVE" + else: + assert False + sns_stubber.assert_no_pending_responses() + +def test_no_AWS_Public(sns_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + sns_stubber.add_response("list_topics", list_topics_response) + sns_stubber.add_response( + "get_topic_attributes", get_topic_attributes_no_AWS + ) + check = SNSPublicAccessCheck() + results = check.execute() + for result in results: + if "MyTopic" in result["Id"]: assert result["RecordState"] == "ARCHIVED" else: assert False diff --git a/govcloud-auditors/Amazon_SNS_Auditor.py b/govcloud-auditors/Amazon_SNS_Auditor.py index e9d07a5a..e6bf6dee 100644 --- a/govcloud-auditors/Amazon_SNS_Auditor.py +++ b/govcloud-auditors/Amazon_SNS_Auditor.py @@ -316,7 +316,7 @@ def execute(self): fail = False # this results in one finding per topic instead of one finding per statement for sid in statement["Statement"]: - access = sid["Principal"]["aws-us-gov"] + access = sid["Principal"].get("aws-us-gov", None) iso8601Time = ( datetime.datetime.utcnow() .replace(tzinfo=datetime.timezone.utc) @@ -469,143 +469,151 @@ def execute(self): response = sns.get_topic_attributes(TopicArn=topicarn) myPolicy_json = str(response["Attributes"]["Policy"]) myPolicy = json.loads(myPolicy_json) + fail = False for statement in myPolicy["Statement"]: - principal = statement["Principal"]["aws-us-gov"] + principal = statement["Principal"].get("aws-us-gov", None) iso8601Time = ( datetime.datetime.utcnow() .replace(tzinfo=datetime.timezone.utc) .isoformat() ) - if principal[0] != "*": - if not principal[0].isdigit(): + if principal and principal != "*": + if not principal.isdigit(): + # This assumes if it is not a digit that it must be an arn. + # not sure if this is a safe assumption. principal = principal.split(":")[4] if principal == awsAccountId: - finding = { - "SchemaVersion": "2018-10-08", - "Id": topicarn + "/sns-cross-account-check", - "ProductArn": "arn:aws-us-gov:securityhub:" - + awsRegion - + ":" - + awsAccountId - + ":product/" - + awsAccountId - + "/default", - "GeneratorId": topicarn, - "AwsAccountId": awsAccountId, - "Types": [ - "Software and Configuration Checks/AWS Security Best Practices", - "Effects/Data Exposure", - ], - "FirstObservedAt": iso8601Time, - "CreatedAt": iso8601Time, - "UpdatedAt": iso8601Time, - "Severity": {"Label": "INFORMATIONAL"}, - "Confidence": 99, - "Title": "[SNS.4] SNS topics should not allow cross-account access", - "Description": "SNS topic " - + topicName - + " does not have cross-account access.", - "Remediation": { - "Recommendation": { - "Text": "For more information on SNS best practices refer to the Amazon SNS security best practices section of the Amazon Simple Notification Service Developer Guide.", - "Url": "https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit", - } - }, - "ProductFields": {"Product Name": "ElectricEye"}, - "Resources": [ - { - "Type": "AwsSnsTopic", - "Id": topicarn, - "Partition": "aws-us-gov", - "Region": awsRegion, - "Details": { - "AwsSnsTopic": {"TopicName": topicName} - }, - } - ], - "Compliance": { - "Status": "PASSED", - "RelatedRequirements": [ - "NIST CSF PR.AC-3", - "NIST SP 800-53 AC-1", - "NIST SP 800-53 AC-17", - "NIST SP 800-53 AC-19", - "NIST SP 800-53 AC-20", - "NIST SP 800-53 SC-15", - "AICPA TSC CC6.6", - "ISO 27001:2013 A.6.2.1", - "ISO 27001:2013 A.6.2.2", - "ISO 27001:2013 A.11.2.6", - "ISO 27001:2013 A.13.1.1", - "ISO 27001:2013 A.13.2.1", - ], - }, - "Workflow": {"Status": "RESOLVED"}, - "RecordState": "ARCHIVED", - } - yield finding + continue else: - finding = { - "SchemaVersion": "2018-10-08", - "Id": topicarn + "/sns-cross-account-check", - "ProductArn": "arn:aws-us-gov:securityhub:" - + awsRegion - + ":" - + awsAccountId - + ":product/" - + awsAccountId - + "/default", - "GeneratorId": topicarn, - "AwsAccountId": awsAccountId, - "Types": [ - "Software and Configuration Checks/AWS Security Best Practices", - "Effects/Data Exposure", - ], - "FirstObservedAt": iso8601Time, - "CreatedAt": iso8601Time, - "UpdatedAt": iso8601Time, - "Severity": {"Label": "Low"}, - "Confidence": 99, - "Title": "[SNS.4] SNS topics should not allow cross-account access", - "Description": "SNS topic " - + topicName - + " has cross-account access.", - "Remediation": { - "Recommendation": { - "Text": "For more information on SNS best practices refer to the Amazon SNS security best practices section of the Amazon Simple Notification Service Developer Guide.", - "Url": "https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit", - } + fail = True + break + if not fail: + finding = { + "SchemaVersion": "2018-10-08", + "Id": topicarn + "/sns-cross-account-check", + "ProductArn": "arn:aws-us-gov:securityhub:" + + awsRegion + + ":" + + awsAccountId + + ":product/" + + awsAccountId + + "/default", + "GeneratorId": topicarn, + "AwsAccountId": awsAccountId, + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Data Exposure", + ], + "FirstObservedAt": iso8601Time, + "CreatedAt": iso8601Time, + "UpdatedAt": iso8601Time, + "Severity": {"Label": "INFORMATIONAL"}, + "Confidence": 99, + "Title": "[SNS.4] SNS topics should not allow cross-account access", + "Description": "SNS topic " + + topicName + + " does not have cross-account access.", + "Remediation": { + "Recommendation": { + "Text": "For more information on SNS best practices refer to the Amazon SNS security best practices section of the Amazon Simple Notification Service Developer Guide.", + "Url": "https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit", + } + }, + "ProductFields": {"Product Name": "ElectricEye"}, + "Resources": [ + { + "Type": "AwsSnsTopic", + "Id": topicarn, + "Partition": "aws-us-gov", + "Region": awsRegion, + "Details": { + "AwsSnsTopic": {"TopicName": topicName} }, - "ProductFields": {"Product Name": "ElectricEye"}, - "Resources": [ - { - "Type": "AwsSnsTopic", - "Id": topicarn, - "Partition": "aws-us-gov", - "Region": awsRegion, - "Details": { - "AwsSnsTopic": {"TopicName": topicName} - }, - } - ], - "Compliance": { - "Status": "FAILED", - "RelatedRequirements": [ - "NIST CSF PR.AC-3", - "NIST SP 800-53 AC-1", - "NIST SP 800-53 AC-17", - "NIST SP 800-53 AC-19", - "NIST SP 800-53 AC-20", - "NIST SP 800-53 SC-15", - "AICPA TSC CC6.6", - "ISO 27001:2013 A.6.2.1", - "ISO 27001:2013 A.6.2.2", - "ISO 27001:2013 A.11.2.6", - "ISO 27001:2013 A.13.1.1", - "ISO 27001:2013 A.13.2.1", - ], + } + ], + "Compliance": { + "Status": "PASSED", + "RelatedRequirements": [ + "NIST CSF PR.AC-3", + "NIST SP 800-53 AC-1", + "NIST SP 800-53 AC-17", + "NIST SP 800-53 AC-19", + "NIST SP 800-53 AC-20", + "NIST SP 800-53 SC-15", + "AICPA TSC CC6.6", + "ISO 27001:2013 A.6.2.1", + "ISO 27001:2013 A.6.2.2", + "ISO 27001:2013 A.11.2.6", + "ISO 27001:2013 A.13.1.1", + "ISO 27001:2013 A.13.2.1", + ], + }, + "Workflow": {"Status": "RESOLVED"}, + "RecordState": "ARCHIVED", + } + yield finding + else: + finding = { + "SchemaVersion": "2018-10-08", + "Id": topicarn + "/sns-cross-account-check", + "ProductArn": "arn:aws-us-gov:securityhub:" + + awsRegion + + ":" + + awsAccountId + + ":product/" + + awsAccountId + + "/default", + "GeneratorId": topicarn, + "AwsAccountId": awsAccountId, + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Data Exposure", + ], + "FirstObservedAt": iso8601Time, + "CreatedAt": iso8601Time, + "UpdatedAt": iso8601Time, + "Severity": {"Label": "Low"}, + "Confidence": 99, + "Title": "[SNS.4] SNS topics should not allow cross-account access", + "Description": "SNS topic " + + topicName + + " has cross-account access.", + "Remediation": { + "Recommendation": { + "Text": "For more information on SNS best practices refer to the Amazon SNS security best practices section of the Amazon Simple Notification Service Developer Guide.", + "Url": "https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit", + } + }, + "ProductFields": {"Product Name": "ElectricEye"}, + "Resources": [ + { + "Type": "AwsSnsTopic", + "Id": topicarn, + "Partition": "aws-us-gov", + "Region": awsRegion, + "Details": { + "AwsSnsTopic": {"TopicName": topicName} }, - "Workflow": {"Status": "NEW"}, - "RecordState": "ACTIVE", } - yield finding + ], + "Compliance": { + "Status": "FAILED", + "RelatedRequirements": [ + "NIST CSF PR.AC-3", + "NIST SP 800-53 AC-1", + "NIST SP 800-53 AC-17", + "NIST SP 800-53 AC-19", + "NIST SP 800-53 AC-20", + "NIST SP 800-53 SC-15", + "AICPA TSC CC6.6", + "ISO 27001:2013 A.6.2.1", + "ISO 27001:2013 A.6.2.2", + "ISO 27001:2013 A.11.2.6", + "ISO 27001:2013 A.13.1.1", + "ISO 27001:2013 A.13.2.1", + ], + }, + "Workflow": {"Status": "NEW"}, + "RecordState": "ACTIVE", + } + yield finding diff --git a/govcloud-auditors/tests/test_Amazon_SNS_Auditor.py b/govcloud-auditors/tests/test_Amazon_SNS_Auditor.py index d355b047..b8e0346c 100644 --- a/govcloud-auditors/tests/test_Amazon_SNS_Auditor.py +++ b/govcloud-auditors/tests/test_Amazon_SNS_Auditor.py @@ -26,6 +26,12 @@ "Topics": [{"TopicArn": "arn:aws-us-gov:sns:us-east-1:012345678901:MyTopic"},], } +get_topic_attributes_no_AWS = { + "Attributes": { + "Policy": '{"Version": "2008-10-17","Id": "__default_policy_ID","Statement": [{"Sid": "__default_statement_ID","Effect": "Allow","Principal": {"AWS": "*"},"Action": ["SNS:GetTopicAttributes","SNS:SetTopicAttributes","SNS:AddPermission","SNS:RemovePermission","SNS:DeleteTopic","SNS:Subscribe","SNS:ListSubscriptionsByTopic","SNS:Publish","SNS:Receive"],"Resource": "arn:aws:sns:us-east-1:012345678901:cloudtrail-sns","Condition": {"StringEquals": {"AWS:SourceOwner": "012345678901"}}},{"Sid": "AWSCloudTrailSNSPolicy20150319","Effect": "Allow","Principal": {"Service": "cloudtrail.amazonaws.com"},"Action": "SNS:Publish","Resource": "arn:aws:sns:us-east-1:012345678901:cloudtrail-sns"}]}' + } +} + get_topic_attributes_arn_response = { "Attributes": { "Policy": '{"Statement":[{"Principal":{"AWS":"arn:aws-us-gov:iam::012345678901:root"},"Condition":{"StringEquals":{"aws-us-gov:SourceOwner":"012345678901"}}}]}', @@ -45,19 +51,19 @@ get_topic_attributes_response1 = { "Attributes": { - "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"arn:aws-us-gov:iam::012345678901:root"},"Action":["SNS:Publish","SNS:RemovePermission","SNS:SetTopicAttributes","SNS:DeleteTopic","SNS:ListSubscriptionsByTopic","SNS:GetTopicAttributes","SNS:Receive","SNS:AddPermission","SNS:Subscribe"],"Resource":"arn:aws-us-gov:sns:us-east-1:012345678901:Test"}]}' + "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"arn:aws-us-gov:iam::012345678901:root"},"Action":["SNS:Publish","SNS:RemovePermission","SNS:SetTopicAttributes","SNS:DeleteTopic","SNS:ListSubscriptionsByTopic","SNS:GetTopicAttributes","SNS:Receive","SNS:AddPermission","SNS:Subscribe"],"Resource":"arn:aws-us-gov:sns:us-east-1:012345678901:MyTopic"}]}' } } get_topic_attributes_response2 = { "Attributes": { - "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:GetTopicAttributes","SNS:SetTopicAttributes","SNS:AddPermission","SNS:RemovePermission","SNS:DeleteTopic","SNS:Subscribe","SNS:ListSubscriptionsByTopic","SNS:Publish","SNS:Receive"],"Resource":"arn:aws-us-gov:sns:us-east-1:012345678901:Test","Condition":{"StringEquals":{"aws-us-gov:SourceOwner":"012345678901"}}}]}' + "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:GetTopicAttributes","SNS:SetTopicAttributes","SNS:AddPermission","SNS:RemovePermission","SNS:DeleteTopic","SNS:Subscribe","SNS:ListSubscriptionsByTopic","SNS:Publish","SNS:Receive"],"Resource":"arn:aws-us-gov:sns:us-east-1:012345678901:MyTopic","Condition":{"StringEquals":{"aws-us-gov:SourceOwner":"012345678901"}}}]}' } } get_topic_attributes_response3 = { "Attributes": { - "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:Publish","SNS:RemovePermission","SNS:SetTopicAttributes","SNS:DeleteTopic","SNS:ListSubscriptionsByTopic","SNS:GetTopicAttributes","SNS:Receive","SNS:AddPermission","SNS:Subscribe"],"Resource":"arn:aws-us-gov:sns:us-east-1:012345678901:Test","Condition":{"StringEquals":{"aws-us-gov:SourceOwner":"012345678901"}}},{"Sid":"__console_pub_0","Effect":"Allow","Principal":{"AWS":"*"},"Action":"SNS:Publish","Resource":"arn:aws-us-gov:sns:us-east-1:012345678901:Test"},{"Sid":"__console_sub_0","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:Subscribe","SNS:Receive"],"Resource":"arn:aws-us-gov:sns:us-east-1:012345678901:Test"}]}' + "Policy": '{"Version":"2008-10-17","Id":"__default_policy_ID","Statement":[{"Sid":"__default_statement_ID","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:Publish","SNS:RemovePermission","SNS:SetTopicAttributes","SNS:DeleteTopic","SNS:ListSubscriptionsByTopic","SNS:GetTopicAttributes","SNS:Receive","SNS:AddPermission","SNS:Subscribe"],"Resource":"arn:aws-us-gov:sns:us-east-1:012345678901:MyTopic","Condition":{"StringEquals":{"aws-us-gov:SourceOwner":"012345678901"}}},{"Sid":"__console_pub_0","Effect":"Allow","Principal":{"AWS":"*"},"Action":"SNS:Publish","Resource":"arn:aws-us-gov:sns:us-east-1:012345678901:MyTopic"},{"Sid":"__console_sub_0","Effect":"Allow","Principal":{"AWS":"*"},"Action":["SNS:Subscribe","SNS:Receive"],"Resource":"arn:aws-us-gov:sns:us-east-1:012345678901:MyTopic"}]}' } } @@ -124,6 +130,20 @@ def test_id_not_principal(sns_stubber, sts_stubber): assert result["RecordState"] == "ACTIVE" sns_stubber.assert_no_pending_responses() +def test_no_AWS(sns_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + sns_stubber.add_response("list_topics", list_topics_response) + sns_stubber.add_response( + "get_topic_attributes", get_topic_attributes_no_AWS + ) + check = SNSCrossAccountCheck() + results = check.execute() + for result in results: + if "MyTopic" in result["Id"]: + assert result["RecordState"] == "ARCHIVED" + else: + assert False + sns_stubber.assert_no_pending_responses() def test_no_access(sts_stubber, sns_stubber): sts_stubber.add_response("get_caller_identity", sts_response) @@ -132,7 +152,7 @@ def test_no_access(sts_stubber, sns_stubber): check = SNSPublicAccessCheck() results = check.execute() for result in results: - if "Test" in result["Id"]: + if "MyTopic" in result["Id"]: assert result["RecordState"] == "ARCHIVED" else: assert False @@ -146,7 +166,7 @@ def test_has_a_condition(sts_stubber, sns_stubber): check = SNSPublicAccessCheck() results = check.execute() for result in results: - if "Test" in result["Id"]: + if "MyTopic" in result["Id"]: assert result["RecordState"] == "ARCHIVED" else: assert False @@ -160,7 +180,22 @@ def test_has_public_access(sts_stubber, sns_stubber): check = SNSPublicAccessCheck() results = check.execute() for result in results: - if "Test" in result["Id"]: + if "MyTopic" in result["Id"]: + assert result["RecordState"] == "ACTIVE" + else: + assert False + sns_stubber.assert_no_pending_responses() + +def test_no_AWS_Public(sns_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + sns_stubber.add_response("list_topics", list_topics_response) + sns_stubber.add_response( + "get_topic_attributes", get_topic_attributes_no_AWS + ) + check = SNSPublicAccessCheck() + results = check.execute() + for result in results: + if "MyTopic" in result["Id"]: assert result["RecordState"] == "ARCHIVED" else: assert False From 8b8bc35ad8012a5cc0b9f9240a111356ff1bb89c Mon Sep 17 00:00:00 2001 From: Patrick Brazil Date: Mon, 15 Jun 2020 10:23:34 -0500 Subject: [PATCH 02/11] new kms auditor --- auditors/AWS_KMS_Auditor.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 auditors/AWS_KMS_Auditor.py diff --git a/auditors/AWS_KMS_Auditor.py b/auditors/AWS_KMS_Auditor.py new file mode 100644 index 00000000..21bd7334 --- /dev/null +++ b/auditors/AWS_KMS_Auditor.py @@ -0,0 +1,32 @@ +# This file is part of ElectricEye. + +# ElectricEye is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ElectricEye is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with ElectricEye. +# If not, see https://github.com/jonrau1/ElectricEye/blob/master/LICENSE. + +import boto3 +import datetime +import os +from auditors.Auditor import Auditor + +# import boto3 clients +sts = boto3.client("sts") +kms = boto3.client("kms") +# create env vars +awsRegion = os.environ["AWS_REGION"] +awsAccountId = sts.get_caller_identity()["Account"] + +class KeyRotationCheck(Auditor): + def execute(self): + +class KeyExposedCheck(Auditor): + def execute(self): \ No newline at end of file From 4c71310808c9bd6f64daae3d5a10eb581de22eee Mon Sep 17 00:00:00 2001 From: aclatham Date: Mon, 15 Jun 2020 12:19:15 -0600 Subject: [PATCH 03/11] Add kms check and test --- auditors/AWS_KMS_Auditor.py | 127 ++++++++++++++++++++++++- auditors/tests/test_AWS_KMS_Auditor.py | 31 ++++++ 2 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 auditors/tests/test_AWS_KMS_Auditor.py diff --git a/auditors/AWS_KMS_Auditor.py b/auditors/AWS_KMS_Auditor.py index 21bd7334..81d0746c 100644 --- a/auditors/AWS_KMS_Auditor.py +++ b/auditors/AWS_KMS_Auditor.py @@ -10,11 +10,12 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# You should have received a copy of the GNU General Public License along with ElectricEye. +# You should have received a copy of the GNU General Public License along with ElectricEye. # If not, see https://github.com/jonrau1/ElectricEye/blob/master/LICENSE. import boto3 import datetime +import json import os from auditors.Auditor import Auditor @@ -22,11 +23,127 @@ sts = boto3.client("sts") kms = boto3.client("kms") # create env vars -awsRegion = os.environ["AWS_REGION"] awsAccountId = sts.get_caller_identity()["Account"] +awsRegion = os.environ["AWS_REGION"] + -class KeyRotationCheck(Auditor): +class KMSKeyRotationCheck(Auditor): def execute(self): -class KeyExposedCheck(Auditor): - def execute(self): \ No newline at end of file + +class KMSKeyExposedCheck(Auditor): + def execute(self): + response = kms.list_aliases() + aliasList = response["Aliases"] + for alias in aliasList: + if "TargetKeyId" in alias: + aliasArn = alias["AliasArn"] + keyid = alias["TargetKeyId"] + policyString = kms.get_key_policy(KeyId=keyid, PolicyName="default") + fail = False + policy_json = policyString["Policy"] + policy = json.loads(policy_json) + iso8601Time = ( + datetime.datetime.utcnow() + .replace(tzinfo=datetime.timezone.utc) + .isoformat() + ) + for sid in policy["Statement"]: + access = sid["Principal"].get("AWS", None) + if access != "*" or (access == "*" and "Condition" in sid): + continue + else: + fail = True + break + if not fail: + finding = { + "SchemaVersion": "2018-10-08", + "Id": aliasArn + "/kms-key-exposed-check", + "ProductArn": "arn:aws:securityhub:" + + awsRegion + + ":" + + awsAccountId + + ":product/" + + awsAccountId + + "/default", + "GeneratorId": aliasArn, + "AwsAccountId": awsAccountId, + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Data Exposure", + ], + "FirstObservedAt": iso8601Time, + "CreatedAt": iso8601Time, + "UpdatedAt": iso8601Time, + "Severity": {"Label": "INFORMATIONAL"}, + "Confidence": 75, # The Condition may not effectively limit access + "Title": "[KMS.2] KMS keys should not have public access", + "Description": "KMS key " + + keyid + + " does not have public access or limited by a Condition. Refer to the remediation instructions to review kms access policy", + "Remediation": { + "Recommendation": { + "Text": "For more information on AWS KMS key policies refer to Using key policies in AWS KMS section of the AWS KMS Developer Guide.", + "Url": "https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html", + } + }, + "ProductFields": {"Product Name": "ElectricEye"}, + "Resources": [ + { + "Type": "AwsKmsAlias", + "Id": aliasArn, + "Partition": "aws", + "Region": awsRegion, + } + ], + "Compliance": {"Status": "PASSED"}, + "Workflow": {"Status": "RESOLVED"}, + "RecordState": "ARCHIVED", + } + yield finding + else: + finding = { + "SchemaVersion": "2018-10-08", + "Id": aliasArn + "/kms-key-exposed-check", + "ProductArn": "arn:aws:securityhub:" + + awsRegion + + ":" + + awsAccountId + + ":product/" + + awsAccountId + + "/default", + "GeneratorId": aliasArn, + "AwsAccountId": awsAccountId, + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Data Exposure", + ], + "FirstObservedAt": iso8601Time, + "CreatedAt": iso8601Time, + "UpdatedAt": iso8601Time, + "Severity": {"Label": "HIGH"}, + "Confidence": 99, + "Title": "[KMS.2] KMS keys should not have public access", + "Description": "KMS key " + + keyid + + " has public access. Refer to the remediation instructions to review kms access policy", + "Remediation": { + "Recommendation": { + "Text": "For more information on AWS KMS key policies refer to Using key policies in AWS KMS section of the AWS KMS Developer Guide.", + "Url": "https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html", + } + }, + "ProductFields": {"Product Name": "ElectricEye"}, + "Resources": [ + { + "Type": "AwsKmsAlias", + "Id": aliasArn, + "Partition": "aws", + "Region": awsRegion, + } + ], + "Compliance": {"Status": "FAILED"}, + "Workflow": {"Status": "NEW"}, + "RecordState": "ACTIVE", + } + yield finding diff --git a/auditors/tests/test_AWS_KMS_Auditor.py b/auditors/tests/test_AWS_KMS_Auditor.py new file mode 100644 index 00000000..16344183 --- /dev/null +++ b/auditors/tests/test_AWS_KMS_Auditor.py @@ -0,0 +1,31 @@ +import json +import os +import pytest +from botocore.stub import Stubber, ANY +from auditors.Amazon_kms_Auditor import ( + KMSKeyRotationCheck, + KMSKeyExposedCheck, + sts, + kms, +) + +# not available in local testing without ECS +os.environ["AWS_REGION"] = "us-east-1" +# for local testing, don't assume default profile exists +os.environ["AWS_DEFAULT_REGION"] = "us-east-1" + + +@pytest.fixture(scope="function") +def sts_stubber(): + sts_stubber = Stubber(sts) + sts_stubber.activate() + yield sts_stubber + sts_stubber.deactivate() + + +@pytest.fixture(scope="function") +def kms_stubber(): + kms_stubber = Stubber(kms) + kms_stubber.activate() + yield kms_stubber + kms_stubber.deactivate() From d6f6ec2ac909afd39ffe6da202198229cdc640ea Mon Sep 17 00:00:00 2001 From: aclatham Date: Mon, 15 Jun 2020 13:24:57 -0600 Subject: [PATCH 04/11] Add more tests to kms --- auditors/AWS_KMS_Auditor.py | 4 +- auditors/tests/test_AWS_KMS_Auditor.py | 90 +++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/auditors/AWS_KMS_Auditor.py b/auditors/AWS_KMS_Auditor.py index 81d0746c..19b5b5eb 100644 --- a/auditors/AWS_KMS_Auditor.py +++ b/auditors/AWS_KMS_Auditor.py @@ -23,16 +23,16 @@ sts = boto3.client("sts") kms = boto3.client("kms") # create env vars -awsAccountId = sts.get_caller_identity()["Account"] awsRegion = os.environ["AWS_REGION"] class KMSKeyRotationCheck(Auditor): - def execute(self): + def execute(self): class KMSKeyExposedCheck(Auditor): def execute(self): + awsAccountId = sts.get_caller_identity()["Account"] response = kms.list_aliases() aliasList = response["Aliases"] for alias in aliasList: diff --git a/auditors/tests/test_AWS_KMS_Auditor.py b/auditors/tests/test_AWS_KMS_Auditor.py index 16344183..5751b893 100644 --- a/auditors/tests/test_AWS_KMS_Auditor.py +++ b/auditors/tests/test_AWS_KMS_Auditor.py @@ -2,8 +2,8 @@ import os import pytest from botocore.stub import Stubber, ANY -from auditors.Amazon_kms_Auditor import ( - KMSKeyRotationCheck, +from auditors.AWS_KMS_Auditor import ( + # KMSKeyRotationCheck, KMSKeyExposedCheck, sts, kms, @@ -14,6 +14,36 @@ # for local testing, don't assume default profile exists os.environ["AWS_DEFAULT_REGION"] = "us-east-1" +sts_response = { + "Account": "012345678901", + "Arn": "arn:aws:iam::012345678901:user/user", +} + +list_aliases_response = { + "Aliases": [ + { + "AliasArn": "arn:aws:kms:us-east-1:012345678901:alias/aws/s3", + "TargetKeyId": "c84a8fab-6c42-4b33-ad64-a8e0b0ec0a15", + }, + ], +} + +get_key_policy_public_response = { + "Policy": '{"Version": "2012-10-17","Id": "KeyPolicy1568312239560","Statement": [{"Sid": "StmtID1672312238115","Effect": "Allow","Principal": {"AWS": "*"},"Action": "kms:*","Resource": "*"}]}' +} + +get_key_policy_not_public_response = { + "Policy": '{"Version": "2012-10-17","Id": "KeyPolicy1568312239560","Statement": [{"Sid": "StmtID1672312238115","Effect": "Allow","Principal": {"AWS": "012345678901"},"Action": "kms:*","Resource": "*"}]}' +} + +get_key_policy_has_condition_response = { + "Policy": '{"Version": "2012-10-17","Id": "KeyPolicy1568312239560","Statement": [{"Sid": "StmtID1672312238115","Effect": "Allow","Principal": {"AWS": "*"},"Action": "kms:*","Resource": "*","Condition": {"StringEquals": {"kms:CallerAccount": "012345678901","kms:ViaService": "sns.us-east-1.amazonaws.com"}}}]}' +} + +get_key_policy_no_AWS_response = { + "Policy": '{"Version": "2012-10-17","Id": "KeyPolicy1568312239560","Statement": [{"Sid": "StmtID1672312238115","Effect": "Allow","Principal": {"Service": "cloudtrail.amazonaws.com"},"Action": "kms:*","Resource": "*","Condition": {"StringEquals": {"kms:CallerAccount": "012345678901","kms:ViaService": "sns.us-east-1.amazonaws.com"}}}]}' +} + @pytest.fixture(scope="function") def sts_stubber(): @@ -29,3 +59,59 @@ def kms_stubber(): kms_stubber.activate() yield kms_stubber kms_stubber.deactivate() + + +def test_has_public_key(kms_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_aliases", list_aliases_response) + kms_stubber.add_response("get_key_policy", get_key_policy_public_response) + check = KMSKeyExposedCheck() + results = check.execute() + for result in results: + if "s3" in result["Id"]: + assert result["RecordState"] == "ACTIVE" + else: + assert False + kms_stubber.assert_no_pending_responses() + + +def test_no_public_key(kms_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_aliases", list_aliases_response) + kms_stubber.add_response("get_key_policy", get_key_policy_not_public_response) + check = KMSKeyExposedCheck() + results = check.execute() + for result in results: + if "s3" in result["Id"]: + assert result["RecordState"] == "ARCHIVED" + else: + assert False + kms_stubber.assert_no_pending_responses() + + +def test_has_condition(kms_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_aliases", list_aliases_response) + kms_stubber.add_response("get_key_policy", get_key_policy_has_condition_response) + check = KMSKeyExposedCheck() + results = check.execute() + for result in results: + if "s3" in result["Id"]: + assert result["RecordState"] == "ARCHIVED" + else: + assert False + kms_stubber.assert_no_pending_responses() + + +def test_no_AWS(kms_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_aliases", list_aliases_response) + kms_stubber.add_response("get_key_policy", get_key_policy_no_AWS_response) + check = KMSKeyExposedCheck() + results = check.execute() + for result in results: + if "s3" in result["Id"]: + assert result["RecordState"] == "ARCHIVED" + else: + assert False + kms_stubber.assert_no_pending_responses() From 6ea8aadcd2db720477bb4a0058ce12f8c010430b Mon Sep 17 00:00:00 2001 From: Patrick Brazil Date: Mon, 15 Jun 2020 14:28:08 -0500 Subject: [PATCH 05/11] kms auditor fixes --- auditors/AWS_KMS_Auditor.py | 108 ++++++++++++++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 5 deletions(-) diff --git a/auditors/AWS_KMS_Auditor.py b/auditors/AWS_KMS_Auditor.py index 81d0746c..dd2bb890 100644 --- a/auditors/AWS_KMS_Auditor.py +++ b/auditors/AWS_KMS_Auditor.py @@ -22,17 +22,115 @@ # import boto3 clients sts = boto3.client("sts") kms = boto3.client("kms") -# create env vars -awsAccountId = sts.get_caller_identity()["Account"] -awsRegion = os.environ["AWS_REGION"] - class KMSKeyRotationCheck(Auditor): def execute(self): - + awsAccountId = sts.get_caller_identity()["Account"] + awsRegion = os.environ["AWS_REGION"] + keys = kms.list_keys() + my_keys = keys["Keys"] + for key in my_keys: + keyid = key["KeyId"] + keyarn = key["KeyArn"] + key_rotation = kms.get_key_rotation_status(KeyId=keyid) + iso8601Time = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat() + if key_rotation["KeyRotationEnabled"] == True: + finding = { + "SchemaVersion": "2018-10-08", + "Id": keyarn + "/kms-key-rotation-check", + "ProductArn": "arn:aws:securityhub:" + + awsRegion + + ":" + + awsAccountId + + ":product/" + + awsAccountId + + "/default", + "GeneratorId": keyarn, + "AwsAccountId": awsAccountId, + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices", + ], + "FirstObservedAt": iso8601Time, + "CreatedAt": iso8601Time, + "UpdatedAt": iso8601Time, + "Severity": {"Label": "INFORMATIONAL"}, + "Confidence": 99, + "Title": "[KMS.1] KMS keys should have key rotation enabled", + "Description": "KMS Key " + + keyid + + " does have key rotation enabled.", + "Remediation": { + "Recommendation": { + "Text": "For more information on KMS key rotation refer to the AWS KMS Developer Guide on Rotating Keys", + "Url": "https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html", + } + }, + "ProductFields": {"Product Name": "ElectricEye"}, + "Resources": [ + { + "Type": "AwsKmsKey", + "Id": keyarn, + "Partition": "aws", + "Region": awsRegion, + "Details": {"AwsKmsKey": {"KeyId": keyid}}, + } + ], + "Compliance": { + "Status": "PASSED" + }, + "Workflow": {"Status": "RESOLVED"}, + "RecordState": "ARCHIVED", + } + yield finding + else: + finding = { + 'SchemaVersion': '2018-10-08', + 'Id': keyarn + '/kms-key-rotation-check', + 'ProductArn': 'arn:aws:securityhub:' + awsRegion + ':' + awsAccountId + ':product/' + awsAccountId + '/default', + 'GeneratorId': keyarn, + 'AwsAccountId': awsAccountId, + 'Types': [ 'Software and Configuration Checks/AWS Security Best Practices' ], + 'FirstObservedAt': iso8601Time, + 'CreatedAt': iso8601Time, + 'UpdatedAt': iso8601Time, + 'Severity': { 'Label': 'MEDIUM' }, + 'Confidence': 99, + 'Title': '[KMS.1] KMS keys should have key rotation enabled', + 'Description': 'KMS key ' + keyid + ' does not have key rotation enabled.', + 'Remediation': { + 'Recommendation': { + 'Text': 'For more information on KMS key rotation refer to the AWS KMS Developer Guide on Rotating Keys', + 'Url': 'https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html' + } + }, + 'ProductFields': { + 'Product Name': 'ElectricEye' + }, + 'Resources': [ + { + 'Type': 'AwsKmsKey', + 'Id': keyarn, + 'Partition': 'aws', + 'Region': awsRegion, + 'Details': { + 'AwsKmsKey': { 'KeyId': keyid } + } + } + ], + 'Compliance': { + 'Status': 'FAILED' + }, + 'Workflow': { + 'Status': 'NEW' + }, + 'RecordState': 'ACTIVE' + } + yield finding class KMSKeyExposedCheck(Auditor): def execute(self): + awsAccountId = sts.get_caller_identity()["Account"] + awsRegion = os.environ["AWS_REGION"] response = kms.list_aliases() aliasList = response["Aliases"] for alias in aliasList: From fbeaedf1c41563e62668ed5351e471d7569e4890 Mon Sep 17 00:00:00 2001 From: Patrick Brazil Date: Mon, 15 Jun 2020 14:36:54 -0500 Subject: [PATCH 06/11] added tests to kms --- auditors/tests/test_AWS_KMS_Auditor.py | 48 ++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/auditors/tests/test_AWS_KMS_Auditor.py b/auditors/tests/test_AWS_KMS_Auditor.py index 5751b893..afb1b3e1 100644 --- a/auditors/tests/test_AWS_KMS_Auditor.py +++ b/auditors/tests/test_AWS_KMS_Auditor.py @@ -1,9 +1,10 @@ +import datetime import json import os import pytest from botocore.stub import Stubber, ANY from auditors.AWS_KMS_Auditor import ( - # KMSKeyRotationCheck, + KMSKeyRotationCheck, KMSKeyExposedCheck, sts, kms, @@ -44,6 +45,22 @@ "Policy": '{"Version": "2012-10-17","Id": "KeyPolicy1568312239560","Statement": [{"Sid": "StmtID1672312238115","Effect": "Allow","Principal": {"Service": "cloudtrail.amazonaws.com"},"Action": "kms:*","Resource": "*","Condition": {"StringEquals": {"kms:CallerAccount": "012345678901","kms:ViaService": "sns.us-east-1.amazonaws.com"}}}]}' } +list_keys_response = { + "Keys": [ + { + "KeyId": "273e5d8e-4746-4ba9-be3a-4dce36783814", + "KeyArn": "arn:aws:kms:us-east-1:012345678901:key/273e5d8e-4746-4ba9-be3a-4dce36783814" + } + ] +} + +get_key_rotation_status_response = { + "KeyRotationEnabled": True +} + +get_key_rotation_status_response1 = { + "KeyRotationEnabled": False +} @pytest.fixture(scope="function") def sts_stubber(): @@ -83,11 +100,25 @@ def test_no_public_key(kms_stubber, sts_stubber): results = check.execute() for result in results: if "s3" in result["Id"]: + print(result["Id"]) assert result["RecordState"] == "ARCHIVED" else: assert False kms_stubber.assert_no_pending_responses() +def test_key_rotation_enabled(sts_stubber, kms_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_keys", list_keys_response) + kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response) + check = KMSKeyRotationCheck() + results = check.execute() + for result in results: + if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: + print(result["Id"]) + assert result["RecordState"] == "ARCHIVED" + else: + assert False + kms_stubber.assert_no_pending_responses() def test_has_condition(kms_stubber, sts_stubber): sts_stubber.add_response("get_caller_identity", sts_response) @@ -102,7 +133,6 @@ def test_has_condition(kms_stubber, sts_stubber): assert False kms_stubber.assert_no_pending_responses() - def test_no_AWS(kms_stubber, sts_stubber): sts_stubber.add_response("get_caller_identity", sts_response) kms_stubber.add_response("list_aliases", list_aliases_response) @@ -115,3 +145,17 @@ def test_no_AWS(kms_stubber, sts_stubber): else: assert False kms_stubber.assert_no_pending_responses() + +def test_key_rotation_not_enabled(sts_stubber, kms_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_keys", list_keys_response) + kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response1) + check = KMSKeyRotationCheck() + results = check.execute() + for result in results: + if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: + print(result["Id"]) + assert result["RecordState"] == "ACTIVE" + else: + assert False + kms_stubber.assert_no_pending_responses() From d6ba55ea283af27206fa0f1437dcc0aa03d0d350 Mon Sep 17 00:00:00 2001 From: Patrick Brazil Date: Mon, 15 Jun 2020 14:52:25 -0500 Subject: [PATCH 07/11] added kms govcloud auditors --- govcloud-auditors/AWS_KMS_Auditor.py | 247 ++++++++++++++++++ .../tests/test_AWS_KMS_Auditor.py | 161 ++++++++++++ 2 files changed, 408 insertions(+) create mode 100644 govcloud-auditors/AWS_KMS_Auditor.py create mode 100644 govcloud-auditors/tests/test_AWS_KMS_Auditor.py diff --git a/govcloud-auditors/AWS_KMS_Auditor.py b/govcloud-auditors/AWS_KMS_Auditor.py new file mode 100644 index 00000000..4c3ef4da --- /dev/null +++ b/govcloud-auditors/AWS_KMS_Auditor.py @@ -0,0 +1,247 @@ +# This file is part of ElectricEye. + +# ElectricEye is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# ElectricEye is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License along with ElectricEye. +# If not, see https://github.com/jonrau1/ElectricEye/blob/master/LICENSE. + +import boto3 +import datetime +import json +import os +from auditors.Auditor import Auditor + +# import boto3 clients +sts = boto3.client("sts") +kms = boto3.client("kms") + +class KMSKeyRotationCheck(Auditor): + def execute(self): + awsAccountId = sts.get_caller_identity()["Account"] + awsRegion = os.environ["AWS_REGION"] + keys = kms.list_keys() + my_keys = keys["Keys"] + for key in my_keys: + keyid = key["KeyId"] + keyarn = key["KeyArn"] + key_rotation = kms.get_key_rotation_status(KeyId=keyid) + iso8601Time = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat() + if key_rotation["KeyRotationEnabled"] == True: + finding = { + "SchemaVersion": "2018-10-08", + "Id": keyarn + "/kms-key-rotation-check", + "ProductArn": "arn:aws-us-gov:securityhub:" + + awsRegion + + ":" + + awsAccountId + + ":product/" + + awsAccountId + + "/default", + "GeneratorId": keyarn, + "AwsAccountId": awsAccountId, + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices", + ], + "FirstObservedAt": iso8601Time, + "CreatedAt": iso8601Time, + "UpdatedAt": iso8601Time, + "Severity": {"Label": "INFORMATIONAL"}, + "Confidence": 99, + "Title": "[KMS.1] KMS keys should have key rotation enabled", + "Description": "KMS Key " + + keyid + + " does have key rotation enabled.", + "Remediation": { + "Recommendation": { + "Text": "For more information on KMS key rotation refer to the AWS KMS Developer Guide on Rotating Keys", + "Url": "https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html", + } + }, + "ProductFields": {"Product Name": "ElectricEye"}, + "Resources": [ + { + "Type": "AwsKmsKey", + "Id": keyarn, + "Partition": "aws-us-gov", + "Region": awsRegion, + "Details": {"AwsKmsKey": {"KeyId": keyid}}, + } + ], + "Compliance": { + "Status": "PASSED" + }, + "Workflow": {"Status": "RESOLVED"}, + "RecordState": "ARCHIVED", + } + yield finding + else: + finding = { + 'SchemaVersion': '2018-10-08', + 'Id': keyarn + '/kms-key-rotation-check', + 'ProductArn': 'arn:aws-us-gov:securityhub:' + awsRegion + ':' + awsAccountId + ':product/' + awsAccountId + '/default', + 'GeneratorId': keyarn, + 'AwsAccountId': awsAccountId, + 'Types': [ 'Software and Configuration Checks/AWS Security Best Practices' ], + 'FirstObservedAt': iso8601Time, + 'CreatedAt': iso8601Time, + 'UpdatedAt': iso8601Time, + 'Severity': { 'Label': 'MEDIUM' }, + 'Confidence': 99, + 'Title': '[KMS.1] KMS keys should have key rotation enabled', + 'Description': 'KMS key ' + keyid + ' does not have key rotation enabled.', + 'Remediation': { + 'Recommendation': { + 'Text': 'For more information on KMS key rotation refer to the AWS KMS Developer Guide on Rotating Keys', + 'Url': 'https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html' + } + }, + 'ProductFields': { + 'Product Name': 'ElectricEye' + }, + 'Resources': [ + { + 'Type': 'AwsKmsKey', + 'Id': keyarn, + 'Partition': 'aws-us-gov', + 'Region': awsRegion, + 'Details': { + 'AwsKmsKey': { 'KeyId': keyid } + } + } + ], + 'Compliance': { + 'Status': 'FAILED' + }, + 'Workflow': { + 'Status': 'NEW' + }, + 'RecordState': 'ACTIVE' + } + yield finding + +class KMSKeyExposedCheck(Auditor): + def execute(self): + awsAccountId = sts.get_caller_identity()["Account"] + awsRegion = os.environ["AWS_REGION"] + response = kms.list_aliases() + aliasList = response["Aliases"] + for alias in aliasList: + if "TargetKeyId" in alias: + aliasArn = alias["AliasArn"] + keyid = alias["TargetKeyId"] + policyString = kms.get_key_policy(KeyId=keyid, PolicyName="default") + fail = False + policy_json = policyString["Policy"] + policy = json.loads(policy_json) + iso8601Time = ( + datetime.datetime.utcnow() + .replace(tzinfo=datetime.timezone.utc) + .isoformat() + ) + for sid in policy["Statement"]: + access = sid["Principal"].get("AWS", None) + if access != "*" or (access == "*" and "Condition" in sid): + continue + else: + fail = True + break + if not fail: + finding = { + "SchemaVersion": "2018-10-08", + "Id": aliasArn + "/kms-key-exposed-check", + "ProductArn": "arn:aws-us-gov:securityhub:" + + awsRegion + + ":" + + awsAccountId + + ":product/" + + awsAccountId + + "/default", + "GeneratorId": aliasArn, + "AwsAccountId": awsAccountId, + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Data Exposure", + ], + "FirstObservedAt": iso8601Time, + "CreatedAt": iso8601Time, + "UpdatedAt": iso8601Time, + "Severity": {"Label": "INFORMATIONAL"}, + "Confidence": 75, # The Condition may not effectively limit access + "Title": "[KMS.2] KMS keys should not have public access", + "Description": "KMS key " + + keyid + + " does not have public access or limited by a Condition. Refer to the remediation instructions to review kms access policy", + "Remediation": { + "Recommendation": { + "Text": "For more information on AWS KMS key policies refer to Using key policies in AWS KMS section of the AWS KMS Developer Guide.", + "Url": "https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html", + } + }, + "ProductFields": {"Product Name": "ElectricEye"}, + "Resources": [ + { + "Type": "AwsKmsAlias", + "Id": aliasArn, + "Partition": "aws-us-gov", + "Region": awsRegion, + } + ], + "Compliance": {"Status": "PASSED"}, + "Workflow": {"Status": "RESOLVED"}, + "RecordState": "ARCHIVED", + } + yield finding + else: + finding = { + "SchemaVersion": "2018-10-08", + "Id": aliasArn + "/kms-key-exposed-check", + "ProductArn": "arn:aws-us-gov:securityhub:" + + awsRegion + + ":" + + awsAccountId + + ":product/" + + awsAccountId + + "/default", + "GeneratorId": aliasArn, + "AwsAccountId": awsAccountId, + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices", + "Effects/Data Exposure", + ], + "FirstObservedAt": iso8601Time, + "CreatedAt": iso8601Time, + "UpdatedAt": iso8601Time, + "Severity": {"Label": "HIGH"}, + "Confidence": 99, + "Title": "[KMS.2] KMS keys should not have public access", + "Description": "KMS key " + + keyid + + " has public access. Refer to the remediation instructions to review kms access policy", + "Remediation": { + "Recommendation": { + "Text": "For more information on AWS KMS key policies refer to Using key policies in AWS KMS section of the AWS KMS Developer Guide.", + "Url": "https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html", + } + }, + "ProductFields": {"Product Name": "ElectricEye"}, + "Resources": [ + { + "Type": "AwsKmsAlias", + "Id": aliasArn, + "Partition": "aws-us-gov", + "Region": awsRegion, + } + ], + "Compliance": {"Status": "FAILED"}, + "Workflow": {"Status": "NEW"}, + "RecordState": "ACTIVE", + } + yield finding diff --git a/govcloud-auditors/tests/test_AWS_KMS_Auditor.py b/govcloud-auditors/tests/test_AWS_KMS_Auditor.py new file mode 100644 index 00000000..afb1b3e1 --- /dev/null +++ b/govcloud-auditors/tests/test_AWS_KMS_Auditor.py @@ -0,0 +1,161 @@ +import datetime +import json +import os +import pytest +from botocore.stub import Stubber, ANY +from auditors.AWS_KMS_Auditor import ( + KMSKeyRotationCheck, + KMSKeyExposedCheck, + sts, + kms, +) + +# not available in local testing without ECS +os.environ["AWS_REGION"] = "us-east-1" +# for local testing, don't assume default profile exists +os.environ["AWS_DEFAULT_REGION"] = "us-east-1" + +sts_response = { + "Account": "012345678901", + "Arn": "arn:aws:iam::012345678901:user/user", +} + +list_aliases_response = { + "Aliases": [ + { + "AliasArn": "arn:aws:kms:us-east-1:012345678901:alias/aws/s3", + "TargetKeyId": "c84a8fab-6c42-4b33-ad64-a8e0b0ec0a15", + }, + ], +} + +get_key_policy_public_response = { + "Policy": '{"Version": "2012-10-17","Id": "KeyPolicy1568312239560","Statement": [{"Sid": "StmtID1672312238115","Effect": "Allow","Principal": {"AWS": "*"},"Action": "kms:*","Resource": "*"}]}' +} + +get_key_policy_not_public_response = { + "Policy": '{"Version": "2012-10-17","Id": "KeyPolicy1568312239560","Statement": [{"Sid": "StmtID1672312238115","Effect": "Allow","Principal": {"AWS": "012345678901"},"Action": "kms:*","Resource": "*"}]}' +} + +get_key_policy_has_condition_response = { + "Policy": '{"Version": "2012-10-17","Id": "KeyPolicy1568312239560","Statement": [{"Sid": "StmtID1672312238115","Effect": "Allow","Principal": {"AWS": "*"},"Action": "kms:*","Resource": "*","Condition": {"StringEquals": {"kms:CallerAccount": "012345678901","kms:ViaService": "sns.us-east-1.amazonaws.com"}}}]}' +} + +get_key_policy_no_AWS_response = { + "Policy": '{"Version": "2012-10-17","Id": "KeyPolicy1568312239560","Statement": [{"Sid": "StmtID1672312238115","Effect": "Allow","Principal": {"Service": "cloudtrail.amazonaws.com"},"Action": "kms:*","Resource": "*","Condition": {"StringEquals": {"kms:CallerAccount": "012345678901","kms:ViaService": "sns.us-east-1.amazonaws.com"}}}]}' +} + +list_keys_response = { + "Keys": [ + { + "KeyId": "273e5d8e-4746-4ba9-be3a-4dce36783814", + "KeyArn": "arn:aws:kms:us-east-1:012345678901:key/273e5d8e-4746-4ba9-be3a-4dce36783814" + } + ] +} + +get_key_rotation_status_response = { + "KeyRotationEnabled": True +} + +get_key_rotation_status_response1 = { + "KeyRotationEnabled": False +} + +@pytest.fixture(scope="function") +def sts_stubber(): + sts_stubber = Stubber(sts) + sts_stubber.activate() + yield sts_stubber + sts_stubber.deactivate() + + +@pytest.fixture(scope="function") +def kms_stubber(): + kms_stubber = Stubber(kms) + kms_stubber.activate() + yield kms_stubber + kms_stubber.deactivate() + + +def test_has_public_key(kms_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_aliases", list_aliases_response) + kms_stubber.add_response("get_key_policy", get_key_policy_public_response) + check = KMSKeyExposedCheck() + results = check.execute() + for result in results: + if "s3" in result["Id"]: + assert result["RecordState"] == "ACTIVE" + else: + assert False + kms_stubber.assert_no_pending_responses() + + +def test_no_public_key(kms_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_aliases", list_aliases_response) + kms_stubber.add_response("get_key_policy", get_key_policy_not_public_response) + check = KMSKeyExposedCheck() + results = check.execute() + for result in results: + if "s3" in result["Id"]: + print(result["Id"]) + assert result["RecordState"] == "ARCHIVED" + else: + assert False + kms_stubber.assert_no_pending_responses() + +def test_key_rotation_enabled(sts_stubber, kms_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_keys", list_keys_response) + kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response) + check = KMSKeyRotationCheck() + results = check.execute() + for result in results: + if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: + print(result["Id"]) + assert result["RecordState"] == "ARCHIVED" + else: + assert False + kms_stubber.assert_no_pending_responses() + +def test_has_condition(kms_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_aliases", list_aliases_response) + kms_stubber.add_response("get_key_policy", get_key_policy_has_condition_response) + check = KMSKeyExposedCheck() + results = check.execute() + for result in results: + if "s3" in result["Id"]: + assert result["RecordState"] == "ARCHIVED" + else: + assert False + kms_stubber.assert_no_pending_responses() + +def test_no_AWS(kms_stubber, sts_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_aliases", list_aliases_response) + kms_stubber.add_response("get_key_policy", get_key_policy_no_AWS_response) + check = KMSKeyExposedCheck() + results = check.execute() + for result in results: + if "s3" in result["Id"]: + assert result["RecordState"] == "ARCHIVED" + else: + assert False + kms_stubber.assert_no_pending_responses() + +def test_key_rotation_not_enabled(sts_stubber, kms_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_keys", list_keys_response) + kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response1) + check = KMSKeyRotationCheck() + results = check.execute() + for result in results: + if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: + print(result["Id"]) + assert result["RecordState"] == "ACTIVE" + else: + assert False + kms_stubber.assert_no_pending_responses() From 9e309528fb245099d5be50130bac09a3d6dd1438 Mon Sep 17 00:00:00 2001 From: aclatham Date: Mon, 15 Jun 2020 14:08:16 -0600 Subject: [PATCH 08/11] Add policy info and update readme --- README.md | 4 +++- cloudformation/ElectricEye_CFN.yaml | 4 ++++ policies/Instance_Profile_IAM_Policy.json | 6 +++++- terraform-config-files/electric_eye.tf | 6 +++++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e8198742..bdd9d0ab 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,7 @@ In this stage we will use the console the manually run the ElectricEye ECS task. 3. Select **Run task**, in the next screen select the hyperlink in the **Task** column and select the **Logs** tab to view the result of the logs. **Note** logs coming to this screen may be delayed, and you may have several auditors report failures due to the lack of in-scope resources. ## Supported Services and Checks -These are the following services and checks perform by each Auditor. There are currently **212** checks supported across **65** AWS services / components using **47** Auditors. There are currently **62** supported response and remediation Playbooks with coverage across **32** AWS services / components supported by [ElectricEye-Response](https://github.com/jonrau1/ElectricEye/blob/master/add-ons/electriceye-response). +These are the following services and checks perform by each Auditor. There are currently **214** checks supported across **66** AWS services / components using **48** Auditors. There are currently **62** supported response and remediation Playbooks with coverage across **32** AWS services / components supported by [ElectricEye-Response](https://github.com/jonrau1/ElectricEye/blob/master/add-ons/electriceye-response). **Regarding Shield Advanced checks:** You must be subscribed to Shield Advanced, be on Business/Enterprise Support and be in us-east-1 to perform all checks. The Shield Adv API only lives in us-east-1, and to have the DRT look at your account you need Biz/Ent support, hence the pre-reqs. @@ -445,6 +445,8 @@ These are the following services and checks perform by each Auditor. There are c | AWS_IAM_Auditor.py | IAM User | Do users have managed policies attached | | AWS_IAM_Auditor.py | Password policy (Account) | Does the IAM password policy meet or exceed
AWS CIS Foundations Benchmark standards | | AWS_IAM_Auditor.py | Server certs (Account) | Are they any Server certificates stored by IAM | +| AWS_KMS_Auditor.py | KMS key | Is key rotation enabled | +| AWS_KMS_Auditor.py | KMS key | Does the key allow public access | | AWS_Lambda_Auditor.py | Lambda function | Has function been used or updated in the last
30 days | | AWS_License_Manager_Auditor | License Manager configuration | Do LM configurations enforce a hard limit on
license consumption | | AWS_Secrets_Manager_Auditor.py | Secrets Manager secret | Is the secret over 90 days old | diff --git a/cloudformation/ElectricEye_CFN.yaml b/cloudformation/ElectricEye_CFN.yaml index 98b41a4d..224f28dd 100644 --- a/cloudformation/ElectricEye_CFN.yaml +++ b/cloudformation/ElectricEye_CFN.yaml @@ -252,6 +252,10 @@ Resources: - kinesis:ListStreams - kms:Decrypt - kms:DescribeKey + - kms:GetKeyPolicy + - kms:GetKeyRotationStatus + - kms:ListAliases + - kms:ListKeys - lambda:ListFunctions - license-manager:GetLicenseConfiguration - license-manager:ListLicenseConfigurations diff --git a/policies/Instance_Profile_IAM_Policy.json b/policies/Instance_Profile_IAM_Policy.json index 83c41403..fd423f41 100644 --- a/policies/Instance_Profile_IAM_Policy.json +++ b/policies/Instance_Profile_IAM_Policy.json @@ -232,7 +232,11 @@ "ecr:GetRepositoryPolicy", "lambda:ListFunctions", "cloudwatch:GetMetricData", - "sns:GetTopicAttributes" + "sns:GetTopicAttributes", + "kms:ListAliases", + "kms:GetKeyPolicy", + "kms:ListKeys", + "kms:GetKeyRotationStatus" ], "Resource": "*" } diff --git a/terraform-config-files/electric_eye.tf b/terraform-config-files/electric_eye.tf index e7d6bcb6..b3ef73e9 100644 --- a/terraform-config-files/electric_eye.tf +++ b/terraform-config-files/electric_eye.tf @@ -338,7 +338,11 @@ resource "aws_iam_role_policy" "Electric_Eye_Task_Role_Policy" { "ecr:GetRepositoryPolicy", "rds:DescribeDBClusterParameterGroups", "lambda:ListFunctions", - "cloudwatch:GetMetricData" + "cloudwatch:GetMetricData", + "kms:ListAliases", + "kms:GetKeyPolicy", + "kms:ListKeys", + "kms:GetKeyRotationStatus" ], "Resource": "*" } From 5a60e89d8f78558375f2240ac43a6d67d96e8811 Mon Sep 17 00:00:00 2001 From: Patrick Brazil Date: Mon, 15 Jun 2020 15:18:30 -0500 Subject: [PATCH 09/11] fixed order on kms tests --- auditors/tests/test_AWS_KMS_Auditor.py | 57 ++++++++++++-------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/auditors/tests/test_AWS_KMS_Auditor.py b/auditors/tests/test_AWS_KMS_Auditor.py index afb1b3e1..8963519e 100644 --- a/auditors/tests/test_AWS_KMS_Auditor.py +++ b/auditors/tests/test_AWS_KMS_Auditor.py @@ -69,7 +69,6 @@ def sts_stubber(): yield sts_stubber sts_stubber.deactivate() - @pytest.fixture(scope="function") def kms_stubber(): kms_stubber = Stubber(kms) @@ -77,43 +76,55 @@ def kms_stubber(): yield kms_stubber kms_stubber.deactivate() +def test_key_rotation_enabled(sts_stubber, kms_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_keys", list_keys_response) + kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response) + check = KMSKeyRotationCheck() + results = check.execute() + for result in results: + if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: + print(result["Id"]) + assert result["RecordState"] == "ARCHIVED" + else: + assert False + kms_stubber.assert_no_pending_responses() -def test_has_public_key(kms_stubber, sts_stubber): +def test_key_rotation_not_enabled(sts_stubber, kms_stubber): sts_stubber.add_response("get_caller_identity", sts_response) - kms_stubber.add_response("list_aliases", list_aliases_response) - kms_stubber.add_response("get_key_policy", get_key_policy_public_response) - check = KMSKeyExposedCheck() + kms_stubber.add_response("list_keys", list_keys_response) + kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response1) + check = KMSKeyRotationCheck() results = check.execute() for result in results: - if "s3" in result["Id"]: + if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: + print(result["Id"]) assert result["RecordState"] == "ACTIVE" else: assert False kms_stubber.assert_no_pending_responses() - -def test_no_public_key(kms_stubber, sts_stubber): +def test_has_public_key(kms_stubber, sts_stubber): sts_stubber.add_response("get_caller_identity", sts_response) kms_stubber.add_response("list_aliases", list_aliases_response) - kms_stubber.add_response("get_key_policy", get_key_policy_not_public_response) + kms_stubber.add_response("get_key_policy", get_key_policy_public_response) check = KMSKeyExposedCheck() results = check.execute() for result in results: if "s3" in result["Id"]: - print(result["Id"]) - assert result["RecordState"] == "ARCHIVED" + assert result["RecordState"] == "ACTIVE" else: assert False kms_stubber.assert_no_pending_responses() -def test_key_rotation_enabled(sts_stubber, kms_stubber): +def test_no_public_key(kms_stubber, sts_stubber): sts_stubber.add_response("get_caller_identity", sts_response) - kms_stubber.add_response("list_keys", list_keys_response) - kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response) - check = KMSKeyRotationCheck() + kms_stubber.add_response("list_aliases", list_aliases_response) + kms_stubber.add_response("get_key_policy", get_key_policy_not_public_response) + check = KMSKeyExposedCheck() results = check.execute() for result in results: - if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: + if "s3" in result["Id"]: print(result["Id"]) assert result["RecordState"] == "ARCHIVED" else: @@ -145,17 +156,3 @@ def test_no_AWS(kms_stubber, sts_stubber): else: assert False kms_stubber.assert_no_pending_responses() - -def test_key_rotation_not_enabled(sts_stubber, kms_stubber): - sts_stubber.add_response("get_caller_identity", sts_response) - kms_stubber.add_response("list_keys", list_keys_response) - kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response1) - check = KMSKeyRotationCheck() - results = check.execute() - for result in results: - if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: - print(result["Id"]) - assert result["RecordState"] == "ACTIVE" - else: - assert False - kms_stubber.assert_no_pending_responses() From 88c80cc44031c24bfd6c21499e7fb416aa81bf95 Mon Sep 17 00:00:00 2001 From: Patrick Brazil Date: Mon, 15 Jun 2020 15:20:20 -0500 Subject: [PATCH 10/11] fixed govcloud kms test order --- .../tests/test_AWS_KMS_Auditor.py | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/govcloud-auditors/tests/test_AWS_KMS_Auditor.py b/govcloud-auditors/tests/test_AWS_KMS_Auditor.py index afb1b3e1..219080b3 100644 --- a/govcloud-auditors/tests/test_AWS_KMS_Auditor.py +++ b/govcloud-auditors/tests/test_AWS_KMS_Auditor.py @@ -77,43 +77,55 @@ def kms_stubber(): yield kms_stubber kms_stubber.deactivate() +def test_key_rotation_enabled(sts_stubber, kms_stubber): + sts_stubber.add_response("get_caller_identity", sts_response) + kms_stubber.add_response("list_keys", list_keys_response) + kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response) + check = KMSKeyRotationCheck() + results = check.execute() + for result in results: + if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: + print(result["Id"]) + assert result["RecordState"] == "ARCHIVED" + else: + assert False + kms_stubber.assert_no_pending_responses() -def test_has_public_key(kms_stubber, sts_stubber): +def test_key_rotation_not_enabled(sts_stubber, kms_stubber): sts_stubber.add_response("get_caller_identity", sts_response) - kms_stubber.add_response("list_aliases", list_aliases_response) - kms_stubber.add_response("get_key_policy", get_key_policy_public_response) - check = KMSKeyExposedCheck() + kms_stubber.add_response("list_keys", list_keys_response) + kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response1) + check = KMSKeyRotationCheck() results = check.execute() for result in results: - if "s3" in result["Id"]: + if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: + print(result["Id"]) assert result["RecordState"] == "ACTIVE" else: assert False kms_stubber.assert_no_pending_responses() - -def test_no_public_key(kms_stubber, sts_stubber): +def test_has_public_key(kms_stubber, sts_stubber): sts_stubber.add_response("get_caller_identity", sts_response) kms_stubber.add_response("list_aliases", list_aliases_response) - kms_stubber.add_response("get_key_policy", get_key_policy_not_public_response) + kms_stubber.add_response("get_key_policy", get_key_policy_public_response) check = KMSKeyExposedCheck() results = check.execute() for result in results: if "s3" in result["Id"]: - print(result["Id"]) - assert result["RecordState"] == "ARCHIVED" + assert result["RecordState"] == "ACTIVE" else: assert False kms_stubber.assert_no_pending_responses() -def test_key_rotation_enabled(sts_stubber, kms_stubber): +def test_no_public_key(kms_stubber, sts_stubber): sts_stubber.add_response("get_caller_identity", sts_response) - kms_stubber.add_response("list_keys", list_keys_response) - kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response) - check = KMSKeyRotationCheck() + kms_stubber.add_response("list_aliases", list_aliases_response) + kms_stubber.add_response("get_key_policy", get_key_policy_not_public_response) + check = KMSKeyExposedCheck() results = check.execute() for result in results: - if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: + if "s3" in result["Id"]: print(result["Id"]) assert result["RecordState"] == "ARCHIVED" else: @@ -145,17 +157,3 @@ def test_no_AWS(kms_stubber, sts_stubber): else: assert False kms_stubber.assert_no_pending_responses() - -def test_key_rotation_not_enabled(sts_stubber, kms_stubber): - sts_stubber.add_response("get_caller_identity", sts_response) - kms_stubber.add_response("list_keys", list_keys_response) - kms_stubber.add_response("get_key_rotation_status", get_key_rotation_status_response1) - check = KMSKeyRotationCheck() - results = check.execute() - for result in results: - if "273e5d8e-4746-4ba9-be3a-4dce36783814" in result["Id"]: - print(result["Id"]) - assert result["RecordState"] == "ACTIVE" - else: - assert False - kms_stubber.assert_no_pending_responses() From 1006958821dde092842ecc1fb1e7744ffacbe7be Mon Sep 17 00:00:00 2001 From: aclatham Date: Mon, 15 Jun 2020 15:11:15 -0600 Subject: [PATCH 11/11] Small edits to kms --- auditors/AWS_KMS_Auditor.py | 86 ++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/auditors/AWS_KMS_Auditor.py b/auditors/AWS_KMS_Auditor.py index dd2bb890..a705838a 100644 --- a/auditors/AWS_KMS_Auditor.py +++ b/auditors/AWS_KMS_Auditor.py @@ -23,17 +23,18 @@ sts = boto3.client("sts") kms = boto3.client("kms") + class KMSKeyRotationCheck(Auditor): def execute(self): awsAccountId = sts.get_caller_identity()["Account"] awsRegion = os.environ["AWS_REGION"] keys = kms.list_keys() my_keys = keys["Keys"] + iso8601Time = datetime.datetime.now(datetime.timezone.utc).isoformat() for key in my_keys: keyid = key["KeyId"] keyarn = key["KeyArn"] key_rotation = kms.get_key_rotation_status(KeyId=keyid) - iso8601Time = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat() if key_rotation["KeyRotationEnabled"] == True: finding = { "SchemaVersion": "2018-10-08", @@ -75,64 +76,66 @@ def execute(self): "Details": {"AwsKmsKey": {"KeyId": keyid}}, } ], - "Compliance": { - "Status": "PASSED" - }, + "Compliance": {"Status": "PASSED"}, "Workflow": {"Status": "RESOLVED"}, "RecordState": "ARCHIVED", } yield finding else: finding = { - 'SchemaVersion': '2018-10-08', - 'Id': keyarn + '/kms-key-rotation-check', - 'ProductArn': 'arn:aws:securityhub:' + awsRegion + ':' + awsAccountId + ':product/' + awsAccountId + '/default', - 'GeneratorId': keyarn, - 'AwsAccountId': awsAccountId, - 'Types': [ 'Software and Configuration Checks/AWS Security Best Practices' ], - 'FirstObservedAt': iso8601Time, - 'CreatedAt': iso8601Time, - 'UpdatedAt': iso8601Time, - 'Severity': { 'Label': 'MEDIUM' }, - 'Confidence': 99, - 'Title': '[KMS.1] KMS keys should have key rotation enabled', - 'Description': 'KMS key ' + keyid + ' does not have key rotation enabled.', - 'Remediation': { - 'Recommendation': { - 'Text': 'For more information on KMS key rotation refer to the AWS KMS Developer Guide on Rotating Keys', - 'Url': 'https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html' + "SchemaVersion": "2018-10-08", + "Id": keyarn + "/kms-key-rotation-check", + "ProductArn": "arn:aws:securityhub:" + + awsRegion + + ":" + + awsAccountId + + ":product/" + + awsAccountId + + "/default", + "GeneratorId": keyarn, + "AwsAccountId": awsAccountId, + "Types": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "FirstObservedAt": iso8601Time, + "CreatedAt": iso8601Time, + "UpdatedAt": iso8601Time, + "Severity": {"Label": "MEDIUM"}, + "Confidence": 99, + "Title": "[KMS.1] KMS keys should have key rotation enabled", + "Description": "KMS key " + + keyid + + " does not have key rotation enabled.", + "Remediation": { + "Recommendation": { + "Text": "For more information on KMS key rotation refer to the AWS KMS Developer Guide on Rotating Keys", + "Url": "https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html", } }, - 'ProductFields': { - 'Product Name': 'ElectricEye' - }, - 'Resources': [ + "ProductFields": {"Product Name": "ElectricEye"}, + "Resources": [ { - 'Type': 'AwsKmsKey', - 'Id': keyarn, - 'Partition': 'aws', - 'Region': awsRegion, - 'Details': { - 'AwsKmsKey': { 'KeyId': keyid } - } + "Type": "AwsKmsKey", + "Id": keyarn, + "Partition": "aws", + "Region": awsRegion, + "Details": {"AwsKmsKey": {"KeyId": keyid}}, } ], - 'Compliance': { - 'Status': 'FAILED' - }, - 'Workflow': { - 'Status': 'NEW' - }, - 'RecordState': 'ACTIVE' + "Compliance": {"Status": "FAILED"}, + "Workflow": {"Status": "NEW"}, + "RecordState": "ACTIVE", } yield finding + class KMSKeyExposedCheck(Auditor): def execute(self): awsAccountId = sts.get_caller_identity()["Account"] awsRegion = os.environ["AWS_REGION"] response = kms.list_aliases() aliasList = response["Aliases"] + iso8601Time = datetime.datetime.now(datetime.timezone.utc).isoformat() for alias in aliasList: if "TargetKeyId" in alias: aliasArn = alias["AliasArn"] @@ -141,11 +144,6 @@ def execute(self): fail = False policy_json = policyString["Policy"] policy = json.loads(policy_json) - iso8601Time = ( - datetime.datetime.utcnow() - .replace(tzinfo=datetime.timezone.utc) - .isoformat() - ) for sid in policy["Statement"]: access = sid["Principal"].get("AWS", None) if access != "*" or (access == "*" and "Condition" in sid):