Repository: cloud-custodian/cloud-custodian

Issue:
AWS - Add SNS Subscription Resource
**Is your feature request related to a problem? Please describe.**
I want to be able to filter for all SNS Subscriptions, as well as retrieving their attributes.

**Describe the solution you'd like**
I'd like to create a new resource ```subscription```, which would use (https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sns.html#SNS.Client.list_subscriptions) to find all the subscriptions.

**Describe alternatives you've considered**
There are two main alternatives. One is just to implement ListSubscriptions and GetSubscriptionAttributes as sns resource actions. The other is to make ```subscription``` a child resource of an sns topic, so that we would replace using ```list_subscriptions``` with ```list_subscriptions_by_topic``` (https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sns.html#SNS.Client.list_subscriptions_by_topic). I think this approach isn't as flexible though, since we can always filter sns subscriptions by their topic ARN anyway. 

**Additional context**
N/A



Discussion:

Resolving patch:
diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py
--- a/c7n/resources/resource_map.py
+++ b/c7n/resources/resource_map.py
@@ -154,6 +154,7 @@
     "aws.snowball": "c7n.resources.snowball.Snowball",
     "aws.snowball-cluster": "c7n.resources.snowball.SnowballCluster",
     "aws.sns": "c7n.resources.sns.SNS",
+    "aws.sns-subscription": "c7n.resources.sns.SNSSubscription",
     "aws.sqs": "c7n.resources.sqs.SQS",
     "aws.ssm-activation": "c7n.resources.ssm.SSMActivation",
     "aws.ssm-managed-instance": "c7n.resources.ssm.ManagedInstance",
diff --git a/c7n/resources/sns.py b/c7n/resources/sns.py
--- a/c7n/resources/sns.py
+++ b/c7n/resources/sns.py
@@ -468,3 +468,60 @@ def process(self, resources):
                 client.delete_topic(TopicArn=r['TopicArn'])
             except client.exceptions.NotFoundException:
                 continue
+
+
+@resources.register('sns-subscription')
+class SNSSubscription(QueryResourceManager):
+
+    class resource_type(TypeInfo):
+        service = 'sns'
+        enum_spec = ('list_subscriptions', 'Subscriptions', None)
+        id = name = dimension = 'SubscriptionArn'
+        arn = 'SubscriptionArn'
+        cfn_type = 'AWS::SNS::Subscription'
+        default_report_fields = (
+            'SubscriptionArn',
+            'Owner',
+            'Protocol',
+            'Endpoint',
+            'TopicArn'
+        )
+
+
+@SNSSubscription.action_registry.register('delete')
+class SubscriptionDeleteAction(BaseAction):
+    """
+    Action to delete a subscription
+
+    :example:
+
+    .. code-block:: yaml
+
+        policies:
+          - name: external-owner-delete
+            resource: sns-subscription
+            filters:
+              - type: value
+                key: "Owner"
+                value: "{account_id}"
+                op: ne
+            actions:
+              - type: delete
+    """
+
+    schema = type_schema('delete')
+    permissions = ("sns:Unsubscribe",)
+
+    def process(self, subscriptions):
+        client = local_session(
+            self.manager.session_factory).client(self.manager.get_model().service)
+
+        for s in subscriptions:
+            self.process_subscription(client, s)
+
+    def process_subscription(self, client, subscription):
+        # Can't delete a pending subscription
+        if subscription['SubscriptionArn'] != 'PendingConfirmation':
+            self.manager.retry(
+                client.unsubscribe, SubscriptionArn=subscription['SubscriptionArn'],
+                ignore_err_codes=('NotFoundException',))


Test patch:
diff --git a/tests/data/placebo/test_subscription_delete/sns.ListSubscriptions_1.json b/tests/data/placebo/test_subscription_delete/sns.ListSubscriptions_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_subscription_delete/sns.ListSubscriptions_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "Subscriptions": [
+            {
+                "SubscriptionArn": "arn:aws:sns:us-east-1:123456789012:test:a72e3629-67d2-4f9f-bf54-f8cecf38a114",
+                "Owner": "123456789012",
+                "Protocol": "email",
+                "Endpoint": "test@test.com",
+                "TopicArn": "arn:aws:sns:us-east-1:123456789012:test"
+            }
+        ],
+        "NextToken": "AAHzN8s/tt6VglGOMISokVWAgS8MkssMztzRcuigQF4AIg==",
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_subscription_delete/sns.ListSubscriptions_2.json b/tests/data/placebo/test_subscription_delete/sns.ListSubscriptions_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_subscription_delete/sns.ListSubscriptions_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Subscriptions": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_subscription_delete/sns.ListSubscriptions_3.json b/tests/data/placebo/test_subscription_delete/sns.ListSubscriptions_3.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_subscription_delete/sns.ListSubscriptions_3.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Subscriptions": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_subscription_delete/sns.Unsubscribe_1.json b/tests/data/placebo/test_subscription_delete/sns.Unsubscribe_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_subscription_delete/sns.Unsubscribe_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_sns.py b/tests/test_sns.py
--- a/tests/test_sns.py
+++ b/tests/test_sns.py
@@ -709,3 +709,33 @@ def test_sns_post_finding(self):
         shape_validate(
             rfinding['Details']['AwsSnsTopic'],
             'AwsSnsTopicDetails', 'securityhub')
+
+
+class TestSubscription(BaseTest):
+
+    def test_subscription_delete(self):
+        factory = self.replay_flight_data("test_subscription_delete")
+
+        p = self.load_policy(
+            {
+                "name": "external-owner-delete",
+                "resource": "sns-subscription",
+                "filters": [
+                    {
+                        "type": "value",
+                        "key": "Owner",
+                        "value": "123456789099",
+                        "op": "ne",
+                    }
+                ],
+                "actions": [{"type": "delete"}],
+            },
+            session_factory=factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertNotEqual(resources[0]["Owner"], "123456789099")
+        client = factory().client("sns")
+        subs = client.list_subscriptions()
+        for s in subs.get("Subscriptions", []):
+            self.assertTrue("123456789099" == s.get("Owner"))