def test_non_matching_filters_are_reported_normally( invalid_security_group_range): mock_config = Config( rules=["EC2SecurityGroupOpenToWorldRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={ "EC2SecurityGroupOpenToWorldRule": RuleConfig(filters=[ Filter(rule_mode=RuleMode.WHITELISTED, eval={ "eq": [{ "ref": "config.stack_name" }, "anotherstack"] }) ], ) }, ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template(invalid_security_group_range, mock_config) assert not result.valid assert result.failed_rules[0].rule == "EC2SecurityGroupOpenToWorldRule" assert ( result.failed_rules[0].reason == "Port(s) 0-79, 81-100 open to public IPs: (11.0.0.0/8) in security group 'SecurityGroup'" )
def test_filter_do_not_report_anything(invalid_security_group_range): mock_config = Config( rules=["EC2SecurityGroupOpenToWorldRule"], aws_account_id="123456789", stack_name="mockstack", rules_filters=[ Filter( rule_mode=RuleMode.ALLOWED, eval={ "and": [ { "eq": [{ "ref": "config.stack_name" }, "mockstack"] }, { "eq": [{ "ref": "open_ports" }, list(range(0, 101))] }, ] }, rules={"EC2SecurityGroupOpenToWorldRule"}, ) ], ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template(invalid_security_group_range, mock_config) assert result.valid assert compare_lists_of_failures(result.failures, [])
def test_non_matching_filters_are_reported_normally( single_security_group_one_cidr_ingress): mock_config = Config( rules=["EC2SecurityGroupMissingEgressRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={ "EC2SecurityGroupMissingEgressRule": RuleConfig(filters=[ Filter(rule_mode=RuleMode.WHITELISTED, eval={ "eq": [{ "ref": "config.stack_name" }, "anotherstack"] }) ], ) }, ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template( single_security_group_one_cidr_ingress, mock_config) assert result.valid assert len(result.failed_rules) == 0 assert len(result.failed_monitored_rules) == 1 assert result.failed_monitored_rules[ 0].rule == "EC2SecurityGroupMissingEgressRule" assert ( result.failed_monitored_rules[0].reason == "Missing egress rule in sg means all traffic is allowed outbound. Make this explicit if it is desired configuration" )
def test_filter_do_not_report_anything(bad_template): mock_config = Config( rules=["EC2SecurityGroupIngressOpenToWorldRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={ "EC2SecurityGroupIngressOpenToWorldRule": RuleConfig(filters=[ Filter( rule_mode=RuleMode.WHITELISTED, eval={ "and": [ { "eq": [{ "ref": "config.stack_name" }, "mockstack"] }, { "eq": [{ "ref": "ingress.FromPort" }, 46] }, ] }, ) ], ) }, ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template(bad_template, mock_config) assert result.valid
def test_filter_context_set_correctly(mock_failure_to_result, bad_template): mock_failure_to_result.side_effect = [None, None] mock_config = Config( rules=["EC2SecurityGroupIngressOpenToWorldRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={}, ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) processor.process_cf_template(bad_template, mock_config) assert mock_failure_to_result.mock_calls[0][2]["context"][ "ingress_ip"] == "11.0.0.0/8" assert mock_failure_to_result.mock_calls[1][2]["context"][ "ingress_ip"] == "::/0" assert mock_failure_to_result.mock_calls[0][2]["context"][ "ingress_obj"] == SecurityGroupIngressProperties( CidrIp=IPv4Network("11.0.0.0/8"), FromPort=46, IpProtocol="tcp", ToPort=46, GroupId="sg-12341234", ) assert mock_failure_to_result.mock_calls[1][2]["context"][ "ingress_obj"] == SecurityGroupIngressProperties( CidrIpv6=IPv6Network("::/0"), FromPort=46, IpProtocol="tcp", ToPort=46, GroupId="sg-12341234", )
def test_with_templates(self): dir_path = os.path.dirname(os.path.realpath(__file__)) test_templates = glob.glob(f"{dir_path}/test_templates/*.*") for template in test_templates: with open(template) as cf_script: cf_template = convert_json_or_yaml_to_dict(cf_script.read()) config = Config( project_name=template, service_name=template, stack_name=template, rules=DEFAULT_RULES.keys() ) # Scan result result = Result() rules = [DEFAULT_RULES.get(rule)(config, result) for rule in config.rules] processor = RuleProcessor(*rules) processor.process_cf_template(cf_template, config, result) # Use this to print the stack if there's an error if len(result.exceptions): print(template) traceback.print_tb(result.exceptions[0].__traceback__) no_resource_templates = ["vulgar_bad_syntax.yml", "rubbish.json"] if template.split("/")[-1] in no_resource_templates: assert len(result.exceptions) == 1 else: assert len(result.exceptions) == 0
def test_filter_works_as_expected(template_two_roles_dict, expected_result_two_roles): config = Config( rules=["CrossAccountTrustRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={ "CrossAccountTrustRule": RuleConfig( filters=[ Filter( rule_mode=RuleMode.WHITELISTED, eval={ "and": [ {"eq": [{"ref": "config.stack_name"}, "mockstack"]}, {"eq": [{"ref": "logical_id"}, "RootRoleOne"]}, ] }, ) ], ) }, ) rules = [DEFAULT_RULES.get(rule)(config) for rule in config.rules] processor = RuleProcessor(*rules) result = processor.process_cf_template(template_two_roles_dict, config) assert not result.valid assert result.failed_rules[0] == expected_result_two_roles[-1]
def test_filter_do_not_report_anything(single_security_group_one_cidr_ingress): mock_config = Config( rules=["EC2SecurityGroupMissingEgressRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={ "EC2SecurityGroupMissingEgressRule": RuleConfig(filters=[ Filter( rule_mode=RuleMode.WHITELISTED, eval={"eq": [{ "ref": "config.stack_name" }, "mockstack"]}, ) ], ) }, ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template( single_security_group_one_cidr_ingress, mock_config) assert result.valid
def test_exist_function_and_property_exists( template_cross_account_role_with_name): mock_config = Config( rules=["CrossAccountTrustRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={ "CrossAccountTrustRule": RuleConfig(filters=[ Filter( rule_mode=RuleMode.WHITELISTED, eval={ "and": [ { "and": [ { "exists": { "ref": "resource.Properties.RoleName" } }, { "regex": [ "^prefix-.*$", { "ref": "resource.Properties.RoleName" } ] }, ] }, { "eq": [{ "ref": "principal" }, "arn:aws:iam::999999999:role/[email protected]" ] }, ] }, ), ]) }, ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template( template_cross_account_role_with_name, mock_config) assert result.valid
def test_non_whitelisted_stacks_are_reported_normally(template_two_roles_dict, expected_result_two_roles): mock_stack_whitelist = {"mockstack": ["CrossAccountTrustRule"]} mock_config = Config( rules=["CrossAccountTrustRule"], aws_account_id="123456789", stack_name="anotherstack", stack_whitelist=mock_stack_whitelist, ) rules = [DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules] processor = RuleProcessor(*rules) result = processor.process_cf_template(template_two_roles_dict, mock_config) assert not result.valid assert result.failed_rules == expected_result_two_roles
def test_whitelisted_stacks_do_not_report_anything(template_two_roles_dict): mock_stack_whitelist = {"mockstack": ["CrossAccountTrustRule"]} mock_config = Config( rules=["CrossAccountTrustRule"], aws_account_id="123456789", stack_name="mockstack", stack_whitelist=mock_stack_whitelist, ) rules = [DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules] processor = RuleProcessor(*rules) result = processor.process_cf_template(template_two_roles_dict, mock_config) assert result.valid
def test_resource_whitelisting_works_as_expected(template_two_roles_dict, expected_result_two_roles): mock_rule_to_resource_whitelist = {"CrossAccountTrustRule": {".*": {"RootRoleOne"}}} mock_config = Config( rules=["CrossAccountTrustRule"], aws_account_id="123456789", rule_to_resource_whitelist=mock_rule_to_resource_whitelist, stack_name="mockstack", stack_whitelist={}, ) rules = [DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules] processor = RuleProcessor(*rules) result = processor.process_cf_template(template_two_roles_dict, mock_config) assert not result.valid assert result.failed_rules[0] == expected_result_two_roles[-1]
def test_non_matching_filters_are_reported_normally(bad_template): mock_config = Config( rules=["EC2SecurityGroupIngressOpenToWorldRule"], aws_account_id="123456789", stack_name="mockstack", rules_filters=[ Filter( rule_mode=RuleMode.ALLOWED, eval={"eq": [{ "ref": "config.stack_name" }, "anotherstack"]}, rules={"EC2SecurityGroupIngressOpenToWorldRule"}, ) ], ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template(bad_template, mock_config) assert not result.valid assert compare_lists_of_failures( result.failures, [ Failure( granularity=RuleGranularity.RESOURCE, reason= "Port(s) 46 open to public IPs: (11.0.0.0/8) in security group 'securityGroupIngress1'", risk_value=RuleRisk.MEDIUM, rule="EC2SecurityGroupIngressOpenToWorldRule", rule_mode=RuleMode.BLOCKING, actions=None, resource_ids={"securityGroupIngress1"}, resource_types={"AWS::EC2::SecurityGroupIngress"}, ), Failure( granularity=RuleGranularity.RESOURCE, reason= "Port(s) 46 open to public IPs: (::/0) in security group 'securityGroupIngress2'", risk_value=RuleRisk.MEDIUM, rule="EC2SecurityGroupIngressOpenToWorldRule", rule_mode=RuleMode.BLOCKING, actions=None, resource_ids={"securityGroupIngress2"}, resource_types={"AWS::EC2::SecurityGroupIngress"}, ), ], )
def test_externally_defined_rule_filter(filters, valid, template_security_group_firehose_ips): mock_config = Config( rules=["EC2SecurityGroupOpenToWorldRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={}, rules_filters=[] if not filters else filters, ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template( template_security_group_firehose_ips, mock_config) assert result.valid == valid
def test_non_matching_filters_are_reported_normally(template_two_roles_dict, expected_result_two_roles): mock_config = Config( rules=["CrossAccountTrustRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={ "CrossAccountTrustRule": RuleConfig( filters=[ Filter(rule_mode=RuleMode.WHITELISTED, eval={"eq": [{"ref": "config.stack_name"}, "anotherstack"]}) ], ) }, ) rules = [DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules] processor = RuleProcessor(*rules) result = processor.process_cf_template(template_two_roles_dict, mock_config) assert not result.valid assert result.failed_rules == expected_result_two_roles
def test_filter_do_not_report_anything(template_two_roles_dict): mock_config = Config( rules=["CrossAccountTrustRule"], aws_account_id="123456789", stack_name="mockstack", rules_config={ "CrossAccountTrustRule": RuleConfig( filters=[ Filter(rule_mode=RuleMode.WHITELISTED, eval={"eq": [{"ref": "config.stack_name"}, "mockstack"]}) ], ) }, ) rules = [DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules] processor = RuleProcessor(*rules) result = processor.process_cf_template(template_two_roles_dict, mock_config) assert result.valid
def test_load_filters_work_with_several_rules(template_two_roles_dict, test_files_location): config = Config( rules=["CrossAccountTrustRule", "PartialWildcardPrincipalRule"], aws_account_id="123456789", stack_name="mockstack", ) config.load_rules_config_file( open( f"{test_files_location}/config/rules_config_CrossAccountTrustRule.py" )) config.add_filters_from_dir(f"{test_files_location}/filters") rules = [DEFAULT_RULES.get(rule)(config) for rule in config.rules] processor = RuleProcessor(*rules) result = processor.process_cf_template(template_two_roles_dict, config) assert not result.valid assert compare_lists_of_failures( result.failures, [ Failure( granularity=RuleGranularity.RESOURCE, reason= "RootRoleTwo has forbidden cross-account trust relationship with arn:aws:iam::999999999:role/[email protected]", risk_value=RuleRisk.MEDIUM, rule="CrossAccountTrustRule", rule_mode=RuleMode.BLOCKING, actions=None, resource_ids={"RootRoleTwo"}, resource_types={"AWS::IAM::Role"}, ), Failure( granularity=RuleGranularity.RESOURCE, reason= "RootRoleTwo should not allow wildcard, account-wide or root in resource-id like 'arn:aws:iam::12345:root' at 'arn:aws:iam::123456789:root'", risk_value=RuleRisk.MEDIUM, rule="PartialWildcardPrincipalRule", rule_mode=RuleMode.BLOCKING, actions=None, resource_ids={"RootRoleTwo"}, resource_types={"AWS::IAM::Role"}, ), ], )
def test_filter_works_as_expected_with_rules_config_file( template_two_roles_dict, expected_result_two_roles, test_files_location): config = Config( rules=["CrossAccountTrustRule"], aws_account_id="123456789", stack_name="mockstack", ) config.load_rules_config_file( open( f"{test_files_location}/config/rules_config_CrossAccountTrustRule.py" )) config.add_filters_from_dir(f"{test_files_location}/filters") rules = [DEFAULT_RULES.get(rule)(config) for rule in config.rules] processor = RuleProcessor(*rules) result = processor.process_cf_template(template_two_roles_dict, config) assert not result.valid assert compare_lists_of_failures(result.failures, expected_result_two_roles[-1:])
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_filter_do_not_report_anything(filter_eval_object, bad_template): mock_config = Config( rules=["EC2SecurityGroupIngressOpenToWorldRule"], aws_account_id="123456789", stack_name="mockstack", rules_filters=[ Filter( rule_mode=RuleMode.ALLOWED, eval=filter_eval_object, rules={"EC2SecurityGroupIngressOpenToWorldRule"}, ) ], ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template(bad_template, mock_config) assert result.valid assert compare_lists_of_failures(result.failures, [])
def test_non_matching_filters_are_reported_normally( single_security_group_one_cidr_ingress): mock_config = Config( rules=["EC2SecurityGroupMissingEgressRule"], aws_account_id="123456789", stack_name="mockstack", rules_filters=[ Filter( rule_mode=RuleMode.ALLOWED, eval={"eq": [{ "ref": "config.stack_name" }, "anotherstack"]}, rules={"EC2SecurityGroupMissingEgressRule"}, ) ], ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) result = processor.process_cf_template( single_security_group_one_cidr_ingress, mock_config) assert not result.valid assert compare_lists_of_failures( result.failures, [ Failure( granularity=RuleGranularity.RESOURCE, reason= "Missing egress rule in sg means all traffic is allowed outbound. Make this explicit if it is desired configuration", risk_value=RuleRisk.MEDIUM, rule="EC2SecurityGroupMissingEgressRule", rule_mode=RuleMode.BLOCKING, actions=None, resource_ids={"sg"}, resource_types={"AWS::EC2::SecurityGroup"}, ) ], )
def init_cfripper() -> Tuple[Config, RuleProcessor]: config = Config(rules=DEFAULT_RULES.keys()) rule_processor = RuleProcessor( *[DEFAULT_RULES.get(rule)(config) for rule in config.rules]) return config, rule_processor
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." ) result = Result() template = get_template(event) if not template: # In case of an invalid script log a warning and return early 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, result) for rule in config.rules] processor = RuleProcessor(*rules) processor.process_cf_template(template, config, result) perform_logging(result, config, event) return { "valid": result.valid, "reason": ",".join([ "{}-{}".format(r["rule"], r["reason"]) for r in result.failed_rules ]), "failed_rules": RuleProcessor.remove_debug_rules(rules=result.failed_rules), "exceptions": [x.args[0] for x in result.exceptions], "warnings": RuleProcessor.remove_debug_rules(rules=result.failed_monitored_rules), }
def test_debug_filter(template_cross_account_role_with_name, caplog): logging.disable(logging.NOTSET) caplog.set_level(logging.DEBUG) mock_config = Config( rules=["CrossAccountTrustRule"], aws_account_id="123456789", stack_name="mockstack", rules_filters=[ Filter( reason="Test reason", rule_mode=RuleMode.ALLOWED, eval={ "and": [ { "and": [ { "exists": { "ref": "resource.Properties.RoleName" } }, { "regex": [ "^prefix-.*$", { "ref": "resource.Properties.RoleName" } ] }, ] }, { "eq": [{ "ref": "principal" }, "arn:aws:iam::999999999:role/[email protected]"] }, ] }, rules={"CrossAccountTrustRule"}, debug=True, ), ], ) rules = [ DEFAULT_RULES.get(rule)(mock_config) for rule in mock_config.rules ] processor = RuleProcessor(*rules) processor.process_cf_template(template_cross_account_role_with_name, mock_config) for line in [ "Filter: Test reason", "ref(resource.Properties.RoleName) -> prefix-test-root-role", "exists(prefix-test-root-role) -> True", "ref(resource.Properties.RoleName) -> prefix-test-root-role", "regex(^prefix-.*$, prefix-test-root-role) -> True", "ref(principal) -> arn:aws:iam::999999999:role/[email protected]", "eq(arn:aws:iam::999999999:role/[email protected], arn:aws:iam::999999999:role/[email protected]) -> True", "Filter result: True", ]: assert line in caplog.text