Back to Checks
aws

AWS account has a CloudWatch Logs metric filter and alarm for VPC changes

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

CloudTrail events for VPC configuration changes are captured in CloudWatch Logs with a metric filter and an associated alarm. The filter targets actions like CreateVpc, DeleteVpc, ModifyVpcAttribute, and VPC peering operations to surface when network topology is altered.

Risk

Without alerting on VPC changes, unauthorized or accidental edits to routes, peering, or attributes can go unnoticed, exposing private networks and enabling data exfiltration (C), lateral movement and traffic tampering (I), and outages from misrouted or bridged networks (A).

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks cloudwatch_changes_to_vpcs_alarm_configured

Recommendation

Create a CloudWatch Logs metric filter and alarm on CloudTrail for critical VPC change events, and notify responders. Apply least privilege to network changes, require change approvals, and use defense in depth (segmentation, route controls) to prevent and contain unauthorized modifications.

Remediation

Terraform

# Metric filter for VPC changes
resource "aws_cloudwatch_log_metric_filter" "<example_resource_name>" {
  name           = "<example_resource_name>"
  log_group_name = "<example_log_group_name>"
  pattern        = "{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }" # Critical: matches VPC change events

  metric_transformation {
    name      = "<example_resource_name>"   # Critical: metric created by the filter
    namespace = "CISBenchmark"
    value     = "1"
  }
}

# Alarm on the VPC changes metric
resource "aws_cloudwatch_metric_alarm" "<example_resource_name>" {
  metric_name        = "<example_resource_name>"  # Critical: alarm monitors the filter's metric
  namespace          = "CISBenchmark"
  statistic          = "Sum"
  period             = 300
  evaluation_periods = 1
  threshold          = 1
  comparison_operator = "GreaterThanOrEqualToThreshold"
}

NativeIaC

# CloudFormation: Create a metric filter and alarm for VPC changes
Resources:
  VPCChangesMetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      LogGroupName: <example_log_group_name>
      FilterPattern: '{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }' # Critical: matches VPC change events
      MetricTransformations:
        - MetricName: vpc_changes_metric
          MetricNamespace: CISBenchmark
          MetricValue: "1"  # Critical: emits a metric on matching events

  VPCChangesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      MetricName: vpc_changes_metric  # Critical: alarm monitors the metric above
      Namespace: CISBenchmark
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 1
      ComparisonOperator: GreaterThanOrEqualToThreshold

Other

1In the AWS Console, go to CloudWatch > Log groups and open the CloudTrail log group
2Choose Create metric filter
3For Filter pattern, paste:
4{ ($.eventName = CreateVpc) || ($.eventName = DeleteVpc) || ($.eventName = ModifyVpcAttribute) || ($.eventName = AcceptVpcPeeringConnection) || ($.eventName = CreateVpcPeeringConnection) || ($.eventName = DeleteVpcPeeringConnection) || ($.eventName = RejectVpcPeeringConnection) || ($.eventName = AttachClassicLinkVpc) || ($.eventName = DetachClassicLinkVpc) || ($.eventName = DisableVpcClassicLink) || ($.eventName = EnableVpcClassicLink) }
5Name the filter and set Metric namespace to CISBenchmark, Metric name to vpc_changes_metric, Metric value to 1; create the filter
6Select the new filter and choose Create alarm
7Set Statistic to Sum, Period 5 minutes, Threshold type Static, Whenever Greater/Equal 1, Evaluation periods 1
8Create the alarm (actions/notifications are optional and not required for pass)

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_vpcs_alarm_configured(Check):
    """AWS account has a CloudWatch Logs metric filter and alarm for VPC changes.

    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=[
                "CreateVpc",
                "DeleteVpc",
                "ModifyVpcAttribute",
                "AcceptVpcPeeringConnection",
                "CreateVpcPeeringConnection",
                "DeleteVpcPeeringConnection",
                "RejectVpcPeeringConnection",
                "AttachClassicLinkVpc",
                "DetachClassicLinkVpc",
                "DisableVpcClassicLink",
                "EnableVpcClassicLink",
            ],
        )
        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