Repository: cloud-custodian/cloud-custodian

Issue:
Add delete action to aws.ssm-parameter resource
**Is your feature request related to a problem? Please describe.**
We would like to enforce that only secure strings are used for the parameter store so we need a delete action to do the actual enforcement as customers will just ignore emails if there is no consequence.

**Describe the solution you'd like**
A delete action - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.delete_parameter

**Describe alternatives you've considered**
We can write out own lambdas and trigger them via custodian policy but that would require managing a seperate script and process

**Additional context**
Thanks for the help!



Discussion:

Resolving patch:
diff --git a/c7n/resources/ssm.py b/c7n/resources/ssm.py
--- a/c7n/resources/ssm.py
+++ b/c7n/resources/ssm.py
@@ -47,6 +47,20 @@ class resource_type(TypeInfo):
     augment = universal_augment
 
 
+@SSMParameter.action_registry.register('delete')
+class DeleteParameter(Action):
+
+    schema = type_schema('delete')
+    permissions = ("ssm:DeleteParameter",)
+
+    def process(self, resources):
+        client = local_session(self.manager.session_factory).client('ssm')
+        for r in resources:
+            self.manager.retry(
+                client.delete_parameter, Name=r['Name'],
+                ignore_err_codes=('ParameterNotFound',))
+
+
 @resources.register('ssm-managed-instance')
 class ManagedInstance(QueryResourceManager):
 
diff --git a/c7n/utils.py b/c7n/utils.py
--- a/c7n/utils.py
+++ b/c7n/utils.py
@@ -346,7 +346,7 @@ def snapshot_identifier(prefix, db_identifier):
 retry_log = logging.getLogger('c7n.retry')
 
 
-def get_retry(codes=(), max_attempts=8, min_delay=1, log_retries=False):
+def get_retry(retry_codes=(), max_attempts=8, min_delay=1, log_retries=False):
     """Decorator for retry boto3 api call on transient errors.
 
     https://www.awsarchitectureblog.com/2015/03/backoff.html
@@ -366,13 +366,15 @@ def get_retry(codes=(), max_attempts=8, min_delay=1, log_retries=False):
     """
     max_delay = max(min_delay, 2) ** max_attempts
 
-    def _retry(func, *args, **kw):
+    def _retry(func, *args, ignore_err_codes=(), **kw):
         for idx, delay in enumerate(
                 backoff_delays(min_delay, max_delay, jitter=True)):
             try:
                 return func(*args, **kw)
             except ClientError as e:
-                if e.response['Error']['Code'] not in codes:
+                if e.response['Error']['Code'] in ignore_err_codes:
+                    return
+                elif e.response['Error']['Code'] not in retry_codes:
                     raise
                 elif idx == max_attempts - 1:
                     raise


Test patch:
diff --git a/tests/data/placebo/test_ssm_parameter_delete/ssm.DeleteParameter_1.json b/tests/data/placebo/test_ssm_parameter_delete/ssm.DeleteParameter_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ssm_parameter_delete/ssm.DeleteParameter_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ssm_parameter_delete/ssm.DescribeParameters_1.json b/tests/data/placebo/test_ssm_parameter_delete/ssm.DescribeParameters_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ssm_parameter_delete/ssm.DescribeParameters_1.json
@@ -0,0 +1,27 @@
+{
+    "status_code": 200,
+    "data": {
+        "Parameters": [
+            {
+                "Name": "not_secret",
+                "Type": "String",
+                "LastModifiedDate": {
+                    "__class__": "datetime",
+                    "year": 2020,
+                    "month": 5,
+                    "day": 29,
+                    "hour": 13,
+                    "minute": 27,
+                    "second": 6,
+                    "microsecond": 475000
+                },
+                "LastModifiedUser": "arn:aws:iam::644160558196:user/kapil",
+                "Version": 1,
+                "Tier": "Standard",
+                "Policies": [],
+                "DataType": "text"
+            }
+        ],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ssm_parameter_delete/ssm.DescribeParameters_2.json b/tests/data/placebo/test_ssm_parameter_delete/ssm.DescribeParameters_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ssm_parameter_delete/ssm.DescribeParameters_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "Parameters": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ssm_parameter_delete/tagging.GetResources_1.json b/tests/data/placebo/test_ssm_parameter_delete/tagging.GetResources_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ssm_parameter_delete/tagging.GetResources_1.json
@@ -0,0 +1,8 @@
+{
+    "status_code": 200,
+    "data": {
+        "PaginationToken": "",
+        "ResourceTagMappingList": [],
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_ssm_parameter_delete_non_existant/ssm.DeleteParameter_1.json b/tests/data/placebo/test_ssm_parameter_delete_non_existant/ssm.DeleteParameter_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_ssm_parameter_delete_non_existant/ssm.DeleteParameter_1.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 400,
+    "data": {
+        "Error": {
+            "Message": "",
+            "Code": "ParameterNotFound"
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/test_ssm.py b/tests/test_ssm.py
--- a/tests/test_ssm.py
+++ b/tests/test_ssm.py
@@ -178,6 +178,36 @@ def test_ssm_send_command(self):
             CommandId=resources[0]['c7n:SendCommand'][0])
         self.assertEqual(result['Status'], 'Success')
 
+    def test_ssm_parameter_delete(self):
+        session_factory = self.replay_flight_data("test_ssm_parameter_delete")
+        p = self.load_policy({
+            'name': 'ssm-param-tags',
+            'resource': 'ssm-parameter',
+            'actions': ['delete']},
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]['Name'], 'not_secret')
+        client = session_factory().client('ssm')
+        if self.recording:
+            time.sleep(1)
+        self.assertEqual(
+            client.describe_parameters(
+                Filters=[{'Key': 'Name', 'Values': [resources[0]['Name']]}])['Parameters'],
+            [])
+
+    def test_ssm_parameter_delete_non_existant(self):
+        session_factory = self.replay_flight_data("test_ssm_parameter_delete_non_existant")
+        p = self.load_policy({
+            'name': 'ssm-param-tags',
+            'resource': 'ssm-parameter',
+            'actions': ['delete']},
+            session_factory=session_factory)
+
+        # if it raises the test fails
+        p.resource_manager.actions[0].process(
+            [{'Name': 'unicorn'}])
+
     def test_ssm_parameter_tag_arn(self):
         session_factory = self.replay_flight_data("test_ssm_parameter_tag_arn")
         p = self.load_policy({