class TestSensitiveAccess(unittest.TestCase): """Test class for single value condition too permissive auditor""" example_policy_string = """ { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject" ], "Resource": "arn:aws:s3:::secretbucket/*", "Condition": { "ForAllValues:StringEquals": { "aws:ResourceTag/Tag": [ "Value" ] } } } ] } """ policy = analyze_policy_string(example_policy_string, include_community_auditors=True) assert_equal(policy.finding_ids, set(["SINGLE_VALUE_CONDITION_TOO_PERMISSIVE"]))
def test_resource_effectively_star(self): policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": [ { "Sid": "CloudtrailReadTrail", "Effect": "Allow", "Action": [ "cloudtrail:GetEventSelectors", "cloudtrail:PutEventSelectors" ], "Resource": [ "arn:*:cloudtrail:*:*:trail/*" ] } ] }""") assert_equal( policy.finding_ids, set(["RESOURCE_EFFECTIVELY_STAR"]), "Resource policy spans all Cloudtrails even without an asterisk.", ) policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": [ { "Sid": "CloudtrailReadTrail", "Effect": "Allow", "Action": [ "cloudtrail:GetEventSelectors", "cloudtrail:PutEventSelectors" ], "Resource": [ "arn:aws:cloudtrail:us-east-1:000000000000:trail/*" ] } ] }""") assert_equal( policy.finding_ids, set(), "Resource policy is scoped by AWS partition, account and region and is therefore not 'STAR'.", )
def test_resource_bad(self): policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listallmybuckets", "Resource": "s3"}}""") assert_equal(len(policy.findings), 1)
def test_analyze_policy_string_no_action(self): policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Resource": "*"}}""") assert_false( len(policy.findings) == 0, "Policy does not have an Action")
def test_notprincipal_allow(self): # NotPrincipal is OK with Effect: Deny. This explcitly omits these # users from the list of Principals denied access to this resource # This example is taken from https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notprincipal.html#specifying-notprincipal policystr = """{ "Version": "2012-10-17", "Statement": [{ "Effect": "Deny", "NotPrincipal": {"AWS": [ "arn:aws:iam::444455556666:user/Bob", "arn:aws:iam::444455556666:root" ]}, "Action": "s3:*", "Resource": [ "arn:aws:s3:::BUCKETNAME", "arn:aws:s3:::BUCKETNAME/*" ] }] }""" policy = analyze_policy_string(policystr, include_community_auditors=True) assert_equal(policy.finding_ids, set()) # This implicitly allows everyone _except_ Bob to access BUCKETNAME! policystr = """{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "NotPrincipal": {"AWS": [ "arn:aws:iam::444455556666:user/Bob", ]}, "Action": "s3:*", "Resource": [ "arn:aws:s3:::BUCKETNAME", "arn:aws:s3:::BUCKETNAME/*" ] }] }""" policy = analyze_policy_string(policystr, include_community_auditors=True) assert_equal(policy.finding_ids, S3_STAR_FINDINGS | {"NOTPRINCIPAL_WITH_ALLOW"})
def test_resource_policy_privilege_escalation(self): # This policy is actually granting essentially s3:* due to the ability to put a policy on a bucket policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutBucketPolicy"], "Resource": "*" }}""") assert_false( len(policy.findings) == 0, "Resource policy privilege escalation") policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": [ { "Action": [ "s3:ListBucket", "s3:Put*", "s3:Get*", "s3:*MultipartUpload*" ], "Resource": [ "*" ], "Effect": "Allow" }, { "Action": [ "s3:*Policy*", "sns:*Permission*", "sns:*Delete*", "s3:*Delete*", "sns:*Remove*" ], "Resource": [ "*" ], "Effect": "Deny" } ]}""") assert_false( len(policy.findings) == 0, "Resource policy privilege escalation across two statement", )
def test_condition_multiple(self): # Both good policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listbucket", "Resource": "arn:aws:s3:::bucket-name", "Condition": { "DateGreaterThan" :{"aws:CurrentTime" : "2019-07-16T12:00:00Z"}, "StringEquals": {"s3:prefix":["home/${aws:username}/*"]} } }}""" ) assert_equal(len(policy.findings), 0) # First bad policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listbucket", "Resource": "arn:aws:s3:::bucket-name", "Condition": { "DateGreaterThan" :{"aws:CurrentTime" : "bad"}, "StringEquals": {"s3:prefix":["home/${aws:username}/*"]} } }}""" ) assert_false(len(policy.findings) == 0, "First condition is bad") # Second bad policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listbucket", "Resource": "arn:aws:s3:::bucket-name", "Condition": { "DateGreaterThan" :{"aws:CurrentTime" : "2019-07-16T12:00:00Z"}, "StringEquals": {"s3:x":["home/${aws:username}/*"]} } }}""" ) assert_false(len(policy.findings) == 0, "Second condition is bad")
def test_resource_good(self): policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:getobject", "Resource": "arn:aws:s3:::my_corporate_bucket/*"}}""") print(policy.findings) assert_equal(len(policy.findings), 0)
def test_condition_mismatch(self): policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": ["ec2:*", "s3:*"], "Resource": "*", "Condition": {"StringNotEquals": {"iam:ResourceTag/status":"prod"}} }}""" ) assert_false(len(policy.findings) == 0, "Condition mismatch")
def test_condition(self): policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listbucket", "Resource": "arn:aws:s3:::bucket-name", "Condition": {"DateGreaterThan" :{"aws:CurrentTime" : "2019-07-16T12:00:00Z"}} }}""" ) assert_equal(len(policy.findings), 0)
def test_condition_operator(self): policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listbucket", "Resource": "arn:aws:s3:::bucket-name", "Condition": {"StringEqualsIfExists": {"s3:prefix":["home/${aws:username}/*"]}} }}""", ignore_private_auditors=True, ) assert_equal(policy.finding_ids, set()) policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listbucket", "Resource": "arn:aws:s3:::bucket-name", "Condition": {"bad": {"s3:prefix":["home/${aws:username}/*"]}} }}""", ignore_private_auditors=True, ) assert_equal( policy.finding_ids, set(["UNKNOWN_OPERATOR", "MISMATCHED_TYPE"]), "Unknown operator", ) policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listbucket", "Resource": "arn:aws:s3:::bucket-name", "Condition": {"NumericEquals": {"s3:prefix":["home/${aws:username}/*"]}} }}""", ignore_private_auditors=True, ) assert_equal( policy.finding_ids, set(["MISMATCHED_TYPE"]), "Operator type mismatch" )
def test_analyze_policy_string_opposites(self): # Policy contains Action and NotAction policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listallmybuckets", "NotAction": "s3:listallmybuckets", "Resource": "*"}}""") assert_false( len(policy.findings) == 0, "Policy contains Action and NotAction")
def test_analyze_policy_string_invalid_sid(self): policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Sid": "Statement With Spaces And Special Chars!?", "Effect": "Allow", "Action": "s3:listallmybuckets", "Resource": "*"}}""" ) assert_false(len(policy.findings) == 0, "Policy statement has invalid Sid")
def test_condition_bad_key(self): policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listbucket", "Resource": "arn:aws:s3:::bucket-name", "Condition": {"DateGreaterThan" :{"bad" : "2019-07-16T12:00:00Z"}} }}""" ) assert_false(len(policy.findings) == 0, "Policy has bad key in Condition")
def test_bad_mfa_condition(self): policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "*", "Resource": "*", "Condition": {"Bool": {"aws:MultiFactorAuthPresent":"false"}} }}""") assert_false( len(policy.findings) == 0, "Policy contains bad MFA check")
def check_policy(self, name, document): policy = parliament.analyze_policy_string(json.dumps(document)) for permission in REQUIRED_PERMISSIONS: if policy.get_allowed_resources(*permission.split(":")) != ["*"]: self._failure( f"Discarding Instance Policy {name}\n\tMissing permission {permission}" ) return False self._success(f"Found Valid SSM Instance Policy {name}") return True
def test_analyze_policy_string_no_action(self): policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Resource": "*"}}""", ignore_private_auditors=True, ) assert_equal(policy.finding_ids, set(["MALFORMED"]), "Policy does not have an Action")
def test_notresource_allow(self): # NotResource is OK with Effect: Deny. This denies access to # all S3 buckets except Payroll buckets. This example is taken from # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notresource.html policystr = """{ "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Action": "s3:*", "NotResource": [ "arn:aws:s3:::HRBucket/Payroll", "arn:aws:s3:::HRBucket/Payroll/*" ] } }""" policy = analyze_policy_string(policystr, include_community_auditors=True) assert_equal(policy.finding_ids, set()) # According to AWS documentation, "This statement is very dangerous, # because it allows all actions in AWS on all resources except the # HRBucket S3 bucket." See: # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_notresource.html#notresource-element-combinations policystr = """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:*", "NotResource": [ "arn:aws:s3:::HRBucket/Payroll", "arn:aws:s3:::HRBucket/Payroll/*" ] } }""" policy = analyze_policy_string(policystr, include_community_auditors=True) assert_equal(policy.finding_ids, S3_STAR_FINDINGS | {"NOTRESOURCE_WITH_ALLOW"})
def test_condition_action_specific_bad_type(self): # s3:signatureage requires a number policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listbucket", "Resource": "arn:aws:s3:::bucket-name", "Condition": {"StringEquals": {"s3:signatureage":"bad"}} }}""") print(policy.findings) assert_false( len(policy.findings) == 0, 'Wrong type, "bad" should be a number')
def test_analyze_policy_string_correct_multiple_statements_and_actions( self): policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": "s3:listallmybuckets", "Resource": "*"}, { "Effect": "Allow", "Action": "iam:listusers", "Resource": "*"}]}""") assert_equal(len(policy.findings), 0)
def validate_file(filename): with (open(filename, 'r')) as file: tf = hcl2.load(file) findings = [] # Validate data.aws_iam_policy_document for policy_document in filter( lambda x: x.get('aws_iam_policy_document', None), tf.get('data', [])): iam_statements = [] policy_name = list( policy_document['aws_iam_policy_document'].keys())[0] for statement_data in policy_document['aws_iam_policy_document'][ policy_name]['statement']: # Don't check assume role policies; these will have spurious findings for # "Statement contains neither Resource nor NotResource" actions = statement_data.get('actions')[0] if actions == ['sts:AssumeRole' ] or actions == ['sts:AssumeRoleWithSAML']: continue iam_statements.append(mock_iam_statement_from_tf(statement_data)) policy_string = policy_template.format( iam_statements=json.dumps(iam_statements)) findings += parliament.analyze_policy_string(policy_string).findings # Validate resource.aws_iam_policy for policy in filter(lambda x: x.get('aws_iam_policy', None), tf.get('resource', [])): try: policy_string = policy['aws_iam_policy']['policy']['policy'][0] except KeyError: continue findings += parliament.analyze_policy_string(policy_string).findings return findings
def test_analyze_policy_string_correct_simple(self): policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listallmybuckets", "Resource": "*"}}""", ignore_private_auditors=True, ) assert_equal( policy.finding_ids, set(), )
def test_analyze_policy_string_invalid_sid(self): policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Sid": "Statement With Spaces And Special Chars!?", "Effect": "Allow", "Action": "s3:listallmybuckets", "Resource": "*"}}""", ignore_private_auditors=True, ) assert_equal(policy.finding_ids, set(["INVALID_SID"]), "Policy statement has invalid Sid")
def test_resource_policy_privilege_escalation_at_bucket_level(self): policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutBucketPolicy"], "Resource": ["arn:aws:s3:::bucket", "arn:aws:s3:::bucket/*"] }}""", ignore_private_auditors=True, ) assert_equal( policy.finding_ids, set(["RESOURCE_POLICY_PRIVILEGE_ESCALATION"]), "Resource policy privilege escalation", ) policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:*Bucket*", "s3:*Object*"], "Resource": ["arn:aws:s3:::bucket1", "arn:aws:s3:::bucket1/*"] }, { "Effect": "Allow", "Action": ["s3:*Object"], "Resource": ["arn:aws:s3:::bucket2/*"] }]}""", ignore_private_auditors=True, ) # There is one finding for "No resources match for s3:ListAllMyBuckets which requires a resource format of *" assert_equal( policy.finding_ids, set(["RESOURCE_MISMATCH"]), "Buckets do not match so no escalation possible", )
def test_condition_type_unqoted_bool(self): policy = analyze_policy_string( """{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "kms:CreateGrant", "Resource": "*", "Condition": {"Bool": {"kms:GrantIsForAWSResource": true}} }}""", ignore_private_auditors=True, ) assert_equal( policy.finding_ids, set(["RESOURCE_STAR"]), )
def test_resource_with_sub(self): policy = analyze_policy_string("""{ "Version":"2012-10-17", "Statement":[ { "Sid":"AddPerm", "Effect":"Allow", "Principal": "*", "Action":["ssm:PutParameter"], "Resource":[{"Fn::Sub": "arn:aws:ssm:*:${AWS::AccountId}:*"}] } ] }""") assert_equal(policy.finding_ids, {"INVALID_ARN"})
def test_analyze_policy_string_multiple_statements_one_bad(self): policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": "s3:listallmybuckets", "Resource": "*"}, { "Effect": "Allow", "Action": ["iam:listusers", "iam:list"], "Resource": "*"}]}""") assert_false( len(policy.findings) == 0, "Policy with multiple statements has one bad")
def test_analyze_policy_string_MFA_formatting(self): policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Sid": "AllowManageOwnVirtualMFADevice", "Effect": "Allow", "Action": [ "iam:CreateVirtualMFADevice", "iam:DeleteVirtualMFADevice" ], "Resource": "arn:aws:iam::*:mfa/${aws:username}" } }""") assert_equal(policy.finding_ids, set([]), "Policy is valid")
def test_condition_action_specific(self): policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listbucket", "Resource": "arn:aws:s3:::bucket-name", "Condition": {"StringEquals": {"s3:prefix":["home/${aws:username}/*"]}} }}""" ) assert_equal(len(policy.findings), 0) # The key s3:x-amz-storage-class is not allowed for ListBucket, # but is for other S3 actions policy = analyze_policy_string("""{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Action": "s3:listbucket", "Resource": "arn:aws:s3:::bucket-name", "Condition": {"StringEquals": {"s3:x-amz-storage-class":"bad"}} }}""") assert_false( len(policy.findings) == 0, "Policy uses key that cannot be used for the action", )
def test_bad_principals(self): # Good principal policy = analyze_policy_string("""{ "Version":"2012-10-17", "Statement":[ { "Sid":"AddPerm", "Effect":"Allow", "Principal": "*", "Action":["s3:GetObject"], "Resource":["arn:aws:s3:::examplebucket/*"] } ] }""") assert_true(len(policy.findings) == 0, "Basic S3 bucket policy")