def _check_policy_document( self, result: Result, logical_id: str, policy_document: PolicyDocument, policy_name: Optional[str], extras: Dict ): statements_to_review = policy_document.statements_with(REGEX_IS_STAR) + policy_document.statements_with( REGEX_WILDCARD_ARN ) for statement in statements_to_review: self._check_statement(result, logical_id, policy_name, statement, extras=extras)
def test_get_iam_actions(): correct_list = [ "IAM:DeleteAccountPasswordPolicy", "IAM:DeleteServiceLinkedRole", "IAM:DeleteRole", "IAM:DeleteOpenIDConnectProvider", "IAM:DeleteGroup", "IAM:DeleteRolePolicy", "IAM:DeleteSSHPublicKey", "IAM:DeleteLoginProfile", "IAM:DeleteServiceSpecificCredential", "IAM:DeleteUserPolicy", "IAM:DeleteVirtualMFADevice", "IAM:DeletePolicyVersion", "IAM:DeleteGroupPolicy", "IAM:DeleteAccountAlias", "IAM:DeleteSigningCertificate", "IAM:DeleteUser", "IAM:DeletePolicy", "IAM:DeleteSAMLProvider", "IAM:DeleteAccessKey", "IAM:DeleteServerCertificate", "IAM:DeleteInstanceProfile", ] pd = { "PolicyDocument": { "Statement": [ { "Action": [ "IAM:Delete*" ], "Effect": "Allow", "Resource": "arn:aws:s3:::fakebucketfakebucket/*", "NotPrincipal": { "AWS": [ "156460612806" ] } } ] } } document = PolicyDocument(pd["PolicyDocument"]) actions = document.get_iam_actions() assert len(actions) == 21 assert correct_list == actions
def test_star_resource(): pd = { "PolicyDocument": { "Statement": [{ "Action": ["*"], "Effect": "Allow", "Resource": "*", "Principal": { "AWS": ["156460612806"] } }] } } document = PolicyDocument(pd["PolicyDocument"]) assert len(document.star_resource_statements()) == 1
def test_not_principla(): pd = { "PolicyDocument": { "Statement": [{ "Action": ["*"], "Effect": "Allow", "Resource": "arn:aws:s3:::fakebucketfakebucket/*", "NotPrincipal": { "AWS": ["156460612806"] } }] } } document = PolicyDocument(pd["PolicyDocument"]) assert len(document.allows_not_principal()) == 1
def test_multi_statements(): pd = { "doc": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Service": ["ec2.amazonaws.com"], "AWS": "arn:aws:iam::324320755747:root" }, "Action": ["sts:AssumeRole"] }, { "Effect": "bar", "Principal": { "Service": ["ec2.amazonaws.com"], "AWS": "arn:aws:iam::324320755747:root" }, "Action": ["sts:AssumeRole"] }] } } document = PolicyDocument(pd["doc"]) statement1 = document.statements[0] assert statement1.effect == "Allow" statement2 = document.statements[1] assert statement2.effect == "bar"
def policy_document_not_principal(): return PolicyDocument( **{ "Statement": [ { "Action": [ "iam:Delete*", "s3:GetObject*", ], "Effect": "Allow", "Resource": "arn:aws:s3:::fakebucketfakebucket/*", "NotPrincipal": { "AWS": ["156460612806"] }, }, { "Action": [ "s3:List*", ], "Effect": "Deny", "Resource": "arn:aws:s3:::fakebucketfakebucket/*", "NotPrincipal": { "AWS": ["156460612806"] }, }, ] })
def invoke(self, cfmodel: CFModel, extras: Optional[Dict] = None) -> Result: result = Result() for logical_id, resource in cfmodel.Resources.items(): for policy in resource.policy_documents: self._check_policy_document(result, logical_id, policy.policy_document, policy.name, extras) if isinstance(resource, IAMRole): self._check_policy_document( result, logical_id, resource.Properties.AssumeRolePolicyDocument, None, extras) elif isinstance(resource, KMSKey): self._check_policy_document(result, logical_id, resource.Properties.KeyPolicy, None, extras) elif isinstance(resource, GenericResource): if hasattr(resource, "Properties"): policy_document = resource.Properties.get("PolicyDocument") if policy_document: self._check_policy_document( result, logical_id, PolicyDocument(**policy_document), None, extras) return result
def test_wildcard_actions(): pd = { "PolicyDocument": { "Statement": [{ "Action": ["s3:*"], "Effect": "Allow", "Resource": "arn:aws:s3:::fakebucketfakebucket2/*", "Principal": { "AWS": "*" } }] } } document = PolicyDocument(pd["PolicyDocument"]) assert len(document.wildcard_allowed_actions()) == 1 assert len( document.wildcard_allowed_actions(pattern=r"^(\w*:){0,1}\*$")) == 1
def _check_policy_document(self, result: Result, logical_id: str, policy_document: PolicyDocument, policy_name: Optional[str], extras: Dict): for statement in policy_document.statements_with(REGEX_IS_STAR): self._check_statement(result, logical_id, policy_name, statement, extras=extras)
def test_get_iam_actions(): correct_list = [ "IAM:DeleteAccountPasswordPolicy", "IAM:DeleteServiceLinkedRole", "IAM:DeleteRole", "IAM:DeleteOpenIDConnectProvider", "IAM:DeleteGroup", "IAM:DeleteRolePolicy", "IAM:DeleteSSHPublicKey", "IAM:DeleteLoginProfile", "IAM:DeleteServiceSpecificCredential", "IAM:DeleteUserPolicy", "IAM:DeleteVirtualMFADevice", "IAM:DeletePolicyVersion", "IAM:DeleteGroupPolicy", "IAM:DeleteAccountAlias", "IAM:DeleteSigningCertificate", "IAM:DeleteUser", "IAM:DeletePolicy", "IAM:DeleteSAMLProvider", "IAM:DeleteAccessKey", "IAM:DeleteServerCertificate", "IAM:DeleteInstanceProfile", ] pd = { "PolicyDocument": { "Statement": [{ "Action": ["IAM:Delete*"], "Effect": "Allow", "Resource": "arn:aws:s3:::fakebucketfakebucket/*", "NotPrincipal": { "AWS": ["156460612806"] } }] } } document = PolicyDocument(pd["PolicyDocument"]) actions = document.get_iam_actions() assert len(actions) == 21 assert correct_list == actions
def policy_document_one_statement(): return PolicyDocument( **{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": {"Service": ["ec2.amazonaws.com"], "AWS": "arn:aws:iam::324320755747:root"}, "Action": ["sts:AssumeRole"], }, } )
def policy_document_wildcard_actions(): return PolicyDocument( **{ "Statement": [{ "Action": ["s3:*"], "Effect": "Allow", "Resource": "arn:aws:s3:::fakebucketfakebucket2/*", "Principal": { "AWS": "*" }, }] })
def policy_document_star_resource(): return PolicyDocument( **{ "Statement": [{ "Action": ["*"], "Effect": "Allow", "Resource": "*", "Principal": { "AWS": ["156460612806"] } }] })
def test_not_principla(): pd = { "PolicyDocument": { "Statement": [ { "Action": [ "*" ], "Effect": "Allow", "Resource": "arn:aws:s3:::fakebucketfakebucket/*", "NotPrincipal": { "AWS": [ "156460612806" ] } } ] } } document = PolicyDocument(pd["PolicyDocument"]) assert len(document.allows_not_principal()) == 1
def policy_document_not_principal(): return PolicyDocument( **{ "Statement": [{ "Action": ["IAM:Delete*"], "Effect": "Allow", "Resource": "arn:aws:s3:::fakebucketfakebucket/*", "NotPrincipal": { "AWS": ["156460612806"] }, }] })
def test_wildcard_actions(): pd = { "PolicyDocument": { "Statement": [ { "Action": [ "s3:*" ], "Effect": "Allow", "Resource": "arn:aws:s3:::fakebucketfakebucket2/*", "Principal": { "AWS": "*" } } ] } } document = PolicyDocument(pd["PolicyDocument"]) assert len(document.wildcard_allowed_actions()) == 1 assert len(document.wildcard_allowed_actions( pattern=r"^(\w*:){0,1}\*$")) == 1
def test_star_resource(): pd = { "PolicyDocument": { "Statement": [ { "Action": [ "*" ], "Effect": "Allow", "Resource": "*", "Principal": { "AWS": [ "156460612806" ] } } ] } } document = PolicyDocument(pd["PolicyDocument"]) assert len(document.star_resource_statements()) == 1
def resource_invoke(self, resource: Resource, logical_id: str, extras: Optional[Dict] = None) -> Result: """ Checks each policy of a given resource. If it's an IAMRole, it will check its AssumeRolePolicyDocument as well. There are some cases where GenericResource contains a property called PolicyDocument that can be a str and therefore, it's not being retrieved in the initial for loop. For those cases, we run another check transforming the str to a PolicyDocument. """ result = Result() for policy in resource.policy_documents: self._check_policy_document(result, logical_id, policy.policy_document, policy.name, extras, resource_type=resource.Type) if isinstance(resource, IAMRole): self._check_policy_document( result, logical_id, resource.Properties.AssumeRolePolicyDocument, None, extras, resource_type=resource.Type, ) elif isinstance(resource, GenericResource): policy_document = getattr(resource.Properties, "PolicyDocument", None) if policy_document: try: formatted_policy_document = ( json.loads(policy_document) if isinstance( policy_document, str) else policy_document) self._check_policy_document( result, logical_id, PolicyDocument(**formatted_policy_document), None, extras, resource_type=resource.Type, ) except Exception: logger.warning( f"Could not process the PolicyDocument {policy_document} on {logical_id}", stack_info=True) return result
def policy_document_condition_with_source_vpce(): return PolicyDocument( **{ "Statement": [ { "Action": ["s3:ListBucket"], "Condition": {"IpAddress": {"aws:SourceVpce": ["vpce-123456"]}}, "Effect": "Allow", "Principal": {"AWS": "*"}, "Resource": "arn:aws:s3:::fakebucketfakebucket/*", }, ], } )
def policy_document_condition_with_source_ip(): return PolicyDocument( **{ "Statement": [ { "Action": ["s3:ListBucket"], "Condition": {"IpAddress": {"aws:SourceIp": ["116.202.65.160", "116.202.68.32/27"]}}, "Effect": "Allow", "Principal": {"AWS": "*"}, "Resource": "arn:aws:s3:::fakebucketfakebucket/*", }, ], } )
def policy_document_not_action(): return PolicyDocument( **{ "Statement": [ { "NotAction": [ "rds:*", ], "Effect": "Allow", "Resource": "arn:aws:s3:::fakebucketfakebucket/*", "NotPrincipal": {"AWS": ["156460612806"]}, }, ] } )
def test_can_obtain_policy_documents_from_inherited_method(valid_opensearch_domain_with_access_policies): assert len(valid_opensearch_domain_with_access_policies.policy_documents) == 1 assert valid_opensearch_domain_with_access_policies.policy_documents == [ OptionallyNamedPolicyDocument( policy_document=PolicyDocument( Statement=[ Statement( Effect="Allow", Action="es:*", Resource="arn:aws:es:us-east-1:123456789012:domain/test/*", Principal=Principal(AWS="arn:aws:iam::123456789012:user/opensearch-user"), ) ] ), name=None, ), ]
def invoke(self, cfmodel: CFModel, extras: Optional[Dict] = None) -> Result: result = Result() for logical_id, resource in cfmodel.Resources.items(): if isinstance(resource, IAMPolicy): self._check_policy_document(result, logical_id, resource.Properties.PolicyDocument, resource.Properties.PolicyName, extras) elif isinstance(resource, (IAMManagedPolicy, S3BucketPolicy, SNSTopicPolicy, SQSQueuePolicy)): self._check_policy_document(result, logical_id, resource.Properties.PolicyDocument, None, extras) elif isinstance(resource, IAMRole): self._check_policy_document( result, logical_id, resource.Properties.AssumeRolePolicyDocument, None, extras) if resource.Properties.Policies: for policy in resource.Properties.Policies: self._check_policy_document(result, logical_id, policy.PolicyDocument, policy.PolicyName, extras) elif isinstance( resource, IAMUser ) and resource.Properties and resource.Properties.Policies: for policy in resource.Properties.Policies: self._check_policy_document(result, logical_id, policy.PolicyDocument, policy.PolicyName, extras) elif isinstance(resource, KMSKey): self._check_policy_document(result, logical_id, resource.Properties.KeyPolicy, None, extras) elif isinstance(resource, GenericResource): if hasattr(resource, "Properties"): policy_document = resource.Properties.get("PolicyDocument") if policy_document: self._check_policy_document( result, logical_id, PolicyDocument(**policy_document), None, extras) return result
def check_for_wildcards( self, result: Result, logical_id: str, resource: PolicyDocument, resource_type: str, extras: Optional[Dict] = None, ): for statement in resource.statement_as_list(): if statement.Effect == "Allow" and statement.principals_with( self.FULL_REGEX): for principal in statement.get_principal_list(): account_id_match = self.IAM_PATTERN.match( principal) or self.AWS_ACCOUNT_ID_PATTERN.match( principal) account_id = account_id_match.group( 1) if account_id_match else None # Check if account ID is allowed. `self._get_allowed_from_config()` used here # to reduce number of false negatives and only allow exemptions for accounts # which belong to AWS Services (such as ELB and ElastiCache). if account_id in self._get_allowed_from_config(): continue if statement.Condition and statement.Condition.dict(): # Ignoring condition checks since they will get reviewed in other rules and future improvements continue else: self.add_failure_to_result( result, self.REASON_WILDCARD_PRINCIPAL.format( logical_id, principal), resource_ids={logical_id}, resource_types={resource_type}, context={ "config": self._config, "extras": extras, "logical_id": logical_id, "resource": resource, "statement": statement, "principal": principal, "account_id": account_id, }, )
def check_for_wildcards(self, result: Result, logical_id: str, resource: PolicyDocument): for statement in resource._statement_as_list(): if statement.Effect == "Allow" and statement.principals_with(self.FULL_REGEX): for principal in statement.get_principal_list(): # Check if account ID is allowed account_id_match = self.IAM_PATTERN.match(principal) if account_id_match: self.validate_account_id(result, logical_id, account_id_match.group(1)) if statement.Condition and statement.Condition.dict(): logger.warning( f"Not adding {type(self).__name__} failure in {logical_id} because there are conditions: " f"{statement.Condition}" ) elif not self.resource_is_whitelisted(logical_id): self.add_failure_to_result( result, self.REASON_WILCARD_PRINCIPAL.format(logical_id, principal), resource_ids={logical_id}, )
def check_for_wildcards( self, result: Result, logical_id: str, resource: PolicyDocument, resource_type: str, extras: Optional[Dict] = None, ): for statement in resource.statement_as_list(): filtered_principals = statement.principals_with(self.FULL_REGEX) if statement.Effect == "Allow" and filtered_principals: for principal in filtered_principals: # if we can't find the account ID it might be a canonical ID identifier = get_account_id_from_principal( principal) or principal # Check if account ID / canonical ID is allowed. `self._get_allowed_from_config()` used here # to reduce number of false negatives and only allow exemptions for accounts # which belong to AWS Services (such as ELB and ElastiCache). if identifier in self._get_allowed_from_config(): continue if statement.Condition and statement.Condition.dict(): # Ignoring condition checks since they will get reviewed in other rules and future improvements continue else: self.add_failure_to_result( result, self.REASON_WILDCARD_PRINCIPAL.format( logical_id, principal), resource_ids={logical_id}, resource_types={resource_type}, context={ "config": self._config, "extras": extras, "logical_id": logical_id, "resource": resource, "statement": statement, "principal": principal, "account_id": identifier, }, )
def check_for_wildcards(self, result: Result, logical_id: str, resource: PolicyDocument, extras: Optional[Dict] = None): for statement in resource._statement_as_list(): if statement.Effect == "Allow" and statement.principals_with( self.FULL_REGEX): for principal in statement.get_principal_list(): account_id_match = self.IAM_PATTERN.match(principal) account_id = account_id_match.group( 1) if account_id_match else None # Check if account ID is allowed. `self._get_allowed_from_config()` used here # to reduce number of false negatives and only allow exemptions for accounts # which belong to AWS Services (such as ELB and ElastiCache). if account_id in self._get_allowed_from_config(): continue if statement.Condition and statement.Condition.dict(): logger.warning( f"Not adding {type(self).__name__} failure in {logical_id} because there are conditions: " f"{statement.Condition}") else: self.add_failure_to_result( result, self.REASON_WILCARD_PRINCIPAL.format( logical_id, principal), resource_ids={logical_id}, context={ "config": self._config, "extras": extras, "logical_id": logical_id, "resource": resource, "statement": statement, "principal": principal, "account_id": account_id, }, )
"Statement": [{ "Effect": "Allow", "Action": ["service:GetService"], "Resource": "*", }], }, }, } }, }, [ OptionallyNamedPolicyDocument( policy_document=PolicyDocument(Statement=[ Statement( Effect="Allow", Action=["service:GetService"], Resource="*", ) ]), name=None, ) ], 1, ), ( { "AWSTemplateFormatVersion": "2010-09-09", "Description": "Test resolving a nonexistent resource to Resource class", "Resources": { "NonexistentResource": {
def policy_document_kms_key(): return PolicyDocument( **{ "Version": "2012-10-17", "Id": "key-consolepolicy-2", "Statement": [ { "Sid": "Enable IAM policies", "Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::111122223333:root"}, "Action": "kms:*", "Resource": "*", }, { "Sid": "Allow access for Key Administrators", "Effect": "Allow", "Principal": { "AWS": [ "arn:aws:iam::111122223333:user/KMSAdminUser", "arn:aws:iam::111122223333:role/KMSAdminRole", ] }, "Action": [ "kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*", "kms:Put*", "kms:Update*", "kms:Revoke*", "kms:Disable*", "kms:Get*", "kms:Delete*", "kms:TagResource", "kms:UntagResource", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion", ], "Resource": "*", }, { "Sid": "Allow use of the key", "Effect": "Allow", "Principal": { "AWS": [ "arn:aws:iam::111122223333:user/ExampleUser", "arn:aws:iam::111122223333:role/ExampleRole", "arn:aws:iam::444455556666:root", ] }, "Action": [ "kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:DescribeKey", ], "Resource": "*", }, { "Sid": "Allow attachment of persistent resources", "Effect": "Allow", "Principal": { "AWS": [ "arn:aws:iam::111122223333:user/ExampleUser", "arn:aws:iam::111122223333:role/ExampleRole", "arn:aws:iam::444455556666:root", ] }, "Action": ["kms:CreateGrant", "kms:ListGrants", "kms:RevokeGrant"], "Resource": "*", "Condition": {"Bool": {"kms:GrantIsForAWSResource": True}}, }, ], } )