Repository: cloud-custodian/cloud-custodian

Issue:
aws - VPC Security Group filters "used" / "unused" and ECS Scheduled Tasks
I just found that those filters were not taking into account the ECS Scheduled Tasks (which do have a Security Group info).

I'm preparing a Pull Request with a proposed workaround.


Discussion:

Resolving patch:
diff --git a/c7n/resources/vpc.py b/c7n/resources/vpc.py
--- a/c7n/resources/vpc.py
+++ b/c7n/resources/vpc.py
@@ -688,7 +688,7 @@ def get_permissions(self):
         return list(itertools.chain(
             *[self.manager.get_resource_manager(m).get_permissions()
              for m in
-             ['lambda', 'eni', 'launch-config', 'security-group']]))
+             ['lambda', 'eni', 'launch-config', 'security-group', 'event-rule-target']]))
 
     def filter_peered_refs(self, resources):
         if not resources:
@@ -705,14 +705,18 @@ def filter_peered_refs(self, resources):
             "%d of %d groups w/ peered refs", len(peered_ids), len(resources))
         return [r for r in resources if r['GroupId'] not in peered_ids]
 
+    def get_scanners(self):
+        return (
+            ("nics", self.get_eni_sgs),
+            ("sg-perm-refs", self.get_sg_refs),
+            ('lambdas', self.get_lambda_sgs),
+            ("launch-configs", self.get_launch_config_sgs),
+            ("ecs-cwe", self.get_ecs_cwe_sgs)
+        )
+
     def scan_groups(self):
         used = set()
-        for kind, scanner in (
-                ("nics", self.get_eni_sgs),
-                ("sg-perm-refs", self.get_sg_refs),
-                ('lambdas', self.get_lambda_sgs),
-                ("launch-configs", self.get_launch_config_sgs),
-        ):
+        for kind, scanner in self.get_scanners():
             sg_ids = scanner()
             new_refs = sg_ids.difference(used)
             used = used.union(sg_ids)
@@ -735,7 +739,7 @@ def get_launch_config_sgs(self):
 
     def get_lambda_sgs(self):
         sg_ids = set()
-        for func in self.manager.get_resource_manager('lambda').resources():
+        for func in self.manager.get_resource_manager('lambda').resources(augment=False):
             if 'VpcConfig' not in func:
                 continue
             for g in func['VpcConfig']['SecurityGroupIds']:
@@ -758,6 +762,16 @@ def get_sg_refs(self):
                         sg_ids.add(g['GroupId'])
         return sg_ids
 
+    def get_ecs_cwe_sgs(self):
+        sg_ids = set()
+        expr = jmespath.compile(
+            'EcsParameters.NetworkConfiguration.awsvpcConfiguration.SecurityGroups[]')
+        for rule in self.manager.get_resource_manager('event-rule-target').resources():
+            ids = expr.search(rule)
+            if ids:
+                sg_ids.update(ids)
+        return sg_ids
+
 
 @SecurityGroup.filter_registry.register('unused')
 class UnusedSecurityGroup(SGUsage):
@@ -768,7 +782,10 @@ class UnusedSecurityGroup(SGUsage):
     lambdas as they may not have extant resources in the vpc at a
     given moment. We also find any security group with references from
     other security group either within the vpc or across peered
-    connections.
+    connections. Also checks cloud watch event targeting ecs.
+
+    Checks - enis, lambda, launch-configs, sg rule refs, and ecs cwe
+    targets.
 
     Note this filter does not support classic security groups atm.
 
@@ -781,6 +798,7 @@ class UnusedSecurityGroup(SGUsage):
                 resource: security-group
                 filters:
                   - unused
