diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index c3684525..a3b5dd56 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -9,56 +9,6 @@ on: - main jobs: - ## TODO: Enable this once the repo is totally formatted to standard. - # lint-style: - # name: Linting and Styling - # runs-on: ubuntu-latest - # steps: - # - name: Checkout Source - # uses: actions/checkout@v3 - # with: - # fetch-depth: 0 - - # - name: Setup Dependencies - # uses: './.github/actions/dep-setup' - # with: - # python-version: '3.10' - - # - name: Run Styling Enforcement - # shell: bash - # run: poetry poe check - - # # TODO: As soon as the repo is in a state to enable this, we'll do so. - # - name: Run Style Linting Enforcement - # shell: bash - # run: poetry poe lint - - ## TODO: Enable unit tests via GH Actions when unit tests are fixed and migrated to pytest. - # unit-tests: - # name: Run Unit Tests - # strategy: - # matrix: - # version: ['3.7', '3.8', '3.9', '3.10', '3.11'] - # os: [ubuntu-latest] - # runs-on: ${{ matrix.os }} - # steps: - # - name: Checkout Source - # uses: actions/checkout@v3 - # with: - # fetch-depth: 0 - - # - name: Setup Dependencies - # uses: './.github/actions/dep-setup' - # with: - # python-version: '${{ matrix.version }}' - - # - name: Run Tests - # shell: bash - # run: poetry poe test - - # - name: Codecov - # uses: codecov/codecov-action@v3 - security: name: Run Security Checks runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 9630f11d..1664c6b2 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,7 @@ API_GW_NOT_EDGE_OPTIMISED/ manageTest/ football/ + +terraform.tfvars +**/.terraform +.terraform.lock.hcl \ No newline at end of file diff --git a/docs/commands/export.md b/docs/commands/export.md index 2165b48e..a67758ba 100644 --- a/docs/commands/export.md +++ b/docs/commands/export.md @@ -1,19 +1,36 @@ # Export -This command will export the specified Rule(s) to the terraform file, it -supports the terraform versions 0.11 and 0.12. - -The `--format` flag can be used to specify export format, currently it -supports only terraform. The `--version` flag can be used to specify the -terraform version. The `--rdklib-layer-arn` flag can be used for -attaching Lambda Layer ARN that contains the desired rdklib. Note that -Lambda Layers are region-specific. The `--lambda-role-arn` flag can be -used for assigning existing iam role to all Lambda functions created for -Custom Config Rules. The `--lambda-layers` flag can be used for -attaching a comma-separated list of Lambda Layer ARNs to deploy with -your Lambda function(s). The `--lambda-subnets` flag can be used for -attaching a comma-separated list of Subnets to deploy your Lambda -function(s). The `--lambda-security-groups` flag can be used for -attaching a comma-separated list of Security Groups to deploy with your -Lambda function(s). The `--lambda-timeout` flag can be used for -specifying the timeout associated to the lambda function +This command will export the specified Rule(s) to Terraform. + +It supports Terraform version 1.x (older TF version support is deprecated). + +In order to reduce repeated code, the 1.x Terraform export will generate a module invocation that passes appropriate arguments to the source module. + +The source module will live in `rdk/template/terraform/1.x/rdk_module` and will be exported by default (though you can also point it to a different module folder if you want to reduce repeated code). + +The intended usage in CI/CD pipelines looks something like this: +```bash +# Assuming a folder of rules, with one subfolder per rule, containing: +# parameters.json +# rule.py +# rule_test.py + +TF_STATE_BUCKET=my-bucket +rdk export -a --add-terragrunt-file --backend-bucket $TF_STATE_BUCKET # Creates a TF manifest, terragrunt placeholder file, and backend in each rule folder in your rules directory +terragrunt run-all apply +``` + +# Arguments + +- The `--format` flag can be used to specify export format, currently it supports only Terraform. +- The `--output-version` flag can be used to specify the Terraform version. Currently, only "1.x" is supported. +- The `--rdklib-layer-arn` flag can be used for attaching Lambda Layer ARN that contains the desired `rdklib` layer. Note that Lambda Layers are region-specific. +- The `--lambda-role-arn` flag can be used for assigning existing iam role to all Lambda functions created for Custom Config Rules. +- The `--lambda-layers` flag can be used for attaching a comma-separated list of Lambda Layer ARNs to deploy with your Lambda function(s). +- The `--lambda-subnets` flag can be used for attaching a comma-separated list of Subnets to deploy your Lambda function(s). +- The `--lambda-security-groups` flag can be used for attaching a comma-separated list of Security Groups to deploy with your Lambda function(s). +- The `--lambda-timeout` flag can be used for specifying the timeout associated to the lambda function +- The `--copy-terraform-module` flag will copy the `rdk_module` folder into your rule directory. +- The `custom-module-source-location` flag will set the exported TF module invocation to be sourced from the location you specify. This is useful if you modify the module or want to source it from a central location. For example, you could pass the module call to a source that deploys an Config Organization rule. +- The `backend-bucket` flag will create a `backend.tf` file in the rule directory, pointing to the specified backend S3 bucket. The key for the state file will be `rdk_modules/`. +- The `add-terragrunt-file` flag will create a `terragrunt.hcl` file in the rule directory. This is used to indicate to `terragrunt` that the module should be included in `terragrunt` automations like `run-all`. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 9dedd35f..1d607e84 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,11 +6,6 @@ theme: primary: navy plugins: - search - # TODO: Enable this if/when docstrings are expanded in the core rdk module. - # - mkdocstrings: - # handlers: - # python: - # paths: [rdk] markdown_extensions: - markdown_include.include: base_path: . diff --git a/pyproject.toml b/pyproject.toml index ff8aa02a..a930d9b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. [tool.poetry] name = "rdk" -version = "0.17.12" +version = "0.17.13" description = "Rule Development Kit CLI for AWS Config" authors = [ "AWS RDK Maintainers ", diff --git a/rdk/__init__.py b/rdk/__init__.py index 5d7d9c39..d26265e9 100644 --- a/rdk/__init__.py +++ b/rdk/__init__.py @@ -6,4 +6,4 @@ # # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -MY_VERSION = "0.17.12" +MY_VERSION = "0.17.13" diff --git a/rdk/_export.py b/rdk/_export.py new file mode 100644 index 00000000..dc3d80f0 --- /dev/null +++ b/rdk/_export.py @@ -0,0 +1,298 @@ +import argparse +import json +import os +import shutil +import subprocess +import sys +import tempfile + +from os import path + +# This is the first method to be externalized from rdk.py! + +rules_dir = "" # This is effectively a placeholder value + + +def get_export_parser(): + parser = argparse.ArgumentParser( + prog="rdk export", + description="Used to export the Config Rule to terraform file.", + ) + parser.add_argument( + "rulename", + metavar="", + nargs="*", + help="Rule name(s) to export to a file.", + ) + parser.add_argument( + "-s", + "--rulesets", + required=False, + help="comma-delimited list of RuleSet names", + ) + parser.add_argument( + "--all", + "-a", + action="store_true", + help="All rules in the working directory will be deployed.", + ) + parser.add_argument( + "--custom-code-bucket", + required=False, + help="[optional] The code bucket to which you want to upload a copy of your RDK rule. If not specified, will use the default code bucket for the current region", + ) + parser.add_argument( + "--lambda-layers", + required=False, + help="[optional] Comma-separated list of Lambda Layer ARNs to deploy with your Lambda function(s).", + ) + parser.add_argument( + "--lambda-subnets", + required=False, + help="[optional] Comma-separated list of Subnets to deploy your Lambda function(s). If specified, you must also specify --lambda-security-groups.", + ) + parser.add_argument( + "--lambda-security-groups", + required=False, + help="[optional] Comma-separated list of Security Groups to deploy with your Lambda function(s). If specified, you must also specify --lambda-subnets.", + ) + parser.add_argument( + "--lambda-timeout", + required=False, + default=60, + help="[optional] Timeout (in seconds) for the lambda function", + type=str, + ) + parser.add_argument( + "--lambda-role-arn", + required=False, + help="[optional] Assign existing iam role to lambda functions. If omitted, new lambda role will be created.", + ) + parser.add_argument( + "--lambda-role-name", + required=False, + help="[optional] Assign existing iam role to lambda functions. If added, will look for a lambda role in the current account with the given name", + ) + parser.add_argument( + "--rdklib-layer-arn", + required=False, + help="[optional] Lambda Layer ARN that contains the desired rdklib. Note that Lambda Layers are region-specific.", + ) + parser.add_argument( + "-o", + "--output-version", + # required=True, + help="The Terraform version to use when outputting the export", + choices=["1.x", "1.x_organization"], + default="1.x", + ) + parser.add_argument( + "-f", + "--format", + # required=True, + help="Export Format", + choices=["terraform"], + default="terraform", + ) + parser.add_argument( + "-g", + "--generated-lambda-layer", + required=False, + action="store_true", + help="[optional] Forces rdk deploy to use the Python lambda layer generated by rdk init --generate-lambda-layer", + ) + parser.add_argument( + "--custom-layer-name", + required=False, + help='[optional] To use with --generated-lambda-layer, forces the flag to look for a specific lambda-layer name. If omitted, "rdklib-layer" will be used', + ) + parser.add_argument( + "--copy-terraform-module", + required=False, + action="store_true", + help="[optional] Copies the terraform module to the current directory", + ) + + return parser + + +def package_function_code(rdk_instance, rule_name, params): + class_name = rdk_instance.__class__.__name__ + my_session = getattr(rdk_instance, f"_{class_name}__get_boto_session")() + if params["SourceRuntime"] == "java8": + # Do java build and package. + print("Running Gradle Build for " + rule_name) + working_dir = os.path.join(os.getcwd(), rules_dir, rule_name) + command = ["gradle", "build"] + subprocess.call(command, cwd=working_dir) + + # set source as distribution zip + s3_src = os.path.join( + os.getcwd(), + rules_dir, + rule_name, + "build", + "distributions", + rule_name + ".zip", + ) + + else: + print("Zipping " + rule_name) + # Remove old zip file if it already exists + package_file_dst = os.path.join(rule_name, rule_name + ".zip") + getattr(rdk_instance, f"_{class_name}__delete_package_file")(package_file_dst) + + # zip rule code files and upload to s3 bucket + s3_src_dir = os.path.join(os.getcwd(), rules_dir, rule_name) + tmp_src = shutil.make_archive( + os.path.join(tempfile.gettempdir(), rule_name + my_session.region_name), + "zip", + s3_src_dir, + ) + if not (os.path.exists(package_file_dst)): + shutil.copy(tmp_src, package_file_dst) + s3_src = os.path.abspath(package_file_dst) + getattr(rdk_instance, f"_{class_name}__delete_package_file")(tmp_src) + + s3_dst = "/".join((rule_name, rule_name + ".zip")) + + print("Zipping complete.") + + return s3_dst + + +def parse_export_args(rdk_instance): + rdk_instance.args = get_export_parser().parse_args(rdk_instance.args.command_args, rdk_instance.args) + + if bool(rdk_instance.args.lambda_security_groups) != bool(rdk_instance.args.lambda_subnets): + print("You must specify both lambda-security-groups and lambda-subnets, or neither.") + sys.exit(1) + + # Check rule names to make sure none are too long. This is needed to catch Rules created before length constraint was added. + if rdk_instance.args.rulename: + for name in rdk_instance.args.rulename: + if len(name) > 128: + print( + f"Error: Found Rule with name over 128 characters: {name} \n Recreate the Rule with a shorter name." + ) + sys.exit(1) + + +def export(rdk_instance): + # Construct mangled names for private methods + class_name = rdk_instance.__class__.__name__ + + # Gather CLI args + rdk_instance.parse_export_args() + + if rdk_instance.args.output_version == "1.x": + module_name = "rdk_module" + elif rdk_instance.args.output_version == "1.x_organization": + module_name = "rdk_organization_module" + else: + raise ValueError("Invalid output version specified") + + # get the rule names + # getattr is used to reference private class names from external modules + rule_names = getattr(rdk_instance, f"_{class_name}__get_rule_list_for_command")("export") + + # run the export code + for rule_name in rule_names: + print(f"Running export of {rule_name}") + rule_params, _ = getattr(rdk_instance, f"_{class_name}__get_rule_parameters")(rule_name) + + if "SourceIdentifier" in rule_params: + print(f"Found Managed Rule {rule_name}, Ignored.") + print("Export supports only Custom Rules.") + continue + + source_events = rule_params.get("SourceEvents", []) + source_periodic = rule_params.get("SourcePeriodic", "") + combined_input_parameters = rule_params.get("InputParameters", {}) + + if "OptionalParameters" in rule_params: + # Add non-empty optional parameters + optional_parameters_json = json.loads(rule_params["OptionalParameters"]) + for key, value in optional_parameters_json.items(): + if value: + combined_input_parameters.update({key: value}) + + print(f"Found Custom Rule {rule_name}.") + + layers = [] + my_session = getattr(rdk_instance, f"_{class_name}__get_boto_session")() + layers = getattr(rdk_instance, f"_{class_name}__get_lambda_layers")(my_session, rdk_instance.args, rule_params) + + if rdk_instance.args.lambda_layers: + additional_layers = rdk_instance.args.lambda_layers.split(",") + layers.extend(additional_layers) + + subnet_ids = [] + security_group_ids = [] + if rdk_instance.args.lambda_security_groups: + security_group_ids = rdk_instance.args.lambda_security_groups.split(",") + + if rdk_instance.args.lambda_subnets: + subnet_ids = rdk_instance.args.lambda_subnets.split(",") + + lambda_role_arn = "" + if rdk_instance.args.lambda_role_arn: + print("Existing IAM Role provided: " + rdk_instance.args.lambda_role_arn) + lambda_role_arn = rdk_instance.args.lambda_role_arn + + if rdk_instance.args.custom_code_bucket: + code_bucket = rdk_instance.args.custom_code_bucket + else: + code_bucket = "config-rule-code-bucket-" + rdk_instance.account_id + "-" + rdk_instance.region_name + + my_params = { + "rule_name": rule_name, + "rule_lambda_name": getattr(rdk_instance, f"_{class_name}__get_lambda_name")(rule_name, rule_params), + "source_bucket": code_bucket, + "source_runtime": getattr(rdk_instance, f"_{class_name}__get_runtime_string")(rule_params), + "source_events": source_events, + "source_periodic": source_periodic, + "source_input_parameters": json.dumps(combined_input_parameters)[1:-1], # Remove first and last quote chars + "source_handler": getattr(rdk_instance, f"_{class_name}__get_handler")(rule_name, rule_params), + "subnet_ids": subnet_ids, + "security_group_ids": security_group_ids, + "lambda_layers": layers, + "lambda_role_arn": lambda_role_arn, + "lambda_timeout": str(rdk_instance.args.lambda_timeout), + } + longest_param_length = max(len(x) for x in my_params.keys() if bool(my_params.get(x, False))) + + # Write out the file with the variable values + params_file_path = os.path.join( + os.getcwd(), + rules_dir, + rule_name, + f"{rule_name}.tf", + ) + with open(params_file_path, "w") as f: + f.write(f'module "{rule_name}" {{\n') + f.write(f' {"source".ljust(longest_param_length)} = "./{module_name}"\n') + for param in my_params.keys(): + if not my_params[param]: + continue # Skip empty values for clarity + padded_param = param.ljust(longest_param_length) # Pad to meet TF formatting standards + f.write(f' {padded_param} = "{my_params[param]}"\n') + f.write("}\n") + + # If requested, copy the Terraform module to the rule directory + if rdk_instance.args.copy_terraform_module: + print(f"Exporting Terraform module {module_name}") + tf_module_dir = os.path.join( + path.dirname(__file__), + "template", + rdk_instance.args.format, + rdk_instance.args.output_version, + module_name, + ) + rule_dir = os.path.join(os.getcwd(), rules_dir, rule_name, module_name) + shutil.copytree( + tf_module_dir, + rule_dir, + dirs_exist_ok=True, + ) + print("Export completed. This generated the .tf files required to deploy this rule.") diff --git a/rdk/rdk.py b/rdk/rdk.py index 35c0675d..0b822c43 100644 --- a/rdk/rdk.py +++ b/rdk/rdk.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and limitations under the License. import argparse import base64 +import boto3 +import botocore import fileinput import fnmatch import json @@ -25,15 +27,14 @@ import tempfile import time import unittest -from boto3 import Session +import uuid import yaml + +from boto3 import Session +from botocore.exceptions import ClientError, EndpointConnectionError from builtins import input from datetime import datetime from os import path -import uuid -import boto3 -import botocore -from botocore.exceptions import ClientError, EndpointConnectionError # sphinx-argparse is a delight. try: @@ -619,85 +620,6 @@ def get_deployment_organization_parser(ForceArgument=False, Command="deploy-orga return parser -def get_export_parser(ForceArgument=False, Command="export"): - parser = argparse.ArgumentParser( - prog="rdk " + Command, - description="Used to " + Command + " the Config Rule to terraform file.", - ) - parser.add_argument( - "rulename", - metavar="", - nargs="*", - help="Rule name(s) to export to a file.", - ) - parser.add_argument("-s", "--rulesets", required=False, help="comma-delimited list of RuleSet names") - parser.add_argument( - "--all", - "-a", - action="store_true", - help="All rules in the working directory will be deployed.", - ) - parser.add_argument( - "--lambda-layers", - required=False, - help="[optional] Comma-separated list of Lambda Layer ARNs to deploy with your Lambda function(s).", - ) - parser.add_argument( - "--lambda-subnets", - required=False, - help="[optional] Comma-separated list of Subnets to deploy your Lambda function(s). If specified, you must also specify --lambda-security-groups.", - ) - parser.add_argument( - "--lambda-security-groups", - required=False, - help="[optional] Comma-separated list of Security Groups to deploy with your Lambda function(s). If specified, you must also specify --lambda-subnets.", - ) - parser.add_argument( - "--lambda-timeout", - required=False, - default=60, - help="[optional] Timeout (in seconds) for the lambda function", - type=str, - ) - parser.add_argument( - "--lambda-role-arn", - required=False, - help="[optional] Assign existing iam role to lambda functions. If omitted, new lambda role will be created.", - ) - parser.add_argument( - "--lambda-role-name", - required=False, - help="[optional] Assign existing iam role to lambda functions. If added, will look for a lambda role in the current account with the given name", - ) - parser.add_argument( - "--rdklib-layer-arn", - required=False, - help="[optional] Lambda Layer ARN that contains the desired rdklib. Note that Lambda Layers are region-specific.", - ) - parser.add_argument( - "-v", - "--version", - required=True, - help="Terraform version", - choices=["0.11", "0.12"], - ) - parser.add_argument("-f", "--format", required=True, help="Export Format", choices=["terraform"]) - parser.add_argument( - "-g", - "--generated-lambda-layer", - required=False, - action="store_true", - help="[optional] Forces rdk deploy to use the Python lambda layer generated by rdk init --generate-lambda-layer", - ) - parser.add_argument( - "--custom-layer-name", - required=False, - help='[optional] To use with --generated-lambda-layer, forces the flag to look for a specific lambda-layer name. If omitted, "rdklib-layer" will be used', - ) - - return parser - - def get_test_parser(command): parser = argparse.ArgumentParser(prog="rdk " + command, description="Used to run tests on your Config Rule code.") parser.add_argument( @@ -860,6 +782,13 @@ def run_multi_region(args): class rdk: def __init__(self, args): self.args = args + self.my_session = self.__get_boto_session() + identity_details = self.__get_caller_identity_details(self.my_session) + self.account_id = identity_details.get("account_id") # Useful for passing to externalized functions + self.region_name = self.my_session.region_name + + # Import external methods + from ._export import export, parse_export_args, package_function_code # Importing export feels natural! @staticmethod def get_command_parser(self): @@ -873,13 +802,14 @@ def process_command(self): def init(self): """ - This is a test. + This is the method containing the rdk init logic. + + This is NOT the class initializer!!! """ self.args = get_init_parser().parse_args(self.args.command_args, self.args) # create custom session based on whatever credentials are available to us - my_session = self.__get_boto_session() - + my_session = self.my_session print(f"[{my_session.region_name}]: Running init!") # Create our ConfigService client @@ -887,7 +817,7 @@ def init(self): # get accountID, AWS partition (e.g. aws or aws-us-gov), region (us-east-1, us-gov-west-1) identity_details = self.__get_caller_identity_details(my_session) - account_id = identity_details["account_id"] + account_id = self.account_id partition = identity_details["partition"] config_recorder_exists = False @@ -1392,7 +1322,6 @@ def modify(self): if not self.args.source_identifier and "SourceIdentifier" in old_params: self.args.source_identifier = old_params["SourceIdentifier"] - # TODO - is this appropriate? if not self.args.evaluation_mode and "EvaluationMode" in old_params: self.args.evaluation_mode = old_params["EvaluationMode"] @@ -1708,13 +1637,8 @@ def deploy(self): rule_params, cfn_tags = self.__get_rule_parameters(rule_name) # create CFN Parameters common for Managed and Custom - source_events = "NONE" - if "SourceEvents" in rule_params: - source_events = rule_params["SourceEvents"] - - source_periodic = "NONE" - if "SourcePeriodic" in rule_params: - source_periodic = rule_params["SourcePeriodic"] + source_events = rule_params.get("SourceEvents", "NONE") + source_periodic = rule_params.get("SourcePeriodic", "NONE") combined_input_parameters = {} if "InputParameters" in rule_params: @@ -1794,7 +1718,6 @@ def deploy(self): ] = ssm_automation if "IAM" in rule_params["SSMAutomation"]: print(f"[{my_session.region_name}]: Lets Build IAM Role and Policy") - # TODO Check For IAM Settings yaml_body["Resources"]["Remediation"]["Properties"]["Parameters"]["AutomationAssumeRole"][ "StaticValue" ]["Values"] = [ @@ -2085,7 +2008,6 @@ def deploy(self): ] = ssm_automation if "IAM" in rule_params["SSMAutomation"]: print("Lets Build IAM Role and Policy") - # TODO Check For IAM Settings yaml_body["Resources"]["Remediation"]["Properties"]["Parameters"]["AutomationAssumeRole"][ "StaticValue" ]["Values"] = [ @@ -2217,18 +2139,13 @@ def deploy_organization(self): sys.exit(1) # create CFN Parameters common for Managed and Custom - source_events = "NONE" if "Remediation" in rule_params: print( f"WARNING: Organization Rules with Remediation is not supported at the moment. {rule_name} will be deployed without auto-remediation." ) - if "SourceEvents" in rule_params: - source_events = rule_params["SourceEvents"] - - source_periodic = "NONE" - if "SourcePeriodic" in rule_params: - source_periodic = rule_params["SourcePeriodic"] + source_events = rule_params.get("SourceEvents", "NONE") + source_periodic = rule_params.get("SourcePeriodic", "NONE") combined_input_parameters = {} if "InputParameters" in rule_params: @@ -2543,114 +2460,6 @@ def deploy_organization(self): return 0 - def export(self): - self.__parse_export_args() - - # get the rule names - rule_names = self.__get_rule_list_for_command("export") - - # run the export code - print("Running export") - - for rule_name in rule_names: - rule_params, cfn_tags = self.__get_rule_parameters(rule_name) - - if "SourceIdentifier" in rule_params: - print("Found Managed Rule, Ignored.") - print("Export support only Custom Rules.") - continue - - source_events = [] - if "SourceEvents" in rule_params: - source_events = [rule_params["SourceEvents"]] - - source_periodic = "NONE" - if "SourcePeriodic" in rule_params: - source_periodic = rule_params["SourcePeriodic"] - - combined_input_parameters = {} - if "InputParameters" in rule_params: - combined_input_parameters.update(json.loads(rule_params["InputParameters"])) - - if "OptionalParameters" in rule_params: - # Remove empty parameters - keys_to_delete = [] - optional_parameters_json = json.loads(rule_params["OptionalParameters"]) - for key, value in optional_parameters_json.items(): - if not value: - keys_to_delete.append(key) - for key in keys_to_delete: - del optional_parameters_json[key] - combined_input_parameters.update(optional_parameters_json) - - print("Found Custom Rule.") - s3_src = "" - s3_dst = self.__package_function_code(rule_name, rule_params) - - layers = [] - rdk_lib_version = "0" - my_session = self.__get_boto_session() - layers = self.__get_lambda_layers(my_session, self.args, rule_params) - - if self.args.lambda_layers: - additional_layers = self.args.lambda_layers.split(",") - layers.extend(additional_layers) - - subnet_ids = [] - security_group_ids = [] - if self.args.lambda_security_groups: - security_group_ids = self.args.lambda_security_groups.split(",") - - if self.args.lambda_subnets: - subnet_ids = self.args.lambda_subnets.split(",") - - lambda_role_arn = "NONE" - if self.args.lambda_role_arn: - print("Existing IAM Role provided: " + self.args.lambda_role_arn) - lambda_role_arn = self.args.lambda_role_arn - - my_params = { - "rule_name": rule_name, - "rule_lambda_name": self.__get_lambda_name(rule_name, rule_params), - "source_runtime": self.__get_runtime_string(rule_params), - "source_events": source_events, - "source_periodic": source_periodic, - "source_input_parameters": json.dumps(combined_input_parameters), - "source_handler": self.__get_handler(rule_name, rule_params), - "subnet_ids": subnet_ids, - "security_group_ids": security_group_ids, - "lambda_layers": layers, - "lambda_role_arn": lambda_role_arn, - "lambda_timeout": str(self.args.lambda_timeout), - } - - params_file_path = os.path.join(os.getcwd(), rules_dir, rule_name, rule_name.lower() + ".tfvars.json") - parameters_file = open(params_file_path, "w") - json.dump(my_params, parameters_file, indent=4) - parameters_file.close() - # create json of CFN template - print(self.args.format + " version: " + self.args.version) - tf_file_body = os.path.join( - path.dirname(__file__), - "template", - self.args.format, - self.args.version, - "config_rule.tf", - ) - tf_file_path = os.path.join(os.getcwd(), rules_dir, rule_name, rule_name.lower() + "_rule.tf") - shutil.copy(tf_file_body, tf_file_path) - - variables_file_body = os.path.join( - path.dirname(__file__), - "template", - self.args.format, - self.args.version, - "variables.tf", - ) - variables_file_path = os.path.join(os.getcwd(), rules_dir, rule_name, rule_name.lower() + "_variables.tf") - shutil.copy(variables_file_body, variables_file_path) - print("Export completed.This will generate three .tf files.") - def test_local(self): print("Running local test!") tests_successful = True @@ -3630,65 +3439,6 @@ def __parse_deploy_organization_args(self, ForceArgument=False): sys.exit(1) self.args.excluded_accounts = self.args.excluded_accounts.split(",") - def __parse_export_args(self, ForceArgument=False): - self.args = get_export_parser(ForceArgument).parse_args(self.args.command_args, self.args) - - if bool(self.args.lambda_security_groups) != bool(self.args.lambda_subnets): - print("You must specify both lambda-security-groups and lambda-subnets, or neither.") - sys.exit(1) - - # Check rule names to make sure none are too long. This is needed to catch Rules created before length constraint was added. - if self.args.rulename: - for name in self.args.rulename: - if len(name) > 128: - print( - f"Error: Found Rule with name over 128 characters: {name} \n Recreate the Rule with a shorter name." - ) - sys.exit(1) - - def __package_function_code(self, rule_name, params): - my_session = self.__get_boto_session() - if params["SourceRuntime"] == "java8": - # Do java build and package. - print("Running Gradle Build for " + rule_name) - working_dir = os.path.join(os.getcwd(), rules_dir, rule_name) - command = ["gradle", "build"] - subprocess.call(command, cwd=working_dir) - - # set source as distribution zip - s3_src = os.path.join( - os.getcwd(), - rules_dir, - rule_name, - "build", - "distributions", - rule_name + ".zip", - ) - - else: - print("Zipping " + rule_name) - # Remove old zip file if it already exists - package_file_dst = os.path.join(rule_name, rule_name + ".zip") - self.__delete_package_file(package_file_dst) - - # zip rule code files and upload to s3 bucket - s3_src_dir = os.path.join(os.getcwd(), rules_dir, rule_name) - tmp_src = shutil.make_archive( - os.path.join(tempfile.gettempdir(), rule_name + my_session.region_name), - "zip", - s3_src_dir, - ) - if not (os.path.exists(package_file_dst)): - shutil.copy(tmp_src, package_file_dst) - s3_src = os.path.abspath(package_file_dst) - self.__delete_package_file(tmp_src) - - s3_dst = "/".join((rule_name, rule_name + ".zip")) - - print("Zipping complete.") - - return s3_dst - def __populate_params(self): # create custom session based on whatever credentials are available to us my_session = self.__get_boto_session() @@ -3940,8 +3690,8 @@ def __get_test_CIs(self, rulename): # Check to see if there is a test_ci.json file in the Rule directory tests_path = os.path.join(os.getcwd(), rules_dir, rulename, test_ci_filename) if os.path.exists(tests_path): - print("\tTesting with CI's provided in test_ci.json file. NOT YET IMPLEMENTED") # TODO - # test_ci_list self._load_cis_from_file(tests_path) + print("\tTesting with CI's provided in test_ci.json file. NOT YET IMPLEMENTED") + # test_ci_list self._load_cis_from_file(tests_path) else: print("\tTesting with generic CI for configured Resource Type(s)") my_rule_params, my_rule_tags = self.__get_rule_parameters(rulename) diff --git a/rdk/template/configRule.yaml b/rdk/template/configRule.yaml index c5a5ca7a..a8eb990b 100644 --- a/rdk/template/configRule.yaml +++ b/rdk/template/configRule.yaml @@ -248,7 +248,7 @@ Resources: cfn_nag: rules_to_suppress: - id: W11 - reason: "TODO - Will scope down permissions gradually, tracked via GitHub Issues" + reason: "Will scope down permissions gradually, tracked via GitHub Issues" Condition: CreateNewLambdaRole Type: AWS::IAM::Role Properties: @@ -289,7 +289,7 @@ Resources: - iam:List* - iam:Get* Effect: Allow - Resource: "*" # TODO - Determine how these permissions can be scoped down. + Resource: "*" - Sid: "AllowRoleAssumption" Action: - sts:AssumeRole diff --git a/rdk/template/configRuleOrganization.yaml b/rdk/template/configRuleOrganization.yaml index ededc6dd..a1966bdc 100644 --- a/rdk/template/configRuleOrganization.yaml +++ b/rdk/template/configRuleOrganization.yaml @@ -235,7 +235,7 @@ Resources: cfn_nag: rules_to_suppress: - id: W11 - reason: "TODO - Will scope down permissions gradually, tracked via GitHub Issues" + reason: "Will scope down permissions gradually, tracked via GitHub Issues" Condition: CreateNewLambdaRole Type: AWS::IAM::Role Properties: diff --git a/rdk/template/runtime/python3.10-lib/rule_test.py b/rdk/template/runtime/python3.10-lib/rule_test.py index 196ab37e..b0c26fba 100644 --- a/rdk/template/runtime/python3.10-lib/rule_test.py +++ b/rdk/template/runtime/python3.10-lib/rule_test.py @@ -12,7 +12,7 @@ ############## # Define the default resource to report to Config Rules -# TODO - Replace with your resource type +# NOTE TO USER - Replace with your resource type RESOURCE_TYPE = "AWS::IAM::Role" ############# diff --git a/rdk/template/runtime/python3.10/rule_code.py b/rdk/template/runtime/python3.10/rule_code.py index 682297b0..c33b7477 100644 --- a/rdk/template/runtime/python3.10/rule_code.py +++ b/rdk/template/runtime/python3.10/rule_code.py @@ -4,11 +4,6 @@ import boto3 import botocore -try: - import liblogging -except ImportError: - pass - ############## # Parameters # ############## @@ -71,6 +66,7 @@ def evaluate_parameters(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. @@ -151,6 +147,7 @@ def build_evaluation_from_config_item(configuration_item, compliance_type, annot # Boilerplate Code # #################### + # Get execution role for Lambda function def get_execution_role_arn(event): role_arn = None @@ -259,8 +256,9 @@ def get_assume_role_credentials(role_arn, region=None): 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) + print( + f"Successfully assumed the role {role_arn} using the assumed role {assume_role_response['AssumedRoleUser']['Arn']}" + ) return assume_role_response["Credentials"] except botocore.exceptions.ClientError as ex: # Scrub error message for any internal account info leaks @@ -311,13 +309,11 @@ def clean_up_old_evaluations(latest_evaluations, event): def lambda_handler(event, context): - if "liblogging" in sys.modules: - liblogging.logEvent(event) global AWS_CONFIG_CLIENT - # print(event) check_defined(event, "event") + print(event) invoking_event = json.loads(event["invokingEvent"]) rule_parameters = {} if "ruleParameters" in event: diff --git a/rdk/template/runtime/python3.11-lib/rule_test.py b/rdk/template/runtime/python3.11-lib/rule_test.py index 196ab37e..b0c26fba 100644 --- a/rdk/template/runtime/python3.11-lib/rule_test.py +++ b/rdk/template/runtime/python3.11-lib/rule_test.py @@ -12,7 +12,7 @@ ############## # Define the default resource to report to Config Rules -# TODO - Replace with your resource type +# NOTE TO USER - Replace with your resource type RESOURCE_TYPE = "AWS::IAM::Role" ############# diff --git a/rdk/template/runtime/python3.11/rule_code.py b/rdk/template/runtime/python3.11/rule_code.py index 682297b0..c33b7477 100644 --- a/rdk/template/runtime/python3.11/rule_code.py +++ b/rdk/template/runtime/python3.11/rule_code.py @@ -4,11 +4,6 @@ import boto3 import botocore -try: - import liblogging -except ImportError: - pass - ############## # Parameters # ############## @@ -71,6 +66,7 @@ def evaluate_parameters(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. @@ -151,6 +147,7 @@ def build_evaluation_from_config_item(configuration_item, compliance_type, annot # Boilerplate Code # #################### + # Get execution role for Lambda function def get_execution_role_arn(event): role_arn = None @@ -259,8 +256,9 @@ def get_assume_role_credentials(role_arn, region=None): 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) + print( + f"Successfully assumed the role {role_arn} using the assumed role {assume_role_response['AssumedRoleUser']['Arn']}" + ) return assume_role_response["Credentials"] except botocore.exceptions.ClientError as ex: # Scrub error message for any internal account info leaks @@ -311,13 +309,11 @@ def clean_up_old_evaluations(latest_evaluations, event): def lambda_handler(event, context): - if "liblogging" in sys.modules: - liblogging.logEvent(event) global AWS_CONFIG_CLIENT - # print(event) check_defined(event, "event") + print(event) invoking_event = json.loads(event["invokingEvent"]) rule_parameters = {} if "ruleParameters" in event: diff --git a/rdk/template/runtime/python3.12-lib/rule_test.py b/rdk/template/runtime/python3.12-lib/rule_test.py index 196ab37e..b0c26fba 100644 --- a/rdk/template/runtime/python3.12-lib/rule_test.py +++ b/rdk/template/runtime/python3.12-lib/rule_test.py @@ -12,7 +12,7 @@ ############## # Define the default resource to report to Config Rules -# TODO - Replace with your resource type +# NOTE TO USER - Replace with your resource type RESOURCE_TYPE = "AWS::IAM::Role" ############# diff --git a/rdk/template/runtime/python3.12/rule_code.py b/rdk/template/runtime/python3.12/rule_code.py index 682297b0..c33b7477 100644 --- a/rdk/template/runtime/python3.12/rule_code.py +++ b/rdk/template/runtime/python3.12/rule_code.py @@ -4,11 +4,6 @@ import boto3 import botocore -try: - import liblogging -except ImportError: - pass - ############## # Parameters # ############## @@ -71,6 +66,7 @@ def evaluate_parameters(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. @@ -151,6 +147,7 @@ def build_evaluation_from_config_item(configuration_item, compliance_type, annot # Boilerplate Code # #################### + # Get execution role for Lambda function def get_execution_role_arn(event): role_arn = None @@ -259,8 +256,9 @@ def get_assume_role_credentials(role_arn, region=None): 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) + print( + f"Successfully assumed the role {role_arn} using the assumed role {assume_role_response['AssumedRoleUser']['Arn']}" + ) return assume_role_response["Credentials"] except botocore.exceptions.ClientError as ex: # Scrub error message for any internal account info leaks @@ -311,13 +309,11 @@ def clean_up_old_evaluations(latest_evaluations, event): def lambda_handler(event, context): - if "liblogging" in sys.modules: - liblogging.logEvent(event) global AWS_CONFIG_CLIENT - # print(event) check_defined(event, "event") + print(event) invoking_event = json.loads(event["invokingEvent"]) rule_parameters = {} if "ruleParameters" in event: diff --git a/rdk/template/runtime/python3.7-lib/rule_test.py b/rdk/template/runtime/python3.7-lib/rule_test.py index 196ab37e..b0c26fba 100644 --- a/rdk/template/runtime/python3.7-lib/rule_test.py +++ b/rdk/template/runtime/python3.7-lib/rule_test.py @@ -12,7 +12,7 @@ ############## # Define the default resource to report to Config Rules -# TODO - Replace with your resource type +# NOTE TO USER - Replace with your resource type RESOURCE_TYPE = "AWS::IAM::Role" ############# diff --git a/rdk/template/runtime/python3.7/rule_code.py b/rdk/template/runtime/python3.7/rule_code.py index 682297b0..c33b7477 100644 --- a/rdk/template/runtime/python3.7/rule_code.py +++ b/rdk/template/runtime/python3.7/rule_code.py @@ -4,11 +4,6 @@ import boto3 import botocore -try: - import liblogging -except ImportError: - pass - ############## # Parameters # ############## @@ -71,6 +66,7 @@ def evaluate_parameters(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. @@ -151,6 +147,7 @@ def build_evaluation_from_config_item(configuration_item, compliance_type, annot # Boilerplate Code # #################### + # Get execution role for Lambda function def get_execution_role_arn(event): role_arn = None @@ -259,8 +256,9 @@ def get_assume_role_credentials(role_arn, region=None): 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) + print( + f"Successfully assumed the role {role_arn} using the assumed role {assume_role_response['AssumedRoleUser']['Arn']}" + ) return assume_role_response["Credentials"] except botocore.exceptions.ClientError as ex: # Scrub error message for any internal account info leaks @@ -311,13 +309,11 @@ def clean_up_old_evaluations(latest_evaluations, event): def lambda_handler(event, context): - if "liblogging" in sys.modules: - liblogging.logEvent(event) global AWS_CONFIG_CLIENT - # print(event) check_defined(event, "event") + print(event) invoking_event = json.loads(event["invokingEvent"]) rule_parameters = {} if "ruleParameters" in event: diff --git a/rdk/template/runtime/python3.8-lib/rule_test.py b/rdk/template/runtime/python3.8-lib/rule_test.py index 196ab37e..b0c26fba 100644 --- a/rdk/template/runtime/python3.8-lib/rule_test.py +++ b/rdk/template/runtime/python3.8-lib/rule_test.py @@ -12,7 +12,7 @@ ############## # Define the default resource to report to Config Rules -# TODO - Replace with your resource type +# NOTE TO USER - Replace with your resource type RESOURCE_TYPE = "AWS::IAM::Role" ############# diff --git a/rdk/template/runtime/python3.8/rule_code.py b/rdk/template/runtime/python3.8/rule_code.py index 682297b0..c33b7477 100644 --- a/rdk/template/runtime/python3.8/rule_code.py +++ b/rdk/template/runtime/python3.8/rule_code.py @@ -4,11 +4,6 @@ import boto3 import botocore -try: - import liblogging -except ImportError: - pass - ############## # Parameters # ############## @@ -71,6 +66,7 @@ def evaluate_parameters(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. @@ -151,6 +147,7 @@ def build_evaluation_from_config_item(configuration_item, compliance_type, annot # Boilerplate Code # #################### + # Get execution role for Lambda function def get_execution_role_arn(event): role_arn = None @@ -259,8 +256,9 @@ def get_assume_role_credentials(role_arn, region=None): 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) + print( + f"Successfully assumed the role {role_arn} using the assumed role {assume_role_response['AssumedRoleUser']['Arn']}" + ) return assume_role_response["Credentials"] except botocore.exceptions.ClientError as ex: # Scrub error message for any internal account info leaks @@ -311,13 +309,11 @@ def clean_up_old_evaluations(latest_evaluations, event): def lambda_handler(event, context): - if "liblogging" in sys.modules: - liblogging.logEvent(event) global AWS_CONFIG_CLIENT - # print(event) check_defined(event, "event") + print(event) invoking_event = json.loads(event["invokingEvent"]) rule_parameters = {} if "ruleParameters" in event: diff --git a/rdk/template/runtime/python3.9-lib/rule_test.py b/rdk/template/runtime/python3.9-lib/rule_test.py index 196ab37e..b0c26fba 100644 --- a/rdk/template/runtime/python3.9-lib/rule_test.py +++ b/rdk/template/runtime/python3.9-lib/rule_test.py @@ -12,7 +12,7 @@ ############## # Define the default resource to report to Config Rules -# TODO - Replace with your resource type +# NOTE TO USER - Replace with your resource type RESOURCE_TYPE = "AWS::IAM::Role" ############# diff --git a/rdk/template/runtime/python3.9/rule_code.py b/rdk/template/runtime/python3.9/rule_code.py index 682297b0..c33b7477 100644 --- a/rdk/template/runtime/python3.9/rule_code.py +++ b/rdk/template/runtime/python3.9/rule_code.py @@ -4,11 +4,6 @@ import boto3 import botocore -try: - import liblogging -except ImportError: - pass - ############## # Parameters # ############## @@ -71,6 +66,7 @@ def evaluate_parameters(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. @@ -151,6 +147,7 @@ def build_evaluation_from_config_item(configuration_item, compliance_type, annot # Boilerplate Code # #################### + # Get execution role for Lambda function def get_execution_role_arn(event): role_arn = None @@ -259,8 +256,9 @@ def get_assume_role_credentials(role_arn, region=None): 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) + print( + f"Successfully assumed the role {role_arn} using the assumed role {assume_role_response['AssumedRoleUser']['Arn']}" + ) return assume_role_response["Credentials"] except botocore.exceptions.ClientError as ex: # Scrub error message for any internal account info leaks @@ -311,13 +309,11 @@ def clean_up_old_evaluations(latest_evaluations, event): def lambda_handler(event, context): - if "liblogging" in sys.modules: - liblogging.logEvent(event) global AWS_CONFIG_CLIENT - # print(event) check_defined(event, "event") + print(event) invoking_event = json.loads(event["invokingEvent"]) rule_parameters = {} if "ruleParameters" in event: diff --git a/rdk/template/terraform/0.11/config_rule.tf b/rdk/template/terraform/0.11/config_rule.tf deleted file mode 100644 index 6e190217..00000000 --- a/rdk/template/terraform/0.11/config_rule.tf +++ /dev/null @@ -1,169 +0,0 @@ - - -data "aws_caller_identity" "current" {} - -data "aws_partition" "current" {} - -data "aws_iam_policy" "read_only_access" { - arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/ReadOnlyAccess" -} - -data "aws_iam_policy_document" "config_iam_policy" { - statement{ - actions=[ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", - "logs:DescribeLogStreams" - ] - resources = ["*"] - effect = "Allow" - sid= "2" - } - statement{ - actions=["config:PutEvaluations"] - resources = ["*"] - effect = "Allow" - sid= "3" - } - statement{ - actions=[ - "iam:List*", - "iam:Get*" - ] - resources = ["*"] - effect = "Allow" - sid= "4" - } - statement{ - actions=["sts:AssumeRole"] - resources = ["*"] - effect = "Allow" - sid= "5" - } - -} - -provider "aws" { - profile = "default" - -} - -resource "aws_s3_bucket_object" "rule_code" { - bucket = "${var.source_bucket}" - key = "${var.rule_name}.zip" - source = "${var.rule_name}.zip" - -} - -resource "aws_lambda_function" "rdk_rule" { - - function_name = "${var.rule_lambda_name}" - description = "Create a new AWS lambda function for rule code" - runtime = "${var.source_runtime}" - handler = "${var.source_handler}" - role = "${ local.create_new_lambda_role ? "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${lower(var.rule_name)}-awsconfig-role" : var.lambda_role_arn}" - - timeout = "${var.lambda_timeout}" - s3_bucket = "${var.source_bucket}" - s3_key = "${var.rule_name}.zip" - memory_size = "256" - layers = "${var.lambda_layers}" - vpc_config { - subnet_ids = "${var.subnet_ids}" - security_group_ids = "${var.security_group_ids}" - } - - depends_on = ["aws_s3_bucket_object.rule_code"] -} - -resource "aws_lambda_permission" "lambda_invoke" { - action = "lambda:InvokeFunction" - function_name = "${aws_lambda_function.rdk_rule.arn}" - principal = "config.amazonaws.com" - statement_id = "AllowExecutionFromConfig" -} - -resource "aws_config_config_rule" "event_triggered" { - count = "${local.event_triggered ? 1 : 0 }" - name = "${var.rule_name}" - description = "${var.rule_name}" - scope { - compliance_resource_types = "${var.source_events}" - } - - input_parameters = "${var.source_input_parameters}" - source { - owner = "CUSTOM_LAMBDA" - source_identifier = "${aws_lambda_function.rdk_rule.arn}" - source_detail { - event_source = "aws.config" - message_type = "ConfigurationItemChangeNotification" - - } - } -} - -resource "aws_config_config_rule" "periodic_triggered_rule" { - count = "${local.periodic_triggered ? 1 : 0 }" - name = "${var.rule_name}" - description = "${var.rule_name}" - - input_parameters = "${var.source_input_parameters}" - source { - owner = "CUSTOM_LAMBDA" - source_identifier = "${aws_lambda_function.rdk_rule.arn}" - source_detail { - event_source = "aws.config" - message_type = "ScheduledNotification" - maximum_execution_frequency = "${var.source_periodic}" - } - } - - - depends_on = ["aws_lambda_permission.lambda_invoke"] -} - -resource "aws_iam_role" "awsconfig" { - count = "${local.create_new_lambda_role ? 1 : 0}" - name = "${lower(var.rule_name)}-awsconfig-role" - - assume_role_policy = <0 ? true : false) - periodic_triggered = var.source_periodic != "NONE" ? true : false - create_new_lambda_role = (var.lambda_role_arn == "NONE" ? true : false) - rule_name_source = format("%s.zip", var.rule_name) - -} diff --git a/rdk/template/terraform/1.x/rdk_module/README.md b/rdk/template/terraform/1.x/rdk_module/README.md new file mode 100644 index 00000000..16e629ad --- /dev/null +++ b/rdk/template/terraform/1.x/rdk_module/README.md @@ -0,0 +1,3 @@ +# Summary + +This module will upload your RDK rule's Lambda code to the default code bucket and create the Lambda and custom config rule. \ No newline at end of file diff --git a/rdk/template/terraform/1.x/rdk_module/config_rule.tf b/rdk/template/terraform/1.x/rdk_module/config_rule.tf new file mode 100644 index 00000000..91568936 --- /dev/null +++ b/rdk/template/terraform/1.x/rdk_module/config_rule.tf @@ -0,0 +1,86 @@ +locals { + event_triggered = length(var.source_events) > 0 ? true : false + periodic_triggered = var.source_periodic != "" ? true : false + create_new_lambda_role = var.lambda_role_arn == "" ? true : false + rule_name_source = "${var.rule_name}.zip" + + rdk_role_name = "${lower(var.rule_name)}-awsconfig-role" + prebuilt_rdk_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${local.rdk_role_name}" + rdk_lambda_rule_role = local.create_new_lambda_role ? local.prebuilt_rdk_role_arn : var.lambda_role_arn +} + +resource "aws_s3_object" "rule_code" { + bucket = var.source_bucket + key = local.rule_name_source + source = data.archive_file.lambda.output_path + etag = filemd5(data.archive_file.lambda.output_path) +} + +resource "aws_lambda_function" "rdk_rule" { + depends_on = [aws_s3_object.rule_code] + function_name = var.rule_lambda_name + description = "Create a new AWS Lambda function for rule code" + runtime = var.source_runtime + handler = var.source_handler + role = local.rdk_lambda_rule_role + timeout = var.lambda_timeout + s3_bucket = aws_s3_object.rule_code.bucket + s3_key = aws_s3_object.rule_code.key + source_code_hash = aws_s3_object.rule_code.etag + memory_size = "256" + layers = var.lambda_layers + + vpc_config { + subnet_ids = var.subnet_ids + security_group_ids = var.security_group_ids + } +} + +resource "aws_lambda_permission" "lambda_invoke" { + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.rdk_rule.arn + principal = "config.amazonaws.com" + statement_id = "AllowExecutionFromConfig" +} + +resource "aws_config_config_rule" "event_triggered" { + count = local.event_triggered ? 1 : 0 + name = var.rule_name + description = var.rule_name + + scope { + compliance_resource_types = var.source_events + } + + input_parameters = var.source_input_parameters + + source { + owner = "CUSTOM_LAMBDA" + source_identifier = aws_lambda_function.rdk_rule.arn + + source_detail { + event_source = "aws.config" + message_type = "ConfigurationItemChangeNotification" + } + } +} + +resource "aws_config_config_rule" "periodic_triggered_rule" { + count = local.periodic_triggered ? 1 : 0 + depends_on = [aws_lambda_permission.lambda_invoke] + name = var.rule_name + description = var.rule_name + + input_parameters = var.source_input_parameters + + source { + owner = "CUSTOM_LAMBDA" + source_identifier = aws_lambda_function.rdk_rule.arn + + source_detail { + event_source = "aws.config" + message_type = "ScheduledNotification" + maximum_execution_frequency = var.source_periodic + } + } +} diff --git a/rdk/template/terraform/1.x/rdk_module/data.tf b/rdk/template/terraform/1.x/rdk_module/data.tf new file mode 100644 index 00000000..3cc33131 --- /dev/null +++ b/rdk/template/terraform/1.x/rdk_module/data.tf @@ -0,0 +1,79 @@ +# Define the data sources used to determine the current context of this execution. +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +data "archive_file" "lambda" { + type = "zip" + source_file = "${path.module}/../${var.rule_name}.py" + output_path = "${path.module}/../${var.rule_name}.zip" +} + +# Trust policy to allow Config service to assume Lambda role +data "aws_iam_policy_document" "aws_config_policy_doc" { + count = local.create_new_lambda_role ? 1 : 0 + + statement { + sid = "AllowLambdaAssumeRole" + + actions = [ + "sts:AssumeRole", + ] + + principals { + type = "Service" + identifiers = [ + "lambda.amazonaws.com", + ] + } + } +} + +data "aws_iam_policy" "read_only_access" { + arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/ReadOnlyAccess" +} + +data "aws_iam_policy_document" "config_iam_policy" { + # Allow reading from the rule bucket + statement { + sid = "AllowGetS3Objects" + actions = ["s3:GetObject"] + resources = ["arn:${data.aws_partition.current.partition}:s3:::${var.source_bucket}/${local.rule_name_source}"] + } + + # Allow Lambda to log events + statement { + sid = "AllowCloudWatchLogging" + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams", + ] + resources = ["*"] + } + + # Allow Lambda to put evaluations + statement { + sid = "AllowConfigPutEvaluations" + actions = ["config:PutEvaluations"] + resources = ["*"] + } + + # Allow Lambda to read IAM resource details + statement { + sid = "AllowIAMDetailRead" + actions = [ + "iam:List*", + "iam:Describe*", + "iam:Get*", + ] + resources = ["*"] + } + + # Allow role assumption + statement { + sid = "AllowAssumeRole" + actions = ["sts:AssumeRole"] + resources = ["*"] + } +} diff --git a/rdk/template/terraform/1.x/rdk_module/lambda_role.tf b/rdk/template/terraform/1.x/rdk_module/lambda_role.tf new file mode 100644 index 00000000..1a99218e --- /dev/null +++ b/rdk/template/terraform/1.x/rdk_module/lambda_role.tf @@ -0,0 +1,25 @@ +resource "aws_iam_role" "awsconfig" { + count = local.create_new_lambda_role ? 1 : 0 + name = local.rdk_role_name + + assume_role_policy = data.aws_iam_policy_document.aws_config_policy_doc[count.index].json +} + +resource "aws_iam_policy" "awsconfig_policy" { + count = local.create_new_lambda_role ? 1 : 0 + name = "${lower(var.rule_name)}-awsconfig-policy" + + policy = data.aws_iam_policy_document.config_iam_policy.json +} + +resource "aws_iam_role_policy_attachment" "awsconfig_policy_attach" { + count = local.create_new_lambda_role ? 1 : 0 + role = aws_iam_role.awsconfig[count.index].name + policy_arn = aws_iam_policy.awsconfig_policy[count.index].arn +} + +resource "aws_iam_role_policy_attachment" "readonly_role_policy_attach" { + count = local.create_new_lambda_role ? 1 : 0 + role = aws_iam_role.awsconfig[count.index].name + policy_arn = data.aws_iam_policy.read_only_access.arn +} diff --git a/rdk/template/terraform/1.x/rdk_module/providers.tf b/rdk/template/terraform/1.x/rdk_module/providers.tf new file mode 100644 index 00000000..64ffe4b0 --- /dev/null +++ b/rdk/template/terraform/1.x/rdk_module/providers.tf @@ -0,0 +1,10 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0.0, < 6.0.0" + } + } + + required_version = ">= 1.0.0" +} diff --git a/rdk/template/terraform/1.x/rdk_module/variables.tf b/rdk/template/terraform/1.x/rdk_module/variables.tf new file mode 100644 index 00000000..223bef61 --- /dev/null +++ b/rdk/template/terraform/1.x/rdk_module/variables.tf @@ -0,0 +1,75 @@ +# Required Arguments + +variable "rule_name" { + type = string + description = "Rule name to export." +} + +variable "rule_lambda_name" { + type = string + description = "Lambda function name for the Config Rule to export." +} + +variable "source_runtime" { + type = string + description = "Runtime for lambda function." +} + +variable "source_handler" { + type = string + description = "Lambda handler name." +} + +variable "source_bucket" { + type = string + description = "Amazon S3 bucket used to export the rule code." +} + +variable "source_input_parameters" { + description = "JSON for required and optional Config parameters." + type = string +} + +# Optional Arguments + +variable "subnet_ids" { + description = "Comma-separated list of Subnets to deploy your Lambda function(s)." + type = list(string) + default = [] +} + +variable "security_group_ids" { + description = "Comma-separated list of Security Groups to deploy with your Lambda function(s)." + type = list(string) + default = [] +} + +variable "source_events" { + description = "Resource types that will trigger event-based Rule evaluation." + type = list(string) + default = [] +} + +variable "lambda_layers" { + type = list(string) + description = "Comma-separated list of Lambda Layer ARNs to deploy with your Lambda function(s)." + default = [] +} + +variable "source_periodic" { + description = "Maximum execution frequency for scheduled Rules." + type = string + default = "" +} + +variable "lambda_role_arn" { + description = "Assign existing iam role to lambda functions. If omitted, new lambda role will be created." + type = string + default = "" +} + +variable "lambda_timeout" { + description = "Lambda function timeout" + type = string + default = "900" +} diff --git a/rdk/template/terraform/1.x_organization/rdk_organization_module/README.md b/rdk/template/terraform/1.x_organization/rdk_organization_module/README.md new file mode 100644 index 00000000..a39cb5bb --- /dev/null +++ b/rdk/template/terraform/1.x_organization/rdk_organization_module/README.md @@ -0,0 +1,3 @@ +# Summary + +This is a module that deploys a Config rule as an Organization-wide rule. \ No newline at end of file diff --git a/rdk/template/terraform/1.x_organization/rdk_organization_module/config_rule.tf b/rdk/template/terraform/1.x_organization/rdk_organization_module/config_rule.tf new file mode 100644 index 00000000..2e6d490d --- /dev/null +++ b/rdk/template/terraform/1.x_organization/rdk_organization_module/config_rule.tf @@ -0,0 +1,64 @@ +locals { + event_triggered = length(var.source_events) > 0 ? true : false + periodic_triggered = var.source_periodic != "" ? true : false + create_new_lambda_role = var.lambda_role_arn == "" ? true : false + rule_name_source = "${var.rule_name}.zip" + + rdk_role_name = "${lower(var.rule_name)}-awsconfig-role" + prebuilt_rdk_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${local.rdk_role_name}" + rdk_lambda_rule_role = local.create_new_lambda_role ? local.prebuilt_rdk_role_arn : var.lambda_role_arn +} + +resource "aws_s3_object" "rule_code" { + bucket = var.source_bucket + key = local.rule_name_source + source = data.archive_file.lambda.output_path + etag = filemd5(data.archive_file.lambda.output_path) +} + +resource "aws_lambda_function" "rdk_rule" { + depends_on = [aws_s3_object.rule_code] + function_name = var.rule_lambda_name + description = "Create a new AWS Lambda function for rule code" + runtime = var.source_runtime + handler = var.source_handler + role = local.rdk_lambda_rule_role + timeout = var.lambda_timeout + s3_bucket = aws_s3_object.rule_code.bucket + s3_key = aws_s3_object.rule_code.key + source_code_hash = aws_s3_object.rule_code.etag + memory_size = "256" + layers = var.lambda_layers + + vpc_config { + subnet_ids = var.subnet_ids + security_group_ids = var.security_group_ids + } +} + +resource "aws_lambda_permission" "lambda_invoke" { + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.rdk_rule.arn + principal = "config.amazonaws.com" + statement_id = "AllowExecutionFromConfig" +} + +resource "aws_config_organization_custom_rule" "event_triggered" { + count = local.event_triggered ? 1 : 0 + trigger_types = ["ConfigurationItemChangeNotification"] + name = var.rule_name + description = var.rule_name + lambda_function_arn = aws_lambda_function.rdk_rule.arn + input_parameters = var.source_input_parameters +} + +resource "aws_config_organization_custom_rule" "periodic_triggered_rule" { + count = local.periodic_triggered ? 1 : 0 + trigger_types = ["ScheduledNotification"] + depends_on = [aws_lambda_permission.lambda_invoke] + name = var.rule_name + description = var.rule_name + + input_parameters = var.source_input_parameters + lambda_function_arn = aws_lambda_function.rdk_rule.arn +} diff --git a/rdk/template/terraform/1.x_organization/rdk_organization_module/data.tf b/rdk/template/terraform/1.x_organization/rdk_organization_module/data.tf new file mode 100644 index 00000000..3cc33131 --- /dev/null +++ b/rdk/template/terraform/1.x_organization/rdk_organization_module/data.tf @@ -0,0 +1,79 @@ +# Define the data sources used to determine the current context of this execution. +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +data "archive_file" "lambda" { + type = "zip" + source_file = "${path.module}/../${var.rule_name}.py" + output_path = "${path.module}/../${var.rule_name}.zip" +} + +# Trust policy to allow Config service to assume Lambda role +data "aws_iam_policy_document" "aws_config_policy_doc" { + count = local.create_new_lambda_role ? 1 : 0 + + statement { + sid = "AllowLambdaAssumeRole" + + actions = [ + "sts:AssumeRole", + ] + + principals { + type = "Service" + identifiers = [ + "lambda.amazonaws.com", + ] + } + } +} + +data "aws_iam_policy" "read_only_access" { + arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/ReadOnlyAccess" +} + +data "aws_iam_policy_document" "config_iam_policy" { + # Allow reading from the rule bucket + statement { + sid = "AllowGetS3Objects" + actions = ["s3:GetObject"] + resources = ["arn:${data.aws_partition.current.partition}:s3:::${var.source_bucket}/${local.rule_name_source}"] + } + + # Allow Lambda to log events + statement { + sid = "AllowCloudWatchLogging" + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams", + ] + resources = ["*"] + } + + # Allow Lambda to put evaluations + statement { + sid = "AllowConfigPutEvaluations" + actions = ["config:PutEvaluations"] + resources = ["*"] + } + + # Allow Lambda to read IAM resource details + statement { + sid = "AllowIAMDetailRead" + actions = [ + "iam:List*", + "iam:Describe*", + "iam:Get*", + ] + resources = ["*"] + } + + # Allow role assumption + statement { + sid = "AllowAssumeRole" + actions = ["sts:AssumeRole"] + resources = ["*"] + } +} diff --git a/rdk/template/terraform/1.x_organization/rdk_organization_module/lambda_role.tf b/rdk/template/terraform/1.x_organization/rdk_organization_module/lambda_role.tf new file mode 100644 index 00000000..1a99218e --- /dev/null +++ b/rdk/template/terraform/1.x_organization/rdk_organization_module/lambda_role.tf @@ -0,0 +1,25 @@ +resource "aws_iam_role" "awsconfig" { + count = local.create_new_lambda_role ? 1 : 0 + name = local.rdk_role_name + + assume_role_policy = data.aws_iam_policy_document.aws_config_policy_doc[count.index].json +} + +resource "aws_iam_policy" "awsconfig_policy" { + count = local.create_new_lambda_role ? 1 : 0 + name = "${lower(var.rule_name)}-awsconfig-policy" + + policy = data.aws_iam_policy_document.config_iam_policy.json +} + +resource "aws_iam_role_policy_attachment" "awsconfig_policy_attach" { + count = local.create_new_lambda_role ? 1 : 0 + role = aws_iam_role.awsconfig[count.index].name + policy_arn = aws_iam_policy.awsconfig_policy[count.index].arn +} + +resource "aws_iam_role_policy_attachment" "readonly_role_policy_attach" { + count = local.create_new_lambda_role ? 1 : 0 + role = aws_iam_role.awsconfig[count.index].name + policy_arn = data.aws_iam_policy.read_only_access.arn +} diff --git a/rdk/template/terraform/1.x_organization/rdk_organization_module/providers.tf b/rdk/template/terraform/1.x_organization/rdk_organization_module/providers.tf new file mode 100644 index 00000000..64ffe4b0 --- /dev/null +++ b/rdk/template/terraform/1.x_organization/rdk_organization_module/providers.tf @@ -0,0 +1,10 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0.0, < 6.0.0" + } + } + + required_version = ">= 1.0.0" +} diff --git a/rdk/template/terraform/1.x_organization/rdk_organization_module/variables.tf b/rdk/template/terraform/1.x_organization/rdk_organization_module/variables.tf new file mode 100644 index 00000000..223bef61 --- /dev/null +++ b/rdk/template/terraform/1.x_organization/rdk_organization_module/variables.tf @@ -0,0 +1,75 @@ +# Required Arguments + +variable "rule_name" { + type = string + description = "Rule name to export." +} + +variable "rule_lambda_name" { + type = string + description = "Lambda function name for the Config Rule to export." +} + +variable "source_runtime" { + type = string + description = "Runtime for lambda function." +} + +variable "source_handler" { + type = string + description = "Lambda handler name." +} + +variable "source_bucket" { + type = string + description = "Amazon S3 bucket used to export the rule code." +} + +variable "source_input_parameters" { + description = "JSON for required and optional Config parameters." + type = string +} + +# Optional Arguments + +variable "subnet_ids" { + description = "Comma-separated list of Subnets to deploy your Lambda function(s)." + type = list(string) + default = [] +} + +variable "security_group_ids" { + description = "Comma-separated list of Security Groups to deploy with your Lambda function(s)." + type = list(string) + default = [] +} + +variable "source_events" { + description = "Resource types that will trigger event-based Rule evaluation." + type = list(string) + default = [] +} + +variable "lambda_layers" { + type = list(string) + description = "Comma-separated list of Lambda Layer ARNs to deploy with your Lambda function(s)." + default = [] +} + +variable "source_periodic" { + description = "Maximum execution frequency for scheduled Rules." + type = string + default = "" +} + +variable "lambda_role_arn" { + description = "Assign existing iam role to lambda functions. If omitted, new lambda role will be created." + type = string + default = "" +} + +variable "lambda_timeout" { + description = "Lambda function timeout" + type = string + default = "900" +}