diff --git a/samples/sample-rdk-rules/.gitignore b/samples/sample-rdk-rules/.gitignore deleted file mode 100644 index 37833f8be..000000000 --- a/samples/sample-rdk-rules/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -*.swp -package-lock.json -__pycache__ -.pytest_cache -.venv -*.egg-info - -# CDK asset staging directory -.cdk.staging -cdk.out diff --git a/samples/sample-rdk-rules/README.md b/samples/sample-rdk-rules/README.md deleted file mode 100644 index 3bfff8dd5..000000000 --- a/samples/sample-rdk-rules/README.md +++ /dev/null @@ -1,134 +0,0 @@ -# Sample RDK Rules pipeline - -This setup will allow you to deploy custom config rules created by the RDK via -ADF pipeline. - -## Architecture - -![Architecture](./meta/custom-configs.png) - -- As a first step it requires to have a Source code repository to store our - code. In this pattern we are using CodeCommit repository. This repository - created by as a part of the pipeline definition in the ADF's - `deployment_map.yml`. Example of the pipeline definition is in the ADF setup - section. -- ADF pipeline definition creates a pipeline that will deploy Lambda function(s) - into the compliance account and Custom Config rule(s) to Target accounts. -- When a Custom Config rule get pushed into the CodeCommit repository; - - CodeBuild will find the RDK rule(s) recursively in the `config-rules` - directory then zip each single rule one by one and upload into ADF bucket. - Buildspec is using a helper script called `lambda_helper.py` to achieve - this task. ADF populates bucket names into SSM Parameter store on the - installation. `lambda_helper.py` fetches the bucket name from the SSM - Parameter Store. Parameter name looks like - `/cross_region/s3_regional_bucket/{region}`. - - Then CodeBuild will generate 2 CloudFormation templates one for Lambda - function(s) deployment and other for the Custom Config rule(s) deployment. -- When a Lambda function get invokes by a Target account Custom config rule; it - will assume the Config role in the target account then put config Evaluations - into the Target account's Config rule. - -### ADF setup - -Sample pipeline definition looks like below: - -```yaml - - name: custom-config-rules-pipeline ## repo name - default_providers: - source: - provider: codecommit - properties: - account_id: - build: - provider: codebuild - properties: - image: "STANDARD_7_0" - deploy: - provider: cloudformation - targets: - - name: LambdaDeployment - regions: - target: - properties: - template_filename: "template-lambda.json" - - name: ConfigRulesDeployment - regions: - target: - - - properties: - template_filename: "template-config-rules.json" -``` - -## Development setup - -After you clone the repo following file/folder structure will be there; - -- `config-rules`: This folder will contain all the custom config rules created - by `rdk create ...`. Make sure to setup correct details in the - `parameters.json` file(ex: SourceRuntime) -- `params`: Contains parameters we need for the generated CloudFormation - templates. You must set the account id of the Compliance account in - `LambdaAccountId` and Target accounts Config role arn as a pattern in - `ConfigRoleArnToAssume`. These will be used as parameters when it deploys - config-rule into Target accounts to refer Lambda function from the Compliance - account. [Refer this - link](../../docs/user-guide.md#cloudformation-parameters-and-tagging) -- `templates`: This folder contains all the CloudFormation (CFn) template pieces - that required to build CFn template for the lambda function deployment. -- `buildspec.yml`: Buildspec file to generate CloudFormation templates for the - Lambda and Custom Config rules -- `lambda_helper.py`: This is the helper file that pack and upload the lambda - code recursively in the config-rules folder -- `requirements.txt`: Requirements for the `lambda_helper.py` script. - -## Lambda function implementation requirements - -In Lambda functions when you want to refer boto3 client or resource make sure - -- Set `ASSUME_ROLE_MODE` constant to `True` -- Use `get_client` method for client. -- Duplicate `get_client` and create the `get_resource` method. - -```py -def get_resource(service, event, region=None): - """Return the service boto resource. It should be used instead of directly - calling the resource. - - Keyword arguments: - service -- the service name used for calling the boto.resource() - event -- the event variable given in the lambda handler - region -- the region where the resource is called (default: None) - """ - if not ASSUME_ROLE_MODE: - return boto3.resource( - service, region - ) - credentials = get_assume_role_credentials( - get_execution_role_arn(event), - region - ) - return boto3.resource( - service, aws_access_key_id=credentials['AccessKeyId'], - aws_secret_access_key=credentials['SecretAccessKey'], - aws_session_token=credentials['SessionToken'], - region_name=region - ) -``` - -These methods use STS and config payload to assume the IAM role in the target -account. If not lambda execution will be failed. - -[More info](https://aws.amazon.com/blogs/mt/aws-config-rdk-multi-account-and-multi-region-deployment/) - -## Prerequisites/ Important bits - -- This solution does not setup config or config recorder. -- When this solution deploys the config-rule to a target account; it expects - config is enabled in the target account. -- Each target account's config role should be able assume by - `` to put evaluations into each target - account's config. AKA config role in the target account (`2222222222`) should - have the `lambda-function-account-id` (`1111111111`) as trusted entity as - below. - -![Trusted entity](./meta/lambda-account-id-trusted-entity.png) diff --git a/samples/sample-rdk-rules/buildspec.yml b/samples/sample-rdk-rules/buildspec.yml deleted file mode 100644 index 9a4a753b6..000000000 --- a/samples/sample-rdk-rules/buildspec.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. -# SPDX-License-Identifier: Apache-2.0 - -version: 0.2 -phases: - install: - runtime-versions: - python: 3.12 - nodejs: 20 - commands: - - aws s3 cp s3://$S3_BUCKET_NAME/adf-build/ adf-build/ --recursive --only-show-errors - - pip install -r adf-build/requirements.txt -q - - python adf-build/generate_params.py - - build: - commands: - - pip install rdk - - cd config-rules - - rdk create-rule-template --rules-only -a -o ../template-config-rules.json - - cd .. - - pip install -r requirements.txt - - python lambda_helper.py --template-name template-lambda.json - -artifacts: - files: '**/*' diff --git a/samples/sample-rdk-rules/config-rules/EC2_CHECKS_TERMINATION_PROTECTION_ADF/EC2_CHECKS_TERMINATION_PROTECTION_ADF.py b/samples/sample-rdk-rules/config-rules/EC2_CHECKS_TERMINATION_PROTECTION_ADF/EC2_CHECKS_TERMINATION_PROTECTION_ADF.py deleted file mode 100644 index 49cdabbdf..000000000 --- a/samples/sample-rdk-rules/config-rules/EC2_CHECKS_TERMINATION_PROTECTION_ADF/EC2_CHECKS_TERMINATION_PROTECTION_ADF.py +++ /dev/null @@ -1,576 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. -# SPDX-License-Identifier: Apache-2.0 - -import json -import sys -import datetime -import boto3 -import botocore - -try: - import liblogging -except ImportError: - pass - -############## -# Parameters # -############## - -# Define the default resource to report to Config Rules -DEFAULT_RESOURCE_TYPE = "AWS::::Account" - -APPLICABLE_RESOURCES = ["AWS::EC2::Instance"] -VALID_ENVIRONMENTS = ["PROD", "UAT"] - -# Set to True to get the lambda to assume the Role attached on the Config Service (useful for cross-account). -ASSUME_ROLE_MODE = True - -# Other parameters (no change needed) -CONFIG_ROLE_TIMEOUT_SECONDS = 900 - -############# -# Main Code # -############# - - -def evaluate_compliance(event, configuration_item, valid_rule_parameters): - """Form the evaluation(s) to be return to Config Rules - - Return either: - None -- when no result needs to be displayed - a string -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE - a dictionary -- the evaluation dictionary, usually built by build_evaluation_from_config_item() - a list of dictionary -- a list of evaluation dictionary , usually built by build_evaluation() - - Keyword arguments: - event -- the event variable given in the lambda handler - configuration_item -- the configurationItem dictionary in the invokingEvent - valid_rule_parameters -- the output of the evaluate_parameters() representing validated parameters of the Config Rule - - Advanced Notes: - 1 -- if a resource is deleted and generate a configuration change with ResourceDeleted status, the Boilerplate code will put a NOT_APPLICABLE on this resource automatically. - 2 -- if a None or a list of dictionary is returned, the old evaluation(s) which are not returned in the new evaluation list are returned as NOT_APPLICABLE by the Boilerplate code - 3 -- if None or an empty string, list or dict is returned, the Boilerplate code will put a "shadow" evaluation to feedback that the evaluation took place properly - """ - - ############################### - # Add your custom logic here. # - ############################### - - if not is_applicable_ec2(configuration_item, event) or configuration_item is None: - return "NOT_APPLICABLE" - - # if termination protection is not enabled resource is not Compliant - if not is_termination_protection_on(configuration_item["resourceId"], event): - return "NON_COMPLIANT" - else: - return "COMPLIANT" - - -def is_applicable_ec2(configuration_item, event): - if configuration_item["resourceType"] not in APPLICABLE_RESOURCES: - return False - - # If instance is not in a valid environment its not applicable - if not is_valid_environment(configuration_item["resourceId"], event): - return False - - # Fetch about autoscaling - if is_part_of_asg(configuration_item["resourceId"], event): - return False - - return True - - -# If instance not in a valid environment its not applicable -def is_valid_environment(instance_id, event): - ec2 = get_resource("ec2", event) - instance = ec2.Instance(instance_id) - for tags in instance.tags or []: - if tags["Key"] == "Env": - return tags["Value"] in VALID_ENVIRONMENTS - - -# Checks is instance part of an Autoscaling Group -def is_part_of_asg(instance_id, event): - asg = get_client("autoscaling", event) - result = asg.describe_auto_scaling_instances(InstanceIds=[instance_id]) - return len(result["AutoScalingInstances"]) > 0 - - -# Checks DisableApiTermination is enabled or not -def is_termination_protection_on(instance_id, event): - client = get_client("ec2", event) - response = client.describe_instance_attribute( - Attribute="disableApiTermination", InstanceId=instance_id - ) - return response["DisableApiTermination"]["Value"] - - -def evaluate_parameters(rule_parameters): - """Evaluate the rule parameters dictionary validity. Raise a ValueError for invalid parameters. - - Return: - anything suitable for the evaluate_compliance() - - Keyword arguments: - rule_parameters -- the Key/Value dictionary of the Config Rules parameters - """ - valid_rule_parameters = rule_parameters - return valid_rule_parameters - - -#################### -# Helper Functions # -#################### - -# Build an error to be displayed in the logs when the parameter is invalid. -def build_parameters_value_error_response(ex): - """Return an error dictionary when the evaluate_parameters() raises a ValueError. - - Keyword arguments: - ex -- Exception text - """ - return build_error_response( - internal_error_message="Parameter value is invalid", - internal_error_details="An ValueError was raised during the validation of the Parameter value", - customer_error_code="InvalidParameterValueException", - customer_error_message=str(ex), - ) - - -# This gets the client after assuming the Config service role -# either in the same AWS account or cross-account. -def get_client(service, event, region=None): - """Return the service boto client. It should be used instead of directly calling the client. - - Keyword arguments: - service -- the service name used for calling the boto.client() - event -- the event variable given in the lambda handler - region -- the region where the client is called (default: None) - """ - if not ASSUME_ROLE_MODE: - return boto3.client(service, region) - credentials = get_assume_role_credentials(get_execution_role_arn(event), region) - return boto3.client( - service, - aws_access_key_id=credentials["AccessKeyId"], - aws_secret_access_key=credentials["SecretAccessKey"], - aws_session_token=credentials["SessionToken"], - region_name=region, - ) - - -def get_resource(service, event, region=None): - """Return the service boto client. It should be used instead of directly calling the client. - - Keyword arguments: - service -- the service name used for calling the boto.client() - event -- the event variable given in the lambda handler - region -- the region where the client is called (default: None) - """ - if not ASSUME_ROLE_MODE: - return boto3.resource(service, region) - credentials = get_assume_role_credentials(get_execution_role_arn(event), region) - return boto3.resource( - service, - aws_access_key_id=credentials["AccessKeyId"], - aws_secret_access_key=credentials["SecretAccessKey"], - aws_session_token=credentials["SessionToken"], - region_name=region, - ) - - -# This generate an evaluation for config -def build_evaluation( - resource_id, - compliance_type, - event, - resource_type=DEFAULT_RESOURCE_TYPE, - annotation=None, -): - """Form an evaluation as a dictionary. Usually suited to report on scheduled rules. - - Keyword arguments: - resource_id -- the unique id of the resource to report - compliance_type -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE - event -- the event variable given in the lambda handler - resource_type -- the CloudFormation resource type (or AWS::::Account) to report on the rule (default DEFAULT_RESOURCE_TYPE) - annotation -- an annotation to be added to the evaluation (default None). It will be truncated to 255 if longer. - """ - eval_cc = {} - if annotation: - eval_cc["Annotation"] = build_annotation(annotation) - eval_cc["ComplianceResourceType"] = resource_type - eval_cc["ComplianceResourceId"] = resource_id - eval_cc["ComplianceType"] = compliance_type - eval_cc["OrderingTimestamp"] = str( - json.loads(event["invokingEvent"])["notificationCreationTime"] - ) - return eval_cc - - -def build_evaluation_from_config_item( - configuration_item, compliance_type, annotation=None -): - """Form an evaluation as a dictionary. Usually suited to report on configuration change rules. - - Keyword arguments: - configuration_item -- the configurationItem dictionary in the invokingEvent - compliance_type -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE - annotation -- an annotation to be added to the evaluation (default None). It will be truncated to 255 if longer. - """ - eval_ci = {} - if annotation: - eval_ci["Annotation"] = build_annotation(annotation) - eval_ci["ComplianceResourceType"] = configuration_item["resourceType"] - eval_ci["ComplianceResourceId"] = configuration_item["resourceId"] - eval_ci["ComplianceType"] = compliance_type - eval_ci["OrderingTimestamp"] = configuration_item["configurationItemCaptureTime"] - return eval_ci - - -#################### -# Boilerplate Code # -#################### - -# Get execution role for Lambda function -def get_execution_role_arn(event): - role_arn = None - if "ruleParameters" in event: - rule_params = json.loads(event["ruleParameters"]) - role_name = rule_params.get("ExecutionRoleName") - if role_name: - execution_role_prefix = event["executionRoleArn"].split("/")[0] - role_arn = "{}/{}".format(execution_role_prefix, role_name) - - if not role_arn: - role_arn = event["executionRoleArn"] - - return role_arn - - -# Build annotation within Service constraints -def build_annotation(annotation_string): - if len(annotation_string) > 256: - return annotation_string[:244] + " [truncated]" - return annotation_string - - -# Helper function used to validate input -def check_defined(reference, reference_name): - if not reference: - raise Exception("Error: ", reference_name, "is not defined") - return reference - - -# Check whether the message is OversizedConfigurationItemChangeNotification or not -def is_oversized_changed_notification(message_type): - check_defined(message_type, "messageType") - return message_type == "OversizedConfigurationItemChangeNotification" - - -# Check whether the message is a ScheduledNotification or not. -def is_scheduled_notification(message_type): - check_defined(message_type, "messageType") - return message_type == "ScheduledNotification" - - -# Get configurationItem using getResourceConfigHistory API -# in case of OversizedConfigurationItemChangeNotification -def get_configuration(resource_type, resource_id, configuration_capture_time): - result = AWS_CONFIG_CLIENT.get_resource_config_history( - resourceType=resource_type, - resourceId=resource_id, - laterTime=configuration_capture_time, - limit=1, - ) - configuration_item = result["configurationItems"][0] - return convert_api_configuration(configuration_item) - - -# Convert from the API model to the original invocation model -def convert_api_configuration(configuration_item): - for k, v in configuration_item.items(): - if isinstance(v, datetime.datetime): - configuration_item[k] = str(v) - configuration_item["awsAccountId"] = configuration_item["accountId"] - configuration_item["ARN"] = configuration_item["arn"] - configuration_item["configurationStateMd5Hash"] = configuration_item[ - "configurationItemMD5Hash" - ] - configuration_item["configurationItemVersion"] = configuration_item["version"] - configuration_item["configuration"] = json.loads( - configuration_item["configuration"] - ) - if "relationships" in configuration_item: - for i in range(len(configuration_item["relationships"])): - configuration_item["relationships"][i]["name"] = configuration_item[ - "relationships" - ][i]["relationshipName"] - return configuration_item - - -# Based on the type of message get the configuration item -# either from configurationItem in the invoking event -# or using the getResourceConfigHistory API in getConfiguration function. -def get_configuration_item(invoking_event): - check_defined(invoking_event, "invokingEvent") - if is_oversized_changed_notification(invoking_event["messageType"]): - configuration_item_summary = check_defined( - invoking_event["configurationItemSummary"], "configurationItemSummary" - ) - return get_configuration( - configuration_item_summary["resourceType"], - configuration_item_summary["resourceId"], - configuration_item_summary["configurationItemCaptureTime"], - ) - if is_scheduled_notification(invoking_event["messageType"]): - return None - return check_defined(invoking_event["configurationItem"], "configurationItem") - - -# Check whether the resource has been deleted. If it has, then the evaluation is unnecessary. -def is_applicable(configuration_item, event): - try: - check_defined(configuration_item, "configurationItem") - check_defined(event, "event") - except: - return True - status = configuration_item["configurationItemStatus"] - event_left_scope = event["eventLeftScope"] - if status == "ResourceDeleted": - print("Resource Deleted, setting Compliance Status to NOT_APPLICABLE.") - - return status in ("OK", "ResourceDiscovered") and not event_left_scope - - -def get_assume_role_credentials(role_arn, region=None): - sts_client = boto3.client("sts", region) - try: - assume_role_response = sts_client.assume_role( - RoleArn=role_arn, - RoleSessionName="configLambdaExecution", - DurationSeconds=CONFIG_ROLE_TIMEOUT_SECONDS, - ) - if "liblogging" in sys.modules: - liblogging.logSession(role_arn, assume_role_response) - return assume_role_response["Credentials"] - except botocore.exceptions.ClientError as ex: - # Scrub error message for any internal account info leaks - print(str(ex)) - if "AccessDenied" in ex.response["Error"]["Code"]: - ex.response["Error"][ - "Message" - ] = "AWS Config does not have permission to assume the IAM role." - else: - ex.response["Error"]["Message"] = "InternalError" - ex.response["Error"]["Code"] = "InternalError" - raise ex - - -# This removes older evaluation (usually useful for periodic rule not reporting on AWS::::Account). -def clean_up_old_evaluations(latest_evaluations, event): - - cleaned_evaluations = [] - - old_eval = AWS_CONFIG_CLIENT.get_compliance_details_by_config_rule( - ConfigRuleName=event["configRuleName"], - ComplianceTypes=["COMPLIANT", "NON_COMPLIANT"], - Limit=100, - ) - - old_eval_list = [] - - while True: - for old_result in old_eval["EvaluationResults"]: - old_eval_list.append(old_result) - if "NextToken" in old_eval: - next_token = old_eval["NextToken"] - old_eval = AWS_CONFIG_CLIENT.get_compliance_details_by_config_rule( - ConfigRuleName=event["configRuleName"], - ComplianceTypes=["COMPLIANT", "NON_COMPLIANT"], - Limit=100, - NextToken=next_token, - ) - else: - break - - for old_eval in old_eval_list: - old_resource_id = old_eval["EvaluationResultIdentifier"][ - "EvaluationResultQualifier" - ]["ResourceId"] - newer_founded = False - for latest_eval in latest_evaluations: - if old_resource_id == latest_eval["ComplianceResourceId"]: - newer_founded = True - if not newer_founded: - cleaned_evaluations.append( - build_evaluation(old_resource_id, "NOT_APPLICABLE", event) - ) - - return cleaned_evaluations + latest_evaluations - - -def lambda_handler(event, context): - if "liblogging" in sys.modules: - liblogging.logEvent(event) - - global AWS_CONFIG_CLIENT - - # print(event) - check_defined(event, "event") - invoking_event = json.loads(event["invokingEvent"]) - rule_parameters = {} - if "ruleParameters" in event: - rule_parameters = json.loads(event["ruleParameters"]) - - try: - valid_rule_parameters = evaluate_parameters(rule_parameters) - except ValueError as ex: - return build_parameters_value_error_response(ex) - - try: - AWS_CONFIG_CLIENT = get_client("config", event) - if invoking_event["messageType"] in [ - "ConfigurationItemChangeNotification", - "ScheduledNotification", - "OversizedConfigurationItemChangeNotification", - ]: - configuration_item = get_configuration_item(invoking_event) - if is_applicable(configuration_item, event): - compliance_result = evaluate_compliance( - event, configuration_item, valid_rule_parameters - ) - else: - compliance_result = "NOT_APPLICABLE" - else: - return build_internal_error_response( - "Unexpected message type", str(invoking_event) - ) - except botocore.exceptions.ClientError as ex: - if is_internal_error(ex): - return build_internal_error_response( - "Unexpected error while completing API request", str(ex) - ) - return build_error_response( - "Customer error while making API request", - str(ex), - ex.response["Error"]["Code"], - ex.response["Error"]["Message"], - ) - except ValueError as ex: - return build_internal_error_response(str(ex), str(ex)) - - evaluations = [] - latest_evaluations = [] - - if not compliance_result: - latest_evaluations.append( - build_evaluation( - event["accountId"], - "NOT_APPLICABLE", - event, - resource_type="AWS::::Account", - ) - ) - evaluations = clean_up_old_evaluations(latest_evaluations, event) - elif isinstance(compliance_result, str): - if configuration_item: - evaluations.append( - build_evaluation_from_config_item(configuration_item, compliance_result) - ) - else: - evaluations.append( - build_evaluation( - event["accountId"], - compliance_result, - event, - resource_type=DEFAULT_RESOURCE_TYPE, - ) - ) - elif isinstance(compliance_result, list): - for evaluation in compliance_result: - missing_fields = False - for field in ( - "ComplianceResourceType", - "ComplianceResourceId", - "ComplianceType", - "OrderingTimestamp", - ): - if field not in evaluation: - print("Missing " + field + " from custom evaluation.") - missing_fields = True - - if not missing_fields: - latest_evaluations.append(evaluation) - evaluations = clean_up_old_evaluations(latest_evaluations, event) - elif isinstance(compliance_result, dict): - missing_fields = False - for field in ( - "ComplianceResourceType", - "ComplianceResourceId", - "ComplianceType", - "OrderingTimestamp", - ): - if field not in compliance_result: - print("Missing " + field + " from custom evaluation.") - missing_fields = True - if not missing_fields: - evaluations.append(compliance_result) - else: - evaluations.append( - build_evaluation_from_config_item(configuration_item, "NOT_APPLICABLE") - ) - - # Put together the request that reports the evaluation status - result_token = event["resultToken"] - test_mode = False - if result_token == "TESTMODE": - # Used solely for RDK test to skip actual put_evaluation API call - test_mode = True - - # Invoke the Config API to report the result of the evaluation - evaluation_copy = [] - evaluation_copy = evaluations[:] - while evaluation_copy: - AWS_CONFIG_CLIENT.put_evaluations( - Evaluations=evaluation_copy[:100], - ResultToken=result_token, - TestMode=test_mode, - ) - del evaluation_copy[:100] - - # Used solely for RDK test to be able to test Lambda function - return evaluations - - -def is_internal_error(exception): - return ( - (not isinstance(exception, botocore.exceptions.ClientError)) - or exception.response["Error"]["Code"].startswith("5") - or "InternalError" in exception.response["Error"]["Code"] - or "ServiceError" in exception.response["Error"]["Code"] - ) - - -def build_internal_error_response(internal_error_message, internal_error_details=None): - return build_error_response( - internal_error_message, internal_error_details, "InternalError", "InternalError" - ) - - -def build_error_response( - internal_error_message, - internal_error_details=None, - customer_error_code=None, - customer_error_message=None, -): - error_response = { - "internalErrorMessage": internal_error_message, - "internalErrorDetails": internal_error_details, - "customerErrorMessage": customer_error_message, - "customerErrorCode": customer_error_code, - } - print(error_response) - return error_response diff --git a/samples/sample-rdk-rules/config-rules/EC2_CHECKS_TERMINATION_PROTECTION_ADF/EC2_CHECKS_TERMINATION_PROTECTION_ADF_test.py b/samples/sample-rdk-rules/config-rules/EC2_CHECKS_TERMINATION_PROTECTION_ADF/EC2_CHECKS_TERMINATION_PROTECTION_ADF_test.py deleted file mode 100644 index c158fee04..000000000 --- a/samples/sample-rdk-rules/config-rules/EC2_CHECKS_TERMINATION_PROTECTION_ADF/EC2_CHECKS_TERMINATION_PROTECTION_ADF_test.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. -# SPDX-License-Identifier: Apache-2.0 - -import sys -import unittest -from unittest.mock import MagicMock -import botocore - -############## -# Parameters # -############## - -# Define the default resource to report to Config Rules -DEFAULT_RESOURCE_TYPE = 'AWS::::Account' - -############# -# Main Code # -############# - -CONFIG_CLIENT_MOCK = MagicMock() -STS_CLIENT_MOCK = MagicMock() - -class Boto3Mock(): - @staticmethod - def client(client_name, *args, **kwargs): - if client_name == 'config': - return CONFIG_CLIENT_MOCK - if client_name == 'sts': - return STS_CLIENT_MOCK - raise Exception("Attempting to create an unknown client") - -sys.modules['boto3'] = Boto3Mock() - -RULE = __import__('EC2_CHECKS_TERMINATION_PROTECTION_ADF') - -class ComplianceTest(unittest.TestCase): - - rule_parameters = '{"SomeParameterKey":"SomeParameterValue","SomeParameterKey2":"SomeParameterValue2"}' - - invoking_event_iam_role_sample = '{"configurationItem":{"relatedEvents":[],"relationships":[],"configuration":{},"tags":{},"configurationItemCaptureTime":"2018-07-02T03:37:52.418Z","awsAccountId":"123456789012","configurationItemStatus":"ResourceDiscovered","resourceType":"AWS::IAM::Role","resourceId":"some-resource-id","resourceName":"some-resource-name","ARN":"some-arn"},"notificationCreationTime":"2018-07-02T23:05:34.445Z","messageType":"ConfigurationItemChangeNotification"}' - - def setUp(self): - pass - - def test_sample(self): - self.assertTrue(True) - -#################### -# Helper Functions # -#################### - -def build_lambda_configurationchange_event(invoking_event, rule_parameters=None): - event_to_return = { - 'configRuleName':'myRule', - 'executionRoleArn':'roleArn', - 'eventLeftScope': False, - 'invokingEvent': invoking_event, - 'accountId': '123456789012', - 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-abc', - 'resultToken':'token' - } - if rule_parameters: - event_to_return['ruleParameters'] = rule_parameters - return event_to_return - -def build_lambda_scheduled_event(rule_parameters=None): - invoking_event = '{"messageType":"ScheduledNotification","notificationCreationTime":"2017-12-23T22:11:18.158Z"}' - event_to_return = { - 'configRuleName':'myRule', - 'executionRoleArn':'roleArn', - 'eventLeftScope': False, - 'invokingEvent': invoking_event, - 'accountId': '123456789012', - 'configRuleArn': 'arn:aws:config:us-east-1:123456789012:config-rule/config-rule-abc', - 'resultToken':'token' - } - if rule_parameters: - event_to_return['ruleParameters'] = rule_parameters - return event_to_return - -def build_expected_response(compliance_type, compliance_resource_id, compliance_resource_type=DEFAULT_RESOURCE_TYPE, annotation=None): - if not annotation: - return { - 'ComplianceType': compliance_type, - 'ComplianceResourceId': compliance_resource_id, - 'ComplianceResourceType': compliance_resource_type - } - return { - 'ComplianceType': compliance_type, - 'ComplianceResourceId': compliance_resource_id, - 'ComplianceResourceType': compliance_resource_type, - 'Annotation': annotation - } - -def assert_successful_evaluation(test_class, response, resp_expected, evaluations_count=1): - if isinstance(response, dict): - test_class.assertEquals(resp_expected['ComplianceResourceType'], response['ComplianceResourceType']) - test_class.assertEquals(resp_expected['ComplianceResourceId'], response['ComplianceResourceId']) - test_class.assertEquals(resp_expected['ComplianceType'], response['ComplianceType']) - test_class.assertTrue(response['OrderingTimestamp']) - if 'Annotation' in resp_expected or 'Annotation' in response: - test_class.assertEquals(resp_expected['Annotation'], response['Annotation']) - elif isinstance(response, list): - test_class.assertEquals(evaluations_count, len(response)) - for i, response_expected in enumerate(resp_expected): - test_class.assertEquals(response_expected['ComplianceResourceType'], response[i]['ComplianceResourceType']) - test_class.assertEquals(response_expected['ComplianceResourceId'], response[i]['ComplianceResourceId']) - test_class.assertEquals(response_expected['ComplianceType'], response[i]['ComplianceType']) - test_class.assertTrue(response[i]['OrderingTimestamp']) - if 'Annotation' in response_expected or 'Annotation' in response[i]: - test_class.assertEquals(response_expected['Annotation'], response[i]['Annotation']) - -def assert_customer_error_response(test_class, response, customer_error_code=None, customer_error_message=None): - if customer_error_code: - test_class.assertEqual(customer_error_code, response['customerErrorCode']) - if customer_error_message: - test_class.assertEqual(customer_error_message, response['customerErrorMessage']) - test_class.assertTrue(response['customerErrorCode']) - test_class.assertTrue(response['customerErrorMessage']) - if "internalErrorMessage" in response: - test_class.assertTrue(response['internalErrorMessage']) - if "internalErrorDetails" in response: - test_class.assertTrue(response['internalErrorDetails']) - -def sts_mock(): - assume_role_response = { - "Credentials": { - "AccessKeyId": "string", - "SecretAccessKey": "string", - "SessionToken": "string"}} - STS_CLIENT_MOCK.reset_mock(return_value=True) - STS_CLIENT_MOCK.assume_role = MagicMock(return_value=assume_role_response) - -################## -# Common Testing # -################## - -class TestStsErrors(unittest.TestCase): - - def test_sts_unknown_error(self): - RULE.ASSUME_ROLE_MODE = True - RULE.evaluate_parameters = MagicMock(return_value=True) - STS_CLIENT_MOCK.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( - {'Error': {'Code': 'unknown-code', 'Message': 'unknown-message'}}, 'operation')) - response = RULE.lambda_handler(build_lambda_configurationchange_event('{}'), {}) - assert_customer_error_response( - self, response, 'InternalError', 'InternalError') - - def test_sts_access_denied(self): - RULE.ASSUME_ROLE_MODE = True - RULE.evaluate_parameters = MagicMock(return_value=True) - STS_CLIENT_MOCK.assume_role = MagicMock(side_effect=botocore.exceptions.ClientError( - {'Error': {'Code': 'AccessDenied', 'Message': 'access-denied'}}, 'operation')) - response = RULE.lambda_handler(build_lambda_configurationchange_event('{}'), {}) - assert_customer_error_response( - self, response, 'AccessDenied', 'AWS Config does not have permission to assume the IAM role.') diff --git a/samples/sample-rdk-rules/config-rules/EC2_CHECKS_TERMINATION_PROTECTION_ADF/parameters.json b/samples/sample-rdk-rules/config-rules/EC2_CHECKS_TERMINATION_PROTECTION_ADF/parameters.json deleted file mode 100644 index 1b8ce5dae..000000000 --- a/samples/sample-rdk-rules/config-rules/EC2_CHECKS_TERMINATION_PROTECTION_ADF/parameters.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Version": "1.0", - "Parameters": { - "RuleName": "EC2_CHECKS_TERMINATION_PROTECTION_ADF", - "Description": "EC2_CHECKS_TERMINATION_PROTECTION_ADF", - "SourceRuntime": "python3.12", - "CodeKey": "EC2_CHECKS_TERMINATION_PROTECTION_ADFeu-central-1.zip", - "InputParameters": "{}", - "OptionalParameters": "{}", - "SourceEvents": "AWS::EC2::Instance", - "SourcePeriodic": "One_Hour" - }, - "Tags": "[]" -} diff --git a/samples/sample-rdk-rules/lambda_helper.py b/samples/sample-rdk-rules/lambda_helper.py deleted file mode 100644 index b2aa42020..000000000 --- a/samples/sample-rdk-rules/lambda_helper.py +++ /dev/null @@ -1,147 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. -# SPDX-License-Identifier: MIT-0 - -import argparse -import json -import os -import shutil -import uuid -import sys -import logging -import boto3 -from s3 import S3 -from pathlib import Path - -deployment_account_region = os.environ.get("AWS_REGION") -project_root = os.path.dirname(__file__) -s3_rdk_assets_prefix = "rdk_assets" -config_rules_dir = "config-rules" -templates_dir = "templates" -config_rules_root = os.path.join(project_root, "config-rules") -templates_root = os.path.join(project_root, templates_dir) - - -def load_json_file(file: str) -> dict: - try: - with open(f"{file}", "r", ecoding="utf-8") as file: - return json.load(file) - except FileNotFoundError: - logging.exception(f"File {file} not found.") - sys.exit(1) - - -def replace_rule_name_and_load(file: str, rule_name:str, rule_name_stripped:str) -> dict: - try: - with open(file, 'r', encoding="utf-8") as f: - content = f.read().replace("RuleNameStripped", rule_name_stripped) - content = content.replace("RuleName", rule_name) - return json.loads(content) - except FileNotFoundError: - logging.exception(f"File {file} not found.") - sys.exit(1) - - -def clean_up_template(file: str): - if os.path.exists(file): - os.remove(file) - - -def get_template_skeleton(shared_modules_bucket: str) -> dict: - #get skeleton - parameters = load_json_file(Path(templates_root).joinpath("parameters.json")) - parameters["SourceBucketFolder"]["Default"] = s3_rdk_assets_prefix - parameters["SourceBucket"]["Default"] = shared_modules_bucket - - #get parameters - skeleton = load_json_file(Path(templates_root).joinpath("skeleton.json")) - - skeleton["Parameters"] = parameters - - return skeleton - - -def add_lambda_to_template_by_rule(template:dict, config_rule_dir: str, rule_name:str, s3_asset_key:str) -> dict: - parameter_file = Path(config_rule_dir).joinpath("parameters.json") - parameter_content = load_json_file(parameter_file) - rule_name_stripped = rule_name.replace("_", "") - runtime = parameter_content.get('Parameters').get('SourceRuntime') - - lambda_role = replace_rule_name_and_load(Path(templates_root).joinpath("lambda-role.json"), rule_name, rule_name_stripped) - template["Resources"][f"{rule_name_stripped}LambdaRole"] = lambda_role - - lambda_function = replace_rule_name_and_load(Path(templates_root).joinpath("lambda-function.json"), rule_name, rule_name_stripped) - lambda_function["Properties"]["Code"]["S3Key"] = s3_asset_key - lambda_function["Properties"]["Runtime"] = runtime - template["Resources"][f"{rule_name_stripped}LambdaFunction"] = lambda_function - - lambda_permission = replace_rule_name_and_load(Path(templates_root).joinpath("lambda-permission.json"), rule_name, rule_name_stripped) - template["Resources"][f"{rule_name_stripped}LambdaPermissions"] = lambda_permission - - return template - - -def write_template(template:dict, file_name:str): - with open(file_name, "a") as file: - json.dump(template, file, indent=4) - - -def main(shared_modules_bucket: str): - s3 = S3(deployment_account_region, shared_modules_bucket) - clean_up_template(template_name) - template = get_template_skeleton(shared_modules_bucket) - - config_rules_dirs = [x for x in Path(config_rules_root).iterdir() if x.is_dir()] - for config_rule_dir in config_rules_dirs: - rule_name = config_rule_dir.name.replace("-", "_") - - logging.info(f'Zipping rule {config_rule_dir.name}') - - file_asset_path = shutil.make_archive( - Path(config_rule_dir).joinpath(config_rule_dir.name), - "zip", - config_rule_dir - ) - unique_id = uuid.uuid4() - if asset_folder: - s3_asset_key =f'{asset_folder}/{rule_name}/{rule_name}-{unique_id}.zip' - - logging.info(f'Uploading rule {config_rule_dir.name}') - uploaded_asset_path = s3.put_object( - s3_asset_key, - file_asset_path, - style="s3-url", - pre_check=True, - ) - print(f"uploaded to {uploaded_asset_path}") - clean_up_template(file_asset_path) - print(f"Creating template for {rule_name}") - template = add_lambda_to_template_by_rule(template, config_rule_dir, rule_name, s3_asset_key) - - write_template(template, template_name) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--asset-folder', required=False, help='[optional] Asset folder in the ADF bucket') - parser.add_argument('--template-name', required=True, help='Name for the generated template name') - parser.add_argument( - "-r", - "--region", - ) - args = parser.parse_args() - - target_region = args.region if args.region else deployment_account_region - parameter_store = boto3.client('ssm') - - template_name = args.template_name - - bucket_path_ssm = f"/cross_region/s3_regional_bucket/{target_region}" - res = parameter_store.get_parameter(Name=bucket_path_ssm) - - shared_modules_bucket = res['Parameter']['Value'] - asset_folder = args.asset_folder if args.asset_folder else s3_rdk_assets_prefix - # If remove trailing slash if exists to be - if asset_folder and asset_folder.endswith('/'): - asset_folder = asset_folder[:-1] - - main(shared_modules_bucket) diff --git a/samples/sample-rdk-rules/meta/custom-configs.png b/samples/sample-rdk-rules/meta/custom-configs.png deleted file mode 100644 index c52993c21..000000000 Binary files a/samples/sample-rdk-rules/meta/custom-configs.png and /dev/null differ diff --git a/samples/sample-rdk-rules/meta/lambda-account-id-trusted-entity.png b/samples/sample-rdk-rules/meta/lambda-account-id-trusted-entity.png deleted file mode 100644 index c7ccd341b..000000000 Binary files a/samples/sample-rdk-rules/meta/lambda-account-id-trusted-entity.png and /dev/null differ diff --git a/samples/sample-rdk-rules/params/global.json b/samples/sample-rdk-rules/params/global.json deleted file mode 100644 index b659fa258..000000000 --- a/samples/sample-rdk-rules/params/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Parameters": { - "LambdaAccountId": "1111111111", - "ConfigRoleArnToAssume": "config-role-arn-here | ex: arn:aws:iam::*:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig" - } -} diff --git a/samples/sample-rdk-rules/requirements.txt b/samples/sample-rdk-rules/requirements.txt deleted file mode 100644 index e34a0b267..000000000 --- a/samples/sample-rdk-rules/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -s3==3.0.0 -boto3==1.34.80 -argparse==1.4.0 diff --git a/samples/sample-rdk-rules/templates/lambda-function.json b/samples/sample-rdk-rules/templates/lambda-function.json deleted file mode 100644 index d8dabb960..000000000 --- a/samples/sample-rdk-rules/templates/lambda-function.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "Type": "AWS::Lambda::Function", - "DependsOn": "RuleNameStrippedLambdaRole", - "Properties": { - "FunctionName": "RDK-Rule-Function-RuleNameStripped", - "Code": { - "S3Bucket": { - "Ref": "SourceBucket" - }, - "S3Key": "RuleName/RuleName.zip" - }, - "Description": "Function for AWS Config Rule RuleName", - "Handler": "RuleName.lambda_handler", - "MemorySize": "256", - "Role": { - "Fn::GetAtt": [ - "RuleNameStrippedLambdaRole", - "Arn" - ] - }, - "Runtime": "RuleRuntime", - "Timeout": "60", - "Tags": [] - } -} diff --git a/samples/sample-rdk-rules/templates/lambda-permission.json b/samples/sample-rdk-rules/templates/lambda-permission.json deleted file mode 100644 index fa151fe3f..000000000 --- a/samples/sample-rdk-rules/templates/lambda-permission.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Type": "AWS::Lambda::Permission", - "DependsOn": "RuleNameStrippedLambdaFunction", - "Properties": { - "FunctionName": { - "Fn::GetAtt": [ - "RuleNameStrippedLambdaFunction", - "Arn" - ] - }, - "Action": "lambda:InvokeFunction", - "Principal": "config.amazonaws.com" - } -} diff --git a/samples/sample-rdk-rules/templates/lambda-role.json b/samples/sample-rdk-rules/templates/lambda-role.json deleted file mode 100644 index ccc249765..000000000 --- a/samples/sample-rdk-rules/templates/lambda-role.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "Type": "AWS::IAM::Role", - "Properties": { - "Path": "/rdk/", - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "AllowLambdaAssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - }, - "Action": "sts:AssumeRole" - } - ] - }, - "Policies": [ - { - "PolicyName": "ConfigRulePolicy", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "1", - "Action": ["s3:GetObject"], - "Effect": "Allow", - "Resource": { - "Fn::Sub": "arn:${AWS::Partition}:s3:::${SourceBucket}/${SourceBucketFolder}/*" - } - }, - { - "Sid": "2", - "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", - "logs:DescribeLogStreams" - ], - "Effect": "Allow", - "Resource": "*" - }, - { - "Sid": "3", - "Action": ["config:PutEvaluations"], - "Effect": "Allow", - "Resource": "*" - }, - { - "Sid": "4", - "Action": [ - "iam:List*", - "iam:Describe*", - "iam:Get*" - ], - "Effect": "Allow", - "Resource": "*" - }, - { - "Sid": "5", - "Action": ["sts:AssumeRole"], - "Effect": "Allow", - "Resource": "${ConfigRoleArnToAssume}" - } - ] - } - } - ], - "ManagedPolicyArns": [ - { - "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess" - } - ] - } -} diff --git a/samples/sample-rdk-rules/templates/parameters.json b/samples/sample-rdk-rules/templates/parameters.json deleted file mode 100644 index 91bb41969..000000000 --- a/samples/sample-rdk-rules/templates/parameters.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "SourceBucket": { - "Description": "Name of the S3 bucket that you have stored the rule zip files in.", - "Type": "String", - "MinLength": "1", - "MaxLength": "255" - }, - "SourceBucketFolder": { - "Description": "Folder in the s3 bucket all the lambda function code stored", - "Type": "String", - "MinLength": "1", - "MaxLength": "255" - }, - "LambdaAccountId": { - "Description": "Account ID that contains Lambda functions for Config Rules.", - "Type": "String", - "MinLength": "12", - "MaxLength": "12" - }, - "ConfigRoleArnToAssume": { - "Description": "Lambda function required to assume this config role in target accounts to put evaluations", - "Type": "String" - } -} diff --git a/samples/sample-rdk-rules/templates/skeleton.json b/samples/sample-rdk-rules/templates/skeleton.json deleted file mode 100644 index 5c8a2f2e9..000000000 --- a/samples/sample-rdk-rules/templates/skeleton.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Description": "AWS CloudFormation template to create Lambda functions for backing custom AWS Config rules. You will be billed for the AWS resources used if you create a stack from this template.", - "Resources": {} -}