+
     """
     schema = type_schema('unused')
 


Test patch:
diff --git a/tests/data/placebo/test_security_group_ecs_unused/config.SelectResourceConfig_1.json b/tests/data/placebo/test_security_group_ecs_unused/config.SelectResourceConfig_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_security_group_ecs_unused/config.SelectResourceConfig_1.json
@@ -0,0 +1,19 @@
+{
+    "status_code": 200,
+    "data": {
+        "Results": [
+            "{\"configuration\":{\"groupName\":\"apichanges-task\",\"groupId\":\"sg-0f026884bba48e350\",\"vpcId\":\"vpc-39831640\",\"ipPermissionsEgress\":[{\"ipRanges\":[\"0.0.0.0/0\"],\"prefixListIds\":[],\"userIdGroupPairs\":[],\"ipProtocol\":\"-1\",\"ipv4Ranges\":[{\"description\":\"Allows task to establish connections to all resources\",\"cidrIp\":\"0.0.0.0/0\"}],\"ipv6Ranges\":[]}],\"description\":\"Limit connections from internal resources while allowing apichanges-task to connect to all external resources\",\"ownerId\":\"644160558196\",\"ipPermissions\":[],\"tags\":[{\"value\":\"AWSAPIChanges\",\"key\":\"App\"}]},\"supplementaryConfiguration\":{}}"
+        ],
+        "QueryInfo": {
+            "SelectFields": [
+                {
+                    "Name": "configuration"
+                },
+                {
+                    "Name": "supplementaryConfiguration"
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_security_group_ecs_unused/events.ListRules_1.json b/tests/data/placebo/test_security_group_ecs_unused/events.ListRules_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_security_group_ecs_unused/events.ListRules_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "Rules": [
+            {
+                "Name": "apichanges-build",
+                "Arn": "arn:aws:events:us-east-1:644160558196:rule/apichanges-build",
+                "State": "ENABLED",
+                "Description": "Runs fargate task apichanges: rate(6 hours)",
+                "ScheduleExpression": "rate(6 hours)",
+                "EventBusName": "default"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_security_group_ecs_unused/events.ListTargetsByRule_1.json b/tests/data/placebo/test_security_group_ecs_unused/events.ListTargetsByRule_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_security_group_ecs_unused/events.ListTargetsByRule_1.json
@@ -0,0 +1,31 @@
+{
+    "status_code": 200,
+    "data": {
+        "Targets": [
+            {
+                "Id": "apichanges-build-target",
+                "Arn": "arn:aws:ecs:us-east-1:644160558196:cluster/apichanges",
+                "RoleArn": "arn:aws:iam::644160558196:role/apichanges-events",
+                "Input": "{}",
+                "EcsParameters": {
+                    "TaskDefinitionArn": "arn:aws:ecs:us-east-1:644160558196:task-definition/apichanges:6",
+                    "TaskCount": 1,
+                    "LaunchType": "FARGATE",
+                    "NetworkConfiguration": {
+                        "awsvpcConfiguration": {
+                            "Subnets": [
+                                "subnet-8e261cb2"
+                            ],
+                            "SecurityGroups": [
+                                "sg-0f026884bba48e350"
+                            ],
+                            "AssignPublicIp": "ENABLED"
+                        }
+                    },
+                    "PlatformVersion": "LATEST"
+                }
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_security_group_unused/events.ListRules_1.json b/tests/data/placebo/test_security_group_unused/events.ListRules_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_security_group_unused/events.ListRules_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Rules": [],
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_security_group_used/events.ListRules_1.json b/tests/data/placebo/test_security_group_used/events.ListRules_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_security_group_used/events.ListRules_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Rules": [],
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/test_vpc.py b/tests/test_vpc.py
--- a/tests/test_vpc.py
+++ b/tests/test_vpc.py
@@ -1125,6 +1125,24 @@ def test_used(self):
             set([r["GroupId"] for r in resources]),
         )
 
+    def test_unused_ecs(self):
+        factory = self.replay_flight_data("test_security_group_ecs_unused")
+        p = self.load_policy(
+            {'name': 'sg-xyz',
+             'source': 'config',
+             'query': [
+                 {'clause': "resourceId ='sg-0f026884bba48e350'"}],
+             'resource': 'security-group',
+             'filters': ['unused']},
+            session_factory=factory)
+        unused = p.resource_manager.filters[0]
+        self.patch(
+            unused,
+            'get_scanners',
+            lambda: (('ecs-cwe', unused.get_ecs_cwe_sgs),))
+        resources = p.run()
+        assert resources == []
+
     def test_unused(self):
         factory = self.replay_flight_data("test_security_group_unused")
         p = self.load_policy(