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({| `[]` | 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 |
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 *)")
}))
object({| `{}` | no | +| [lambda](#input\_lambda) | Map of any additional arguments for the upstream lambda module. See
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),
}), {})
})
object({| `{}` | 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}} +
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"])
}), {})
})
+ 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).
+
IAM User Name | +Access Key ID | +Key Age | +Key Status | +Last Used | +
---|---|---|---|---|
{{user_name}} | +{{access_key_id}} | +{{key_age}} | +{{key_status}} | +{{last_used_date}} | +
+ 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({| `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 |
arn = string
})
object({| `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 == "
input_template = string
})