Repository: cloud-custodian/cloud-custodian

Issue:
aws - cloudwatch alarms add support to filter out alarms with a parent composite alarm
### Describe the feature

Add support for CWA composite alarms

### Extra information or context

_No response_


Discussion:

Resolving patch:
diff --git a/c7n/resources/cw.py b/c7n/resources/cw.py
--- a/c7n/resources/cw.py
+++ b/c7n/resources/cw.py
@@ -22,8 +22,9 @@
 from c7n.resources import load_resources
 from c7n.resources.aws import ArnResolver
 from c7n.tags import universal_augment
-from c7n.utils import type_schema, local_session, chunks, get_retry
+from c7n.utils import type_schema, local_session, chunks, get_retry, jmespath_search
 from botocore.config import Config
+import re
 
 
 class DescribeAlarm(DescribeSource):
@@ -89,6 +90,36 @@ def process(self, resources):
                 AlarmNames=[r['AlarmName'] for r in resource_set])
 
 
+@Alarm.filter_registry.register('is-composite-child')
+class IsCompositeChild(Filter):
+    schema = type_schema('is-composite-child', state={"type": "boolean"})
+    permissions = ('cloudwatch:DescribeAlarms',)
+
+    def process(self, resources, event=None):
+        state = self.data.get("state", True)
+        # Get the composite alarms since filtered out in enum_spec
+        composite_alarms = self.manager.get_resource_manager("composite-alarm").resources()
+        composite_alarm_rules = jmespath_search('[].AlarmRule', composite_alarms)
+
+        child_alarm_names = set()
+        # Loop through, find child alarm names
+        for rule in composite_alarm_rules:
+            names = self.extract_alarm_names_from_rule(rule)
+            child_alarm_names.update(names)
+
+        if state:
+            # If we want to filter out alarms that are a child of a composite alarm
+            return [r for r in resources if r['AlarmName'] in child_alarm_names]
+
+        return [r for r in resources if r['AlarmName'] not in child_alarm_names]
+
+
+    def extract_alarm_names_from_rule(self, rule):
+        # Check alarm references (OK/ALARM/INSUFFICIENT_DATA)
+        pattern = r"\b(?:ALARM|OK|INSUFFICIENT_DATA)\s*\(\s*([^\)]+)\s*\)"
+        matches = re.findall(pattern, rule)
+        return set(matches)
+
 @resources.register('composite-alarm')
 class CompositeAlarm(QueryResourceManager):
 


