def audit_s3_buckets(region): buckets_json = query_aws(region.account, "s3-list-buckets", region) buckets = pyjq.all('.Buckets[].Name', buckets_json) for bucket in buckets: # Check policy try: policy_file_json = get_parameter_file(region, 's3', 'get-bucket-policy', bucket) # Find the entity we need policy_string = policy_file_json['Policy'] # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) if policy.is_internet_accessible(): print('- Internet accessible S3 bucket {}: {}'.format( bucket, policy_string)) except Exception as e: print('- Exception checking policy of S3 bucket {}: {}; e'.format( bucket, policy_string, e)) # Check ACL try: file_json = get_parameter_file(region, 's3', 'get-bucket-acl', bucket) for grant in file_json['Grants']: uri = grant['Grantee'].get('URI', "") if (uri == 'http://acs.amazonaws.com/groups/global/AllUsers' or uri == 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' ): print('- Public grant to S3 bucket {}: {}'.format( bucket, grant)) except Exception as e: print('- Exception checking ACL of S3 bucket {}: {}; {}'.format( bucket, grant, e))
def recordToText(record): temp_string="" for key in record.keys(): if key=="Actions": #Creating a temporary policy_template #since policy universe expects a policy object to be passed to it #The action identified from Aaia DB will be passed to this template #which will be used to process further to get the summary using policy universe policy_template={ "Statement": [{ "Action": ["s3:put*", "sqs:get*", "sns:*"], "Resource": "*", "Effect": "Allow" }] } actions=record[key].replace(" ","").split(",") policy_template['Statement'][0]['Action']=actions policy=Policy(policy_template) for service, summary_action in policy.action_summary().items(): temp_string+="{"+str(service)+"-"+str(summary_action).replace("{","").replace("}","")+"}," temp_string+="`" else: temp_string+=str(record[key])+"`" return(temp_string.rstrip("`"))
def audit_sqs(findings, region): # Check for publicly accessible sqs. json_blob = query_aws(region.account, "sqs-list-queues", region) if json_blob is None: # Service not supported in the region return for queue in json_blob.get("QueueUrls", []): queue_name = queue.split("/")[-1] # Check policy queue_attributes = get_parameter_file(region, "sqs", "get-queue-attributes", queue) if queue_attributes is None: # No policy continue # Find the entity we need attributes = queue_attributes["Attributes"] if "Policy" in attributes: policy_string = attributes["Policy"] else: # No policy set continue # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) if policy.is_internet_accessible(): findings.add( Finding(region, "SQS_PUBLIC", queue_name, resource_details=policy_string))
def audit_glacier(region): # Check for publicly accessible vaults. json_blob = query_aws(region.account, "glacier-list-vaults", region) if json_blob is None: # Service not supported in the region return for vault in json_blob.get('VaultList', []): name = vault['VaultName'] # Check policy policy_file_json = get_parameter_file(region, 'glacier', 'get-vault-access-policy', name) if policy_file_json is None: # No policy continue # Find the entity we need policy_string = policy_file_json['policy']['Policy'] # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) if policy.is_internet_accessible(): print('- Internet accessible Glacier vault {}: {}'.format( name, policy_string))
def policy(resource): iam_policy = Policy(expand_policy(json.loads(resource["PolicyDocument"]))) action_summary = iam_policy.action_summary() # # These first two checks can technically be skipped and this policy will still return correct # results, but they prevent the more computationally expensive check the majority of the time. # # Check if the policy applies to EC2 resources if "ec2" not in action_summary: return True # Check if the policy grants administrative privileges if not ADMIN_ACTIONS.intersection(action_summary["ec2"]): return True # Get the EC2 actions pertaining specifically to network resources network_actions = set() for statement in iam_policy.statements: # Only check statements granting access if statement.effect != "Allow": continue # Only check actions that are granted on network resources for action in statement.actions: if any(resource in action for resource in NETWORK_RESOURCES): network_actions.add(action) # For all actions that have been granted on network resources, ensure none grant admin access network_actions_summary = categories_for_actions(network_actions) return not any(action in ADMIN_ACTIONS for action in network_actions_summary["ec2"])
def audit_glacier(findings, region): # Check for publicly accessible vaults. json_blob = query_aws(region.account, "glacier-list-vaults", region) if json_blob is None: # Service not supported in the region return for vault in json_blob.get("VaultList", []): name = vault["VaultName"] # Check policy policy_file_json = get_parameter_file(region, "glacier", "get-vault-access-policy", name) if policy_file_json is None: # No policy continue # Find the entity we need policy_string = policy_file_json["policy"]["Policy"] # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) if policy.is_internet_accessible(): findings.add( Finding(region, "GLACIER_PUBLIC", name, resource_details=policy_string))
def audit_es(findings, region): json_blob = query_aws(region.account, "es-list-domain-names", region) for domain in json_blob.get("DomainNames", []): name = domain["DomainName"] # Check policy policy_file_json = get_parameter_file(region, "es", "describe-elasticsearch-domain", name) # Find the entity we need policy_string = policy_file_json["DomainStatus"]["AccessPolicies"] # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) # ES clusters or either public, with an "Endpoint" (singular), which is bad, or # they are VPC-only, in which case they have an "Endpoints" (plural) array containing a "vpc" element if (policy_file_json["DomainStatus"].get("Endpoint", "") != "" or policy_file_json["DomainStatus"].get("Endpoints", {}).get( "vpc", "") == ""): if policy.is_internet_accessible(): findings.add( Finding(region, "ES_PUBLIC", name, resource_details=policy_string))
def audit_sns(region): # Check for publicly accessible sns. json_blob = query_aws(region.account, "sns-list-topics", region) if json_blob is None: # Service not supported in the region return for topic in json_blob.get('Topics', []): # Check policy attributes = get_parameter_file(region, 'sns', 'get-topic-attributes', topic['TopicArn']) if attributes is None: # No policy continue # Find the entity we need attributes = attributes['Attributes'] if 'Policy' in attributes: policy_string = attributes['Policy'] else: # No policy set continue # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) if policy.is_internet_accessible(): print('- Internet accessible SNS {}: {}'.format( topic['TopicArn'], policy_string))
def audit_kms(findings, region): # Check for publicly accessible KMS keys. json_blob = query_aws(region.account, "kms-list-keys", region) if json_blob is None: # Service not supported in the region return for key in json_blob.get("Keys", []): name = key["KeyId"] # Check policy policy_file_json = get_parameter_file(region, "kms", "get-key-policy", name) if policy_file_json is None: # No policy continue # Find the entity we need policy_string = policy_file_json["Policy"] # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) if policy.is_internet_accessible(): findings.add( Finding(region, "KMS_PUBLIC", name, resource_details=policy_string))
def audit_kms(region): # Check for publicly accessible KMS keys. json_blob = query_aws(region.account, "kms-list-keys", region) if json_blob is None: # Service not supported in the region return for key in json_blob.get('Keys', []): name = key['KeyId'] # Check policy policy_file_json = get_parameter_file(region, 'kms', 'get-key-policy', name) if policy_file_json is None: # No policy continue # Find the entity we need policy_string = policy_file_json['Policy'] # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) if policy.is_internet_accessible(): print('- Internet accessible KMS {}: {}'.format( name, policy_string))
def audit_sqs(region): # Check for publicly accessible sqs. json_blob = query_aws(region.account, "sqs-list-queues", region) if json_blob is None: # Service not supported in the region return for queue in json_blob.get('QueueUrls', []): queue_name = queue.split("/")[-1] # Check policy queue_attributes = get_parameter_file(region, 'sqs', 'get-queue-attributes', queue) if queue_attributes is None: # No policy continue # Find the entity we need attributes = queue_attributes['Attributes'] if 'Policy' in attributes: policy_string = attributes['Policy'] else: # No policy set continue # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) if policy.is_internet_accessible(): print('- Internet accessible SQS {}: {}'.format( queue_name, policy_string))
def parse_policy(key: str, policy: Policy) -> Optional[Dict[Any, Any]]: """ Uses PolicyUniverse to parse KMS key policies and returns the internet accessibility results """ # policy is not required, so may be None # policy JSON format. Note condition can be any JSON statement so will need to import as-is # policy is a very complex format, so the policyuniverse library will be used for parsing out important data # ...metadata... # "Policy" : # { # "Version": "2012-10-17", # "Id": "key-consolepolicy-5", # "Statement": [ # { # "Sid": "Enable IAM User Permissions", # "Effect": "Allow", # "Principal": { # "AWS": "arn:aws:iam::123456789012:root" # }, # "Action": "kms:*", # "Resource": "*" # }, # { # "Sid": "Allow access for Key Administrators", # "Effect": "Allow", # "Principal": { # "AWS": "arn:aws:iam::123456789012:role/ec2-manager" # }, # "Action": [ # "kms:Create*", # "kms:Describe*", # "kms:Enable*", # "kms:List*", # "kms:Put*", # "kms:Update*", # "kms:Revoke*", # "kms:Disable*", # "kms:Get*", # "kms:Delete*", # "kms:ScheduleKeyDeletion", # "kms:CancelKeyDeletion" # ], # "Resource": "*" # } # ] # } if policy is not None: # get just the policy element and convert to JSON because boto3 returns this as string policy = Policy(json.loads(policy['Policy'])) if policy.is_internet_accessible(): return { "kms_key": key, "internet_accessible": True, "accessible_actions": list(policy.internet_accessible_actions()), } else: return None else: return None
def audit_sns(findings, region): # Check for publicly accessible sns. json_blob = query_aws(region.account, "sns-list-topics", region) if json_blob is None: # Service not supported in the region return for topic in json_blob.get("Topics", []): # Check policy attributes = get_parameter_file(region, "sns", "get-topic-attributes", topic["TopicArn"]) if attributes is None: # No policy continue # Find the entity we need attributes = attributes["Attributes"] if "Policy" in attributes: policy_string = attributes["Policy"] else: # No policy set continue # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) if policy.is_internet_accessible(): findings.add( Finding( region, "SNS_PUBLIC", topic["TopicArn"], resource_details=policy_string, ))
def audit_s3_buckets(findings, region): buckets_json = query_aws(region.account, "s3-list-buckets", region) buckets = pyjq.all('.Buckets[].Name', buckets_json) for bucket in buckets: # Check policy try: policy_file_json = get_parameter_file(region, 's3', 'get-bucket-policy', bucket) if policy_file_json is not None: # Find the entity we need policy_string = policy_file_json['Policy'] # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) if policy.is_internet_accessible(): if len(policy.statements) == 1 and len( policy.statements[0].actions ) == 1 and 's3:GetObject' in policy.statements[0].actions: findings.add( Finding(region, 'S3_PUBLIC_POLICY_GETOBJECT_ONLY', bucket)) else: findings.add( Finding(region, 'S3_PUBLIC_POLICY', bucket, resource_details=policy_string)) #region, issue_id, resource_id, resource_details except Exception as e: raise e #findings.add(Finding( # region, # 'EXCEPTION', # bucket, # resource_details={'policy': policy_string, 'exception': e, 'location': 'Exception checking policy of S3 bucket'})) # Check ACL try: file_json = get_parameter_file(region, 's3', 'get-bucket-acl', bucket) for grant in file_json['Grants']: uri = grant['Grantee'].get('URI', "") if (uri == 'http://acs.amazonaws.com/groups/global/AllUsers' or uri == 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' ): findings.add( Finding(region, 'S3_PUBLIC_ACL', bucket, resource_details=grant)) except Exception as e: findings.add( Finding(region, 'EXCEPTION', bucket, resource_details={ 'grant': grant, 'exception': e, 'location': 'Exception checking ACL of S3 bucket' }))
def audit(resource, remediate=False): is_compliant = True if resource["type"] != "iam_role": raise Exception("Mismatched type. Expected {} but received {}".format( "iam_role", resource["type"])) # Get a session in the account where this resource is iam = get_session_for_account(resource["account"], resource["region"], "iam") role_is_permissive = False try: role = iam.get_role(RoleName=resource["id"])["Role"] policy = role["AssumeRolePolicyDocument"] policy = Policy(policy) role_is_permissive = policy.is_internet_accessible() except Exception as e: print(e) print("No role policy: {}".format(resource["id"])) if role_is_permissive: is_compliant = False issue = "IAM role {} is publicly exposed".format(resource["id"]) if remediate: if not remediation_make_role_restricted(resource, iam): issue += " - Not remediated" send_notification(issue, "", resource) if is_compliant: print("Role is compliant: {}".format(resource["id"])) return is_compliant
def policy(resource): iam_policy = Policy(expand_policy(json.loads(resource['PolicyDocument']))) action_summary = iam_policy.action_summary() # Check if the policy grants any administrative privileges return not any( ADMIN_ACTIONS.intersection(action_summary[service]) for service in action_summary)
def scan_resource_conf(self, conf): if 'access_policy' not in conf: return CheckResult.PASSED policy_obj = conf['access_policy'][0] if isinstance(policy_obj, str) and re.match(DATA_TO_JSON_PATTERN, policy_obj): return CheckResult.UNKNOWN policy = Policy(policy_obj) if policy.is_internet_accessible(): return CheckResult.FAILED return CheckResult.PASSED
def scan_resource_conf(self, conf): conf_policy = conf.get("policy") if conf_policy: if isinstance(conf_policy[0], dict): policy = Policy(conf['policy'][0]) if policy.is_internet_accessible(): return CheckResult.FAILED else: return CheckResult.UNKNOWN return CheckResult.PASSED
def parse_policy(bucket, policy): """ Uses PolicyUniverse to parse S3 policies and returns the internet accessibility results """ # policy is not required, so may be None # policy JSON format. Note condition can be any JSON statement so will need to import as-is # policy is a very complex format, so the policyuniverse library will be used for parsing out important data # ...metadata... # "Policy" : # { # "Version": "2012-10-17", # { # "Statement": [ # { # "Effect": "Allow", # "Principal": "*", # "Action": "s3:GetObject", # "Resource": "arn:aws:s3:::MyBucket/*" # }, # { # "Effect": "Deny", # "Principal": "*", # "Action": "s3:GetObject", # "Resource": "arn:aws:s3:::MyBucket/MySecretFolder/*" # }, # { # "Effect": "Allow", # "Principal": { # "AWS": "arn:aws:iam::123456789012:root" # }, # "Action": [ # "s3:DeleteObject", # "s3:PutObject" # ], # "Resource": "arn:aws:s3:::MyBucket/*" # } # ] # } # } if policy is not None: # get just the policy element and convert to JSON because boto3 returns this as string policy = Policy(json.loads(policy['Policy'])) if policy.is_internet_accessible(): return { "bucket": bucket, "internet_accessible": True, "accessible_actions": list(policy.internet_accessible_actions()), } else: return None else: return None
def get_role_policy_permissions(self, role): if role not in self.__roles: try: iam_client = client_session_creator('iam') self.__logger.debug( "Getting the permissions attached to the role: {0}".format( role)) attached_role_policies = iam_client.list_attached_role_policies( RoleName=role) role_policies = iam_client.list_role_policies(RoleName=role) attached_role_policies_list = attached_role_policies[ 'AttachedPolicies'] policy_permissions = { INLINE_POLICIES: {}, ATTACHED_POLICIES: {} } for attached_policy in attached_role_policies_list: attached_role_arn = attached_policy["PolicyArn"] if attached_role_arn not in self.__attached_policies: current_policy_version = iam_client.get_policy( PolicyArn=attached_role_arn )['Policy']['DefaultVersionId'] policy_permissions_statement_list = iam_client.get_policy_version( PolicyArn=attached_role_arn, VersionId=current_policy_version )['PolicyVersion']['Document'] policy_object = Policy( policy_permissions_statement_list) policy_permissions_statement_list = policy_object.statements self.__attached_policies[ attached_role_arn] = policy_permissions_statement_list policy_permissions[ATTACHED_POLICIES][ attached_role_arn] = self.__attached_policies[ attached_role_arn] role_policies_list = role_policies["PolicyNames"] for policy in role_policies_list: policy_data = iam_client.get_role_policy(RoleName=role, PolicyName=policy) policy_object = Policy(policy_data["PolicyDocument"]) policy_statement_list = policy_object.statements policy_permissions[INLINE_POLICIES][ policy] = policy_statement_list except Exception as e: policy_permissions = { INLINE_POLICIES: {}, ATTACHED_POLICIES: {} } self.__roles[role] = policy_permissions return self.__roles[role]
def test_whos_allowed(self): allowed = Policy(policy03).whos_allowed() self.assertEqual(len(allowed), 2) allowed = Policy(policy04).whos_allowed() self.assertEqual(len(allowed), 3) principal_allowed = set( [item for item in allowed if item.category == 'principal']) self.assertEqual(len(principal_allowed), 2) condition_account_allowed = set( [item for item in allowed if item.category == 'account']) self.assertEqual(len(condition_account_allowed), 1)
def audit_ecr_repos(region): json_blob = query_aws(region.account, "ecr-describe-repositories", region) for repo in json_blob.get('repositories', []): name = repo['repositoryName'] # Check policy policy_file_json = get_parameter_file(region, 'ecr', 'get-repository-policy', name) # Find the entity we need policy_string = policy_file_json['policyText'] # Load the string value as json policy = json.loads(policy_string) policy = Policy(policy) if policy.is_internet_accessible(): print('- Internet accessible ECR repo {}: {}'.format(name, policy_string))
def test_condition_entries(self): from policyuniverse.statement import ConditionTuple self.assertEqual( Policy(policy05).condition_entries, set([ ConditionTuple(category="cidr", value="0.0.0.0/0"), ConditionTuple(category="account", value="012345678910"), ]), ) self.assertEqual( Policy(policy06).condition_entries, set([ConditionTuple(category="org-id", value="o-xxxxxxxxxx")]), )
def test_evasion_policies(self): """Some policies that may have been crafted to evade policycheckers.""" S3_PUBLIC_BUCKET_POLICY = ( '{"Version":"2008-10-17","Statement":[' + "{" + '"Effect":"Allow","Principal":{"AWS":"*"},' + '"Action":["s3:GetObject","s3:GetObjectTorrent"],' + '"Resource":"arn:aws:s3:::%s/*",' + '"Condition":{"StringNotLike":{"aws:UserAgent":"|_(..)_|"},"NotIpAddress":{"aws:SourceIp":"8.8.8.8"}}' + "}" + "]}" ) policy = Policy(json.loads(S3_PUBLIC_BUCKET_POLICY)) self.assertTrue(policy.is_internet_accessible()) S3_REPLICATION_DESTINATION_POLICY = ( '{"Version":"2008-10-17","Statement":[' + "{" + '"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::%s:root"},' + '"Action":["s3:*"],"Resource":"arn:aws:s3:::%s/*"' + "}," + "{" + '"Effect":"Allow","Principal":{"AWS":"*"},' + '"Action":["s3:GetObject"],' + '"Resource":"arn:aws:s3:::%s/*",' + '"Condition":{"StringNotLike":{"aws:UserAgent": "|_(..)_|"},"NotIpAddress":{"aws:SourceIp":"8.8.8.8"}}' + "}" + "]}" ) policy = Policy(json.loads(S3_REPLICATION_DESTINATION_POLICY)) self.assertTrue(policy.is_internet_accessible()) SQS_NOTIFICATION_POLICY = ( '{"Version":"2008-10-17","Statement":[' + "{" + '"Effect":"Allow","Principal":"*",' + '"Action":["SQS:ReceiveMessage","SQS:DeleteMessage"],' + '"Resource":"%s",' + '"Condition":{"StringNotLike":{"aws:UserAgent": "|_(..)_|"},"NotIpAddress":{"aws:SourceIp":"8.8.8.8"}}' + "}," + "{" + '"Effect":"Allow","Principal":{"AWS":"*"},' + '"Action":["SQS:SendMessage"],' + '"Resource":"%s",' + '"Condition":{"ArnLike":{"aws:SourceArn":"arn:aws:s3:*:*:%s"}}' + "}" + "]}" ) policy = Policy(json.loads(SQS_NOTIFICATION_POLICY)) self.assertTrue(policy.is_internet_accessible())
def test_principals(self): self.assertEqual( Policy(policy04).principals, set([ "arn:aws:iam::012345678910:root", "arn:aws:iam::*:role/Hello" ]), )
def parse_policy(api_id: str, policy: Policy) -> Optional[Dict[Any, Any]]: """ Uses PolicyUniverse to parse API Gateway REST API policy and returns the internet accessibility results """ if policy is not None: policy = Policy(json.loads(policy)) if policy.is_internet_accessible(): return { "api_id": api_id, "internet_accessible": True, "accessible_actions": list(policy.internet_accessible_actions()), } else: return None else: return None
def test_action_summary(self): summary = Policy(policy05).action_summary() self.assertEqual( summary, { 'ec2': {'DataPlaneMutating', 'DataPlaneListRead'}, 's3': {'DataPlaneMutating', 'DataPlaneListRead', 'Permissions'} })
def test_condition_entries(self): from policyuniverse.statement import ConditionTuple self.assertEqual( Policy(policy05).condition_entries, set([ ConditionTuple(category='cidr', value='0.0.0.0/0'), ConditionTuple(category='account', value='012345678910') ]))
def policy_action_count(policy_doc, location): # Counts how many unrestricted actions a policy grants policy = Policy(policy_doc) actions_count = 0 for stmt in policy.statements: if (stmt.effect == "Allow" and len(stmt.condition_entries) == 0 and stmt.resources == set("*")): actions_count += len(stmt.actions_expanded) return actions_count
def test_action_summary(self): summary = Policy(policy05).action_summary() self.assertEqual( summary, { "ec2": {"List", "Write", "Read", "Tagging", "Permissions"}, "s3": {"Write", "Read", "List", "Permissions", "Tagging"}, }, )