├── .config ├── .checkov.yml ├── .mdlrc ├── .terraform-docs.yaml ├── .tflint.hcl ├── .tfsec.yml ├── .tfsec │ ├── launch_configuration_imdsv2_tfchecks.json │ ├── launch_template_imdsv2_tfchecks.json │ ├── no_launch_config_tfchecks.json │ ├── sg_no_embedded_egress_rules_tfchecks.json │ └── sg_no_embedded_ingress_rules_tfchecks.json ├── functional_tests │ ├── post-entrypoint-helpers.sh │ └── pre-entrypoint-helpers.sh └── static_tests │ ├── post-entrypoint-helpers.sh │ └── pre-entrypoint-helpers.sh ├── .copier-answers.yml ├── .gitignore ├── .header.md ├── .pre-commit-config.yaml ├── .project_automation ├── deprecation │ └── entrypoint.sh ├── deprovision │ └── entrypoint.sh ├── functional_tests │ ├── Dockerfile │ ├── entrypoint.sh │ └── functional_tests.sh ├── init │ └── noop.sh ├── provision │ └── entrypoint.sh ├── publication │ ├── Dockerfile │ └── entrypoint.sh ├── static_tests │ ├── Dockerfile │ ├── entrypoint.sh │ └── static_tests.sh └── update │ └── noop.sh ├── .project_config.yml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE.txt ├── README.md ├── VERSION ├── cloudfront.tf ├── data.tf ├── diagram ├── RunTask-EventBridge.drawio ├── RunTask-EventBridge.png ├── TerraformCloud-RunTaskOutput.png ├── TerraformCloud-RunTaskWorkspace.png └── TerraformCloud-VariableSets.png ├── event └── runtask_rule.tpl ├── eventbridge.tf ├── examples ├── demo_workspace │ ├── .header.md │ ├── .terraform-docs.yaml │ ├── README.md │ ├── data.tf │ ├── iam │ │ ├── .DS_Store │ │ ├── role-policies │ │ │ └── invalid-iam-role-policy.tpl │ │ └── trust-policies │ │ │ └── invalid-trust.tpl │ ├── main.tf │ ├── variables.tf │ └── versions.tf └── module_workspace │ ├── .header.md │ ├── .terraform-docs.yaml │ ├── README.md │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── iam.tf ├── iam ├── role-policies │ ├── runtask-eventbridge-lambda-role-policy.tpl │ ├── runtask-fulfillment-lambda-role-policy.tpl │ ├── runtask-rule-role-policy.tpl │ └── runtask-state-role-policy.tpl └── trust-policies │ ├── events.tpl │ ├── lambda.tpl │ ├── lambda_edge.tpl │ └── states.tpl ├── kms.tf ├── lambda.tf ├── lambda ├── runtask_callback │ ├── Makefile │ ├── handler.py │ └── requirements.txt ├── runtask_edge │ ├── Makefile │ ├── handler.py │ └── requirements.txt ├── runtask_eventbridge │ ├── Makefile │ ├── handler.py │ └── requirements.txt ├── runtask_fulfillment │ ├── Makefile │ ├── default.yaml │ ├── handler.py │ └── requirements.txt └── runtask_request │ ├── Makefile │ ├── handler.py │ └── requirements.txt ├── locals.tf ├── outputs.tf ├── providers.tf ├── runtasks.tf ├── secrets.tf ├── states.tf ├── states └── runtask_states.asl.json ├── tests └── 01_mandatory.tftest.hcl ├── variables.tf ├── versions.tf └── waf.tf /.config/.checkov.yml: -------------------------------------------------------------------------------- 1 | download-external-modules: False 2 | evaluate-variables: true 3 | directory: 4 | - ./ 5 | framework: 6 | - terraform 7 | skip-check: 8 | - CKV2_GCP* 9 | - CKV_AZURE* 10 | - CKV2_AZURE* 11 | - CKV_TF_1 # default to Terraform registry instead of Git 12 | - CKV_AWS_109 # the given example intentionally violates this rule 13 | - CKV_AWS_111 # the given example intentionally violates this rule 14 | - CKV_AWS_356 # the given example intentionally violates this rule 15 | - CKV_AWS_7 # not required for this example 16 | - CKV_AWS_158 # not required for this example 17 | - CKV_AWS_66 # not required for this example 18 | - CKV_AWS_338 # not required for this example 19 | - CKV2_AWS_31 # false alarm 20 | summary-position: bottom 21 | output: 'cli' 22 | compact: True 23 | quiet: True -------------------------------------------------------------------------------- /.config/.mdlrc: -------------------------------------------------------------------------------- 1 | # Ignoring the following rules 2 | # MD007 Unordered list indentation 3 | # MD013 Line length 4 | # MD029 Ordered list item prefix 5 | rules "~MD007", "~MD013", "~MD029" -------------------------------------------------------------------------------- /.config/.terraform-docs.yaml: -------------------------------------------------------------------------------- 1 | formatter: markdown 2 | header-from: .header.md 3 | settings: 4 | anchor: true 5 | color: true 6 | default: true 7 | escape: true 8 | html: true 9 | indent: 2 10 | required: true 11 | sensitive: true 12 | type: true 13 | 14 | sort: 15 | enabled: true 16 | by: required 17 | 18 | output: 19 | file: README.md 20 | mode: replace 21 | -------------------------------------------------------------------------------- /.config/.tflint.hcl: -------------------------------------------------------------------------------- 1 | # https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/module-inspection.md 2 | # borrowed & modified indefinitely from https://github.com/ksatirli/building-infrastructure-you-can-mostly-trust/blob/main/.tflint.hcl 3 | 4 | plugin "aws" { 5 | enabled = true 6 | version = "0.54.0" 7 | source = "github.com/terraform-linters/tflint-ruleset-aws" 8 | } 9 | 10 | config { 11 | module = true 12 | force = false 13 | } 14 | 15 | rule "terraform_required_providers" { 16 | enabled = true 17 | } 18 | 19 | rule "terraform_required_version" { 20 | enabled = true 21 | } 22 | 23 | rule "terraform_naming_convention" { 24 | enabled = true 25 | format = "snake_case" 26 | } 27 | 28 | rule "terraform_typed_variables" { 29 | enabled = true 30 | } 31 | 32 | rule "terraform_unused_declarations" { 33 | enabled = true 34 | } 35 | 36 | rule "terraform_comment_syntax" { 37 | enabled = true 38 | } 39 | 40 | rule "terraform_deprecated_index" { 41 | enabled = true 42 | } 43 | 44 | rule "terraform_deprecated_interpolation" { 45 | enabled = true 46 | } 47 | 48 | rule "terraform_documented_outputs" { 49 | enabled = true 50 | } 51 | 52 | rule "terraform_documented_variables" { 53 | enabled = true 54 | } 55 | 56 | rule "terraform_module_pinned_source" { 57 | enabled = true 58 | } 59 | 60 | rule "terraform_standard_module_structure" { 61 | enabled = true 62 | } 63 | 64 | rule "terraform_workspace_remote" { 65 | enabled = true 66 | } 67 | -------------------------------------------------------------------------------- /.config/.tfsec.yml: -------------------------------------------------------------------------------- 1 | { 2 | "minimum_severity": "MEDIUM" 3 | } -------------------------------------------------------------------------------- /.config/.tfsec/launch_configuration_imdsv2_tfchecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "checks": [ 3 | { 4 | "code": "CUS002", 5 | "description": "Check to IMDSv2 is required on EC2 instances created by this Launch Template", 6 | "impact": "Instance metadata service can be interacted with freely", 7 | "resolution": "Enable HTTP token requirement for IMDS", 8 | "requiredTypes": [ 9 | "resource" 10 | ], 11 | "requiredLabels": [ 12 | "aws_launch_configuration" 13 | ], 14 | "severity": "CRITICAL", 15 | "matchSpec": { 16 | "action": "isPresent", 17 | "name": "metadata_options", 18 | "subMatch": { 19 | "action": "and", 20 | "predicateMatchSpec": [ 21 | { 22 | "action": "equals", 23 | "name": "http_tokens", 24 | "value": "required" 25 | 26 | } 27 | ] 28 | } 29 | }, 30 | 31 | "errorMessage": "is missing `metadata_options` block - it is required with `http_tokens` set to `required` to make Instance Metadata Service more secure.", 32 | "relatedLinks": [ 33 | "https://tfsec.dev/docs/aws/ec2/enforce-http-token-imds#aws/ec2", 34 | "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_configuration#metadata-options", 35 | "https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service" 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.config/.tfsec/launch_template_imdsv2_tfchecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "checks": [ 3 | { 4 | "code": "CUS001", 5 | "description": "Check to IMDSv2 is required on EC2 instances created by this Launch Template", 6 | "impact": "Instance metadata service can be interacted with freely", 7 | "resolution": "Enable HTTP token requirement for IMDS", 8 | "requiredTypes": [ 9 | "resource" 10 | ], 11 | "requiredLabels": [ 12 | "aws_launch_template" 13 | ], 14 | "severity": "CRITICAL", 15 | "matchSpec": { 16 | "action": "isPresent", 17 | "name": "metadata_options", 18 | "subMatch": { 19 | "action": "and", 20 | "predicateMatchSpec": [ 21 | { 22 | "action": "equals", 23 | "name": "http_tokens", 24 | "value": "required" 25 | 26 | } 27 | ] 28 | } 29 | }, 30 | 31 | "errorMessage": "is missing `metadata_options` block - it is required with `http_tokens` set to `required` to make Instance Metadata Service more secure.", 32 | "relatedLinks": [ 33 | "https://tfsec.dev/docs/aws/ec2/enforce-http-token-imds#aws/ec2", 34 | "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template#metadata-options", 35 | "https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service" 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.config/.tfsec/no_launch_config_tfchecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "checks": [ 3 | { 4 | "code": "CUS003", 5 | "description": "Use `aws_launch_template` over `aws_launch_configuration", 6 | "impact": "Launch configurations are not capable of versions", 7 | "resolution": "Convert resource type and attributes to `aws_launch_template`", 8 | "requiredTypes": [ 9 | "resource" 10 | ], 11 | "requiredLabels": [ 12 | "aws_launch_configuration" 13 | ], 14 | "severity": "MEDIUM", 15 | "matchSpec": { 16 | "action": "notPresent", 17 | "name": "image_id" 18 | }, 19 | 20 | "errorMessage": "should be changed to `aws_launch_template` since the functionality is the same but templates can be versioned.", 21 | "relatedLinks": [ 22 | "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template", 23 | "https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service" 24 | ] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.config/.tfsec/sg_no_embedded_egress_rules_tfchecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "checks": [ 3 | { 4 | "code": "CUS005", 5 | "description": "Security group rules should be defined with `aws_security_group_rule` instead of embedded.", 6 | "impact": "Embedded security group rules can cause issues during configuration updates.", 7 | "resolution": "Move `egress` rules to `aws_security_group_rule` and attach to `aws_security_group`.", 8 | "requiredTypes": [ 9 | "resource" 10 | ], 11 | "requiredLabels": [ 12 | "aws_security_group" 13 | ], 14 | "severity": "MEDIUM", 15 | "matchSpec": { 16 | "action": "notPresent", 17 | "name": "egress" 18 | }, 19 | 20 | "errorMessage": "`egress` rules should be moved to `aws_security_group_rule` and attached to `aws_security_group` instead of embedded.", 21 | "relatedLinks": [ 22 | "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule", 23 | "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group" 24 | ] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.config/.tfsec/sg_no_embedded_ingress_rules_tfchecks.json: -------------------------------------------------------------------------------- 1 | { 2 | "checks": [ 3 | { 4 | "code": "CUS004", 5 | "description": "Security group rules should be defined with `aws_security_group_rule` instead of embedded.", 6 | "impact": "Embedded security group rules can cause issues during configuration updates.", 7 | "resolution": "Move `ingress` rules to `aws_security_group_rule` and attach to `aws_security_group`.", 8 | "requiredTypes": [ 9 | "resource" 10 | ], 11 | "requiredLabels": [ 12 | "aws_security_group" 13 | ], 14 | "severity": "MEDIUM", 15 | "matchSpec": { 16 | "action": "notPresent", 17 | "name": "ingress" 18 | }, 19 | 20 | "errorMessage": "`ingress` rules should be moved to `aws_security_group_rule` and attached to `aws_security_group` instead of embedded.", 21 | "relatedLinks": [ 22 | "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule", 23 | "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group" 24 | ] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.config/functional_tests/post-entrypoint-helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## NOTE: this script runs at the end of functional test 3 | ## Use this to load any configurations after the functional test 4 | ## TIPS: avoid modifying the .project_automation/functional_test/entrypoint.sh 5 | ## migrate any customization you did on entrypoint.sh to this helper script 6 | echo "Executing Post-Entrypoint Helpers" 7 | 8 | #********** Project Path ************* 9 | PROJECT_PATH=${BASE_PATH}/project 10 | PROJECT_TYPE_PATH=${BASE_PATH}/projecttype 11 | cd ${PROJECT_PATH} 12 | 13 | #********** CLEANUP ************* 14 | echo "Cleaning up all temp files and artifacts" 15 | cd ${PROJECT_PATH} 16 | make -s clean -------------------------------------------------------------------------------- /.config/functional_tests/pre-entrypoint-helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## NOTE: this script runs at the start of functional test 3 | ## use this to load any configuration before the functional test 4 | ## TIPS: avoid modifying the .project_automation/functional_test/entrypoint.sh 5 | ## migrate any customization you did on entrypoint.sh to this helper script 6 | echo "Executing Pre-Entrypoint Helpers" 7 | 8 | #********** Project Path ************* 9 | PROJECT_PATH=${BASE_PATH}/project 10 | PROJECT_TYPE_PATH=${BASE_PATH}/projecttype 11 | cd ${PROJECT_PATH} 12 | 13 | #********** TFC Env Vars ************* 14 | export AWS_DEFAULT_REGION=us-east-1 15 | export TFE_TOKEN=`aws secretsmanager get-secret-value --secret-id abp/hcp/token --region us-west-2 | jq -r ".SecretString"` 16 | export TF_TOKEN_app_terraform_io=`aws secretsmanager get-secret-value --secret-id abp/hcp/token --region us-west-2 | jq -r ".SecretString"` 17 | 18 | #********** MAKEFILE ************* 19 | echo "Build the lambda function packages" 20 | make all 21 | 22 | #********** Get tfvars from SSM ************* 23 | echo "Get *.tfvars from SSM parameter" 24 | aws ssm get-parameter \ 25 | --name "/abp/hcp/functional/terraform-aws-runtask-iam-access-analyzer/terraform_tests.tfvars" \ 26 | --with-decryption \ 27 | --query "Parameter.Value" \ 28 | --output "text" \ 29 | --region "us-east-1" >> ./tests/terraform.auto.tfvars 30 | -------------------------------------------------------------------------------- /.config/static_tests/post-entrypoint-helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## NOTE: this script runs at the end of static test 3 | ## Use this to load any configurations after the static test 4 | ## TIPS: avoid modifying the .project_automation/static_test/entrypoint.sh 5 | ## migrate any customization you did on entrypoint.sh to this helper script 6 | echo "Executing Post-Entrypoint Helpers" -------------------------------------------------------------------------------- /.config/static_tests/pre-entrypoint-helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## NOTE: this script runs at the start of static test 3 | ## use this to load any configuration before the static test 4 | ## TIPS: avoid modifying the .project_automation/static_test/entrypoint.sh 5 | ## migrate any customization you did on entrypoint.sh to this helper script 6 | echo "Executing Pre-Entrypoint Helpers" 7 | 8 | #********** TFC Env Vars ************* 9 | export AWS_DEFAULT_REGION=us-east-1 10 | export TFE_TOKEN=`aws secretsmanager get-secret-value --secret-id abp/tfc/token | jq -r ".SecretString"` 11 | export TF_TOKEN_app_terraform_io=`aws secretsmanager get-secret-value --secret-id abp/tfc/token | jq -r ".SecretString"` 12 | 13 | #********** MAKEFILE ************* 14 | echo "Build the lambda function packages" 15 | make all 16 | 17 | #********** Get tfvars from SSM ************* 18 | echo "Get *.tfvars from SSM parameter" 19 | aws ssm get-parameter \ 20 | --name "/abp/tfc/functional/tfvars" \ 21 | --with-decryption \ 22 | --query "Parameter.Value" \ 23 | --output "text" \ 24 | --region "us-east-1" >> functional_test.tfvars 25 | -------------------------------------------------------------------------------- /.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # This file is auto-generated, changes will be overwritten 2 | _commit: v0.1.4 3 | _src_path: /task/f281c4e2-f68d-11ee-b0d0-1a8eb7bb45c9/projecttype 4 | starting_version: v0.0.0 5 | version_file: VERSION 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | plan.out 3 | plan.out.json 4 | 5 | # Local .terraform directories 6 | **/.terraform/* 7 | 8 | # .tfstate files 9 | *.tfstate 10 | *.tfstate.* 11 | 12 | # Crash log files 13 | crash.log 14 | 15 | # Exclude all .tfvars files, which are likely to contain sentitive data, such as 16 | # password, private keys, and other secrets. These should not be part of version 17 | # control as they are data points which are potentially sensitive and subject 18 | # to change depending on the environment. 19 | # 20 | ./*.tfvars 21 | *.tfvars 22 | 23 | # Ignore override files as they are usually used to override resources locally and so 24 | # are not checked in 25 | override.tf 26 | override.tf.json 27 | *_override.tf 28 | *_override.tf.json 29 | 30 | # Include override files you do wish to add to version control using negated pattern 31 | # 32 | # !example_override.tf 33 | 34 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 35 | # example: *tfplan* 36 | 37 | # Ignore CLI configuration files 38 | .terraformrc 39 | terraform.rc 40 | .terraform.lock.hcl 41 | 42 | go.mod 43 | go.sum 44 | 45 | 46 | **/site-packages 47 | *.zip 48 | settings.json 49 | TODO.md 50 | .DS_Store 51 | .idea 52 | .venv -------------------------------------------------------------------------------- /.header.md: -------------------------------------------------------------------------------- 1 | # terraform-runtask-iam-access-analyzer 2 | 3 | Use this module to integrate HCP Terraform Run Tasks with AWS IAM Access Analyzer for policy validation. 4 | 5 | ![Diagram](./diagram/RunTask-EventBridge.png) 6 | 7 | ## Prerequisites 8 | 9 | To use this module you need have the following: 10 | 11 | 1. AWS account and credentials 12 | 2. HCP Terraform with Run Task entitlement (Business subscription or higher) 13 | 14 | ## Usage 15 | 16 | * Build and package the Lambda files 17 | 18 | ``` 19 | make all 20 | ``` 21 | 22 | * Refer to the [module_workspace](./examples/module_workspace/README.md) for steps to deploy this module in HCP Terraform. 23 | 24 | * After you deployed the [module_workspace](./examples/module_workspace/README.md), navigate to your HCP Terraform organization, go to Organization Settings > Integrations > Run tasks to find the newly created Run Task. 25 | 26 | * You can use this run task in any workspace where you have standard IAM resource policy document. Refer to the [demo_workspace](./examples/demo_workspace/README.md) for more details. 27 | 28 | ## Limitations 29 | 30 | 1. Does not provide verbose error / warning messages in Run Task console. In the future, we will explore possibility to provide verbose logging. 31 | 32 | 2. Does not support Terraform [computed resources](https://www.terraform.io/plugin/sdkv2/schemas/schema-behaviors). 33 | 34 | For example, the tool will report no IAM policy found for the following Terraform template. The policy json string is a computed resource. The plan output doesn't contain information of IAM policy document. 35 | 36 | ``` 37 | resource "aws_s3_bucket" "b" { 38 | bucket = "my-tf-test-bucket" 39 | 40 | tags = { 41 | Name = "My bucket" 42 | Environment = "Dev" 43 | } 44 | } 45 | 46 | resource "aws_iam_policy" "policy" { 47 | name = "test-policy" 48 | description = "A test policy" 49 | 50 | policy = jsonencode({ 51 | Version = "2012-10-17" 52 | Statement = [ 53 | { 54 | Action = [ 55 | "s3:GetObject", 56 | ] 57 | Effect = "Allow" 58 | Resource = "${aws_s3_bucket.b.id}" 59 | } 60 | ] 61 | }) 62 | } 63 | ``` 64 | 65 | ## Best practice 66 | 67 | * **Do not** re-use the Run Tasks URL across different trust-boundary (organizations, accounts, team). We recommend you to deploy separate Run Task deployment per trust-boundary. 68 | 69 | * **Do not** use Run Tasks URL from untrusted party, remember that Run Tasks execution sent Terraform plan output to the Run Task endpoint. Only use trusted Run Tasks URL. 70 | 71 | * Enable the AWS WAF setup by setting variable `deploy_waf` to `true` (additional cost will apply). This will add WAF protection to the Run Tasks URL endpoint. 72 | 73 | * We recommend you to setup additional CloudWatch alarm to monitor Lambda concurrency and WAF rules. 74 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fail_fast: false 3 | minimum_pre_commit_version: "2.6.0" 4 | repos: 5 | - 6 | repo: https://github.com/terraform-docs/terraform-docs 7 | # To update run: 8 | # pre-commit autoupdate --freeze 9 | rev: 212db41760d7fc45d736d5eb94a483d0d2a12049 # frozen: v0.16.0 10 | hooks: 11 | - id: terraform-docs-go 12 | args: 13 | - "--config=.config/.terraform-docs.yaml" 14 | - "--lockfile=false" 15 | - "--recursive" 16 | - "--recursive-path=examples/" 17 | - "./" -------------------------------------------------------------------------------- /.project_automation/deprecation/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | ## NOTE: paths may differ when running in a managed task. To ensure behavior is consistent between 4 | # managed and local tasks always use these variables for the project and project type path 5 | PROJECT_PATH=${BASE_PATH}/project 6 | PROJECT_TYPE_PATH=${BASE_PATH}/projecttype 7 | -------------------------------------------------------------------------------- /.project_automation/deprovision/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | ## NOTE: paths may differ when running in a managed task. To ensure behavior is consistent between 4 | # managed and local tasks always use these variables for the project and project type path 5 | PROJECT_PATH=${BASE_PATH}/project 6 | PROJECT_TYPE_PATH=${BASE_PATH}/projecttype 7 | -------------------------------------------------------------------------------- /.project_automation/functional_tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/codebuild/amazonlinux2-x86_64-standard:4.0 2 | ENV TERRAFORM_VERSION=1.7.4 3 | RUN cd /tmp && \ 4 | wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \ 5 | unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip -d /usr/local/bin && chmod 755 /usr/local/bin/terraform -------------------------------------------------------------------------------- /.project_automation/functional_tests/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## WARNING: DO NOT modify the content of entrypoint.sh 4 | # Use ./config/functional_tests/pre-entrypoint-helpers.sh or ./config/functional_tests/post-entrypoint-helpers.sh 5 | # to load any customizations or additional configurations 6 | 7 | ## NOTE: paths may differ when running in a managed task. To ensure behavior is consistent between 8 | # managed and local tasks always use these variables for the project and project type path 9 | PROJECT_PATH=${BASE_PATH}/project 10 | PROJECT_TYPE_PATH=${BASE_PATH}/projecttype 11 | 12 | #********** helper functions ************* 13 | pre_entrypoint() { 14 | if [ -f ${PROJECT_PATH}/.config/functional_tests/pre-entrypoint-helpers.sh ]; then 15 | echo "Pre-entrypoint helper found" 16 | source ${PROJECT_PATH}/.config/functional_tests/pre-entrypoint-helpers.sh 17 | echo "Pre-entrypoint helper loaded" 18 | else 19 | echo "Pre-entrypoint helper not found - skipped" 20 | fi 21 | } 22 | post_entrypoint() { 23 | if [ -f ${PROJECT_PATH}/.config/functional_tests/post-entrypoint-helpers.sh ]; then 24 | echo "Post-entrypoint helper found" 25 | source ${PROJECT_PATH}/.config/functional_tests/post-entrypoint-helpers.sh 26 | echo "Post-entrypoint helper loaded" 27 | else 28 | echo "Post-entrypoint helper not found - skipped" 29 | fi 30 | } 31 | 32 | #********** Pre-entrypoint helper ************* 33 | pre_entrypoint 34 | 35 | #********** Functional Test ************* 36 | /bin/bash ${PROJECT_PATH}/.project_automation/functional_tests/functional_tests.sh 37 | if [ $? -eq 0 ] 38 | then 39 | echo "Functional test completed" 40 | EXIT_CODE=0 41 | else 42 | echo "Functional test failed" 43 | EXIT_CODE=1 44 | fi 45 | 46 | #********** Post-entrypoint helper ************* 47 | post_entrypoint 48 | 49 | #********** Exit Code ************* 50 | exit $EXIT_CODE 51 | -------------------------------------------------------------------------------- /.project_automation/functional_tests/functional_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## NOTE: paths may differ when running in a managed task. To ensure behavior is consistent between 4 | # managed and local tasks always use these variables for the project and project type path 5 | PROJECT_PATH=${BASE_PATH}/project 6 | PROJECT_TYPE_PATH=${BASE_PATH}/projecttype 7 | 8 | echo "Starting Functional Tests" 9 | cd ${PROJECT_PATH} 10 | 11 | #********** Terraform Test ********** 12 | 13 | # Look up the mandatory test file 14 | MANDATORY_TEST_PATH="./tests/01_mandatory.tftest.hcl" 15 | if test -f ${MANDATORY_TEST_PATH}; then 16 | echo "File ${MANDATORY_TEST_PATH} is found, resuming test" 17 | # Run Terraform test 18 | terraform init 19 | terraform test 20 | else 21 | echo "File ${MANDATORY_TEST_PATH} not found. You must include at least one test run in file ${MANDATORY_TEST_PATH}" 22 | (exit 1) 23 | fi 24 | 25 | if [ $? -eq 0 ]; then 26 | echo "Terraform Test Successfull" 27 | else 28 | echo "Terraform Test Failed" 29 | exit 1 30 | fi 31 | 32 | echo "End of Functional Tests" -------------------------------------------------------------------------------- /.project_automation/init/noop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Not Supported!" 3 | -------------------------------------------------------------------------------- /.project_automation/provision/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | ## NOTE: paths may differ when running in a managed task. To ensure behavior is consistent between 4 | # managed and local tasks always use these variables for the project and project type path 5 | PROJECT_PATH=${BASE_PATH}/project 6 | PROJECT_TYPE_PATH=${BASE_PATH}/projecttype 7 | -------------------------------------------------------------------------------- /.project_automation/publication/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/codebuild/amazonlinux2-x86_64-standard:4.0 2 | RUN yum install -y yum-utils && yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo && yum install -y gh 3 | RUN pip install awscli 4 | -------------------------------------------------------------------------------- /.project_automation/publication/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | ## NOTE: paths may differ when running in a managed task. To ensure behavior is consistent between 4 | # managed and local tasks always use these variables for the project and project type path 5 | PROJECT_PATH=${BASE_PATH}/project 6 | PROJECT_TYPE_PATH=${BASE_PATH}/projecttype 7 | 8 | echo "[STAGE: Publication]" 9 | VERSION=$(cat VERSION) 10 | echo $VERSION 11 | BRANCH=main 12 | EXISTING_GIT_VERSION="$(git tag -l)" 13 | 14 | if [[ $(echo $EXISTING_GIT_VERSION | grep $VERSION) ]] 15 | then 16 | echo "version exists skipping release creation hint: Bump version in VERSION file" 17 | else 18 | echo "creating new version" 19 | gh release create ${VERSION} --target ${BRANCH} --generate-notes 20 | fi 21 | -------------------------------------------------------------------------------- /.project_automation/static_tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/codebuild/amazonlinux2-x86_64-standard:4.0 2 | ENV TERRAFORM_VERSION=1.7.4 3 | RUN cd /tmp && \ 4 | wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \ 5 | unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip -d /usr/local/bin && chmod 755 /usr/local/bin/terraform 6 | 7 | ENV TFLINT_VERSION=v0.45.0 8 | 9 | RUN cd /tmp && \ 10 | wget https://github.com/terraform-linters/tflint/releases/download/${TFLINT_VERSION}/tflint_linux_amd64.zip && \ 11 | unzip tflint_linux_amd64.zip -d /usr/local/bin && chmod 755 /usr/local/bin/tflint 12 | 13 | RUN mkdir -p ~/.tflint.d/plugins 14 | 15 | ENV TFLINT_VERSION=v0.22.1 16 | 17 | RUN wget -O /tmp/tflint-ruleset-aws.zip https://github.com/terraform-linters/tflint-ruleset-aws/releases/download/${TFLINT_VERSION}/tflint-ruleset-aws_darwin_arm64.zip \ 18 | && unzip /tmp/tflint-ruleset-aws.zip -d ~/.tflint.d/plugins \ 19 | && rm /tmp/tflint-ruleset-aws.zip 20 | 21 | RUN curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash 22 | 23 | RUN pip3 install checkov 24 | 25 | RUN gem install mdl 26 | 27 | ENV TERRAFORM_DOCS_VERSION=v0.16.0 28 | RUN wget https://github.com/terraform-docs/terraform-docs/releases/download/${TERRAFORM_DOCS_VERSION}/terraform-docs-${TERRAFORM_DOCS_VERSION}-linux-amd64.tar.gz && \ 29 | tar -C /usr/local/bin -xzf terraform-docs-${TERRAFORM_DOCS_VERSION}-linux-amd64.tar.gz && chmod +x /usr/local/bin/terraform-docs 30 | -------------------------------------------------------------------------------- /.project_automation/static_tests/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## WARNING: DO NOT modify the content of entrypoint.sh 4 | # Use ./config/static_tests/pre-entrypoint-helpers.sh or ./config/static_tests/post-entrypoint-helpers.sh 5 | # to load any customizations or additional configurations 6 | 7 | ## NOTE: paths may differ when running in a managed task. To ensure behavior is consistent between 8 | # managed and local tasks always use these variables for the project and project type path 9 | PROJECT_PATH=${BASE_PATH}/project 10 | PROJECT_TYPE_PATH=${BASE_PATH}/projecttype 11 | 12 | #********** helper functions ************* 13 | pre_entrypoint() { 14 | if [ -f ${PROJECT_PATH}/.config/static_tests/pre-entrypoint-helpers.sh ]; then 15 | echo "Pre-entrypoint helper found" 16 | source ${PROJECT_PATH}/.config/static_tests/pre-entrypoint-helpers.sh 17 | echo "Pre-entrypoint helper loaded" 18 | else 19 | echo "Pre-entrypoint helper not found - skipped" 20 | fi 21 | } 22 | post_entrypoint() { 23 | if [ -f ${PROJECT_PATH}/.config/static_tests/post-entrypoint-helpers.sh ]; then 24 | echo "Post-entrypoint helper found" 25 | source ${PROJECT_PATH}/.config/static_tests/post-entrypoint-helpers.sh 26 | echo "Post-entrypoint helper loaded" 27 | else 28 | echo "Post-entrypoint helper not found - skipped" 29 | fi 30 | } 31 | 32 | #********** Pre-entrypoint helper ************* 33 | pre_entrypoint 34 | 35 | #********** Static Test ************* 36 | /bin/bash ${PROJECT_PATH}/.project_automation/static_tests/static_tests.sh 37 | if [ $? -eq 0 ] 38 | then 39 | echo "Static test completed" 40 | EXIT_CODE=0 41 | else 42 | echo "Static test failed" 43 | EXIT_CODE=1 44 | fi 45 | 46 | #********** Post-entrypoint helper ************* 47 | post_entrypoint 48 | 49 | #********** Exit Code ************* 50 | exit $EXIT_CODE -------------------------------------------------------------------------------- /.project_automation/static_tests/static_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## NOTE: paths may differ when running in a managed task. To ensure behavior is consistent between 4 | # managed and local tasks always use these variables for the project and project type path 5 | PROJECT_PATH=${BASE_PATH}/project 6 | PROJECT_TYPE_PATH=${BASE_PATH}/projecttype 7 | 8 | echo "Starting Static Tests" 9 | 10 | #********** Terraform Validate ************* 11 | cd ${PROJECT_PATH} 12 | terraform init 13 | terraform validate 14 | if [ $? -eq 0 ] 15 | then 16 | echo "Success - Terraform validate" 17 | else 18 | echo "Failure - Terraform validate" 19 | exit 1 20 | fi 21 | 22 | #********** tflint ******************** 23 | echo 'Starting tflint' 24 | tflint --init --config ${PROJECT_PATH}/.config/.tflint.hcl 25 | MYLINT=$(tflint --force --config ${PROJECT_PATH}/.config/.tflint.hcl) 26 | if [ -z "$MYLINT" ] 27 | then 28 | echo "Success - tflint found no linting issues!" 29 | else 30 | echo "Failure - tflint found linting issues!" 31 | echo "$MYLINT" 32 | exit 1 33 | fi 34 | 35 | #********** tfsec ********************* 36 | echo 'Starting tfsec' 37 | MYTFSEC=$(tfsec . --config-file ${PROJECT_PATH}/.config/.tfsec.yml --custom-check-dir ${PROJECT_PATH}/.config/.tfsec) 38 | if [[ $MYTFSEC == *"No problems detected!"* ]]; 39 | then 40 | echo "Success - tfsec found no security issues!" 41 | echo "$MYTFSEC" 42 | else 43 | echo "Failure - tfsec found security issues!" 44 | echo "$MYTFSEC" 45 | exit 1 46 | fi 47 | 48 | #********** Checkov Analysis ************* 49 | echo "Running Checkov Analysis" 50 | checkov --config-file ${PROJECT_PATH}/.config/.checkov.yml 51 | if [ $? -eq 0 ] 52 | then 53 | echo "Success - Checkov found no issues!" 54 | else 55 | echo "Failure - Checkov found issues!" 56 | exit 1 57 | fi 58 | 59 | #********** Markdown Lint ************** 60 | echo 'Starting markdown lint' 61 | MYMDL=$(mdl --config ${PROJECT_PATH}/.config/.mdlrc .header.md examples/*/.header.md) 62 | if [ -z "$MYMDL" ] 63 | then 64 | echo "Success - markdown lint found no linting issues!" 65 | else 66 | echo "Failure - markdown lint found linting issues!" 67 | echo "$MYMDL" 68 | exit 1 69 | fi 70 | 71 | #********** Terraform Docs ************* 72 | echo 'Starting terraform-docs' 73 | TDOCS="$(terraform-docs --config ${PROJECT_PATH}/.config/.terraform-docs.yaml --lockfile=false ./)" 74 | git add -N README.md 75 | GDIFF="$(git diff --compact-summary)" 76 | if [ -z "$GDIFF" ] 77 | then 78 | echo "Success - Terraform Docs creation verified!" 79 | else 80 | echo "Failure - Terraform Docs creation failed, ensure you have precommit installed and running before submitting the Pull Request. TIPS: false error may occur if you have unstaged files in your repo" 81 | echo "$GDIFF" 82 | exit 1 83 | fi 84 | 85 | #*************************************** 86 | echo "End of Static Tests" -------------------------------------------------------------------------------- /.project_automation/update/noop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Not Supported!" 3 | -------------------------------------------------------------------------------- /.project_config.yml: -------------------------------------------------------------------------------- 1 | version: "1.0.0" 2 | 3 | init: 4 | entrypoint: .project_automation/init/noop.sh 5 | update: 6 | entrypoint: .project_automation/update/noop.sh 7 | static_tests: 8 | dockerfile: .project_automation/static_tests/Dockerfile 9 | entrypoint: .project_automation/static_tests/entrypoint.sh 10 | functional_tests: 11 | github_permissions: 12 | contents: write 13 | dockerfile: .project_automation/functional_tests/Dockerfile 14 | entrypoint: .project_automation/functional_tests/entrypoint.sh 15 | publication: 16 | github_permissions: 17 | contents: write 18 | dockerfile: .project_automation/publication/Dockerfile 19 | entrypoint: .project_automation/publication/entrypoint.sh 20 | deprecation: 21 | entrypoint: .project_automation/deprecation/entrypoint.sh 22 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @aws-ia/aws-ia -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Creating modules for Terraform 2 | 3 | This repository contains code for an application that is published using the Application Builder Platform (ABP). 4 | 5 | ## Module Standards 6 | 7 | For best practices and information on developing with Terraform, see the [I&A Module Standards](https://aws-ia.github.io/standards-terraform/) 8 | 9 | ## Contributing Code 10 | 11 | In order to contibute code to this repository, you must submit a *[Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)*. To do so, you must *[fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo)* this repostiory, make your changes in your forked version and submit a *Pull Request*. 12 | 13 | ## Writing Documentation 14 | 15 | > :bangbang: **Do not manually update README.md**. 16 | 17 | README.md is automatically generated by pulling in content from other files. For instructions, including a fill-in-the-blank content template, see [Create readmes for Terraform-based Partner Solutions.](https://aws-ia-us-west-2.s3.us-west-2.amazonaws.com/docs/content/index.html#/lessons/8rpYWWL59M7dcS-NsjYmaISUu-L_UqEv) 18 | 19 | 20 | ## Checks and Validation 21 | 22 | Pull Requests (PRs) submitted against this repository undergo a series of static and functional checks. 23 | 24 | > :exclamation: Note: Failures during funtional or static checks will prevent a pull request from being accepted. 25 | 26 | It is a best practice to perform these checks locally prior to submitting a pull request. 27 | 28 | ## Checks Performed 29 | - TFLint 30 | - tfsec 31 | - Markdown Lint 32 | - Checkov 33 | - Terratest 34 | 35 | > :bangbang: The readme.md file will be created after all checks have completed successfuly, it is recommended that you install terraform-docs locally in order to preview your readme.md file prior to publication. 36 | 37 | ## Install the required tools 38 | 39 | Prerequisites: 40 | - [Python](https://docs.python.org/3/using/index.html) 41 | - [Pip](https://pip.pypa.io/en/stable/installation/) 42 | - [golang](https://go.dev/doc/install) (for macos you can use `brew`) 43 | - [tflint](https://github.com/terraform-linters/tflint) 44 | - [tfsec](https://aquasecurity.github.io/tfsec/v1.0.11/) 45 | - [Markdown Lint](https://github.com/markdownlint/markdownlint) 46 | - [Checkov](https://www.checkov.io/2.Basics/Installing%20Checkov.html) 47 | - [terraform-docs](https://github.com/terraform-docs/terraform-docs) 48 | - [coreutils](https://www.gnu.org/software/coreutils/) 49 | 50 | ## Performing Checks manually 51 | 52 | Preparation 53 | ``` 54 | terraform init 55 | terraform validate 56 | ``` 57 | ## Checks 58 | 59 | ### tflint 60 | ``` 61 | tflint --init 62 | tflint 63 | ``` 64 | ### tfsec 65 | ``` 66 | tfsec . 67 | ``` 68 | ### Markdown Lint 69 | ``` 70 | mdl .header.md 71 | ``` 72 | ### Checkov 73 | ``` 74 | terraform init 75 | terraform plan -out tf.plan 76 | terraform show -json tf.plan > tf.json 77 | checkov 78 | ``` 79 | ### Terratest 80 | 81 | Include tests to validate your examples/<> root modules, at a minimum. This can be accomplished with usually only slight modifications to the [boilerplate test provided in this template](./test/examples\_basic\_test.go) 82 | 83 | ``` 84 | # from the root of the repository 85 | cd test 86 | go mod init github.com/aws-ia/terraform-project-ephemeral 87 | go mod tidy 88 | go install github.com/gruntwork-io/terratest/modules/terraform 89 | go test -timeout 45m 90 | ``` 91 | 92 | ## Documentation 93 | 94 | ### terraform-docs 95 | 96 | ``` 97 | # from the root of the repository 98 | terraform-docs --lockfile=false ./ 99 | ``` 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TOPTARGETS := all clean build 2 | 3 | SUBDIRS := $(wildcard lambda/*/.) 4 | BASE = $(shell /bin/pwd) 5 | 6 | $(TOPTARGETS): $(SUBDIRS) 7 | 8 | $(SUBDIRS): 9 | $(MAKE) -C $@ $(MAKECMDGOALS) $(ARGS) BASE="${BASE}" 10 | 11 | .PHONY: $(TOPTARGETS) $(SUBDIRS) 12 | 13 | clean: 14 | rm -f .terraform.lock.hcl 15 | rm -rf .terraform 16 | rm -rf ./lambda/*.zip 17 | rm -f ./test/go.mod 18 | rm -f ./test/go.sum 19 | rm -f tf.json 20 | rm -f tf.plan 21 | rm -f *.tfvars -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 4 | 5 | http://aws.amazon.com/apache2.0/ 6 | 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. 8 | 9 | ********************** 10 | THIRD PARTY COMPONENTS 11 | ********************** 12 | This software includes third party software subject to the following copyrights: 13 | 14 | @yaml/pyyaml under the Massachusetts Institute of Technology (MIT) license -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # terraform-runtask-iam-access-analyzer 3 | 4 | Use this module to integrate HCP Terraform Run Tasks with AWS IAM Access Analyzer for policy validation. 5 | 6 | ![Diagram](./diagram/RunTask-EventBridge.png) 7 | 8 | ## Prerequisites 9 | 10 | To use this module you need have the following: 11 | 12 | 1. AWS account and credentials 13 | 2. HCP Terraform with Run Task entitlement (Business subscription or higher) 14 | 15 | ## Usage 16 | 17 | * Build and package the Lambda files 18 | 19 | ``` 20 | make all 21 | ``` 22 | 23 | * Refer to the [module\_workspace](./examples/module\_workspace/README.md) for steps to deploy this module in HCP Terraform. 24 | 25 | * After you deployed the [module\_workspace](./examples/module\_workspace/README.md), navigate to your HCP Terraform organization, go to Organization Settings > Integrations > Run tasks to find the newly created Run Task. 26 | 27 | * You can use this run task in any workspace where you have standard IAM resource policy document. Refer to the [demo\_workspace](./examples/demo\_workspace/README.md) for more details. 28 | 29 | ## Limitations 30 | 31 | 1. Does not provide verbose error / warning messages in Run Task console. In the future, we will explore possibility to provide verbose logging. 32 | 33 | 2. Does not support Terraform [computed resources](https://www.terraform.io/plugin/sdkv2/schemas/schema-behaviors). 34 | 35 | For example, the tool will report no IAM policy found for the following Terraform template. The policy json string is a computed resource. The plan output doesn't contain information of IAM policy document. 36 | 37 | ``` 38 | resource "aws_s3_bucket" "b" { 39 | bucket = "my-tf-test-bucket" 40 | 41 | tags = { 42 | Name = "My bucket" 43 | Environment = "Dev" 44 | } 45 | } 46 | 47 | resource "aws_iam_policy" "policy" { 48 | name = "test-policy" 49 | description = "A test policy" 50 | 51 | policy = jsonencode({ 52 | Version = "2012-10-17" 53 | Statement = [ 54 | { 55 | Action = [ 56 | "s3:GetObject", 57 | ] 58 | Effect = "Allow" 59 | Resource = "${aws_s3_bucket.b.id}" 60 | } 61 | ] 62 | }) 63 | } 64 | ``` 65 | 66 | ## Best practice 67 | 68 | * **Do not** re-use the Run Tasks URL across different trust-boundary (organizations, accounts, team). We recommend you to deploy separate Run Task deployment per trust-boundary. 69 | 70 | * **Do not** use Run Tasks URL from untrusted party, remember that Run Tasks execution sent Terraform plan output to the Run Task endpoint. Only use trusted Run Tasks URL. 71 | 72 | * Enable the AWS WAF setup by setting variable `deploy_waf` to `true` (additional cost will apply). This will add WAF protection to the Run Tasks URL endpoint. 73 | 74 | * We recommend you to setup additional CloudWatch alarm to monitor Lambda concurrency and WAF rules. 75 | 76 | ## Requirements 77 | 78 | | Name | Version | 79 | |------|---------| 80 | | [terraform](#requirement\_terraform) | >= 1.0.7 | 81 | | [archive](#requirement\_archive) | ~>2.2.0 | 82 | | [aws](#requirement\_aws) | >=5.72.0 | 83 | | [random](#requirement\_random) | >=3.4.0 | 84 | | [tfe](#requirement\_tfe) | >=0.38.0 | 85 | | [time](#requirement\_time) | >=0.12.0 | 86 | 87 | ## Providers 88 | 89 | | Name | Version | 90 | |------|---------| 91 | | [archive](#provider\_archive) | ~>2.2.0 | 92 | | [aws](#provider\_aws) | >=5.72.0 | 93 | | [aws.cloudfront\_waf](#provider\_aws.cloudfront\_waf) | >=5.72.0 | 94 | | [random](#provider\_random) | >=3.4.0 | 95 | | [tfe](#provider\_tfe) | >=0.38.0 | 96 | | [time](#provider\_time) | >=0.12.0 | 97 | 98 | ## Modules 99 | 100 | | Name | Source | Version | 101 | |------|--------|---------| 102 | | [runtask\_cloudfront](#module\_runtask\_cloudfront) | terraform-aws-modules/cloudfront/aws | 3.4.0 | 103 | 104 | ## Resources 105 | 106 | | Name | Type | 107 | |------|------| 108 | | [aws_cloudfront_origin_request_policy.runtask_cloudfront](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_request_policy) | resource | 109 | | [aws_cloudwatch_event_rule.runtask_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | 110 | | [aws_cloudwatch_event_target.runtask_target](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | 111 | | [aws_cloudwatch_log_group.runtask_callback](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | 112 | | [aws_cloudwatch_log_group.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | 113 | | [aws_cloudwatch_log_group.runtask_fulfillment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | 114 | | [aws_cloudwatch_log_group.runtask_fulfillment_output](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | 115 | | [aws_cloudwatch_log_group.runtask_request](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | 116 | | [aws_cloudwatch_log_group.runtask_states](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | 117 | | [aws_cloudwatch_log_group.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | 118 | | [aws_cloudwatch_log_resource_policy.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_resource_policy) | resource | 119 | | [aws_iam_role.runtask_callback](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 120 | | [aws_iam_role.runtask_edge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 121 | | [aws_iam_role.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 122 | | [aws_iam_role.runtask_fulfillment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 123 | | [aws_iam_role.runtask_request](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 124 | | [aws_iam_role.runtask_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 125 | | [aws_iam_role.runtask_states](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 126 | | [aws_iam_role_policy.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 127 | | [aws_iam_role_policy.runtask_fulfillment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 128 | | [aws_iam_role_policy.runtask_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 129 | | [aws_iam_role_policy.runtask_states](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 130 | | [aws_iam_role_policy_attachment.runtask_callback](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | 131 | | [aws_iam_role_policy_attachment.runtask_edge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | 132 | | [aws_iam_role_policy_attachment.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | 133 | | [aws_iam_role_policy_attachment.runtask_fulfillment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | 134 | | [aws_iam_role_policy_attachment.runtask_request](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | 135 | | [aws_kms_alias.runtask_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource | 136 | | [aws_kms_alias.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource | 137 | | [aws_kms_key.runtask_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | 138 | | [aws_kms_key.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | 139 | | [aws_lambda_function.runtask_callback](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | 140 | | [aws_lambda_function.runtask_edge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | 141 | | [aws_lambda_function.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | 142 | | [aws_lambda_function.runtask_fulfillment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | 143 | | [aws_lambda_function.runtask_request](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | 144 | | [aws_lambda_function_url.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function_url) | resource | 145 | | [aws_lambda_permission.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | 146 | | [aws_secretsmanager_secret.runtask_cloudfront](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | 147 | | [aws_secretsmanager_secret.runtask_hmac](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | 148 | | [aws_secretsmanager_secret_version.runtask_cloudfront](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource | 149 | | [aws_secretsmanager_secret_version.runtask_hmac](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource | 150 | | [aws_sfn_state_machine.runtask_states](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sfn_state_machine) | resource | 151 | | [aws_wafv2_web_acl.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl) | resource | 152 | | [aws_wafv2_web_acl_logging_configuration.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration) | resource | 153 | | [random_string.solution_prefix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | 154 | | [random_uuid.runtask_cloudfront](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/uuid) | resource | 155 | | [random_uuid.runtask_hmac](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/uuid) | resource | 156 | | [tfe_organization_run_task.aws_iam_analyzer](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/organization_run_task) | resource | 157 | | [time_sleep.wait_1800_seconds](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | 158 | | [archive_file.runtask_callback](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 159 | | [archive_file.runtask_edge](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 160 | | [archive_file.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 161 | | [archive_file.runtask_fulfillment](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 162 | | [archive_file.runtask_request](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | 163 | | [aws_caller_identity.current_account](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | 164 | | [aws_iam_policy.aws_lambda_basic_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | 165 | | [aws_iam_policy_document.runtask_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 166 | | [aws_iam_policy_document.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 167 | | [aws_iam_policy_document.runtask_waf_log](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 168 | | [aws_partition.current_partition](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | 169 | | [aws_region.cloudfront_region](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | 170 | | [aws_region.current_region](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | 171 | 172 | ## Inputs 173 | 174 | | Name | Description | Type | Default | Required | 175 | |------|-------------|------|---------|:--------:| 176 | | [aws\_region](#input\_aws\_region) | The region from which this module will be executed. | `string` | n/a | yes | 177 | | [tfc\_org](#input\_tfc\_org) | Terraform Organization name | `string` | n/a | yes | 178 | | [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | RunTask CloudWatch log group name | `string` | `"/hashicorp/terraform/runtask/iam-access-analyzer/"` | no | 179 | | [cloudwatch\_log\_group\_retention](#input\_cloudwatch\_log\_group\_retention) | Lambda CloudWatch log group retention period | `string` | `"365"` | no | 180 | | [deploy\_waf](#input\_deploy\_waf) | Set to true to deploy CloudFront and WAF in front of the Lambda function URL | `string` | `false` | no | 181 | | [event\_bus\_name](#input\_event\_bus\_name) | EventBridge event bus name | `string` | `"default"` | no | 182 | | [event\_source](#input\_event\_source) | EventBridge source name | `string` | `"app.terraform.io"` | no | 183 | | [lambda\_architecture](#input\_lambda\_architecture) | Lambda architecture (arm64 or x86\_64) | `string` | `"x86_64"` | no | 184 | | [lambda\_default\_timeout](#input\_lambda\_default\_timeout) | Lambda default timeout in seconds | `number` | `30` | no | 185 | | [lambda\_reserved\_concurrency](#input\_lambda\_reserved\_concurrency) | Maximum Lambda reserved concurrency, make sure your AWS quota is sufficient | `number` | `100` | no | 186 | | [name\_prefix](#input\_name\_prefix) | Name to be used on all the resources as identifier. | `string` | `"aws-ia2"` | no | 187 | | [recovery\_window](#input\_recovery\_window) | Numbers of day Number of days that AWS Secrets Manager waits before it can delete the secret | `number` | `0` | no | 188 | | [runtask\_stages](#input\_runtask\_stages) | List of all supported RunTask stages | `list(string)` |
[
"pre_plan",
"post_plan",
"pre_apply"
]
| no | 189 | | [supported\_policy\_document](#input\_supported\_policy\_document) | (Optional) allow list of the supported IAM policy document | `string` | `""` | no | 190 | | [tags](#input\_tags) | Map of tags to apply to resources deployed by this solution. | `map(any)` | `null` | no | 191 | | [waf\_managed\_rule\_set](#input\_waf\_managed\_rule\_set) | List of AWS Managed rules to use inside the WAF ACL | `list(map(string))` |
[
{
"metric_suffix": "common",
"name": "AWSManagedRulesCommonRuleSet",
"priority": 10,
"vendor_name": "AWS"
},
{
"metric_suffix": "bad_input",
"name": "AWSManagedRulesKnownBadInputsRuleSet",
"priority": 20,
"vendor_name": "AWS"
}
]
| no | 192 | | [waf\_rate\_limit](#input\_waf\_rate\_limit) | Rate limit for request coming to WAF | `number` | `100` | no | 193 | | [workspace\_prefix](#input\_workspace\_prefix) | TFC workspace name prefix that allowed to run this runtask | `string` | `""` | no | 194 | 195 | ## Outputs 196 | 197 | | Name | Description | 198 | |------|-------------| 199 | | [runtask\_hmac](#output\_runtask\_hmac) | HMAC key value, keep this sensitive data safe | 200 | | [runtask\_id](#output\_runtask\_id) | The Run Tasks id configured in HCP Terraform | 201 | | [runtask\_url](#output\_runtask\_url) | The Run Tasks URL endpoint, you can use this to configure the Run Task setup in HCP Terraform | 202 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v0.1.0 2 | -------------------------------------------------------------------------------- /cloudfront.tf: -------------------------------------------------------------------------------- 1 | module "runtask_cloudfront" { 2 | depends_on = [time_sleep.wait_1800_seconds] 3 | #checkov:skip=CKV2_AWS_42:custom domain name is optional 4 | 5 | count = local.waf_deployment 6 | source = "terraform-aws-modules/cloudfront/aws" 7 | version = "3.4.0" 8 | 9 | comment = "CloudFront for RunTask integration: ${var.name_prefix}" 10 | enabled = true 11 | price_class = "PriceClass_100" 12 | retain_on_delete = false 13 | wait_for_deployment = true 14 | web_acl_id = aws_wafv2_web_acl.runtask_waf[count.index].arn 15 | 16 | create_origin_access_control = true 17 | origin_access_control = { 18 | lambda_oac_access_analyzer = { 19 | description = "CloudFront OAC to Lambda AWS-IA Access Analyzer" 20 | origin_type = "lambda" 21 | signing_behavior = "always" 22 | signing_protocol = "sigv4" 23 | } 24 | } 25 | 26 | origin = { 27 | runtask_eventbridge = { 28 | domain_name = split("/", aws_lambda_function_url.runtask_eventbridge.function_url)[2] 29 | custom_origin_config = { 30 | http_port = 80 31 | https_port = 443 32 | origin_protocol_policy = "https-only" 33 | origin_ssl_protocols = ["TLSv1"] 34 | } 35 | origin_access_control = "lambda_oac_access_analyzer" 36 | custom_header = var.deploy_waf ? [local.cloudfront_custom_header] : null 37 | } 38 | } 39 | 40 | default_cache_behavior = { 41 | target_origin_id = "runtask_eventbridge" 42 | viewer_protocol_policy = "https-only" 43 | 44 | #SecurityHeadersPolicy: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-response-headers-policies.html#managed-response-headers-policies-security 45 | response_headers_policy_id = "67f7725c-6f97-4210-82d7-5512b31e9d03" 46 | 47 | # caching disabled: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html#managed-cache-policy-caching-disabled 48 | cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" 49 | 50 | origin_request_policy_id = aws_cloudfront_origin_request_policy.runtask_cloudfront[count.index].id 51 | use_forwarded_values = false 52 | 53 | allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"] 54 | cached_methods = ["GET", "HEAD", "OPTIONS"] 55 | 56 | lambda_function_association = { 57 | # This function will append header x-amz-content-sha256 to allow OAC to authenticate with Lambda Function URL 58 | viewer-request = { 59 | lambda_arn = aws_lambda_function.runtask_edge.qualified_arn 60 | include_body = true 61 | } 62 | } 63 | } 64 | 65 | viewer_certificate = { 66 | cloudfront_default_certificate = true 67 | minimum_protocol_version = "TLSv1" 68 | } 69 | tags = local.combined_tags 70 | } 71 | 72 | resource "aws_cloudfront_origin_request_policy" "runtask_cloudfront" { 73 | count = local.waf_deployment 74 | name = "${var.name_prefix}-runtask_cloudfront_origin_request_policy" 75 | comment = "Forward all request headers except host" 76 | 77 | cookies_config { 78 | cookie_behavior = "all" 79 | } 80 | 81 | headers_config { 82 | header_behavior = "whitelist" 83 | headers { 84 | items = [ 85 | "x-tfc-task-signature", 86 | "content-type", 87 | "user-agent", 88 | "x-amzn-trace-id" 89 | ] 90 | } 91 | } 92 | 93 | query_strings_config { 94 | query_string_behavior = "all" 95 | } 96 | } 97 | 98 | resource "time_sleep" "wait_1800_seconds" { 99 | # wait for CloudFront Lambda@Edge removal that can take up to 30 mins / 1800s 100 | # before deleting the Lambda function 101 | depends_on = [aws_lambda_function.runtask_edge] 102 | destroy_duration = "1800s" 103 | } -------------------------------------------------------------------------------- /data.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current_region" {} 2 | 3 | data "aws_region" "cloudfront_region" { 4 | provider = aws.cloudfront_waf 5 | } 6 | 7 | data "aws_caller_identity" "current_account" {} 8 | 9 | data "aws_partition" "current_partition" {} 10 | 11 | data "aws_iam_policy" "aws_lambda_basic_execution_role" { 12 | name = "AWSLambdaBasicExecutionRole" 13 | } 14 | 15 | data "archive_file" "runtask_eventbridge" { 16 | type = "zip" 17 | source_dir = "${path.module}/lambda/runtask_eventbridge/site-packages/" 18 | output_path = "${path.module}/lambda/runtask_eventbridge.zip" 19 | } 20 | 21 | data "archive_file" "runtask_request" { 22 | type = "zip" 23 | source_dir = "${path.module}/lambda/runtask_request/site-packages/" 24 | output_path = "${path.module}/lambda/runtask_request.zip" 25 | } 26 | 27 | data "archive_file" "runtask_fulfillment" { 28 | type = "zip" 29 | source_dir = "${path.module}/lambda/runtask_fulfillment/site-packages/" 30 | output_path = "${path.module}/lambda/runtask_fulfillment.zip" 31 | } 32 | 33 | data "archive_file" "runtask_callback" { 34 | type = "zip" 35 | source_dir = "${path.module}/lambda/runtask_callback/site-packages" 36 | output_path = "${path.module}/lambda/runtask_callback.zip" 37 | } 38 | 39 | data "archive_file" "runtask_edge" { 40 | type = "zip" 41 | source_dir = "${path.module}/lambda/runtask_edge/site-packages" 42 | output_path = "${path.module}/lambda/runtask_edge.zip" 43 | } 44 | 45 | 46 | data "aws_iam_policy_document" "runtask_key" { 47 | #checkov:skip=CKV_AWS_109:Skip 48 | #checkov:skip=CKV_AWS_111:Skip 49 | statement { 50 | sid = "Enable IAM User Permissions" 51 | effect = "Allow" 52 | actions = [ 53 | "kms:Create*", 54 | "kms:Describe*", 55 | "kms:Enable*", 56 | "kms:List*", 57 | "kms:Put*", 58 | "kms:Update*", 59 | "kms:Revoke*", 60 | "kms:Disable*", 61 | "kms:Get*", 62 | "kms:Delete*", 63 | "kms:TagResource", 64 | "kms:UntagResource", 65 | "kms:ScheduleKeyDeletion", 66 | "kms:CancelKeyDeletion", 67 | "kms:Encrypt", 68 | "kms:Decrypt", 69 | "kms:ReEncrypt*", 70 | "kms:GenerateDataKey*" 71 | ] 72 | resources = ["*"] 73 | 74 | principals { 75 | type = "AWS" 76 | identifiers = [ 77 | "arn:${data.aws_partition.current_partition.id}:iam::${data.aws_caller_identity.current_account.account_id}:root" 78 | ] 79 | } 80 | } 81 | statement { 82 | sid = "Allow Service CloudWatchLogGroup" 83 | effect = "Allow" 84 | actions = [ 85 | "kms:Encrypt", 86 | "kms:Decrypt", 87 | "kms:ReEncrypt*", 88 | "kms:Describe", 89 | "kms:GenerateDataKey*" 90 | ] 91 | resources = ["*"] 92 | 93 | principals { 94 | type = "Service" 95 | identifiers = [ 96 | "logs.${data.aws_region.current_region.name}.amazonaws.com" 97 | ] 98 | } 99 | condition { 100 | test = "ArnEquals" 101 | variable = "kms:EncryptionContext:aws:logs:arn" 102 | values = [ 103 | "arn:${data.aws_partition.current_partition.id}:logs:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:log-group:/aws/lambda/${var.name_prefix}*", 104 | "arn:${data.aws_partition.current_partition.id}:logs:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:log-group:/aws/state/${var.name_prefix}*", 105 | "arn:${data.aws_partition.current_partition.id}:logs:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:log-group:/aws/vendedlogs/states/${var.name_prefix}*", 106 | "arn:${data.aws_partition.current_partition.id}:logs:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:log-group:${var.cloudwatch_log_group_name}*" 107 | ] 108 | } 109 | } 110 | statement { 111 | sid = "Allow Service Secrets Manager" 112 | effect = "Allow" 113 | actions = [ 114 | "kms:Decrypt", 115 | "kms:ReEncrypt*", 116 | "kms:GenerateDataKey*", 117 | "kms:CreateGrant", 118 | "kms:Describe" 119 | ] 120 | resources = ["*"] 121 | 122 | principals { 123 | type = "AWS" 124 | identifiers = [ 125 | aws_iam_role.runtask_eventbridge.arn 126 | ] 127 | } 128 | 129 | condition { 130 | test = "StringEquals" 131 | variable = "kms:ViaService" 132 | values = [ 133 | "secretsmanager.${data.aws_region.current_region.name}.amazonaws.com" 134 | ] 135 | } 136 | 137 | condition { 138 | test = "StringEquals" 139 | variable = "kms:CallerAccount" 140 | values = [ 141 | data.aws_caller_identity.current_account.account_id 142 | ] 143 | } 144 | } 145 | } 146 | 147 | data "aws_iam_policy_document" "runtask_waf" { 148 | #checkov:skip=CKV_AWS_109:Skip 149 | #checkov:skip=CKV_AWS_111:Skip 150 | count = local.waf_deployment 151 | provider = aws.cloudfront_waf 152 | statement { 153 | sid = "Enable IAM User Permissions" 154 | effect = "Allow" 155 | actions = [ 156 | "kms:Create*", 157 | "kms:Describe*", 158 | "kms:Enable*", 159 | "kms:List*", 160 | "kms:Put*", 161 | "kms:Update*", 162 | "kms:Revoke*", 163 | "kms:Disable*", 164 | "kms:Get*", 165 | "kms:Delete*", 166 | "kms:TagResource", 167 | "kms:UntagResource", 168 | "kms:ScheduleKeyDeletion", 169 | "kms:CancelKeyDeletion", 170 | "kms:Encrypt", 171 | "kms:Decrypt", 172 | "kms:ReEncrypt*", 173 | "kms:GenerateDataKey*" 174 | ] 175 | resources = ["*"] 176 | 177 | principals { 178 | type = "AWS" 179 | identifiers = [ 180 | "arn:${data.aws_partition.current_partition.id}:iam::${data.aws_caller_identity.current_account.account_id}:root" 181 | ] 182 | } 183 | } 184 | statement { 185 | sid = "Allow Service CloudWatchLogGroup" 186 | effect = "Allow" 187 | actions = [ 188 | "kms:Encrypt", 189 | "kms:Decrypt", 190 | "kms:ReEncrypt*", 191 | "kms:Describe", 192 | "kms:GenerateDataKey*" 193 | ] 194 | resources = ["*"] 195 | 196 | principals { 197 | type = "Service" 198 | identifiers = [ 199 | "logs.${data.aws_region.cloudfront_region.name}.amazonaws.com" 200 | ] 201 | } 202 | condition { 203 | test = "ArnEquals" 204 | variable = "kms:EncryptionContext:aws:logs:arn" 205 | values = [ 206 | "arn:${data.aws_partition.current_partition.id}:logs:${data.aws_region.cloudfront_region.name}:${data.aws_caller_identity.current_account.account_id}:log-group:aws-waf-logs-${var.name_prefix}-runtask_waf_acl*" 207 | ] 208 | } 209 | } 210 | } 211 | 212 | data "aws_iam_policy_document" "runtask_waf_log" { 213 | count = local.waf_deployment 214 | version = "2012-10-17" 215 | statement { 216 | effect = "Allow" 217 | principals { 218 | identifiers = ["delivery.logs.amazonaws.com"] 219 | type = "Service" 220 | } 221 | actions = ["logs:CreateLogStream", "logs:PutLogEvents"] 222 | resources = ["${aws_cloudwatch_log_group.runtask_waf[count.index].arn}:*"] 223 | condition { 224 | test = "ArnLike" 225 | values = ["arn:aws:logs:${data.aws_region.cloudfront_region.name}:${data.aws_caller_identity.current_account.account_id}:*"] 226 | variable = "aws:SourceArn" 227 | } 228 | condition { 229 | test = "StringEquals" 230 | values = [tostring(data.aws_caller_identity.current_account.account_id)] 231 | variable = "aws:SourceAccount" 232 | } 233 | } 234 | } -------------------------------------------------------------------------------- /diagram/RunTask-EventBridge.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /diagram/RunTask-EventBridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-ia/terraform-aws-runtask-iam-access-analyzer/cad781c0e7ee1bea0124420262e4ea3755ea9f63/diagram/RunTask-EventBridge.png -------------------------------------------------------------------------------- /diagram/TerraformCloud-RunTaskOutput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-ia/terraform-aws-runtask-iam-access-analyzer/cad781c0e7ee1bea0124420262e4ea3755ea9f63/diagram/TerraformCloud-RunTaskOutput.png -------------------------------------------------------------------------------- /diagram/TerraformCloud-RunTaskWorkspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-ia/terraform-aws-runtask-iam-access-analyzer/cad781c0e7ee1bea0124420262e4ea3755ea9f63/diagram/TerraformCloud-RunTaskWorkspace.png -------------------------------------------------------------------------------- /diagram/TerraformCloud-VariableSets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-ia/terraform-aws-runtask-iam-access-analyzer/cad781c0e7ee1bea0124420262e4ea3755ea9f63/diagram/TerraformCloud-VariableSets.png -------------------------------------------------------------------------------- /event/runtask_rule.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "source": [ 3 | { 4 | "prefix": "${var_event_source}" 5 | } 6 | ], 7 | "detail": { 8 | "stage": ${var_runtask_stages} 9 | }, 10 | "detail-type" : ["${var_event_rule_detail_type}"] 11 | } -------------------------------------------------------------------------------- /eventbridge.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_event_rule" "runtask_rule" { 2 | name = "${var.name_prefix}-runtask-rule" 3 | description = "Rule to capture HashiCorp Terraform Cloud RunTask events" 4 | event_bus_name = var.event_bus_name 5 | event_pattern = templatefile("${path.module}/event/runtask_rule.tpl", { 6 | var_event_source = var.event_source 7 | var_runtask_stages = jsonencode(var.runtask_stages) 8 | var_event_rule_detail_type = local.solution_prefix 9 | }) 10 | tags = local.combined_tags 11 | } 12 | 13 | resource "aws_cloudwatch_event_target" "runtask_target" { 14 | rule = aws_cloudwatch_event_rule.runtask_rule.id 15 | event_bus_name = var.event_bus_name 16 | arn = aws_sfn_state_machine.runtask_states.arn 17 | role_arn = aws_iam_role.runtask_rule.arn 18 | } -------------------------------------------------------------------------------- /examples/demo_workspace/.header.md: -------------------------------------------------------------------------------- 1 | # Usage Example 2 | 3 | **IMPORTANT**: To successfully complete this example, you must first deploy the module by following [module workspace example](../module_workspace/README.md). 4 | 5 | ## Attach Run Task into HCP Terraform Workspace 6 | 7 | Follow the steps below to attach the run task created from the module into a new HCP Terraform workspace. The new workspace will attempt to create multiple invalid IAM resources. The Run tasks integration with IAM Access Analyzer will validate it as part of post-plan stage. 8 | 9 | * Use the provided demo workspace configuration. 10 | 11 | ```bash 12 | cd examples/demo_workspace 13 | ``` 14 | 15 | * Change the org name in with your own HCP Terraform org name. Optionally, change the workspace name. 16 | 17 | ```hcl 18 | terraform { 19 | 20 | cloud { 21 | # TODO: Change this to your HCP Terraform org name. 22 | organization = "wellsiau-org" 23 | 24 | # OPTIONAL: Change the workspace name 25 | workspaces { 26 | name = "AWS-Runtask-IAM-Access-Analyzer-Demo" 27 | } 28 | } 29 | ... 30 | } 31 | ``` 32 | 33 | * Populate the required variables, change the placeholder value below. 34 | 35 | ```bash 36 | echo 'tfc_org=""' >> tf.auto.tfvars 37 | echo 'aws_region=""' >> tf.auto.tfvars 38 | echo 'runtask_id=""' >> tf.auto.tfvars 39 | echo 'demo_workspace_name=""' >> tf.auto.tfvars 40 | ``` 41 | 42 | * Initialize HCP Terraform. 43 | 44 | ```bash 45 | terraform init 46 | ``` 47 | 48 | * We recommend configuring dynamic credentials to provision to AWS from your HCP Terraform workspace or organization. [Follow these instructions to learn more.](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/aws-configuration) 49 | 50 | * In order to create and configure the run tasks, you also need to have HCP Terraform token stored as Variable/Variable Sets in the workspace. Add `TFE_HOSTNAME` and `TFE_TOKEN` environment variable to the same variable set or directly on the workspace. ![TFC Configure Variable Set](../diagram/TerraformCloud-VariableSets.png?raw=true "Configure HCP Terraform Variable Set") 51 | 52 | * Enable the flag to attach the run task to the demo workspace. 53 | 54 | ```bash 55 | echo 'flag_attach_runtask="true"' >> tf.auto.tfvars 56 | terraform apply 57 | ``` 58 | 59 | * Navigate back to HCP Terraform, locate the new demo workspace and confirm that the Run Task is attached to the demo workspace. ![TFC Run Task in Workspace](../../diagram/TerraformCloud-RunTaskWorkspace.png?raw=true "Run Task attached to the demo workspace") 60 | 61 | ## Test IAM Access Analyzer using Run Task 62 | 63 | The following steps deploy simple IAM policy with invalid permissions. This should trigger the Run Task to send failure and stop the apply. 64 | 65 | * Enable the flag to deploy invalid IAM policy to the demo workspace. 66 | 67 | ```bash 68 | echo 'flag_deploy_invalid_resource="true"' >> tf.auto.tfvars 69 | ``` 70 | 71 | * Run Terraform apply again 72 | 73 | ```bash 74 | terraform apply 75 | ``` 76 | 77 | * Terraform apply will fail due to several errors, use the CloudWatch link to review the errors. ![HCP TF Run Task results](../../diagram/TerraformCloud-RunTaskOutput.png?raw=true "Run Task output with IAM Access Analyzer validation") 78 | -------------------------------------------------------------------------------- /examples/demo_workspace/.terraform-docs.yaml: -------------------------------------------------------------------------------- 1 | formatter: markdown 2 | header-from: .header.md 3 | settings: 4 | anchor: true 5 | color: true 6 | default: true 7 | escape: true 8 | html: true 9 | indent: 2 10 | required: true 11 | sensitive: true 12 | type: true 13 | lockfile: false 14 | 15 | sort: 16 | enabled: true 17 | by: required 18 | 19 | output: 20 | file: README.md 21 | mode: replace 22 | -------------------------------------------------------------------------------- /examples/demo_workspace/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Usage Example 3 | 4 | **IMPORTANT**: To successfully complete this example, you must first deploy the module by following [module workspace example](../module\_workspace/README.md). 5 | 6 | ## Attach Run Task into HCP Terraform Workspace 7 | 8 | Follow the steps below to attach the run task created from the module into a new HCP Terraform workspace. The new workspace will attempt to create multiple invalid IAM resources. The Run tasks integration with IAM Access Analyzer will validate it as part of post-plan stage. 9 | 10 | * Use the provided demo workspace configuration. 11 | 12 | ```bash 13 | cd examples/demo_workspace 14 | ``` 15 | 16 | * Change the org name in with your own HCP Terraform org name. Optionally, change the workspace name. 17 | 18 | ```hcl 19 | terraform { 20 | 21 | cloud { 22 | # TODO: Change this to your HCP Terraform org name. 23 | organization = "wellsiau-org" 24 | 25 | # OPTIONAL: Change the workspace name 26 | workspaces { 27 | name = "AWS-Runtask-IAM-Access-Analyzer-Demo" 28 | } 29 | } 30 | ... 31 | } 32 | ``` 33 | 34 | * Populate the required variables, change the placeholder value below. 35 | 36 | ```bash 37 | echo 'tfc_org=""' >> tf.auto.tfvars 38 | echo 'aws_region=""' >> tf.auto.tfvars 39 | echo 'runtask_id=""' >> tf.auto.tfvars 40 | echo 'demo_workspace_name=""' >> tf.auto.tfvars 41 | ``` 42 | 43 | * Initialize HCP Terraform. 44 | 45 | ```bash 46 | terraform init 47 | ``` 48 | 49 | * We recommend configuring dynamic credentials to provision to AWS from your HCP Terraform workspace or organization. [Follow these instructions to learn more.](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/aws-configuration) 50 | 51 | * In order to create and configure the run tasks, you also need to have HCP Terraform token stored as Variable/Variable Sets in the workspace. Add `TFE_HOSTNAME` and `TFE_TOKEN` environment variable to the same variable set or directly on the workspace. ![TFC Configure Variable Set](../diagram/TerraformCloud-VariableSets.png?raw=true "Configure HCP Terraform Variable Set") 52 | 53 | * Enable the flag to attach the run task to the demo workspace. 54 | 55 | ```bash 56 | echo 'flag_attach_runtask="true"' >> tf.auto.tfvars 57 | terraform apply 58 | ``` 59 | 60 | * Navigate back to HCP Terraform, locate the new demo workspace and confirm that the Run Task is attached to the demo workspace. ![TFC Run Task in Workspace](../../diagram/TerraformCloud-RunTaskWorkspace.png?raw=true "Run Task attached to the demo workspace") 61 | 62 | ## Test IAM Access Analyzer using Run Task 63 | 64 | The following steps deploy simple IAM policy with invalid permissions. This should trigger the Run Task to send failure and stop the apply. 65 | 66 | * Enable the flag to deploy invalid IAM policy to the demo workspace. 67 | 68 | ```bash 69 | echo 'flag_deploy_invalid_resource="true"' >> tf.auto.tfvars 70 | ``` 71 | 72 | * Run Terraform apply again 73 | 74 | ```bash 75 | terraform apply 76 | ``` 77 | 78 | * Terraform apply will fail due to several errors, use the CloudWatch link to review the errors. ![HCP TF Run Task results](../../diagram/TerraformCloud-RunTaskOutput.png?raw=true "Run Task output with IAM Access Analyzer validation") 79 | 80 | ## Requirements 81 | 82 | | Name | Version | 83 | |------|---------| 84 | | [terraform](#requirement\_terraform) | >= 1.0.7 | 85 | | [aws](#requirement\_aws) | >=5.72.0 | 86 | | [tfe](#requirement\_tfe) | >=0.38.0 | 87 | 88 | ## Providers 89 | 90 | | Name | Version | 91 | |------|---------| 92 | | [aws](#provider\_aws) | >=5.72.0 | 93 | | [tfe](#provider\_tfe) | >=0.38.0 | 94 | 95 | ## Modules 96 | 97 | No modules. 98 | 99 | ## Resources 100 | 101 | | Name | Type | 102 | |------|------| 103 | | [aws_cloudwatch_log_group.sample_log](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | 104 | | [aws_iam_policy.policy_with_computed_values](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | 105 | | [aws_iam_policy.policy_with_data_source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | 106 | | [aws_iam_policy.policy_with_eof](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | 107 | | [aws_iam_role.invalid_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 108 | | [aws_iam_role_policy.invalid_iam_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | 109 | | [aws_kms_key.invalid_kms_key_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | 110 | | [aws_organizations_policy.invalid_scp_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_policy) | resource | 111 | | [tfe_workspace_run_task.aws-iam-analyzer-attach](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/workspace_run_task) | resource | 112 | | [aws_caller_identity.current_account](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | 113 | | [aws_iam_policy_document.invalid_kms_key_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 114 | | [aws_iam_policy_document.policy_with_data_source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 115 | | [aws_partition.current_partition](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | 116 | | [aws_region.current_region](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | 117 | | [tfe_organization.org](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/organization) | data source | 118 | | [tfe_workspace.workspace](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/workspace) | data source | 119 | 120 | ## Inputs 121 | 122 | | Name | Description | Type | Default | Required | 123 | |------|-------------|------|---------|:--------:| 124 | | [aws\_region](#input\_aws\_region) | The region from which this module will be executed. | `string` | n/a | yes | 125 | | [demo\_workspace\_name](#input\_demo\_workspace\_name) | The workspace name | `string` | n/a | yes | 126 | | [runtask\_id](#input\_runtask\_id) | The run task id of the IAM Access Analyzer run task | `string` | n/a | yes | 127 | | [tfc\_org](#input\_tfc\_org) | Terraform Organization name | `string` | n/a | yes | 128 | | [flag\_attach\_runtask](#input\_flag\_attach\_runtask) | Switch this flag to true to attach the run task to the workspace | `bool` | `false` | no | 129 | | [flag\_deploy\_invalid\_resource](#input\_flag\_deploy\_invalid\_resource) | Switch this flag to true to deploy sample invalid IAM policy and validate it with Run Task | `bool` | `false` | no | 130 | | [runtask\_enforcement\_level](#input\_runtask\_enforcement\_level) | The description give to the attached run task (optional) | `string` | `"mandatory"` | no | 131 | | [runtask\_stage](#input\_runtask\_stage) | The description give to the attached run task (optional) | `string` | `"post_plan"` | no | 132 | 133 | ## Outputs 134 | 135 | No outputs. 136 | -------------------------------------------------------------------------------- /examples/demo_workspace/data.tf: -------------------------------------------------------------------------------- 1 | data "tfe_organization" "org" { 2 | name = var.tfc_org 3 | } 4 | 5 | data "tfe_workspace" "workspace" { 6 | name = var.demo_workspace_name 7 | organization = data.tfe_organization.org.name 8 | } 9 | 10 | data "aws_region" "current_region" {} 11 | 12 | data "aws_caller_identity" "current_account" {} 13 | 14 | data "aws_partition" "current_partition" {} 15 | -------------------------------------------------------------------------------- /examples/demo_workspace/iam/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-ia/terraform-aws-runtask-iam-access-analyzer/cad781c0e7ee1bea0124420262e4ea3755ea9f63/examples/demo_workspace/iam/.DS_Store -------------------------------------------------------------------------------- /examples/demo_workspace/iam/role-policies/invalid-iam-role-policy.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "ValidPermission", 6 | "Action": [ 7 | "lambda:*" 8 | ], 9 | "Resource": "*", 10 | "Effect": "Allow" 11 | }, 12 | { 13 | "Sid": "InvalidPermission", 14 | "Action": "events:PutEvent", 15 | "Resource": "arn:${aws_partition}:${aws_service}:${aws_region}:${aws_account_id}:event-bus/${name_prefix}", 16 | "Effect": "Allow" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /examples/demo_workspace/iam/trust-policies/invalid-trust.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "AWSService": "lambda.amazonaws.com" 8 | }, 9 | "Action": "sts:AssumeRole" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/demo_workspace/main.tf: -------------------------------------------------------------------------------- 1 | # ========================================================================== 2 | # ATTACH RUN TASKS 3 | # ========================================================================== 4 | 5 | resource "tfe_workspace_run_task" "aws-iam-analyzer-attach" { 6 | count = var.flag_attach_runtask ? 1 : 0 7 | workspace_id = data.tfe_workspace.workspace.id 8 | task_id = var.runtask_id 9 | enforcement_level = var.runtask_enforcement_level 10 | stages = [var.runtask_stage] 11 | } 12 | 13 | # ========================================================================== 14 | # SIMPLE IAM POLICY WITH INVALID PERMISSION 15 | # ========================================================================== 16 | 17 | resource "aws_iam_policy" "policy_with_eof" { 18 | # the sample policy below contains invalid iam permissions (syntax-wise) 19 | count = var.flag_deploy_invalid_resource ? 1 : 0 20 | policy = < 2 | # Usage Example 3 | 4 | First step is to deploy the module into dedicated HCP Terraform workspace. The output `runtask_id` is used on other HCP Terraform workspace to configure the runtask. 5 | 6 | * Build and package the Lambda files using the makefile. Run this command from the root directory of this repository. 7 | 8 | ```bash 9 | make all 10 | ``` 11 | 12 | * Use the provided module example to deploy the solution. 13 | 14 | ```bash 15 | cd examples/module_workspace 16 | ``` 17 | 18 | * Change the org name to your TFC org. 19 | 20 | ```hcl 21 | terraform { 22 | 23 | cloud { 24 | # TODO: Change this to your HCP Terraform org name. 25 | organization = "" 26 | workspaces { 27 | ... 28 | } 29 | } 30 | ... 31 | } 32 | ``` 33 | 34 | * Initialize HCP Terraform. When prompted, enter a new workspace name, i.e. `aws-ia2-infra` 35 | 36 | ```bash 37 | terraform init 38 | ``` 39 | 40 | * Configure the new workspace (i.e `aws-ia2-infra`) in HCP Terraform to use `local` execution mode. Skip this if you publish the module into Terraform registry. 41 | 42 | * Configure the AWS credentials (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) by using environment variables. 43 | 44 | * In order to create and configure the run tasks, you also need to have HCP Terraform token stored as Environment Variables. Add `TFE_HOSTNAME` and `TFE_TOKEN` environment variable. 45 | 46 | * Run Terraform apply 47 | 48 | ```bash 49 | terraform apply 50 | ``` 51 | 52 | * Use the output value `runtask_id` when deploying the demo workspace. See example of [demo workspace here](../demo\_workspace/README.md) 53 | 54 | ## Requirements 55 | 56 | | Name | Version | 57 | |------|---------| 58 | | [terraform](#requirement\_terraform) | >= 1.0.7 | 59 | | [archive](#requirement\_archive) | ~>2.2.0 | 60 | | [aws](#requirement\_aws) | >=5.72.0 | 61 | | [random](#requirement\_random) | >=3.4.0 | 62 | | [tfe](#requirement\_tfe) | >=0.38.0 | 63 | 64 | ## Providers 65 | 66 | | Name | Version | 67 | |------|---------| 68 | | [aws](#provider\_aws) | >=5.72.0 | 69 | 70 | ## Modules 71 | 72 | | Name | Source | Version | 73 | |------|--------|---------| 74 | | [runtask\_iam\_access\_analyzer](#module\_runtask\_iam\_access\_analyzer) | ../../ | n/a | 75 | 76 | ## Resources 77 | 78 | | Name | Type | 79 | |------|------| 80 | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | 81 | 82 | ## Inputs 83 | 84 | | Name | Description | Type | Default | Required | 85 | |------|-------------|------|---------|:--------:| 86 | | [tfc\_org](#input\_tfc\_org) | n/a | `string` | n/a | yes | 87 | | [workspace\_prefix](#input\_workspace\_prefix) | n/a | `string` | n/a | yes | 88 | 89 | ## Outputs 90 | 91 | | Name | Description | 92 | |------|-------------| 93 | | [runtask\_id](#output\_runtask\_id) | n/a | 94 | -------------------------------------------------------------------------------- /examples/module_workspace/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" { 2 | } 3 | 4 | module "runtask_iam_access_analyzer" { 5 | source = "../../" # set your HCP Terraform workspace with Local execution mode to allow module reference like this 6 | tfc_org = var.tfc_org 7 | aws_region = data.aws_region.current.name 8 | workspace_prefix = var.workspace_prefix 9 | deploy_waf = true 10 | } 11 | -------------------------------------------------------------------------------- /examples/module_workspace/outputs.tf: -------------------------------------------------------------------------------- 1 | output "runtask_id" { 2 | value = module.runtask_iam_access_analyzer.runtask_id 3 | } 4 | -------------------------------------------------------------------------------- /examples/module_workspace/variables.tf: -------------------------------------------------------------------------------- 1 | variable "tfc_org" { 2 | type = string 3 | } 4 | 5 | variable "workspace_prefix" { 6 | type = string 7 | } 8 | -------------------------------------------------------------------------------- /examples/module_workspace/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | cloud { 3 | # TODO: Change this to your HCP Terraform org name. 4 | organization = "wellsiau-org" 5 | workspaces { 6 | name = "TestExamplesLaunchModule" 7 | } 8 | } 9 | 10 | required_version = ">= 1.0.7" 11 | required_providers { 12 | aws = { 13 | source = "hashicorp/aws" 14 | version = ">=5.72.0" 15 | } 16 | 17 | tfe = { 18 | source = "hashicorp/tfe" 19 | version = ">=0.38.0" 20 | } 21 | 22 | random = { 23 | source = "hashicorp/random" 24 | version = ">=3.4.0" 25 | } 26 | 27 | archive = { 28 | source = "hashicorp/archive" 29 | version = "~>2.2.0" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /iam.tf: -------------------------------------------------------------------------------- 1 | ################# IAM for run task Lambda@Edge ################## 2 | resource "aws_iam_role" "runtask_edge" { 3 | name = "${var.name_prefix}-runtask-edge" 4 | assume_role_policy = templatefile("${path.module}/iam/trust-policies/lambda_edge.tpl", { none = "none" }) 5 | tags = local.combined_tags 6 | } 7 | 8 | resource "aws_iam_role_policy_attachment" "runtask_edge" { 9 | count = length(local.lambda_managed_policies) 10 | role = aws_iam_role.runtask_edge.name 11 | policy_arn = local.lambda_managed_policies[count.index] 12 | } 13 | 14 | 15 | ################# RunTask EventBridge ################## 16 | resource "aws_iam_role" "runtask_eventbridge" { 17 | name = "${var.name_prefix}-runtask-eventbridge" 18 | assume_role_policy = templatefile("${path.module}/iam/trust-policies/lambda.tpl", { none = "none" }) 19 | tags = local.combined_tags 20 | } 21 | 22 | resource "aws_iam_role_policy_attachment" "runtask_eventbridge" { 23 | count = length(local.lambda_managed_policies) 24 | role = aws_iam_role.runtask_eventbridge.name 25 | policy_arn = local.lambda_managed_policies[count.index] 26 | } 27 | 28 | resource "aws_iam_role_policy" "runtask_eventbridge" { 29 | name = "${var.name_prefix}-runtask-eventbridge-policy" 30 | role = aws_iam_role.runtask_eventbridge.id 31 | policy = templatefile("${path.module}/iam/role-policies/runtask-eventbridge-lambda-role-policy.tpl", { 32 | data_aws_region = data.aws_region.current_region.name 33 | data_aws_account_id = data.aws_caller_identity.current_account.account_id 34 | data_aws_partition = data.aws_partition.current_partition.partition 35 | var_event_bus_name = var.event_bus_name 36 | resource_runtask_secrets = var.deploy_waf ? [aws_secretsmanager_secret.runtask_hmac.arn, aws_secretsmanager_secret.runtask_cloudfront[0].arn] : [aws_secretsmanager_secret.runtask_hmac.arn] 37 | }) 38 | } 39 | 40 | ################# RunTask Request ################## 41 | resource "aws_iam_role" "runtask_request" { 42 | name = "${var.name_prefix}-runtask-request" 43 | assume_role_policy = templatefile("${path.module}/iam/trust-policies/lambda.tpl", { none = "none" }) 44 | tags = local.combined_tags 45 | } 46 | 47 | resource "aws_iam_role_policy_attachment" "runtask_request" { 48 | count = length(local.lambda_managed_policies) 49 | role = aws_iam_role.runtask_request.name 50 | policy_arn = local.lambda_managed_policies[count.index] 51 | } 52 | 53 | ################# RunTask CallBack ################## 54 | resource "aws_iam_role" "runtask_callback" { 55 | name = "${var.name_prefix}-runtask-callback" 56 | assume_role_policy = templatefile("${path.module}/iam/trust-policies/lambda.tpl", { none = "none" }) 57 | tags = local.combined_tags 58 | } 59 | 60 | resource "aws_iam_role_policy_attachment" "runtask_callback" { 61 | count = length(local.lambda_managed_policies) 62 | role = aws_iam_role.runtask_callback.name 63 | policy_arn = local.lambda_managed_policies[count.index] 64 | } 65 | 66 | ################# RunTask Fulfillment ################## 67 | resource "aws_iam_role" "runtask_fulfillment" { 68 | name = "${var.name_prefix}-runtask-fulfillment" 69 | assume_role_policy = templatefile("${path.module}/iam/trust-policies/lambda.tpl", { none = "none" }) 70 | tags = local.combined_tags 71 | } 72 | 73 | resource "aws_iam_role_policy_attachment" "runtask_fulfillment" { 74 | count = length(local.lambda_managed_policies) 75 | role = aws_iam_role.runtask_fulfillment.name 76 | policy_arn = local.lambda_managed_policies[count.index] 77 | } 78 | 79 | resource "aws_iam_role_policy" "runtask_fulfillment" { 80 | name = "${var.name_prefix}-runtask-fulfillment-policy" 81 | role = aws_iam_role.runtask_fulfillment.id 82 | policy = templatefile("${path.module}/iam/role-policies/runtask-fulfillment-lambda-role-policy.tpl", { 83 | data_aws_region = data.aws_region.current_region.name 84 | data_aws_account_id = data.aws_caller_identity.current_account.account_id 85 | data_aws_partition = data.aws_partition.current_partition.partition 86 | local_log_group_name = local.cloudwatch_log_group_name 87 | }) 88 | } 89 | 90 | ################# RunTask State machine ################## 91 | resource "aws_iam_role" "runtask_states" { 92 | name = "${var.name_prefix}-runtask-statemachine" 93 | assume_role_policy = templatefile("${path.module}/iam/trust-policies/states.tpl", { none = "none" }) 94 | tags = local.combined_tags 95 | } 96 | 97 | resource "aws_iam_role_policy" "runtask_states" { 98 | name = "${var.name_prefix}-runtask-statemachine-policy" 99 | role = aws_iam_role.runtask_states.id 100 | policy = templatefile("${path.module}/iam/role-policies/runtask-state-role-policy.tpl", { 101 | data_aws_region = data.aws_region.current_region.name 102 | data_aws_account_id = data.aws_caller_identity.current_account.account_id 103 | data_aws_partition = data.aws_partition.current_partition.partition 104 | var_name_prefix = var.name_prefix 105 | }) 106 | } 107 | 108 | 109 | ################# RunTask EventBridge rule ################## 110 | resource "aws_iam_role" "runtask_rule" { 111 | name = "${var.name_prefix}-runtask-rule" 112 | assume_role_policy = templatefile("${path.module}/iam/trust-policies/events.tpl", { none = "none" }) 113 | tags = local.combined_tags 114 | } 115 | 116 | resource "aws_iam_role_policy" "runtask_rule" { 117 | name = "${var.name_prefix}-runtask-rule-policy" 118 | role = aws_iam_role.runtask_rule.id 119 | policy = templatefile("${path.module}/iam/role-policies/runtask-rule-role-policy.tpl", { 120 | resource_runtask_states = aws_sfn_state_machine.runtask_states.arn 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /iam/role-policies/runtask-eventbridge-lambda-role-policy.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": [ 6 | "secretsmanager:DescribeSecret", 7 | "secretsmanager:GetSecretValue" 8 | ], 9 | "Resource": ${jsonencode(resource_runtask_secrets)}, 10 | "Effect": "Allow", 11 | "Sid": "SecretsManagerGet" 12 | }, 13 | { 14 | "Action": "events:PutEvents", 15 | "Resource": "arn:${data_aws_partition}:events:${data_aws_region}:${data_aws_account_id}:event-bus/${var_event_bus_name}", 16 | "Effect": "Allow", 17 | "Sid": "EventBridgePut" 18 | }, 19 | { 20 | "Action": [ 21 | "xray:PutTraceSegments", 22 | "xray:PutTelemetryRecords" 23 | ], 24 | "Resource": "*", 25 | "Effect": "Allow", 26 | "Sid": "XRayTracing" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /iam/role-policies/runtask-fulfillment-lambda-role-policy.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Action": [ 6 | "access-analyzer:ValidatePolicy" 7 | ], 8 | "Resource": "arn:${data_aws_partition}:access-analyzer:${data_aws_region}:${data_aws_account_id}:*", 9 | "Effect": "Allow", 10 | "Sid": "AccessAnalyzerOps" 11 | }, 12 | { 13 | "Action": [ 14 | "logs:CreateLogGroup", 15 | "logs:CreateLogStream", 16 | "logs:PutLogEvents" 17 | ], 18 | "Resource": "arn:${data_aws_partition}:logs:${data_aws_region}:${data_aws_account_id}:log-group:${local_log_group_name}/*", 19 | "Effect": "Allow", 20 | "Sid": "CloudWatchLogOps" 21 | }, 22 | { 23 | "Action": [ 24 | "xray:PutTraceSegments", 25 | "xray:PutTelemetryRecords" 26 | ], 27 | "Resource": "*", 28 | "Effect": "Allow", 29 | "Sid": "XRayTracing" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /iam/role-policies/runtask-rule-role-policy.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "states:StartExecution" 8 | ], 9 | "Resource": [ 10 | "${resource_runtask_states}" 11 | ] 12 | }, 13 | { 14 | "Action": [ 15 | "xray:PutTraceSegments", 16 | "xray:PutTelemetryRecords" 17 | ], 18 | "Resource": "*", 19 | "Effect": "Allow", 20 | "Sid": "XRayTracing" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /iam/role-policies/runtask-state-role-policy.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "lambda:InvokeFunction" 8 | ], 9 | "Resource": [ 10 | "arn:${data_aws_partition}:lambda:${data_aws_region}:${data_aws_account_id}:function:${var_name_prefix}*:*" 11 | ] 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "logs:CreateLogDelivery", 17 | "logs:GetLogDelivery", 18 | "logs:UpdateLogDelivery", 19 | "logs:DeleteLogDelivery", 20 | "logs:ListLogDeliveries", 21 | "logs:PutResourcePolicy", 22 | "logs:DescribeResourcePolicies", 23 | "logs:DescribeLogGroups" 24 | ], 25 | "Resource": [ 26 | "*" 27 | ] 28 | }, 29 | { 30 | "Effect": "Allow", 31 | "Action": [ 32 | "logs:PutLogEvents", 33 | "logs:CreateLogStream" 34 | ], 35 | "Resource": [ 36 | "arn:${data_aws_partition}:logs:${data_aws_region}:${data_aws_account_id}:log-group:/aws/state/${var_name_prefix}-runtask-statemachine*" 37 | ] 38 | }, 39 | { 40 | "Action": [ 41 | "xray:PutTraceSegments", 42 | "xray:PutTelemetryRecords" 43 | ], 44 | "Resource": "*", 45 | "Effect": "Allow", 46 | "Sid": "XRayTracing" 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /iam/trust-policies/events.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Service": "events.amazonaws.com" 8 | }, 9 | "Action": "sts:AssumeRole" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /iam/trust-policies/lambda.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Service": "lambda.amazonaws.com" 8 | }, 9 | "Action": "sts:AssumeRole" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /iam/trust-policies/lambda_edge.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Service": [ 8 | "lambda.amazonaws.com", 9 | "edgelambda.amazonaws.com" 10 | ] 11 | }, 12 | "Action": "sts:AssumeRole" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /iam/trust-policies/states.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Service": "states.amazonaws.com" 8 | }, 9 | "Action": "sts:AssumeRole" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /kms.tf: -------------------------------------------------------------------------------- 1 | resource "aws_kms_key" "runtask_key" { 2 | description = "KMS key for Run Task integration" 3 | policy = data.aws_iam_policy_document.runtask_key.json 4 | enable_key_rotation = true 5 | tags = local.combined_tags 6 | } 7 | 8 | # Assign an alias to the key 9 | resource "aws_kms_alias" "runtask_key" { 10 | name = "alias/RunTaskIA2" 11 | target_key_id = aws_kms_key.runtask_key.key_id 12 | } 13 | 14 | resource "aws_kms_key" "runtask_waf" { 15 | count = local.waf_deployment 16 | provider = aws.cloudfront_waf 17 | description = "KMS key for WAF" 18 | policy = data.aws_iam_policy_document.runtask_waf[count.index].json 19 | enable_key_rotation = true 20 | tags = local.combined_tags 21 | } 22 | 23 | # Assign an alias to the key 24 | resource "aws_kms_alias" "runtask_waf" { 25 | count = local.waf_deployment 26 | provider = aws.cloudfront_waf 27 | name = "alias/RunTaskIA2-WAF" 28 | target_key_id = aws_kms_key.runtask_waf[count.index].key_id 29 | } 30 | -------------------------------------------------------------------------------- /lambda.tf: -------------------------------------------------------------------------------- 1 | ################# RunTask EventBridge ################## 2 | resource "aws_lambda_function" "runtask_eventbridge" { 3 | function_name = "${var.name_prefix}-runtask-eventbridge" 4 | description = "HCP Terraform Run Task - EventBridge handler" 5 | role = aws_iam_role.runtask_eventbridge.arn 6 | architectures = local.lambda_architecture 7 | source_code_hash = data.archive_file.runtask_eventbridge.output_base64sha256 8 | filename = data.archive_file.runtask_eventbridge.output_path 9 | handler = "handler.lambda_handler" 10 | runtime = local.lambda_python_runtime 11 | timeout = local.lambda_default_timeout 12 | environment { 13 | variables = { 14 | HCP_TF_HMAC_SECRET_ARN = aws_secretsmanager_secret.runtask_hmac.arn 15 | HCP_TF_USE_WAF = var.deploy_waf ? "True" : "False" 16 | HCP_TF_CF_SECRET_ARN = var.deploy_waf ? aws_secretsmanager_secret.runtask_cloudfront[0].arn : null 17 | HCP_TF_CF_SIGNATURE = var.deploy_waf ? local.cloudfront_sig_name : null 18 | EVENT_BUS_NAME = var.event_bus_name 19 | EVENT_RULE_DETAIL_TYPE = local.solution_prefix # ensure uniqueness of event sent to each runtask state machine 20 | } 21 | } 22 | tracing_config { 23 | mode = "Active" 24 | } 25 | reserved_concurrent_executions = local.lambda_reserved_concurrency 26 | tags = local.combined_tags 27 | #checkov:skip=CKV_AWS_116:not using DLQ 28 | #checkov:skip=CKV_AWS_117:VPC is not required 29 | #checkov:skip=CKV_AWS_173:non sensitive environment variables 30 | #checkov:skip=CKV_AWS_272:skip code-signing 31 | } 32 | 33 | resource "aws_lambda_function_url" "runtask_eventbridge" { 34 | function_name = aws_lambda_function.runtask_eventbridge.function_name 35 | authorization_type = "AWS_IAM" 36 | } 37 | 38 | resource "aws_lambda_permission" "runtask_eventbridge" { 39 | count = local.waf_deployment 40 | statement_id = "AllowCloudFrontToFunctionUrl" 41 | action = "lambda:InvokeFunctionUrl" 42 | function_name = aws_lambda_function.runtask_eventbridge.function_name 43 | principal = "cloudfront.amazonaws.com" 44 | source_arn = module.runtask_cloudfront[count.index].cloudfront_distribution_arn 45 | } 46 | 47 | resource "aws_cloudwatch_log_group" "runtask_eventbridge" { 48 | name = "/aws/lambda/${aws_lambda_function.runtask_eventbridge.function_name}" 49 | retention_in_days = var.cloudwatch_log_group_retention 50 | kms_key_id = aws_kms_key.runtask_key.arn 51 | tags = local.combined_tags 52 | } 53 | 54 | ################# RunTask Request ################## 55 | resource "aws_lambda_function" "runtask_request" { 56 | function_name = "${var.name_prefix}-runtask-request" 57 | description = "HCP Terraform Run Task - Request handler" 58 | role = aws_iam_role.runtask_request.arn 59 | architectures = local.lambda_architecture 60 | source_code_hash = data.archive_file.runtask_request.output_base64sha256 61 | filename = data.archive_file.runtask_request.output_path 62 | handler = "handler.lambda_handler" 63 | runtime = local.lambda_python_runtime 64 | timeout = local.lambda_default_timeout 65 | tracing_config { 66 | mode = "Active" 67 | } 68 | reserved_concurrent_executions = local.lambda_reserved_concurrency 69 | environment { 70 | variables = { 71 | HCP_TF_ORG = var.tfc_org 72 | RUNTASK_STAGES = join(",", var.runtask_stages) 73 | WORKSPACE_PREFIX = length(var.workspace_prefix) > 0 ? var.workspace_prefix : null 74 | EVENT_RULE_DETAIL_TYPE = local.solution_prefix # ensure uniqueness of event sent to each runtask state machine 75 | } 76 | } 77 | tags = local.combined_tags 78 | #checkov:skip=CKV_AWS_116:not using DLQ 79 | #checkov:skip=CKV_AWS_117:VPC is not required 80 | #checkov:skip=CKV_AWS_173:no sensitive data in env var 81 | #checkov:skip=CKV_AWS_272:skip code-signing 82 | } 83 | 84 | resource "aws_cloudwatch_log_group" "runtask_request" { 85 | name = "/aws/lambda/${aws_lambda_function.runtask_request.function_name}" 86 | retention_in_days = var.cloudwatch_log_group_retention 87 | kms_key_id = aws_kms_key.runtask_key.arn 88 | } 89 | 90 | ################# RunTask Callback ################## 91 | resource "aws_lambda_function" "runtask_callback" { 92 | function_name = "${var.name_prefix}-runtask-callback" 93 | description = "HCP Terraform Run Task - Callback handler" 94 | role = aws_iam_role.runtask_callback.arn 95 | architectures = local.lambda_architecture 96 | source_code_hash = data.archive_file.runtask_callback.output_base64sha256 97 | filename = data.archive_file.runtask_callback.output_path 98 | handler = "handler.lambda_handler" 99 | runtime = local.lambda_python_runtime 100 | timeout = local.lambda_default_timeout 101 | tracing_config { 102 | mode = "Active" 103 | } 104 | reserved_concurrent_executions = local.lambda_reserved_concurrency 105 | tags = local.combined_tags 106 | #checkov:skip=CKV_AWS_116:not using DLQ 107 | #checkov:skip=CKV_AWS_117:VPC is not required 108 | #checkov:skip=CKV_AWS_272:skip code-signing 109 | } 110 | 111 | resource "aws_cloudwatch_log_group" "runtask_callback" { 112 | name = "/aws/lambda/${aws_lambda_function.runtask_callback.function_name}" 113 | retention_in_days = var.cloudwatch_log_group_retention 114 | kms_key_id = aws_kms_key.runtask_key.arn 115 | tags = local.combined_tags 116 | } 117 | 118 | ################# RunTask Fulfillment ################## 119 | resource "aws_lambda_function" "runtask_fulfillment" { 120 | function_name = "${var.name_prefix}-runtask-fulfillment" 121 | description = "HCP Terraform Run Task - Fulfillment handler" 122 | role = aws_iam_role.runtask_fulfillment.arn 123 | architectures = local.lambda_architecture 124 | source_code_hash = data.archive_file.runtask_fulfillment.output_base64sha256 125 | filename = data.archive_file.runtask_fulfillment.output_path 126 | handler = "handler.lambda_handler" 127 | runtime = local.lambda_python_runtime 128 | timeout = local.lambda_default_timeout 129 | tracing_config { 130 | mode = "Active" 131 | } 132 | reserved_concurrent_executions = local.lambda_reserved_concurrency 133 | environment { 134 | variables = { 135 | CW_LOG_GROUP_NAME = local.cloudwatch_log_group_name 136 | SUPPORTED_POLICY_DOCUMENT = length(var.supported_policy_document) > 0 ? var.supported_policy_document : null 137 | } 138 | } 139 | tags = local.combined_tags 140 | #checkov:skip=CKV_AWS_116:not using DLQ 141 | #checkov:skip=CKV_AWS_117:VPC is not required 142 | #checkov:skip=CKV_AWS_173:no sensitive data in env var 143 | #checkov:skip=CKV_AWS_272:skip code-signing 144 | } 145 | 146 | resource "aws_cloudwatch_log_group" "runtask_fulfillment" { 147 | name = "/aws/lambda/${aws_lambda_function.runtask_fulfillment.function_name}" 148 | retention_in_days = var.cloudwatch_log_group_retention 149 | kms_key_id = aws_kms_key.runtask_key.arn 150 | tags = local.combined_tags 151 | } 152 | 153 | resource "aws_cloudwatch_log_group" "runtask_fulfillment_output" { 154 | name = local.cloudwatch_log_group_name 155 | retention_in_days = var.cloudwatch_log_group_retention 156 | kms_key_id = aws_kms_key.runtask_key.arn 157 | tags = local.combined_tags 158 | } 159 | 160 | 161 | ################# Run task Edge ################## 162 | resource "aws_lambda_function" "runtask_edge" { 163 | provider = aws.cloudfront_waf # Lambda@Edge must be in us-east-1 164 | function_name = "${var.name_prefix}-runtask-edge" 165 | description = "HCP Terraform run task - Lambda@Edge handler" 166 | role = aws_iam_role.runtask_edge.arn 167 | architectures = local.lambda_architecture 168 | source_code_hash = data.archive_file.runtask_edge.output_base64sha256 169 | filename = data.archive_file.runtask_edge.output_path 170 | handler = "handler.lambda_handler" 171 | runtime = local.lambda_python_runtime 172 | timeout = 5 # Lambda@Edge max timout is 5 173 | reserved_concurrent_executions = local.lambda_reserved_concurrency 174 | publish = true # Lambda@Edge must be published 175 | tags = local.combined_tags 176 | #checkov:skip=CKV_AWS_116:not using DLQ 177 | #checkov:skip=CKV_AWS_117:VPC is not required 178 | #checkov:skip=CKV_AWS_173:no sensitive data in env var 179 | #checkov:skip=CKV_AWS_272:skip code-signing 180 | #checkov:skip=CKV_AWS_50:no x-ray for lambda@edge 181 | } 182 | -------------------------------------------------------------------------------- /lambda/runtask_callback/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = $(CURDIR) 2 | 3 | all: build 4 | 5 | .PHONY: clean build 6 | 7 | clean: 8 | rm -rf build 9 | rm -rf site-packages 10 | 11 | build: 12 | $(info ************ Starting Build: $(PROJECT) ************) 13 | mkdir -p site-packages 14 | cp *.* ./site-packages 15 | mkdir -p build 16 | python3 -m venv build/ 17 | . build/bin/activate; \ 18 | pip3 install -r requirements.txt -t ./site-packages; 19 | rm -rf build -------------------------------------------------------------------------------- /lambda/runtask_callback/handler.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 12 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 14 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 15 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | ''' 17 | import json 18 | import logging 19 | import os 20 | import re 21 | from urllib.request import urlopen, Request 22 | from urllib.error import HTTPError, URLError 23 | 24 | logger = logging.getLogger() 25 | log_level = os.environ.get("log_level", logging.INFO) 26 | 27 | logger.setLevel(log_level) 28 | logger.info("Log level set to %s" % logger.getEffectiveLevel()) 29 | 30 | HCP_TF_HOST_NAME = os.environ.get("HCP_TF_HOST_NAME", "app.terraform.io") 31 | 32 | def lambda_handler(event, context): 33 | logger.debug(json.dumps(event)) 34 | try: 35 | # trim empty url from the payload 36 | if "fulfillment" in event["payload"]["result"] and event["payload"]["result"]["fulfillment"]["url"] == False: 37 | event["payload"]["result"]["fulfillment"].pop("url") 38 | 39 | if event["payload"]["result"]["request"]["status"] == "unverified": # unverified runtask execution 40 | payload = { 41 | "data": { 42 | "attributes": { 43 | "status": "failed", 44 | "message": "Verification failed, check HCP Terraform org, workspace prefix or Runtasks stage", 45 | }, 46 | "type": "task-results", 47 | } 48 | } 49 | elif event["payload"]["result"]["stage"]["status"] == "not implemented": # unimplemented runtask stage 50 | payload = { 51 | "data": { 52 | "attributes": { 53 | "status": "failed", 54 | "message": "Runtask is not configured to run on this stage {}".format(event["payload"]["detail"]["stage"]), 55 | }, 56 | "type": "task-results", 57 | } 58 | } 59 | elif event["payload"]["result"]["fulfillment"]["status"] in ["passed", "failed"]: # return from fulfillment regardless of status 60 | payload = { 61 | "data": { 62 | "attributes": event["payload"]["result"]["fulfillment"], 63 | "type": "task-results", 64 | } 65 | } 66 | 67 | logger.info("Payload : {}".format(json.dumps(payload))) 68 | 69 | # Send runtask callback response to HCP Terraform 70 | endpoint = event["payload"]["detail"]["task_result_callback_url"] 71 | access_token = event["payload"]["detail"]["access_token"] 72 | headers = __build_standard_headers(access_token) 73 | response = __patch(endpoint, headers, bytes(json.dumps(payload), encoding="utf-8")) 74 | logger.debug("HCP Terraform response: {}".format(response)) 75 | return "completed" 76 | 77 | except Exception as e: 78 | logger.exception("Run Task Callback error: {}".format(e)) 79 | raise 80 | 81 | def __build_standard_headers(api_token): 82 | return { 83 | "Authorization": "Bearer {}".format(api_token), 84 | "Content-type": "application/vnd.api+json", 85 | } 86 | 87 | def __patch(endpoint, headers, payload): 88 | request = Request(endpoint, headers=headers or {}, data=payload, method = "PATCH") 89 | try: 90 | if validate_endpoint(endpoint): 91 | with urlopen(request, timeout=10) as response: #nosec URL validation 92 | return response.read(), response 93 | else: 94 | raise URLError("Invalid endpoint URL, expected host is: {}".format(HCP_TF_HOST_NAME)) 95 | except HTTPError as error: 96 | logger.error(error.status, error.reason) 97 | except URLError as error: 98 | logger.error(error.reason) 99 | except TimeoutError: 100 | logger.error("Request timed out") 101 | 102 | def validate_endpoint(endpoint): # validate that the endpoint hostname is valid 103 | pattern = "^https:\/\/" + str(HCP_TF_HOST_NAME).replace(".", "\.") + "\/"+ ".*" 104 | result = re.match(pattern, endpoint) 105 | return result -------------------------------------------------------------------------------- /lambda/runtask_callback/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-ia/terraform-aws-runtask-iam-access-analyzer/cad781c0e7ee1bea0124420262e4ea3755ea9f63/lambda/runtask_callback/requirements.txt -------------------------------------------------------------------------------- /lambda/runtask_edge/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = $(CURDIR) 2 | 3 | all: build 4 | 5 | .PHONY: clean build 6 | 7 | clean: 8 | rm -rf build 9 | rm -rf site-packages 10 | 11 | build: 12 | $(info ************ Starting Build: $(PROJECT) ************) 13 | mkdir -p site-packages 14 | cp *.* ./site-packages 15 | mkdir -p build 16 | python3 -m venv build/ 17 | . build/bin/activate; \ 18 | pip3 install -r requirements.txt -t ./site-packages; 19 | rm -rf build -------------------------------------------------------------------------------- /lambda/runtask_edge/handler.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import json 4 | import logging 5 | import os 6 | 7 | logger = logging.getLogger() 8 | log_level = os.environ.get("log_level", logging.INFO) 9 | 10 | logger.setLevel(log_level) 11 | logger.info("Log level set to %s" % logger.getEffectiveLevel()) 12 | 13 | 14 | def lambda_handler(event, _): 15 | logger.info("Incoming event : {}".format(json.dumps(event))) 16 | request = event['Records'][0]['cf']['request'] 17 | headers = request["headers"] 18 | headerName = 'x-amz-content-sha256' 19 | 20 | ''' 21 | CloudFront Origin Access Control will not automatically calculate the payload hash. 22 | this Lambda@Edge will calculate the payload hash and append new header x-amz-content-sha256 23 | ''' 24 | payload_body = decode_body(request['body']['data']) 25 | logger.debug("Payload : {}".format(payload_body)) 26 | payload_hash = calculate_payload_hash(payload_body) 27 | 28 | # inject new header 29 | headers[headerName] = [{'key': headerName, 'value': payload_hash}] 30 | 31 | logger.info("Returning request: %s" % json.dumps(request)) 32 | return request 33 | 34 | 35 | def decode_body(encoded_body): 36 | return base64.b64decode(encoded_body).decode('utf-8') 37 | 38 | 39 | def calculate_payload_hash(payload): 40 | ## generate sha256 from payload 41 | return hashlib.sha256(payload.encode('utf-8')).hexdigest() -------------------------------------------------------------------------------- /lambda/runtask_edge/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-ia/terraform-aws-runtask-iam-access-analyzer/cad781c0e7ee1bea0124420262e4ea3755ea9f63/lambda/runtask_edge/requirements.txt -------------------------------------------------------------------------------- /lambda/runtask_eventbridge/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = $(CURDIR) 2 | 3 | all: build 4 | 5 | .PHONY: clean build 6 | 7 | clean: 8 | rm -rf build 9 | rm -rf site-packages 10 | 11 | build: 12 | $(info ************ Starting Build: $(PROJECT) ************) 13 | mkdir -p site-packages 14 | cp *.* ./site-packages 15 | mkdir -p build 16 | python3 -m venv build/ 17 | . build/bin/activate; \ 18 | pip3 install -r requirements.txt -t ./site-packages; 19 | rm -rf build -------------------------------------------------------------------------------- /lambda/runtask_eventbridge/handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 12 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 14 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 15 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | """ 17 | 18 | """HCP Terraform run task event handler implementation""" 19 | 20 | import os 21 | import hmac 22 | import json 23 | import base64 24 | import hashlib 25 | import logging 26 | import urllib.parse 27 | import boto3 28 | import botocore.session 29 | 30 | from cgi import parse_header 31 | from aws_secretsmanager_caching import SecretCache, SecretCacheConfig 32 | 33 | client = botocore.session.get_session().create_client("secretsmanager") 34 | cache_config = SecretCacheConfig() 35 | cache = SecretCache(config=cache_config, client=client) 36 | 37 | hcp_tf_hmac_secret_arn = os.environ.get("HCP_TF_HMAC_SECRET_ARN") 38 | hcp_tf_use_waf = os.environ.get("HCP_TF_USE_WAF") 39 | hcp_tf_cf_secret_arn = os.environ.get("HCP_TF_CF_SECRET_ARN") 40 | hcp_tf_cf_signature = os.environ.get("HCP_TF_CF_SIGNATURE") 41 | 42 | logger = logging.getLogger() 43 | log_level = os.environ.get("log_level", logging.INFO) 44 | 45 | logger.setLevel(log_level) 46 | logger.info("Log level set to %s" % logger.getEffectiveLevel()) 47 | 48 | event_bus_name = os.environ.get("EVENT_BUS_NAME", "default") 49 | event_rule_detail_type = os.environ.get("EVENT_RULE_DETAIL_TYPE", "aws-ia2") # assume there could be multiple deployment of this module, this will ensure each rule are unique 50 | event_bridge_client = boto3.client("events") 51 | 52 | ## Add user-agent to event-bridge event 53 | def _add_header(request, **kwargs): 54 | userAgentHeader = request.headers["User-Agent"] + " fURLWebhook/1.0 (HashiCorp)" 55 | del request.headers["User-Agent"] 56 | request.headers["User-Agent"] = userAgentHeader 57 | 58 | ## Add user-agent to event-bridge event 59 | event_system = event_bridge_client.meta.events 60 | event_system.register_first("before-sign.events.PutEvents", _add_header) 61 | 62 | class PutEventError(Exception): 63 | """Raised when Put Events Failed""" 64 | pass 65 | 66 | def lambda_handler(event, _): 67 | """Terraform run task function""" 68 | logger.debug(json.dumps(event)) 69 | 70 | headers = event.get("headers") 71 | # Input validation 72 | try: 73 | json_payload = get_json_payload(event=event) 74 | except ValueError as err: 75 | print_error(f"400 Bad Request - {err}", headers) 76 | return {"statusCode": 400, "body": str(err)} 77 | except BaseException as err: # Unexpected Error 78 | print_error( 79 | "500 Internal Server Error\n" + f"Unexpected error: {err}, {type(err)}", 80 | headers, 81 | ) 82 | return {"statusCode": 500, "body": "Internal Server Error"} 83 | 84 | try: 85 | if hcp_tf_use_waf == "True" and not contains_valid_cloudfront_signature( 86 | event=event 87 | ): 88 | print_error("401 Unauthorized - Invalid CloudFront Signature", headers) 89 | return {"statusCode": 401, "body": "Invalid CloudFront Signature"} 90 | 91 | if not contains_valid_signature(event=event): 92 | print_error("401 Unauthorized - Invalid Payload Signature", headers) 93 | return {"statusCode": 401, "body": "Invalid Payload Signature"} 94 | 95 | response = forward_event(json_payload, event_rule_detail_type) 96 | 97 | if response["FailedEntryCount"] > 0: 98 | print_error( 99 | "500 FailedEntry Error - The event was not successfully forwarded to Amazon EventBridge\n" 100 | + str(response["Entries"][0]), 101 | headers, 102 | ) 103 | return { 104 | "statusCode": 500, 105 | "body": "FailedEntry Error - The entry could not be successfully forwarded to Amazon EventBridge", 106 | } 107 | 108 | return {"statusCode": 200, "body": "Message forwarded to Amazon EventBridge"} 109 | 110 | except PutEventError as err: 111 | print_error(f"500 Put Events Error - {err}", headers) 112 | return { 113 | "statusCode": 500, 114 | "body": "Internal Server Error - The request was rejected by Amazon EventBridge API", 115 | } 116 | 117 | except BaseException as err: # Unexpected Error 118 | print_error( 119 | "500 Internal Server Error\n" + f"Unexpected error: {err}, {type(err)}", 120 | headers, 121 | ) 122 | return {"statusCode": 500, "body": "Internal Server Error"} 123 | 124 | 125 | def normalize_payload(raw_payload, is_base64_encoded): 126 | """Decode payload if needed""" 127 | if raw_payload is None: 128 | raise ValueError("Missing event body") 129 | if is_base64_encoded: 130 | return base64.b64decode(raw_payload).decode("utf-8") 131 | return raw_payload 132 | 133 | 134 | def contains_valid_cloudfront_signature( 135 | event, 136 | ): # Check for the special header value from CloudFront 137 | try: 138 | secret = cache.get_secret_string(hcp_tf_cf_secret_arn) 139 | payload_signature = event["headers"]["x-cf-sig"] 140 | if secret == payload_signature: 141 | return True 142 | else: 143 | return False 144 | except: 145 | logger.error("Unable to validate CloudFront custom header signature value") 146 | return False 147 | 148 | 149 | def contains_valid_signature(event): 150 | """Check for the payload signature 151 | HashiCorp Terraform run task documentation: https://developer.hashicorp.com/terraform/cloud-docs/integrations/run-tasks#securing-your-run-task 152 | """ 153 | secret = cache.get_secret_string(hcp_tf_hmac_secret_arn) 154 | payload_bytes = get_payload_bytes( 155 | raw_payload=event["body"], is_base64_encoded=event["isBase64Encoded"] 156 | ) 157 | computed_signature = compute_signature(payload_bytes=payload_bytes, secret=secret) 158 | 159 | return hmac.compare_digest( 160 | event["headers"].get("x-tfc-task-signature", ""), computed_signature 161 | ) 162 | 163 | 164 | def get_payload_bytes(raw_payload, is_base64_encoded): 165 | """Get payload bytes to feed hash function""" 166 | if is_base64_encoded: 167 | return base64.b64decode(raw_payload) 168 | else: 169 | return raw_payload.encode() 170 | 171 | 172 | def compute_signature(payload_bytes, secret): 173 | """Compute HMAC-SHA512""" 174 | m = hmac.new(key=secret.encode(), msg=payload_bytes, digestmod=hashlib.sha512) 175 | return m.hexdigest() 176 | 177 | 178 | def get_json_payload(event): 179 | """Get JSON string from payload""" 180 | content_type = get_content_type(event.get("headers", {})) 181 | if not ( 182 | content_type == "application/json" 183 | or content_type == "application/x-www-form-urlencoded" 184 | ): 185 | raise ValueError("Unsupported content-type") 186 | 187 | payload = normalize_payload( 188 | raw_payload=event.get("body"), is_base64_encoded=event["isBase64Encoded"] 189 | ) 190 | 191 | if content_type == "application/x-www-form-urlencoded": 192 | parsed_qs = urllib.parse.parse_qs(payload) 193 | if "payload" not in parsed_qs or len(parsed_qs["payload"]) != 1: 194 | raise ValueError("Invalid urlencoded payload") 195 | 196 | payload = parsed_qs["payload"][0] 197 | 198 | try: 199 | json.loads(payload) 200 | 201 | except ValueError as err: 202 | raise ValueError("Invalid JSON payload") from err 203 | 204 | return payload 205 | 206 | 207 | def forward_event(payload, detail_type): 208 | """Forward event to EventBridge""" 209 | try: 210 | return event_bridge_client.put_events( 211 | Entries=[ 212 | { 213 | "Source": "app.terraform.io", 214 | "DetailType": detail_type, 215 | "Detail": payload, 216 | "EventBusName": event_bus_name, 217 | }, 218 | ] 219 | ) 220 | except BaseException as err: 221 | raise PutEventError("Put Events Failed") 222 | 223 | 224 | def get_content_type(headers): 225 | """Helper function to parse content-type from the header""" 226 | raw_content_type = headers.get("content-type") 227 | 228 | if raw_content_type is None: 229 | return None 230 | content_type, _ = parse_header(raw_content_type) 231 | return content_type 232 | 233 | 234 | def print_error(message, headers): 235 | """Helper function to print errors""" 236 | logger.error(f"ERROR: {message}\nHeaders: {str(headers)}") -------------------------------------------------------------------------------- /lambda/runtask_eventbridge/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-secretsmanager-caching -------------------------------------------------------------------------------- /lambda/runtask_fulfillment/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = $(CURDIR) 2 | 3 | all: build 4 | 5 | .PHONY: clean build 6 | 7 | clean: 8 | rm -rf build 9 | rm -rf site-packages 10 | 11 | build: 12 | $(info ************ Starting Build: $(PROJECT) ************) 13 | mkdir -p site-packages 14 | cp *.* ./site-packages 15 | mkdir -p build 16 | python3 -m venv build/ 17 | . build/bin/activate; \ 18 | pip3 install -r requirements.txt -t ./site-packages; 19 | rm -rf build -------------------------------------------------------------------------------- /lambda/runtask_fulfillment/default.yaml: -------------------------------------------------------------------------------- 1 | # Config map consisting the Terraform plan IAM policy attribute name and IAM Access Analyzer policy type 2 | iamConfigMap: 3 | aws_iam_group_policy: 4 | attribute: policy 5 | type: IDENTITY_POLICY 6 | aws_iam_policy: 7 | attribute: policy 8 | type: IDENTITY_POLICY 9 | aws_iam_role: 10 | - attribute: assume_role_policy 11 | type: RESOURCE_POLICY 12 | - attribute: inline_policy.policy 13 | type: IDENTITY_POLICY 14 | aws_iam_role_policy: 15 | attribute: policy 16 | type: IDENTITY_POLICY 17 | aws_iam_user_policy: 18 | attribute: policy 19 | type: RESOURCE_POLICY 20 | aws_api_gateway_rest_api: 21 | attribute: policy 22 | type: RESOURCE_POLICY 23 | aws_api_gateway_rest_api_policy: 24 | attribute: policy 25 | type: RESOURCE_POLICY 26 | aws_backup_vault_policy: 27 | attribute: policy 28 | type: RESOURCE_POLICY 29 | aws_cloudwatch_event_bus_policy: 30 | attribute: policy 31 | type: RESOURCE_POLICY 32 | aws_cloudwatch_log_destination_policy: 33 | attribute: access_policy 34 | type: RESOURCE_POLICY 35 | aws_cloudwatch_log_resource_policy: 36 | attribute: policy 37 | type: RESOURCE_POLICY 38 | aws_codeartifact_domain_permissions_policy: 39 | attribute: policy_document 40 | type: RESOURCE_POLICY 41 | aws_codeartifact_repository_permissions_policy: 42 | attribute: policy_document 43 | type: RESOURCE_POLICY 44 | aws_codebuild_resource_policy: 45 | attribute: policy 46 | type: RESOURCE_POLICY 47 | aws_ecr_registry_policy: 48 | attribute: policy 49 | type: RESOURCE_POLICY 50 | aws_ecr_repository_policy: 51 | attribute: policy 52 | type: RESOURCE_POLICY 53 | aws_ecrpublic_repository_policy: 54 | attribute: policy 55 | type: RESOURCE_POLICY 56 | aws_efs_file_system_policy: 57 | attribute: policy 58 | type: RESOURCE_POLICY 59 | aws_elasticsearch_domain: 60 | attribute: access_policies 61 | type: RESOURCE_POLICY 62 | aws_elasticsearch_domain_policy: 63 | attribute: access_policies 64 | type: RESOURCE_POLICY 65 | aws_glacier_vault: 66 | attribute: access_policy 67 | type: RESOURCE_POLICY 68 | aws_glacier_vault_lock: 69 | attribute: access_policy 70 | type: RESOURCE_POLICY 71 | aws_glue_resource_policy: 72 | attribute: policy 73 | type: RESOURCE_POLICY 74 | aws_iot_policy: 75 | attribute: policy 76 | type: RESOURCE_POLICY 77 | aws_kms_external_key: 78 | attribute: policy 79 | type: RESOURCE_POLICY 80 | aws_kms_key: 81 | attribute: policy 82 | type: RESOURCE_POLICY 83 | aws_kms_replica_external_key: 84 | attribute: policy 85 | type: RESOURCE_POLICY 86 | aws_kms_replica_key: 87 | attribute: policy 88 | type: RESOURCE_POLICY 89 | aws_lambda_layer_version_permission: 90 | attribute: policy 91 | type: RESOURCE_POLICY 92 | aws_media_store_container_policy: 93 | attribute: policy 94 | type: RESOURCE_POLICY 95 | aws_networkfirewall_resource_policy: 96 | attribute: policy 97 | type: RESOURCE_POLICY 98 | aws_organizations_policy: 99 | attribute: content 100 | type: SERVICE_CONTROL_POLICY 101 | aws_s3_access_point: 102 | attribute: policy 103 | type: RESOURCE_POLICY 104 | aws_s3_bucket: 105 | attribute: policy 106 | type: RESOURCE_POLICY 107 | aws_s3_bucket_policy: 108 | attribute: policy 109 | type: RESOURCE_POLICY 110 | aws_s3control_access_point_policy: 111 | attribute: policy 112 | type: RESOURCE_POLICY 113 | aws_s3control_bucket_policy: 114 | attribute: policy 115 | type: RESOURCE_POLICY 116 | aws_s3control_multi_region_access_point_policy: 117 | attribute: details.policy 118 | type: RESOURCE_POLICY 119 | aws_s3control_object_lambda_access_point_policy: 120 | attribute: policy 121 | type: RESOURCE_POLICY 122 | aws_ses_identity_policy: 123 | attribute: policy 124 | type: RESOURCE_POLICY 125 | aws_sns_topic: 126 | attribute: policy 127 | type: RESOURCE_POLICY 128 | aws_sns_topic_policy: 129 | attribute: policy 130 | type: RESOURCE_POLICY 131 | aws_sqs_queue: 132 | attribute: policy 133 | type: RESOURCE_POLICY 134 | aws_sqs_queue_policy: 135 | attribute: policy 136 | type: RESOURCE_POLICY 137 | aws_ssoadmin_permission_set_inline_policy: 138 | attribute: inline_policy 139 | type: RESOURCE_POLICY 140 | aws_sagemaker_model_package_group_policy: 141 | attribute: resource_policy 142 | type: RESOURCE_POLICY 143 | aws_secretsmanager_secret: 144 | attribute: policy 145 | type: RESOURCE_POLICY 146 | aws_secretsmanager_secret_policy: 147 | attribute: policy 148 | type: RESOURCE_POLICY 149 | aws_transfer_access: 150 | attribute: policy 151 | type: RESOURCE_POLICY 152 | aws_transfer_user: 153 | attribute: policy 154 | type: RESOURCE_POLICY 155 | aws_vpc_endpoint: 156 | attribute: policy 157 | type: RESOURCE_POLICY -------------------------------------------------------------------------------- /lambda/runtask_fulfillment/handler.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 12 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 14 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 15 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | ''' 17 | import json 18 | import logging 19 | import os 20 | import boto3 21 | import time 22 | import yaml 23 | import re 24 | from urllib.request import urlopen, Request 25 | from urllib.error import HTTPError, URLError 26 | from urllib.parse import urlencode 27 | from enum import Enum 28 | from collections import Counter 29 | 30 | session = boto3.Session() 31 | ia2_client = session.client('accessanalyzer') 32 | cwl_client = session.client('logs') 33 | logger = logging.getLogger() 34 | 35 | iamConfigMap = {} # map of terraform plan attribute and IAM access analyzer resource type, loaded from default.yaml 36 | 37 | log_level = os.environ.get("log_level", logging.INFO) 38 | 39 | logger = logging.getLogger() 40 | logger.setLevel(log_level) 41 | 42 | if "SUPPORTED_POLICY_DOCUMENT" in os.environ: 43 | SUPPORTED_POLICY_DOCUMENT = os.environ["SUPPORTED_POLICY_DOCUMENT"] 44 | else: 45 | SUPPORTED_POLICY_DOCUMENT = False # default to False and then load it from config file default.yaml 46 | 47 | HCP_TF_HOST_NAME = os.environ.get("HCP_TF_HOST_NAME", "app.terraform.io") 48 | 49 | 50 | IAM_ACCESS_ANALYZER_COUNTER = { 51 | "ERROR" : 0, 52 | "SECURITY_WARNING" : 0, 53 | "SUGGESTION" : 0, 54 | "WARNING" : 0 55 | } 56 | 57 | if "CW_LOG_GROUP_NAME" in os.environ: 58 | LOG_GROUP_NAME = os.environ["CW_LOG_GROUP_NAME"] 59 | LOG_STREAM_NAME = "" 60 | SEQUENCE_TOKEN = "" # nosec B105 61 | else: # disable logging if environment variable is not set 62 | LOG_GROUP_NAME = False 63 | 64 | def lambda_handler(event, context): 65 | logger.debug(json.dumps(event)) 66 | try: 67 | if not iamConfigMap: load_config("default.yaml") # load the config file 68 | 69 | # Get plan output from HCP Terraform 70 | endpoint = event["payload"]["detail"]["plan_json_api_url"] 71 | access_token = event["payload"]["detail"]["access_token"] 72 | headers = __build_standard_headers(access_token) 73 | response, response_raw = __get(endpoint, headers) 74 | json_response = json.loads(response.decode("utf-8")) 75 | logger.debug("Headers : {}".format(response_raw.headers)) 76 | logger.debug("JSON Response : {}".format(json.dumps(json_response))) 77 | 78 | # Get workspace and run task metadata 79 | run_id = event["payload"]["detail"]["run_id"] 80 | workspace_id = event["payload"]["detail"]["workspace_id"] 81 | 82 | # Initialize log 83 | global LOG_STREAM_NAME 84 | LOG_STREAM_NAME = workspace_id + "_" + run_id 85 | log_helper(LOG_GROUP_NAME, LOG_STREAM_NAME, 86 | "Start IAM Access Analyzer analysis for workspace: {} - run: {}".format(workspace_id, run_id) 87 | ) 88 | 89 | if get_plan_changes(json_response): # Check if there are any changes in plan output 90 | logger.info("Resource changes detected") 91 | total_ia2_violation_count = ia2_handler(json_response["resource_changes"]) # analyze and calculate number of violations 92 | fulfillment_response = fulfillment_response_helper(total_ia2_violation_count, skip_log = False) # generate response 93 | else: 94 | logger.info("No resource changes detected") 95 | fulfillment_response = fulfillment_response_helper(total_ia2_violation_count = {}, skip_log = True, override_message = "No resource changes detected", override_status = "passed") # override response 96 | 97 | return fulfillment_response 98 | 99 | except Exception as e: # run-task must return response despite of exception 100 | logger.exception("Run Task Fulfillment error: {}".format(e)) 101 | fulfillment_response = fulfillment_response_helper(total_ia2_violation_count = {}, skip_log = True, override_message = "Run Task IAM Access Analyzer failed to complete successfully", override_status = "failed") # override response 102 | return fulfillment_response 103 | 104 | def get_plan_changes(plan_payload): 105 | if "resource_changes" in plan_payload: 106 | return True 107 | else: 108 | return False 109 | 110 | # IAM Access Analyzer handler: 111 | # Search for resource changes in Terraform plan that match the supported resource 112 | # Map the right Terraform plan attribute according to resource type to find the policy value 113 | # Analyze the policy using IAM Access Analyzer 114 | # Calculate the number of findings per policy, per resource and total all resources 115 | def ia2_handler(plan_resource_changes): 116 | total_ia2_violation_count = IAM_ACCESS_ANALYZER_COUNTER 117 | 118 | for resource in plan_resource_changes: # look for resource changes and match the supported policy document 119 | if resource["type"] in SUPPORTED_POLICY_DOCUMENT: 120 | logger.info("Resource : {}".format(json.dumps(resource))) 121 | ia2_violation_count = analyze_resource_policy_changes(resource) # get the policy difference per resource 122 | if ia2_violation_count: # calculate total violation count 123 | total_ia2_violation_count = iam_policy_violation_counter_helper(total_ia2_violation_count, ia2_violation_count) 124 | else: 125 | logger.info("Resource type : {} is not supported".format(resource["type"])) 126 | 127 | return total_ia2_violation_count 128 | 129 | def analyze_resource_policy_changes(resource): # parse terraform plan to find the policy changes and validate it with IAM Access analyzer 130 | if "create" in resource["change"]["actions"]: # skip any deleted resources 131 | resource_violation_counter = IAM_ACCESS_ANALYZER_COUNTER 132 | resource_config_map = get_resource_type_and_attribute(resource) # look up from config map to find the right attribute and resource type 133 | 134 | for item in resource_config_map: # certain resource type have two attributes (i.e. iam role assume policy and in-line policy) 135 | # check for nested attribute , i.e. for aws_iam_role : inline_policy.policy 136 | if "." in item["attribute"]: 137 | item_attribute, item_sub_attribute = item["attribute"].split(".") 138 | else: 139 | item_attribute = item["attribute"] 140 | item_sub_attribute = False 141 | item_type = item["type"] 142 | logger.info("Policy type : {}".format(item_type)) 143 | 144 | if item_attribute in resource["change"]["after"]: # ensure that the policy is available in plan output 145 | resource_policies = get_resource_policy(item_attribute, item_sub_attribute, resource) 146 | per_item_violation_counter = IAM_ACCESS_ANALYZER_COUNTER 147 | 148 | for policy in resource_policies: # resource like iam_role can include multiple in-line policies 149 | iam_policy = json.loads(policy) # take the new changed policy document 150 | logger.info("Policy : {}".format(json.dumps(iam_policy))) 151 | 152 | ia2_response = validate_policy(json.dumps(iam_policy), item_type) # run IAM Access analyzer validation 153 | logger.info("Response : {}".format(ia2_response["findings"])) 154 | 155 | per_policy_violation_counter = get_iam_policy_violation_count(resource, ia2_response) # calculate any IA2 violations 156 | per_item_violation_counter = iam_policy_violation_counter_helper(per_item_violation_counter, per_policy_violation_counter) # sum all findings per resource item 157 | 158 | resource_violation_counter = iam_policy_violation_counter_helper(resource_violation_counter, per_item_violation_counter) # sum all findings per resource 159 | 160 | elif item_attribute in resource["change"]["after_unknown"] and resource["change"]["after_unknown"][item_attribute] == True: # missing computed values is not supported 161 | logger.info("Unsupported resource due to missing computed values") 162 | log_helper(LOG_GROUP_NAME, LOG_STREAM_NAME, "resource: {} - unsupported resource due to missing computed values" .format(resource["address"])) 163 | 164 | return resource_violation_counter 165 | 166 | elif "delete" in resource["change"]["actions"]: 167 | logger.info("New policy is null / deleted") 168 | log_helper(LOG_GROUP_NAME, LOG_STREAM_NAME, "resource: {} - policy is null / deleted" .format(resource["address"])) 169 | 170 | else: 171 | logger.error("Unknown / unsupported action") 172 | raise 173 | 174 | def get_resource_type_and_attribute(resource): # look up resource type and terraform plan attribute name from config file 175 | if isinstance(iamConfigMap[resource["type"]], list): 176 | return iamConfigMap[resource["type"]] 177 | else: 178 | return [iamConfigMap[resource["type"]]] # return it as list 179 | 180 | def get_resource_policy(attribute, sub_attribute, resource): # extract the resource policy, including nested policies 181 | resource_policies = [] 182 | 183 | if not sub_attribute: # standard non-nested attribute 184 | resource_policies.append(resource["change"]["after"][attribute]) 185 | 186 | else: # nested attribute 187 | # convert all nested attribute into list for easy comparison 188 | if isinstance (resource["change"]["after"][attribute], list): 189 | sub_attribute_policies = resource["change"]["after"][attribute] 190 | else: 191 | sub_attribute_policies = [resource["change"]["after"][attribute]] 192 | 193 | for item in sub_attribute_policies: # resource like iam_role can include multiple in-line policies 194 | if sub_attribute in item.keys(): 195 | resource_policies.append(item[sub_attribute]) 196 | 197 | return resource_policies 198 | 199 | def get_iam_policy_violation_count(resource, ia2_response): # count the policy violation and return a dictionary 200 | ia2_violation_count = { 201 | "ERROR" : 0, 202 | "SECURITY_WARNING" : 0, 203 | "SUGGESTION" : 0, 204 | "WARNING" : 0 205 | } 206 | 207 | if len(ia2_response["findings"]) > 0: # calculate violation if there's any findings 208 | for finding in ia2_response["findings"]: 209 | ia2_violation_count[finding["findingType"]] += 1 210 | log_helper(LOG_GROUP_NAME, LOG_STREAM_NAME, "resource: {} ".format(resource["address"]) + json.dumps(finding)) 211 | else: 212 | log_helper(LOG_GROUP_NAME, LOG_STREAM_NAME, "resource: {} - no new findings".format(resource["address"]) ) 213 | 214 | logger.info("Findings : {}".format(ia2_violation_count)) 215 | return ia2_violation_count 216 | 217 | def iam_policy_violation_counter_helper(total_ia2_violation_count, ia2_violation_count): # add new violation to existing counter 218 | total_counter = Counter(total_ia2_violation_count) 219 | total_counter.update(Counter(ia2_violation_count)) 220 | total_ia2_violation_count = dict(total_counter) 221 | return total_ia2_violation_count 222 | 223 | def validate_policy(policy_document, policy_type): # call IAM access analyzer to validate policy 224 | response = ia2_client.validate_policy( 225 | policyDocument=policy_document, 226 | policyType=policy_type 227 | ) 228 | return response 229 | 230 | def log_helper(log_group_name, log_stream_name, log_message): # helper function to write RunTask results to dedicated cloudwatch log group 231 | if log_group_name: # true if CW log group name is specified 232 | global SEQUENCE_TOKEN 233 | try: 234 | SEQUENCE_TOKEN = log_writer(log_group_name, log_stream_name, log_message, SEQUENCE_TOKEN)["nextSequenceToken"] 235 | except: 236 | cwl_client.create_log_stream(logGroupName = log_group_name,logStreamName = log_stream_name) 237 | SEQUENCE_TOKEN = log_writer(log_group_name, log_stream_name, log_message)["nextSequenceToken"] 238 | 239 | def log_writer(log_group_name, log_stream_name, log_message, sequence_token = False): # writer to CloudWatch log stream based on sequence token 240 | if sequence_token: # if token exist, append to the previous token stream 241 | response = cwl_client.put_log_events( 242 | logGroupName = log_group_name, 243 | logStreamName = log_stream_name, 244 | logEvents = [{ 245 | 'timestamp' : int(round(time.time() * 1000)), 246 | 'message' : time.strftime('%Y-%m-%d %H:%M:%S') + ": " + log_message 247 | }], 248 | sequenceToken = sequence_token 249 | ) 250 | else: # new log stream, no token exist 251 | response = cwl_client.put_log_events( 252 | logGroupName = log_group_name, 253 | logStreamName = log_stream_name, 254 | logEvents = [{ 255 | 'timestamp' : int(round(time.time() * 1000)), 256 | 'message' : time.strftime('%Y-%m-%d %H:%M:%S') + ": " + log_message 257 | }] 258 | ) 259 | return response 260 | 261 | def fulfillment_response_helper(total_ia2_violation_count, skip_log = False, override_message = False, override_status = False): # helper function to send response to callback step function 262 | runtask_response = {} # run tasks call back includes three attribute: status, message and url 263 | 264 | # Return message 265 | if not override_message: 266 | fulfillment_output = "{} ERROR, {} SECURITY_WARNING, {} SUGGESTION, {} WARNING".format( 267 | total_ia2_violation_count["ERROR"], total_ia2_violation_count["SECURITY_WARNING"], total_ia2_violation_count["SUGGESTION"], total_ia2_violation_count["WARNING"]) 268 | else: 269 | fulfillment_output = override_message 270 | logger.info("Summary : " + fulfillment_output) 271 | runtask_response["message"] = fulfillment_output 272 | 273 | # Hyperlink to CloudWatch log 274 | if not skip_log: 275 | if LOG_GROUP_NAME: 276 | fulfillment_logs_link = "https://console.aws.amazon.com/cloudwatch/home?region={}#logEventViewer:group={};stream={}".format(os.environ["AWS_REGION"], LOG_GROUP_NAME, LOG_STREAM_NAME) 277 | else: 278 | fulfillment_logs_link = "https://console.aws.amazon.com" 279 | logger.info("Logs : " + fulfillment_logs_link) 280 | else: 281 | fulfillment_logs_link = False 282 | runtask_response["url"] = fulfillment_logs_link 283 | 284 | # Run Tasks status 285 | if not override_status: 286 | if total_ia2_violation_count["ERROR"] + total_ia2_violation_count["SECURITY_WARNING"] > 0: 287 | fulfillment_status = "failed" 288 | else: 289 | fulfillment_status = "passed" 290 | else: 291 | fulfillment_status = override_status 292 | logger.info("Status : " + fulfillment_status) 293 | runtask_response["status"] = fulfillment_status 294 | 295 | return runtask_response 296 | 297 | def __build_standard_headers(api_token): # TFC API header 298 | return { 299 | "Authorization": "Bearer {}".format(api_token), 300 | "Content-type": "application/vnd.api+json", 301 | } 302 | 303 | def __get(endpoint, headers): # HTTP request helper function 304 | request = Request(endpoint, headers=headers or {}, method = "GET") 305 | try: 306 | if validate_endpoint(endpoint): 307 | with urlopen(request, timeout=10) as response: #nosec URL validation 308 | return response.read(), response 309 | else: 310 | raise URLError("Invalid endpoint URL, expected host is: {}".format(HCP_TF_HOST_NAME)) 311 | except HTTPError as error: 312 | logger.error(error.status, error.reason) 313 | except URLError as error: 314 | logger.error(error.reason) 315 | except TimeoutError: 316 | logger.error("Request timed out") 317 | 318 | def validate_endpoint(endpoint): # validate that the endpoint hostname is valid 319 | pattern = "^https:\/\/" + str(HCP_TF_HOST_NAME).replace(".", "\.") + "\/"+ ".*" 320 | result = re.match(pattern, endpoint) 321 | return result 322 | 323 | def load_config(file_name): # load the config file 324 | global iamConfigMap 325 | global SUPPORTED_POLICY_DOCUMENT 326 | 327 | with open(file_name, "r") as config_stream: 328 | config_dict = yaml.safe_load(config_stream) 329 | 330 | iamConfigMap = config_dict.get("iamConfigMap") # load the config map 331 | logger.debug("Config map loaded: {}".format(json.dumps(iamConfigMap))) 332 | 333 | if not SUPPORTED_POLICY_DOCUMENT: # load the supported resource if there's no override from environment variables 334 | SUPPORTED_POLICY_DOCUMENT = list(iamConfigMap.keys()) -------------------------------------------------------------------------------- /lambda/runtask_fulfillment/requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml -------------------------------------------------------------------------------- /lambda/runtask_request/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT = $(CURDIR) 2 | 3 | all: build 4 | 5 | .PHONY: clean build 6 | 7 | clean: 8 | rm -rf build 9 | rm -rf site-packages 10 | 11 | build: 12 | $(info ************ Starting Build: $(PROJECT) ************) 13 | mkdir -p site-packages 14 | cp *.* ./site-packages 15 | mkdir -p build 16 | python3 -m venv build/ 17 | . build/bin/activate; \ 18 | pip3 install -r requirements.txt -t ./site-packages; 19 | rm -rf build -------------------------------------------------------------------------------- /lambda/runtask_request/handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 12 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 14 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 15 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | """ 17 | 18 | import json 19 | import logging 20 | import os 21 | 22 | HCP_TF_ORG = os.environ.get("HCP_TF_ORG", False) 23 | WORKSPACE_PREFIX = os.environ.get("WORKSPACE_PREFIX", False) 24 | RUNTASK_STAGES = os.environ.get("RUNTASK_STAGES", False) 25 | EVENT_RULE_DETAIL_TYPE = os.environ.get("EVENT_RULE_DETAIL_TYPE", "aws-ia2") # assume there could be multiple deployment of this module, this will ensure each rule are unique 26 | 27 | logger = logging.getLogger() 28 | log_level = os.environ.get("log_level", logging.INFO) 29 | 30 | logger.setLevel(log_level) 31 | logger.info("Log level set to %s" % logger.getEffectiveLevel()) 32 | 33 | 34 | def lambda_handler(event, _): 35 | logger.debug(json.dumps(event)) 36 | try: 37 | VERIFY = True 38 | if event["payload"]["detail-type"] == EVENT_RULE_DETAIL_TYPE: 39 | if ( 40 | HCP_TF_ORG 41 | and event["payload"]["detail"]["organization_name"] != HCP_TF_ORG 42 | ): 43 | logger.error( 44 | "HCP Terraform Org verification failed : {}".format( 45 | event["payload"]["detail"]["organization_name"] 46 | ) 47 | ) 48 | VERIFY = False 49 | if WORKSPACE_PREFIX and not ( 50 | str(event["payload"]["detail"]["workspace_name"]).startswith( 51 | WORKSPACE_PREFIX 52 | ) 53 | ): 54 | logger.error( 55 | "HCP Terraform workspace prefix verification failed : {}".format( 56 | event["payload"]["detail"]["workspace_name"] 57 | ) 58 | ) 59 | VERIFY = False 60 | if RUNTASK_STAGES and not ( 61 | event["payload"]["detail"]["stage"] in RUNTASK_STAGES 62 | ): 63 | logger.error( 64 | "HCP Terraform run task stage verification failed: {}".format( 65 | event["payload"]["detail"]["stage"] 66 | ) 67 | ) 68 | VERIFY = False 69 | 70 | if VERIFY: 71 | return "verified" 72 | else: 73 | return "unverified" 74 | 75 | except Exception as e: 76 | logger.exception("Run Task Request error: {}".format(e)) 77 | raise -------------------------------------------------------------------------------- /lambda/runtask_request/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-ia/terraform-aws-runtask-iam-access-analyzer/cad781c0e7ee1bea0124420262e4ea3755ea9f63/lambda/runtask_request/requirements.txt -------------------------------------------------------------------------------- /locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | solution_prefix = "${var.name_prefix}-${random_string.solution_prefix.result}" 3 | 4 | lambda_managed_policies = [data.aws_iam_policy.aws_lambda_basic_execution_role.arn] 5 | lambda_reserved_concurrency = var.lambda_reserved_concurrency 6 | lambda_default_timeout = var.lambda_default_timeout 7 | lambda_python_runtime = "python3.11" 8 | lambda_architecture = [var.lambda_architecture] 9 | 10 | cloudwatch_log_group_name = var.cloudwatch_log_group_name 11 | 12 | waf_deployment = var.deploy_waf ? 1 : 0 13 | waf_rate_limit = var.waf_rate_limit 14 | 15 | cloudfront_sig_name = "x-cf-sig" 16 | cloudfront_custom_header = { 17 | name = local.cloudfront_sig_name 18 | value = var.deploy_waf ? aws_secretsmanager_secret_version.runtask_cloudfront[0].secret_string : null 19 | } 20 | 21 | combined_tags = merge( 22 | var.tags, 23 | { 24 | Solution = local.solution_prefix 25 | } 26 | ) 27 | 28 | } 29 | 30 | resource "random_string" "solution_prefix" { 31 | length = 4 32 | special = false 33 | upper = false 34 | } 35 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "runtask_hmac" { 2 | value = aws_secretsmanager_secret_version.runtask_hmac.secret_string 3 | description = "HMAC key value, keep this sensitive data safe" 4 | sensitive = true 5 | } 6 | 7 | output "runtask_url" { 8 | value = var.deploy_waf ? "https://${module.runtask_cloudfront[0].cloudfront_distribution_domain_name}" : trim(aws_lambda_function_url.runtask_eventbridge.function_url, "/") 9 | description = "The Run Tasks URL endpoint, you can use this to configure the Run Task setup in HCP Terraform" 10 | } 11 | 12 | output "runtask_id" { 13 | value = tfe_organization_run_task.aws_iam_analyzer.id 14 | description = "The Run Tasks id configured in HCP Terraform" 15 | } -------------------------------------------------------------------------------- /providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { # for Cloudfront WAF only - must be in us-east-1 2 | region = "us-east-1" 3 | alias = "cloudfront_waf" 4 | } -------------------------------------------------------------------------------- /runtasks.tf: -------------------------------------------------------------------------------- 1 | resource "tfe_organization_run_task" "aws_iam_analyzer" { 2 | organization = var.tfc_org 3 | url = var.deploy_waf ? "https://${module.runtask_cloudfront[0].cloudfront_distribution_domain_name}" : trim(aws_lambda_function_url.runtask_eventbridge.function_url, "/") 4 | name = "${var.name_prefix}-runtask" 5 | enabled = true 6 | hmac_key = aws_secretsmanager_secret_version.runtask_hmac.secret_string 7 | description = "Run Task integration with AWS IAM Access Analyzer" 8 | depends_on = [aws_lambda_function.runtask_eventbridge] # explicit dependency for URL verification 9 | } 10 | -------------------------------------------------------------------------------- /secrets.tf: -------------------------------------------------------------------------------- 1 | resource "random_uuid" "runtask_hmac" {} 2 | 3 | resource "aws_secretsmanager_secret" "runtask_hmac" { 4 | #checkov:skip=CKV2_AWS_57:run terraform apply to rotate hmac 5 | name = "${var.name_prefix}-runtask-hmac" 6 | recovery_window_in_days = var.recovery_window 7 | kms_key_id = aws_kms_key.runtask_key.arn 8 | tags = local.combined_tags 9 | } 10 | 11 | resource "aws_secretsmanager_secret_version" "runtask_hmac" { 12 | secret_id = aws_secretsmanager_secret.runtask_hmac.id 13 | secret_string = random_uuid.runtask_hmac.result 14 | } 15 | 16 | resource "random_uuid" "runtask_cloudfront" { 17 | count = local.waf_deployment 18 | } 19 | 20 | resource "aws_secretsmanager_secret" "runtask_cloudfront" { 21 | #checkov:skip=CKV2_AWS_57:run terraform apply to rotate cloudfront secret 22 | count = local.waf_deployment 23 | name = "${var.name_prefix}-runtask_cloudfront" 24 | recovery_window_in_days = var.recovery_window 25 | kms_key_id = aws_kms_key.runtask_key.arn 26 | tags = local.combined_tags 27 | } 28 | 29 | resource "aws_secretsmanager_secret_version" "runtask_cloudfront" { 30 | count = local.waf_deployment 31 | secret_id = aws_secretsmanager_secret.runtask_cloudfront[count.index].id 32 | secret_string = random_uuid.runtask_cloudfront[count.index].result 33 | } 34 | -------------------------------------------------------------------------------- /states.tf: -------------------------------------------------------------------------------- 1 | resource "aws_sfn_state_machine" "runtask_states" { 2 | name = "${var.name_prefix}-runtask-statemachine" 3 | role_arn = aws_iam_role.runtask_states.arn 4 | definition = templatefile("${path.module}/states/runtask_states.asl.json", { 5 | resource_runtask_request = aws_lambda_function.runtask_request.arn 6 | resource_runtask_fulfillment = aws_lambda_function.runtask_fulfillment.arn 7 | resource_runtask_callback = aws_lambda_function.runtask_callback.arn 8 | }) 9 | 10 | logging_configuration { 11 | log_destination = "${aws_cloudwatch_log_group.runtask_states.arn}:*" 12 | include_execution_data = true 13 | level = "ERROR" 14 | } 15 | 16 | tracing_configuration { 17 | enabled = true 18 | } 19 | tags = local.combined_tags 20 | } 21 | 22 | resource "aws_cloudwatch_log_group" "runtask_states" { 23 | name = "/aws/vendedlogs/states/${var.name_prefix}-runtask-statemachine" 24 | retention_in_days = var.cloudwatch_log_group_retention 25 | kms_key_id = aws_kms_key.runtask_key.arn 26 | tags = local.combined_tags 27 | } -------------------------------------------------------------------------------- /states/runtask_states.asl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "HCP Terraform - Run Task Handler Demo", 3 | "StartAt": "runtask_request", 4 | "States": { 5 | "runtask_request": { 6 | "Type": "Task", 7 | "Resource": "arn:aws:states:::lambda:invoke", 8 | "Parameters": { 9 | "FunctionName": "${resource_runtask_request}:$LATEST", 10 | "Payload": { 11 | "job_name.$": "$$.Execution.Name", 12 | "payload.$": "$", 13 | "action": "request" 14 | } 15 | }, 16 | "InputPath" : "$", 17 | "ResultPath": "$.result.request", 18 | "OutputPath": "$", 19 | "ResultSelector": { 20 | "status.$": "$.Payload", 21 | "raw.$": "$" 22 | }, 23 | "Retry": [ 24 | { 25 | "ErrorEquals": [ 26 | "Lambda.ServiceException", 27 | "Lambda.AWSLambdaException", 28 | "Lambda.SdkClientException" 29 | ], 30 | "IntervalSeconds": 2, 31 | "MaxAttempts": 6, 32 | "BackoffRate": 2 33 | } 34 | ], 35 | "Next": "verification", 36 | "Catch": [ 37 | { 38 | "ErrorEquals": [ 39 | "States.ALL" 40 | ], 41 | "Next": "fail" 42 | } 43 | ] 44 | }, 45 | 46 | "verification" : { 47 | "Type": "Choice", 48 | "Choices": [ 49 | { 50 | "Variable": "$.result.request.status", 51 | "StringEquals": "verified", 52 | "Next": "verified" 53 | }, 54 | { 55 | "Variable": "$.result.request.status", 56 | "StringEquals": "unverified", 57 | "Next": "unverified" 58 | } 59 | ], 60 | "Default": "fail" 61 | }, 62 | 63 | "verified": { 64 | "Type": "Pass", 65 | "Next": "select_stage" 66 | }, 67 | 68 | "unverified": { 69 | "Type": "Pass", 70 | "Next": "runtask_callback" 71 | }, 72 | 73 | "select_stage": { 74 | "Type" : "Choice", 75 | "Choices": [ 76 | { 77 | "Variable": "$.detail.stage", 78 | "StringEquals": "post_plan", 79 | "Next": "post_plan" 80 | }, 81 | { 82 | "Variable": "$.detail.stage", 83 | "StringEquals": "pre_plan", 84 | "Next": "pre_plan" 85 | }, 86 | { 87 | "Variable": "$.detail.stage", 88 | "StringEquals": "pre_apply", 89 | "Next": "pre_apply" 90 | } 91 | ], 92 | "Default": "post_plan" 93 | }, 94 | 95 | "post_plan": { 96 | "Type": "Pass", 97 | "ResultPath": "$.result.stage", 98 | "Result": { 99 | "status": "implemented" 100 | }, 101 | "Next": "runtask_fulfillment" 102 | }, 103 | 104 | "pre_plan": { 105 | "Type": "Pass", 106 | "ResultPath": "$.result.stage", 107 | "Result": { 108 | "status": "not implemented" 109 | }, 110 | "Next": "not_implemented" 111 | }, 112 | 113 | "pre_apply": { 114 | "Type": "Pass", 115 | "ResultPath": "$.result.stage", 116 | "Result": { 117 | "status": "not implemented" 118 | }, 119 | "Next": "not_implemented" 120 | }, 121 | 122 | "not_implemented": { 123 | "Type": "Pass", 124 | "Next": "runtask_callback" 125 | }, 126 | 127 | "runtask_fulfillment": { 128 | "Type": "Task", 129 | "Resource": "arn:aws:states:::lambda:invoke", 130 | "Parameters": { 131 | "FunctionName": "${resource_runtask_fulfillment}:$LATEST", 132 | "Payload": { 133 | "job_name.$": "$$.Execution.Name", 134 | "payload.$": "$", 135 | "action": "fulfillment" 136 | } 137 | }, 138 | "InputPath" : "$", 139 | "ResultPath": "$.result.fulfillment", 140 | "OutputPath": "$", 141 | "ResultSelector": { 142 | "status.$": "$.Payload.status", 143 | "message.$": "$.Payload.message", 144 | "url.$": "$.Payload.url" 145 | }, 146 | "Retry": [ 147 | { 148 | "ErrorEquals": [ 149 | "Lambda.ServiceException", 150 | "Lambda.AWSLambdaException", 151 | "Lambda.SdkClientException" 152 | ], 153 | "IntervalSeconds": 2, 154 | "MaxAttempts": 6, 155 | "BackoffRate": 2 156 | } 157 | ], 158 | "Next": "runtask_callback", 159 | "Catch": [ 160 | { 161 | "ErrorEquals": [ 162 | "States.ALL" 163 | ], 164 | "Next": "fail" 165 | } 166 | ] 167 | }, 168 | 169 | "runtask_callback": { 170 | "Type": "Task", 171 | "Resource": "arn:aws:states:::lambda:invoke", 172 | "Parameters": { 173 | "FunctionName": "${resource_runtask_callback}:$LATEST", 174 | "Payload": { 175 | "job_name.$": "$$.Execution.Name", 176 | "payload.$": "$", 177 | "action": "callback" 178 | } 179 | }, 180 | "InputPath" : "$", 181 | "ResultPath": "$.result.callback", 182 | "OutputPath": "$", 183 | "ResultSelector": { 184 | "status.$": "$.Payload" 185 | }, 186 | "Retry": [ 187 | { 188 | "ErrorEquals": [ 189 | "Lambda.ServiceException", 190 | "Lambda.AWSLambdaException", 191 | "Lambda.SdkClientException" 192 | ], 193 | "IntervalSeconds": 2, 194 | "MaxAttempts": 6, 195 | "BackoffRate": 2 196 | } 197 | ], 198 | "Next": "success", 199 | "Catch": [ 200 | { 201 | "ErrorEquals": [ 202 | "States.ALL" 203 | ], 204 | "Next": "fail" 205 | } 206 | ] 207 | }, 208 | 209 | "fail": { 210 | "Type": "Fail" 211 | }, 212 | 213 | "success": { 214 | "Type": "Succeed" 215 | } 216 | } 217 | } -------------------------------------------------------------------------------- /tests/01_mandatory.tftest.hcl: -------------------------------------------------------------------------------- 1 | ## NOTE: This is the minimum mandatory test 2 | # run at least one test using the ./examples directory as your module source 3 | # create additional *.tftest.hcl for your own unit / integration tests 4 | # use tests/*.auto.tfvars to add non-default variables 5 | 6 | run "mandatory_plan_basic" { 7 | command = plan 8 | module { 9 | source = "./examples/module_workspace" 10 | } 11 | } 12 | 13 | run "mandatory_apply_basic" { 14 | command = apply 15 | module { 16 | source = "./examples/module_workspace" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "name_prefix" { 2 | description = "Name to be used on all the resources as identifier." 3 | type = string 4 | default = "aws-ia2" 5 | } 6 | 7 | variable "event_bus_name" { 8 | description = "EventBridge event bus name" 9 | type = string 10 | default = "default" 11 | } 12 | 13 | variable "cloudwatch_log_group_name" { 14 | description = "RunTask CloudWatch log group name" 15 | type = string 16 | default = "/hashicorp/terraform/runtask/iam-access-analyzer/" 17 | } 18 | 19 | variable "cloudwatch_log_group_retention" { 20 | description = "Lambda CloudWatch log group retention period" 21 | type = string 22 | default = "365" 23 | validation { 24 | condition = contains(["1", "3", "5", "7", "14", "30", "60", "90", "120", "150", "180", "365", "400", "545", "731", "1827", "3653", "0"], var.cloudwatch_log_group_retention) 25 | error_message = "Valid values for var: cloudwatch_log_group_retention are (1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653, and 0)." 26 | } 27 | } 28 | 29 | variable "event_source" { 30 | description = "EventBridge source name" 31 | type = string 32 | default = "app.terraform.io" 33 | } 34 | 35 | variable "runtask_stages" { 36 | description = "List of all supported RunTask stages" 37 | type = list(string) 38 | default = ["pre_plan", "post_plan", "pre_apply"] 39 | } 40 | 41 | variable "tfc_org" { 42 | description = "Terraform Organization name" 43 | type = string 44 | } 45 | 46 | variable "workspace_prefix" { 47 | description = "TFC workspace name prefix that allowed to run this runtask" 48 | type = string 49 | default = "" 50 | } 51 | 52 | variable "supported_policy_document" { 53 | description = "(Optional) allow list of the supported IAM policy document" 54 | type = string 55 | default = "" 56 | } 57 | 58 | variable "aws_region" { 59 | description = "The region from which this module will be executed." 60 | type = string 61 | validation { 62 | condition = can(regex("(us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\\d", var.aws_region)) 63 | error_message = "Variable var: region is not valid." 64 | } 65 | } 66 | 67 | variable "recovery_window" { 68 | description = "Numbers of day Number of days that AWS Secrets Manager waits before it can delete the secret" 69 | type = number 70 | default = 0 71 | validation { 72 | condition = (var.recovery_window >= 0 && var.recovery_window <= 30) 73 | error_message = "Variable var: recovery_window must be between 0 and 30" 74 | } 75 | } 76 | 77 | variable "lambda_reserved_concurrency" { 78 | description = "Maximum Lambda reserved concurrency, make sure your AWS quota is sufficient" 79 | type = number 80 | default = 100 81 | } 82 | 83 | variable "lambda_default_timeout" { 84 | description = "Lambda default timeout in seconds" 85 | type = number 86 | default = 30 87 | } 88 | 89 | variable "deploy_waf" { 90 | description = "Set to true to deploy CloudFront and WAF in front of the Lambda function URL" 91 | type = string 92 | default = false 93 | validation { 94 | condition = contains(["true", "false"], var.deploy_waf) 95 | error_message = "Valid values for var: deploy_waf are true, false" 96 | } 97 | } 98 | 99 | variable "waf_rate_limit" { 100 | description = "Rate limit for request coming to WAF" 101 | type = number 102 | default = 100 103 | } 104 | 105 | variable "waf_managed_rule_set" { 106 | description = "List of AWS Managed rules to use inside the WAF ACL" 107 | type = list(map(string)) 108 | default = [ 109 | { 110 | name = "AWSManagedRulesCommonRuleSet" 111 | priority = 10 112 | vendor_name = "AWS" 113 | metric_suffix = "common" 114 | }, 115 | { 116 | name = "AWSManagedRulesKnownBadInputsRuleSet" 117 | priority = 20 118 | vendor_name = "AWS" 119 | metric_suffix = "bad_input" 120 | } 121 | ] 122 | } 123 | 124 | variable "tags" { 125 | description = "Map of tags to apply to resources deployed by this solution." 126 | type = map(any) 127 | default = null 128 | } 129 | 130 | variable "lambda_architecture" { 131 | description = "Lambda architecture (arm64 or x86_64)" 132 | type = string 133 | default = "x86_64" 134 | validation { 135 | condition = contains(["arm64", "x86_64"], var.lambda_architecture) 136 | error_message = "Valid values for var: lambda_architecture are arm64 or x86_64" 137 | } 138 | } -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.0.7" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = ">=5.72.0" 7 | } 8 | 9 | tfe = { 10 | source = "hashicorp/tfe" 11 | version = ">=0.38.0" 12 | } 13 | 14 | random = { 15 | source = "hashicorp/random" 16 | version = ">=3.4.0" 17 | } 18 | 19 | archive = { 20 | source = "hashicorp/archive" 21 | version = "~>2.2.0" 22 | } 23 | 24 | time = { 25 | source = "hashicorp/time" 26 | version = ">=0.12.0" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /waf.tf: -------------------------------------------------------------------------------- 1 | resource "aws_wafv2_web_acl" "runtask_waf" { 2 | count = local.waf_deployment 3 | provider = aws.cloudfront_waf 4 | 5 | name = "${var.name_prefix}-runtask_waf_acl" 6 | description = "Run Task WAF with simple rate base rules" 7 | scope = "CLOUDFRONT" 8 | 9 | default_action { 10 | allow {} 11 | } 12 | 13 | rule { 14 | name = "rate-base-limit" 15 | priority = 1 16 | 17 | action { 18 | block {} 19 | } 20 | 21 | statement { 22 | rate_based_statement { 23 | limit = local.waf_rate_limit 24 | aggregate_key_type = "IP" 25 | } 26 | } 27 | 28 | visibility_config { 29 | cloudwatch_metrics_enabled = true 30 | metric_name = "${var.name_prefix}-runtask_request_rate" 31 | sampled_requests_enabled = true 32 | } 33 | } 34 | 35 | dynamic "rule" { 36 | for_each = var.waf_managed_rule_set 37 | content { 38 | name = rule.value.name 39 | priority = rule.value.priority 40 | 41 | override_action { 42 | none {} 43 | } 44 | 45 | statement { 46 | managed_rule_group_statement { 47 | name = rule.value.name 48 | vendor_name = rule.value.vendor_name 49 | } 50 | } 51 | 52 | visibility_config { 53 | cloudwatch_metrics_enabled = true 54 | metric_name = "${var.name_prefix}-runtask_request_${rule.value.metric_suffix}" 55 | sampled_requests_enabled = true 56 | } 57 | } 58 | } 59 | 60 | visibility_config { 61 | cloudwatch_metrics_enabled = true 62 | metric_name = "${var.name_prefix}-runtask_waf_acl" 63 | sampled_requests_enabled = true 64 | } 65 | tags = local.combined_tags 66 | } 67 | 68 | resource "aws_cloudwatch_log_group" "runtask_waf" { 69 | count = local.waf_deployment 70 | provider = aws.cloudfront_waf 71 | name = "aws-waf-logs-${var.name_prefix}-runtask_waf_acl" 72 | retention_in_days = var.cloudwatch_log_group_retention 73 | kms_key_id = aws_kms_key.runtask_waf[count.index].arn 74 | tags = local.combined_tags 75 | } 76 | 77 | resource "aws_cloudwatch_log_resource_policy" "runtask_waf" { 78 | count = local.waf_deployment 79 | provider = aws.cloudfront_waf 80 | policy_document = data.aws_iam_policy_document.runtask_waf_log[count.index].json 81 | policy_name = "aws-waf-logs-${var.name_prefix}-runtask_waf_acl" 82 | } 83 | 84 | resource "aws_wafv2_web_acl_logging_configuration" "runtask_waf" { 85 | count = local.waf_deployment 86 | provider = aws.cloudfront_waf 87 | log_destination_configs = [aws_cloudwatch_log_group.runtask_waf[count.index].arn] 88 | resource_arn = aws_wafv2_web_acl.runtask_waf[count.index].arn 89 | redacted_fields { 90 | single_header { 91 | name = "x-tfc-task-signature" 92 | } 93 | } 94 | } 95 | --------------------------------------------------------------------------------