Test patch:
diff --git a/tests/data/placebo/test_is_composite_child_filter/monitoring.DescribeAlarms_1.json b/tests/data/placebo/test_is_composite_child_filter/monitoring.DescribeAlarms_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_is_composite_child_filter/monitoring.DescribeAlarms_1.json
@@ -0,0 +1,58 @@
+{
+    "status_code": 200,
+    "data": {
+        "CompositeAlarms": [],
+        "MetricAlarms": [
+            {
+                "AlarmName": "c7n-test-alarm-tags-filter",
+                "AlarmArn": "arn:aws:cloudwatch:us-east-1:644160558196:alarm:c7n-test-alarm-tags-filter",
+                "AlarmConfigurationUpdatedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 3,
+                    "day": 24,
+                    "hour": 18,
+                    "minute": 1,
+                    "second": 32,
+                    "microsecond": 583000
+                },
+                "ActionsEnabled": true,
+                "OKActions": [],
+                "AlarmActions": [],
+                "InsufficientDataActions": [],
+                "StateValue": "INSUFFICIENT_DATA",
+                "StateReason": "Insufficient Data: 5 datapoints were unknown.",
+                "StateReasonData": "{\"version\":\"1.0\",\"queryDate\":\"2022-09-27T10:05:12.193+0000\",\"statistic\":\"Average\",\"period\":3600,\"recentDatapoints\":[],\"threshold\":10.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2022-09-27T09:05:00.000+0000\"},{\"timestamp\":\"2022-09-27T08:05:00.000+0000\"},{\"timestamp\":\"2022-09-27T07:05:00.000+0000\"},{\"timestamp\":\"2022-09-27T06:05:00.000+0000\"},{\"timestamp\":\"2022-09-27T05:05:00.000+0000\"}]}",
+                "StateUpdatedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2022,
+                    "month": 9,
+                    "day": 27,
+                    "hour": 10,
+                    "minute": 5,
+                    "second": 12,
+                    "microsecond": 198000
+                },
+                "MetricName": "CPUUtilization",
+                "Namespace": "AWS/EC2",
+                "Statistic": "Average",
+                "Dimensions": [],
+                "Period": 3600,
+                "EvaluationPeriods": 5,
+                "Threshold": 10.0,
+                "ComparisonOperator": "GreaterThanThreshold",
+                "StateTransitionedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2022,
+                    "month": 9,
+                    "day": 27,
+                    "hour": 10,
+                    "minute": 5,
+                    "second": 12,
+                    "microsecond": 198000
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_is_composite_child_filter/monitoring.DescribeAlarms_2.json b/tests/data/placebo/test_is_composite_child_filter/monitoring.DescribeAlarms_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_is_composite_child_filter/monitoring.DescribeAlarms_2.json
@@ -0,0 +1,58 @@
+{
+    "status_code": 200,
+    "data": {
+        "CompositeAlarms": [
+            {
+                "ActionsEnabled": true,
+                "AlarmActions": [
+                    "arn:aws:sns:us-east-1:644160558196:test-xyz"
+                ],
+                "AlarmArn": "arn:aws:cloudwatch:us-east-1:644160558196:alarm:ExampleCompositeAlarm",
+                "AlarmConfigurationUpdatedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2024,
+                    "month": 2,
+                    "day": 16,
+                    "hour": 16,
+                    "minute": 32,
+                    "second": 54,
+                    "microsecond": 256000
+                },
+                "AlarmDescription": "Composite alarm based on c7n-test-alarm-tags-filter",
+                "AlarmName": "ExampleCompositeAlarm",
+                "AlarmRule": "ALARM(c7n-test-alarm-tags-filter)",
+                "InsufficientDataActions": [
+                    "arn:aws:sns:us-east-1:644160558196:test-zyx"
+                ],
+                "OKActions": [
+                    "arn:aws:sns:us-east-1:644160558196:test-xyz"
+                ],
+                "StateReason": "arn:aws:cloudwatch:us-east-1:644160558196:alarm:ExampleCompositeAlarm was created and its alarm rule evaluates to OK",
+                "StateReasonData": "{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:644160558196:alarm:c7n-test-alarm-tags-filter\",\"state\":{\"value\":\"INSUFFICIENT_DATA\",\"timestamp\":\"2022-09-27T10:05:12.198+0000\"}}]}",
+                "StateUpdatedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2024,
+                    "month": 2,
+                    "day": 16,
+                    "hour": 16,
+                    "minute": 32,
+                    "second": 54,
+                    "microsecond": 256000
+                },
+                "StateValue": "OK",
+                "StateTransitionedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2024,
+                    "month": 2,
+                    "day": 16,
+                    "hour": 16,
+                    "minute": 32,
+                    "second": 54,
+                    "microsecond": 256000
+                }
+            }
+        ],
+        "MetricAlarms": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_is_composite_child_filter/tagging.GetResources_1.json b/tests/data/placebo/test_is_composite_child_filter/tagging.GetResources_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_is_composite_child_filter/tagging.GetResources_1.json
@@ -0,0 +1,26 @@
+{
+    "status_code": 200,
+    "data": {
+        "PaginationToken": "",
+        "ResourceTagMappingList": [
+            {
+                "ResourceARN": "arn:aws:cloudwatch:us-east-1:644160558196:alarm:c7n-test-alarm-tags-filter",
+                "Tags": [
+                    {
+                        "Key": "some-tag",
+                        "Value": "some-value"
+                    },
+                    {
+                        "Key": "OwnerName",
+                        "Value": "SomeName"
+                    },
+                    {
+                        "Key": "pratyush",
+                        "Value": "test"
+                    }
+                ]
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_is_composite_child_filter/tagging.GetResources_2.json b/tests/data/placebo/test_is_composite_child_filter/tagging.GetResources_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_is_composite_child_filter/tagging.GetResources_2.json
@@ -0,0 +1,13 @@
+{
+    "status_code": 200,
+    "data": {
+        "PaginationToken": "",
+        "ResourceTagMappingList": [
+            {
+                "ResourceARN": "arn:aws:cloudwatch:us-east-1:644160558196:alarm:ExampleCompositeAlarm",
+                "Tags": []
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_is_not_composite_child_filter/monitoring.DescribeAlarms_1.json b/tests/data/placebo/test_is_not_composite_child_filter/monitoring.DescribeAlarms_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_is_not_composite_child_filter/monitoring.DescribeAlarms_1.json
@@ -0,0 +1,58 @@
+{
+    "status_code": 200,
+    "data": {
+        "CompositeAlarms": [],
+        "MetricAlarms": [
+            {
+                "AlarmName": "c7n-test-alarm-tags-filter",
+                "AlarmArn": "arn:aws:cloudwatch:us-east-1:644160558196:alarm:c7n-test-alarm-tags-filter",
+                "AlarmConfigurationUpdatedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2021,
+                    "month": 3,
+                    "day": 24,
+                    "hour": 18,
+                    "minute": 1,
+                    "second": 32,
+                    "microsecond": 583000
+                },
+                "ActionsEnabled": true,
+                "OKActions": [],
+                "AlarmActions": [],
+                "InsufficientDataActions": [],
+                "StateValue": "INSUFFICIENT_DATA",
+                "StateReason": "Insufficient Data: 5 datapoints were unknown.",
+                "StateReasonData": "{\"version\":\"1.0\",\"queryDate\":\"2022-09-27T10:05:12.193+0000\",\"statistic\":\"Average\",\"period\":3600,\"recentDatapoints\":[],\"threshold\":10.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2022-09-27T09:05:00.000+0000\"},{\"timestamp\":\"2022-09-27T08:05:00.000+0000\"},{\"timestamp\":\"2022-09-27T07:05:00.000+0000\"},{\"timestamp\":\"2022-09-27T06:05:00.000+0000\"},{\"timestamp\":\"2022-09-27T05:05:00.000+0000\"}]}",
+                "StateUpdatedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2022,
+                    "month": 9,
+                    "day": 27,
+                    "hour": 10,
+                    "minute": 5,
+                    "second": 12,
+                    "microsecond": 198000
+                },
+                "MetricName": "CPUUtilization",
+                "Namespace": "AWS/EC2",
+                "Statistic": "Average",
+                "Dimensions": [],
+                "Period": 3600,
+                "EvaluationPeriods": 5,
+                "Threshold": 10.0,
+                "ComparisonOperator": "GreaterThanThreshold",
+                "StateTransitionedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2022,
+                    "month": 9,
+                    "day": 27,
+                    "hour": 10,
+                    "minute": 5,
+                    "second": 12,
+                    "microsecond": 198000
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_is_not_composite_child_filter/monitoring.DescribeAlarms_2.json b/tests/data/placebo/test_is_not_composite_child_filter/monitoring.DescribeAlarms_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_is_not_composite_child_filter/monitoring.DescribeAlarms_2.json
@@ -0,0 +1,58 @@
+{
+    "status_code": 200,
+    "data": {
+        "CompositeAlarms": [
+            {
+                "ActionsEnabled": true,
+                "AlarmActions": [
+                    "arn:aws:sns:us-east-1:644160558196:test-xyz"
+                ],
+                "AlarmArn": "arn:aws:cloudwatch:us-east-1:644160558196:alarm:ExampleCompositeAlarm",
+                "AlarmConfigurationUpdatedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2024,
+                    "month": 2,
+                    "day": 16,
+                    "hour": 16,
+                    "minute": 32,
+                    "second": 54,
+                    "microsecond": 256000
+                },
+                "AlarmDescription": "Composite alarm based on c7n-test-alarm-tags-filter",
+                "AlarmName": "ExampleCompositeAlarm",
+                "AlarmRule": "ALARM(c7n-test-alarm-tags-filter)",
+                "InsufficientDataActions": [
+                    "arn:aws:sns:us-east-1:644160558196:test-zyx"
+                ],
+                "OKActions": [
+                    "arn:aws:sns:us-east-1:644160558196:test-xyz"
+                ],
+                "StateReason": "arn:aws:cloudwatch:us-east-1:644160558196:alarm:ExampleCompositeAlarm was created and its alarm rule evaluates to OK",
+                "StateReasonData": "{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:644160558196:alarm:c7n-test-alarm-tags-filter\",\"state\":{\"value\":\"INSUFFICIENT_DATA\",\"timestamp\":\"2022-09-27T10:05:12.198+0000\"}}]}",
+                "StateUpdatedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2024,
+                    "month": 2,
+                    "day": 16,
+                    "hour": 16,
+                    "minute": 32,
+                    "second": 54,
+                    "microsecond": 256000
+                },
+                "StateValue": "OK",
+                "StateTransitionedTimestamp": {
+                    "__class__": "datetime",
+                    "year": 2024,
+                    "month": 2,
+                    "day": 16,
+                    "hour": 16,
+                    "minute": 32,
+                    "second": 54,
+                    "microsecond": 256000
+                }
+            }
+        ],
+        "MetricAlarms": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_is_not_composite_child_filter/tagging.GetResources_1.json b/tests/data/placebo/test_is_not_composite_child_filter/tagging.GetResources_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_is_not_composite_child_filter/tagging.GetResources_1.json
@@ -0,0 +1,26 @@
+{
+    "status_code": 200,
+    "data": {
+        "PaginationToken": "",
+        "ResourceTagMappingList": [
+            {
+                "ResourceARN": "arn:aws:cloudwatch:us-east-1:644160558196:alarm:c7n-test-alarm-tags-filter",
+                "Tags": [
+                    {
+                        "Key": "some-tag",
+                        "Value": "some-value"
+                    },
+                    {
+                        "Key": "OwnerName",
+                        "Value": "SomeName"
+                    },
+                    {
+                        "Key": "pratyush",
+                        "Value": "test"
+                    }
+                ]
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_is_not_composite_child_filter/tagging.GetResources_2.json b/tests/data/placebo/test_is_not_composite_child_filter/tagging.GetResources_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_is_not_composite_child_filter/tagging.GetResources_2.json
@@ -0,0 +1,13 @@
+{
+    "status_code": 200,
+    "data": {
+        "PaginationToken": "",
+        "ResourceTagMappingList": [
+            {
+                "ResourceARN": "arn:aws:cloudwatch:us-east-1:644160558196:alarm:ExampleCompositeAlarm",
+                "Tags": []
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_cwa.py b/tests/test_cwa.py
--- a/tests/test_cwa.py
+++ b/tests/test_cwa.py
@@ -77,6 +77,50 @@ def test_add_alarm_tags(self):
         self.assertEqual(len(resources), 1)
         self.assertTrue({'Key': 'OwnerName', 'Value': 'SomeName'} in resources[0].get('Tags'))
 
+    def test_is_not_composite_child_filter(self):
+        factory = self.replay_flight_data("test_is_not_composite_child_filter")
+        p = self.load_policy(
+            {
+                "name": "is-composite-child",
+                "resource": "aws.alarm",
+                "filters": [
+                    {
+                        'type': 'is-composite-child',
+                        'state': False,
+                    }
+                ],
+            },
+            session_factory=factory,
+        )
+
+        resources = p.run()
+        self.assertEqual(len(resources), 0)
+
+        for alarm in resources:
+            self.assertNotIn(alarm['AlarmName'], "c7n-test-alarm-tags-filter")
+
+    def test_is_composite_child_filter(self):
+        factory = self.replay_flight_data("test_is_composite_child_filter")
+        p = self.load_policy(
+            {
+                "name": "is-composite-child",
+                "resource": "aws.alarm",
+                "filters": [
+                    {
+                        'type': 'is-composite-child',
+                        'state': True,
+                    }
+                ],
+            },
+            session_factory=factory,
+        )
+
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
+        for alarm in resources:
+            self.assertIn(alarm['AlarmName'], "c7n-test-alarm-tags-filter")
+
 
 class CompositeAlarmTest(BaseTest):