cdklabs / cdk-validator-cfnguard

Apache License 2.0
69 stars 6 forks source link

Use cdk metadata to selectively disable a control #202

Open Jacco opened 1 year ago

Jacco commented 1 year ago

For making fine graned exceptions:

I saw the usage in the AWS Guard Rules Registry

They just add a condition to the resource selectors:

#
# Select all API GW Method resources from incoming template (payload)
#
let api_gw_resources_type_check = Resources.*[ Type == 'AWS::ApiGateway::DomainName'
  Metadata.guard.SuppressedRules not exists or
  Metadata.guard.SuppressedRules.* != "API_GW_ENDPOINT_TYPE_CHECK"
]

In cloudformation the exception looks like this:

"Resources": {
  "ElasticsearchDomain": {
    "Type": "AWS::Elasticsearch::Domain",
    "Metadata": {
      "guard": {
        "SuppressedRules": ["ELASTICSEARCH_ENCRYPTED_AT_REST"]
      }
    },
    "Properties": {
      "DomainName": "test"
    }
  }
}

In CDK code something like this:

function addExceptionToResource(resource: IConstruct, rulename: string) {
          const resource = resource as CfnResource;
          let metadata = resource.getMetadata('guard') || {};
          metadata['SuppressedRules'] |= []
          metadata['SuppressedRules'].push(rulename)
          resource.addMetadata('guard', metadata);
}

BTW: I have created some python/nodejs scripts that can automatically pull all the data needed for cdk-validator-cfnguard. The sources it uses to match the detailed metadata are: blackbeard service used by controltower console, two javascript files uses by the controltower console containing metadata, the aws documentation website (to get the config rule). I am already parsing the .guard resources to extract the RemediationMessage from the guard file. It will be very easy to add the metadata condition to the guard files while extracting the assets.

!! If you are interested in these scripts let me know. !!

Jacco commented 1 year ago

Here is the python code I used to scrape the undocumented BlackBeard service. I hoped it contained all the necessary data but unfortunately more datasources were needed.

This script does not yet place the metadata/guard files in separate directories. That is done in another step that involves javascript from the console page using nodejs. The servicenames are in there that I use to determine the directory names.

I'd be happy to convert this script to TypeScript if you want :-)

import json
import requests
import boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
import inspect
import re

def snake_to_camel(snake):
    if '_' in snake:
        return ''.join([part.capitalize() for part in snake.split('_')])
    else:
        return snake

class BlackbeardSession:
    def __init__(self, boto_session):
        self._sigv4 = SigV4Auth(boto_session.get_credentials(), 'controltower', 'eu-west-1')
        self._endpoint = 'https://prod.eu-west-1.blackbeard.aws.a2z.com'

    def do_request(self, method, **kwargs):
        args = { snake_to_camel(k): v for k, v in kwargs.items() }
        data = json.dumps(args)
        headers = {
            'Content-Type': 'application/x-amz-json-1.0',
            "X-Amz-Target": "AWSBlackbeardService." + snake_to_camel(method)
        }
        request = AWSRequest(method='POST', url=self._endpoint, data=data, headers=headers)
        request.context["payload_signing_enabled"] = True # payload signing is not supported
        self._sigv4.add_auth(request)
        prepped = request.prepare()
        response = requests.post(prepped.url, headers=prepped.headers, data=data)
        return json.loads(response.text)

    def describe_guardrail(self, **kwargs): 
        nm = inspect.currentframe().f_code.co_name
        return self.do_request(nm, **kwargs)

    def list_guardrails(self, **kwargs):
        nm = inspect.currentframe().f_code.co_name
        return self.do_request(nm, **kwargs)

def get_remediation_text(template):
    remeds = set()
    for m in re.finditer(r'\[FIX\]:\s+(?P<remed>.*)\n', template, flags=re.MULTILINE):
        remeds.add(m.groupdict()["remed"])
    if len(remeds) > 1:
        print("WRONG", name)
    return list(remeds)[0]

if __name__ == "__main__":
    # you need to set up so these actions are allowed 
    # "controltower:DescribeGuardrail",
    # "controltower:ListGuardrails",
    boto_session = boto3.Session(region_name='us-east-1')

    bb = BlackbeardSession(boto_session)
    nxt = None
    guardrails = []
    while True:
        r = bb.list_guardrails(**({ "NextToken": nxt } if nxt else {}))
        lst = r['GuardrailList']
        names = [guardrail["Name"] for guardrail in lst]
        guardrails.extend(r['GuardrailList'])
        nxt = r.get("NextToken", None)
        if not nxt:
            break

    for guardrail in guardrails:
        if not guardrail["Name"].startswith('CT.'):
            continue
        r = bb.describe_guardrail(guardrail_name=guardrail["Name"])
        print(guardrail["Name"])
        if r["ImplementationType"] == "AWS_CLOUDFORMATION_GUARD_RULE":
            template = r["Artifacts"][0]["Content"]
            with open("./guardrails/cfn-guard/" + guardrail["Name"].lower() + ".guard", "w") as f:
                f.write(template)
            del r["Artifacts"]
            r["RegionalPreference"] = guardrail["RegionalPreference"]
            r["RemediationMessage"] = get_remediation_text(template)
            with open("./guardrails/metadata/" + guardrail["Name"].lower() + ".metadata.json", "w") as f:
                f.write(json.dumps(r, indent=2, default=str))
eppeters commented 11 months ago

Hi @Jacco. Thanks for your feedback. Regardless of the implementation mechanism, is it fair to say your goal is to have a more fine-grained way to turn controls on and off?

Regarding the script to pull from the Blackbeard API - I'm not sure how that relates to the above goal, if in fact that is your goal. If you could help me understand what you use the script for, I would appreciate it. If you're just looking for the metadata associated with each Guard control, this directory might already have what you're looking for: https://github.com/cdklabs/cdk-validator-cfnguard/blob/main/rules/control-tower/metadata

Jacco commented 11 months ago

The issue is indeed fine grained control :-)

The script does not really relate to the goal, sorry for the confusion. I used this script to pull all the guard rules from ControlTower (I used it to parse the guardrules and inject the metadata conditions for my use case). I was wondering if you construct the metadata directory manually. I (almost) fully automated this using above script plus a few others. If you are doing it manually then I am happy to donate/explain my script :-)