From c83571b671fcaa8a11f49720cba0b8a7c170b2d8 Mon Sep 17 00:00:00 2001 From: Patrick Kelley Date: Tue, 5 Sep 2017 22:56:31 -0700 Subject: [PATCH] =?UTF-8?q?Adding=20ResourcePolicyAuditor=20base=20class?= =?UTF-8?q?=20for=20*much*=20more=20intelligent=20cross=20account=20intros?= =?UTF-8?q?pection=20=20(#789)=20=F0=9F=91=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Creating a new ResourcePolicyAuditor base class that sqs,sns,kms,elasticsearch,lambda,vpcendpoints,glacier,opsworks,s3 can use. Moving ARN and Policy parsing code into policyuniverse. * Updating SNS. * Naively updating s3,sns,kms,es to use new ResourcePolicyAuditor. * Removing aws:username as a trusted field in condition block and bumping policyuniverse version. * Updating ResourcePolicyAuditor to look for cross account account-wide access and other small fixes to get the ElasticSearch tests to work. * Fixing RPA Tests. * Alerting on ThirdParty and access granted to Root ARNs. * Unitests for ResourcePolicyAuditor and subclasses need to clear the OBJECT_STORE in the pre_test_setup() * Adding tests for the SNS Auditor. * Upping coverage on the S3 Auditor. * Ignoring service principals. Fixing Item.config hybrid name. * Fixing the add method to ignore empty userids. * Going back to using shorter issue text inline with existing issues. * Removing SQS one-off where ResourcePolicyAuditor wouldn't look for a Policy block. * Common Issue Category Text. Moving S3 ACL Issues into new category format. Saving actions with resource policy issues. * Standardizing on Cross Account Root IAM and SNS Subscription wording. * Fixing bug in sns auditor and fixing tests * Fixing bug found running in prod. --- .gitignore | 1 - dart/web/ui.html | 24 +- migrations/versions/c9dd06c919ac_.py | 38 ++ security_monkey/auditor.py | 130 +++-- .../auditors/elasticsearch_service.py | 147 +----- security_monkey/auditors/iam/iam_role.py | 6 +- security_monkey/auditors/kms.py | 81 +--- security_monkey/auditors/lambda_function.py | 37 ++ .../auditors/resource_policy_auditor.py | 352 ++++++++++++++ security_monkey/auditors/s3.py | 217 +++------ security_monkey/auditors/sns.py | 115 ++--- security_monkey/auditors/sqs.py | 83 +--- security_monkey/common/arn.py | 101 ---- security_monkey/datastore.py | 11 + security_monkey/tests/auditors/test_arn.py | 160 ------ .../auditors/test_elasticsearch_service.py | 55 ++- security_monkey/tests/auditors/test_kms.py | 140 +++++- .../auditors/test_resouce_policy_auditor.py | 456 ++++++++++++++++++ security_monkey/tests/auditors/test_s3.py | 332 +++++++++++-- security_monkey/tests/auditors/test_sns.py | 104 ++++ setup.py | 1 + 21 files changed, 1655 insertions(+), 936 deletions(-) create mode 100644 migrations/versions/c9dd06c919ac_.py create mode 100644 security_monkey/auditors/lambda_function.py create mode 100644 security_monkey/auditors/resource_policy_auditor.py delete mode 100644 security_monkey/common/arn.py delete mode 100644 security_monkey/tests/auditors/test_arn.py create mode 100644 security_monkey/tests/auditors/test_resouce_policy_auditor.py create mode 100644 security_monkey/tests/auditors/test_sns.py diff --git a/.gitignore b/.gitignore index 0a480d568..04d7df4a9 100644 --- a/.gitignore +++ b/.gitignore @@ -61,5 +61,4 @@ secmonkey.env *.crt *.key postgres-data/ - docker-compose.override.yml diff --git a/dart/web/ui.html b/dart/web/ui.html index 6dc0a162d..738b0212a 100644 --- a/dart/web/ui.html +++ b/dart/web/ui.html @@ -47,35 +47,25 @@ diff --git a/migrations/versions/c9dd06c919ac_.py b/migrations/versions/c9dd06c919ac_.py new file mode 100644 index 000000000..534bfcdcb --- /dev/null +++ b/migrations/versions/c9dd06c919ac_.py @@ -0,0 +1,38 @@ +"""Add fields to ItemAudit table to support a future revamp of the issue system. + +Mostly not using these new fields yet. + +Action Instructions will hold information for how the user can fix the issue. +Background Information will hold information on why something is a problem, likely with links to AWS documentation for the user to read more. +Origin will hold the statement causing the issue. Hopefully the UI can use this to highlight the offending part of an item policy. +Origin Summary will hold a summary of the Origin. A JSON Policy statement may be summarized as something like "S3 READ FROM * TO s3:mybucket". +Class UUID will be used so that the text (itemaudit.issue, itemaudit.notes) can be changed in the future without losing justifications. + +Revision ID: c9dd06c919ac +Revises: b8ccf5b8089b +Create Date: 2017-09-05 17:21:08.162000 + +""" + +# revision identifiers, used by Alembic. +revision = 'c9dd06c919ac' +down_revision = 'b8ccf5b8089b' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('itemaudit', sa.Column('action_instructions', sa.Text(), nullable=True)) + op.add_column('itemaudit', sa.Column('background_info', sa.Text(), nullable=True)) + op.add_column('itemaudit', sa.Column('origin', sa.Text(), nullable=True)) + op.add_column('itemaudit', sa.Column('origin_summary', sa.Text(), nullable=True)) + op.add_column('itemaudit', sa.Column('class_uuid', sa.VARCHAR(length=32), nullable=True)) + + +def downgrade(): + op.drop_column('itemaudit', 'action_instructions') + op.drop_column('itemaudit', 'background_info') + op.drop_column('itemaudit', 'class_uuid') + op.drop_column('itemaudit', 'origin') + op.drop_column('itemaudit', 'origin_summary') \ No newline at end of file diff --git a/security_monkey/auditor.py b/security_monkey/auditor.py index ddb9abca2..551e54948 100644 --- a/security_monkey/auditor.py +++ b/security_monkey/auditor.py @@ -33,10 +33,61 @@ from sqlalchemy import and_ from collections import defaultdict +import json auditor_registry = defaultdict(list) +class Categories: + """ Define common issue categories to maintain consistency. """ + INTERNET_ACCESSIBLE = 'Internet Accessible' + INTERNET_ACCESSIBLE_NOTES = '{entity} Actions: {actions}' + + FRIENDLY_CROSS_ACCOUNT = 'Friendly Cross Account' + FRIENDLY_CROSS_ACCOUNT_NOTES = '{entity} Actions: {actions}' + + THIRDPARTY_CROSS_ACCOUNT = 'Thirdparty Cross Account' + THIRDPARTY_CROSS_ACCOUNT_NOTES = '{entity} Actions: {actions}' + + UNKNOWN_ACCESS = 'Unknown Access' + UNKNOWN_ACCESS_NOTES = '{entity} Actions: {actions}' + + PARSE_ERROR = 'Parse Error' + PARSE_ERROR_NOTES = 'Could not parse {input_type} - {input}' + + CROSS_ACCOUNT_ROOT = 'Cross-Account Root IAM' + CROSS_ACCOUNT_ROOT_NOTES = '{entity} Actions: {actions}' + + # TODO + # INSECURE_CERTIFICATE = 'Insecure Certificate' + # INSECURE_TLS = 'Insecure TLS' + # OVERLY_BROAD_ACCESS = 'Access Granted Broadly' + # ADMIN_ACCESS = 'Administrator Access' + # SENSITIVE_PERMISSIONS = 'Sensitive Permissions' + + +class Entity: + """ Entity instances provide a place to map policy elements like s3:my_bucket to the related account. """ + def __init__(self, category, value, account_name=None, account_identifier=None): + self.category = category + self.value = value + self.account_name = account_name + self.account_identifier = account_identifier + + @staticmethod + def from_tuple(entity_tuple): + return Entity(category=entity_tuple.category, value=entity_tuple.value) + + def __str__(self): + strval = '' + if self.account_name or self.account_identifier: + strval = 'Account: [{identifier}/{account_name}] '.format(identifier=self.account_identifier, account_name=self.account_name) + strval += 'Entity: [{category}:{value}]'.format(category=self.category, value=self.value) + return strval + + def __repr__(self): + return self.__str__() + class AuditorType(type): def __init__(cls, name, bases, attrs): super(AuditorType, cls).__init__(name, bases, attrs) @@ -88,7 +139,43 @@ def __init__(self, accounts=None, debug=False): users = User.query.filter(User.daily_audit_email==True).filter(User.accounts.any(name=account)).all() self.emails.extend([user.email for user in users]) - def add_issue(self, score, issue, item, notes=None): + def record_internet_access(self, item, entity, actions): + tag = Categories.INTERNET_ACCESSIBLE + notes = Categories.INTERNET_ACCESSIBLE_NOTES.format(entity=entity, actions=json.dumps(actions)) + action_instructions = "An {singular} ".format(singular=self.i_am_singular) + action_instructions += "with { 'Principal': { 'AWS': '*' } } must also have a strong condition block or it is Internet Accessible. " + self.add_issue(10, tag, item, notes=notes, action_instructions=action_instructions) + + def record_friendly_access(self, item, entity, actions): + tag = Categories.FRIENDLY_CROSS_ACCOUNT + notes = Categories.FRIENDLY_CROSS_ACCOUNT_NOTES.format( + entity=entity, actions=json.dumps(actions)) + self.add_issue(0, tag, item, notes=notes) + + def record_thirdparty_access(self, item, entity, actions): + tag = Categories.THIRDPARTY_CROSS_ACCOUNT + notes = Categories.THIRDPARTY_CROSS_ACCOUNT_NOTES.format( + entity=entity, actions=json.dumps(actions)) + self.add_issue(0, tag, item, notes=notes) + + def record_unknown_access(self, item, entity, actions): + tag = Categories.UNKNOWN_ACCESS + notes = Categories.UNKNOWN_ACCESS_NOTES.format( + entity=entity, actions=json.dumps(actions)) + self.add_issue(10, tag, item, notes=notes) + + def record_cross_account_root(self, item, entity, actions): + tag = Categories.CROSS_ACCOUNT_ROOT + notes = Categories.CROSS_ACCOUNT_ROOT_NOTES.format( + entity=entity, actions=json.dumps(actions)) + self.add_issue(6, tag, item, notes=notes) + + def record_arn_parse_issue(self, item, arn): + tag = Categories.PARSE_ERROR + notes = Categories.PARSE_ERROR_NOTES.format(input_type='ARN', input=arn) + self.add_issue(3, tag, item, notes=notes) + + def add_issue(self, score, issue, item, notes=None, action_instructions=None): """ Adds a new issue to an item, if not already reported. :return: The new issue @@ -118,6 +205,7 @@ def add_issue(self, score, issue, item, notes=None): new_issue = datastore.ItemAudit(score=score, issue=issue, notes=notes, + action_instructions=action_instructions, justified=False, justified_user_id=None, justified_date=None, @@ -389,46 +477,6 @@ def _set_auditor_setting_for_issue(self, issue): return auditor_setting - def _check_cross_account(self, src_account_number, dest_item, location): - account = Account.query.filter(Account.identifier == src_account_number).first() - account_name = None - if account is not None: - account_name = account.name - - src = account_name or src_account_number - dst = dest_item.account - - if src == dst: - return None - - notes = "SRC [{}] DST [{}]. Location: {}".format(src, dst, location) - - if not account_name: - tag = "Unknown Cross Account Access" - self.add_issue(10, tag, dest_item, notes=notes) - elif account_name != dest_item.account and not account.third_party: - tag = "Friendly Cross Account Access" - self.add_issue(0, tag, dest_item, notes=notes) - elif account_name != dest_item.account and account.third_party: - tag = "Friendly Third Party Cross Account Access" - self.add_issue(0, tag, dest_item, notes=notes) - - def _check_cross_account_root(self, source_item, dest_arn, actions): - if not actions: - return None - - account = Account.query.filter(Account.name == source_item.account).first() - source_item_account_number = account.identifier - - if source_item_account_number == dest_arn.account_number: - return None - - tag = "Cross-Account Root IAM" - notes = "ALL IAM Roles/users/groups in account {} can perform the following actions:\n"\ - .format(dest_arn.account_number) - notes += "{}".format(actions) - self.add_issue(6, tag, source_item, notes=notes) - def get_auditor_support_items(self, auditor_index, account): for index in self.support_auditor_indexes: if index == auditor_index: diff --git a/security_monkey/auditors/elasticsearch_service.py b/security_monkey/auditors/elasticsearch_service.py index f09df3615..ea1b1e3ff 100644 --- a/security_monkey/auditors/elasticsearch_service.py +++ b/security_monkey/auditors/elasticsearch_service.py @@ -17,156 +17,19 @@ .. version:: $$VERSION$$ .. moduleauthor:: Mike Grima +.. moduleauthor:: Patrick Kelley @monkeysecurity """ -from security_monkey.auditor import Auditor -from security_monkey.common.arn import ARN -from security_monkey.datastore import NetworkWhitelistEntry from security_monkey.watchers.elasticsearch_service import ElasticSearchService +from security_monkey.auditors.resource_policy_auditor import ResourcePolicyAuditor +from policyuniverse.arn import ARN -import ipaddr - -class ElasticSearchServiceAuditor(Auditor): +class ElasticSearchServiceAuditor(ResourcePolicyAuditor): index = ElasticSearchService.index i_am_singular = ElasticSearchService.i_am_singular i_am_plural = ElasticSearchService.i_am_plural def __init__(self, accounts=None, debug=False): super(ElasticSearchServiceAuditor, self).__init__(accounts=accounts, debug=debug) - - def prep_for_audit(self): - self.network_whitelist = NetworkWhitelistEntry.query.all() - - def _parse_arn(self, arn_input, account_numbers, es_domain): - if arn_input == '*': - notes = "An ElasticSearch Service domain policy where { 'Principal': { 'AWS': '*' } } must also have" - notes += " a {'Condition': {'IpAddress': { 'AWS:SourceIp': '' } } }" - notes += " or it is open to any AWS account." - self.add_issue(20, 'ES cluster open to all AWS accounts', es_domain, notes=notes) - return - - arn = ARN(arn_input) - if arn.error: - self.add_issue(3, 'Auditor could not parse ARN', es_domain, notes=arn_input) - return - - account_numbers.append(arn.account_number) - - def check_es_access_policy(self, es_domain): - policy = es_domain.config["policy"] - - for statement in policy.get("Statement", []): - effect = statement.get("Effect") - # We only care about "Allows" - if effect.lower() == "deny": - continue - - account_numbers = [] - princ = statement.get("Principal", {}) - if isinstance(princ, dict): - princ_val = princ.get("AWS") or princ.get("Service") - else: - princ_val = princ - - if princ_val == "*": - condition = statement.get('Condition', {}) - - # Get the IpAddress subcondition: - ip_addr_condition = condition.get("IpAddress") - - if ip_addr_condition: - source_ip_condition = ip_addr_condition.get("aws:SourceIp") - - if not ip_addr_condition or not source_ip_condition: - tag = "ElasticSearch Service domain open to everyone" - notes = "An ElasticSearch Service domain policy where { 'Principal': { '*' } } OR" - notes += " { 'Principal': { 'AWS': '*' } } must also have a" - notes += " {'Condition': {'IpAddress': { 'AWS:SourceIp': '' } } }" - notes += " or it is open to the world. In this case, anyone is allowed to perform " - notes += " this action(s): {}".format(statement.get("Action")) - self.add_issue(20, tag, es_domain, notes=notes) - - else: - # Check for "aws:SourceIp" as a condition: - if isinstance(source_ip_condition, list): - for cidr in source_ip_condition: - self._check_proper_cidr(cidr, es_domain, statement.get("Action")) - - else: - self._check_proper_cidr(source_ip_condition, es_domain, statement.get("Action")) - - else: - if isinstance(princ_val, list): - for entry in princ_val: - arn = ARN(entry) - if arn.error: - self.add_issue(3, 'Auditor could not parse ARN', es_domain, notes=entry) - continue - - if arn.root: - self._check_cross_account_root(es_domain, arn, statement.get("Action")) - - if not arn.service: - account_numbers.append(arn.account_number) - else: - arn = ARN(princ_val) - if arn.error: - self.add_issue(3, 'Auditor could not parse ARN', es_domain, notes=princ_val) - else: - if arn.root: - self._check_cross_account_root(es_domain, arn, statement.get("Action")) - if not arn.service: - account_numbers.append(arn.account_number) - - for account_number in account_numbers: - self._check_cross_account(account_number, es_domain, 'policy') - - def _check_proper_cidr(self, cidr, es_domain, actions): - try: - any, ip_cidr = self._check_for_any_ip(cidr, es_domain, actions) - if any: - return - - if not self._check_inclusion_in_network_whitelist(cidr): - message = "A CIDR that is not in the whitelist has access to this ElasticSearch Service domain:\n" - message += "CIDR: {}, Actions: {}".format(cidr, actions) - self.add_issue(5, message, es_domain, notes=cidr) - - # Check if the CIDR is in a large subnet (and not whitelisted): - # Check if it's 10.0.0.0/8 - if ip_cidr == ipaddr.IPNetwork("10.0.0.0/8"): - message = "aws:SourceIp Condition contains a very large IP range: 10.0.0.0/8" - self.add_issue(7, message, es_domain, notes=cidr) - else: - mask = int(ip_cidr.exploded.split('/')[1]) - if 0 < mask < 24: - message = "aws:SourceIp contains a large IP Range: {}".format(cidr) - self.add_issue(3, message, es_domain, notes=cidr) - - - except ValueError as ve: - self.add_issue(3, 'Auditor could not parse CIDR', es_domain, notes=cidr) - - def _check_for_any_ip(self, cidr, es_domain, actions): - if cidr == '*': - self.add_issue(20, 'Any IP can perform the following actions against this ElasticSearch Service ' - 'domain:\n{}'.format(actions), - es_domain, notes=cidr) - return True, None - - zero = ipaddr.IPNetwork("0.0.0.0/0") - ip_cidr = ipaddr.IPNetwork(cidr) - if zero == ip_cidr: - self.add_issue(20, 'Any IP can perform the following actions against this ElasticSearch Service ' - 'domain:\n{}'.format(actions), - es_domain, notes=cidr) - return True, None - - return False, ip_cidr - - def _check_inclusion_in_network_whitelist(self, cidr): - for entry in self.network_whitelist: - if ipaddr.IPNetwork(cidr) in ipaddr.IPNetwork(str(entry.cidr)): - return True - return False + self.policy_keys = ['policy'] diff --git a/security_monkey/auditors/iam/iam_role.py b/security_monkey/auditors/iam/iam_role.py index d0c4683a8..fbbfd3fb8 100644 --- a/security_monkey/auditors/iam/iam_role.py +++ b/security_monkey/auditors/iam/iam_role.py @@ -19,13 +19,14 @@ .. moduleauthor:: Patrick Kelley @monkeysecurity """ -import json - from security_monkey.watchers.iam.iam_role import IAMRole from security_monkey.auditors.iam.iam_policy import IAMPolicyAuditor from security_monkey.watchers.iam.managed_policy import ManagedPolicy from security_monkey.datastore import Account +from policyuniverse.arn import ARN +import json + class IAMRoleAuditor(IAMPolicyAuditor): index = IAMRole.index @@ -77,7 +78,6 @@ def check_assume_role_from_unknown_account(self, iamrole_item): def check_statement(statement): def check_account_in_arn(input): - from security_monkey.common.arn import ARN arn = ARN(input) if arn.error: diff --git a/security_monkey/auditors/kms.py b/security_monkey/auditors/kms.py index 72285b4c4..c55d9ed88 100644 --- a/security_monkey/auditors/kms.py +++ b/security_monkey/auditors/kms.py @@ -15,39 +15,22 @@ .. version:: $$VERSION$$ .. moduleauthor:: Alex Cline @alex.cline +.. moduleauthor:: Patrick Kelley @monkeysecurity """ -from security_monkey.auditor import Auditor from security_monkey.watchers.kms import KMS +from security_monkey.auditors.resource_policy_auditor import ResourcePolicyAuditor import json -def extract_condition_account_numbers(condition): - condition_subsection = condition.get('StringLike', {}) or \ - condition.get('ForAllValues:StringLike', {}) or \ - condition.get('ForAnyValue:StringLike', {}) or \ - condition.get('StringEquals', {}) or \ - condition.get('ForAllValues:StringEquals', {}) or \ - condition.get('ForAnyValue:StringEquals', {}) - - condition_accounts = [] - for key, value in condition_subsection.iteritems(): - if key.lower() == 'kms:calleraccount': - if isinstance(value, list): - condition_accounts.extend(value) - else: - condition_accounts.append(value) - - return condition_accounts - - -class KMSAuditor(Auditor): +class KMSAuditor(ResourcePolicyAuditor): index = KMS.index i_am_singular = KMS.i_am_singular i_am_plural = KMS.i_am_plural def __init__(self, accounts=None, debug=False): super(KMSAuditor, self).__init__(accounts=accounts, debug=debug) + self.policy_keys = ['Policies'] def check_for_kms_key_rotation(self, kms_item): """ @@ -57,59 +40,3 @@ def check_for_kms_key_rotation(self, kms_item): rotation_status = kms_item.config.get('KeyRotationEnabled') if not rotation_status: self.add_issue(1, 'KMS key is not configured for rotation.', kms_item) - - def check_for_kms_policy_with_foreign_account(self, kms_item): - """ - alert when a KMS master key contains a policy giving permissions - to a foreign account - """ - tag = '{0} contains policies with foreign account permissions.'.format(self.i_am_singular) - key_account_id = kms_item.config.get("AWSAccountId") - key_policies = kms_item.config.get("Policies") - - for policy in key_policies: - for statement in policy.get("Statement"): - condition_accounts = [] - if 'Condition' in statement: - condition = statement.get('Condition') - if condition: - condition_accounts = extract_condition_account_numbers(condition) - - cross_accounts = [account for account in condition_accounts if account != key_account_id] - if cross_accounts: - notes = "Condition - kms:CallerAccount: {}".format(json.dumps(cross_accounts)) - self.add_issue(5, tag, kms_item, notes=notes) - - if statement and statement.get("Principal"): - aws_principal = statement.get("Principal") - if isinstance(aws_principal, dict): - if 'AWS' in aws_principal: - aws_principal = aws_principal.get("AWS") - elif 'Service' in aws_principal: - aws_principal = aws_principal.get("Service") - - if isinstance(aws_principal, basestring): - # Handles the case where the prnciple is * - aws_principal = [aws_principal] - - principal_account_ids = set() - for arn in aws_principal: - if arn == "*" and not condition_accounts and "allow" == statement.get('Effect').lower(): - notes = "An KMS policy where { 'Principal': { 'AWS': '*' } } must also have" - notes += " a {'Condition': {'StringEquals': { 'kms:CallerAccount': '' } } }" - notes += " or it is open to the world." - self.add_issue(5, tag, kms_item, notes=notes) - continue - - if ':' not in arn: - # can happen if role is deleted - # and ARN is replaced wih role id. - continue - - statement_account_id = arn.split(":")[4] - if statement_account_id != key_account_id: - principal_account_ids.add(statement_account_id) - - if principal_account_ids: - notes = "Principal - {}".format(json.dumps(sorted(list(principal_account_ids)))) - self.add_issue(5, tag, kms_item, notes=notes) diff --git a/security_monkey/auditors/lambda_function.py b/security_monkey/auditors/lambda_function.py new file mode 100644 index 000000000..af5539ee9 --- /dev/null +++ b/security_monkey/auditors/lambda_function.py @@ -0,0 +1,37 @@ +# Copyright 2017 Netflix, Inc. +# +# 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. +""" +.. module: security_monkey.auditors.lambda_function + :platform: Unix + +.. version:: $$VERSION$$ +.. moduleauthor:: Patrick Kelley + +""" + +from security_monkey.auditors.resource_policy_auditor import ResourcePolicyAuditor +from security_monkey.watchers.lambda_function import LambdaFunction + +from policyuniverse.arn import ARN +import json + + +class LambdaFunctionAuditor(ResourcePolicyAuditor): + index = LambdaFunction.index + i_am_singular = LambdaFunction.i_am_singular + i_am_plural = LambdaFunction.i_am_plural + + def __init__(self, accounts=None, debug=False): + super(LambdaFunctionAuditor, self).__init__(accounts=accounts, debug=debug) + self.policy_keys = ['Policy$DEFAULT', 'Policy$Aliases', 'Policy$Versions'] diff --git a/security_monkey/auditors/resource_policy_auditor.py b/security_monkey/auditors/resource_policy_auditor.py new file mode 100644 index 000000000..f0a7b79d2 --- /dev/null +++ b/security_monkey/auditors/resource_policy_auditor.py @@ -0,0 +1,352 @@ +# Copyright 2014 Netflix, Inc. +# +# 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. +""" +.. module: security_monkey.auditors.resource_policy_auditor + :platform: Unix + +.. version:: $$VERSION$$ +.. moduleauthor:: Patrick Kelley + +""" +from security_monkey import app +from security_monkey.auditor import Auditor, Categories, Entity +from security_monkey.datastore import Account, Item, Technology, NetworkWhitelistEntry + +from policyuniverse.arn import ARN +from policyuniverse.policy import Policy +from policyuniverse.statement import Statement +import json +import dpath.util +from dpath.exceptions import PathNotFound +from collections import defaultdict +import ipaddr + + +def add(to, key, value): + if not key: + return + if key in to: + to[key].add(value) + else: + to[key] = set([value]) + +class ResourcePolicyAuditor(Auditor): + OBJECT_STORE = defaultdict(dict) + + def __init__(self, accounts=None, debug=False): + super(ResourcePolicyAuditor, self).__init__(accounts=accounts, debug=debug) + self.policy_keys = ['Policy'] + + def prep_for_audit(self): + if not self.OBJECT_STORE: + self._load_s3_buckets() + self._load_userids() + self._load_accounts() + self._load_vpcs() + self._load_vpces() + self._load_natgateways() + self._load_network_whitelist() + + @classmethod + def _load_s3_buckets(cls): + """Store the S3 bucket ARNs from all our accounts""" + results = cls._load_related_items('s3') + for item in results: + add(cls.OBJECT_STORE['s3'], item.name, item.account.identifier) + + @classmethod + def _load_vpcs(cls): + """Store the VPC IDs. Also, extract & store network/NAT ranges.""" + results = cls._load_related_items('vpc') + for item in results: + add(cls.OBJECT_STORE['vpc'], item.latest_config.get('id'), item.account.identifier) + add(cls.OBJECT_STORE['cidr'], item.latest_config.get('cidr_block'), item.account.identifier) + + @classmethod + def _load_vpces(cls): + """Store the VPC Endpoint IDs.""" + results = cls._load_related_items('endpoint') + for item in results: + add(cls.OBJECT_STORE['vpce'], item.latest_config.get('id'), item.account.identifier) + + @classmethod + def _load_natgateways(cls): + """Store the NAT Gateway CIDRs.""" + results = cls._load_related_items('natgateway') + for gateway in results: + for address in gateway.latest_config.get('nat_gateway_addresses', []): + add(cls.OBJECT_STORE['cidr'], address['public_ip'], gateway.account.identifier) + add(cls.OBJECT_STORE['cidr'], address['private_ip'], gateway.account.identifier) + + @classmethod + def _load_network_whitelist(cls): + """Stores the Network Whitelist CIDRs.""" + whitelist_entries = NetworkWhitelistEntry.query.all() + for entry in whitelist_entries: + add(cls.OBJECT_STORE['cidr'], entry.cidr, '000000000000') + + @classmethod + def _load_userids(cls): + """Store the UserIDs from all IAMUsers and IAMRoles.""" + user_results = cls._load_related_items('iamuser') + role_results = cls._load_related_items('iamrole') + + for item in user_results: + add(cls.OBJECT_STORE['userid'], item.latest_config.get('UserId'), item.account.identifier) + + for item in role_results: + add(cls.OBJECT_STORE['userid'], item.latest_config.get('RoleId'), item.account.identifier) + + @classmethod + def _load_accounts(cls): + """Store the account IDs of all friendly/thirdparty accounts.""" + friendly_accounts = Account.query.filter(Account.third_party == False).all() + third_party = Account.query.filter(Account.third_party == True).all() + + cls.OBJECT_STORE['ACCOUNTS']['DESCRIPTIONS'] = list() + cls.OBJECT_STORE['ACCOUNTS']['FRIENDLY'] = set() + cls.OBJECT_STORE['ACCOUNTS']['THIRDPARTY'] = set() + + for account in friendly_accounts: + add(cls.OBJECT_STORE['ACCOUNTS'], 'FRIENDLY', account.identifier) + cls.OBJECT_STORE['ACCOUNTS']['DESCRIPTIONS'].append(dict( + name=account.name, + identifier=account.identifier, + label='friendly', + s3_name=account.getCustom('s3_name'), + s3_canonical_id=account.getCustom('canonical_id'))) + + for account in third_party: + add(cls.OBJECT_STORE['ACCOUNTS'], 'THIRDPARTY', account.identifier) + cls.OBJECT_STORE['ACCOUNTS']['DESCRIPTIONS'].append(dict( + name=account.name, + identifier=account.identifier, + label='thirdparty', + s3_name=account.getCustom('s3_name'), + s3_canonical_id=account.getCustom('canonical_id'))) + + @staticmethod + def _load_related_items(technology_name): + query = Item.query.join((Technology, Technology.id == Item.tech_id)) + query = query.filter(Technology.name==technology_name) + return query.all() + + def _get_account(self, key, value): + """ _get_account('s3_name', 'blah') """ + if key == 'aws': + return dict(name='AWS', identifier='AWS') + for account in self.OBJECT_STORE['ACCOUNTS']['DESCRIPTIONS']: + if unicode(account.get(key, '')).lower() == value.lower(): + return account + + def load_policies(self, item): + """For a given item, return a list of all resource policies. + + Most items only have a single resource policy, typically found + inside the config with the key, "Policy". + + Some technologies have multiple resource policies. A lambda function + is an example of an item with multiple resource policies. + + The lambda function auditor can define a list of `policy_keys`. Each + item in this list is the dpath to one of the resource policies. + + The `policy_keys` defaults to ['Policy'] unless overriden by a subclass. + + Returns: + list of Policy objects + """ + policies = list() + for key in self.policy_keys: + try: + policy = dpath.util.values(item.config, key, separator='$') + if isinstance(policy, list): + for p in policy: + if not p: + continue + if isinstance(p, list): + policies.extend([Policy(pp) for pp in p]) + else: + policies.append(Policy(p)) + else: + policies.append(Policy(policy)) + except PathNotFound: + continue + return policies + + def check_internet_accessible(self, item): + policies = self.load_policies(item) + for policy in policies: + if policy.is_internet_accessible(): + entity = Entity(category='principal', value='*') + actions = list(policy.internet_accessible_actions()) + self.record_internet_access(item, entity, actions) + + def check_friendly_cross_account(self, item): + policies = self.load_policies(item) + for policy in policies: + for statement in policy.statements: + if statement.effect != 'Allow': + continue + for who in statement.whos_allowed(): + entity = Entity.from_tuple(who) + if 'FRIENDLY' in self.inspect_entity(entity, item): + self.record_friendly_access(item, entity, list(statement.actions)) + + def check_thirdparty_cross_account(self, item): + policies = self.load_policies(item) + for policy in policies: + for statement in policy.statements: + if statement.effect != 'Allow': + continue + for who in statement.whos_allowed(): + entity = Entity.from_tuple(who) + if 'THIRDPARTY' in self.inspect_entity(entity, item): + self.record_thirdparty_access(item, entity, list(statement.actions)) + + def check_unknown_cross_account(self, item): + policies = self.load_policies(item) + for policy in policies: + if policy.is_internet_accessible(): + continue + for statement in policy.statements: + if statement.effect != 'Allow': + continue + for who in statement.whos_allowed(): + if who.value == '*' and who.category == 'principal': + continue + + # Ignore Service Principals + if who.category == 'principal': + arn = ARN(who.value) + if arn.service: + continue + + entity = Entity.from_tuple(who) + if 'UNKNOWN' in self.inspect_entity(entity, item): + self.record_unknown_access(item, entity, list(statement.actions)) + + def check_root_cross_account(self, item): + policies = self.load_policies(item) + for policy in policies: + for statement in policy.statements: + if statement.effect != 'Allow': + continue + for who in statement.whos_allowed(): + if who.category not in ['arn', 'principal']: + continue + if who.value == '*': + continue + arn = ARN(who.value) + entity = Entity.from_tuple(who) + if arn.root and self.inspect_entity(entity, item).intersection(set(['FRIENDLY', 'THIRDPARTY', 'UNKNOWN'])): + self.record_cross_account_root(item, entity, list(statement.actions)) + + def inspect_entity(self, entity, item): + """A entity can represent an: + + - ARN + - Account Number + - UserID + - CIDR + - VPC + - VPCE + + Determine if the who is in our current account. Add the associated account + to the entity. + + Return: + 'SAME' - The who is in our same account. + 'FRIENDLY' - The who is in an account Security Monkey knows about. + 'UNKNOWN' - The who is in an account Security Monkey does not know about. + """ + same = Account.query.filter(Account.name == item.account).first() + + if entity.category in ['arn', 'principal']: + return self.inspect_entity_arn(entity, same, item) + if entity.category == 'account': + return set([self.inspect_entity_account(entity, entity.value, same)]) + if entity.category == 'userid': + return self.inspect_entity_userid(entity, same) + if entity.category == 'cidr': + return self.inspect_entity_cidr(entity, same) + if entity.category == 'vpc': + return self.inspect_entity_vpc(entity, same) + if entity.category == 'vpce': + return self.inspect_entity_vpce(entity, same) + + return 'ERROR' + + def inspect_entity_arn(self, entity, same, item): + arn_input = entity.value + if arn_input == '*': + return set(['UNKNOWN']) + + arn = ARN(arn_input) + if arn.error: + self.record_arn_parse_issue(item, arn_input) + + if arn.tech == 's3': + return self.inspect_entity_s3(entity, arn.name, same) + + return set([self.inspect_entity_account(entity, arn.account_number, same)]) + + def inspect_entity_account(self, entity, account_number, same): + + # Enrich the entity with account data if available. + for account in self.OBJECT_STORE['ACCOUNTS']['DESCRIPTIONS']: + if account['identifier'] == account_number: + entity.account_name = account['name'] + entity.account_identifier = account['identifier'] + break + + if account_number == '000000000000': + return 'SAME' + if account_number == same.identifier: + return 'SAME' + if account_number in self.OBJECT_STORE['ACCOUNTS']['FRIENDLY']: + return 'FRIENDLY' + if account_number in self.OBJECT_STORE['ACCOUNTS']['THIRDPARTY']: + return 'THIRDPARTY' + return 'UNKNOWN' + + def inspect_entity_s3(self, entity, bucket_name, same): + return self.inspect_entity_generic('s3', entity, bucket_name, same) + + def inspect_entity_userid(self, entity, same): + return self.inspect_entity_generic('userid', entity, entity.value.split(':')[0], same) + + def inspect_entity_vpc(self, entity, same): + return self.inspect_entity_generic('vpc', entity, entity.value, same) + + def inspect_entity_vpce(self, entity, same): + return self.inspect_entity_generic('vpce', entity, entity.value, same) + + def inspect_entity_cidr(self, entity, same): + values = set() + for str_cidr in self.OBJECT_STORE.get('cidr', []): + if ipaddr.IPNetwork(entity.value) in ipaddr.IPNetwork(str_cidr): + for account in self.OBJECT_STORE['cidr'].get(str_cidr, []): + values.add(self.inspect_entity_account(entity, account, same)) + if not values: + return set(['UNKNOWN']) + return values + + def inspect_entity_generic(self, key, entity, item, same): + if item in self.OBJECT_STORE.get(key, []): + values = set() + for account in self.OBJECT_STORE[key].get(item, []): + values.add(self.inspect_entity_account(entity, account, same)) + return values + return set(['UNKNOWN']) diff --git a/security_monkey/auditors/s3.py b/security_monkey/auditors/s3.py index 9825fcd10..a9eb67fbc 100644 --- a/security_monkey/auditors/s3.py +++ b/security_monkey/auditors/s3.py @@ -19,167 +19,80 @@ .. moduleauthor:: Patrick Kelley @monkeysecurity """ - -from security_monkey.auditor import Auditor +from security_monkey.auditors.resource_policy_auditor import ResourcePolicyAuditor +from security_monkey.auditor import Entity from security_monkey.watchers.s3 import S3 from security_monkey.datastore import Account -class S3Auditor(Auditor): +class S3Auditor(ResourcePolicyAuditor): index = S3.index i_am_singular = S3.i_am_singular i_am_plural = S3.i_am_plural def __init__(self, accounts=None, debug=False): super(S3Auditor, self).__init__(accounts=accounts, debug=debug) - - def check_acl(self, s3_item): - accounts = Account.query.all() - S3_ACCOUNT_NAMES = [account.getCustom("s3_name").lower() for account in accounts if not account.third_party and account.getCustom("s3_name")] - S3_CANONICAL_IDS = [account.getCustom("canonical_id").lower() for account in accounts if not account.third_party and account.getCustom("canonical_id")] - S3_THIRD_PARTY_ACCOUNTS = [account.getCustom("s3_name").lower() for account in accounts if account.third_party and account.getCustom("s3_name")] - S3_THIRD_PARTY_ACCOUNT_CANONICAL_IDS = [account.getCustom("canonical_id").lower() for account in accounts if account.third_party and account.getCustom("canonical_id")] - - # Get the owner ID: - owner = s3_item.config["Owner"]["ID"].lower() - - acl = s3_item.config.get('Grants', {}) - for user in acl.keys(): - if user == 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers': - message = "ACL - AuthenticatedUsers USED. " - notes = "{}".format(",".join(acl[user])) - self.add_issue(10, message, s3_item, notes=notes) - elif user == 'http://acs.amazonaws.com/groups/global/AllUsers': - message = "ACL - AllUsers USED." - notes = "{}".format(",".join(acl[user])) - self.add_issue(10, message, s3_item, notes=notes) - elif user == 'http://acs.amazonaws.com/groups/s3/LogDelivery': - message = "ACL - LogDelivery USED." - notes = "{}".format(",".join(acl[user])) - self.add_issue(0, message, s3_item, notes=notes) - - # DEPRECATED: - elif user.lower() in S3_ACCOUNT_NAMES: - message = "ACL - Friendly Account Access." - notes = "{} {}".format(",".join(acl[user]), user) - self.add_issue(0, message, s3_item, notes=notes) - elif user.lower() in S3_THIRD_PARTY_ACCOUNTS: - message = "ACL - Friendly Third Party Access." - notes = "{} {}".format(",".join(acl[user]), user) - self.add_issue(0, message, s3_item, notes=notes) - - elif user.lower() in S3_CANONICAL_IDS: - # Owning account -- no issue - if user.lower() == owner.lower(): - continue - - message = "ACL - Friendly Account Access." - notes = "{} {}".format(",".join(acl[user]), user) - self.add_issue(0, message, s3_item, notes=notes) - - elif user.lower() in S3_THIRD_PARTY_ACCOUNT_CANONICAL_IDS: - message = "ACL - Friendly Third Party Access." - notes = "{} {}".format(",".join(acl[user]), user) - self.add_issue(0, message, s3_item, notes=notes) - - else: - message = "ACL - Unknown Cross Account Access." - notes = "{} {}".format(",".join(acl[user]), user) - self.add_issue(10, message, s3_item, notes=notes) - - def check_policy(self, s3_item): - policy = s3_item.config.get('Policy', {}) + self.policy_keys = ['Policy'] + + def prep_for_audit(self): + super(S3Auditor, self).prep_for_audit() + self.FRIENDLY_S3NAMES = [unicode(account['s3_name']).lower() for account in self.OBJECT_STORE['ACCOUNTS']['DESCRIPTIONS'] if account['label'] == 'friendly'] + self.THIRDPARTY_S3NAMES = [unicode(account['s3_name']).lower() for account in self.OBJECT_STORE['ACCOUNTS']['DESCRIPTIONS'] if account['label'] == 'thirdparty'] + self.FRIENDLY_S3CANONICAL = [unicode(account['s3_canonical_id']).lower() for account in self.OBJECT_STORE['ACCOUNTS']['DESCRIPTIONS'] if account['label'] == 'friendly'] + self.THIRDPARTY_S3CANONICAL = [unicode(account['s3_canonical_id']).lower() for account in self.OBJECT_STORE['ACCOUNTS']['DESCRIPTIONS'] if account['label'] == 'thirdparty'] + self.INTERNET_ACCESSIBLE = [ + 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers'.lower(), + 'http://acs.amazonaws.com/groups/global/AllUsers'.lower()] + self.LOG_DELIVERY = ['http://acs.amazonaws.com/groups/s3/LogDelivery'.lower()] + self.KNOWN_ACLS = self.FRIENDLY_S3NAMES + self.THIRDPARTY_S3NAMES + self.FRIENDLY_S3CANONICAL + self.THIRDPARTY_S3CANONICAL + self.INTERNET_ACCESSIBLE + self.LOG_DELIVERY + + def _check_acl(self, item, field, keys, recorder): + acl = item.config.get('Grants', {}) + owner = item.config["Owner"]["ID"].lower() + for key in acl.keys(): + if key.lower() not in keys: + continue + + # Canonical ID == Owning Account - No issue + if key.lower() == owner.lower(): + continue + + entity = Entity(category='ACL', value=key) + account = self._get_account(field, key) + if account: + entity.account_name=account['name'] + entity.account_identifier=account['identifier'] + recorder(item, actions=acl[key], entity=entity) + + def check_acl_internet_accessible(self, item): + """ Handles AllUsers and AuthenticatedUsers. """ + self._check_acl(item, 'aws', self.INTERNET_ACCESSIBLE, self.record_internet_access) + + def check_acl_log_delivery(self, item): + self._check_acl(item, 'aws', self.LOG_DELIVERY, self.record_thirdparty_access) + + def check_acl_friendly_legacy(self, item): + self._check_acl(item, 's3_name', self.FRIENDLY_S3NAMES, self.record_friendly_access) + + def check_acl_thirdparty_legacy(self, item): + self._check_acl(item, 's3_name', self.THIRDPARTY_S3NAMES, self.record_thirdparty_access) + + def check_acl_friendly_canonical(self, item): + self._check_acl(item, 's3_canonical_id', self.FRIENDLY_S3CANONICAL, self.record_friendly_access) + + def check_acl_thirdparty_canonical(self, item): + self._check_acl(item, 's3_canonical_id', self.THIRDPARTY_S3CANONICAL, self.record_thirdparty_access) + + def check_acl_unknown(self, item): + acl = item.config.get('Grants', {}) + + for key in acl.keys(): + if key.lower() not in self.KNOWN_ACLS: + entity = Entity(category='ACL', value=key) + self.record_unknown_access(item, entity, actions=acl[key]) + + def check_policy_exists(self, item): + policy = item.config.get('Policy', {}) if not policy: message = "POLICY - No Policy." - self.add_issue(0, message, s3_item) - return - - statements = policy.get('Statement', {}) - complained = [] - for statement in statements: - self.inspect_policy_allow_all(statement, s3_item) - self.inspect_policy_cross_account(statement, s3_item, complained) - - def _condition_summary(self, statement): - summary_values = set() - try: - for key in statement['Condition']: - for subkey in statement['Condition'][key]: - summary_values.add('{k}/{s}'.format(k=key, s=subkey)) - except: - pass - return ', '.join(sorted(list(summary_values))) - - def inspect_policy_allow_all(self, statement, s3_item): - - if 'Condition' in statement: - notes = self._condition_summary(statement) - score = 3 - message = "POLICY - This Policy Allows Conditional Access From Anyone." - else: - notes = None - score = 10 - message = "POLICY - This Policy Allows Access From Anyone." - - if statement.get('Effect') == "Allow": - principal = statement.get('Principal') - if isinstance(principal, basestring) and principal == "*": - self.add_issue(score, message, s3_item, notes=notes) - return - - if isinstance(principal, dict) and principal.get('AWS') == "*": - self.add_issue(score, message, s3_item, notes=notes) - return - - def inspect_policy_cross_account(self, statement, s3_item, complained): - try: - if statement.get('Effect') == 'Allow' and isinstance(statement.get("Principal"), dict): - aws_entries = statement["Principal"].get("AWS", []) - if isinstance(aws_entries, basestring): - aws_entries = [aws_entries] - for aws_entry in aws_entries: - if aws_entry not in complained: - self.process_cross_account(aws_entry, s3_item) - complained.append(aws_entry) - - except Exception as e: - print("Exception in cross_account. {} {}".format(Exception, e)) - import traceback - print(traceback.print_exc()) - - def process_cross_account(self, input, s3_item): - from security_monkey.common.arn import ARN - arn = ARN(input) - - if arn.error and input != input: - message = "POLICY - Bad ARN" - notes = "{}".format(arn) - self.add_issue(3, message, s3_item, notes=notes) - return - - # 'WILDCARD ARN: *' - # This is caught by check_policy_allow_all(), so ignore here. - if '*' == arn.account_number: - print("This is an odd arn: {}".format(arn)) - return - - account = Account.query.filter(Account.identifier==arn.account_number).first() - if account: - # Friendly Account. - if not account.third_party: - message = "POLICY - Friendly Account Access." - notes = "{}".format(account.name) - self.add_issue(0, message, s3_item, notes=notes) - return - # Friendly Third Party - else: - message = "POLICY - Friendly Third Party Account Access." - notes = "{}".format(account.name) - self.add_issue(0, message, s3_item, notes=notes) - return - - # Foreign Unknown Account - message = "POLICY - Unknown Cross Account Access." - notes = "Account ID: {} ARN: {}".format(arn.account_number, input) - self.add_issue(10, message, s3_item, notes=notes) - return + self.add_issue(0, message, item) diff --git a/security_monkey/auditors/sns.py b/security_monkey/auditors/sns.py index 874715e60..7c23f1255 100644 --- a/security_monkey/auditors/sns.py +++ b/security_monkey/auditors/sns.py @@ -19,24 +19,20 @@ .. moduleauthor:: Patrick Kelley @monkeysecurity """ - -from security_monkey.common.arn import ARN -from security_monkey.auditor import Auditor from security_monkey.watchers.sns import SNS -from security_monkey.exceptions import InvalidARN -from security_monkey.exceptions import InvalidSourceOwner -from security_monkey.datastore import Account - -import re +from security_monkey.auditor import Categories, Entity +from security_monkey.auditors.resource_policy_auditor import ResourcePolicyAuditor +from policyuniverse.arn import ARN -class SNSAuditor(Auditor): +class SNSAuditor(ResourcePolicyAuditor): index = SNS.index i_am_singular = SNS.i_am_singular i_am_plural = SNS.i_am_plural def __init__(self, accounts=None, debug=False): super(SNSAuditor, self).__init__(accounts=accounts, debug=debug) + self.policy_keys = ['policy'] def check_snstopicpolicy_empty(self, snsitem): """ @@ -47,7 +43,7 @@ def check_snstopicpolicy_empty(self, snsitem): if snsitem.config.get('policy', {}) == {}: self.add_issue(severity, tag, snsitem, notes=None) - def check_subscriptions_crossaccount(self, snsitem): + def check_subscriptions_crossaccount(self, item): """ "subscriptions": [ { @@ -59,80 +55,27 @@ def check_subscriptions_crossaccount(self, snsitem): } ] """ - subscriptions = snsitem.config.get('subscriptions', []) + subscriptions = item.config.get('subscriptions', []) for subscription in subscriptions: - source = '{0} subscription to {1}'.format( - subscription.get('Protocol', None), - subscription.get('Endpoint', None) - ) - owner = subscription.get('Owner', None) - self._check_cross_account(owner, snsitem, source) - - def _parse_arn(self, arn_input, account_numbers, snsitem): - if arn_input == '*': - notes = "An SNS policy where { 'Principal': { 'AWS': '*' } } must also have" - notes += " a {'Condition': {'StringEquals': { 'AWS:SourceOwner': '' } } }" - notes += " or it is open to the world." - self.add_issue(10, 'SNS Topic open to everyone', snsitem, notes=notes) - return - - arn = ARN(arn_input) - if arn.error: - self.add_issue(3, 'Auditor could not parse ARN', snsitem, notes=arn_input) - return - - if arn.tech == 's3': - notes = "SNS allows access from S3 bucket [{}]. ".format(arn.name) - notes += "Security Monkey does not yet have the capability to determine if this is " - notes += "a friendly S3 bucket. Please verify manually." - self.add_issue(3, 'SNS allows access from S3 bucket', snsitem, notes=notes) - else: - account_numbers.append(arn.account_number) - - def check_snstopicpolicy_crossaccount(self, snsitem): - """ - alert on cross account access - """ - policy = snsitem.config.get('policy', {}) - for statement in policy.get("Statement", []): - account_numbers = [] - princ = statement.get("Principal", {}) - if isinstance(princ, dict): - princ_val = princ.get("AWS") or princ.get("Service") - else: - princ_val = princ - - if princ_val == "*": - condition = statement.get('Condition', {}) - arns = ARN.extract_arns_from_statement_condition(condition) - - if not arns: - tag = "SNS Topic open to everyone" - notes = "An SNS policy where { 'Principal': { 'AWS': '*' } } must also have" - notes += " a {'Condition': {'StringEquals': { 'AWS:SourceOwner': '' } } }" - notes += " or it is open to the world. In this case, anyone is allowed to perform " - notes += " this action(s): {}".format(statement.get("Action")) - self.add_issue(10, tag, snsitem, notes=notes) - - for arn in arns: - self._parse_arn(arn, account_numbers, snsitem) - - else: - if isinstance(princ_val, list): - for entry in princ_val: - arn = ARN(entry) - if arn.error: - self.add_issue(3, 'Auditor could not parse ARN', snsitem, notes=entry) - continue - - if not arn.service: - account_numbers.append(arn.account_number) - else: - arn = ARN(princ_val) - if arn.error: - self.add_issue(3, 'Auditor could not parse ARN', snsitem, notes=princ_val) - elif not arn.service: - account_numbers.append(arn.account_number) - - for account_number in account_numbers: - self._check_cross_account(account_number, snsitem, 'policy') + src_account_number = subscription.get('Owner', None) + + entity = Entity( + category=subscription.get('Protocol'), + value=subscription.get('Endpoint'), + account_identifier=src_account_number, + account_name='UNKNOWN') + + account = self._get_account('identifier', src_account_number) + if not account: + self.record_unknown_access(item, entity, actions=['subscription']) + continue + + if account['name'] == item.account: + # Same Account + continue + + entity.account_name = account['name'] + if account['label'] == 'friendly': + self.record_friendly_access(item, entity, actions=['subscription']) + elif account['label'] == 'thirdparty': + self.record_thirdparty_access(item, entity, actions=['subscription']) diff --git a/security_monkey/auditors/sqs.py b/security_monkey/auditors/sqs.py index 493809f5e..d409f19b2 100644 --- a/security_monkey/auditors/sqs.py +++ b/security_monkey/auditors/sqs.py @@ -20,92 +20,15 @@ """ -from security_monkey.common.arn import ARN -from security_monkey.auditor import Auditor +from security_monkey.auditors.resource_policy_auditor import ResourcePolicyAuditor from security_monkey.watchers.sqs import SQS -import json - -class SQSAuditor(Auditor): +class SQSAuditor(ResourcePolicyAuditor): index = SQS.index i_am_singular = SQS.i_am_singular i_am_plural = SQS.i_am_plural def __init__(self, accounts=None, debug=False): super(SQSAuditor, self).__init__(accounts=accounts, debug=debug) - - def _parse_arn(self, arn_input, account_numbers, sqsitem): - if arn_input == '*': - notes = "An SQS policy where { 'Principal': { 'AWS': '*' } } must also have" - notes += " a {'Condition': {'StringEquals': { 'AWS:SourceOwner': '' } } }" - notes += " or it is open to the world." - self.add_issue(10, 'SQS Queue open to everyone', sqsitem, notes=notes) - return - - arn = ARN(arn_input) - if arn.error: - self.add_issue(3, 'Auditor could not parse ARN', sqsitem, notes=arn_input) - return - - if arn.tech == 's3': - notes = "SQS allows access from S3 bucket [{}]. ".format(arn.name) - notes += "Security Monkey does not yet have the capability to determine if this is " - notes += "a friendly S3 bucket. Please verify manually." - self.add_issue(3, 'SQS allows access from S3 bucket', sqsitem, notes=notes) - else: - account_numbers.append(arn.account_number) - - def check_sqsqueue_crossaccount(self, sqsitem): - """ - alert on cross account access - """ - policy = sqsitem.config['Policy'] - for statement in policy.get("Statement", []): - account_numbers = [] - princ = statement.get("Principal", None) - if not princ: - # It is possible not to define a principal, AWS ignores these statements. - # We should raise an issue. - tag = "SQS Policy is lacking Principal field" - notes = json.dumps(statement) - self.add_issue(5, tag, sqsitem, notes=notes) - continue - if isinstance(princ, dict): - princ_val = princ.get("AWS") or princ.get("Service") - else: - princ_val = princ - - if princ_val == "*": - condition = statement.get('Condition', {}) - arns = ARN.extract_arns_from_statement_condition(condition) - if not arns: - tag = "SQS Queue open to everyone" - notes = "An SQS policy where { 'Principal': { 'AWS': '*' } } must also have" - notes += " a {'Condition': {'ArnEquals': { 'AWS:SourceArn': '' } } }" - notes += " or it is open to the world. In this case, anyone is allowed to perform " - notes += " this action(s): {}".format(statement.get("Action")) - self.add_issue(10, tag, sqsitem, notes=notes) - - for arn in arns: - self._parse_arn(arn, account_numbers, sqsitem) - - else: - if isinstance(princ_val, list): - for entry in princ_val: - arn = ARN(entry) - if arn.error: - self.add_issue(3, 'Auditor could not parse ARN', sqsitem, notes=entry) - continue - - if not arn.service: - account_numbers.append(arn.account_number) - else: - arn = ARN(princ_val) - if arn.error: - self.add_issue(3, 'Auditor could not parse ARN', sqsitem, notes=princ_val) - elif not arn.service: - account_numbers.append(arn.account_number) - - for account_number in account_numbers: - self._check_cross_account(account_number, sqsitem, 'policy') + self.policy_keys = ['Policy'] diff --git a/security_monkey/common/arn.py b/security_monkey/common/arn.py deleted file mode 100644 index b0fae5621..000000000 --- a/security_monkey/common/arn.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2015 Netflix, Inc. -# -# 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. -""" -.. module: security_monkey.common.arn - :platform: Unix - -.. version:: $$VERSION$$ -.. moduleauthor:: Patrick Kelley - -""" - -import re -from security_monkey import app - - -class ARN(object): - tech = None - region = None - account_number = None - name = None - partition = None - error = False - root = False - service = False - - def __init__(self, input): - arn_match = re.search('^arn:([^:]*):([^:]*):([^:]*):(|\*|[\d]{12}|cloudfront):(.+)$', input) - if arn_match: - if arn_match.group(2) == "iam" and arn_match.group(5) == "root": - self.root = True - - self._from_arn(arn_match, input) - return - - acct_number_match = re.search('^(\d{12})+$', input) - if acct_number_match: - self._from_account_number(input) - return - - aws_service_match = re.search('^([^.]*)\.amazonaws\.com$', input) - if aws_service_match: - self._from_aws_service(input, aws_service_match.group(1)) - return - - self.error = True - app.logger.warn('ARN Could not parse [{}].'.format(input)) - - def _from_arn(self, arn_match, input): - self.partition = arn_match.group(1) - self.tech = arn_match.group(2) - self.region = arn_match.group(3) - self.account_number = arn_match.group(4) - self.name = arn_match.group(5) - - def _from_account_number(self, input): - self.account_number = input - - def _from_aws_service(self, input, service): - self.tech = service - self.service = True - - @staticmethod - def extract_arns_from_statement_condition(condition): - condition_subsection \ - = condition.get('ArnEquals', {}) or \ - condition.get('ForAllValues:ArnEquals', {}) or \ - condition.get('ForAnyValue:ArnEquals', {}) or \ - condition.get('ArnLike', {}) or \ - condition.get('ForAllValues:ArnLike', {}) or \ - condition.get('ForAnyValue:ArnLike', {}) or \ - condition.get('StringLike', {}) or \ - condition.get('ForAllValues:StringLike', {}) or \ - condition.get('ForAnyValue:StringLike', {}) or \ - condition.get('StringEquals', {}) or \ - condition.get('StringEqualsIgnoreCase', {}) or \ - condition.get('ForAllValues:StringEquals', {}) or \ - condition.get('ForAnyValue:StringEquals', {}) - - # aws:sourcearn can be found with in lowercase or camelcase or other cases... - condition_arns = [] - for key, value in condition_subsection.iteritems(): - if key.lower() == 'aws:sourcearn' or key.lower() == 'aws:sourceowner': - if isinstance(value, list): - condition_arns.extend(value) - else: - condition_arns.append(value) - - if not isinstance(condition_arns, list): - return [condition_arns] - return condition_arns diff --git a/security_monkey/datastore.py b/security_monkey/datastore.py index 875ed59fd..b0d485fea 100644 --- a/security_monkey/datastore.py +++ b/security_monkey/datastore.py @@ -181,6 +181,11 @@ class ItemAudit(db.Model): score = Column(Integer) issue = Column(String(512)) notes = Column(String(1024)) + action_instructions = Column(Text(), nullable=True) + background_info = Column(Text(), nullable=True) + origin = Column(Text(), nullable=True) + origin_summary = Column(Text(), nullable=True) + class_uuid = Column(String(32), nullable=True) justified = Column(Boolean) justified_user_id = Column(Integer, ForeignKey("user.id"), nullable=True, index=True) justification = Column(String(512)) @@ -285,6 +290,12 @@ def unjustified_score(cls): deferred=True ) + @hybrid_property + def latest_config(self): + """Returns the config from the latest item revision.""" + return db.session.query(ItemRevision + ).filter(ItemRevision.id==self.latest_revision_id).one().config + class ItemComment(db.Model): """ diff --git a/security_monkey/tests/auditors/test_arn.py b/security_monkey/tests/auditors/test_arn.py deleted file mode 100644 index 8f72d8ddc..000000000 --- a/security_monkey/tests/auditors/test_arn.py +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright 2014 Netflix, Inc. -# -# 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. -""" -.. module: security_monkey.tests.auditors.test_arn - :platform: Unix - -.. version:: $$VERSION$$ -.. moduleauthor:: Mike Grima - -""" -from security_monkey.common.arn import ARN -from security_monkey.tests import SecurityMonkeyTestCase -from security_monkey import app - - -class ARNTestCase(SecurityMonkeyTestCase): - def test_from_arn(self): - proper_arns = [ - 'events.amazonaws.com', - 'cloudtrail.amazonaws.com', - 'arn:aws:iam::012345678910:root', - 'arn:aws:iam::012345678910:role/SomeTestRoleForTesting', - 'arn:aws:iam::012345678910:instance-profile/SomeTestInstanceProfileForTesting', - 'arn:aws:iam::012345678910:role/*', - 'arn:aws:iam::012345678910:role/SomeTestRole*', - 'arn:aws:s3:::some-s3-bucket', - 'arn:aws:s3:*:*:some-s3-bucket', - 'arn:aws:s3:::some-s3-bucket/some/path/within/the/bucket' - 'arn:aws:s3:::some-s3-bucket/*', - 'arn:aws:ec2:us-west-2:012345678910:instance/*', - 'arn:aws:ec2:ap-northeast-1:012345678910:security-group/*', - 'arn:aws-cn:ec2:ap-northeast-1:012345678910:security-group/*', - 'arn:aws-us-gov:ec2:gov-west-1:012345678910:instance/*', - 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity EXXXXXXXXXXXXX' - ] - - # Proper ARN Tests: - for arn in proper_arns: - app.logger.info('Testing Proper ARN: {}'.format(arn)) - arn_obj = ARN(arn) - - self.assertFalse(arn_obj.error) - if "root" in arn: - self.assertTrue(arn_obj.root) - else: - self.assertFalse(arn_obj.root) - - if ".amazonaws.com" in arn: - self.assertTrue(arn_obj.service) - else: - self.assertFalse(arn_obj.service) - - bad_arns = [ - 'arn:aws:iam::012345678910', - 'arn:aws:iam::012345678910:', - '*', - 'arn:s3::::', - "arn:arn:arn:arn:arn:arn" - ] - - # Improper ARN Tests: - for arn in bad_arns: - app.logger.info('Testing IMPROPER ARN: {}'.format(arn)) - arn_obj = ARN(arn) - - self.assertTrue(arn_obj.error) - - def test_from_account_number(self): - proper_account_numbers = [ - '012345678912', - '123456789101', - '123456789101' - ] - - improper_account_numbers = [ - '*', - 'O12345678912', # 'O' instead of '0' - 'asdfqwer', - '123456', - '89789456314356132168978945', - '568947897*' - ] - - # Proper account number tests: - for accnt in proper_account_numbers: - app.logger.info('Testing Proper Account Number: {}'.format(accnt)) - arn_obj = ARN(accnt) - - self.assertFalse(arn_obj.error) - - # Improper account number tests: - for accnt in improper_account_numbers: - app.logger.info('Testing IMPROPER Account Number: {}'.format(accnt)) - arn_obj = ARN(accnt) - - self.assertTrue(arn_obj.error) - - def test_extract_arns_from_statement_condition(self): - test_condition_list = [ - 'ArnEquals', - 'ForAllValues:ArnEquals', - 'ForAnyValue:ArnEquals', - 'ArnLike', - 'ForAllValues:ArnLike', - 'ForAnyValue:ArnLike', - 'StringLike', - 'ForAllValues:StringLike', - 'ForAnyValue:StringLike', - 'StringEquals', - 'ForAllValues:StringEquals', - 'ForAnyValue:StringEquals' - ] - - bad_condition_list = [ - 'NotACondition', - 'ArnLikeSomethingNotARealCondition' - ] - - arn_types = [ - ('aws:sourcearn', 'arn:aws:s3:::some-s3-bucket'), - ('aws:sourcearn', 'arn:aws:s3:::some-s3-bucket/*'), - ('aws:sourcearn', "*"), - ('aws:sourceowner', '012345678912'), - ('aws:sourceowner', '*') - ] - - for condition in test_condition_list: - for arn_type in arn_types: - test_condition = { - condition: { - arn_type[0]: arn_type[1] - } - } - - result = ARN.extract_arns_from_statement_condition(test_condition) - self.assertIsInstance(result, list) - self.assertTrue(len(result) > 0) - - for condition in bad_condition_list: - for arn_type in arn_types: - test_condition = { - condition: { - arn_type[0]: arn_type[1] - } - } - - result = ARN.extract_arns_from_statement_condition(test_condition) - self.assertIsInstance(result, list) - self.assertTrue(len(result) == 0) diff --git a/security_monkey/tests/auditors/test_elasticsearch_service.py b/security_monkey/tests/auditors/test_elasticsearch_service.py index 47cede515..1ba207c72 100644 --- a/security_monkey/tests/auditors/test_elasticsearch_service.py +++ b/security_monkey/tests/auditors/test_elasticsearch_service.py @@ -27,6 +27,7 @@ # TODO: Make a ES test for spulec/moto, then make test cases that use it. from security_monkey.watchers.elasticsearch_service import ElasticSearchServiceItem +from security_monkey.auditors.elasticsearch_service import ElasticSearchServiceAuditor CONFIG_ONE = { @@ -282,14 +283,11 @@ } -WHITELIST_CIDRS = [ - ("Test one", "192.168.1.1/32"), - ("Test two", "100.0.0.1/16"), -] class ElasticSearchServiceTestCase(SecurityMonkeyTestCase): def pre_test_setup(self): + ElasticSearchServiceAuditor(accounts=['TEST_ACCOUNT']).OBJECT_STORE.clear() self.es_items = [ ElasticSearchServiceItem(region="us-east-1", account="TEST_ACCOUNT", name="es_test", config=CONFIG_ONE), ElasticSearchServiceItem(region="us-west-2", account="TEST_ACCOUNT", name="es_test_2", config=CONFIG_TWO), @@ -313,39 +311,45 @@ def pre_test_setup(self): db.session.add(account) db.session.commit() - def test_es_auditor(self): - from security_monkey.auditors.elasticsearch_service import ElasticSearchServiceAuditor - es_auditor = ElasticSearchServiceAuditor(accounts=["012345678910"]) - # Add some test network whitelists into this: - es_auditor.network_whitelist = [] + # es_auditor.network_whitelist = [] + WHITELIST_CIDRS = [ + ("Test one", "192.168.1.1/32"), + ("Test two", "100.0.0.0/16"), + ] for cidr in WHITELIST_CIDRS: whitelist_cidr = NetworkWhitelistEntry() - whitelist_cidr.cidr = cidr[1] whitelist_cidr.name = cidr[0] + whitelist_cidr.notes = cidr[0] + whitelist_cidr.cidr = cidr[1] + db.session.add(whitelist_cidr) + db.session.commit() - es_auditor.network_whitelist.append(whitelist_cidr) + def test_es_auditor(self): + es_auditor = ElasticSearchServiceAuditor(accounts=["012345678910"]) + es_auditor.prep_for_audit() for es_domain in self.es_items: - es_auditor.check_es_access_policy(es_domain) + es_auditor.check_internet_accessible(es_domain) + es_auditor.check_friendly_cross_account(es_domain) + es_auditor.check_unknown_cross_account(es_domain) + es_auditor.check_root_cross_account(es_domain) # Check for correct number of issues located: - # CONFIG ONE: self.assertEquals(len(self.es_items[0].audit_issues), 1) - self.assertEquals(self.es_items[0].audit_issues[0].score, 20) + self.assertEquals(self.es_items[0].audit_issues[0].score, 10) # CONFIG TWO: self.assertEquals(len(self.es_items[1].audit_issues), 1) - self.assertEquals(self.es_items[1].audit_issues[0].score, 20) + self.assertEquals(self.es_items[1].audit_issues[0].score, 10) # CONFIG THREE: - self.assertEquals(len(self.es_items[2].audit_issues), 2) - self.assertEquals(self.es_items[2].audit_issues[0].score, 5) - self.assertEquals(self.es_items[2].audit_issues[1].score, 7) + self.assertEquals(len(self.es_items[2].audit_issues), 1) + self.assertEquals(self.es_items[2].audit_issues[0].score, 10) # CONFIG FOUR: self.assertEquals(len(self.es_items[3].audit_issues), 1) - self.assertEquals(self.es_items[3].audit_issues[0].score, 20) + self.assertEquals(self.es_items[3].audit_issues[0].score, 10) # CONFIG FIVE: self.assertEquals(len(self.es_items[4].audit_issues), 0) @@ -354,16 +358,15 @@ def test_es_auditor(self): self.assertEquals(len(self.es_items[5].audit_issues), 0) # CONFIG SEVEN: - self.assertEquals(len(self.es_items[6].audit_issues), 3) - self.assertEquals(self.es_items[6].audit_issues[0].score, 5) - self.assertEquals(self.es_items[6].audit_issues[1].score, 5) - self.assertEquals(self.es_items[6].audit_issues[2].score, 7) + self.assertEquals(len(self.es_items[6].audit_issues), 2) + self.assertEquals(self.es_items[6].audit_issues[0].score, 10) + self.assertEquals(self.es_items[6].audit_issues[1].score, 10) # CONFIG EIGHT: self.assertEquals(len(self.es_items[7].audit_issues), 1) - self.assertEquals(self.es_items[7].audit_issues[0].score, 20) + self.assertEquals(self.es_items[7].audit_issues[0].score, 10) # CONFIG NINE: self.assertEquals(len(self.es_items[8].audit_issues), 2) - self.assertEquals(self.es_items[8].audit_issues[0].score, 6) - self.assertEquals(self.es_items[8].audit_issues[1].score, 10) + self.assertEquals(self.es_items[8].audit_issues[0].score, 10) + self.assertEquals(self.es_items[8].audit_issues[1].score, 6) diff --git a/security_monkey/tests/auditors/test_kms.py b/security_monkey/tests/auditors/test_kms.py index 811667e90..5546aa69c 100644 --- a/security_monkey/tests/auditors/test_kms.py +++ b/security_monkey/tests/auditors/test_kms.py @@ -20,12 +20,18 @@ """ - +from security_monkey import db +from security_monkey.datastore import Account, AccountType from security_monkey.tests import SecurityMonkeyTestCase from security_monkey.auditors.kms import KMSAuditor from security_monkey.watchers.kms import KMSMasterKey +from copy import deepcopy + -key_no_condition = { +# Internet Accessible +# No Condition +# rotation Enabled +key0 = { "Origin": "AWS_KMS", "KeyId": "key_id", "Description": "Description", @@ -50,12 +56,15 @@ } ], "KeyState": "Enabled", + "KeyRotationEnabled": True, "CreationDate": "2017-01-05T20:39:18.960000+00:00", "Arn": "arn:aws:kms:us-east-1:123456789123:key/key_id", "AWSAccountId": "123456789123" } -key_arn_is_role_id = { +# Access provided to role in same account +# Rotation Not Enabled +key1 = { "Origin": "AWS_KMS", "KeyId": "key_id", "Description": "Description", @@ -82,13 +91,14 @@ } }, "Principal": { - "AWS": "role_id_for_arn" + "AWS": "arn:aws:iam::123456789123:role/SuperRole" } } ] } ], "KeyState": "Enabled", + "KeyRotationEnabled": False, "CreationDate": "2017-01-05T20:39:18.960000+00:00", "Arn": "arn:aws:kms:us-east-1:123456789123:key/key_id", "AWSAccountId": "123456789123" @@ -97,20 +107,124 @@ class KMSTestCase(SecurityMonkeyTestCase): - def test_check_for_kms_policy_with_foreign_account_no_condition(self): - auditor = KMSAuditor(accounts=['unittestaccount']) - item = KMSMasterKey(arn='arn:aws:kms:us-east-1:123456789123:key/key_id', - config=key_no_condition) + def pre_test_setup(self): + KMSAuditor(accounts=['TEST_ACCOUNT']).OBJECT_STORE.clear() + account_type_result = AccountType(name='AWS') + db.session.add(account_type_result) + db.session.commit() + + # main + account = Account(identifier="123456789123", name="TEST_ACCOUNT", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT", + third_party=False, active=True) + # friendly + account2 = Account(identifier="222222222222", name="TEST_ACCOUNT_TWO", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT_TWO", + third_party=False, active=True) + # third party + account3 = Account(identifier="333333333333", name="TEST_ACCOUNT_THREE", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT_THREE", + third_party=True, active=True) + + db.session.add(account) + db.session.add(account2) + db.session.add(account3) + db.session.commit() + + def test_check_internet_accessible(self): + auditor = KMSAuditor(accounts=['TEST_ACCOUNT']) + # Make sure it detects an internet accessible policy + item = KMSMasterKey( + arn='arn:aws:kms:us-east-1:123456789123:key/key_id', + config=key0) + auditor.check_internet_accessible(item) + + self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 10) + + # Copy of key0, but not internet accessible + key0_fixed = deepcopy(key0) + key0_fixed['Policies'][0]['Statement'][0]['Principal']['AWS'] \ + = 'arn:aws:iam::123456789123:role/SomeRole' + item = KMSMasterKey( + arn='arn:aws:kms:us-east-1:123456789123:key/key_id', + config=key0_fixed) + auditor.check_internet_accessible(item) self.assertEquals(len(item.audit_issues), 0) - auditor.check_for_kms_policy_with_foreign_account(item) + + def test_check_friendly_cross_account(self): + auditor = KMSAuditor(accounts=['TEST_ACCOUNT']) + auditor.prep_for_audit() + + key0_friendly_cross_account = deepcopy(key0) + key0_friendly_cross_account['Policies'][0]['Statement'][0]['Principal']['AWS'] \ + = 'arn:aws:iam::222222222222:role/SomeRole' + item = KMSMasterKey( + account='TEST_ACCOUNT', + arn='arn:aws:kms:us-east-1:123456789123:key/key_id', + config=key0_friendly_cross_account) + auditor.check_friendly_cross_account(item) self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 0) + + def test_check_thirdparty_cross_account(self): + auditor = KMSAuditor(accounts=['TEST_ACCOUNT']) + auditor.prep_for_audit() - def test_check_for_kms_policy_with_foreign_account_key_arn_is_role_id(self): + key0_friendly_cross_account = deepcopy(key0) + key0_friendly_cross_account['Policies'][0]['Statement'][0]['Principal']['AWS'] \ + = 'arn:aws:iam::333333333333:role/SomeRole' + item = KMSMasterKey( + account='TEST_ACCOUNT', + arn='arn:aws:kms:us-east-1:123456789123:key/key_id', + config=key0_friendly_cross_account) + auditor.check_thirdparty_cross_account(item) + self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 0) + + def test_check_unknown_cross_account(self): + auditor = KMSAuditor(accounts=['TEST_ACCOUNT']) + auditor.prep_for_audit() + + key0_friendly_cross_account = deepcopy(key0) + key0_friendly_cross_account['Policies'][0]['Statement'][0]['Principal']['AWS'] \ + = 'arn:aws:iam::444444444444:role/SomeRole' + item = KMSMasterKey( + account='TEST_ACCOUNT', + arn='arn:aws:kms:us-east-1:123456789123:key/key_id', + config=key0_friendly_cross_account) + auditor.check_unknown_cross_account(item) + self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 10) + + def test_check_root_cross_account(self): + auditor = KMSAuditor(accounts=['TEST_ACCOUNT']) + auditor.prep_for_audit() + + key0_friendly_cross_account = deepcopy(key0) + key0_friendly_cross_account['Policies'][0]['Statement'][0]['Principal']['AWS'] \ + = 'arn:aws:iam::222222222222:root' + item = KMSMasterKey( + account='TEST_ACCOUNT', + arn='arn:aws:kms:us-east-1:123456789123:key/key_id', + config=key0_friendly_cross_account) + auditor.check_root_cross_account(item) + self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 6) + + def test_check_for_kms_key_rotation(self): auditor = KMSAuditor(accounts=['unittestaccount']) item = KMSMasterKey(arn='arn:aws:kms:us-east-1:123456789123:key/key_id', - config=key_arn_is_role_id) + config=key0) + auditor.check_for_kms_key_rotation(item) self.assertEquals(len(item.audit_issues), 0) - auditor.check_for_kms_policy_with_foreign_account(item) - self.assertEquals(len(item.audit_issues), 0) + + item = KMSMasterKey(arn='arn:aws:kms:us-east-1:123456789123:key/key_id', + config=key1) + + auditor.check_for_kms_key_rotation(item) + + self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 1) diff --git a/security_monkey/tests/auditors/test_resouce_policy_auditor.py b/security_monkey/tests/auditors/test_resouce_policy_auditor.py new file mode 100644 index 000000000..b78ea7a4a --- /dev/null +++ b/security_monkey/tests/auditors/test_resouce_policy_auditor.py @@ -0,0 +1,456 @@ +from security_monkey.tests import SecurityMonkeyTestCase +from security_monkey.auditor import Entity +from security_monkey.auditors.resource_policy_auditor import ResourcePolicyAuditor +from security_monkey import db +from security_monkey.watcher import ChangeItem +from security_monkey.datastore import Datastore +from security_monkey.datastore import Account, AccountType, ItemAudit +from collections import namedtuple +from policyuniverse.policy import Policy +from copy import deepcopy + + +Item = namedtuple('Item', 'config account') + +# Example KMS Config +# Internet Accessible +# No Condition +# rotation Enabled +key0 = { + "Origin": "AWS_KMS", + "KeyId": "key_id", + "Description": "Description", + "Enabled": True, + "KeyUsage": "ENCRYPT_DECRYPT", + "Grants": [], + "Policy": [ + { + "Version": "2012-10-17", + "Id": "key-consolepolicy-2", + "Statement": [ + { + "Action": "kms:*", + "Sid": "Enable IAM User Permissions", + "Resource": "*", + "Effect": "Allow", + "Principal": { + "AWS": "*" + } + } + ] + } + ], + "KeyState": "Enabled", + "KeyRotationEnabled": True, + "CreationDate": "2017-01-05T20:39:18.960000+00:00", + "Arn": "arn:aws:kms:us-east-1:123456789123:key/key_id", + "AWSAccountId": "123456789123" +} + + +class ResourcePolicyTestCase(SecurityMonkeyTestCase): + + def pre_test_setup(self): + ResourcePolicyAuditor(accounts=['TEST_ACCOUNT']).OBJECT_STORE.clear() + account_type_result = AccountType(name='AWS') + db.session.add(account_type_result) + db.session.commit() + + # main + account = Account(identifier="012345678910", name="TEST_ACCOUNT", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT", + third_party=False, active=True) + # friendly + account2 = Account(identifier="222222222222", name="TEST_ACCOUNT_TWO", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT_TWO", + third_party=False, active=True) + # third party + account3 = Account(identifier="333333333333", name="TEST_ACCOUNT_THREE", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT_THREE", + third_party=True, active=True) + + db.session.add(account) + db.session.add(account2) + db.session.add(account3) + db.session.commit() + + datastore = Datastore() + # S3 + datastore.store('s3', 'us-east-1', 'TEST_ACCOUNT', 'my-test-s3-bucket', + True, dict(), arn='arn:aws:s3:::my-test-s3-bucket') + + datastore.store('s3', 'us-east-1', 'TEST_ACCOUNT_TWO', 'my-test-s3-bucket-two', + True, dict(), arn='arn:aws:s3:::my-test-s3-bucket-two') + + datastore.store('s3', 'us-east-1', 'TEST_ACCOUNT_THREE', 'my-test-s3-bucket-three', + True, dict(), arn='arn:aws:s3:::my-test-s3-bucket-three') + + # IAM User + datastore.store('iamuser', 'us-east-1', 'TEST_ACCOUNT', 'my-test-iam-user', + True, dict(UserId='AIDA11111111111111111', UserName='my-test-iam-user'), + arn='arn:aws:iam::012345678910:user/my-test-iam-user') + + datastore.store('iamuser', 'us-east-1', 'TEST_ACCOUNT_TWO', 'my-test-iam-user-two', + True, dict(UserId='AIDA22222222222222222', UserName='my-test-iam-user-two'), + arn='arn:aws:iam::222222222222:user/my-test-iam-user-two') + + datastore.store('iamuser', 'us-east-1', 'TEST_ACCOUNT_THREE', 'my-test-iam-user-three', + True, dict(UserId='AIDA33333333333333333', UserName='my-test-iam-user-three'), + arn='arn:aws:iam::333333333333:user/my-test-iam-user-three') + + # IAM Role + datastore.store('iamrole', 'us-east-1', 'TEST_ACCOUNT', 'my-test-iam-role', + True, dict(RoleId='AISA11111111111111111', RoleName='my-test-iam-role'), + arn='arn:aws:iam::012345678910:role/my-test-iam-role') + + datastore.store('iamrole', 'us-east-1', 'TEST_ACCOUNT_TWO', 'my-test-iam-role-two', + True, dict(RoleId='AISA22222222222222222', RoleName='my-test-iam-role-two'), + arn='arn:aws:iam::222222222222:role/my-test-iam-role-two') + + datastore.store('iamrole', 'us-east-1', 'TEST_ACCOUNT_THREE', 'my-test-iam-role-three', + True, dict(RoleId='AISA33333333333333333', RoleName='my-test-iam-role-three'), + arn='arn:aws:iam::333333333333:role/my-test-iam-role-three') + + # NAT Gateway + datastore.store('natgateway', 'us-east-1', 'TEST_ACCOUNT', 'my-test-natgateway', + True, dict(nat_gateway_addresses=[dict(public_ip='54.11.11.11', private_ip='172.16.11.11')]), + arn=None) # natgateway has no ARN :( + + datastore.store('natgateway', 'us-east-1', 'TEST_ACCOUNT_TWO', 'my-test-natgateway-two', + True, dict(nat_gateway_addresses=[dict(public_ip='54.22.22.22', private_ip='172.16.22.22')]), + arn=None) # natgateway has no ARN :( + + datastore.store('natgateway', 'us-east-1', 'TEST_ACCOUNT_THREE', 'my-test-natgateway-three', + True, dict(nat_gateway_addresses=[dict(public_ip='54.33.33.33', private_ip='172.16.33.33')]), + arn=None) # natgateway has no ARN :( + + # VPC + datastore.store('vpc', 'us-east-1', 'TEST_ACCOUNT', 'my-test-vpc', True, + dict(id='vpc-11111111', cidr_block='10.1.1.1/18'), + arn='arn:aws:ec2:us-east-1:012345678910:vpc/vpc-11111111') + + datastore.store('vpc', 'us-east-1', 'TEST_ACCOUNT_TWO', 'my-test-vpc-two', True, + dict(id='vpc-22222222', cidr_block='10.2.2.2/18'), + arn='arn:aws:ec2:us-east-1:222222222222:vpc/vpc-22222222') + + datastore.store('vpc', 'us-east-1', 'TEST_ACCOUNT_THREE', 'my-test-vpc-three', True, + dict(id='vpc-33333333', cidr_block='10.3.3.3/18'), + arn='arn:aws:ec2:us-east-1:333333333333:vpc/vpc-33333333') + + # VPC Service Endpoint (For S3 and things) + datastore.store('endpoint', 'us-east-1', 'TEST_ACCOUNT', 'my-test-vpce', + True, dict(id='vpce-11111111'), + arn=None) # vpce has no ARN :( + + datastore.store('endpoint', 'us-east-1', 'TEST_ACCOUNT_TWO', 'my-test-vpce-two', + True, dict(id='vpce-22222222'), + arn=None) # vpce has no ARN :( + + datastore.store('endpoint', 'us-east-1', 'TEST_ACCOUNT_THREE', 'my-test-vpce-three', + True, dict(id='vpce-33333333'), + arn=None) # vpce has no ARN :( + + def test_load_policies(self): + + policy01 = dict(Version='2012-10-08', Statement=[]) + test_item = Item(account=None, config=dict(Policy=policy01)) + + rpa = ResourcePolicyAuditor(accounts=["012345678910"]) + # Policy class has no equivelance test at the moment. + # Compare the underlying dicts instead + policies = [policy.policy for policy in rpa.load_policies(test_item)] + self.assertEqual([policy01], policies) + + + policy02 = dict(Version='2012-10-08', Statement=[ + dict( + Effect='Allow', + Action='*', + Resource='*')]) + + policy03 = dict(Version='2012-10-08', Statement=[ + dict( + Effect='Allow', + Action='lambda:*', + Resource='*')]) + + policy04 = dict(Version='2012-10-08', Statement=[ + dict( + Effect='Allow', + Action='ec2:*', + Resource='*')]) + + # simulate a lambda function, which contains multiple policies + test_item = Item( + account=None, + config=dict( + Policies=dict( + Aliases=dict( + stable=policy01), + DEFAULT=policy02, + Versions={ + "3": policy03, + "4": policy04 + }))) + + rpa.policy_keys = ['Policies$Aliases$*', 'Policies$DEFAULT', 'Policies$Versions$*'] + policies = [policy.policy for policy in rpa.load_policies(test_item)] + self.assertEqual([policy01, policy02, policy03, policy04], policies) + + def test_prep_for_audit(self): + rpa = ResourcePolicyAuditor(accounts=["012345678910"]) + rpa.prep_for_audit() + + self.assertEqual(rpa.OBJECT_STORE['s3']['my-test-s3-bucket'], set(['012345678910'])) + self.assertEqual(rpa.OBJECT_STORE['ACCOUNTS']['FRIENDLY'], set(['012345678910', '222222222222'])) + self.assertEqual(rpa.OBJECT_STORE['ACCOUNTS']['THIRDPARTY'], set(['333333333333'])) + + self.assertEqual( + set(rpa.OBJECT_STORE['userid'].keys()), + set(['AIDA11111111111111111', 'AISA11111111111111111', + 'AIDA22222222222222222', 'AISA22222222222222222', + 'AIDA33333333333333333', 'AISA33333333333333333'])) + + self.assertEqual( + set(rpa.OBJECT_STORE['cidr'].keys()), + set(['10.1.1.1/18', '172.16.11.11', '54.11.11.11', + '10.2.2.2/18', '172.16.22.22', '54.22.22.22', + '10.3.3.3/18', '172.16.33.33', '54.33.33.33'])) + + self.assertEqual( + set(rpa.OBJECT_STORE['vpc'].keys()), + set(['vpc-11111111', 'vpc-22222222', 'vpc-33333333'])) + + self.assertEqual( + set(rpa.OBJECT_STORE['vpce'].keys()), + set(['vpce-11111111', 'vpce-22222222', 'vpce-33333333'])) + + def test_inspect_entity(self): + rpa = ResourcePolicyAuditor(accounts=["012345678910"]) + rpa.prep_for_audit() + + # All conditions are SAME account. + policy01 = dict( + Version='2010-08-14', + Statement=[ + dict( + Effect='Allow', + Principal='arn:aws:iam::012345678910:root', + Action=['ec2:*'], + Resource='*', + Condition={ + 'StringEquals': { + 'AWS:SourceOwner': '012345678910', + 'AWS:SourceARN': 'arn:aws:iam::012345678910:root', + 'AWS:SourceVPC': 'vpc-11111111', + 'AWS:Sourcevpce': 'vpce-11111111', + 'AWS:username': 'my-test-iam-role' + }, 'StringLike': { + 'AWS:userid': ['AIDA11111111111111111:*', 'AISA11111111111111111:*'] + }, 'IpAddress': { + 'AWS:SourceIP': ['54.11.11.11', '10.1.1.1/18', '172.16.11.11'] + }})]) + + test_item = Item(account='TEST_ACCOUNT', config=None) + policy = Policy(policy01) + for who in policy.whos_allowed(): + entity = Entity.from_tuple(who) + self.assertEqual(set(['SAME']), rpa.inspect_entity(entity, test_item)) + + # All conditions are FRIENDLY account. + policy02 = dict( + Version='2010-08-14', + Statement=[ + dict( + Effect='Allow', + Principal='arn:aws:iam::222222222222:root', + Action=['ec2:*'], + Resource='*', + Condition={ + 'StringEquals': { + 'AWS:SourceOwner': '222222222222', + 'AWS:SourceARN': 'arn:aws:s3:::my-test-s3-bucket-two', + 'AWS:SourceVPC': 'vpc-22222222', + 'AWS:Sourcevpce': 'vpce-22222222', + 'AWS:username': 'my-test-iam-role-two' + }, 'StringLike': { + 'AWS:userid': ['AIDA22222222222222222:*', 'AISA22222222222222222:*'] + }, 'IpAddress': { + 'AWS:SourceIP': ['54.22.22.22', '10.2.2.2/18', '172.16.22.22'] + }})]) + + test_item = Item(account='TEST_ACCOUNT', config=None) + policy = Policy(policy02) + for who in policy.whos_allowed(): + entity = Entity.from_tuple(who) + self.assertEqual(set(['FRIENDLY']), rpa.inspect_entity(entity, test_item)) + + # All conditions are THIRDPARTY account. + policy03 = dict( + Version='2010-08-14', + Statement=[ + dict( + Effect='Allow', + Principal='arn:aws:iam::333333333333:root', + Action=['ec2:*'], + Resource='*', + Condition={ + 'StringEquals': { + 'AWS:SourceOwner': '333333333333', + 'AWS:SourceARN': 'arn:aws:iam::333333333333:root', + 'AWS:SourceVPC': 'vpc-33333333', + 'AWS:Sourcevpce': 'vpce-33333333', + 'AWS:username': 'my-test-iam-role-three' + }, 'StringLike': { + 'AWS:userid': ['AIDA33333333333333333:*', 'AISA33333333333333333:*'] + }, 'IpAddress': { + 'AWS:SourceIP': ['54.33.33.33', '10.3.3.3/18', '172.16.33.33'] + }})]) + + test_item = Item(account='TEST_ACCOUNT', config=None) + policy = Policy(policy03) + for who in policy.whos_allowed(): + entity = Entity.from_tuple(who) + self.assertEqual(set(['THIRDPARTY']), rpa.inspect_entity(entity, test_item)) + + # All conditions are from an UNKNOWN account. + policy04 = dict( + Version='2010-08-14', + Statement=[ + dict( + Effect='Allow', + Principal='arn:aws:iam::444444444444:root', + Action=['ec2:*'], + Resource='*', + Condition={ + 'StringEquals': { + 'AWS:SourceOwner': '444444444444', + 'AWS:SourceARN': 'arn:aws:iam::444444444444:root', + 'AWS:SourceVPC': 'vpc-44444444', + 'AWS:Sourcevpce': 'vpce-44444444', + 'AWS:username': 'my-test-iam-role-four' + }, 'StringLike': { + 'AWS:userid': ['AIDA44444444444444444:*', 'AISA44444444444444444:*'] + }, 'IpAddress': { + 'AWS:SourceIP': ['54.44.44.44', '10.4.4.4/18', '172.16.44.44'] + }})]) + + test_item = Item(account='TEST_ACCOUNT', config=None) + policy = Policy(policy04) + for who in policy.whos_allowed(): + entity = Entity.from_tuple(who) + self.assertEqual(set(['UNKNOWN']), rpa.inspect_entity(entity, test_item)) + + def test_check_internet_accessible(self): + rpa = ResourcePolicyAuditor(accounts=["012345678910"]) + rpa.prep_for_audit() + + policy01 = dict( + Version='2010-08-14', + Statement=[ + dict( + Effect='Allow', + Principal='arn:aws:iam::*:root', + Action=['ec2:*'], + Resource='*')]) + + test_item = Item(account='TEST_ACCOUNT', config=dict(Policy=policy01)) + def mock_add_issue(score, issue, item, notes=None, action_instructions=None): + self.assertEqual(10, score) + self.assertEqual('Internet Accessible', issue) + self.assertEqual('Entity: [principal:*] Actions: ["ec2:*"]', notes) + + rpa.add_issue = lambda *args, **kwargs: mock_add_issue(*args, **kwargs) + rpa.check_internet_accessible(test_item) + + policy02 = dict( + Version='2010-08-14', + Statement=[ + dict( + Effect='Allow', + Principal='arn:aws:iam::012345678910:root', + Action=['ec2:*'], + Resource='*')]) + + test_item = Item(account='TEST_ACCOUNT', config=dict(Policy=policy02)) + + def mock_add_issue_two(score, issue, item, notes=None): + # should not get here + self.assertTrue(False) + + rpa.add_issue = lambda *args, **kwargs: mock_add_issue_two(*args, **kwargs) + rpa.check_internet_accessible(test_item) + + def test_check_friendly_cross_account(self): + rpa = ResourcePolicyAuditor(accounts=["012345678910"]) + rpa.prep_for_audit() + + policy01 = dict( + Version='2010-08-14', + Statement=[ + dict( + Effect='Allow', + Principal='arn:aws:iam::222222222222:root', + Action=['ec2:*'], + Resource='*')]) + + test_item = Item(account='TEST_ACCOUNT', config=dict(Policy=policy01)) + def mock_add_issue(score, issue, item, notes=None): + self.assertEqual(0, score) + self.assertEqual('Friendly Cross Account', issue) + self.assertEqual('Account: [222222222222/TEST_ACCOUNT_TWO] Entity: [principal:arn:aws:iam::222222222222:root] Actions: ["ec2:*"]', notes) + + rpa.add_issue = lambda *args, **kwargs: mock_add_issue(*args, **kwargs) + rpa.check_friendly_cross_account(test_item) + + def test_check_unknown_cross_account(self): + rpa = ResourcePolicyAuditor(accounts=["012345678910"]) + rpa.prep_for_audit() + + policy01 = dict( + Version='2010-08-14', + Statement=[ + dict( + Effect='Allow', + Principal='arn:aws:iam::444444444444:root', + Action=['ec2:*'], + Resource='*')]) + + test_item = Item(account='TEST_ACCOUNT', config=dict(Policy=policy01)) + def mock_add_issue(score, issue, item, notes=None): + self.assertEqual(10, score) + self.assertEqual('Unknown Access', issue) + self.assertEqual('Entity: [principal:arn:aws:iam::444444444444:root] Actions: ["ec2:*"]', notes) + + rpa.add_issue = lambda *args, **kwargs: mock_add_issue(*args, **kwargs) + rpa.check_unknown_cross_account(test_item) + + def test_check_thirdparty_cross_account(self): + rpa = ResourcePolicyAuditor(accounts=['TEST_ACCOUNT']) + rpa.prep_for_audit() + + key0_friendly_cross_account = deepcopy(key0) + key0_friendly_cross_account['Policy'][0]['Statement'][0]['Principal']['AWS'] \ + = 'arn:aws:iam::333333333333:role/SomeRole' + item = ChangeItem( + account='TEST_ACCOUNT', + arn='arn:aws:kms:us-east-1:012345678910:key/key_id', + new_config=key0_friendly_cross_account) + rpa.check_thirdparty_cross_account(item) + self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 0) + + def test_check_root_cross_account(self): + rpa = ResourcePolicyAuditor(accounts=['TEST_ACCOUNT']) + rpa.prep_for_audit() + + key0_friendly_cross_account = deepcopy(key0) + key0_friendly_cross_account['Policy'][0]['Statement'][0]['Principal']['AWS'] \ + = 'arn:aws:iam::222222222222:root' + item = ChangeItem( + account='TEST_ACCOUNT', + arn='arn:aws:kms:us-east-1:012345678910:key/key_id', + new_config=key0_friendly_cross_account) + rpa.check_root_cross_account(item) + self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 6) diff --git a/security_monkey/tests/auditors/test_s3.py b/security_monkey/tests/auditors/test_s3.py index faa15a2ca..fc0b38e56 100644 --- a/security_monkey/tests/auditors/test_s3.py +++ b/security_monkey/tests/auditors/test_s3.py @@ -174,72 +174,330 @@ } """) +# ACL with AllUsers: +CONFIG_FIVE = json.loads(b"""{ + "Acceleration": null, + "AnalyticsConfigurations": [], + "Arn": "arn:aws:s3:::bucket5", + "Cors": [], + "GrantReferences": { + "23984723987489237489237489237489uwedfjhdsjklfhksdfh2389": "test_accnt1" + }, + "Grants": { + "http://acs.amazonaws.com/groups/global/AllUsers": [ + "READ" + ] + }, + "InventoryConfigurations": [], + "LifecycleRules": [], + "Logging": {}, + "MetricsConfigurations": [], + "Notifications": {}, + "Owner": { + "ID": "23984723987489237489237489237489uwedfjhdsjklfhksdfh2389" + }, + "Policy": null, + "Region": "us-east-1", + "Replication": {}, + "Tags": { + "LOL": "UNITTESTS" + }, + "Versioning": {}, + "Website": null, + "_version": 5 + } + """) + +# ACL with AuthenticatedUsers: +CONFIG_SIX = json.loads(b"""{ + "Acceleration": null, + "AnalyticsConfigurations": [], + "Arn": "arn:aws:s3:::bucket6", + "Cors": [], + "GrantReferences": { + "23984723987489237489237489237489uwedfjhdsjklfhksdfh2389": "test_accnt1" + }, + "Grants": { + "http://acs.amazonaws.com/groups/global/AuthenticatedUsers": [ + "READ" + ] + }, + "InventoryConfigurations": [], + "LifecycleRules": [], + "Logging": {}, + "MetricsConfigurations": [], + "Notifications": {}, + "Owner": { + "ID": "23984723987489237489237489237489uwedfjhdsjklfhksdfh2389" + }, + "Policy": null, + "Region": "us-east-1", + "Replication": {}, + "Tags": { + "LOL": "UNITTESTS" + }, + "Versioning": {}, + "Website": null, + "_version": 5 + } + """) + +# ACL with LogDelivery: +CONFIG_SEVEN = json.loads(b"""{ + "Acceleration": null, + "AnalyticsConfigurations": [], + "Arn": "arn:aws:s3:::bucket7", + "Cors": [], + "GrantReferences": { + "23984723987489237489237489237489uwedfjhdsjklfhksdfh2389": "test_accnt1" + }, + "Grants": { + "http://acs.amazonaws.com/groups/s3/LogDelivery": [ + "READ" + ] + }, + "InventoryConfigurations": [], + "LifecycleRules": [], + "Logging": {}, + "MetricsConfigurations": [], + "Notifications": {}, + "Owner": { + "ID": "23984723987489237489237489237489uwedfjhdsjklfhksdfh2389" + }, + "Policy": null, + "Region": "us-east-1", + "Replication": {}, + "Tags": { + "LOL": "UNITTESTS" + }, + "Versioning": {}, + "Website": null, + "_version": 5 + } + """) + +# ACL with deprecated friendly account name: +CONFIG_EIGHT = json.loads(b"""{ + "Acceleration": null, + "AnalyticsConfigurations": [], + "Arn": "arn:aws:s3:::bucket8", + "Cors": [], + "GrantReferences": { + "23984723987489237489237489237489uwedfjhdsjklfhksdfh2389": "test_accnt1" + }, + "Grants": { + "test_accnt2": [ + "READ" + ] + }, + "InventoryConfigurations": [], + "LifecycleRules": [], + "Logging": {}, + "MetricsConfigurations": [], + "Notifications": {}, + "Owner": { + "ID": "23984723987489237489237489237489uwedfjhdsjklfhksdfh2389" + }, + "Policy": null, + "Region": "us-east-1", + "Replication": {}, + "Tags": { + "LOL": "UNITTESTS" + }, + "Versioning": {}, + "Website": null, + "_version": 5 + } + """) + +# ACL with deprecated thirdparty account name: +CONFIG_NINE = json.loads(b"""{ + "Acceleration": null, + "AnalyticsConfigurations": [], + "Arn": "arn:aws:s3:::bucket9", + "Cors": [], + "GrantReferences": { + "23984723987489237489237489237489uwedfjhdsjklfhksdfh2389": "test_accnt1" + }, + "Grants": { + "test_accnt3": [ + "READ" + ] + }, + "InventoryConfigurations": [], + "LifecycleRules": [], + "Logging": {}, + "MetricsConfigurations": [], + "Notifications": {}, + "Owner": { + "ID": "23984723987489237489237489237489uwedfjhdsjklfhksdfh2389" + }, + "Policy": null, + "Region": "us-east-1", + "Replication": {}, + "Tags": { + "LOL": "UNITTESTS" + }, + "Versioning": {}, + "Website": null, + "_version": 5 + } + """) class S3AuditorTestCase(SecurityMonkeyTestCase): def pre_test_setup(self): + S3Auditor(accounts=['TEST_ACCOUNT']).OBJECT_STORE.clear() self.s3_items = [ + # Same Account CloudAuxChangeItem(region="us-east-1", account="TEST_ACCOUNT", name="bucket1", config=CONFIG_ONE), + # ACL with unknown cross account access CloudAuxChangeItem(region="us-east-1", account="TEST_ACCOUNT", name="bucket2", config=CONFIG_TWO), + # ACL with friendly access CloudAuxChangeItem(region="us-east-1", account="TEST_ACCOUNT2", name="bucket3", config=CONFIG_THREE), - CloudAuxChangeItem(region="us-east-1", account="TEST_ACCOUNT3", name="bucket4", config=CONFIG_FOUR) + # ACL with friendly thirdparty access + CloudAuxChangeItem(region="us-east-1", account="TEST_ACCOUNT3", name="bucket4", config=CONFIG_FOUR), + # Bucket without a policy + CloudAuxChangeItem(region="us-east-1", account="TEST_ACCOUNT", name="bucket5", config=CONFIG_FOUR), + # Bucket with AllUsers + CloudAuxChangeItem(region="us-east-1", account="TEST_ACCOUNT", name="bucket5", config=CONFIG_FIVE), + # Bucket with AuthenticatedUsers + CloudAuxChangeItem(region="us-east-1", account="TEST_ACCOUNT", name="bucket6", config=CONFIG_SIX), + # Bucket with LogDelivery + CloudAuxChangeItem(region="us-east-1", account="TEST_ACCOUNT", name="bucket7", config=CONFIG_SEVEN), + # Bucket with deprecated friendly short s3 name + CloudAuxChangeItem(region="us-east-1", account="TEST_ACCOUNT", name="bucket8", config=CONFIG_EIGHT), + # Bucket with deprecated thirdparty short s3 name + CloudAuxChangeItem(region="us-east-1", account="TEST_ACCOUNT", name="bucket9", config=CONFIG_NINE) ] account_type_result = AccountType(name='AWS') db.session.add(account_type_result) db.session.commit() - account = Account(identifier="012345678910", name="TEST_ACCOUNT", - account_type_id=account_type_result.id, notes="TEST_ACCOUNT", - third_party=False, active=True) - account.custom_fields.append(AccountTypeCustomValues(name="canonical_id", - value="23984723987489237489237489237489uwedfjhdsjklfhksdf" - "h2389")) - account.custom_fields.append(AccountTypeCustomValues(name="s3_name", value="test_accnt1")) + # SAME Account + account = Account( + identifier="012345678910", name="TEST_ACCOUNT", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT", + third_party=False, active=True) + account.custom_fields.append( + AccountTypeCustomValues( + name="canonical_id", + value="23984723987489237489237489237489uwedfjhdsjklfhksdfh2389")) + account.custom_fields.append( + AccountTypeCustomValues(name="s3_name", value="test_accnt1")) - account2 = Account(identifier="012345678911", name="TEST_ACCOUNT2", - account_type_id=account_type_result.id, notes="TEST_ACCOUNT2", - third_party=False, active=True) + # Friendly Account + account2 = Account( + identifier="012345678911", name="TEST_ACCOUNT2", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT2", + third_party=False, active=True) + account2.custom_fields.append( + AccountTypeCustomValues( + name="canonical_id", + value="lksdjfilou32890u47238974189237euhuu128937192837189uyh1hr3")) + account2.custom_fields.append( + AccountTypeCustomValues(name="s3_name", value="test_accnt2")) - account2.custom_fields.append(AccountTypeCustomValues(name="canonical_id", - value="lksdjfilou32890u47238974189237euhuu128937192837189" - "uyh1hr3")) - account2.custom_fields.append(AccountTypeCustomValues(name="s3_name", value="test_accnt2")) - - account3 = Account(identifier="012345678912", name="TEST_ACCOUNT3", - account_type_id=account_type_result.id, notes="TEST_ACCOUNT3", - third_party=True, active=True) - - account3.custom_fields.append(AccountTypeCustomValues(name="canonical_id", - value="dsfhgiouhy23984723789y4riuwhfkajshf91283742389u" - "823723")) - account3.custom_fields.append(AccountTypeCustomValues(name="s3_name", value="test_accnt3")) + # Thirdparty Account + account3 = Account( + identifier="012345678912", name="TEST_ACCOUNT3", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT3", + third_party=True, active=True) + account3.custom_fields.append( + AccountTypeCustomValues(name="canonical_id", + value="dsfhgiouhy23984723789y4riuwhfkajshf91283742389u823723")) + account3.custom_fields.append( + AccountTypeCustomValues(name="s3_name", value="test_accnt3")) db.session.add(account) db.session.add(account2) db.session.add(account3) db.session.commit() + def run_acl_checks(self, auditor, item): + auditor.check_acl_internet_accessible(item) + auditor.check_acl_log_delivery(item) + auditor.check_acl_friendly_legacy(item) + auditor.check_acl_thirdparty_legacy(item) + auditor.check_acl_friendly_canonical(item) + auditor.check_acl_thirdparty_canonical(item) + auditor.check_acl_unknown(item) + def test_s3_acls(self): s3_auditor = S3Auditor(accounts=["012345678910"]) + s3_auditor.prep_for_audit() # CONFIG ONE: - s3_auditor.check_acl(self.s3_items[0]) + self.run_acl_checks(s3_auditor, self.s3_items[0]) assert len(self.s3_items[0].audit_issues) == 0 # CONFIG TWO: - s3_auditor.check_acl(self.s3_items[1]) - assert len(self.s3_items[1].audit_issues) == 1 - assert self.s3_items[1].audit_issues[0].score == 10 - assert self.s3_items[1].audit_issues[0].issue == "ACL - Unknown Cross Account Access." + item = self.s3_items[1] + self.run_acl_checks(s3_auditor, item) + assert len(item.audit_issues) == 1 + assert item.audit_issues[0].score == 10 + assert item.audit_issues[0].issue == "Unknown Access" + assert item.audit_issues[0].notes == "Entity: [ACL:34589673489752397023749287uiouwshjksdhfdjkshfdjkshf2381] Actions: [\"FULL_CONTROL\"]" # CONFIG THREE: - s3_auditor.check_acl(self.s3_items[2]) - assert len(self.s3_items[2].audit_issues) == 1 - assert self.s3_items[2].audit_issues[0].score == 0 - assert self.s3_items[2].audit_issues[0].issue == "ACL - Friendly Account Access." + item = self.s3_items[2] + self.run_acl_checks(s3_auditor, item) + assert len(item.audit_issues) == 1 + assert item.audit_issues[0].score == 0 + assert item.audit_issues[0].issue == "Friendly Cross Account" + assert item.audit_issues[0].notes == "Account: [012345678911/TEST_ACCOUNT2] Entity: [ACL:lksdjfilou32890u47238974189237euhuu128937192837189uyh1hr3] Actions: [\"FULL_CONTROL\"]" # CONFIG FOUR: - s3_auditor.check_acl(self.s3_items[3]) - assert len(self.s3_items[3].audit_issues) == 1 - assert self.s3_items[3].audit_issues[0].score == 0 - assert self.s3_items[3].audit_issues[0].issue == "ACL - Friendly Third Party Access." + item = self.s3_items[3] + self.run_acl_checks(s3_auditor, item) + assert len(item.audit_issues) == 1 + assert item.audit_issues[0].score == 0 + assert item.audit_issues[0].issue == "Thirdparty Cross Account" + assert item.audit_issues[0].notes == "Account: [012345678912/TEST_ACCOUNT3] Entity: [ACL:dsfhgiouhy23984723789y4riuwhfkajshf91283742389u823723] Actions: [\"FULL_CONTROL\"]" + + # CONFIG FIVE: + item = self.s3_items[5] + self.run_acl_checks(s3_auditor, item) + assert len(item.audit_issues) == 1 + assert item.audit_issues[0].score == 10 + assert item.audit_issues[0].issue == "Internet Accessible" + assert item.audit_issues[0].notes == "Account: [AWS/AWS] Entity: [ACL:http://acs.amazonaws.com/groups/global/AllUsers] Actions: [\"READ\"]" + + # CONFIG SIX: + item = self.s3_items[6] + self.run_acl_checks(s3_auditor, item) + assert len(item.audit_issues) == 1 + assert item.audit_issues[0].score == 10 + assert item.audit_issues[0].issue == "Internet Accessible" + assert item.audit_issues[0].notes == "Account: [AWS/AWS] Entity: [ACL:http://acs.amazonaws.com/groups/global/AuthenticatedUsers] Actions: [\"READ\"]" + + # CONFIG SEVEN: + item = self.s3_items[7] + self.run_acl_checks(s3_auditor, item) + assert len(item.audit_issues) == 1 + assert item.audit_issues[0].score == 0 + assert item.audit_issues[0].issue == "Thirdparty Cross Account" + assert item.audit_issues[0].notes == "Account: [AWS/AWS] Entity: [ACL:http://acs.amazonaws.com/groups/s3/LogDelivery] Actions: [\"READ\"]" + + # CONFIG EIGHT: + item = self.s3_items[8] + self.run_acl_checks(s3_auditor, item) + assert len(item.audit_issues) == 1 + assert item.audit_issues[0].score == 0 + assert item.audit_issues[0].issue == "Friendly Cross Account" + assert item.audit_issues[0].notes == "Account: [012345678911/TEST_ACCOUNT2] Entity: [ACL:test_accnt2] Actions: [\"READ\"]" + + # CONFIG NINE: + item = self.s3_items[9] + self.run_acl_checks(s3_auditor, item) + assert len(item.audit_issues) == 1 + assert item.audit_issues[0].score == 0 + assert item.audit_issues[0].issue == "Thirdparty Cross Account" + assert item.audit_issues[0].notes == "Account: [012345678912/TEST_ACCOUNT3] Entity: [ACL:test_accnt3] Actions: [\"READ\"]" + + def test_check_policy_exists(self): + auditor = S3Auditor(accounts=['012345678910']) + auditor.check_policy_exists(self.s3_items[4]) + assert len(self.s3_items[4].audit_issues) == 1 + assert self.s3_items[4].audit_issues[0].score == 0 + assert self.s3_items[4].audit_issues[0].issue == "POLICY - No Policy." \ No newline at end of file diff --git a/security_monkey/tests/auditors/test_sns.py b/security_monkey/tests/auditors/test_sns.py new file mode 100644 index 000000000..ae7468d7d --- /dev/null +++ b/security_monkey/tests/auditors/test_sns.py @@ -0,0 +1,104 @@ +# Copyright 2017 Netflix, Inc. +# +# 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. +""" +.. module: security_monkey.tests.auditors.sns + :platform: Unix + +.. version:: $$VERSION$$ +.. moduleauthor:: Patrick Kelley @monkeysecurity + +""" +from security_monkey.tests import SecurityMonkeyTestCase +from security_monkey.auditors.sns import SNSAuditor +from security_monkey.watchers.sns import SNSItem +from security_monkey.datastore import Account, AccountType +from security_monkey import db + + +class SNSAuditorTestCase(SecurityMonkeyTestCase): + + def pre_test_setup(self): + SNSAuditor(accounts=['TEST_ACCOUNT']).OBJECT_STORE.clear() + account_type_result = AccountType(name='AWS') + db.session.add(account_type_result) + db.session.commit() + + # main + account = Account(identifier="123456789123", name="TEST_ACCOUNT", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT", + third_party=False, active=True) + # friendly + account2 = Account(identifier="222222222222", name="TEST_ACCOUNT_TWO", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT_TWO", + third_party=False, active=True) + # third party + account3 = Account(identifier="333333333333", name="TEST_ACCOUNT_THREE", + account_type_id=account_type_result.id, notes="TEST_ACCOUNT_THREE", + third_party=True, active=True) + + db.session.add(account) + db.session.add(account2) + db.session.add(account3) + db.session.commit() + + def test_check_snstopicpolicy_empty(self): + auditor = SNSAuditor(accounts=['TEST_ACCOUNT']) + item = SNSItem(config=dict()) + auditor.check_snstopicpolicy_empty(item) + + self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 1) + + def test_check_subscriptions_crossaccount(self): + auditor = SNSAuditor(accounts=['TEST_ACCOUNT']) + auditor.prep_for_audit() + + # Unknown account ID + item = SNSItem(config=dict( + subscriptions=[{ + "Owner": "020202020202", + "Endpoint": "someemail@example.com", + "Protocol": "email", + "TopicArn": "arn:aws:sns:us-east-1:020202020202:somesnstopic", + "SubscriptionArn": "arn:aws:sns:us-east-1:020202020202:somesnstopic:..." + }])) + auditor.check_subscriptions_crossaccount(item) + self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 10) + + # Friendly account ID + item = SNSItem(config=dict( + subscriptions=[{ + "Owner": "222222222222", + "Endpoint": "someemail@example.com", + "Protocol": "email", + "TopicArn": "arn:aws:sns:us-east-1:012345678910:somesnstopic", + "SubscriptionArn": "arn:aws:sns:us-east-1:012345678910:somesnstopic:..." + }])) + auditor.check_subscriptions_crossaccount(item) + self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 0) + + # ThirdParty account ID + item = SNSItem(config=dict( + subscriptions=[{ + "Owner": "333333333333", + "Endpoint": "someemail@example.com", + "Protocol": "email", + "TopicArn": "arn:aws:sns:us-east-1:012345678910:somesnstopic", + "SubscriptionArn": "arn:aws:sns:us-east-1:012345678910:somesnstopic:..." + }])) + auditor.check_subscriptions_crossaccount(item) + self.assertEquals(len(item.audit_issues), 1) + self.assertEquals(item.audit_issues[0].score, 0) \ No newline at end of file diff --git a/setup.py b/setup.py index 174dcce13..85bc3746d 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ 'pyyaml==3.11', 'jira==0.32', 'cloudaux>=1.3.3', + 'policyuniverse>=1.0.7.1', 'joblib>=0.9.4', 'pyjwt>=1.01', 'idna==2.5' # Pinning to idna to avoid a dependency problem with requests.