Repository: cloud-custodian/cloud-custodian

Issue:
AWS.EC2 - SSM Compliance Support via Config-Filter
**Description**
Currently, you cannot leverage config-filter to query SSM Patch Compliance or SSM Association Compliance.  This is because Custodian queries config for `AWS::EC2::Instance` type which does not expose SSM compliance status. The two types for querying SSM Compliance status of an EC2 are `AWS::Config::ResourceCompliance` and `AWS::SSM::ManagedInstanceInventory`.

Amazon pre-packages two great config rules:
  * [Patch Compliance](https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-patch-compliance-status-check.html)
  * [Association Compliance](https://docs.aws.amazon.com/config/latest/developerguide/ec2-managedinstance-association-compliance-status-check.html)

The objective is to filter EC2 instances which are not compliant with these config rules and take appropriate action (notify resource owner, tag for remediation, etc.)

Ultimately, I am looking to push these two via Organization Config in addition to EC2_INSTANCE_MANAGED_BY_SSM.  By implementing all three of these together, we can effectively evaluate and enforce policy compliance on the EC2 fleet at the organization level, feed this information to Security Hub, and hook into Config & SecHub for remediation via Custodian.

**Solution**
A. Enhance config-filter to support overriding the resource type to query
B. Add a new filter type (maybe a superclass of config-filter) specific to EC2 to achieve the same.

**Alternatives**
If I am not able to do this, I will write a lambda which will tag instances with their compliance status whenever the SSM Patch Compliance or SSM Association Compliance status changes.


**Thoughts**

https://github.com/cloud-custodian/cloud-custodian/blob/cc7a6dc2927ea94e71c313627cab24284bb31b16/c7n/resources/ec2.py#L66


Maybe we can query Config for a specific instance via resource ID or if gathering an array in the return, check for where resourceId ends with the instance-id instead of resourceId == instance-id

#### Example Resource ID:
```
AWS::SSM::PatchCompliance/AWS::SSM::ManagedInstanceInventory/i-0050852ea15a9ea0f
```

#### Example Query:
```
SELECT
  configuration,
  *
WHERE
  resourceType = 'AWS::Config::ResourceCompliance'
  AND configuration.targetResourceType = 'AWS::SSM::PatchCompliance'
  AND configuration.complianceType = 'NON_COMPLIANT'

# Can also filter for an instance id
AND resourceId LIKE 'AWS::SSM::PatchCompliance%i-0012345ab23a2da0f'
```

Discussion:
kapilt: thanks for the pr, this looks like it was based on the release instead of trunk though (re merge conflicts).
bdwyertech: Yeh I noticed that... I'll try to get this going on master.

On Thu, Mar 19, 2020, 6:33 PM Kapil Thangavelu <notifications@github.com>
wrote:

> thanks for the pr, this looks like it was based on the release instead of
> trunk though (re merge conflicts).
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/cloud-custodian/cloud-custodian/pull/5472#issuecomment-601447953>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAWV4WNM2OLAKA5EEYADLQDRIKMUJANCNFSM4LPVTHJA>
> .
>

bdwyertech: @kapilt any insight on why the documentation CI job is failing?  Can't figure it out.
bdwyertech: Nevermind... Figured it out @kapilt 
bdwyertech: Any love @kapilt I have merged master a few times now lmk and I will stop 😂
kapilt: @bdwyertech hey sorry for the delay, adjusting to new schedules post cancelling of academic year in my state. on cursory glance its a little different but seems fine/lgtm, the only question is for non value filter derived if the data in the response is worth considering.
bdwyertech: All good! @kapilt not sure what the use case would be there wrt non-value filter; could you elaborate on scenario?
kapilt: mostly wrt to the evaluation date, ie want to consider that as a part of a policy... tbh. in this context its purely additive as a future addition if its relevant, so i'm fine as is, but.. if you see a reasonable use case for it ootb then its a simple change sans recording to add.

ie specifically this part of the response on https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.list_resource_compliance_summaries

```
            'ExecutionSummary': {
                'ExecutionTime': datetime(2015, 1, 1),
                'ExecutionId': 'string',
                'ExecutionType': 'string'
}
```
bdwyertech: That makes sense -- can you think of another resource that'd be good to
copy pasta, maybe similar to the eval_filter on the ssm resource?  That one
had me kind of confused.

On Tue, Mar 24, 2020, 8:41 PM Kapil Thangavelu <notifications@github.com>
wrote:

> mostly wrt to the evaluation date, ie want to consider that as a part of a
> policy... tbh. in this context its purely additive as a future addition if
> its relevant, so i'm fine as is, but.. if you see a reasonable use case for
> it ootb then its a simple change sans recording to add.
>
> ie specifically this part of the response on
> https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.list_resource_compliance_summaries
>
>             'ExecutionSummary': {
>                 'ExecutionTime': datetime(2015, 1, 1),
>                 'ExecutionId': 'string',
>                 'ExecutionType': 'string'
> }
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/cloud-custodian/cloud-custodian/pull/5472#issuecomment-603577294>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAWV4WLHB3XU3HLWL6OOZCTRJFHM3ANCNFSM4LPVTHJA>
> .
>

kapilt: there's are two styles here, deriving from ValueFilter, overriding process to call match(annotation_value) ie

```python
process(self, resources):
    self.annotate_resources(resources)
    results = []
    for r in resources:
         if self.match(r['annotation_key']:
             resources.append(r)
    return results
```

that also wants rinherit=ValueFilter.schema in the type_schema line to get the jsonschema stuff going.

that works for doing a single attribute check on the underlying annotation. 

the other style is for multi attribute checks, there's a baseclass in filters/multiattr.py that helps set that up multiple embedded value filters on named subattributes of the annotation,  i don't know that we really need it for this use case though.

bdwyertech: @kapilt is this what you're looking for?  Lmk and I will squash the commits
kapilt: > @kapilt is this what you're looking for? Lmk and I will squash the commits

yup, that looks pretty good, thanks. no need on squash, the repo is setup to squash all pr merges.

Patch:
diff --git a/c7n/resources/ec2.py b/c7n/resources/ec2.py
index abd9a5b64..f83ccdb9b 100644
--- a/c7n/resources/ec2.py
+++ b/c7n/resources/ec2.py
@@ -920,6 +920,112 @@ class SsmStatus(ValueFilter):
             r[self.annotation] = info_map.get(r['InstanceId'], {})
 
 
+@EC2.filter_registry.register('ssm-compliance')
+class SsmCompliance(Filter):
+    """Filter ec2 instances by their ssm compliance status.
+
+    :Example:
+
+    Find non-compliant ec2 instances.
+
+    .. code-block:: yaml
+
+        policies:
+          - name: ec2-ssm-compliance
+            resource: ec2
+            filters:
+              - type: ssm-compliance
+                compliance_types:
+                  - Association
+                  - Patch
+                severity:
+                  - CRITICAL
+                  - HIGH
+                  - MEDIUM
+                  - LOW
+                  - UNSPECIFIED
+                states:
+                  - NON_COMPLIANT
+                eval_filters:
+                 - type: value
+                   key: ExecutionSummary.ExecutionTime
+                   value_type: age
+                   value: 30
+                   op: less-than
+    """
+    schema = type_schema(
+        'ssm-compliance',
+        **{'required': ['compliance_types'],
+           'compliance_types': {'type': 'array', 'items': {'type': 'string'}},
+           'severity': {'type': 'array', 'items': {'type': 'string'}},
+           'op': {'enum': ['or', 'and']},
+           'eval_filters': {'type': 'array', 'items': {
+                            'oneOf': [
+                                {'$ref': '#/definitions/filters/valuekv'},
+                                {'$ref': '#/definitions/filters/value'}]}},
+           'states': {'type': 'array',
+                      'default': ['NON_COMPLIANT'],
+                      'items': {
+                          'enum': [
+                              'COMPLIANT',
+                              'NON_COMPLIANT'
+                          ]}}})
+    permissions = ('ssm:ListResourceComplianceSummaries',)
+    annotation = 'c7n:ssm-compliance'
+
+    def process(self, resources, event=None):
+        op = self.data.get('op', 'or') == 'or' and any or all
+        eval_filters = []
+        for f in self.data.get('eval_filters', ()):
+            vf = ValueFilter(f)
+            vf.annotate = False
+            eval_filters.append(vf)
+
+        client = utils.local_session(self.manager.session_factory).client('ssm')
+        filters = [
+            {
+                'Key': 'Status',
+                'Values': self.data['states'],
+                'Type': 'EQUAL'
+            },
+            {
+                'Key': 'ComplianceType',
+                'Values': self.data['compliance_types'],
+                'Type': 'EQUAL'
+            }
+        ]
+        severity = self.data.get('severity')
+        if severity:
+            filters.append(
+                {
+                    'Key': 'OverallSeverity',
+                    'Values': severity,
+                    'Type': 'EQUAL'
+                })
+
+        resource_map = {}
+        pager = client.get_paginator('list_resource_compliance_summaries')
+        for page in pager.paginate(Filters=filters):
+            items = page['ResourceComplianceSummaryItems']
+            for i in items:
+                if not eval_filters:
+                    resource_map.setdefault(
+                        i['ResourceId'], []).append(i)
+                    continue
+                if op([f.match(i) for f in eval_filters]):
+                    resource_map.setdefault(
+                        i['ResourceId'], []).append(i)
+
+        results = []
+        for r in resources:
+            result = resource_map.get(r['InstanceId'])
+            if result:
+                r[self.annotation] = result
+                results.append(r)
+
+        return results
+
+
 @actions.register('set-monitoring')
 class MonitorInstances(BaseAction, StateTransitionFilter):
     """Action on EC2 Instances to enable/disable detailed monitoring


Test patch:
diff --git a/tests/data/placebo/test_ec2_ssm_compliance_filter/ec2.DescribeInstances_1.json b/tests/data/placebo/test_ec2_ssm_compliance_filter/ec2.DescribeInstances_1.json
new file mode 100644
index 000000000..5ee1da1b1
--- /dev/null
+++ b/tests/data/placebo/test_ec2_ssm_compliance_filter/ec2.DescribeInstances_1.json
@@ -0,0 +1,326 @@
+{
+    "status_code": 200,
+    "data": {
+        "Reservations": [
+            {
+                "Groups": [],
+                "Instances": [
+                    {
+                        "AmiLaunchIndex": 0,
+                        "ImageId": "ami-0f65671a86f061fcd",
+                        "InstanceId": "i-0dea82d960d56dc1d",
+                        "InstanceType": "t2.medium",
+                        "KeyName": "ssm-dev",
+                        "LaunchTime": {
+                            "__class__": "datetime",
+                            "year": 2019,
+                            "month": 1,
+                            "day": 30,
+                            "hour": 13,
+                            "minute": 14,
+                            "second": 36,
+                            "microsecond": 0
+                        },
+                        "Monitoring": {
+                            "State": "disabled"
+                        },
+                        "Placement": {
+                            "AvailabilityZone": "us-east-2a",
+                            "GroupName": "",
+                            "Tenancy": "default"
+                        },
+                        "PrivateDnsName": "ip-172-31-11-50.us-east-2.compute.internal",
+                        "PrivateIpAddress": "172.31.11.50",
+                        "ProductCodes": [],
+                        "PublicDnsName": "ec2-18-191-215-59.us-east-2.compute.amazonaws.com",
+                        "PublicIpAddress": "18.191.215.59",
+                        "State": {
+                            "Code": 64,
+                            "Name": "stopping"
+                        },
+                        "StateTransitionReason": "User initiated (2019-01-30 13:33:45 GMT)",
+                        "SubnetId": "subnet-db9f62b2",
+                        "VpcId": "vpc-5f6c9936",
+                        "Architecture": "x86_64",
+                        "BlockDeviceMappings": [
+                            {
+                                "DeviceName": "/dev/sda1",
+                                "Ebs": {
+                                    "AttachTime": {
+                                        "__class__": "datetime",
+                                        "year": 2019,
+                                        "month": 1,
+                                        "day": 30,
+                                        "hour": 13,
+                                        "minute": 14,
+                                        "second": 36,
+                                        "microsecond": 0
+                                    },
+                                    "DeleteOnTermination": true,
+                                    "Status": "attached",
+                                    "VolumeId": "vol-08d1573a7cd3fd1f2"
+                                }
+                            }
+                        ],
+                        "ClientToken": "",
+                        "EbsOptimized": false,
+                        "EnaSupport": true,
+                        "Hypervisor": "xen",
+                        "IamInstanceProfile": {
+                            "Arn": "arn:aws:iam::644160558196:instance-profile/CloudCustodianRole",
+                            "Id": "AIPAJO5EMJ3YEMNUF5ZWM"
+                        },
+                        "NetworkInterfaces": [
+                            {
+                                "Association": {
+                                    "IpOwnerId": "amazon",
+                                    "PublicDnsName": "ec2-18-191-215-59.us-east-2.compute.amazonaws.com",
+                                    "PublicIp": "18.191.215.59"
+                                },
+                                "Attachment": {
+                                    "AttachTime": {
+                                        "__class__": "datetime",
+                                        "year": 2019,
+                                        "month": 1,
+                                        "day": 30,
+                                        "hour": 13,
+                                        "minute": 14,
+                                        "second": 36,
+                                        "microsecond": 0
+                                    },
+                                    "AttachmentId": "eni-attach-0bc8d36721d13f0a6",
+                                    "DeleteOnTermination": true,
+                                    "DeviceIndex": 0,
+                                    "Status": "attached"
+                                },
+                                "Description": "Primary network interface",
+                                "Groups": [
+                                    {
+                                        "GroupName": "launch-wizard-1",
+                                        "GroupId": "sg-bebafdd5"
+                                    }
+                                ],
+                                "Ipv6Addresses": [],
+                                "MacAddress": "02:27:4e:43:cb:e6",
+                                "NetworkInterfaceId": "eni-01075318d08f25bf5",
+                                "OwnerId": "644160558196",
+                                "PrivateDnsName": "ip-172-31-11-50.us-east-2.compute.internal",
+                                "PrivateIpAddress": "172.31.11.50",
+                                "PrivateIpAddresses": [
+                                    {
+                                        "Association": {
+                                            "IpOwnerId": "amazon",
+                                            "PublicDnsName": "ec2-18-191-215-59.us-east-2.compute.amazonaws.com",
+                                            "PublicIp": "18.191.215.59"
+                                        },
+                                        "Primary": true,
+                                        "PrivateDnsName": "ip-172-31-11-50.us-east-2.compute.internal",
+                                        "PrivateIpAddress": "172.31.11.50"
+                                    }
+                                ],
+                                "SourceDestCheck": true,
+                                "Status": "in-use",
+                                "SubnetId": "subnet-db9f62b2",
+                                "VpcId": "vpc-5f6c9936"
+                            }
+                        ],
+                        "RootDeviceName": "/dev/sda1",
+                        "RootDeviceType": "ebs",
+                        "SecurityGroups": [
+                            {
+                                "GroupName": "launch-wizard-1",
+                                "GroupId": "sg-bebafdd5"
+                            }
+                        ],
+                        "SourceDestCheck": true,
+                        "StateReason": {
+                            "Code": "Client.UserInitiatedShutdown",
+                            "Message": "Client.UserInitiatedShutdown: User initiated shutdown"
+                        },
+                        "Tags": [
+                            {
+                                "Key": "Name",
+                                "Value": "kapil-ssm-dev"
+                            }
+                        ],
+                        "VirtualizationType": "hvm",
+                        "CpuOptions": {
+                            "CoreCount": 2,
+                            "ThreadsPerCore": 1
+                        },
+                        "HibernationOptions": {
+                            "Configured": false
+                        }
+                    }
+                ],
+                "OwnerId": "644160558196",
+                "ReservationId": "r-07103c7504d70d128"
+            },
+            {
+                "Groups": [],
+                "Instances": [
+                    {
+                        "AmiLaunchIndex": 0,
+                        "ImageId": "ami-0f65671a86f061fcd",
+                        "InstanceId": "i-0ba3874e85bb97244",
+                        "InstanceType": "t2.medium",
+                        "KeyName": "ssm-dev",
+                        "LaunchTime": {
+                            "__class__": "datetime",
+                            "year": 2019,
+                            "month": 1,
+                            "day": 30,
+                            "hour": 11,
+                            "minute": 58,
+                            "second": 37,
+                            "microsecond": 0
+                        },
+                        "Monitoring": {
+                            "State": "disabled"
+                        },
+                        "Placement": {
+                            "AvailabilityZone": "us-east-2a",
+                            "GroupName": "",
+                            "Tenancy": "default"
+                        },
+                        "PrivateDnsName": "ip-172-31-10-93.us-east-2.compute.internal",
+                        "PrivateIpAddress": "172.31.10.93",
+                        "ProductCodes": [],
+                        "PublicDnsName": "ec2-18-222-166-169.us-east-2.compute.amazonaws.com",
+                        "PublicIpAddress": "18.222.166.169",
+                        "State": {
+                            "Code": 16,
+                            "Name": "running"
+                        },
+                        "StateTransitionReason": "",
+                        "SubnetId": "subnet-db9f62b2",
+                        "VpcId": "vpc-5f6c9936",
+                        "Architecture": "x86_64",
+                        "BlockDeviceMappings": [
+                            {
+                                "DeviceName": "/dev/sda1",
+                                "Ebs": {
+                                    "AttachTime": {
+                                        "__class__": "datetime",
+                                        "year": 2019,
+                                        "month": 1,
+                                        "day": 30,
+                                        "hour": 11,
+                                        "minute": 58,
+                                        "second": 37,
+                                        "microsecond": 0
+                                    },
+                                    "DeleteOnTermination": true,
+                                    "Status": "attached",
+                                    "VolumeId": "vol-0fcfccae49c73e29a"
+                                }
+                            }
+                        ],
+                        "ClientToken": "",
+                        "EbsOptimized": false,
+                        "EnaSupport": true,
+                        "Hypervisor": "xen",
+                        "IamInstanceProfile": {
+                            "Arn": "arn:aws:iam::644160558196:instance-profile/CloudCustodianRole",
+                            "Id": "AIPAJO5EMJ3YEMNUF5ZWM"
+                        },
+                        "NetworkInterfaces": [
+                            {
+                                "Association": {
+                                    "IpOwnerId": "amazon",
+                                    "PublicDnsName": "ec2-18-222-166-169.us-east-2.compute.amazonaws.com",
+                                    "PublicIp": "18.222.166.169"
+                                },
+                                "Attachment": {
+                                    "AttachTime": {
+                                        "__class__": "datetime",
+                                        "year": 2019,
+                                        "month": 1,
+                                        "day": 30,
+                                        "hour": 11,
+                                        "minute": 58,
+                                        "second": 37,
+                                        "microsecond": 0
+                                    },
+                                    "AttachmentId": "eni-attach-0dbd4eeabb5c3ffa0",
+                                    "DeleteOnTermination": true,
+                                    "DeviceIndex": 0,
+                                    "Status": "attached"
+                                },
+                                "Description": "",
+                                "Groups": [
+                                    {
+                                        "GroupName": "launch-wizard-1",
+                                        "GroupId": "sg-bebafdd5"
+                                    }
+                                ],
+                                "Ipv6Addresses": [],
+                                "MacAddress": "02:ac:dd:b6:54:8a",
+                                "NetworkInterfaceId": "eni-0fe02396b03961784",
+                                "OwnerId": "644160558196",
+                                "PrivateDnsName": "ip-172-31-10-93.us-east-2.compute.internal",
+                                "PrivateIpAddress": "172.31.10.93",
+                                "PrivateIpAddresses": [
+                                    {
+                                        "Association": {
+                                            "IpOwnerId": "amazon",
+                                            "PublicDnsName": "ec2-18-222-166-169.us-east-2.compute.amazonaws.com",
+                                            "PublicIp": "18.222.166.169"
+                                        },
+                                        "Primary": true,
+                                        "PrivateDnsName": "ip-172-31-10-93.us-east-2.compute.internal",
+                                        "PrivateIpAddress": "172.31.10.93"
+                                    }
+                                ],
+                                "SourceDestCheck": true,
+                                "Status": "in-use",
+                                "SubnetId": "subnet-db9f62b2",
+                                "VpcId": "vpc-5f6c9936"
+                            }
+                        ],
+                        "RootDeviceName": "/dev/sda1",
+                        "RootDeviceType": "ebs",
+                        "SecurityGroups": [
+                            {
+                                "GroupName": "launch-wizard-1",
+                                "GroupId": "sg-bebafdd5"
+                            }
+                        ],
+                        "SourceDestCheck": true,
+                        "Tags": [
+                            {
+                                "Key": "Name",
+                                "Value": "kapil-ssm-dev"
+                            }
+                        ],
+                        "VirtualizationType": "hvm",
+                        "CpuOptions": {
+                            "CoreCount": 2,
+                            "ThreadsPerCore": 1
+                        },
+                        "CapacityReservationSpecification": {
+                            "CapacityReservationPreference": "open"
+                        },
+                        "HibernationOptions": {
+                            "Configured": false
+                        }
+                    }
+                ],
+                "OwnerId": "644160558196",
+                "ReservationId": "r-074f65542bb72fd56"
+            }
+        ],
+        "ResponseMetadata": {
+            "RequestId": "8151c8c3-afeb-4a62-b10d-b801d72a7458",
+            "HTTPStatusCode": 200,
+            "HTTPHeaders": {
+                "content-type": "text/xml;charset=UTF-8",
+                "transfer-encoding": "chunked",
+                "vary": "Accept-Encoding",
+                "date": "Wed, 30 Jan 2019 13:34:16 GMT",
+                "server": "AmazonEC2"
+            },
+            "RetryAttempts": 0
+        }
+    }
+}
diff --git a/tests/data/placebo/test_ec2_ssm_compliance_filter/ssm.ListResourceComplianceSummaries_1.json b/tests/data/placebo/test_ec2_ssm_compliance_filter/ssm.ListResourceComplianceSummaries_1.json
new file mode 100644
index 000000000..cddb0b11a
--- /dev/null
+++ b/tests/data/placebo/test_ec2_ssm_compliance_filter/ssm.ListResourceComplianceSummaries_1.json
@@ -0,0 +1,150 @@
+{
+  "status_code": 200,
+  "data": {
+    "ResourceComplianceSummaryItems": [
+      {
+        "ComplianceType": "Association",
+        "CompliantSummary": {
+          "CompliantCount": 1,
+          "SeveritySummary": {
+            "CriticalCount": 0,
+            "HighCount": 0,
+            "InformationalCount": 0,
+            "LowCount": 0,
+            "MediumCount": 0,
+            "UnspecifiedCount": 1
+          }
+        },
+        "ExecutionSummary": {
+          "ExecutionTime": "2020-03-20 10:52:50-04:00"
+        },
+        "NonCompliantSummary": {
+          "NonCompliantCount": 3,
+          "SeveritySummary": {
+            "CriticalCount": 0,
+            "HighCount": 0,
+            "InformationalCount": 0,
+            "LowCount": 0,
+            "MediumCount": 0,
+            "UnspecifiedCount": 3
+          }
+        },
+        "OverallSeverity": "UNSPECIFIED",
+        "ResourceId": "i-0dea82d960d56dc1d",
+        "ResourceType": "ManagedInstance",
+        "Status": "NON_COMPLIANT"
+      },
+      {
+        "ComplianceType": "Patch",
+        "CompliantSummary": {
+          "CompliantCount": 310,
+          "SeveritySummary": {
+            "CriticalCount": 0,
+            "HighCount": 0,
+            "InformationalCount": 0,
+            "LowCount": 0,
+            "MediumCount": 0,
+            "UnspecifiedCount": 310
+          }
+        },
+        "ExecutionSummary": {
+          "ExecutionId": "e44bffd7-4017-44ec-80f9-a6fbf932672f",
+          "ExecutionTime": "2020-03-19 18:50:59-04:00",
+          "ExecutionType": "Command"
+        },
+        "NonCompliantSummary": {
+          "NonCompliantCount": 36,
+          "SeveritySummary": {
+            "CriticalCount": 0,
+            "HighCount": 0,
+            "InformationalCount": 0,
+            "LowCount": 0,
+            "MediumCount": 0,
+            "UnspecifiedCount": 36
+          }
+        },
+        "OverallSeverity": "UNSPECIFIED",
+        "ResourceId": "i-0dea82d960d56dc1d",
+        "ResourceType": "ManagedInstance",
+        "Status": "NON_COMPLIANT"
+      },
+      {
+        "ComplianceType": "Association",
+        "CompliantSummary": {
+          "CompliantCount": 1,
+          "SeveritySummary": {
+            "CriticalCount": 0,
+            "HighCount": 0,
+            "InformationalCount": 0,
+            "LowCount": 0,
+            "MediumCount": 0,
+            "UnspecifiedCount": 1
+          }
+        },
+        "ExecutionSummary": {
+          "ExecutionTime": "2020-03-20 10:52:50-04:00"
+        },
+        "NonCompliantSummary": {
+          "NonCompliantCount": 3,
+          "SeveritySummary": {
+            "CriticalCount": 0,
+            "HighCount": 0,
+            "InformationalCount": 0,
+            "LowCount": 0,
+            "MediumCount": 0,
+            "UnspecifiedCount": 3
+          }
+        },
+        "OverallSeverity": "UNSPECIFIED",
+        "ResourceId": "i-0ba3874e85bb97244",
+        "ResourceType": "ManagedInstance",
+        "Status": "NON_COMPLIANT"
+      },
+      {
+        "ComplianceType": "Patch",
+        "CompliantSummary": {
+          "CompliantCount": 310,
+          "SeveritySummary": {
+            "CriticalCount": 0,
+            "HighCount": 0,
+            "InformationalCount": 0,
+            "LowCount": 0,
+            "MediumCount": 0,
+            "UnspecifiedCount": 310
+          }
+        },
+        "ExecutionSummary": {
+          "ExecutionId": "e44bffd7-4017-44ec-80f9-a6fbf932672f",
+          "ExecutionTime": "2020-03-19 18:50:59-04:00",
+          "ExecutionType": "Command"
+        },
+        "NonCompliantSummary": {
+          "NonCompliantCount": 36,
+          "SeveritySummary": {
+            "CriticalCount": 0,
+            "HighCount": 0,
+            "InformationalCount": 0,
+            "LowCount": 0,
+            "MediumCount": 0,
+            "UnspecifiedCount": 36
+          }
+        },
+        "OverallSeverity": "UNSPECIFIED",
+        "ResourceId": "i-0ba3874e85bb97244",
+        "ResourceType": "ManagedInstance",
+        "Status": "NON_COMPLIANT"
+      }
+    ],
+    "ResponseMetadata": {
+      "HTTPHeaders": {
+        "content-length": "12345",
+        "content-type": "application/x-amz-json-1.1",
+        "date": "Fri, 20 Mar 2020 15:09:20 GMT",
+        "x-amzn-requestid": "88383b45-36f4-44bb-b8a6-942dd5ffca3b"
+      },
+      "HTTPStatusCode": 200,
+      "RequestId": "88383b45-36f4-44bb-b8a6-942dd5ffca3b",
+      "RetryAttempts": 0
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/test_ec2.py b/tests/test_ec2.py
index 2b0eb8497..e949d90ff 100644
--- a/tests/test_ec2.py
+++ b/tests/test_ec2.py
@@ -263,6 +263,34 @@ class TestSsm(BaseTest):
             [r['InstanceId'] for r in resources],
             ['i-0dea82d960d56dc1d', 'i-0ba3874e85bb97244'])
 
+    def test_ssm_compliance(self):
+        session_factory = self.replay_flight_data('test_ec2_ssm_compliance_filter')
+        policy = self.load_policy({
+            'name': 'ec2-ssm-compliance',
+            'resource': 'aws.ec2',
+            'filters': [
+                {'type': 'ssm-compliance',
+                 'compliance_types': [
+                     'Association',
+                     'Patch'
+                 ],
+                 'severity': [
+                     'CRITICAL',
+                     'HIGH',
+                     'MEDIUM',
+                     'LOW',
+                     'UNSPECIFIED'
+                 ],
+                 'states': ['NON_COMPLIANT']}]},
+            session_factory=session_factory,
+            config={'region': 'us-east-2'})
+        resources = policy.run()
+        self.assertEqual(len(resources), 2)
+        self.assertTrue('c7n:ssm-compliance' in resources[0])
+        self.assertEqual(
+            [r['InstanceId'] for r in resources],
+            ['i-0dea82d960d56dc1d', 'i-0ba3874e85bb97244'])
+
 
 class TestHealthEventsFilter(BaseTest):