diff --git a/.tflint.hcl b/.tflint.hcl index 1a823ac..0d42d9b 100644 --- a/.tflint.hcl +++ b/.tflint.hcl @@ -4,13 +4,13 @@ config { } plugin "aws" { - enabled = true - version = "0.30.0" - source = "github.com/terraform-linters/tflint-ruleset-aws" + enabled = true + version = "0.30.0" + source = "github.com/terraform-linters/tflint-ruleset-aws" } rule "terraform_comment_syntax" { - enabled = true + enabled = true } rule "terraform_naming_convention" { diff --git a/README.md b/README.md index 610a647..9d41145 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ This Terraform module consists of the configuration for automating the remediation of AWS EC2 vulnerabilities using AWS Inspector findings. It provisions essential resources such as an SSM document, Lambda function, and CloudWatch event rules for automated vulnerability management. +## Prerequisites + +> **Important** +> +> The AWS Systems Manager (SSM) agent **must be installed and running** on all EC2 instances you wish to remediate. Without SSM, this module cannot trigger remediation actions on your instances. + ## Description This Terraform module sets up an automated vulnerability remediation environment optimized for production use. By creating an SSM document to define the remediation steps, setting up a Lambda function to execute the remediation, and establishing CloudWatch event rules to trigger the process based on AWS Inspector findings, the module offers a straightforward approach to managing EC2 vulnerabilities on AWS. @@ -47,7 +53,7 @@ module "remediation" { aws_region = "us-east-1" account_id = "2123232323" lambda_log_group = "/aws/lambda/vulne-soldier-compliance-remediate" - lambda_zip = "./lambda.zip" + path_to_lambda_zip = "./lambda.zip" remediation_options = { region = "us-east-1" reboot_option = "NoReboot" @@ -58,6 +64,8 @@ module "remediation" { vulnerability_severities = ["CRITICAL, HIGH"] override_findings_for_target_instances_ids = [] } + remediation_schedule_days = ["15", "L"] # Schedule remediation on the 15th and last day of each month + ssm_notification_topic_arn = null # Optional: Specify an SNS topic ARN to receive notifications for remediation events } provider "aws" { @@ -70,6 +78,16 @@ provider "aws" { On successful deployment, navigate to the AWS Systems Manager console and search for the SSM document created by the module (vulne-soldier-compliance-remediate-inspector-findings) or similar. You can trigger the remediation process by running the document on the affected EC2 instances. You can also create an AWS CloudWatch event rule to automate the process based on AWS Inspector findings. +## What's New in v2 + +- Remediation is now **automated** using EventBridge rules, running by default with the `NoReboot` option for minimal disruption. You can update this option as needed in your configuration. + +## Walkthrough Video + +[![v2 Walkthrough Demo](assets/v2-walkthrough.png)](https://vimeo.com/1098910908?share=copy#t=3.684) + +> Watch the [v2 walkthrough video](https://vimeo.com/1098910908?share=copy#t=3.684) for a step-by-step demonstration of setup and usage. + ## Inputs | Name | Description | Type | Default | Required | @@ -79,14 +97,16 @@ On successful deployment, navigate to the AWS Systems Manager console and search | `aws_region` | AWS region where the resources will be created | `string` | n/a | yes | | `account_id` | AWS account ID | `string` | n/a | yes | | `lambda_log_group` | Name of the CloudWatch Log Group for the Lambda function | `string` | n/a | yes | -| `lambda_zip` | File location of the lambda zip file for remediation | `string` | `lambda.zip` | yes | -| `remediation_options` | Options for the remediation document | `object` | n/a | yes | +| `path_to_lambda_zip` | File location of the lambda zip file for remediation | `string` | `lambda.zip` | yes | +| `remediation_options` | Options for the remediation document | `object list` | n/a | yes | | `remediation_options.region` | The region to use | `string` | `us-east-1` | no | | `remediation_options.reboot_option` | Reboot option for patching | `string` | `NoReboot` | no | | `remediation_options.target_ec2_tag_name`| The tag name to filter EC2 instances | `string` | `AmazonECSManaged` | no | | `remediation_options.target_ec2_tag_value`| The tag value to filter EC2 instances | `string` | `true` | no | | `remediation_options.vulnerability_severities`| Comma separated list of vulnerability severities to filter findings | `string`| `"CRITICAL, HIGH"` | no | | `remediation_options.override_findings_for_target_instances_ids`| Comma separated list of instance IDs to override findings for target instances | `string`| `""` | no | +| `remediation_schedule_days` | Days of the month to schedule remediation (e.g., ["15", "L"]) | `list(string)`| `["15", "L"]` | no | +| `ssm_notification_topic_arn` | SNS topic ARN to receive notifications for remediation events (optional) | `string` | `null` | no | ## Outputs diff --git a/assets/v2-walkthrough.png b/assets/v2-walkthrough.png new file mode 100644 index 0000000..22358b0 Binary files /dev/null and b/assets/v2-walkthrough.png differ diff --git a/examples/basic/main.tf b/examples/basic/main.tf index 464b661..0b9a941 100644 --- a/examples/basic/main.tf +++ b/examples/basic/main.tf @@ -1,19 +1,21 @@ module "remediation" { source = "iKnowJavaScript/vulne-soldier/aws" - version = "1.0.2" + version = "2.0.0" - name = "vulne-soldier-compliance-remediate" - environment = "dev" - aws_region = "us-east-1" - account_id = "111122223333" - lambda_log_group = "/aws/lambda/vulne-soldier-compliance-remediate" - lambda_zip = "../../lambda.zip" - remediation_options = { + name = "vulne-soldier-compliance-remediate" + environment = "prod" + aws_region = "us-east-1" + account_id = "111122223333" + lambda_log_group = "/aws/lambda/vulne-soldier-compliance-remediate" + path_to_lambda_zip = "../../lambda.zip" + remediation_options = [{ region = "us-east-1" reboot_option = "NoReboot" target_ec2_tag_name = "AmazonECSManaged" target_ec2_tag_value = "true" vulnerability_severities = "CRITICAL, HIGH" override_findings_for_target_instances_ids = "" - } + }] + remediation_schedule_days = ["15", "L"] + ssn_notification_topic_arn = null } diff --git a/examples/basic/terraform.tf b/examples/basic/terraform.tf index 091282f..8d05edc 100644 --- a/examples/basic/terraform.tf +++ b/examples/basic/terraform.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = "~> 5.0" } } } diff --git a/main.tf b/main.tf index a255ba1..002b908 100644 --- a/main.tf +++ b/main.tf @@ -5,7 +5,7 @@ provider "aws" { locals { function_name = "${var.name}-${var.environment}" ssm_document_name = "${var.name}-inspector-findings-${var.environment}" - lambda_zip = var.lambda_zip + lambda_zip = var.path_to_lambda_zip } resource "aws_ssm_document" "remediation_document" { @@ -20,32 +20,32 @@ resource "aws_ssm_document" "remediation_document" { "region": { "type": "String", "description": "(Required) The region to use.", - "default": "${var.remediation_options.region}" + "default": "${local.default_remediation_option.region}" }, "rebootOption": { "type": "String", "description": "(Optional) Reboot option for patching. Allowed values: NoReboot, RebootIfNeeded, AlwaysReboot", - "default": "${var.remediation_options.reboot_option}" + "default": "${local.default_remediation_option.reboot_option}" }, "targetEC2TagName": { "type": "String", "description": "The tag name to filter EC2 instances.", - "default": "${var.remediation_options.target_ec2_tag_name}" + "default": "${local.default_remediation_option.target_ec2_tag_name}" }, "targetEC2TagValue": { "type": "String", "description": "The tag value to filter EC2 instances.", - "default": "${var.remediation_options.target_ec2_tag_value}" + "default": "${local.default_remediation_option.target_ec2_tag_value}" }, "vulnerabilitySeverities": { "type": "String", "description": "(Optional) Comma separated list of vulnerability severities to filter findings. Allowed values are comma separated list of : CRITICAL, HIGH, MEDIUM, LOW, INFORMATIONAL", - "default": "${var.remediation_options.vulnerability_severities}" + "default": "${local.default_remediation_option.vulnerability_severities}" }, "overrideFindingsForTargetInstancesIDs": { "type": "String", "description": "(Optional) Comma separated list of instance IDs to override findings for target instances. If not provided, all matched findings will be remediated. Values are in comma separated list of instance IDs.", - "default": "${var.remediation_options.override_findings_for_target_instances_ids}" + "default": "${local.default_remediation_option.override_findings_for_target_instances_ids}" } }, "mainSteps": [ @@ -62,34 +62,62 @@ resource "aws_ssm_document" "remediation_document" { DOC } -# Set up an EventBridge rule that triggers on AWS Inspector findings. -resource "aws_cloudwatch_event_rule" "inspector_findings" { - name = "manual-inspector-findings-rule" - description = "Triggers on AWS Inspector findings." - - event_pattern = jsonencode({ - source = ["aws.inspector"], - "detail-type" = ["Inspector Finding"], - detail = { - severity = ["High", "Critical", "MEDIUM", "LOW", "INFORMATIONAL"] - } - }) +locals { + remediation_options_count = length(var.remediation_options) + default_remediation_option = var.remediation_options[0] + remediation_schedule_crons = [ + for day in var.remediation_schedule_days : + "cron(0 2 ${day} * ? *)" + ] + remediation_target_matrix = flatten([ + for schedule_idx, schedule in local.remediation_schedule_crons : [ + for opt_idx, opt in var.remediation_options : { + schedule_idx = schedule_idx + schedule = schedule + opt_idx = opt_idx + opt = opt + } + ] + ]) } -resource "aws_cloudwatch_event_target" "ssm_remediation" { - rule = aws_cloudwatch_event_rule.inspector_findings.name - target_id = "SSMVulneRemediationTarget" - arn = aws_ssm_document.remediation_document.arn +resource "aws_cloudwatch_event_rule" "inspector_findings_schedule" { + count = length(local.remediation_schedule_crons) + name = "${var.name}-rule-${count.index}-${var.environment}" + description = "Triggers SSM remediation document on day ${var.remediation_schedule_days[count.index]} of each month." + schedule_expression = local.remediation_schedule_crons[count.index] - run_command_targets { - key = "tag:${var.remediation_options.target_ec2_tag_name}" - values = [var.remediation_options.target_ec2_tag_value] + tags = { + Environment = var.environment + Name = "${var.name}-rule-${var.environment}-${count.index}" } +} - +resource "aws_cloudwatch_event_target" "ssm_remediation_scheduled" { + for_each = { + for item in local.remediation_target_matrix : + "${item.schedule_idx}-${item.opt_idx}" => item + } + rule = aws_cloudwatch_event_rule.inspector_findings_schedule[each.value.schedule_idx].name + target_id = "SSMVulneRemediationTarget-${each.value.opt_idx}-${each.value.schedule_idx}-${each.value.opt.region}" + arn = aws_ssm_document.remediation_document.arn + input = jsonencode({ + region = each.value.opt.region, + rebootOption = each.value.opt.reboot_option, + targetEC2TagName = each.value.opt.target_ec2_tag_name, + targetEC2TagValue = each.value.opt.target_ec2_tag_value, + vulnerabilitySeverities = each.value.opt.vulnerability_severities, + overrideFindingsForTargetInstancesIDs = each.value.opt.override_findings_for_target_instances_ids, + }) role_arn = aws_iam_role.ssm_role.arn } +resource "aws_cloudwatch_event_target" "sns_inspector_alert_scheduled" { + count = var.ssn_notification_topic_arn != null ? length(local.remediation_schedule_crons) : 0 + rule = aws_cloudwatch_event_rule.inspector_findings_schedule[count.index].name + target_id = "InspectorCriticalHighAlertsSNS-${count.index}-${var.environment}" + arn = var.ssn_notification_topic_arn +} resource "aws_iam_role" "ssm_role" { name = "SSMVulneAutomationRole" @@ -106,12 +134,16 @@ resource "aws_iam_role" "ssm_role" { } ] }) +} + +resource "aws_iam_role_policy" "ssm_document_execution" { + name = "SSMDocumentExecution" + role = aws_iam_role.ssm_role.id - inline_policy { - name = "SSMDocumentExecution" - policy = jsonencode({ - Version = "2012-10-17", - Statement = [ + policy = jsonencode({ + Version = "2012-10-17", + Statement = concat( + [ { Action = [ "ssm:StartAutomationExecution", @@ -120,12 +152,18 @@ resource "aws_iam_role" "ssm_role" { Effect = "Allow", Resource = "arn:aws:ssm:*:*:document/${local.ssm_document_name}*" } - ] - }) - } + ], + var.ssn_notification_topic_arn != null ? [ + { + Effect = "Allow", + Action = "SNS:Publish", + Resource = var.ssn_notification_topic_arn + } + ] : [] + ) + }) } - resource "aws_iam_role" "lambda_execution_role" { name = "compliance-vulne-remediate_lambda_execution_role" @@ -154,16 +192,16 @@ resource "aws_iam_role_policy" "lambda_policy" { "logs:PutLogEvents" ], Effect = "Allow", - Resource = "arn:aws:logs:*:*:*" + Resource = "arn:aws:logs:*:${var.account_id}:*" }, { - "Effect" : "Allow", - "Action" : [ + Effect = "Allow", + Action = [ "ssm:StartAutomationExecution", "ssm:DescribeAutomationExecutions", "ssm:GetAutomationExecution" ], - "Resource" : "*" + Resource = "arn:aws:ssm:*:${var.account_id}:automation-definition/*" }, { "Effect" : "Allow", @@ -173,11 +211,14 @@ resource "aws_iam_role_policy" "lambda_policy" { "Resource" : "*" }, { - "Effect" : "Allow", - "Action" : [ + Effect = "Allow", + Action = [ "ssm:SendCommand" ], - "Resource" : "*" + Resource = [ + "*", + "arn:aws:ec2:*:${var.account_id}:instance/*" + ] }, { "Effect" : "Allow", @@ -186,7 +227,7 @@ resource "aws_iam_role_policy" "lambda_policy" { "inspector2:ListFindings", "inspector2:updateFindings" ], - "Resource" : "*" + "Resource" : "arn:aws:inspector2:*:${var.account_id}:*" }, { "Effect" : "Allow", @@ -194,7 +235,7 @@ resource "aws_iam_role_policy" "lambda_policy" { "inspector:DescribeFindings", "inspector:ListFindings" ], - "Resource" : "*" + "Resource" : "arn:aws:inspector:*:${var.account_id}:*" }], }) } @@ -205,7 +246,7 @@ resource "aws_lambda_function" "inspector_remediation" { function_name = local.function_name role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" - runtime = "nodejs18.x" + runtime = "nodejs20.x" source_code_hash = filebase64sha256(local.lambda_zip) timeout = 300 @@ -220,20 +261,7 @@ resource "aws_lambda_function" "inspector_remediation" { tags = { Environment = var.environment } -} - - -resource "aws_cloudwatch_event_target" "lambda" { - rule = aws_cloudwatch_event_rule.inspector_findings.name - arn = aws_lambda_function.inspector_remediation.arn -} - -resource "aws_lambda_permission" "allow_cloudwatch_to_call_lambda" { - statement_id = "AllowExecutionFromCloudWatch" - action = "lambda:InvokeFunction" - function_name = aws_lambda_function.inspector_remediation.function_name - principal = "events.amazonaws.com" - source_arn = aws_cloudwatch_event_rule.inspector_findings.arn -} - - + tracing_config { + mode = "Active" + } +} \ No newline at end of file diff --git a/variables.tf b/variables.tf index df9e58f..501e490 100644 --- a/variables.tf +++ b/variables.tf @@ -1,19 +1,19 @@ variable "name" { description = "Name of the application" type = string - default = "vulne-soldier-compliance-remediate" + default = "vulne-soldier-compliance-remediate" } variable "aws_region" { description = "AWS region where the resources will be created" type = string - default = "us-east-1" + default = "us-east-1" } variable "environment" { description = "Name of the environment" type = string - default = "dev" + default = "dev" } variable "account_id" { @@ -30,39 +30,65 @@ variable "lambda_log_group" { type = string } -variable "lambda_zip" { +variable "path_to_lambda_zip" { description = "File location of the lambda zip file for remediation." type = string validation { - condition = can(regex("^.+\\.zip$", var.lambda_zip)) - error_message = "The lambda_zip must be a path to a zip file." + condition = can(regex("^.+\\.zip$", var.path_to_lambda_zip)) + error_message = "The path_to_lambda_zip must be a path to a zip file." } } variable "remediation_options" { - description = "Options for the remediation document" - type = object({ + description = "List of remediation option objects" + type = list(object({ region = string reboot_option = string target_ec2_tag_name = string target_ec2_tag_value = string vulnerability_severities = string override_findings_for_target_instances_ids = string - }) - default = { - region = "us-east-1" - reboot_option = "NoReboot" - target_ec2_tag_name = "AmazonECSManaged" - target_ec2_tag_value = "true" - vulnerability_severities = "CRITICAL, HIGH" - override_findings_for_target_instances_ids = null + })) + default = [ + { + region = "us-east-1" + reboot_option = "NoReboot" + target_ec2_tag_name = "AmazonECSManaged" + target_ec2_tag_value = "true" + vulnerability_severities = "CRITICAL, HIGH" + override_findings_for_target_instances_ids = null + } + ] + validation { + condition = alltrue([ + for opt in var.remediation_options : contains(["NoReboot", "RebootIfNeeded"], opt.reboot_option) + ]) + error_message = "Each remediation_option.reboot_option must be either NoReboot or RebootIfNeeded." } validation { - condition = contains(["NoReboot", "RebootIfNeeded"], var.remediation_options.reboot_option) - error_message = "The reboot_option must be either NoReboot or RebootIfNeeded." + condition = alltrue([ + for opt in var.remediation_options : can(regex("^([A-Z]+, )*[A-Z]+$", opt.vulnerability_severities)) + ]) + error_message = "Each remediation_option.vulnerability_severities must be a comma-separated list of severities in uppercase." } +} + +variable "ssn_notification_topic_arn" { + description = "SNS topic ARN for notifications" + type = string + default = null validation { - condition = can(regex("^([A-Z]+, )*[A-Z]+$", var.remediation_options.vulnerability_severities)) - error_message = "The vulnerability_severities must be a comma-separated list of severities in uppercase." + condition = var.ssn_notification_topic_arn == null || can(regex("^arn:aws:sns:[a-z0-9-]+:[0-9]{12}:[a-zA-Z0-9_-]+$", var.ssn_notification_topic_arn)) + error_message = "The ssn_notification_topic_arn must be null or a valid SNS topic ARN." } } + +variable "remediation_schedule_days" { + description = "List of days in the month to trigger remediation (e.g., [15, \"L\"] for 15th and last day)" + type = list(string) + default = ["15", "L"] + validation { + condition = length(var.remediation_schedule_days) > 0 && alltrue([for d in var.remediation_schedule_days : can(regex("^(0?[1-9]|[12][0-9]|3[01]|L)$", d))]) + error_message = "Each value in remediation_schedule_days must be a day number (1-31) or 'L' for last day." + } +} \ No newline at end of file diff --git a/versions.tf b/versions.tf index bdd81f0..478e910 100644 --- a/versions.tf +++ b/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.0" + version = "~> 5.0" } } } \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy