Repository: cloud-custodian/cloud-custodian

Issue:
Ingress and Egress Filters Should Support Matching a Port within a Range



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
@@ -13,7 +13,9 @@
 # limitations under the License.
 
 from c7n.actions import BaseAction
-from c7n.filters import Filter, ValueFilter, DefaultVpcBase
+from c7n.filters import (
+    DefaultVpcBase, Filter, FilterValidationError, ValueFilter)
+
 from c7n.query import QueryResourceManager
 from c7n.manager import resources
 from c7n.utils import local_session, type_schema
@@ -94,11 +96,19 @@ class SGPermission(Filter):
     """
 
     attrs = set(('IpProtocol', 'FromPort', 'ToPort', 'UserIdGroupPairs',
-                'IpRanges', 'PrefixListIds'))
+                 'IpRanges', 'PrefixListIds', 'Ports'))
+
+    def validate(self):
+        delta = set(self.data.keys()).difference(self.attrs)
+        delta.remove('type')
+        if delta:
+            raise FilterValidationError("Unknown keys %s" % ", ".join(delta))
+        return self
 
     def process(self, resources, event=None):
         self.vfilters = []
         fattrs = list(sorted(self.attrs.intersection(self.data.keys())))
+        self.ports = 'Ports' in self.data and self.data['Ports'] or ()
 
         for f in fattrs:
             fv = self.data.get(f)
@@ -114,14 +124,19 @@ def process(self, resources, event=None):
     def __call__(self, resource):
         matched = []
         for p in resource[self.ip_permissions_key]:
-            found = True
+            found = False
             for f in self.vfilters:
-                if not f(p):
-                    found = False
+                if f(p):
+                    found = True
+                    break
+            for p in self.ports:
+                if p >= resource['FromPort'] and p <= resource['ToPort']:
+                    found = True
                     break
             if not found:
                 continue
             matched.append(p)
+
         if matched:
             resource['Matched%s' % self.ip_permissions_key] = matched
             return True
@@ -135,7 +150,8 @@ class IPPermission(SGPermission):
         'type': 'object',
         #'additionalProperties': True,
         'properties': {
-            'type': {'enum': ['ingress']}
+            'type': {'enum': ['ingress']},
+            'Ports': {'type': 'array', 'items': {'type': 'integer'}}
             },
         'required': ['type']}
 


Test patch:
diff --git a/tests/data/placebo/test_security_group_port_in_range/ec2.AuthorizeSecurityGroupIngress_1.json b/tests/data/placebo/test_security_group_port_in_range/ec2.AuthorizeSecurityGroupIngress_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_security_group_port_in_range/ec2.AuthorizeSecurityGroupIngress_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200, 
+            "RequestId": "6a7b4192-ce8d-4db7-a53d-9120b9cca815", 
+            "HTTPHeaders": {
+                "transfer-encoding": "chunked", 
+                "vary": "Accept-Encoding", 
+                "server": "AmazonEC2", 
+                "content-type": "text/xml;charset=UTF-8", 
+                "date": "Sun, 07 Aug 2016 23:59:17 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_security_group_port_in_range/ec2.CreateSecurityGroup_1.json b/tests/data/placebo/test_security_group_port_in_range/ec2.CreateSecurityGroup_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_security_group_port_in_range/ec2.CreateSecurityGroup_1.json
@@ -0,0 +1,17 @@
+{
+    "status_code": 200, 
+    "data": {
+        "GroupId": "sg-46684020", 
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200, 
+            "RequestId": "5056697f-1eb0-4bda-8c17-359c623c9510", 
+            "HTTPHeaders": {
+                "transfer-encoding": "chunked", 
+                "vary": "Accept-Encoding", 
+                "server": "AmazonEC2", 
+                "content-type": "text/xml;charset=UTF-8", 
+                "date": "Sun, 07 Aug 2016 23:59:16 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_security_group_port_in_range/ec2.CreateVpc_1.json b/tests/data/placebo/test_security_group_port_in_range/ec2.CreateVpc_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_security_group_port_in_range/ec2.CreateVpc_1.json
@@ -0,0 +1,24 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200, 
+            "RequestId": "a3bc8a71-8977-43b2-b27c-01a8a1a479c9", 
+            "HTTPHeaders": {
+                "transfer-encoding": "chunked", 
+                "vary": "Accept-Encoding", 
+                "server": "AmazonEC2", 
+                "content-type": "text/xml;charset=UTF-8", 
+                "date": "Sun, 07 Aug 2016 23:59:15 GMT"
+            }
+        }, 
+        "Vpc": {
+            "VpcId": "vpc-1f451e7b", 
+            "InstanceTenancy": "default", 
+            "State": "pending", 
+            "DhcpOptionsId": "dopt-78ab4f1c", 
+            "CidrBlock": "10.4.0.0/16", 
+            "IsDefault": false
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_security_group_port_in_range/ec2.DeleteSecurityGroup_1.json b/tests/data/placebo/test_security_group_port_in_range/ec2.DeleteSecurityGroup_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_security_group_port_in_range/ec2.DeleteSecurityGroup_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200, 
+            "RequestId": "47ad8687-51a6-4ddf-8aa5-c1e358c7b982", 
+            "HTTPHeaders": {
+                "transfer-encoding": "chunked", 
+                "vary": "Accept-Encoding", 
+                "server": "AmazonEC2", 
+                "content-type": "text/xml;charset=UTF-8", 
+                "date": "Sun, 07 Aug 2016 23:59:22 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_security_group_port_in_range/ec2.DeleteVpc_1.json b/tests/data/placebo/test_security_group_port_in_range/ec2.DeleteVpc_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_security_group_port_in_range/ec2.DeleteVpc_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200, 
+    "data": {
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200, 
+            "RequestId": "3c0250da-aa04-475f-a851-269c12ade410", 
+            "HTTPHeaders": {
+                "transfer-encoding": "chunked", 
+                "vary": "Accept-Encoding", 
+                "server": "AmazonEC2", 
+                "content-type": "text/xml;charset=UTF-8", 
+                "date": "Sun, 07 Aug 2016 23:59:23 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_security_group_port_in_range/ec2.DescribeSecurityGroups_1.json b/tests/data/placebo/test_security_group_port_in_range/ec2.DescribeSecurityGroups_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_security_group_port_in_range/ec2.DescribeSecurityGroups_1.json
@@ -0,0 +1,180 @@
+{
+    "status_code": 200, 
+    "data": {
+        "SecurityGroups": [
+            {
+                "IpPermissionsEgress": [
+                    {
+                        "IpProtocol": "-1", 
+                        "IpRanges": [
+                            {
+                                "CidrIp": "0.0.0.0/0"
+                            }
+                        ], 
+                        "UserIdGroupPairs": [], 
+                        "PrefixListIds": []
+                    }
+                ], 
+                "Description": "default VPC security group", 
+                "IpPermissions": [
+                    {
+                        "IpProtocol": "-1", 
+                        "IpRanges": [], 
+                        "UserIdGroupPairs": [
+                            {
+                                "UserId": "644160558196", 
+                                "GroupId": "sg-13de8f75"
+                            }
+                        ], 
+                        "PrefixListIds": []
+                    }
+                ], 
+                "GroupName": "default", 
+                "VpcId": "vpc-dc0452b8", 
+                "OwnerId": "644160558196", 
+                "GroupId": "sg-13de8f75"
+            }, 
+            {
+                "IpPermissionsEgress": [
+                    {
+                        "IpProtocol": "-1", 
+                        "IpRanges": [
+                            {
+                                "CidrIp": "0.0.0.0/0"
+                            }
+                        ], 
+                        "UserIdGroupPairs": [], 
+                        "PrefixListIds": []
+                    }
+                ], 
+                "Description": "default VPC security group", 
+                "IpPermissions": [
+                    {
+                        "IpProtocol": "-1", 
+                        "IpRanges": [], 
+                        "UserIdGroupPairs": [
+                            {
+                                "UserId": "644160558196", 
+                                "GroupId": "sg-7968401f"
+                            }
+                        ], 
+                        "PrefixListIds": []
+                    }
+                ], 
+                "GroupName": "default", 
+                "VpcId": "vpc-1f451e7b", 
+                "OwnerId": "644160558196", 
+                "GroupId": "sg-7968401f"
+            }, 
+            {
+                "IpPermissionsEgress": [
+                    {
+                        "IpProtocol": "-1", 
+                        "IpRanges": [
+                            {
+                                "CidrIp": "0.0.0.0/0"
+                            }
+                        ], 
+                        "UserIdGroupPairs": [], 
+                        "PrefixListIds": []
+                    }
+                ], 
+                "Description": "default VPC security group", 
+                "IpPermissions": [
+                    {
+                        "IpProtocol": "-1", 
+                        "IpRanges": [], 
+                        "UserIdGroupPairs": [
+                            {
+                                "UserId": "644160558196", 
+                                "GroupId": "sg-f9cc4d9f"
+                            }
+                        ], 
+                        "PrefixListIds": []
+                    }
+                ], 
+                "GroupName": "default", 
+                "VpcId": "vpc-4a9ff72e", 
+                "OwnerId": "644160558196", 
+                "GroupId": "sg-f9cc4d9f"
+            }, 
+            {
+                "IpPermissionsEgress": [
+                    {
+                        "IpProtocol": "-1", 
+                        "IpRanges": [
+                            {
+                                "CidrIp": "0.0.0.0/0"
+                            }
+                        ], 
+                        "UserIdGroupPairs": [], 
+                        "PrefixListIds": []
+                    }
+                ], 
+                "Description": "Created from the RDS Management Console", 
+                "IpPermissions": [
+                    {
+                        "PrefixListIds": [], 
+                        "FromPort": 5432, 
+                        "IpRanges": [
+                            {
+                                "CidrIp": "108.56.194.116/32"
+                            }
+                        ], 
+                        "ToPort": 5432, 
+                        "IpProtocol": "tcp", 
+                        "UserIdGroupPairs": []
+                    }
+                ], 
+                "GroupName": "rds-launch-wizard", 
+                "VpcId": "vpc-4a9ff72e", 
+                "OwnerId": "644160558196", 
+                "GroupId": "sg-f6a42890"
+            }, 
+            {
+                "IpPermissionsEgress": [
+                    {
+                        "IpProtocol": "-1", 
+                        "IpRanges": [
+                            {
+                                "CidrIp": "0.0.0.0/0"
+                            }
+                        ], 
+                        "UserIdGroupPairs": [], 
+                        "PrefixListIds": []
+                    }
+                ], 
+                "Description": "for apps", 
+                "IpPermissions": [
+                    {
+                        "PrefixListIds": [], 
+                        "FromPort": 60000, 
+                        "IpRanges": [
+                            {
+                                "CidrIp": "10.2.0.0/16"
+                            }
+                        ], 
+                        "ToPort": 62000, 
+                        "IpProtocol": "tcp", 
+                        "UserIdGroupPairs": []
+                    }
+                ], 
+                "GroupName": "web-tier", 
+                "VpcId": "vpc-1f451e7b", 
+                "OwnerId": "644160558196", 
+                "GroupId": "sg-46684020"
+            }
+        ], 
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200, 
+            "RequestId": "36d8b296-e49d-4149-a6ba-212a6927ab23", 
+            "HTTPHeaders": {
+                "transfer-encoding": "chunked", 
+                "vary": "Accept-Encoding", 
+                "server": "AmazonEC2", 
+                "content-type": "text/xml;charset=UTF-8", 
+                "date": "Sun, 07 Aug 2016 23:59:21 GMT"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_vpc.py b/tests/test_vpc.py
--- a/tests/test_vpc.py
+++ b/tests/test_vpc.py
@@ -1,6 +1,3 @@
-import time
-
-
 from common import BaseTest
 
 
@@ -64,6 +61,35 @@ def test_interface_subnet(self):
 
 class SecurityGroupTest(BaseTest):
 
+    def test_port_within_range(self):
+        factory = self.replay_flight_data(
+            'test_security_group_port_in_range')
+        client = factory().client('ec2')
+        vpc_id = client.create_vpc(CidrBlock="10.4.0.0/16")['Vpc']['VpcId']
+        self.addCleanup(client.delete_vpc, VpcId=vpc_id)
+        sg_id = client.create_security_group(
+            GroupName="web-tier",
+            VpcId=vpc_id,
+            Description="for apps")['GroupId']
+        self.addCleanup(client.delete_security_group, GroupId=sg_id)
+        client.authorize_security_group_ingress(
+            GroupId=sg_id,
+            IpProtocol='tcp',
+            FromPort=60000,
+            ToPort=62000,
+            CidrIp='10.2.0.0/16')
+        p = self.load_policy({
+            'name': 'sg-find',
+            'resource': 'security-group',
+            'filters': [
+                {'type': 'ingress',
+                 'IpProtocol': 'tcp',
+                 'FromPort': 60000},
+                {'GroupName': 'web-tier'}]
+            }, session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
     def test_ingress_remove(self):
         factory = self.replay_flight_data(
             'test_security_group_ingress_filter')
@@ -85,6 +111,7 @@ def test_ingress_remove(self):
             'name': 'sg-find',
             'resource': 'security-group',
             'filters': [
+                {'VpcId': vpc_id},
                 {'type': 'ingress',
                  'IpProtocol': 'tcp',
                  'FromPort': 0},