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 check_sqsqueue_crossaccount(self, sqsitem): """ alert on cross account access """ policy = sqsitem.config 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': '<ARN>' } } }" 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')
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': '<ARN>' } } }" 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')
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)
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': '<ARN>' } } }" 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 if arn.tech == 's3': notes = "The ElasticSearch Service domain 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, 'ES cluster allows access from S3 bucket', es_domain, notes=notes) else: account_numbers.append(arn.account_number)
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': '<ARN>' } } }" 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 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 check_sqsqueue_crossaccount(self, sqsitem): """ alert on cross account access """ policy = sqsitem.config 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': '<ARN>' } } }" 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')
def _parse_arn(self, arn_input, account_numbers, snsitem): 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 test_from_arn(self): proper_arns = [ '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/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/*' ] # 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) 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_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)
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': '<ARN>' } } }" 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')
def check_account_in_arn(input): from security_monkey.common.arn import ARN arn = ARN(input) if arn.error: print('Could not parse ARN in Trust Policy: {arn}'.format( arn=input)) if not arn.error and arn.account_number: account = Account.query.filter( Account.number == arn.account_number).first() if not account: tag = "IAM Role allows assume-role from an " \ + "Unknown Account ({account_number})".format( account_number=arn.account_number) self.add_issue(10, tag, iamrole_item, notes=json.dumps(statement))
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
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': '<ARN>' } } }" 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')