Back to Checks
aws

CloudWatch log metric filter and alarm exist for Network ACL (NACL) change events

cloudwatch_changes_to_network_acls_alarm_configured
Service: cloudwatchSeverity: medium
OfficialBuilt-inVerified
Categories:logging
Resource Type:AwsCloudWatchAlarm

CloudTrail records for Network ACL changes are matched by a CloudWatch Logs metric filter with an associated alarm for events like CreateNetworkAcl, CreateNetworkAclEntry, DeleteNetworkAcl, DeleteNetworkAclEntry, ReplaceNetworkAclEntry, and ReplaceNetworkAclAssociation.

Risk

Absent monitoring of NACL changes reduces detection of policy tampering, risking loss of confidentiality (opened ingress/egress), degraded network integrity (lateral movement, bypassed segmentation), and reduced availability (traffic blackholes or lockouts).

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks cloudwatch_changes_to_network_acls_alarm_configured

Recommendation

Implement a CloudWatch Logs metric filter and alarm for NACL change events from CloudTrail and route alerts to responders. Enforce least privilege on NACL management, require change control, and use defense in depth with configuration monitoring and flow logs to validate and monitor network posture.

Remediation

Terraform

# CloudWatch metric filter and alarm for NACL changes
resource "aws_cloudwatch_log_metric_filter" "nacl" {
  name           = "nacl_changes"
  log_group_name = "<example_resource_name>"  # CloudTrail log group
  pattern        = "{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }"  # CRITICAL: detects NACL changes

  metric_transformation {
    name      = "nacl_changes"
    namespace = "CISBenchmark"
    value     = "1"
  }
}

resource "aws_cloudwatch_metric_alarm" "nacl" {
  alarm_name          = "nacl_changes"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  metric_name         = "nacl_changes"   # CRITICAL: alarm targets the metric from the filter
  namespace           = "CISBenchmark"
  period              = 300
  statistic           = "Sum"
  threshold           = 1
}

NativeIaC

# CloudFormation to alert on NACL changes
Resources:
  MetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      LogGroupName: "<example_resource_name>"  # CRITICAL: CloudTrail log group to monitor
      FilterPattern: '{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }'  # CRITICAL: detects NACL changes
      MetricTransformations:
        - MetricValue: "1"
          MetricNamespace: "CISBenchmark"
          MetricName: "nacl_changes"

  NaclChangesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: "nacl_changes"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      EvaluationPeriods: 1
      MetricName: "nacl_changes"   # CRITICAL: alarm targets the metric from the filter
      Namespace: "CISBenchmark"
      Period: 300
      Statistic: Sum
      Threshold: 1

Other

1In the AWS Console, go to CloudWatch > Log groups and open the CloudTrail log group
2Metric filters tab > Create metric filter
3Set Filter pattern to:
4{ ($.eventName = CreateNetworkAcl) || ($.eventName = CreateNetworkAclEntry) || ($.eventName = DeleteNetworkAcl) || ($.eventName = DeleteNetworkAclEntry) || ($.eventName = ReplaceNetworkAclEntry) || ($.eventName = ReplaceNetworkAclAssociation) }
5Next > Filter name: nacl_changes; Metric namespace: CISBenchmark; Metric name: nacl_changes; Metric value: 1 > Create metric filter
6Select the new metric filter > Create alarm
7Set Statistic: Sum, Period: 5 minutes, Threshold type: Static, Condition: Greater/Equal, Threshold: 1
8Next through actions (optional) > Name: nacl_changes > Create alarm

Check Code

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.cloudtrail.cloudtrail_client import (
    cloudtrail_client,
)
from prowler.providers.aws.services.cloudwatch.cloudwatch_client import (
    cloudwatch_client,
)
from prowler.providers.aws.services.cloudwatch.lib.metric_filters import (
    build_metric_filter_pattern,
    check_cloudwatch_log_metric_filter,
)
from prowler.providers.aws.services.cloudwatch.logs_client import logs_client


class cloudwatch_changes_to_network_acls_alarm_configured(Check):
    """CloudWatch log metric filter and alarm exist for Network ACL (NACL) change events.

    Looks for a CloudWatch Logs metric filter matching the expected pattern on a
    log group used by a CloudTrail trail, with at least one alarm on its metric.

    - PASS: A matching metric filter with an associated alarm exists.
    - FAIL: No matching metric filter, or a filter without an alarm, was found.
    - MANUAL: CloudTrail trails, log groups, metric filters or alarms could
      not be listed in at least one region, so the absence of a filter/alarm
      cannot be asserted.
    """

    def execute(self) -> list[Check_Report_AWS]:
        """Evaluate the metric filter and alarm coverage for the account.

        Returns:
            list[Check_Report_AWS]: A single report for the account.
        """
        pattern = build_metric_filter_pattern(
            event_names=[
                "CreateNetworkAcl",
                "CreateNetworkAclEntry",
                "DeleteNetworkAcl",
                "DeleteNetworkAclEntry",
                "ReplaceNetworkAclEntry",
                "ReplaceNetworkAclAssociation",
            ],
        )
        findings = []

        report = check_cloudwatch_log_metric_filter(
            pattern,
            cloudtrail_client.trails,
            logs_client.metric_filters,
            cloudwatch_client.metric_alarms,
            self.metadata(),
        )

        inventory_unavailable = (
            cloudtrail_client.trails_unavailable
            or logs_client.log_groups_unavailable
            or logs_client.metric_filters_unavailable
            or cloudwatch_client.metric_alarms_unavailable
        )

        if report is None:
            report = Check_Report_AWS(metadata=self.metadata(), resource={})
            report.status = "FAIL"
            report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
            report.region = logs_client.region
            report.resource_id = logs_client.audited_account
            report.resource_arn = logs_client.log_group_arn_template
            report.resource_tags = []

        # A denied listing in any region means the inventory is incomplete: a
        # PASS is still backed by a real filter and alarm, but a FAIL (nothing
        # found, or a filter found without its alarm) cannot be trusted.
        if report.status == "FAIL" and inventory_unavailable:
            report.status = "MANUAL"
            report.status_extended = "Cannot evaluate CloudWatch metric filters and alarms: CloudTrail trails, log groups, metric filters or alarms could not be listed in at least one region. Verify that the scanning credentials are allowed to call cloudtrail:DescribeTrails, logs:DescribeLogGroups, logs:DescribeMetricFilters and cloudwatch:DescribeAlarms."

        findings.append(report)

        return findings