Automating Custom Compliance Scanning in AWS

What problem are we trying to solve?

The current state of AWS-provided instance compliance scanning is limited to the vanilla profiles available within Amazon Inspector for various operating systems. However, AWS Systems Manager (SSM) has an AWS-provided document (AWS-RunInspecCheck) that can be used to run Chef InSpec profiles on managed instances in order to assess their compliance level. While this document is limited in scope to a single profile, we can build a solution that leverages the document by deploying an SSM Automation Association that runs various Chef InSpec profiles against multiple operating systems within the environment.

Solution Overview
Download the CloudFormation template here.

Systems Manager Custom Document

In order to leverage the AWS-RunInSpecCheck document against multiple operating systems, we have to create a custom document that performs OS detection. Since this will be an Automation type document, we must use schema version 0.3.

The document will do a couple of things. First, we describe the instance in order to determine the instance ID and if it is in a running state before moving forward. If the instance is running, we ask the instance for the PlatformName and PlatformVersion. Based on this information, we can filter the instance into a step designed to execute the AWS-RunInspecChecks with the appropriate Chef InSpec profile. The Chef InSpec profile is located in an S3 bucket path designated as a parameter in the CloudFormation template.

The code below is provided as a CloudFormation resource. Included are comments regarding where other Operating System steps can be added.

CustomDocument:
  Type: AWS::SSM::Document
  Properties:
    Content:
      description: Document to detect OS version of running instances and execute the correct Chef InSpec profile on them
      schemaVersion: '0.3'
      assumeRole: '{{AutomationAssumeRole}}'
      parameters:
        AutomationAssumeRole:
          default: ''
          type: String
          description: The ARN of the role that allows Automation to perform the actions on your behalf
        InstanceId:
          type: String
          description: EC2 Instance target
      mainSteps:
        - name: GetInstanceState
          action: 'aws:executeAwsApi'
          inputs:
            Service: ec2
            Api: DescribeInstances
            InstanceIds:
              - '{{InstanceId}}'
          outputs:
            - Name: runningstate
              Selector: '$.Reservations[0].Instances[0].State.Name'
              Type: String
        - name: DetermineIfRunning
          action: 'aws:branch'
          inputs:
            Choices:
              - NextStep: GetInstance
                Variable: '{{GetInstanceState.runningstate}}'
                StringEquals: running
            Default: Sleep
        - name: GetInstance
          action: 'aws:executeAwsApi'
          inputs:
            Service: ssm
            Api: DescribeInstanceInformation
            InstanceInformationFilterList:
              - key: InstanceIds
                valueSet:
                  - '{{ InstanceId }}'
          outputs:
            - Name: platformName
              Selector: '$.InstanceInformationList[0].PlatformName'
              Type: String
            - Name: platformVersion
              Selector: '$.InstanceInformationList[0].PlatformVersion'
              Type: String
        - name: ChooseOSforCommands
          action: 'aws:branch'
          inputs:
            Choices:
              - And:
                  - Variable: '{{GetInstance.platformName}}'
                    Contains: # Linux Distro e.g. Amazon Linux
                  - Variable: '{{GetInstance.platformVersion}}'
                    Contains: # Linux Distro Version e.g. 2
                NextStep: runInspecForLinux
            Default: Sleep
        # Add more steps here like the one above in order to look for other types and versions of Operating Systems
        - name: runInspecForLinux
          action: 'aws:executeAwsApi'
          maxAttempts: 3
          timeoutSeconds: 3600
          onFailure: Abort
          inputs:
            Service: ssm
            Api: SendCommand
            DocumentName: AWS-RunInspecChecks
            InstanceIds:
              - '{{ InstanceId }}'
            Parameters:
              sourceType:
                - S3
              sourceInfo:
                - !Ref InSpecProfile1S3Path
          isEnd: true
        # Add more steps here like the one above to redirect to the correct S3 source path
        - name: Sleep
          action: 'aws:sleep'
          inputs:
            Duration: PT3S
    DocumentType: Automation

Systems Manager Association

But Stephen, CloudFormation doesn’t currently support creating SSM Automation Associations!

Well, yes. But that doesn’t stop us from creating a custom resource that executes a Lambda function to create and maintain it for us. Here is some python code we can use inside a Lamnbda function. The environment variables referenced are definind in the CloudFormation template in Solution Overview

import json
import boto3
import os
import cfnresponse

ssmCustomDoc = os.environ['ssmDoc']
ssmAutomationRole = os.environ['ssmRole']
ssmAssociationName = os.environ['ssmAssociation']
scanSchedule = os.environ['scanSchedule']

ssm = boto3.client('ssm')

def handler(event, context):
       
  if event['RequestType'] == 'Create':
    response = ssm.create_association(
      Name=ssmCustomDoc,
      AssociationName=ssmAssociationName,
      Parameters={
        'AutomationAssumeRole':[ssmAutomationRole]
      },
      Targets=[
        {
          'Key':'InstanceIds',
          'Values':['*']
        }
      ],
      AutomationTargetParameterName='InstanceId',
      ScheduleExpression=scanSchedule
    )

  if event['RequestType'] == 'Update' or event['RequestType'] == 'Delete':
    associations = ssm.list_associations(
    AssociationFilterList=[
      {
        'key':'AssociationName',
        'value':ssmAssociationName
      }
    ])
    associationID = associations['Associations'][0]['AssociationId']

    if event['RequestType'] == 'Update':
      response = ssm.update_association(
        AssociationId=associationID,
        Parameters={
          'AutomationAssumeRole':[ssmAutomationRole]
        },
        Name=ssmCustomDoc,
        Targets=[
          {
            'Key':'InstanceIds',
            'Values':['*']
          }
        ],
        AssociationName=ssmAssociationName,
        AutomationTargetParameterName='InstanceId',
        ScheduleExpression=scanSchedule
      )

    if event['RequestType'] == 'Delete':
      response = ssm.delete_association(
        Name=ssmCustomDoc,
        AssociationId=associationID
      )
  
  cfnresponse.send(event, context, cfnresponse.SUCCESS, {})

Results

So let’s take a look at the entire stack after it’s deployed.

First, let’s verify that the Lambda function successfully created the association for our custom document.

Cloudwatch log showing successful invocation of the custom resource to create the automation association

Next, let’s go ahead and execute this association ad-hoc in order to ensure it works. We can see in the Run Command section of Systems Manager that several commands have been kicked off.

Okay great. Once they finish executing, where are the results? We can see them in the Compliance section of Systems Manager.

From here, you can drill down into instances that are compliant and non-compliant, and then drill down even further to see which checks are failing as part of the Chef InSpec profile that was run.

There you have it. We have successfully scanned multiple operating systems via a single automation association. Now that our results are in the Compliance section of Systems Manager, we can track their resolution either via the console or programmatically.

Looking Ahead

This solution leverages S3 buckets to store Chef InSpec profiles. While a CI/CD pipeline can be set up to populate those buckets with new changes to the profiles, this is not ideal when the SSM document can tie directly into Github. Improvements to this stack are possible in that regard.

We can also create infrastructure in order to forward these results to a CloudWatch log group in order to subscribe results to further automation mechanisms or log forwarding to a SIEM.

Further Reading

What is Chef InSpec?

AWS: Using Chef InSpec profiles with Systems Manager Compliance

MITRE Chef InSpec profiles

Standard,Quality,Control,Certification,Assurance,Guarantee,Concept