diff --git a/prowler/providers/aws/services/waf/waf_global_webacl_with_rules/__init__.py b/prowler/providers/aws/services/waf/waf_global_webacl_with_rules/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/aws/services/waf/waf_global_webacl_with_rules/waf_global_webacl_with_rules.metadata.json b/prowler/providers/aws/services/waf/waf_global_webacl_with_rules/waf_global_webacl_with_rules.metadata.json new file mode 100644 index 00000000000..64808465ac6 --- /dev/null +++ b/prowler/providers/aws/services/waf/waf_global_webacl_with_rules/waf_global_webacl_with_rules.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "waf_global_webacl_with_rules", + "CheckTitle": "Check if AWS WAF Classic Global WebACL has at least one rule or rule group.", + "CheckType": [ + "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls" + ], + "ServiceName": "waf", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:waf:account-id:webacl/web-acl-id", + "Severity": "medium", + "ResourceType": "AwsWafWebAcl", + "Description": "Ensure that every AWS WAF Classic Global WebACL contains at least one rule or rule group.", + "Risk": "An empty AWS WAF Classic Global web ACL allows all web traffic to bypass inspection, potentially exposing resources to unauthorized access and attacks.", + "RelatedUrl": "https://docs.aws.amazon.com/waf/latest/developerguide/waf-rules.html", + "Remediation": { + "Code": { + "CLI": "aws waf update-web-acl --web-acl-id --change-token --updates '[{\"Action\":\"INSERT\",\"ActivatedRule\":{\"Priority\":1,\"RuleId\":\"\",\"Action\":{\"Type\":\"BLOCK\"}}}]' --default-action Type=ALLOW --region ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/waf-controls.html#waf-8", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that every AWS WAF Classic Global web ACL includes at least one rule or rule group to monitor and control web traffic effectively.", + "Url": "https://docs.aws.amazon.com/waf/latest/developerguide/classic-web-acl-editing.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/waf/waf_global_webacl_with_rules/waf_global_webacl_with_rules.py b/prowler/providers/aws/services/waf/waf_global_webacl_with_rules/waf_global_webacl_with_rules.py new file mode 100644 index 00000000000..571ac7a7e60 --- /dev/null +++ b/prowler/providers/aws/services/waf/waf_global_webacl_with_rules/waf_global_webacl_with_rules.py @@ -0,0 +1,23 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.waf.waf_client import waf_client + + +class waf_global_webacl_with_rules(Check): + def execute(self): + findings = [] + for acl in waf_client.web_acls.values(): + report = Check_Report_AWS(self.metadata()) + report.region = acl.region + report.resource_id = acl.id + report.resource_arn = acl.arn + report.resource_tags = acl.tags + report.status = "FAIL" + report.status_extended = f"AWS WAF Global Web ACL {acl.name} does not have any rules or rule groups." + + if acl.rules or acl.rule_groups: + report.status = "PASS" + report.status_extended = f"AWS WAF Global Web ACL {acl.name} has at least one rule or rule group." + + findings.append(report) + + return findings diff --git a/tests/providers/aws/services/waf/waf_global_webacl_with_rules/waf_global_webacl_with_rules_test.py b/tests/providers/aws/services/waf/waf_global_webacl_with_rules/waf_global_webacl_with_rules_test.py new file mode 100644 index 00000000000..777b340581b --- /dev/null +++ b/tests/providers/aws/services/waf/waf_global_webacl_with_rules/waf_global_webacl_with_rules_test.py @@ -0,0 +1,381 @@ +from unittest import mock +from unittest.mock import patch + +import botocore +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +WEB_ACL_ID = "test-web-acl-id" +WEB_ACL_NAME = "test-web-acl-name" + +# Original botocore _make_api_call function +orig = botocore.client.BaseClient._make_api_call + + +# Mocked botocore _make_api_call function +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "GetChangeToken": + return {"ChangeToken": "my-change-token"} + if operation_name == "ListWebACLs": + return { + "WebACLs": [ + {"WebACLId": WEB_ACL_ID, "Name": WEB_ACL_NAME}, + ] + } + if operation_name == "GetWebACL": + return { + "WebACL": { + "Rules": [], + } + } + # If we don't want to patch the API call + return orig(self, operation_name, kwarg) + + +def mock_make_api_call_only_rules(self, operation_name, kwarg): + unused_operations = [ + "ListResourcesForWebACL", + "ListRuleGroups", + "ListActivatedRulesInRuleGroup", + ] + if operation_name in unused_operations: + return {} + if operation_name == "ListRules": + return { + "Rules": [ + { + "RuleId": "my-rule-id", + "Name": "my-rule", + }, + ] + } + if operation_name == "GetRule": + return { + "Rule": { + "RuleId": "my-rule-id", + "Name": "my-rule", + "Predicates": [ + { + "Negated": False, + "Type": "IPMatch", + "DataId": "my-data-id", + } + ], + } + } + if operation_name == "GetChangeToken": + return {"ChangeToken": "my-change-token"} + if operation_name == "ListWebACLs": + return { + "WebACLs": [ + {"WebACLId": WEB_ACL_ID, "Name": WEB_ACL_NAME}, + ] + } + if operation_name == "GetWebACL": + return { + "WebACL": { + "Rules": [ + { + "RuleId": "my-rule-id", + "Type": "BLOCK", + } + ], + } + } + # If we don't want to patch the API call + return orig(self, operation_name, kwarg) + + +def mock_make_api_call_only_rule_groups(self, operation_name, kwarg): + unused_operations = [ + "ListResourcesForWebACL", + "ListRules", + "GetRule", + ] + if operation_name in unused_operations: + return {} + if operation_name == "ListRuleGroups": + return { + "RuleGroups": [ + { + "RuleGroupId": "my-rule-group-id", + "Name": "my-rule-group", + }, + ] + } + if operation_name == "ListActivatedRulesInRuleGroup": + return {} + if operation_name == "GetChangeToken": + return {"ChangeToken": "my-change-token"} + if operation_name == "ListWebACLs": + return { + "WebACLs": [ + {"WebACLId": WEB_ACL_ID, "Name": WEB_ACL_NAME}, + ] + } + if operation_name == "GetWebACL": + return { + "WebACL": { + "Rules": [ + { + "RuleId": "my-rule-group-id", + "Type": "GROUP", + } + ], + } + } + # If we don't want to patch the API call + return orig(self, operation_name, kwarg) + + +def mock_make_api_call_both(self, operation_name, kwarg): + unused_operations = [ + "ListResourcesForWebACL", + ] + if operation_name in unused_operations: + return {} + if operation_name == "ListRules": + return { + "Rules": [ + { + "RuleId": "my-rule-id", + "Name": "my-rule", + }, + ] + } + if operation_name == "GetRule": + return { + "Rule": { + "RuleId": "my-rule-id", + "Name": "my-rule", + "Predicates": [ + { + "Negated": False, + "Type": "IPMatch", + "DataId": "my-data-id", + } + ], + } + } + if operation_name == "ListRuleGroups": + return { + "RuleGroups": [ + { + "RuleGroupId": "my-rule-group-id", + "Name": "my-rule-group", + }, + ] + } + if operation_name == "ListActivatedRulesInRuleGroup": + return { + "ActivatedRules": [ + { + "RuleId": "my-rule-id", + }, + ] + } + if operation_name == "GetChangeToken": + return {"ChangeToken": "my-change-token"} + if operation_name == "ListWebACLs": + return { + "WebACLs": [ + {"WebACLId": WEB_ACL_ID, "Name": WEB_ACL_NAME}, + ] + } + if operation_name == "GetWebACL": + return { + "WebACL": { + "Rules": [ + { + "RuleId": "my-rule-id", + "Type": "BLOCK", + }, + { + "RuleId": "my-rule-group-id", + "Type": "GROUP", + }, + ], + } + } + # If we don't want to patch the API call + return orig(self, operation_name, kwarg) + + +class Test_waf_global_webacl_with_rules: + @mock_aws + def test_no_waf(self): + from prowler.providers.aws.services.waf.waf_service import WAF + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.waf.waf_global_webacl_with_rules.waf_global_webacl_with_rules.waf_client", + new=WAF(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.waf.waf_global_webacl_with_rules.waf_global_webacl_with_rules import ( + waf_global_webacl_with_rules, + ) + + check = waf_global_webacl_with_rules() + result = check.execute() + + assert len(result) == 0 + + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @mock_aws + def test_waf_no_rules_and_no_rule_group(self): + from prowler.providers.aws.services.waf.waf_service import WAF + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.waf.waf_global_webacl_with_rules.waf_global_webacl_with_rules.waf_client", + new=WAF(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.waf.waf_global_webacl_with_rules.waf_global_webacl_with_rules import ( + waf_global_webacl_with_rules, + ) + + check = waf_global_webacl_with_rules() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"AWS WAF Global Web ACL {WEB_ACL_NAME} does not have any rules or rule groups." + ) + assert result[0].resource_id == WEB_ACL_ID + assert ( + result[0].resource_arn + == f"arn:aws:waf:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:webacl/{WEB_ACL_ID}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @patch( + "botocore.client.BaseClient._make_api_call", new=mock_make_api_call_only_rules + ) + @mock_aws + def test_waf_rules_and_no_rule_group(self): + from prowler.providers.aws.services.waf.waf_service import WAF + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.waf.waf_global_webacl_with_rules.waf_global_webacl_with_rules.waf_client", + new=WAF(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.waf.waf_global_webacl_with_rules.waf_global_webacl_with_rules import ( + waf_global_webacl_with_rules, + ) + + check = waf_global_webacl_with_rules() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"AWS WAF Global Web ACL {WEB_ACL_NAME} has at least one rule or rule group." + ) + assert result[0].resource_id == WEB_ACL_ID + assert ( + result[0].resource_arn + == f"arn:aws:waf:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:webacl/{WEB_ACL_ID}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @patch( + "botocore.client.BaseClient._make_api_call", + new=mock_make_api_call_only_rule_groups, + ) + @mock_aws + def test_waf_no_rules_and_rule_group(self): + from prowler.providers.aws.services.waf.waf_service import WAF + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.waf.waf_global_webacl_with_rules.waf_global_webacl_with_rules.waf_client", + new=WAF(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.waf.waf_global_webacl_with_rules.waf_global_webacl_with_rules import ( + waf_global_webacl_with_rules, + ) + + check = waf_global_webacl_with_rules() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"AWS WAF Global Web ACL {WEB_ACL_NAME} has at least one rule or rule group." + ) + assert result[0].resource_id == WEB_ACL_ID + assert ( + result[0].resource_arn + == f"arn:aws:waf:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:webacl/{WEB_ACL_ID}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call_both) + @mock_aws + def test_waf_rules_and_rule_group(self): + from prowler.providers.aws.services.waf.waf_service import WAF + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.waf.waf_global_webacl_with_rules.waf_global_webacl_with_rules.waf_client", + new=WAF(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.waf.waf_global_webacl_with_rules.waf_global_webacl_with_rules import ( + waf_global_webacl_with_rules, + ) + + check = waf_global_webacl_with_rules() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"AWS WAF Global Web ACL {WEB_ACL_NAME} has at least one rule or rule group." + ) + assert result[0].resource_id == WEB_ACL_ID + assert ( + result[0].resource_arn + == f"arn:aws:waf:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:webacl/{WEB_ACL_ID}" + ) + assert result[0].region == AWS_REGION_US_EAST_1