mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(waf): add check for regional web ACL logging enabled (#11539)
Co-authored-by: Lydia Vilchez <lydiavilchezlopez@gmail.com>
This commit is contained in:
@@ -9,6 +9,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- `entra_conditional_access_policy_explicitly_targets_azure_devops` check for M365 provider, verifying at least one enabled Conditional Access policy explicitly includes the Azure DevOps cloud application instead of relying on a broad "All cloud apps" policy [(#11182)](https://github.com/prowler-cloud/prowler/pull/11182)
|
||||
- `entra_conditional_access_policy_no_exclusion_gaps` check for M365 provider, verifying every user, group, role, or application excluded from an enabled Conditional Access policy stays in scope of another enabled policy [(#11577)](https://github.com/prowler-cloud/prowler/pull/11577)
|
||||
- `stepfunctions_statemachine_encrypted_with_cmk` check for AWS provider, verifying that each Step Functions state machine uses a customer-managed KMS key for encryption at rest rather than the default AWS-owned key [(#11538)](https://github.com/prowler-cloud/prowler/pull/11538)
|
||||
- `waf_regional_webacl_logging_enabled` check for AWS provider, verifying that each AWS WAF Classic Regional Web ACL has logging enabled to a Kinesis Data Firehose stream [(#11539)](https://github.com/prowler-cloud/prowler/pull/11539)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "waf_regional_webacl_logging_enabled",
|
||||
"CheckTitle": "AWS WAF Classic Regional Web ACL has logging enabled",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices",
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls (USA)",
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/PCI-DSS"
|
||||
],
|
||||
"ServiceName": "waf",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "AwsWafRegionalWebAcl",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "**AWS WAF Classic Regional Web ACLs** are evaluated for **logging** enabled to capture evaluated web requests and rule actions. Regional Web ACLs protect Application Load Balancers and API Gateway stages.",
|
||||
"Risk": "Without **WAF logging**, you lose **visibility** into attacks (SQLi/XSS probes, bots, brute-force) and into allow/block decisions for ALB and API Gateway traffic. This limits detection, forensics, and incident response, weakening **confidentiality**, **integrity**, and **availability**.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/waf/latest/developerguide/classic-logging.html",
|
||||
"https://docs.aws.amazon.com/securityhub/latest/userguide/waf-controls.html",
|
||||
"https://docs.aws.amazon.com/cli/latest/reference/waf-regional/put-logging-configuration.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws waf-regional put-logging-configuration --logging-configuration ResourceArn=<web_acl_arn>,LogDestinationConfigs=<kinesis_firehose_delivery_stream_arn> --region <region>",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Create an Amazon Kinesis Data Firehose delivery stream with a name starting with \"aws-waf-logs-\" in the same region as your Web ACL\n2. Open the AWS WAF console and switch to AWS WAF Classic\n3. Select Filter: Regional (your region) and go to Web ACLs\n4. Open the target Web ACL and go to the Logging tab\n5. Click Enable logging and select the Firehose delivery stream created in step 1\n6. Click Enable/Save",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Enable **logging** on all Regional Web ACLs and send records to a centralized logging platform. Apply **least privilege** to log destinations, redact sensitive fields, and monitor for anomalies. Integrate logs with incident response for **defense in depth** and faster containment.",
|
||||
"Url": "https://hub.prowler.com/check/waf_regional_webacl_logging_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"logging"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"waf_global_webacl_logging_enabled"
|
||||
],
|
||||
"Notes": ""
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
from typing import List
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.waf.wafregional_client import wafregional_client
|
||||
|
||||
|
||||
class waf_regional_webacl_logging_enabled(Check):
|
||||
"""Ensure AWS WAF Classic Regional Web ACLs have logging enabled.
|
||||
|
||||
This check evaluates whether each AWS WAF Classic Regional Web ACL has logging
|
||||
enabled by verifying the presence of at least one log destination configured
|
||||
in its logging configuration.
|
||||
|
||||
- PASS: The Web ACL has at least one log destination configured.
|
||||
- FAIL: The Web ACL has no log destinations configured (logging is disabled).
|
||||
"""
|
||||
|
||||
def execute(self) -> List[Check_Report_AWS]:
|
||||
"""Execute the WAF Regional Web ACL logging enabled check.
|
||||
|
||||
Iterates over all WAF Classic Regional Web ACLs and generates a report
|
||||
indicating whether each Web ACL has logging enabled.
|
||||
|
||||
Returns:
|
||||
List[Check_Report_AWS]: A list of report objects with the results of the check.
|
||||
"""
|
||||
findings = []
|
||||
for acl in wafregional_client.web_acls.values():
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=acl)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"AWS WAF Regional Web ACL {acl.name} does not have logging enabled."
|
||||
)
|
||||
|
||||
if acl.logging_enabled:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"AWS WAF Regional Web ACL {acl.name} does have logging enabled."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -168,6 +168,7 @@ class WAFRegional(AWSService):
|
||||
)
|
||||
self.__threading_call__(self._list_web_acls)
|
||||
self.__threading_call__(self._get_web_acl, self.web_acls.values())
|
||||
self.__threading_call__(self._get_logging_configuration, self.web_acls.values())
|
||||
self.__threading_call__(self._list_resources_for_web_acl)
|
||||
|
||||
def _list_rules(self, regional_client):
|
||||
@@ -277,6 +278,34 @@ class WAFRegional(AWSService):
|
||||
f"{acl.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _get_logging_configuration(self, acl):
|
||||
"""Fetch and store the logging configuration for a Regional Web ACL.
|
||||
|
||||
Calls the WAF Regional GetLoggingConfiguration API for the given ACL and
|
||||
sets acl.logging_enabled to True if at least one log destination is configured,
|
||||
False otherwise.
|
||||
|
||||
Args:
|
||||
acl (WebAcl): The Regional Web ACL instance to update.
|
||||
"""
|
||||
logger.info(
|
||||
f"WAFRegional - Getting Regional Web ACL {acl.name} logging configuration..."
|
||||
)
|
||||
try:
|
||||
get_logging_configuration = self.regional_clients[
|
||||
acl.region
|
||||
].get_logging_configuration(ResourceArn=acl.arn)
|
||||
acl.logging_enabled = bool(
|
||||
get_logging_configuration.get("LoggingConfiguration", {}).get(
|
||||
"LogDestinationConfigs", []
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{acl.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def _list_resources_for_web_acl(self, regional_client):
|
||||
logger.info("WAFRegional - Describing resources...")
|
||||
try:
|
||||
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
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"
|
||||
WEB_ACL_ARN = f"arn:aws:waf-regional:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:webacl/{WEB_ACL_ID}"
|
||||
FIREHOSE_ARN = f"arn:aws:firehose:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:deliverystream/aws-waf-logs-regional"
|
||||
|
||||
# Original botocore _make_api_call function
|
||||
orig = botocore.client.BaseClient._make_api_call
|
||||
|
||||
|
||||
def _base_waf_regional_calls(operation_name, kwarg):
|
||||
"""Return responses for WAFRegional API calls that are common across all test scenarios.
|
||||
|
||||
Args:
|
||||
operation_name (str): The name of the botocore operation being called.
|
||||
kwarg (dict): The keyword arguments passed to the API call.
|
||||
|
||||
Returns:
|
||||
dict or None: The mocked API response if the operation is handled, otherwise None.
|
||||
"""
|
||||
unused_operations = [
|
||||
"ListRules",
|
||||
"GetRule",
|
||||
"ListRuleGroups",
|
||||
"ListActivatedRulesInRuleGroup",
|
||||
"ListResourcesForWebACL",
|
||||
]
|
||||
if operation_name in unused_operations:
|
||||
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": []}}
|
||||
return None
|
||||
|
||||
|
||||
def mock_make_api_call_logging_enabled(self, operation_name, kwarg):
|
||||
"""Mock botocore API calls with logging enabled on the Regional Web ACL.
|
||||
|
||||
Args:
|
||||
self: The botocore client instance.
|
||||
operation_name (str): The name of the botocore operation being called.
|
||||
kwarg (dict): The keyword arguments passed to the API call.
|
||||
|
||||
Returns:
|
||||
dict: The mocked API response.
|
||||
"""
|
||||
base = _base_waf_regional_calls(operation_name, kwarg)
|
||||
if base is not None:
|
||||
return base
|
||||
if operation_name == "GetLoggingConfiguration":
|
||||
return {
|
||||
"LoggingConfiguration": {
|
||||
"ResourceArn": WEB_ACL_ARN,
|
||||
"LogDestinationConfigs": [FIREHOSE_ARN],
|
||||
"RedactedFields": [],
|
||||
"ManagedByFirewallManager": False,
|
||||
}
|
||||
}
|
||||
return orig(self, operation_name, kwarg)
|
||||
|
||||
|
||||
def mock_make_api_call_logging_disabled(self, operation_name, kwarg):
|
||||
"""Mock botocore API calls with logging disabled on the Regional Web ACL.
|
||||
|
||||
Args:
|
||||
self: The botocore client instance.
|
||||
operation_name (str): The name of the botocore operation being called.
|
||||
kwarg (dict): The keyword arguments passed to the API call.
|
||||
|
||||
Returns:
|
||||
dict: The mocked API response.
|
||||
"""
|
||||
base = _base_waf_regional_calls(operation_name, kwarg)
|
||||
if base is not None:
|
||||
return base
|
||||
if operation_name == "GetLoggingConfiguration":
|
||||
return {
|
||||
"LoggingConfiguration": {
|
||||
"ResourceArn": WEB_ACL_ARN,
|
||||
"LogDestinationConfigs": [],
|
||||
"RedactedFields": [],
|
||||
"ManagedByFirewallManager": False,
|
||||
}
|
||||
}
|
||||
return orig(self, operation_name, kwarg)
|
||||
|
||||
|
||||
class Test_waf_regional_webacl_logging_enabled:
|
||||
"""Tests for the waf_regional_webacl_logging_enabled check."""
|
||||
|
||||
@mock_aws
|
||||
def test_no_waf(self):
|
||||
"""Test that no findings are returned when no Regional Web ACLs exist."""
|
||||
from prowler.providers.aws.services.waf.waf_service import WAFRegional
|
||||
|
||||
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_regional_webacl_logging_enabled.waf_regional_webacl_logging_enabled.wafregional_client",
|
||||
new=WAFRegional(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.waf.waf_regional_webacl_logging_enabled.waf_regional_webacl_logging_enabled import (
|
||||
waf_regional_webacl_logging_enabled,
|
||||
)
|
||||
|
||||
check = waf_regional_webacl_logging_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
@patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_make_api_call_logging_disabled,
|
||||
)
|
||||
@mock_aws
|
||||
def test_waf_regional_webacl_logging_disabled(self):
|
||||
"""Test that a FAIL finding is returned when logging is disabled on a Regional Web ACL."""
|
||||
from prowler.providers.aws.services.waf.waf_service import WAFRegional
|
||||
|
||||
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_regional_webacl_logging_enabled.waf_regional_webacl_logging_enabled.wafregional_client",
|
||||
new=WAFRegional(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.waf.waf_regional_webacl_logging_enabled.waf_regional_webacl_logging_enabled import (
|
||||
waf_regional_webacl_logging_enabled,
|
||||
)
|
||||
|
||||
check = waf_regional_webacl_logging_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"AWS WAF Regional Web ACL {WEB_ACL_NAME} does not have logging enabled."
|
||||
)
|
||||
assert result[0].resource_id == WEB_ACL_ID
|
||||
assert result[0].resource_arn == WEB_ACL_ARN
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
@patch(
|
||||
"botocore.client.BaseClient._make_api_call",
|
||||
new=mock_make_api_call_logging_enabled,
|
||||
)
|
||||
@mock_aws
|
||||
def test_waf_regional_webacl_logging_enabled(self):
|
||||
"""Test that a PASS finding is returned when logging is enabled on a Regional Web ACL."""
|
||||
from prowler.providers.aws.services.waf.waf_service import WAFRegional
|
||||
|
||||
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_regional_webacl_logging_enabled.waf_regional_webacl_logging_enabled.wafregional_client",
|
||||
new=WAFRegional(aws_provider),
|
||||
):
|
||||
from prowler.providers.aws.services.waf.waf_regional_webacl_logging_enabled.waf_regional_webacl_logging_enabled import (
|
||||
waf_regional_webacl_logging_enabled,
|
||||
)
|
||||
|
||||
check = waf_regional_webacl_logging_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"AWS WAF Regional Web ACL {WEB_ACL_NAME} does have logging enabled."
|
||||
)
|
||||
assert result[0].resource_id == WEB_ACL_ID
|
||||
assert result[0].resource_arn == WEB_ACL_ARN
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
Reference in New Issue
Block a user