Repository: cloud-custodian/cloud-custodian

Issue:
Delete Action for AppSync
### Describe the feature

I am using AppSync GraphQL APIs and would like to instruct Custodian to delete resources that do not adhere to compliance standards. However, to date, there is no such action available. 

### Extra information or context
These actions are currently supported:
actions:
  - auto-tag-user
  - copy-related-tag
  - invoke-lambda
  - invoke-sfn
  - mark-for-op
  - notify
  - post-finding
  - post-item
  - put-metric
  - remove-tag
  - set-wafv2
  - tag
  - webhook


Discussion:
Could you comment to what defines compliant for app sync in your context re filters?
One scenario I'm concerned about here is the security risk associated with unauthorized access to an AppSync GraphQL API endpoint. This could be due to the use of unapproved authentication/authorization methods being used in deployed AppSync resources within an organization, where highly confidential information may be at risk. A delete action could be used to take down a non-compliant resource after filtering deployed resources by authentication type to check if it is not equal to an approved method (i.e. AWS_IAM). 

Resolving patch:
diff --git a/c7n/resources/appsync.py b/c7n/resources/appsync.py
--- a/c7n/resources/appsync.py
+++ b/c7n/resources/appsync.py
@@ -2,11 +2,12 @@
 # SPDX-License-Identifier: Apache-2.0
 import re
 
-from c7n.actions import BaseAction
+from c7n.actions import BaseAction, Action
 from c7n.filters import Filter, ValueFilter
 from c7n.manager import resources
 from c7n.query import QueryResourceManager, TypeInfo
 from c7n.utils import local_session, type_schema, get_retry
+from botocore.exceptions import ClientError
 
 
 @resources.register('graphql-api')
@@ -212,3 +213,36 @@ def process(self, resources):
             else:
                 self.retry(client.disassociate_web_acl,
                            ResourceArn=r[arn_key])
+                
+
+@GraphQLApi.action_registry.register('delete')
+class Delete(Action):
+    """Delete an AppSync GraphQL API.
+
+    :example:
+
+    .. code-block:: yaml
+
+            policies:
+              - name: appsync-delete-unlogged-api
+                resource: graphql-api
+                filters:
+                  - type: value
+                    key: logConfig
+                    value: absent
+                actions:
+                  - delete
+
+    """
+    schema = type_schema('delete')
+    permissions = ("appsync:DeleteGraphqlApi",)
+
+    def process(self, apis):
+        client = local_session(self.manager.session_factory).client('appsync')
+        for api in apis:
+            try:
+                client.delete_graphql_api(apiId=api['apiId'])
+            except ClientError as e:
+                if e.response['Error']['Code'] == "ResourceNotFoundException":
+                    continue
+                raise


Test patch:
diff --git a/tests/data/placebo/test_delete_appsync_api/appsync.DeleteGraphqlApi_1.json b/tests/data/placebo/test_delete_appsync_api/appsync.DeleteGraphqlApi_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_delete_appsync_api/appsync.DeleteGraphqlApi_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_delete_appsync_api/appsync.ListGraphqlApis_1.json b/tests/data/placebo/test_delete_appsync_api/appsync.ListGraphqlApis_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_delete_appsync_api/appsync.ListGraphqlApis_1.json
@@ -0,0 +1,20 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "graphqlApis": [
+            {
+                "name": "My AppSync App",
+                "apiId": "5n6gf6os2nbixi3mew55oklvrq",
+                "authenticationType": "API_KEY",
+                "arn": "arn:aws:appsync:us-east-1:644160558196:apis/5n6gf6os2nbixi3mew55oklvrq",
+                "uris": {
+                    "REALTIME": "wss://ijuwrzlwrbfhrgrglmi7skivnq.appsync-realtime-api.us-east-1.amazonaws.com/graphql",
+                    "GRAPHQL": "https://ijuwrzlwrbfhrgrglmi7skivnq.appsync-api.us-east-1.amazonaws.com/graphql"
+                },
+                "tags": {},
+                "xrayEnabled": false
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_delete_appsync_api/appsync.ListGraphqlApis_2.json b/tests/data/placebo/test_delete_appsync_api/appsync.ListGraphqlApis_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_delete_appsync_api/appsync.ListGraphqlApis_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "graphqlApis": []
+    }
+}
\ No newline at end of file
diff --git a/tests/test_appsync.py b/tests/test_appsync.py
--- a/tests/test_appsync.py
+++ b/tests/test_appsync.py
@@ -3,7 +3,6 @@
 
 from .common import BaseTest
 
-
 class AppSyncWafV2(BaseTest):
 
     def test_graphql_api_filter_wafv2(self):
@@ -137,3 +136,20 @@ def test_graphql_api_cache_filter(self):
 
         resources = p.run()
         self.assertEqual(len(resources), 1)
+
+    def test_delete_appsync_api(self):
+        factory = self.replay_flight_data("test_delete_appsync_api")
+        p = self.load_policy(
+            {
+                "name": "appsync-delete",
+                "resource": "graphql-api",
+                "filters": [{"name": "My AppSync App"}],
+                "actions": [{"type": "delete"}],
+            },
+            session_factory=factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]['name'], "My AppSync App")
+        client = factory().client("appsync")
+        self.assertEqual(client.list_graphql_apis()["graphqlApis"], [])