def test_with_invalid_role_inline_policy_fn_if(): role_props = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "RootRole": { "Type": "AWS::IAM::Role", "Properties": { "Path": "/", "Policies": [{ "Fn::If": [ "IsSandbox", { "PolicyDocument": { "Statement": [{ "Action": "sts:AssumeRole", "Effect": "Allow", "Resource": "arn:aws:iam::325714046698:role/sandbox-secrets-access", }], "Version": "2012-10-17", }, "PolicyName": "SandboxSecretsAccessAssumerole", }, { "PolicyDocument": { "Statement": [{ "Action": ["ec2:DeleteVpc"], "Effect": "Allow", "Resource": ["*"] }], "Version": "2012-10-17", }, "PolicyName": "ProdCredentialStoreAccessPolicy", }, ] }], }, } }, } result = Result() rule = IAMRolesOverprivilegedRule(None, result) rule.check_managed_policies = Mock() resources = pycfmodel.parse(role_props).resources rule.invoke(resources, []) rule.check_managed_policies.assert_called() assert not result.valid assert ( result.failed_rules[0]["reason"] == 'Role "RootRole" contains an insecure permission "ec2:DeleteVpc" in policy "ProdCredentialStoreAccessPolicy"' ) assert result.failed_rules[0]["rule"] == "IAMRolesOverprivilegedRule"
def template(self): dir_path = os.path.dirname(os.path.realpath(__file__)) with open( f"{dir_path}/test_templates/rds_instance_plain_parameter.json" ) as cf_script: cf_template = convert_json_or_yaml_to_dict(cf_script.read()) return pycfmodel.parse(cf_template)
def template(self): dir_path = os.path.dirname(os.path.realpath(__file__)) cf_script = open( '{}/test_templates/sqs_queue_with_wildcards.json'.format(dir_path)) cf_template = S3Adapter().convert_json_or_yaml_to_dict( cf_script.read()) return pycfmodel.parse(cf_template)
def test_template_conditions(): template = { "Conditions": { "Bool": True, "BoolStr": "True", "IsEqualNum": { "Fn::Equals": [123456, 123456] }, "IsEqualStr": { "Fn::Equals": ["a", "a"] }, "IsEqualBool": { "Fn::Equals": [True, True] }, "IsEqualRef": { "Fn::Equals": [{ "Ref": "AWS::AccountId" }, "123"] }, "Not": { "Fn::Not": [False] }, }, "Resources": {}, } model = parse(template).resolve(extra_params={"AWS::AccountId": "123"}) assert isinstance(model.Conditions, Dict) assert all(isinstance(cv, bool) for cv in model.Conditions.values())
def process_cf_template(self, cf_template_dict, config, result): if not cf_template_dict or not isinstance(cf_template_dict, dict): result.add_exception( TypeError("CF template not converted to dict")) return cf_model = pycfmodel.parse(cf_template_dict) # Fetch referenced managed policies for validation transformer = ManagedPolicyTransformer(cf_model) transformer.transform_managed_policies() for rule in self.rules: try: rule.invoke(cf_model.resources, cf_model.parameters) except Exception as other_exception: result.add_exception(other_exception) logger.exception( "{} crashed with {} for project - {}, service - {}, stack - {}" .format( type(rule).__name__, type(other_exception).__name__, config.project_name, config.service_name, config.stack_name, )) continue self.remove_failures_of_whitelisted_actions(config=config, result=result) self.remove_failures_of_whitelisted_resources(config=config, result=result)
def template(self): dir_path = os.path.dirname(os.path.realpath(__file__)) cf_script = open( "{}/test_templates/s3_bucket_cross_account.json".format(dir_path)) cf_template = S3Adapter().convert_json_or_yaml_to_dict( cf_script.read()) return pycfmodel.parse(cf_template)
def test_resolve_include_resource_when_condition_is_not_present(): template = { "AWSTemplateFormatVersion": "2010-09-09", "Conditions": { "Bool": True, "BoolStr": "True", "IsEqualNum": {"Fn::Equals": [123456, 123456]}, "IsEqualStr": {"Fn::Equals": ["a", "a"]}, "IsEqualBool": {"Fn::Equals": [True, True]}, "IsEqualRef": {"Fn::Equals": [{"Ref": "AWS::AccountId"}, "123"]}, "Not": {"Fn::Not": [False]}, }, "Resources": { "test_resource_id": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [], }, "Path": "/", "Policies": [], }, } }, } model = parse(template).resolve() assert list(model.Resources.keys()) == ["test_resource_id"]
def template(self): dir_path = os.path.dirname(os.path.realpath(__file__)) with open( f"{dir_path}/test_templates/single_security_group_one_cidr_ingress.json" ) as cf_script: cf_template = convert_json_or_yaml_to_dict(cf_script.read()) return pycfmodel.parse(cf_template)
def test_with_invalid_role_managed_policy(): role_props = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "RootRole": { "Type": "AWS::IAM::Role", "Properties": { "Path": "/", "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AdministratorAccess"] }, } }, } result = Result() rule = IAMRolesOverprivilegedRule(None, result) resources = pycfmodel.parse(role_props).resources rule.invoke(resources, []) assert not result.valid assert ( result.failed_rules[0]["reason"] == "Role RootRole has forbidden Managed Policy arn:aws:iam::aws:policy/AdministratorAccess" ) assert result.failed_rules[0]["rule"] == "IAMRolesOverprivilegedRule"
def template(self): dir_path = os.path.dirname(os.path.realpath(__file__)) with open( f"{dir_path}/test_templates/iam_role_with_wildcard_action_on_trust.json" ) as cf_script: cf_template = convert_json_or_yaml_to_dict(cf_script.read()) return pycfmodel.parse(cf_template)
def test_correct_event(): event = {"stack_template_url": "https://asdfasdfasdf/bucket/key", "stack": {"name": "blooblah"}} mock_created_s3_adapter_object = Mock() mock_created_s3_adapter_object.download_template_to_dictionary.return_value = {"Resources": {}} mock_boto3_adapter = Mock(return_value=mock_created_s3_adapter_object) mock_created_boto3_client_object = Mock() mock_created_boto3_client_object.get_template.return_value = {"Resources": {}} mock_created_boto3_client_object.compare_outputs.return_value = {} mock_boto3_client = Mock(return_value=mock_created_boto3_client_object) mock_created_rule_processor_object = Mock(spec=RuleProcessor) mock_created_rule_processor_object.process_cf_template.return_value = Result() mock_rule_processor = Mock(return_value=mock_created_rule_processor_object) mock_rule_processor.remove_debug_rules.return_value = [] with patch("cfripper.main.Boto3Client", new=mock_boto3_adapter): with patch("cfripper.main.RuleProcessor", new=mock_rule_processor): with patch("cfripper.main.Boto3Client", new=mock_boto3_client): from cfripper.main import handler handler(event, None) cfmodel = pycfmodel.parse({"Resources": {}}).resolve() mock_created_s3_adapter_object.download_template_to_dictionary.assert_called_once_with( "https://asdfasdfasdf/bucket/key" ) mock_created_rule_processor_object.process_cf_template.assert_called_once_with(cfmodel, ANY)
def test_resolve_scenario_3(): template = { "AWSTemplateFormatVersion": "2010-09-09", "Description": "Test resolving IP address in security group", "Parameters": { "IPValueIngress": {"Description": "Some IP Ingress", "Type": "String"}, "IPValueEgress": {"Description": "Some IP Egress", "Type": "String"}, }, "Resources": { "InstanceSecurityGroup": { "Type": "AWS::EC2::SecurityGroup", "Properties": { "GroupDescription": "Allow http to client host", "VpcId": "VPCID", "SecurityGroupIngress": [ {"IpProtocol": "tcp", "FromPort": 80, "ToPort": 80, "CidrIp": {"Ref": "IPValueIngress"}} ], "SecurityGroupEgress": [ {"IpProtocol": "tcp", "FromPort": 80, "ToPort": 80, "CidrIp": {"Ref": "IPValueEgress"}} ], }, } }, } model = parse(template).resolve(extra_params={"IPValueIngress": "1.1.1.1/16", "IPValueEgress": "127.0.0.1"}) assert ( model.Resources["InstanceSecurityGroup"].Properties.SecurityGroupIngress[0].CidrIp.with_netmask == "1.1.0.0/255.255.0.0" ) assert not model.Resources["InstanceSecurityGroup"].Properties.SecurityGroupIngress[0].CidrIp.is_private assert ( model.Resources["InstanceSecurityGroup"].Properties.SecurityGroupEgress[0].CidrIp.with_netmask == "127.0.0.1/255.255.255.255" )
def test_with_valid_role_inline_policy(): role_props = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "RootRole": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyName": "root", "PolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["IAM:CREATEPOLICY"], "Resource": ["arn:aws:glue:eu-west-1:12345678:catalog"], }], }, "Roles": "some_role", }, } }, } resource = pycfmodel.parse(role_props).resources result = Result() rule = PrivilegeEscalationRule(None, result) rule.invoke(resource, []) assert not result.valid assert len(result.failed_rules) == 1
def get_cfmodel(template: TextIOWrapper) -> CFModel: template_file = convert_json_or_yaml_to_dict(template.read()) if not template_file: raise FileEmptyException( f"{template.name} is empty and not a valid template.") cfmodel = pycfmodel.parse(template_file) return cfmodel
def template(self): dir_path = os.path.dirname(os.path.realpath(__file__)) with open( f"{dir_path}/test_templates/s3_bucket_cross_account_and_normal.json" ) as cf_script: cf_template = convert_json_or_yaml_to_dict(cf_script.read()) return pycfmodel.parse(cf_template)
def test_with_invalid_role_inline_policy_fn_if(): role_props = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "RootRole": { "Type": "AWS::IAM::Role", "Properties": { "Path": "/", "Policies": [{ 'Fn::If': [ 'IsSandbox', { 'PolicyDocument': { 'Statement': [{ 'Action': 'sts:AssumeRole', 'Effect': 'Allow', 'Resource': 'arn:aws:iam::325714046698:role/sandbox-secrets-access' }], 'Version': '2012-10-17' }, 'PolicyName': 'SandboxSecretsAccessAssumerole' }, { 'PolicyDocument': { 'Statement': [{ 'Action': ['ec2:DeleteVpc'], 'Effect': 'Allow', 'Resource': ["*"] }], 'Version': '2012-10-17' }, 'PolicyName': 'ProdCredentialStoreAccessPolicy' } ] }] } } } } result = Result() rule = IAMRolesOverprivilegedRule(None, result) rule.check_managed_policies = Mock() resources = pycfmodel.parse(role_props).resources rule.invoke(resources) rule.check_managed_policies.assert_called() assert not result.valid assert result.failed_rules[0][ 'reason'] == 'Role "RootRole" contains an insecure permission "ec2:DeleteVpc" in policy "ProdCredentialStoreAccessPolicy"' assert result.failed_rules[0]['rule'] == 'IAMRolesOverprivilegedRule'
def test_flow(): cf_model = pycfmodel.parse(test_cf) transformer = ManagedPolicyTransformer(cf_model) iam_client = Mock() iam_client.get_policy = Mock(return_value={"Policy": {"DefaultVersionId": "TestV"}}) iam_client.get_policy_version = Mock(return_value=test_policy) transformer.iam_client = iam_client transformer.transform_managed_policies() test_iam_role = cf_model.resources["AWS::IAM::Role"][0] assert len(test_iam_role.policies) == 1 assert test_iam_role.policies[0].policy_name == "AutoTransformedManagedPolicyTestV"
def test_resolve_scenario_1(): template = { "AWSTemplateFormatVersion": "2010-09-09", "Parameters": {"StarParameter": {"Type": "String", "Default": "*", "Description": "Star Param"}}, "Resources": { "rootRole": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": {"AWS": {"Fn::Sub": "arn:aws:iam::${AWS::AccountId}:root"}}, "Action": ["sts:AssumeRole"], } ], }, "Path": "/", "Policies": [ { "PolicyName": "root", "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": {"Ref": "StarParameter"}, "Resource": {"Ref": "StarParameter"}, } ], }, } ], }, } }, } model = parse(template).resolve(extra_params={"AWS::AccountId": "123"}) role = model.Resources["rootRole"] policy = role.Properties.Policies[0] statement = policy.PolicyDocument.Statement[0] assert statement.Action == "*" assert statement.Resource == "*" assert role.Properties.AssumeRolePolicyDocument.Statement[0].Principal == {"AWS": "arn:aws:iam::123:root"}
def test_expand_actions_scenario_1(): template = { "AWSTemplateFormatVersion": "2010-09-09", "Parameters": {"StarParameter": {"Type": "String", "Default": "*", "Description": "Star Param"}}, "Resources": { "rootRole": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": {"AWS": {"Fn::Sub": "arn:aws:iam::${AWS::AccountId}:root"}}, "Action": ["sts:AssumeRole"], } ], }, "Path": "/", "Policies": [ { "PolicyName": "root", "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": {"Ref": "StarParameter"}, "Resource": {"Ref": "StarParameter"}, } ], }, } ], }, } }, } model = parse(template).resolve(extra_params={"AWS::AccountId": "123"}).expand_actions() assert ( model.Resources["rootRole"].Properties.Policies[0].PolicyDocument.Statement[0].Action == CLOUDFORMATION_ACTIONS )
def test_resolve_booleans(): template = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "KMSKey": { "Type": "AWS::KMS::Key", "Properties": { "Enabled": True, "EnableKeyRotation": True, "KeyPolicy": { "Version": "2012-10-17", "Statement": [], }, }, } }, } model = parse(template).resolve(extra_params={"some-service-arn:1": "vpc-123-abc"}) assert isinstance(model.Resources["KMSKey"], KMSKey)
def test_resolve_ssm(): template = { "AWSTemplateFormatVersion": "2010-09-09", "Description": "Test resolving SSM dynamic values", "Resources": { "InstanceHTTPTargets": { "Type": "Custom::DynamicNLBTarget", "Properties": { "ServiceArn": "{{resolve:ssm:some-service-arn:1}}", "Cluster": "{{resolve:ssm:main-k8s-cluster-arn:3}}", }, } }, } model = parse(template).resolve(extra_params={"some-service-arn:1": "vpc-123-abc"}) assert model.Resources["InstanceHTTPTargets"].Properties == { "Cluster": "UNDEFINED_PARAM_main-k8s-cluster-arn:3", "ServiceArn": "vpc-123-abc", }
def test_with_templates(cf_path): with open(cf_path) as cf_script: cf_template = convert_json_or_yaml_to_dict(cf_script.read()) config = Config(project_name=cf_path, service_name=cf_path, stack_name=cf_path, rules=DEFAULT_RULES.keys()) # Scan result cfmodel = pycfmodel.parse(cf_template).resolve() rules = [DEFAULT_RULES.get(rule)(config) for rule in config.rules] processor = RuleProcessor(*rules) result = processor.process_cf_template(cfmodel, config) # Use this to print the stack if there'IAMManagedPolicyWildcardActionRule an error if len(result.exceptions): print(cf_path) traceback.print_tb(result.exceptions[0].__traceback__) assert len(result.exceptions) == 0
def test_with_invalid_role_inline_policy(): role_props = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "RootRole": { "Type": "AWS::IAM::Role", "Properties": { "Path": "/", "Policies": [{ "PolicyName": "not_so_chill_policy", "PolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["ec2:DeleteInternetGateway"], "Resource": "*" }], }, }], }, } }, } result = Result() rule = IAMRolesOverprivilegedRule(None, result) rule.check_managed_policies = Mock() resources = pycfmodel.parse(role_props).resources rule.invoke(resources, []) rule.check_managed_policies.assert_called() assert not result.valid assert ( result.failed_rules[0]["reason"] == 'Role "RootRole" contains an insecure permission "ec2:DeleteInternetGateway" in policy "not_so_chill_policy"' ) assert result.failed_rules[0]["rule"] == "IAMRolesOverprivilegedRule"
def test_resolve_include_resource_when_condition_is_true_or_doesnt_exist(conditions: Dict, expected: List): template = { "AWSTemplateFormatVersion": "2010-09-09", "Conditions": conditions, "Resources": { "test_resource_id": { "Type": "AWS::IAM::Role", "Condition": "testCondition", "Properties": { "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [], }, "Path": "/", "Policies": [], }, } }, } model = parse(template).resolve() assert list(model.Resources.keys()) == expected
def test_with_valid_role_inline_policy(): role_props = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "RootRole": { "Type": "AWS::IAM::Role", "Properties": { "Path": "/", "Policies": [{ "PolicyName": "chill_policy", "PolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": [ "ec2:DescribeInstances", ], "Resource": "*" }] } }] } } } } resource = pycfmodel.parse(role_props).resources result = Result() rule = IAMRolesOverprivilegedRule(None, result) rule.check_managed_policies = Mock() rule.invoke(resource) rule.check_managed_policies.assert_called() assert result.valid assert len(result.failed_rules) == 0
def test_with_valid_role_managed_policy(): role_props = { "AWSTemplateFormatVersion": "2010-09-09", "Resources": { "RootRole": { "Type": "AWS::IAM::Role", "Properties": { "Path": "/", "ManagedPolicyArns": ["arn:aws:iam::aws:policy/YadaYadaYada"] }, } }, } result = Result() rule = IAMRolesOverprivilegedRule(None, result) rule.check_inline_policies = Mock() resources = pycfmodel.parse(role_props).resources rule.invoke(resources, []) rule.check_inline_policies.assert_called() assert result.valid assert len(result.failed_rules) == 0
def test_given_a_template_with_a_resource_it_should_return_its_policy_documents( template, expected_policy_documents, expected_policy_documents_length): resource = parse(template).Resources["NonexistentResource"] assert resource.policy_documents == expected_policy_documents assert len(resource.policy_documents) == expected_policy_documents_length
def handler(event, context): """ Main entry point of the Lambda function. :param event: { "stack_template_url": String, "project": String, "stack": { "name": String, }, "event": String, "region": String, "account": { "name": String, "id": String, }, "user_agent": String, } :param context: :return: """ setup_logging() if not event.get("stack_template_url") and not event.get("stack", {}).get("name"): raise ValueError( "Invalid event type: no parameter 'stack_template_url' or 'stack::name' in request." ) template = get_template(event) if not template: # In case of an invalid script log a warning and return early result = Result() result.add_exception( TypeError( f"Malformed Event - could not parse!! Event: {str(event)}")) logger.exception( f"Malformed Event - could not parse!! Event: {str(event)}") return { "valid": True, "reason": "", "failed_rules": [], "exceptions": [x.args[0] for x in result.exceptions] } # Process Rules config = Config( project_name=event.get("project"), service_name=event.get("serviceName"), stack_name=event.get("stack", {}).get("name"), rules=DEFAULT_RULES.keys(), event=event.get("event"), template_url=event.get("stack_template_url", "N/A"), aws_region=event.get("region", "N/A"), aws_account_name=event.get("account", {}).get("name", "N/A"), aws_account_id=event.get("account", {}).get("id", "N/A"), aws_user_agent=event.get("user_agent", "N/A"), ) logger.info("Scan started for: {}; {}; {};".format(config.project_name, config.service_name, config.stack_name)) rules = [DEFAULT_RULES.get(rule)(config) for rule in config.rules] processor = RuleProcessor(*rules) # TODO get AWS variables/parameters and pass them to resolve cfmodel = pycfmodel.parse(template).resolve() result = processor.process_cf_template(cfmodel, config) perform_logging(result, config, event) return { "valid": result.valid, "reason": ",".join( ["{}-{}".format(r.rule, r.reason) for r in result.failed_rules]), "failed_rules": [ failure.serializable() for failure in RuleProcessor.remove_debug_rules( rules=result.failed_rules) ], "exceptions": [x.args[0] for x in result.exceptions], "warnings": [ failure.serializable() for failure in RuleProcessor.remove_debug_rules( rules=result.failed_monitored_rules) ], }
def test_basic_json(): model = pycfmodel.parse(basic_template) assert type(model).__name__ == "CFModel" assert len(model.resources) == 1
def template(self): return pycfmodel.parse(test_template)
def main(): pycfmodel.parse(template)