def test_only_whitelisted_resources_are_removed(mock_rule_to_resource_whitelist): config = Config( stack_name="otherstack", rules=["S3CrossAccountTrustRule"], rule_to_resource_whitelist=mock_rule_to_resource_whitelist, ) result = Result() failed_rules = [ Failure( rule="S3CrossAccountTrustRule", reason="Forbidden cross-account policy allow with 123456789 for an S3 bucket.", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, resource_ids={"rolething", "thenotwhitelistedthing", "anotherone"}, actions=None, granularity=RuleGranularity.RESOURCE, ) ] result.failed_rules = failed_rules RuleProcessor.remove_failures_of_whitelisted_resources(config=config, result=result) assert result.failed_rules == [ Failure( rule="S3CrossAccountTrustRule", reason="Forbidden cross-account policy allow with 123456789 for an S3 bucket.", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, resource_ids={"thenotwhitelistedthing", "anotherone"}, actions=None, granularity=RuleGranularity.RESOURCE, ) ]
def test_action_whitelist_keeps_non_whitelisted_actions(): whitelist_for_all_stacks = {"MockRule": {".*": {"s3:List"}}} config = Config(stack_name="abcd", rules=["MockRule"], rule_to_action_whitelist=whitelist_for_all_stacks) result = Result() failed_rules = [ Failure( rule="MockRule", reason="MockRule is invalid for some actions", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, actions={"s3:ListBucket", "s3:GetBucket"}, granularity=RuleGranularity.ACTION, ) ] result.failed_rules = failed_rules RuleProcessor.remove_failures_of_whitelisted_actions(config=config, result=result) assert result.failed_rules == [ Failure( rule="MockRule", reason="MockRule is invalid for some actions", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, actions={"s3:GetBucket"}, granularity=RuleGranularity.ACTION, ) ]
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_remove_failures_from_whitelisted_resources_only_removes_resource_granularity(mock_rule_to_resource_whitelist): config = Config( stack_name="otherstack", rules=["S3CrossAccountTrustRule"], rule_to_resource_whitelist=mock_rule_to_resource_whitelist, ) result = Result() failed_rules = [ { "rule": "S3CrossAccountTrustRule", "reason": "rolething has forbidden cross-account policy allow with 123456789 for an S3 bucket.", "rule_mode": RuleMode.BLOCKING, "risk_value": RuleRisk.HIGH, "resource_ids": {"rolething"}, "actions": None, "granularity": RuleGranularity.ACTION, }, { "rule": "S3CrossAccountTrustRule", "reason": "anotherthing has forbidden cross-account policy allow with 123456789 for an S3 bucket.", "rule_mode": RuleMode.BLOCKING, "risk_value": RuleRisk.HIGH, "resource_ids": {"anotherthing"}, "actions": None, "granularity": RuleGranularity.RESOURCE, } ] result.failed_rules = failed_rules RuleProcessor.remove_failures_of_whitelisted_resources(config=config, result=result) assert result.failed_rules == failed_rules
def test_with_no_rules(mock_remove_whitelisted_actions, mock_remove_whitelisted_resources, template): processor = RuleProcessor() config = Mock() result = Result() processor.process_cf_template(template, config, result) mock_remove_whitelisted_actions.assert_called() mock_remove_whitelisted_resources.assert_called()
def test_with_mock_rule(template): rule = Mock() processor = RuleProcessor(rule) config = Mock() result = Result() processor.process_cf_template(template, config, result) rule.invoke.assert_called()
def test_with_mock_rule(self): rule = Mock() processor = RuleProcessor(rule) config = Mock() result = Mock() processor.process_cf_template(EXAMPLE_CF_TEMPLATE, config, result) rule.invoke.assert_called()
def test_can_whitelist_resource_from_any_stack_if_granularity_is_resource(): whitelist_for_all_stacks = { "S3CrossAccountTrustRule": { ".*": { "ProductionAccessTest", }, "otherstack": { "rolething", } }, } config = Config( stack_name="abcd", rules=["S3CrossAccountTrustRule"], rule_to_resource_whitelist=whitelist_for_all_stacks, ) result = Result() failed_rules = [ { "rule": "S3CrossAccountTrustRule", "reason": "ProductionAccessTest has forbidden cross-account policy allow with 123456789 for an S3 bucket.", "rule_mode": RuleMode.BLOCKING, "risk_value": RuleRisk.HIGH, "resource_ids": {"ProductionAccessTest"}, "actions": None, "granularity": RuleGranularity.RESOURCE, }, { "rule": "S3CrossAccountTrustRule", "reason": "This one isn't whitelisted because granularity is ACTION and not RESOURCE", "rule_mode": RuleMode.BLOCKING, "risk_value": RuleRisk.HIGH, "resource_ids": {"ProductionAccessTest"}, "actions": None, "granularity": RuleGranularity.ACTION, }, ] result.failed_rules = failed_rules RuleProcessor.remove_failures_of_whitelisted_resources(config=config, result=result) assert result.failed_rules == [{ "rule": "S3CrossAccountTrustRule", "reason": "This one isn't whitelisted because granularity is ACTION and not RESOURCE", "rule_mode": RuleMode.BLOCKING, "risk_value": RuleRisk.HIGH, "resource_ids": {"ProductionAccessTest"}, "actions": None, "granularity": RuleGranularity.ACTION, }]
def test_remove_debug_rules(): original_failed_monitored_rules = [ Failure( rule="a", reason="something", rule_mode=RuleMode.MONITOR, granularity=RuleGranularity.STACK, risk_value=RuleRisk.HIGH, ), Failure( rule="b", reason="something", rule_mode=RuleMode.DEBUG, granularity=RuleGranularity.STACK, risk_value=RuleRisk.MEDIUM, ), Failure( rule="c", reason="something", rule_mode=RuleMode.MONITOR, granularity=RuleGranularity.STACK, risk_value=RuleRisk.LOW, ), ] list_with_no_debug_rules = [original_failed_monitored_rules[0], original_failed_monitored_rules[2]] processed_list = RuleProcessor.remove_debug_rules(rules=original_failed_monitored_rules) assert list_with_no_debug_rules == processed_list
def test_whitelisted_stacks_do_not_report_anything(template_two_roles_dict): result = Result() 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, result) for rule in mock_config.rules ] processor = RuleProcessor(*rules) processor.process_cf_template(template_two_roles_dict, mock_config, result) assert result.valid
def test_non_whitelisted_stacks_are_reported_normally( template_two_roles_dict, expected_result_two_roles): result = Result() 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, result) for rule in mock_config.rules ] processor = RuleProcessor(*rules) processor.process_cf_template(template_two_roles_dict, mock_config, result) assert not result.valid assert result.failed_rules == expected_result_two_roles
def test_remove_failures_from_whitelisted_actions_only_removes_action_granularity(mock_rule_to_action_whitelist): config = Config( stack_name="teststack", rules=["S3CrossAccountTrustRule"], rule_to_action_whitelist=mock_rule_to_action_whitelist, ) result = Result() failed_rules = [ Failure( rule="WildcardResourceRule", reason="rolething is using a wildcard resource in BucketAccessPolicy", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, resource_ids={"BucketAccessPolicy"}, actions={"s3:Get*"}, granularity=RuleGranularity.ACTION, ), Failure( rule="WildcardResourceRule", reason="rolething is using a wildcard resource in BucketAccessPolicy", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, resource_ids=set(), actions=set(), granularity=RuleGranularity.STACK, ), ] result.failed_rules = failed_rules RuleProcessor.remove_failures_of_whitelisted_actions(config=config, result=result) assert result.failed_rules == [ Failure( rule="WildcardResourceRule", reason="rolething is using a wildcard resource in BucketAccessPolicy", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, resource_ids=set(), actions=set(), granularity=RuleGranularity.STACK, ) ]
def test_remove_failures_from_whitelisted_actions_failure_no_actions_is_removed(mock_logger, mock_rule_to_action_whitelist): config = Config( stack_name="teststack", rules=["S3CrossAccountTrustRule"], rule_to_action_whitelist=mock_rule_to_action_whitelist, ) result = Result() failure = { "rule": "S3CrossAccountTrustRule", "reason": "rolething has forbidden cross-account policy allow with 123456789 for an S3 bucket.", "rule_mode": RuleMode.BLOCKING, "risk_value": RuleRisk.HIGH, "actions": set(), "granularity": RuleGranularity.ACTION, } result.failed_rules = [failure] RuleProcessor.remove_failures_of_whitelisted_actions(config=config, result=result) assert result.failed_rules == [] mock_logger.assert_called_once_with(f"Failure with action granularity doesn't have actions: {failure}")
def test_remove_failures_from_whitelisted_actions_uses_whitelist(mock_rule_to_action_whitelist): config = Config( stack_name="teststack", rules=["WildcardResourceRule"], rule_to_action_whitelist=mock_rule_to_action_whitelist ) result = Result() result.failed_rules = [ Failure( rule="WildcardResourceRule", reason="rolething is using a wildcard resource in BucketAccessPolicy", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, resource_ids={"BucketAccessPolicy"}, actions={"s3:Get*"}, granularity=RuleGranularity.ACTION, ), Failure( rule="WildcardResourceRule", reason="rolething is using a wildcard resource in DynamoAccessPolicy", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, resource_ids={"DynamoAccessPolicy"}, actions={"dynamodb:Get"}, granularity=RuleGranularity.ACTION, ), ] RuleProcessor.remove_failures_of_whitelisted_actions(config=config, result=result) assert result.failed_rules == [ Failure( rule="WildcardResourceRule", reason="rolething is using a wildcard resource in DynamoAccessPolicy", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, resource_ids={"DynamoAccessPolicy"}, actions={"dynamodb:Get"}, granularity=RuleGranularity.ACTION, ) ]
def test_can_whitelist_action_from_any_stack_if_granularity_is_action(): whitelist_for_all_stacks = {"S3CrossAccountTrustRule": {".*": {"s3:ListBucket"}}} config = Config( stack_name="abcd", rules=["S3CrossAccountTrustRule"], rule_to_action_whitelist=whitelist_for_all_stacks ) result = Result() failed_rules = [ Failure( rule="S3CrossAccountTrustRule", reason="ProductionAccessTest has forbidden cross-account policy allow with 123456789 for an S3 bucket.", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, actions={"s3:ListBucket"}, granularity=RuleGranularity.ACTION, ), Failure( rule="S3CrossAccountTrustRule", reason="This one isn't whitelisted because granularity is STACK and not ACTION", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, actions=set(), granularity=RuleGranularity.STACK, ), ] result.failed_rules = failed_rules RuleProcessor.remove_failures_of_whitelisted_actions(config=config, result=result) assert result.failed_rules == [ Failure( rule="S3CrossAccountTrustRule", reason="This one isn't whitelisted because granularity is STACK and not ACTION", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, actions=set(), granularity=RuleGranularity.STACK, ) ]
def test_remove_failures_from_whitelisted_resources_failure_no_resources_is_removed( mock_logger, mock_rule_to_resource_whitelist ): config = Config( stack_name="otherstack", rules=["S3CrossAccountTrustRule"], rule_to_resource_whitelist=mock_rule_to_resource_whitelist, ) result = Result() failure = Failure( rule="S3CrossAccountTrustRule", reason="rolething has forbidden cross-account policy allow with 123456789 for an S3 bucket.", rule_mode=RuleMode.BLOCKING, risk_value=RuleRisk.HIGH, actions=None, granularity=RuleGranularity.RESOURCE, ) result.failed_rules = [failure] RuleProcessor.remove_failures_of_whitelisted_resources(config=config, result=result) assert result.failed_rules == [] mock_logger.assert_called_once_with(f"Failure with resource granularity doesn't have resources: {failure}")
def test_remove_debug_rules(): original_failed_monitored_rules = [ {"rule": "a", "reason": "something", "rule_mode": RuleMode.MONITOR, "risk_value": RuleRisk.HIGH}, {"rule": "b", "reason": "something", "rule_mode": RuleMode.DEBUG, "risk_value": RuleRisk.MEDIUM}, {"rule": "c", "reason": "something", "rule_mode": RuleMode.MONITOR, "risk_value": RuleRisk.LOW}, ] list_with_no_debug_rules = [ {"rule": "a", "reason": "something", "rule_mode": RuleMode.MONITOR, "risk_value": RuleRisk.HIGH}, {"rule": "c", "reason": "something", "rule_mode": RuleMode.MONITOR, "risk_value": RuleRisk.LOW}, ] processed_list = RuleProcessor.remove_debug_rules(rules=original_failed_monitored_rules) assert list_with_no_debug_rules == processed_list
def test_with_templates(self): dir_path = os.path.dirname(os.path.realpath(__file__)) test_templates = glob.glob('{}/test_templates/*.*'.format(dir_path)) for template in test_templates: cf_script = open(template) cf_template = S3Adapter().convert_json_or_yaml_to_dict( cf_script.read()) config = Config( project_name=template, service_name=template, stack_name=template, rules=ALL_RULES.keys(), ) # Scan result result = Result() rules = [ ALL_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_resource_whitelisting_works_as_expected(template_two_roles_dict, expected_result_two_roles): result = Result() 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, result) for rule in mock_config.rules ] processor = RuleProcessor(*rules) processor.process_cf_template(template_two_roles_dict, mock_config, result) assert not result.valid assert result.failed_rules == expected_result_two_roles[-2:]
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 result = Result() cfmodel = pycfmodel.parse(cf_template).resolve() rules = [DEFAULT_RULES.get(rule)(config, result) for rule in config.rules] processor = RuleProcessor(*rules) processor.process_cf_template(cfmodel, config, result) # 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 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_remove_debug_rules_no_rules(): processed_list = RuleProcessor.remove_debug_rules(rules=[]) assert [] == processed_list
def test_with_no_rules(self): processor = RuleProcessor() config = Mock() result = Mock() processor.process_cf_template(EXAMPLE_CF_TEMPLATE, config, result)
def handler(event, context): """ Main entry point of the Lambda function. :param event: { "stack_template_url": String } :param context: :return: """ if not event.get("stack_template_url"): raise ValueError( "Invalid event type: no parameter 'stack_template_url' in request." ) result = Result() s3 = S3Adapter() template = s3.download_template_to_dictionary(event["stack_template_url"]) if not template: # In case of an ivalid script log a warning and return early result.add_exception( TypeError("Malformated CF script: {}".format( event["stack_template_url"]))) 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=ALL_RULES.keys(), ) logger.info("Scan started for: {}; {}; {};".format( config.project_name, config.service_name, config.stack_name, )) rules = [ALL_RULES.get(rule)(config, result) for rule in config.RULES] processor = RuleProcessor(*rules) processor.process_cf_template(template, config, result) if not result.valid: log_results( "Failed rules", config.project_name, config.service_name, config.stack_name, result.failed_rules, result.warnings, event["stack_template_url"], ) logger.info("FAIL: {}; {}; {}".format( config.project_name, config.service_name, config.stack_name, )) else: logger.info("PASS: {}; {}; {}".format( config.project_name, config.service_name, config.stack_name, )) if len(result.failed_monitored_rules) > 0 or len(result.warnings) > 0: log_results( "Failed monitored rules", config.project_name, config.service_name, config.stack_name, result.failed_monitored_rules, result.warnings, event["stack_template_url"], ) return { "valid": str(result.valid).lower(), "reason": ",".join([ "{}-{}".format(r["rule"], r["reason"]) for r in result.failed_rules ]), "failed_rules": result.failed_rules, "exceptions": [x.args[0] for x in result.exceptions], "warnings": result.failed_monitored_rules, }
def handler(event, context): """ Main entry point of the Lambda function. :param event: Request JSON format for proxy integration { "resource": "Resource path", "path": "Path parameter", "httpMethod": "Incoming request's method name" "headers": {Incoming request headers} "queryStringParameters": {"stack_template_url": String } "pathParameters": {path parameters} "stageVariables": {Applicable stage variables} "requestContext": {Request context, including authorizer-returned key-value pairs} "body": "A JSON string of the request payload." "isBase64Encoded": "A boolean flag to indicate if the applicable request payload is Base64-encode" } :param context: :return: Response JSON format { "isBase64Encoded": true|false, "statusCode": httpStatusCode, "headers": { "headerName": "headerValue", ... }, "body": "..." } """ print(event) qp = event.get("queryStringParameters") if not qp.get("stack_template_url"): raise ValueError( "Invalid event type: no parameter 'stack_template_url' in request." ) result = Result() s3 = S3Adapter() template = s3.download_template_to_dictionary(qp["stack_template_url"]) if not template: # In case of an ivalid script log a warning and return early result.add_exception( TypeError("Malformated CF script: {}".format( qp["stack_template_url"]))) return { "isBase64Encoded": False, "statusCode": 400, "headers": {}, "body": str({ "valid": "false", "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=ALL_RULES.keys(), event=event.get("event"), template_url=event.get("stack_template_url"), ) logger.info("Scan started for: {}; {}; {};".format( config.project_name, config.service_name, config.stack_name, )) rules = [ALL_RULES.get(rule)(config, result) for rule in config.RULES] processor = RuleProcessor(*rules) processor.process_cf_template(template, config, result) if not result.valid: log_results( "Failed rules", config.project_name, config.service_name, config.stack_name, result.failed_rules, result.warnings, qp["stack_template_url"], ) logger.info("FAIL: {}; {}; {}".format( config.project_name, config.service_name, config.stack_name, )) else: logger.info("PASS: {}; {}; {}".format( config.project_name, config.service_name, config.stack_name, )) if len(result.failed_monitored_rules) > 0 or len(result.warnings) > 0: log_results( "Failed monitored rules", config.project_name, config.service_name, config.stack_name, result.failed_monitored_rules, result.warnings, qp["stack_template_url"], ) # TODO base64 encode and implement more error code responses return { "isBase64Encoded": False, "statusCode": 200, "headers": {}, "body": str({ "valid": str(result.valid).lower(), "reason": ",".join([ "{}-{}".format(r["rule"], r["reason"]) for r in result.failed_rules ]), "failed_rules": result.failed_rules, "exceptions": [x.args[0] for x in result.exceptions], "warnings": result.failed_monitored_rules, }) }