From e8a4816107c76cde994c7ed39cb37c73398ce879 Mon Sep 17 00:00:00 2001 From: John Ricords <122303445+johnricords@users.noreply.github.com> Date: Wed, 20 Mar 2024 10:59:57 -0400 Subject: [PATCH 1/4] repo setup --- README.md | 67 +++- email_templates/admin_email.html | 53 +++ email_templates/admin_email.txt | 26 ++ email_templates/user_email.html | 16 + email_templates/user_email.txt | 13 + main.tf | 191 +++++++++ modules/scheduled_event/README.md | 38 ++ modules/scheduled_event/main.tf | 27 ++ modules/scheduled_event/variables.tf | 54 +++ modules/scheduled_event/versions.tf | 10 + output.tf | 9 + src/python/iam_key_enforcer.py | 570 +++++++++++++++++++++++++++ src/python/requirements.txt | 1 + tests/.gitkeep | 0 tests/create_all/main.tf | 133 +++++++ tests/create_all/prereq/main.tf | 10 + tests/create_all/variables.tf | 76 ++++ variables.tf | 162 ++++++++ versions.tf | 11 + 19 files changed, 1465 insertions(+), 2 deletions(-) create mode 100644 email_templates/admin_email.html create mode 100644 email_templates/admin_email.txt create mode 100644 email_templates/user_email.html create mode 100644 email_templates/user_email.txt create mode 100644 main.tf create mode 100644 modules/scheduled_event/README.md create mode 100644 modules/scheduled_event/main.tf create mode 100644 modules/scheduled_event/variables.tf create mode 100644 modules/scheduled_event/versions.tf create mode 100644 output.tf create mode 100644 src/python/iam_key_enforcer.py create mode 100644 src/python/requirements.txt delete mode 100644 tests/.gitkeep create mode 100644 tests/create_all/main.tf create mode 100644 tests/create_all/prereq/main.tf create mode 100644 tests/create_all/variables.tf create mode 100644 variables.tf create mode 100644 versions.tf diff --git a/README.md b/README.md index e6c354a..c17f7ab 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,66 @@ -# terraform-aws-tardigrade-iam-key-enforcer -Module to manage IAM key enforcer +# Tardigrade IAM Key Enforcer +This repo contains the Python-based Lambda function that will audit IAM Access keys for an account and will enforce key rotation as well as notify users. + +## Basic Function + +The Lambda function is triggered for each account by an Event notification that is configured to run on a schedule. +The function audits each user in an account for access keys and determines how long before they expire, it will then notify users that their key expires in X days and that automatic key enforcement is forthcoming. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.1 | +| [aws](#requirement\_aws) | >= 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 3.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [admin\_email](#input\_admin\_email) | Admin Email that will receive all emails and reports about actions taken if email is enabled | `string` | n/a | yes | +| [assume\_role\_name](#input\_assume\_role\_name) | Name of the IAM role that the lambda will assume in the target account | `string` | n/a | yes | +| [email\_source](#input\_email\_source) | Email that will be used to send messages | `string` | n/a | yes | +| [key\_age\_delete](#input\_key\_age\_delete) | Age at which a key should be deleted (e.g. 120) | `number` | n/a | yes | +| [key\_age\_inactive](#input\_key\_age\_inactive) | Age at which a key should be inactive (e.g. 90) | `number` | n/a | yes | +| [key\_age\_warning](#input\_key\_age\_warning) | Age at which to warn (e.g. 75) | `number` | n/a | yes | +| [key\_use\_threshold](#input\_key\_use\_threshold) | Age at which unused keys should be deleted (e.g.30) | `number` | n/a | yes | +| [accounts](#input\_accounts) | List of account objects to create events for |
list(object({
account_name = string
account_number = string
role_name = optional(string) # deprecated
armed = bool
debug = optional(bool, false)
email_user_enabled = bool
email_targets = list(string)
exempt_groups = list(string)
schedule_expression = optional(string, "cron(0 1 ? * SUN *)")

}))
| `[]` | no | +| [email\_admin\_report\_enabled](#input\_email\_admin\_report\_enabled) | Used to enable or disable the SES emailed report | `bool` | `false` | no | +| [email\_admin\_report\_subject](#input\_email\_admin\_report\_subject) | Subject of the report email that is sent | `string` | `null` | no | +| [email\_banner\_message](#input\_email\_banner\_message) | Messages that will be at the top of all emails sent to notify recipients of important information | `string` | `""` | no | +| [email\_banner\_message\_color](#input\_email\_banner\_message\_color) | Color of email banner message, must be valid html color | `string` | `"red"` | no | +| [email\_tag](#input\_email\_tag) | Tag to be placed on the IAM user that we can use to notify when their key is going to be disabled/deleted | `string` | `"keyenforcer:email"` | no | +| [email\_templates](#input\_email\_templates) | Email templates to use for Admin and User emails |
object({
admin = optional(object({
subject = optional(string, null),
html = optional(string, null),
text = optional(string, null),
}), {}),
user = optional(object({
subject = optional(string, null),
html = optional(string, null),
text = optional(string, null),
}), {})
})
| `{}` | no | +| [lambda](#input\_lambda) | Map of any additional arguments for the upstream lambda module. See |
object({
artifacts_dir = optional(string, "builds")
build_in_docker = optional(bool, false)
create_package = optional(bool, true)
ephemeral_storage_size = optional(number)
ignore_source_code_hash = optional(bool, true)
local_existing_package = optional(string)
recreate_missing_package = optional(bool, false)
runtime = optional(string, "python3.11")
s3_bucket = optional(string)
s3_existing_package = optional(map(string))
s3_prefix = optional(string)
store_on_s3 = optional(bool, false)
timeout = optional(number, 300)
source_path = optional(object({
patterns = optional(list(string), ["!\\.terragrunt-source-manifest"])
}), {})
})
| `{}` | no | +| [log\_level](#input\_log\_level) | Log level for lambda | `string` | `"INFO"` | no | +| [project\_name](#input\_project\_name) | Project name to prefix resources with | `string` | `"iam-key-enforcer"` | no | +| [s3\_bucket](#input\_s3\_bucket) | Bucket name to write the audit report to if s3\_enabled is set to 'true' | `string` | `null` | no | +| [s3\_enabled](#input\_s3\_enabled) | Set to 'true' and provide s3\_bucket if the audit report should be written to S3 | `bool` | `false` | no | +| [schedule\_expression](#input\_schedule\_expression) | (DEPRECATED) Schedule Expressions for Rules | `string` | `null` | no | +| [tags](#input\_tags) | Tags for resource | `map(string)` | `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [lambda](#output\_lambda) | The lambda module object | +| [queue](#output\_queue) | The SQS Queue resource object | + + diff --git a/email_templates/admin_email.html b/email_templates/admin_email.html new file mode 100644 index 0000000..10ab80d --- /dev/null +++ b/email_templates/admin_email.html @@ -0,0 +1,53 @@ +{{#if email_banner_msg}} +

{{email_banner_msg}}

+{{/if}} + +

Expiring Access Key Report for {{account_number}} - {{account_name}}

+ +{{#if unarmed}} +

+ The IAM Key Enforcer is not active and NO action has been taken on your key +

+

+ The information below is for informational purposes and represents the results if the IAM Key Enforcer were active. +

+{{/if}} + +

+ Access Keys over {{key_age_inactive}} days old have been DEACTIVATED, keys older than {{key_age_delete}} days have + been DELETED. + Access keys over {{key_age_warning}} days old are DEACTIVATED at {{key_age_inactive}} days old and DELETED after + {{key_age_delete}} days old. + Rotate any keys as necessary to prevent disruption to your applications. +

+ +{{#if exempt_groups}} +

+ Grayed out rows are exempt via membership in an exempt IAM Group(s):{{exempt_groups}}. +
+ Exempted group members also have a key status value of <STATUS> (Exempt). +

+{{/if}} + + + + + + + + + + + + + {{#each key_report_contents}} + + + + + + + + {{/each}} + +
IAM User NameAccess Key IDKey AgeKey StatusLast Used
{{user_name}}{{access_key_id}}{{key_age}}{{key_status}}{{last_used_date}}
diff --git a/email_templates/admin_email.txt b/email_templates/admin_email.txt new file mode 100644 index 0000000..aa53525 --- /dev/null +++ b/email_templates/admin_email.txt @@ -0,0 +1,26 @@ +{{#if email_banner_msg}} +{{email_banner_msg}} +{{/if}} + +

Expiring Access Key Report for {{account_number}} - {{account_name}}

+ +{{#if unarmed}} + The IAM Key Enforcer is not active and NO action has been taken on your key + + The information below is for informational purposes and represents the results if the IAM Key Enforcer were active. +{{/if}} + +Access Keys over {{key_age_inactive}} days old have been DEACTIVATED, keys older than {{key_age_delete}} days have been DELETED. +Access keys over {{key_age_warning}} days old are DEACTIVATED at {{key_age_inactive}} days old and DELETED after {{key_age_delete}} days old. +Rotate any keys as necessary to prevent disruption to your applications. + + +{{#if exempt_groups}} + Exempted IAM Group(s):{{exempt_groups}}. + Exempted group members have a key status value of (Exempt). +{{/if}} + +IAM User Name, Access Key ID, Key Age, Key Status, Last Used +{{#each key_report_contents}} + {{user_name}}, {{access_key_id}}, {{key_age}}, {{key_status}}, {{last_used_date}} +{{/each}} diff --git a/email_templates/user_email.html b/email_templates/user_email.html new file mode 100644 index 0000000..5df2a40 --- /dev/null +++ b/email_templates/user_email.html @@ -0,0 +1,16 @@ +{{#if email_banner_msg}} +

{{email_banner_msg}}

+{{/if}} + +

Expiring Access Key Report for {{user_name}}

+ +{{#if unarmed}} +

+ The IAM Key Enforcer is not active and NO action has been taken on your key +

+

+ The information below is for informational purposes and represents the results if the IAM Key Enforcer were active. +

+{{/if}} + +

The access key {{access_key_id}} is over {{key_age}} days old and has been {{action}}.

diff --git a/email_templates/user_email.txt b/email_templates/user_email.txt new file mode 100644 index 0000000..2a78895 --- /dev/null +++ b/email_templates/user_email.txt @@ -0,0 +1,13 @@ +{{#if email_banner_msg}} +{{email_banner_msg}} +{{/if}} + +Expiring Access Key Report for {{user_name}} + +{{#if unarmed}} + The IAM Key Enforcer is not active and NO action has been taken on your key + + The information below is for informational purposes and represents the results if the IAM Key Enforcer were active. +{{/if}} + +The access key {{access_key_id}} is over {{key_age}} days old and has been {{action}}. diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..76cba98 --- /dev/null +++ b/main.tf @@ -0,0 +1,191 @@ +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} +data "aws_region" "current" {} + +data "aws_iam_policy_document" "lambda" { + statement { + sid = "AllowS3Object" + actions = [ + "s3:PutObject", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", + ] + resources = ["arn:${data.aws_partition.current.partition}:s3:::${var.s3_bucket}/*"] + } + + statement { + actions = [ + "ses:SendEmail", + "ses:SendTemplatedEmail", + "ses:TestRenderTemplate" + ] + resources = [ + "*" + ] + } + + statement { + sid = "AllowAssumeRole" + actions = [ + "sts:AssumeRole" + ] + resources = [ + "arn:${data.aws_partition.current.partition}:iam::*:role/${var.assume_role_name}" + ] + } +} + +module "lambda" { + source = "git::https://github.com/terraform-aws-modules/terraform-aws-lambda.git?ref=v7.2.1" + + description = "Lambda function for Key Enforcement" + function_name = var.project_name + handler = "iam_key_enforcer.lambda_handler" + tags = var.tags + + attach_policy_json = true + policy_json = data.aws_iam_policy_document.lambda.json + + artifacts_dir = var.lambda.artifacts_dir + build_in_docker = var.lambda.build_in_docker + create_package = var.lambda.create_package + ignore_source_code_hash = var.lambda.ignore_source_code_hash + local_existing_package = var.lambda.local_existing_package + recreate_missing_package = var.lambda.recreate_missing_package + ephemeral_storage_size = var.lambda.ephemeral_storage_size + runtime = var.lambda.runtime + s3_bucket = var.lambda.s3_bucket + s3_existing_package = var.lambda.s3_existing_package + s3_prefix = var.lambda.s3_prefix + store_on_s3 = var.lambda.store_on_s3 + timeout = var.lambda.timeout + + environment_variables = { + LOG_LEVEL = var.log_level + EMAIL_ADMIN_REPORT_ENABLED = var.email_admin_report_enabled + EMAIL_ADMIN_REPORT_SUBJECT = var.email_admin_report_subject + EMAIL_SOURCE = var.email_source + ADMIN_EMAIL = var.admin_email + KEY_AGE_WARNING = var.key_age_warning + KEY_AGE_INACTIVE = var.key_age_inactive + KEY_AGE_DELETE = var.key_age_delete + KEY_USE_THRESHOLD = var.key_use_threshold + S3_ENABLED = var.s3_enabled + S3_BUCKET = var.s3_bucket + EMAIL_TAG = var.email_tag + EMAIL_BANNER_MSG = var.email_banner_message + EMAIL_BANNER_MSG_COLOR = var.email_banner_message_color + EMAIL_USER_TEMPLATE = aws_ses_template.user_template.id + EMAIL_ADMIN_TEMPLATE = aws_ses_template.admin_template.id + } + + source_path = [ + { + path = "${path.module}/src/python", + prefix_in_zip = "" + pip_requirements = true + patterns = var.lambda.source_path.patterns + }, + ] +} + +resource "aws_lambda_permission" "this" { + action = "lambda:InvokeFunction" + function_name = module.lambda.lambda_function_name + principal = "events.amazonaws.com" + source_arn = "arn:${data.aws_partition.current.partition}:events:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:rule/${var.project_name}-*" +} + +############################## +# SQS Queue Policy +############################## +resource "aws_sqs_queue_policy" "this" { + queue_url = aws_sqs_queue.this.id + policy = jsonencode( + { + Version = "2012-10-17", + Id = "sqspolicy", + Statement : [ + { + Sid = "AllowSend", + Effect = "Allow", + Principal = "*", + Action = "sqs:SendMessage", + Resource = aws_sqs_queue.this.arn, + Condition = { + "ArnLike" : { + "aws:SourceArn" : "arn:${data.aws_partition.current.partition}:events:*:*:rule/${var.project_name}-*" + } + } + }, + { + Sid = "AllowRead", + Effect = "Allow", + "Principal" : { + "AWS" : "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:root" + }, + Action = "sqs:ReceiveMessage", + Resource = aws_sqs_queue.this.arn, + } + ] + } + ) +} + +############################## +# SQS Queue +############################## +resource "aws_sqs_queue" "this" { + name = "${var.project_name}-dlq" + message_retention_seconds = 1209600 + receive_wait_time_seconds = 20 + visibility_timeout_seconds = 30 + tags = var.tags +} + +############################## +# Schedule Event +############################## +module "scheduled_events" { + source = "./modules/scheduled_event" + for_each = { for account in var.accounts : account.account_name => account } + + event_name = each.value.account_name + event_rule_description = "Scheduled Event that runs IAM Key Enforcer Lambda for account ${each.value.account_number} - ${each.value.account_name}" + lambda_arn = module.lambda.lambda_function_arn + project_name = var.project_name + tags = var.tags + schedule_expression = var.schedule_expression != null ? var.schedule_expression : each.value.schedule_expression + + + dead_letter_config = { + arn = aws_sqs_queue.this.arn + } + + input_transformer = { + input_template = jsonencode({ + "account_number" : each.value.account_number, + "account_name" : each.value.account_name, + "role_arn" : "arn:${data.aws_partition.current.partition}:iam::${each.value.account_number}:role/${var.assume_role_name}", + "armed" : each.value.armed, + "debug" : each.value.debug, + "email_targets" : each.value.email_targets, + "exempt_groups" : each.value.exempt_groups, + "email_user_enabled" : each.value.email_user_enabled, + }) + } +} + +resource "aws_ses_template" "user_template" { + name = "${var.project_name}-user" + html = var.email_templates.user.html != null ? var.email_templates.user.html : file("${path.module}/email_templates/user_email.html") + subject = var.email_templates.user.subject != null ? var.email_templates.user.subject : "IAM User Key {{armed_state_msg}} for {{user_name}}" + text = var.email_templates.user.text != null ? var.email_templates.user.text : file("${path.module}/email_templates/user_email.txt") +} + +resource "aws_ses_template" "admin_template" { + name = "${var.project_name}-admin" + html = var.email_templates.admin.html != null ? var.email_templates.admin.html : file("${path.module}/email_templates/admin_email.html") + subject = var.email_templates.admin.subject != null ? var.email_templates.admin.subject : "IAM Key Enforcement Report for {{account_number}}" + text = var.email_templates.admin.text != null ? var.email_templates.admin.text : file("${path.module}/email_templates/admin_email.txt") +} diff --git a/modules/scheduled_event/README.md b/modules/scheduled_event/README.md new file mode 100644 index 0000000..5c4dc1f --- /dev/null +++ b/modules/scheduled_event/README.md @@ -0,0 +1,38 @@ + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.1 | +| [aws](#requirement\_aws) | >= 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 3.0 | + +## Resources + +| Name | Type | +|------|------| + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [event\_name](#input\_event\_name) | Name of the event | `string` | n/a | yes | +| [event\_rule\_description](#input\_event\_rule\_description) | Description of what the event rule does | `string` | n/a | yes | +| [lambda\_arn](#input\_lambda\_arn) | ARN of the target lambda | `string` | n/a | yes | +| [dead\_letter\_config](#input\_dead\_letter\_config) | Configuration of the dead letter queue |
object({
arn = string
})
| `null` | no | +| [event\_bus\_name](#input\_event\_bus\_name) | EventBridge event bus | `string` | `"default"` | no | +| [input\_transformer](#input\_input\_transformer) | Transform to apply on the event input |
object({
input_template = string
})
| `null` | no | +| [project\_name](#input\_project\_name) | Project name to prefix resources with | `string` | `"iam-key-enforcer"` | no | +| [schedule\_expression](#input\_schedule\_expression) | Schedule Expression for scheduled event | `string` | `"cron(0 0 * * 1 *)"` | no | +| [tags](#input\_tags) | A map of tags to add to the module resources | `map(string)` | `{}` | no | + +## Outputs + +No outputs. + + diff --git a/modules/scheduled_event/main.tf b/modules/scheduled_event/main.tf new file mode 100644 index 0000000..7c57723 --- /dev/null +++ b/modules/scheduled_event/main.tf @@ -0,0 +1,27 @@ +resource "aws_cloudwatch_event_rule" "this" { + name = "${var.project_name}-${var.event_name}" + description = var.event_rule_description + tags = var.tags + event_bus_name = var.event_bus_name + schedule_expression = var.schedule_expression +} + +resource "aws_cloudwatch_event_target" "this" { + event_bus_name = var.event_bus_name + arn = var.lambda_arn + rule = aws_cloudwatch_event_rule.this.name + + dynamic "input_transformer" { + for_each = var.input_transformer != null ? [var.input_transformer] : [] + content { + input_template = input_transformer.value.input_template + } + } + + dynamic "dead_letter_config" { + for_each = var.dead_letter_config != null ? [var.dead_letter_config] : [] + content { + arn = dead_letter_config.value.arn + } + } +} diff --git a/modules/scheduled_event/variables.tf b/modules/scheduled_event/variables.tf new file mode 100644 index 0000000..eef225d --- /dev/null +++ b/modules/scheduled_event/variables.tf @@ -0,0 +1,54 @@ +variable "event_bus_name" { + description = "EventBridge event bus" + type = string + default = "default" +} + +variable "event_rule_description" { + description = "Description of what the event rule does" + type = string +} + +variable "event_name" { + description = "Name of the event" + type = string +} + +variable "lambda_arn" { + description = "ARN of the target lambda" + type = string +} + +variable "project_name" { + description = "Project name to prefix resources with" + type = string + default = "iam-key-enforcer" +} + +variable "input_transformer" { + description = "Transform to apply on the event input" + type = object({ + input_template = string + }) + default = null +} + +variable "dead_letter_config" { + description = "Configuration of the dead letter queue" + type = object({ + arn = string + }) + default = null +} + +variable "schedule_expression" { + description = "Schedule Expression for scheduled event" + type = string + default = "cron(0 0 * * 1 *)" +} + +variable "tags" { + description = "A map of tags to add to the module resources" + type = map(string) + default = {} +} diff --git a/modules/scheduled_event/versions.tf b/modules/scheduled_event/versions.tf new file mode 100644 index 0000000..ee77236 --- /dev/null +++ b/modules/scheduled_event/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.1" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.0" + } + } +} diff --git a/output.tf b/output.tf new file mode 100644 index 0000000..bfea2f4 --- /dev/null +++ b/output.tf @@ -0,0 +1,9 @@ +output "lambda" { + description = "The lambda module object" + value = module.lambda +} + +output "queue" { + description = "The SQS Queue resource object" + value = aws_sqs_queue.this +} diff --git a/src/python/iam_key_enforcer.py b/src/python/iam_key_enforcer.py new file mode 100644 index 0000000..d5716b3 --- /dev/null +++ b/src/python/iam_key_enforcer.py @@ -0,0 +1,570 @@ +"""Audit Access Key Age. + +Purpose: + Reads the credential report: + - Determines the age of each access key + - Builds a report of all keys older than KEY_AGE_WARNING + - Takes action (inactive/delete) on non-compliant Access Keys +Permissions: + iam:GetCredentialReport + iam:GetAccessKeyLastUsed + iam:ListAccessKeys + iam:ListGroupsForUser + s3:putObject + ses:SendEmail + ses:SendRawEmail +Environment Variables: + LOG_LEVEL: (optional): sets the level for function logging + valid input: critical, error, warning, info (default), debug + EMAIL_ADMIN_REPORT_ENABLED: used to enable or disable the SES emailed report + EMAIL_SOURCE: send from address for the email, authorized in SES + EMAIL_USER_TEMPLATE: Name of the SES template for user emails + EMAIL_ADMIN_TEMPLATE: Name of the SES template for admin emails + KEY_AGE_DELETE: age at which a key should be deleted (e.g. 120) + KEY_AGE_INACTIVE: age at which a key should be inactive (e.g. 90) + KEY_AGE_WARNING: age at which to warn (e.g. 75) + KEY_USE_THRESHOLD: age at which unused keys should be deleted (e.g.30) + S3_ENABLED: set to "true" and provide S3_BUCKET if the audit report + should be written to S3 + S3_BUCKET: bucket name to write the audit report to if S3_ENABLED is + set to "true" +Event Variables: + armed: Set to "true" to take action on keys; + "false" limits to reporting + role_arn: Arn of role to assume + account_name: AWS Account (friendly) Name + account_number: AWS Account Number + email_user_enabled: used to enable or disable the SES emailed report + email_targets: default email address if event fails to pass a valid one + exempt_groups: IAM Groups that are exempt from actions on access keys + +""" +import collections +import csv +import io +import json +import logging +import os +import re +from time import sleep +import datetime +import dateutil + +import boto3 +from aws_assume_role_lib import assume_role, generate_lambda_session_name + +# Standard logging config +DEFAULT_LOG_LEVEL = logging.INFO +LOG_LEVELS = collections.defaultdict( + lambda: DEFAULT_LOG_LEVEL, + { + "CRITICAL": logging.CRITICAL, + "ERROR": logging.ERROR, + "WARNING": logging.WARNING, + "INFO": logging.INFO, + "DEBUG": logging.DEBUG, + }, +) + +# Lambda initializes a root logger that needs to be removed in order to set a +# different logging config +root = logging.getLogger() +if root.handlers: + for handler in root.handlers: + root.removeHandler(handler) + +logging.basicConfig( + format="%(asctime)s.%(msecs)03dZ [%(name)s][%(levelname)-5s]: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", + level=LOG_LEVELS[os.environ.get("LOG_LEVEL", "").lower()], +) + +log = logging.getLogger(__name__) + +ADMIN_EMAIL = os.environ.get("ADMIN_EMAIL") +EMAIL_ADMIN_REPORT_ENABLED = ( + os.environ.get("EMAIL_ADMIN_REPORT_ENABLED", "False").lower() == "true" +) + +EMAIL_SOURCE = os.environ.get("EMAIL_SOURCE") +KEY_AGE_WARNING = int(os.environ.get("KEY_AGE_WARNING", 75)) +KEY_AGE_INACTIVE = int(os.environ.get("KEY_AGE_INACTIVE", 90)) +KEY_AGE_DELETE = int(os.environ.get("KEY_AGE_DELETE", 120)) +KEY_USE_THRESHOLD = int(os.environ.get("KEY_USE_THRESHOLD", 30)) +S3_ENABLED = os.environ.get("S3_ENABLED", "False").lower() == "true" +S3_BUCKET = os.environ.get("S3_BUCKET", None) +EMAIL_TAG = os.environ.get("EMAIL_TAG", "keyenforcer:email").lower() +EMAIL_BANNER_MSG = os.environ.get("EMAIL_BANNER_MSG", "").strip() +EMAIL_BANNER_MSG_COLOR = os.environ.get("EMAIL_BANNER_MSG_COLOR", "black").strip() +EMAIL_USER_TEMPLATE = os.environ.get("EMAIL_USER_TEMPLATE") +EMAIL_ADMIN_TEMPLATE = os.environ.get("EMAIL_ADMIN_TEMPLATE") +NOT_ARMED_PREFIX = "NOT ARMED:" +ARMED_PREFIX = "ARMED:" + +# Get the Lambda session and clients +SESSION = boto3.Session() +CLIENT_SES = SESSION.client("ses") +CLIENT_S3 = SESSION.client("s3") +email_regex = re.compile( + r"([A-Za-z0-9]+[._-])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+" +) + + +class IamKeyEnforcerError(Exception): + """All errors raised by IamKeyEnforcer Lambda.""" + + +def lambda_handler(event, context): # pylint: disable=unused-argument + """Audit Access Key Age. + + Reads the credential report: + - Determines the age of each access key + - Builds a report of all keys older than KEY_AGE_WARNING + - Takes action (inactive/delete) on non-compliant Access Keys + """ + log.debug("Event:\n%s", event) + + # Assume the session + assumed_role_session = assume_role( + SESSION, event["role_arn"], RoleSessionName=generate_lambda_session_name() + ) + + assumed_acct_arn = assumed_role_session.client("sts").get_caller_identity()["Arn"] + # do stuff with the assumed role using assumed_role_session + log.debug("IAM Key Enforce account arn %s", assumed_acct_arn) + + client_iam = assumed_role_session.client("iam") + + # Generate Credential Report + generate_credential_report(client_iam, report_counter=0) + + # Get Credential Report + report = get_credential_report(client_iam) + + # Process Users in Credential Report + key_report_contents = process_credential_report(client_iam, event, report) + + if key_report_contents: + store_and_email_report(key_report_contents, event) + else: + log.info("No expiring access keys for account arn %s", assumed_acct_arn) + + +def generate_credential_report(client_iam, report_counter, max_attempts=5): + """Generate IAM Credential Report.""" + generate_report = client_iam.generate_credential_report() + + if generate_report["State"] == "COMPLETE": + # Report is generated, proceed in Handler + return None + + # Report is not ready, try again + report_counter += 1 + log.info("Generate credential report count %s", report_counter) + if report_counter < max_attempts: + log.info("Still waiting on report generation") + sleep(10) + return generate_credential_report(client_iam, report_counter) + + throttle_error = "Credential report generation throttled - exit" + log.error(throttle_error) + raise IamKeyEnforcerError(throttle_error) + + +def get_credential_report(client_iam): + """Process IAM Credential Report.""" + credential_report = client_iam.get_credential_report() + credential_report_csv = io.StringIO(credential_report["Content"].decode("utf-8")) + return list(csv.DictReader(credential_report_csv)) + + +def process_credential_report( + client_iam, event, report +): # pylint: disable=too-many-branches + """Process each user and key in the Credential Report.""" + # Initialize message content + key_report_contents = [] + + # Access the credential report and process it + for row in report: + # A row is a unique IAM user + user_name = row["user"] + log.debug("Processing user: %s", user_name) + + if user_name == "": + log.debug("Skipping user: %s", user_name) + continue + + # Test group exempted + exempted = is_exempted(client_iam, user_name, event) + + # Process Access Keys for user + access_keys = client_iam.list_access_keys(UserName=user_name) + for key in access_keys["AccessKeyMetadata"]: + key_age = object_age(key["CreateDate"]) + access_key_id = key["AccessKeyId"] + + # Log it + log.info( + "User Key Details: %s \t %s \t %s \t %s", + user_name, + key["AccessKeyId"], + str(key_age), + key["Status"], + ) + + # get time of last key use + get_key = client_iam.get_access_key_last_used(AccessKeyId=access_key_id) + + # last_used_date value will not exist if key not used + last_used_date = get_key["AccessKeyLastUsed"].get("LastUsedDate") + + if not exempted and not last_used_date and key_age >= KEY_USE_THRESHOLD: + # Not Exempted and Key has not been used and + # is older than the usage threshold, delete and report + delete_access_key(access_key_id, user_name, client_iam, event) + bg_color = "#E6B0AA" + key_status = "DELETED" + elif key_age < KEY_AGE_WARNING: + # Key age is < warning, do nothing, continue + continue + elif exempted: + # EXEMPT:, do not take action on key, but report it + bg_color = "#D7DBDD" + key_status = f'{key["Status"]} (Exempt)' + elif key_age >= KEY_AGE_DELETE: + # NOT EXEMPT: Delete and report + delete_access_key(access_key_id, user_name, client_iam, event) + bg_color = "#E6B0AA" + key_status = "DELETED" + elif key_age >= KEY_AGE_INACTIVE: + # NOT EXEMPT: Disable and report + disable_access_key(access_key_id, user_name, client_iam, event) + bg_color = "#F4D03F" + key_status = key["Status"] + else: + # NOT EXEMPT: Report + bg_color = "#FFFFFF" + key_status = key["Status"] + + key_report_contents.append( + { + "bg_color": bg_color, + "user_name": user_name, + "access_key_id": key["AccessKeyId"], + "key_age": str(key_age), + "key_status": key_status, + "last_used_date": str(last_used_date), + } + ) + + return key_report_contents + + +def is_exempted(client_iam, user_name, event): + """Determine if user is in an exempted group.""" + groups = client_iam.list_groups_for_user(UserName=user_name) + for group in groups["Groups"]: + if group["GroupName"] in event["exempt_groups"]: + log.info("User is exempt via group membership in: %s", group["GroupName"]) + return True + return False + + +def delete_access_key(access_key_id, user_name, client_iam, event): + """Delete Access Key.""" + armed_log_prefix = NOT_ARMED_PREFIX + if event["armed"]: + armed_log_prefix = ARMED_PREFIX + log.info( + "%s Deleting AccessKeyId %s for user %s", + armed_log_prefix, + access_key_id, + user_name, + ) + if event["armed"]: + client_iam.delete_access_key(UserName=user_name, AccessKeyId=access_key_id) + + if event["email_user_enabled"]: + armed_state_msg = ( + "has been deleted" if event["armed"] else "is marked for deletion" + ) + email_user( + client_iam, + { + "armed_state_msg": armed_state_msg, + "access_key_id": access_key_id, + "action": "deleted", + "key_age": KEY_AGE_DELETE, + "user_name": user_name, + }, + event, + ) + else: + log.info("Email User not enabled per event email_user_enabled variable setting") + + +def disable_access_key(access_key_id, user_name, client_iam, event): + """Disable Access Key.""" + armed_log_prefix = NOT_ARMED_PREFIX + if event["armed"]: + armed_log_prefix = ARMED_PREFIX + log.info( + "%s Disabling AccessKeyId %s for user %s", + armed_log_prefix, + access_key_id, + user_name, + ) + + if event["armed"]: + client_iam.update_access_key( + UserName=user_name, AccessKeyId=access_key_id, Status="Inactive" + ) + + if event["email_user_enabled"]: + armed_state_msg = ( + "has been marked 'Inactive'" + if event["armed"] + else "would be marked 'Inactive'" + ) + email_user( + client_iam, + { + "armed_state_msg": armed_state_msg, + "access_key_id": access_key_id, + "action": "disabled", + "key_age": KEY_AGE_INACTIVE, + "user_name": user_name, + }, + event, + ) + else: + log.info("Email User not enabled per event email_user_enabled variable setting") + + +def email_user(client_iam, user_key_details, event): + """Email user.""" + to_addresses = get_to_addresses(event) + user_email = get_user_email(client_iam, user_key_details["user_name"], event) + if user_email: + to_addresses.append(user_email) + + if not to_addresses: + log.error("User email list is empty, no emails sent") + return + + template_data = user_email_template_data(user_key_details, event) + + send_email(EMAIL_USER_TEMPLATE, template_data, to_addresses) + + +def email_admin(event, template_data): + """Email admin.""" + to_addresses = get_to_addresses(event) + + if not to_addresses: + log.error("Admin email list is empty, no emails sent") + return + + # Construct and Send Email + send_email( + EMAIL_ADMIN_TEMPLATE, + template_data, + to_addresses, + ) + + +def get_to_addresses(event): + """Get the addresses to send the user email to.""" + to_addresses = [] + + if validate_email(ADMIN_EMAIL): + to_addresses.append(ADMIN_EMAIL) + else: + log_invalid_email("admin", ADMIN_EMAIL) + + event_email_targets = get_event_email_targets(event) + to_addresses.extend(event_email_targets) + + return to_addresses + + +def get_event_email_targets(event): + """Get list of email targets from the provided event.""" + email_targets = [] + for email_target in event["email_targets"]: + if validate_email(email_target): + email_targets.append(email_target) + else: + log_invalid_email("target", email_target) + + # if mode is debug we do not want to email the actual targets + # log whatever targets there were and return an empty list + if event.get("debug") and email_targets: + log.debug("Debug Mode:Event email targets %s", ", ".join(email_targets)) + return [] + return email_targets + + +def get_user_email(client_iam, user_name, event): + """Get and validate user email from Key Tags.""" + tags = client_iam.list_user_tags(UserName=user_name) + email = None + for tag in tags["Tags"]: + if tag["Key"].lower() == EMAIL_TAG: + email = tag["Value"] + break + + if not email: + log.debug("No email found for user %s", user_name) + return None + + if validate_email(email): + if not event.get("debug"): + return email + log.debug("Debug Mode: Append user email %s", email) + else: + log_invalid_email(f"user ({user_name})", email) + + return None + + +def log_invalid_email(email_type, email): + """Log error for invalid email and specify the type.""" + log.error("Invalid %s email found - email: %s", email_type, email) + + +def validate_email(email): + """Validate email provided matches regex.""" + if not email or not re.fullmatch(email_regex, email): + return False + + return True + + +def admin_email_template_data(key_report_contents, event, exempt_groups): + """Build email template data for admin emails.""" + template_data = { + "account_number": event["account_number"], + "account_name": event["account_name"], + "key_report_contents": key_report_contents, + "key_age_inactive": KEY_AGE_INACTIVE, + "key_age_delete": KEY_AGE_DELETE, + "key_age_warning": KEY_AGE_WARNING, + } + + template_data.update(optional_email_template_data(event, exempt_groups)) + return template_data + + +def user_email_template_data(user_email_details, event): + """Build email template data for user emails.""" + template_data = { + "armed_state_msg": user_email_details["armed_state_msg"], + "access_key_id": user_email_details["access_key_id"], + "action": user_email_details["action"], + "key_age": user_email_details["key_age"], + "user_name": user_email_details["user_name"], + } + template_data.update(optional_email_template_data(event)) + return template_data + + +def optional_email_template_data(event, exempt_groups=None): + """Set and return optional email template data.""" + template_data = {} + if EMAIL_BANNER_MSG: + template_data["email_banner_msg"] = EMAIL_BANNER_MSG + template_data["email_banner_msg_color"] = EMAIL_BANNER_MSG_COLOR + + if not event["armed"]: + template_data["unarmed"] = True + + if exempt_groups: + template_data["exempt_groups"] = exempt_groups + + return template_data + + +def store_and_email_report(key_report_contents, event): + """Generate HTML and send report to email_targets list for tenant \ + account and ADMIN_EMAIL via SES.""" + if not S3_ENABLED: + log.info("S3 report not enabled per setting") + + if not EMAIL_ADMIN_REPORT_ENABLED: + log.info("Admin Email not enabled per setting") + + if not (S3_ENABLED and EMAIL_ADMIN_REPORT_ENABLED): + return + + exempt_groups = ( + ", ".join(event["exempt_groups"]) if event["exempt_groups"] else None + ) + + template_data = admin_email_template_data(key_report_contents, event, exempt_groups) + + store_in_s3(event["account_number"], template_data) + + email_admin(event, template_data) + + +def store_in_s3(account_number, template_data): + """Store email report in S3 Bucket.""" + s3_key = ( + f"{account_number}" + "/access_key_audit_report_" + f"{str(datetime.date.today())}.html" + ) + + response = CLIENT_SES.test_render_template( + TemplateName=EMAIL_ADMIN_TEMPLATE, TemplateData=json.dumps(template_data) + ) + + email_contents = response.get("RenderedTemplate", None) + + if email_contents: + log.debug( + "Storing report to S3 key %s Report Details: %s", s3_key, email_contents + ) + response = CLIENT_S3.put_object( + Bucket=S3_BUCKET, Key=s3_key, Body=email_contents + ) + else: + log.error( + "Error generating S3 report using TemplateName: %s and TemplateData: %s", + EMAIL_ADMIN_TEMPLATE, + template_data, + ) + + +def send_email(template, template_data, email_targets): + """Email user with the action taken on their key.""" + if not email_targets: + log.error("Email targets list is empty, no emails sent") + return + + response = CLIENT_SES.send_templated_email( + Source=EMAIL_SOURCE, + Destination={ + "ToAddresses": email_targets, + }, + Template=template, + TemplateData=json.dumps(template_data), + ) + + log.info("Email Sent Successfully. Message ID: %s", response["MessageId"]) + + +def object_age(last_changed): + """Determine days since last change.""" + # Handle as string + if isinstance(last_changed, str): + last_changed_date = dateutil.parser.parse(last_changed).date() + # Handle as native datetime + elif isinstance(last_changed, datetime.datetime): + last_changed_date = last_changed.date() + else: + return 0 + age = datetime.date.today() - last_changed_date + return age.days diff --git a/src/python/requirements.txt b/src/python/requirements.txt new file mode 100644 index 0000000..a5ffafe --- /dev/null +++ b/src/python/requirements.txt @@ -0,0 +1 @@ +aws-assume-role-lib diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/create_all/main.tf b/tests/create_all/main.tf new file mode 100644 index 0000000..ee10686 --- /dev/null +++ b/tests/create_all/main.tf @@ -0,0 +1,133 @@ +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} +data "aws_partition" "current" {} + +locals { + id = data.terraform_remote_state.prereq.outputs.test_id.result + project = "${var.project}-${local.id}" + + tags = { + "broker_managed" = true + "contact" = var.contact_email + "project" = local.project + } +} + +module "iam_key_enforcer" { + source = "../.." + + project_name = local.project + + accounts = [ + { + account_name = var.account_name + account_number = data.aws_caller_identity.current.account_id + armed = false + debug = true + email_user_enabled = true + email_targets = ["communications@example.com"] + exempt_groups = var.exempt_groups + schedule_expression = "rate(10 minutes)" + + } + ] + assume_role_name = aws_iam_role.assume_role.name + admin_email = "communications@example.com" + email_admin_report_enabled = true + email_source = var.email_source + email_banner_message = "IAM Key Enforcement will be armed on 07/31/2023" + email_banner_message_color = "red" + # email_templates = { + # # admin = { + # # subject = "IAM Key Enforcement Report for {{account_number}}" + # # html = "HTML" + # # } + # # user = { + # # subject = "IAM User Key {{armed_state_msg}} for {{user_name}}" + # # } + # } + key_age_delete = var.key_age_delete + key_age_inactive = var.key_age_inactive + key_use_threshold = var.key_use_threshold + key_age_warning = var.key_age_warning + log_level = "DEBUG" + s3_bucket = aws_s3_bucket.this.id + s3_enabled = var.s3_enabled + tags = local.tags +} + +resource "aws_s3_bucket" "this" { + bucket = "${local.project}-bucket" + tags = local.tags + force_destroy = true + +} + +resource "aws_s3_bucket_public_access_block" "this" { + bucket = aws_s3_bucket.this.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +data "aws_iam_policy_document" "iam_key" { + statement { + actions = [ + "iam:GenerateCredentialReport", + "iam:GetCredentialReport", + "iam:ListUsers", + "iam:GetAccessKeyLastUsed" + ] + + resources = [ + "*" + ] + } + + statement { + actions = [ + "iam:DeleteAccessKey", + "iam:ListGroupsForUser", + "iam:UpdateAccessKey", + "iam:ListAccessKeys", + "iam:ListUserTags", + ] + + resources = [ + "arn:${data.aws_partition.current.partition}:iam::*:user/*" + ] + } +} + +resource "aws_iam_policy" "iam_policy" { + name = "${local.project}-policy" + + policy = data.aws_iam_policy_document.iam_key.json +} + +resource "aws_iam_role" "assume_role" { + name = "${local.project}-role" + managed_policy_arns = [aws_iam_policy.iam_policy.arn] + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + "Sid" : "AssumeRoleCrossAccount", + "Effect" : "Allow", + "Principal" : { + "AWS" : "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:root" + }, + "Action" : "sts:AssumeRole" + } + ] + }) +} + +data "terraform_remote_state" "prereq" { + backend = "local" + config = { + path = "prereq/terraform.tfstate" + } +} diff --git a/tests/create_all/prereq/main.tf b/tests/create_all/prereq/main.tf new file mode 100644 index 0000000..f6e28ae --- /dev/null +++ b/tests/create_all/prereq/main.tf @@ -0,0 +1,10 @@ +resource "random_string" "this" { + length = 6 + upper = false + special = false + numeric = false +} + +output "test_id" { + value = random_string.this +} diff --git a/tests/create_all/variables.tf b/tests/create_all/variables.tf new file mode 100644 index 0000000..47b0388 --- /dev/null +++ b/tests/create_all/variables.tf @@ -0,0 +1,76 @@ +variable "project" { + description = "Project name to prefix resources with" + type = string + default = "test-iam-key-enforcer" +} + +variable "account_name" { + description = "Account name referenced in report" + type = string + default = "TEST_ACCOUNT_NAME" +} + +variable "email_target" { + description = "Email to send reports to for an account" + type = string + default = "communications@example.com" +} + +variable "email_source" { + description = "Email to send reports from" + type = string + default = "communications@example.com" +} + +variable "admin_email" { + description = "Admin Email that report will be emailed to" + type = string + default = "communications@example.com" +} + +variable "key_age_warning" { + description = "Age at which to warn (e.g. 75)" + type = number + default = 1 +} + +variable "key_age_inactive" { + description = "Age at which a key should be inactive (e.g. 90)" + type = number + default = 2 +} + +variable "key_age_delete" { + description = "Age at which a key should be deleted (e.g. 120)" + type = number + default = 2 +} + +variable "key_use_threshold" { + description = "Age at which unused keys should be deleted (e.g.30)" + type = number + default = 1 +} + +variable "s3_enabled" { + description = "Set to 'true' and provide s3_bucket if the audit report should be written to S3" + type = bool + default = true +} + +variable "exempt_groups" { + description = "Groups that are exempt from processing" + type = list(string) + default = [ + "sample-group", + "test-group", + "other-group", + "service-accounts" + ] +} + +variable "contact_email" { + description = "Contact Email" + type = string + default = "test" +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..7096b26 --- /dev/null +++ b/variables.tf @@ -0,0 +1,162 @@ +variable "project_name" { + description = "Project name to prefix resources with" + type = string + default = "iam-key-enforcer" +} + +variable "assume_role_name" { + description = "Name of the IAM role that the lambda will assume in the target account" + type = string +} + +variable "email_admin_report_enabled" { + description = "Used to enable or disable the SES emailed report" + type = bool + default = false +} + +variable "email_admin_report_subject" { + description = "Subject of the report email that is sent" + type = string + default = null +} + +variable "email_source" { + description = "Email that will be used to send messages" + type = string +} + +variable "email_banner_message" { + description = "Messages that will be at the top of all emails sent to notify recipients of important information" + type = string + default = "" +} + +variable "email_banner_message_color" { + description = "Color of email banner message, must be valid html color" + type = string + default = "red" +} + +variable "email_tag" { + description = "Tag to be placed on the IAM user that we can use to notify when their key is going to be disabled/deleted" + type = string + default = "keyenforcer:email" +} + +variable "email_templates" { + description = "Email templates to use for Admin and User emails" + type = object({ + admin = optional(object({ + subject = optional(string, null), + html = optional(string, null), + text = optional(string, null), + }), {}), + user = optional(object({ + subject = optional(string, null), + html = optional(string, null), + text = optional(string, null), + }), {}) + }) + + default = {} +} + +variable "admin_email" { + description = "Admin Email that will receive all emails and reports about actions taken if email is enabled" + type = string +} + +variable "key_age_warning" { + description = "Age at which to warn (e.g. 75)" + type = number +} + +variable "key_age_inactive" { + description = "Age at which a key should be inactive (e.g. 90)" + type = number +} + +variable "key_age_delete" { + description = "Age at which a key should be deleted (e.g. 120)" + type = number +} + +variable "key_use_threshold" { + description = "Age at which unused keys should be deleted (e.g.30)" + type = number +} + +variable "s3_enabled" { + description = "Set to 'true' and provide s3_bucket if the audit report should be written to S3" + type = bool + default = false +} + +variable "s3_bucket" { + description = "Bucket name to write the audit report to if s3_enabled is set to 'true'" + type = string + default = null +} + +variable "schedule_expression" { + description = "(DEPRECATED) Schedule Expressions for Rules" + type = string + default = null +} + +variable "accounts" { + description = "List of account objects to create events for" + type = list(object({ + account_name = string + account_number = string + role_name = optional(string) # deprecated + armed = bool + debug = optional(bool, false) + email_user_enabled = bool + email_targets = list(string) + exempt_groups = list(string) + schedule_expression = optional(string, "cron(0 1 ? * SUN *)") + + })) + default = [] +} + +variable "lambda" { + description = "Map of any additional arguments for the upstream lambda module. See " + type = object({ + artifacts_dir = optional(string, "builds") + build_in_docker = optional(bool, false) + create_package = optional(bool, true) + ephemeral_storage_size = optional(number) + ignore_source_code_hash = optional(bool, true) + local_existing_package = optional(string) + recreate_missing_package = optional(bool, false) + runtime = optional(string, "python3.11") + s3_bucket = optional(string) + s3_existing_package = optional(map(string)) + s3_prefix = optional(string) + store_on_s3 = optional(bool, false) + timeout = optional(number, 300) + source_path = optional(object({ + patterns = optional(list(string), ["!\\.terragrunt-source-manifest"]) + }), {}) + }) + default = {} +} + +variable "log_level" { + description = "Log level for lambda" + type = string + default = "INFO" + validation { + condition = contains(["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], var.log_level) + error_message = "Valid values for log level are (CRITICAL, ERROR, WARNING, INFO, DEBUG)." + } +} + +variable "tags" { + description = "Tags for resource" + type = map(string) + default = {} +} diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..c39be1b --- /dev/null +++ b/versions.tf @@ -0,0 +1,11 @@ +terraform { + required_version = ">= 1.1" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.0" + } + + } +} From effbb3886abea55c6e2ec74cab141130a4255df8 Mon Sep 17 00:00:00 2001 From: John Ricords <122303445+johnricords@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:39:54 -0400 Subject: [PATCH 2/4] changed xml tag --- email_templates/admin_email.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/email_templates/admin_email.txt b/email_templates/admin_email.txt index aa53525..f940b04 100644 --- a/email_templates/admin_email.txt +++ b/email_templates/admin_email.txt @@ -17,7 +17,7 @@ Rotate any keys as necessary to prevent disruption to your applications. {{#if exempt_groups}} Exempted IAM Group(s):{{exempt_groups}}. - Exempted group members have a key status value of (Exempt). + Exempted group members have a key status value of "STATUS" (Exempt). {{/if}} IAM User Name, Access Key ID, Key Age, Key Status, Last Used From d5e77c389e7fa2cf011d164e9997c79b33180536 Mon Sep 17 00:00:00 2001 From: John Ricords <122303445+johnricords@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:51:23 -0400 Subject: [PATCH 3/4] modified docker file --- Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dockerfile b/Dockerfile index 28119ee..30b77ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1 +1,6 @@ FROM plus3it/tardigrade-ci:0.24.15 + +COPY ./src/python/requirements.txt /app/requirements/lambda.txt + +RUN python -m pip install --no-cache-dir \ + -r /app/requirements/lambda.txt From f23fda1119926119d3599834293dd7aacfe40e6e Mon Sep 17 00:00:00 2001 From: John Ricords <122303445+johnricords@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:11:58 -0400 Subject: [PATCH 4/4] added tf config to dependabot --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 653a660..5514df2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,3 +18,12 @@ updates: docker: patterns: - "*" + # Maintain dependencies for terraform + - package-ecosystem: terraform + directory: / + schedule: + interval: weekly + groups: + terraform: + patterns: + - "*"