├── .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 | 
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 | 
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. 
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. 
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. 
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. 
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. 
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. 
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 |
--------------------------------------------------------------------------------