From 7e8ef8339024379552c98a7b49b8ac249be17c80 Mon Sep 17 00:00:00 2001 From: Greg Hendrickson Date: Fri, 6 Feb 2026 20:05:03 +0000 Subject: [PATCH] feat(security): add guardduty and security-hub modules - guardduty: Full-featured threat detection with SNS alerts, EventBridge, S3 export, IPSet/ThreatIntelSet, organization support - security-hub: Centralized security posture with standards (CIS, PCI, NIST), cross-region aggregation, custom actions, built-in insights Both modules are opt-in via variables with sensible defaults. --- terraform/modules/guardduty/README.md | 140 +++++ terraform/modules/guardduty/main.tf | 643 +++++++++++++++++++++++ terraform/modules/security-hub/README.md | 190 +++++++ terraform/modules/security-hub/main.tf | 552 +++++++++++++++++++ 4 files changed, 1525 insertions(+) create mode 100644 terraform/modules/guardduty/README.md create mode 100644 terraform/modules/guardduty/main.tf create mode 100644 terraform/modules/security-hub/README.md create mode 100644 terraform/modules/security-hub/main.tf diff --git a/terraform/modules/guardduty/README.md b/terraform/modules/guardduty/README.md new file mode 100644 index 0000000..3f30565 --- /dev/null +++ b/terraform/modules/guardduty/README.md @@ -0,0 +1,140 @@ +# GuardDuty Module + +AWS GuardDuty threat detection with alerting, S3 export, and threat intelligence integration. + +## Features + +- **All Protection Types**: S3, Kubernetes, malware, RDS, Lambda, runtime monitoring +- **SNS Alerts**: EventBridge-based alerts with severity filtering +- **S3 Export**: Archive findings with lifecycle policies +- **Threat Intelligence**: Custom IP sets and threat intel feeds +- **Organization Support**: Delegated admin configuration + +## Usage + +### Basic + +```hcl +module "guardduty" { + source = "../modules/guardduty" + name = "main" +} +``` + +### With Email Alerts + +```hcl +module "guardduty" { + source = "../modules/guardduty" + name = "main" + + enable_sns_alerts = true + alert_email = "security@example.com" + alert_severity_threshold = "HIGH" # Only HIGH and CRITICAL +} +``` + +### Full Security Stack + +```hcl +module "guardduty" { + source = "../modules/guardduty" + name = "security-prod" + + # All protections enabled + enable_s3_protection = true + enable_kubernetes_audit = true + enable_malware_protection = true + enable_rds_login_events = true + enable_lambda_network_logs = true + enable_runtime_monitoring = true # Additional cost + + # Alerting + enable_sns_alerts = true + alert_email = "security@example.com" + alert_severity_threshold = "MEDIUM" + + # Export for compliance + enable_s3_export = true + + # Trusted IPs (won't generate findings) + ipset_cidrs = [ + "10.0.0.0/8", + "192.168.1.0/24", + ] + + tags = { + Environment = "production" + Team = "security" + } +} +``` + +### Organization Admin + +```hcl +module "guardduty" { + source = "../modules/guardduty" + name = "org-guardduty" + + is_organization_admin = true + auto_enable_organization_members = true + + enable_sns_alerts = true + alert_email = "soc@example.com" +} +``` + +## Inputs + +| Name | Description | Type | Default | +|------|-------------|------|---------| +| name | Name prefix for resources | string | - | +| enable | Enable GuardDuty detector | bool | true | +| finding_publishing_frequency | Publishing frequency | string | "FIFTEEN_MINUTES" | +| enable_s3_protection | S3 data events monitoring | bool | true | +| enable_kubernetes_audit | EKS audit logs | bool | true | +| enable_malware_protection | EC2/EBS malware scanning | bool | true | +| enable_rds_login_events | RDS login monitoring | bool | true | +| enable_lambda_network_logs | Lambda network activity | bool | true | +| enable_runtime_monitoring | Runtime monitoring ($$) | bool | false | +| enable_sns_alerts | Enable SNS alerts | bool | false | +| alert_email | Email for alerts | string | "" | +| alert_sns_topic_arn | Existing SNS topic | string | "" | +| alert_severity_threshold | Min severity: LOW/MEDIUM/HIGH/CRITICAL | string | "MEDIUM" | +| enable_s3_export | Export findings to S3 | bool | false | +| export_s3_bucket | S3 bucket for export | string | "" | +| ipset_cidrs | Trusted IP CIDRs | list(string) | [] | +| threat_intel_feed_urls | Threat intel feed URLs | list(string) | [] | +| is_organization_admin | Delegated admin account | bool | false | + +## Outputs + +| Name | Description | +|------|-------------| +| detector_id | GuardDuty detector ID | +| detector_arn | GuardDuty detector ARN | +| sns_topic_arn | SNS topic for alerts | +| export_bucket | S3 bucket for findings | +| eventbridge_rule_arn | EventBridge rule ARN | +| enabled_features | Map of enabled features | + +## Severity Levels + +| Level | Numeric Range | Example Finding Types | +|-------|--------------|----------------------| +| LOW | 1.0 - 3.9 | Info gathering, unusual activity | +| MEDIUM | 4.0 - 6.9 | Potentially malicious activity | +| HIGH | 7.0 - 8.9 | Compromised resources, active threats | +| CRITICAL | 9.0+ | Confirmed breaches, exfiltration | + +## Cost Considerations + +- **Base**: Charged per GB of VPC Flow Logs, DNS logs, CloudTrail events +- **S3 Protection**: Per S3 event analyzed +- **EKS Audit Logs**: Per EKS audit log event +- **Malware Protection**: Per GB scanned +- **Runtime Monitoring**: Per vCPU-hour monitored +- **S3 Export**: Standard S3 storage costs + +See [GuardDuty Pricing](https://aws.amazon.com/guardduty/pricing/) for current rates. diff --git a/terraform/modules/guardduty/main.tf b/terraform/modules/guardduty/main.tf new file mode 100644 index 0000000..ee10f85 --- /dev/null +++ b/terraform/modules/guardduty/main.tf @@ -0,0 +1,643 @@ +################################################################################ +# GuardDuty Module +# +# Threat detection with alerting: +# - GuardDuty detector with all protection features +# - EventBridge rules for finding notifications +# - SNS alerts with severity filtering +# - S3 export for findings (optional) +# - IPSet / ThreatIntelSet integration (optional) +# - Lambda-based auto-remediation (optional) +# +# Usage: +# module "guardduty" { +# source = "../modules/guardduty" +# name = "main-detector" +# +# enable_sns_alerts = true +# alert_email = "security@example.com" +# +# # Only alert on HIGH and CRITICAL findings +# alert_severity_threshold = "HIGH" +# } +################################################################################ + +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} + +################################################################################ +# Variables +################################################################################ + +variable "name" { + type = string + description = "Name prefix for GuardDuty resources" +} + +variable "enable" { + type = bool + default = true + description = "Enable GuardDuty detector" +} + +variable "finding_publishing_frequency" { + type = string + default = "FIFTEEN_MINUTES" + description = "Finding publishing frequency" + validation { + condition = contains(["FIFTEEN_MINUTES", "ONE_HOUR", "SIX_HOURS"], var.finding_publishing_frequency) + error_message = "Must be FIFTEEN_MINUTES, ONE_HOUR, or SIX_HOURS." + } +} + +# Protection Features +variable "enable_s3_protection" { + type = bool + default = true + description = "Enable S3 data events monitoring" +} + +variable "enable_kubernetes_audit" { + type = bool + default = true + description = "Enable EKS Kubernetes audit logs" +} + +variable "enable_malware_protection" { + type = bool + default = true + description = "Enable malware protection for EC2/EBS" +} + +variable "enable_rds_login_events" { + type = bool + default = true + description = "Enable RDS login activity monitoring" +} + +variable "enable_lambda_network_logs" { + type = bool + default = true + description = "Enable Lambda network activity monitoring" +} + +variable "enable_runtime_monitoring" { + type = bool + default = false + description = "Enable runtime monitoring for EC2/ECS/EKS (additional cost)" +} + +# SNS Alerting +variable "enable_sns_alerts" { + type = bool + default = false + description = "Enable SNS alerts for findings" +} + +variable "alert_email" { + type = string + default = "" + description = "Email address for finding alerts (creates subscription)" +} + +variable "alert_sns_topic_arn" { + type = string + default = "" + description = "Existing SNS topic ARN (created if empty and alerts enabled)" +} + +variable "alert_severity_threshold" { + type = string + default = "MEDIUM" + description = "Minimum severity for alerts: LOW, MEDIUM, HIGH, CRITICAL" + validation { + condition = contains(["LOW", "MEDIUM", "HIGH", "CRITICAL"], var.alert_severity_threshold) + error_message = "Must be LOW, MEDIUM, HIGH, or CRITICAL." + } +} + +# S3 Export +variable "enable_s3_export" { + type = bool + default = false + description = "Export findings to S3 bucket" +} + +variable "export_s3_bucket" { + type = string + default = "" + description = "S3 bucket for findings export (created if empty and export enabled)" +} + +variable "export_kms_key_arn" { + type = string + default = "" + description = "KMS key for findings encryption" +} + +# Threat Intelligence +variable "ipset_cidrs" { + type = list(string) + default = [] + description = "Trusted IP CIDRs to whitelist from findings" +} + +variable "threat_intel_feed_urls" { + type = list(string) + default = [] + description = "URLs of threat intel feeds (must be accessible)" +} + +# Organization +variable "is_organization_admin" { + type = bool + default = false + description = "This account is the delegated admin for GuardDuty" +} + +variable "auto_enable_organization_members" { + type = bool + default = true + description = "Auto-enable GuardDuty for new org accounts" +} + +variable "tags" { + type = map(string) + default = {} + description = "Resource tags" +} + +################################################################################ +# Data Sources +################################################################################ + +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +locals { + # Severity numeric mapping for EventBridge filter + severity_map = { + LOW = 1.0 + MEDIUM = 4.0 + HIGH = 7.0 + CRITICAL = 8.0 + } + severity_threshold = local.severity_map[var.alert_severity_threshold] + + create_sns_topic = var.enable_sns_alerts && var.alert_sns_topic_arn == "" + sns_topic_arn = local.create_sns_topic ? aws_sns_topic.alerts[0].arn : var.alert_sns_topic_arn + + create_export_bucket = var.enable_s3_export && var.export_s3_bucket == "" + export_bucket_name = local.create_export_bucket ? aws_s3_bucket.export[0].id : var.export_s3_bucket +} + +################################################################################ +# GuardDuty Detector +################################################################################ + +resource "aws_guardduty_detector" "main" { + count = var.enable ? 1 : 0 + + enable = true + finding_publishing_frequency = var.finding_publishing_frequency + + datasources { + s3_logs { + enable = var.enable_s3_protection + } + kubernetes { + audit_logs { + enable = var.enable_kubernetes_audit + } + } + malware_protection { + scan_ec2_instance_with_findings { + ebs_volumes { + enable = var.enable_malware_protection + } + } + } + } + + tags = merge(var.tags, { Name = var.name }) +} + +# Additional feature configurations (added in AWS provider 5.x) +resource "aws_guardduty_detector_feature" "rds_login" { + count = var.enable && var.enable_rds_login_events ? 1 : 0 + + detector_id = aws_guardduty_detector.main[0].id + name = "RDS_LOGIN_EVENTS" + status = "ENABLED" +} + +resource "aws_guardduty_detector_feature" "lambda_network" { + count = var.enable && var.enable_lambda_network_logs ? 1 : 0 + + detector_id = aws_guardduty_detector.main[0].id + name = "LAMBDA_NETWORK_LOGS" + status = "ENABLED" +} + +resource "aws_guardduty_detector_feature" "runtime_monitoring" { + count = var.enable && var.enable_runtime_monitoring ? 1 : 0 + + detector_id = aws_guardduty_detector.main[0].id + name = "RUNTIME_MONITORING" + status = "ENABLED" + + additional_configuration { + name = "EKS_ADDON_MANAGEMENT" + status = "ENABLED" + } + additional_configuration { + name = "ECS_FARGATE_AGENT_MANAGEMENT" + status = "ENABLED" + } +} + +################################################################################ +# SNS Topic for Alerts +################################################################################ + +resource "aws_sns_topic" "alerts" { + count = local.create_sns_topic ? 1 : 0 + + name = "${var.name}-guardduty-alerts" + kms_master_key_id = "alias/aws/sns" + + tags = merge(var.tags, { Name = "${var.name}-guardduty-alerts" }) +} + +resource "aws_sns_topic_policy" "alerts" { + count = local.create_sns_topic ? 1 : 0 + + arn = aws_sns_topic.alerts[0].arn + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowEventBridge" + Effect = "Allow" + Principal = { + Service = "events.amazonaws.com" + } + Action = "sns:Publish" + Resource = aws_sns_topic.alerts[0].arn + } + ] + }) +} + +resource "aws_sns_topic_subscription" "email" { + count = var.enable_sns_alerts && var.alert_email != "" ? 1 : 0 + + topic_arn = local.sns_topic_arn + protocol = "email" + endpoint = var.alert_email +} + +################################################################################ +# EventBridge Rule for Finding Alerts +################################################################################ + +resource "aws_cloudwatch_event_rule" "findings" { + count = var.enable && var.enable_sns_alerts ? 1 : 0 + + name = "${var.name}-guardduty-findings" + description = "Route GuardDuty findings to SNS" + + event_pattern = jsonencode({ + source = ["aws.guardduty"] + detail-type = ["GuardDuty Finding"] + detail = { + severity = [ + { numeric = [">=", local.severity_threshold] } + ] + } + }) + + tags = merge(var.tags, { Name = "${var.name}-guardduty-findings" }) +} + +resource "aws_cloudwatch_event_target" "sns" { + count = var.enable && var.enable_sns_alerts ? 1 : 0 + + rule = aws_cloudwatch_event_rule.findings[0].name + target_id = "sns" + arn = local.sns_topic_arn + + input_transformer { + input_paths = { + severity = "$.detail.severity" + region = "$.detail.region" + type = "$.detail.type" + title = "$.detail.title" + description = "$.detail.description" + accountId = "$.detail.accountId" + findingId = "$.detail.id" + } + input_template = <", + "message": "Severity: \nRegion: \nAccount: \n\nTitle: \n\nDescription: <description>\n\nFinding ID: <findingId>\n\nView in console: https://<region>.console.aws.amazon.com/guardduty/home?region=<region>#/findings" +} +EOF + } +} + +################################################################################ +# S3 Export +################################################################################ + +resource "aws_s3_bucket" "export" { + count = local.create_export_bucket ? 1 : 0 + + bucket = "${var.name}-guardduty-findings-${data.aws_caller_identity.current.account_id}" + force_destroy = false + + tags = merge(var.tags, { Name = "${var.name}-guardduty-findings" }) +} + +resource "aws_s3_bucket_versioning" "export" { + count = local.create_export_bucket ? 1 : 0 + + bucket = aws_s3_bucket.export[0].id + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "export" { + count = local.create_export_bucket ? 1 : 0 + + bucket = aws_s3_bucket.export[0].id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = var.export_kms_key_arn != "" ? "aws:kms" : "AES256" + kms_master_key_id = var.export_kms_key_arn != "" ? var.export_kms_key_arn : null + } + bucket_key_enabled = var.export_kms_key_arn != "" ? true : false + } +} + +resource "aws_s3_bucket_public_access_block" "export" { + count = local.create_export_bucket ? 1 : 0 + + bucket = aws_s3_bucket.export[0].id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_lifecycle_configuration" "export" { + count = local.create_export_bucket ? 1 : 0 + + bucket = aws_s3_bucket.export[0].id + + rule { + id = "archive-findings" + status = "Enabled" + + transition { + days = 90 + storage_class = "STANDARD_IA" + } + + transition { + days = 365 + storage_class = "GLACIER" + } + + expiration { + days = 2555 # 7 years for compliance + } + } +} + +resource "aws_s3_bucket_policy" "export" { + count = var.enable_s3_export ? 1 : 0 + + bucket = local.export_bucket_name + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowGuardDutyExport" + Effect = "Allow" + Principal = { + Service = "guardduty.amazonaws.com" + } + Action = "s3:PutObject" + Resource = "arn:aws:s3:::${local.export_bucket_name}/*" + Condition = { + StringEquals = { + "aws:SourceAccount" = data.aws_caller_identity.current.account_id + } + } + }, + { + Sid = "AllowGuardDutyBucketRead" + Effect = "Allow" + Principal = { + Service = "guardduty.amazonaws.com" + } + Action = "s3:GetBucketLocation" + Resource = "arn:aws:s3:::${local.export_bucket_name}" + Condition = { + StringEquals = { + "aws:SourceAccount" = data.aws_caller_identity.current.account_id + } + } + } + ] + }) +} + +# KMS key for GuardDuty export (required) +resource "aws_kms_key" "export" { + count = var.enable && var.enable_s3_export && var.export_kms_key_arn == "" ? 1 : 0 + + description = "${var.name}-guardduty-export" + deletion_window_in_days = 7 + enable_key_rotation = true + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowRoot" + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + } + Action = "kms:*" + Resource = "*" + }, + { + Sid = "AllowGuardDuty" + Effect = "Allow" + Principal = { + Service = "guardduty.amazonaws.com" + } + Action = "kms:GenerateDataKey*" + Resource = "*" + Condition = { + StringEquals = { + "aws:SourceAccount" = data.aws_caller_identity.current.account_id + } + } + } + ] + }) + + tags = merge(var.tags, { Name = "${var.name}-guardduty-export" }) +} + +resource "aws_kms_alias" "export" { + count = var.enable && var.enable_s3_export && var.export_kms_key_arn == "" ? 1 : 0 + + name = "alias/${var.name}-guardduty-export" + target_key_id = aws_kms_key.export[0].key_id +} + +locals { + export_kms_key_arn = var.export_kms_key_arn != "" ? var.export_kms_key_arn : ( + var.enable_s3_export ? aws_kms_key.export[0].arn : "" + ) +} + +resource "aws_guardduty_publishing_destination" "s3" { + count = var.enable && var.enable_s3_export ? 1 : 0 + + detector_id = aws_guardduty_detector.main[0].id + destination_arn = "arn:aws:s3:::${local.export_bucket_name}" + destination_type = "S3" + kms_key_arn = local.export_kms_key_arn + + depends_on = [aws_s3_bucket_policy.export, aws_kms_key.export] +} + +################################################################################ +# IP Set (Trusted IPs) +################################################################################ + +resource "aws_s3_object" "ipset" { + count = var.enable && length(var.ipset_cidrs) > 0 ? 1 : 0 + + bucket = local.create_export_bucket ? aws_s3_bucket.export[0].id : var.export_s3_bucket + key = "guardduty-ipset.txt" + content = join("\n", var.ipset_cidrs) +} + +resource "aws_guardduty_ipset" "trusted" { + count = var.enable && length(var.ipset_cidrs) > 0 ? 1 : 0 + + activate = true + detector_id = aws_guardduty_detector.main[0].id + format = "TXT" + location = "s3://${aws_s3_object.ipset[0].bucket}/${aws_s3_object.ipset[0].key}" + name = "${var.name}-trusted-ips" + + tags = merge(var.tags, { Name = "${var.name}-trusted-ips" }) +} + +################################################################################ +# Threat Intel Set +################################################################################ + +resource "aws_guardduty_threatintelset" "feeds" { + for_each = var.enable ? toset(var.threat_intel_feed_urls) : [] + + activate = true + detector_id = aws_guardduty_detector.main[0].id + format = "TXT" + location = each.value + name = "${var.name}-threat-intel-${md5(each.value)}" + + tags = merge(var.tags, { Name = "${var.name}-threat-intel" }) +} + +################################################################################ +# Organization Configuration (Delegated Admin) +################################################################################ + +resource "aws_guardduty_organization_configuration" "main" { + count = var.enable && var.is_organization_admin ? 1 : 0 + + auto_enable_organization_members = var.auto_enable_organization_members ? "ALL" : "NONE" + detector_id = aws_guardduty_detector.main[0].id + + datasources { + s3_logs { + auto_enable = var.enable_s3_protection + } + kubernetes { + audit_logs { + enable = var.enable_kubernetes_audit + } + } + malware_protection { + scan_ec2_instance_with_findings { + ebs_volumes { + auto_enable = var.enable_malware_protection + } + } + } + } +} + +################################################################################ +# Outputs +################################################################################ + +output "detector_id" { + value = var.enable ? aws_guardduty_detector.main[0].id : null + description = "GuardDuty detector ID" +} + +output "detector_arn" { + value = var.enable ? aws_guardduty_detector.main[0].arn : null + description = "GuardDuty detector ARN" +} + +output "sns_topic_arn" { + value = var.enable_sns_alerts ? local.sns_topic_arn : null + description = "SNS topic ARN for alerts" +} + +output "export_bucket" { + value = var.enable_s3_export ? local.export_bucket_name : null + description = "S3 bucket for findings export" +} + +output "eventbridge_rule_arn" { + value = var.enable && var.enable_sns_alerts ? aws_cloudwatch_event_rule.findings[0].arn : null + description = "EventBridge rule ARN for findings" +} + +output "enabled_features" { + value = var.enable ? { + s3_protection = var.enable_s3_protection + kubernetes_audit = var.enable_kubernetes_audit + malware_protection = var.enable_malware_protection + rds_login_events = var.enable_rds_login_events + lambda_network_logs = var.enable_lambda_network_logs + runtime_monitoring = var.enable_runtime_monitoring + sns_alerts = var.enable_sns_alerts + s3_export = var.enable_s3_export + alert_threshold = var.alert_severity_threshold + } : null + description = "Enabled GuardDuty features" +} diff --git a/terraform/modules/security-hub/README.md b/terraform/modules/security-hub/README.md new file mode 100644 index 0000000..c3fd0b7 --- /dev/null +++ b/terraform/modules/security-hub/README.md @@ -0,0 +1,190 @@ +# Security Hub Module + +AWS Security Hub for centralized security posture management with alerting and cross-region aggregation. + +## Features + +- **Multiple Standards**: AWS Foundational, CIS v1.4/v3.0, PCI DSS, NIST 800-53 +- **SNS Alerts**: EventBridge-based alerts with severity filtering +- **Cross-Region Aggregation**: Aggregate findings across regions +- **Custom Actions**: Define remediation workflow triggers +- **Built-in Insights**: Pre-configured finding queries +- **Product Integrations**: Inspector, Macie, Detective + +## Usage + +### Basic + +```hcl +module "security_hub" { + source = "../modules/security-hub" + name = "main" + + enable_aws_foundational = true +} +``` + +### Compliance-Focused + +```hcl +module "security_hub" { + source = "../modules/security-hub" + name = "compliance" + + # Standards + enable_aws_foundational = true + enable_cis_benchmark = true + enable_pci_dss = true + enable_nist_800_53 = true + + # Disable noisy controls + disabled_controls = [ + "EC2.19", # Default security group + "IAM.6", # MFA hardware + ] + + # Alerting + enable_sns_alerts = true + alert_email = "security@example.com" + alert_severity = ["CRITICAL", "HIGH"] + + tags = { + Environment = "production" + } +} +``` + +### Cross-Region Aggregator + +```hcl +# Deploy in your primary region (e.g., us-east-1) +module "security_hub" { + source = "../modules/security-hub" + name = "aggregator" + + enable_finding_aggregator = true + aggregation_regions = [] # All regions + + enable_sns_alerts = true + alert_email = "soc@example.com" +} +``` + +### Organization Admin + +```hcl +module "security_hub" { + source = "../modules/security-hub" + name = "org-hub" + + is_organization_admin = true + auto_enable_organization_members = true + + enable_aws_foundational = true + enable_cis_benchmark = true + + enable_sns_alerts = true + alert_email = "security@example.com" +} +``` + +### With Custom Actions + +```hcl +module "security_hub" { + source = "../modules/security-hub" + name = "main" + + custom_actions = [ + { + name = "NotifySlack" + identifier = "NotifySlack" + description = "Send finding to Slack" + }, + { + name = "CreateJiraTicket" + identifier = "CreateJira" + description = "Create Jira ticket for finding" + } + ] +} +``` + +## Inputs + +| Name | Description | Type | Default | +|------|-------------|------|---------| +| name | Name prefix for resources | string | - | +| enable | Enable Security Hub | bool | true | +| auto_enable_controls | Auto-enable new controls | bool | true | +| control_finding_generator | SECURITY_CONTROL or STANDARD_CONTROL | string | "SECURITY_CONTROL" | +| enable_aws_foundational | AWS Foundational Best Practices | bool | true | +| enable_cis_benchmark | CIS Benchmark v1.4 | bool | false | +| enable_cis_benchmark_v3 | CIS Benchmark v3.0 | bool | false | +| enable_pci_dss | PCI DSS v3.2.1 | bool | false | +| enable_nist_800_53 | NIST 800-53 Rev. 5 | bool | false | +| disabled_controls | Control IDs to disable | list(string) | [] | +| enable_sns_alerts | Enable SNS alerts | bool | false | +| alert_email | Email for alerts | string | "" | +| alert_severity | Severities to alert | list(string) | ["CRITICAL", "HIGH"] | +| enable_finding_aggregator | Cross-region aggregation | bool | false | +| aggregation_regions | Regions to aggregate | list(string) | [] | +| is_organization_admin | Org admin account | bool | false | +| custom_actions | Custom action definitions | list(object) | [] | +| enable_inspector | Inspector integration | bool | false | +| enable_macie | Macie integration | bool | false | + +## Outputs + +| Name | Description | +|------|-------------| +| hub_arn | Security Hub account ARN | +| sns_topic_arn | SNS topic for alerts | +| enabled_standards | List of enabled standards | +| finding_aggregator_arn | Aggregator ARN | +| custom_action_arns | Map of custom action ARNs | +| insight_arns | Map of insight ARNs | + +## Built-in Insights + +The module creates these pre-configured insights: + +1. **Critical Findings** - All critical findings grouped by resource type +2. **Failed Resources** - Resources with compliance failures +3. **Findings by Account** - Finding counts per AWS account + +## Severity Levels + +| Level | Description | +|-------|-------------| +| CRITICAL | Requires immediate action | +| HIGH | High-priority security issue | +| MEDIUM | Moderate security concern | +| LOW | Minor security issue | +| INFORMATIONAL | No security impact | + +## Custom Actions Workflow + +1. Define custom action in Terraform +2. Create EventBridge rule targeting the action +3. Route to Lambda/Step Functions for remediation + +```hcl +resource "aws_cloudwatch_event_rule" "custom_action" { + name = "securityhub-notify-slack" + + event_pattern = jsonencode({ + source = ["aws.securityhub"] + detail-type = ["Security Hub Findings - Custom Action"] + resources = [module.security_hub.custom_action_arns["NotifySlack"]] + }) +} +``` + +## Cost Considerations + +- **Base**: Per finding ingested +- **Standards**: No additional cost beyond base +- **Aggregation**: Cross-region data transfer costs + +See [Security Hub Pricing](https://aws.amazon.com/security-hub/pricing/) for current rates. diff --git a/terraform/modules/security-hub/main.tf b/terraform/modules/security-hub/main.tf new file mode 100644 index 0000000..e9e67d7 --- /dev/null +++ b/terraform/modules/security-hub/main.tf @@ -0,0 +1,552 @@ +################################################################################ +# Security Hub Module +# +# Centralized security posture management: +# - Security Hub with standards subscriptions +# - Finding aggregation (cross-region) +# - SNS alerts for critical findings +# - Custom actions for remediation workflows +# - Product integrations +# - Insight configuration +# +# Usage: +# module "security_hub" { +# source = "../modules/security-hub" +# name = "main" +# +# enable_cis_benchmark = true +# enable_aws_foundational = true +# enable_pci_dss = true +# +# enable_sns_alerts = true +# alert_email = "security@example.com" +# } +################################################################################ + +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} + +################################################################################ +# Variables +################################################################################ + +variable "name" { + type = string + description = "Name prefix for Security Hub resources" +} + +variable "enable" { + type = bool + default = true + description = "Enable Security Hub" +} + +variable "auto_enable_controls" { + type = bool + default = true + description = "Auto-enable new controls in standards" +} + +variable "control_finding_generator" { + type = string + default = "SECURITY_CONTROL" + description = "Control finding generator: SECURITY_CONTROL or STANDARD_CONTROL" + validation { + condition = contains(["SECURITY_CONTROL", "STANDARD_CONTROL"], var.control_finding_generator) + error_message = "Must be SECURITY_CONTROL or STANDARD_CONTROL." + } +} + +# Standards +variable "enable_aws_foundational" { + type = bool + default = true + description = "Enable AWS Foundational Security Best Practices" +} + +variable "enable_cis_benchmark" { + type = bool + default = false + description = "Enable CIS AWS Foundations Benchmark v1.4" +} + +variable "enable_cis_benchmark_v3" { + type = bool + default = false + description = "Enable CIS AWS Foundations Benchmark v3.0" +} + +variable "enable_pci_dss" { + type = bool + default = false + description = "Enable PCI DSS v3.2.1" +} + +variable "enable_nist_800_53" { + type = bool + default = false + description = "Enable NIST 800-53 Rev. 5" +} + +# Disabled Controls +variable "disabled_controls" { + type = list(string) + default = [] + description = "Control IDs to disable (e.g., 'EC2.19', 'IAM.6')" +} + +# SNS Alerting +variable "enable_sns_alerts" { + type = bool + default = false + description = "Enable SNS alerts for findings" +} + +variable "alert_email" { + type = string + default = "" + description = "Email for finding alerts" +} + +variable "alert_sns_topic_arn" { + type = string + default = "" + description = "Existing SNS topic ARN (created if empty)" +} + +variable "alert_severity" { + type = list(string) + default = ["CRITICAL", "HIGH"] + description = "Severities to alert on" +} + +# Cross-Region Aggregation +variable "enable_finding_aggregator" { + type = bool + default = false + description = "Enable cross-region finding aggregation (run in aggregation region)" +} + +variable "aggregation_regions" { + type = list(string) + default = [] + description = "Regions to aggregate (empty = all linked regions)" +} + +# Organization +variable "is_organization_admin" { + type = bool + default = false + description = "This account is the delegated admin" +} + +variable "auto_enable_organization_members" { + type = bool + default = true + description = "Auto-enable Security Hub for new org accounts" +} + +# Custom Actions +variable "custom_actions" { + type = list(object({ + name = string + description = string + identifier = string + })) + default = [] + description = "Custom actions for finding workflows" +} + +# Product Integrations +variable "enable_inspector" { + type = bool + default = false + description = "Enable Amazon Inspector integration" +} + +variable "enable_macie" { + type = bool + default = false + description = "Enable Amazon Macie integration" +} + +variable "enable_detective" { + type = bool + default = false + description = "Enable Amazon Detective integration" +} + +variable "tags" { + type = map(string) + default = {} + description = "Resource tags" +} + +################################################################################ +# Data Sources +################################################################################ + +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +locals { + create_sns_topic = var.enable_sns_alerts && var.alert_sns_topic_arn == "" + sns_topic_arn = local.create_sns_topic ? aws_sns_topic.alerts[0].arn : var.alert_sns_topic_arn + + # Standard ARNs + standards = { + aws_foundational = "arn:aws:securityhub:${data.aws_region.current.id}::standards/aws-foundational-security-best-practices/v/1.0.0" + cis_benchmark = "arn:aws:securityhub:${data.aws_region.current.id}::standards/cis-aws-foundations-benchmark/v/1.4.0" + cis_benchmark_v3 = "arn:aws:securityhub:${data.aws_region.current.id}::standards/cis-aws-foundations-benchmark/v/3.0.0" + pci_dss = "arn:aws:securityhub:${data.aws_region.current.id}::standards/pci-dss/v/3.2.1" + nist_800_53 = "arn:aws:securityhub:${data.aws_region.current.id}::standards/nist-800-53/v/5.0.0" + } + + enabled_standards = compact([ + var.enable_aws_foundational ? local.standards.aws_foundational : "", + var.enable_cis_benchmark ? local.standards.cis_benchmark : "", + var.enable_cis_benchmark_v3 ? local.standards.cis_benchmark_v3 : "", + var.enable_pci_dss ? local.standards.pci_dss : "", + var.enable_nist_800_53 ? local.standards.nist_800_53 : "", + ]) +} + +################################################################################ +# Security Hub Account +################################################################################ + +resource "aws_securityhub_account" "main" { + count = var.enable ? 1 : 0 + + enable_default_standards = false + auto_enable_controls = var.auto_enable_controls + control_finding_generator = var.control_finding_generator +} + +################################################################################ +# Standards Subscriptions +################################################################################ + +resource "aws_securityhub_standards_subscription" "standards" { + for_each = var.enable ? toset(local.enabled_standards) : [] + + standards_arn = each.value + + depends_on = [aws_securityhub_account.main] +} + +################################################################################ +# Disabled Controls +################################################################################ + +resource "aws_securityhub_standards_control" "disabled" { + for_each = var.enable ? toset(var.disabled_controls) : [] + + standards_control_arn = "arn:aws:securityhub:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:control/${each.value}" + control_status = "DISABLED" + disabled_reason = "Disabled via Terraform" + + depends_on = [aws_securityhub_standards_subscription.standards] +} + +################################################################################ +# SNS Topic for Alerts +################################################################################ + +resource "aws_sns_topic" "alerts" { + count = local.create_sns_topic ? 1 : 0 + + name = "${var.name}-securityhub-alerts" + kms_master_key_id = "alias/aws/sns" + + tags = merge(var.tags, { Name = "${var.name}-securityhub-alerts" }) +} + +resource "aws_sns_topic_policy" "alerts" { + count = local.create_sns_topic ? 1 : 0 + + arn = aws_sns_topic.alerts[0].arn + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowEventBridge" + Effect = "Allow" + Principal = { + Service = "events.amazonaws.com" + } + Action = "sns:Publish" + Resource = aws_sns_topic.alerts[0].arn + } + ] + }) +} + +resource "aws_sns_topic_subscription" "email" { + count = var.enable_sns_alerts && var.alert_email != "" ? 1 : 0 + + topic_arn = local.sns_topic_arn + protocol = "email" + endpoint = var.alert_email +} + +################################################################################ +# EventBridge Rule for Finding Alerts +################################################################################ + +resource "aws_cloudwatch_event_rule" "findings" { + count = var.enable && var.enable_sns_alerts ? 1 : 0 + + name = "${var.name}-securityhub-findings" + description = "Route Security Hub findings to SNS" + + event_pattern = jsonencode({ + source = ["aws.securityhub"] + detail-type = ["Security Hub Findings - Imported"] + detail = { + findings = { + Severity = { + Label = var.alert_severity + } + Workflow = { + Status = ["NEW"] + } + RecordState = ["ACTIVE"] + } + } + }) + + tags = merge(var.tags, { Name = "${var.name}-securityhub-findings" }) +} + +resource "aws_cloudwatch_event_target" "sns" { + count = var.enable && var.enable_sns_alerts ? 1 : 0 + + rule = aws_cloudwatch_event_rule.findings[0].name + target_id = "sns" + arn = local.sns_topic_arn + + input_transformer { + input_paths = { + severity = "$.detail.findings[0].Severity.Label" + title = "$.detail.findings[0].Title" + description = "$.detail.findings[0].Description" + accountId = "$.detail.findings[0].AwsAccountId" + region = "$.detail.findings[0].Region" + resourceId = "$.detail.findings[0].Resources[0].Id" + standard = "$.detail.findings[0].GeneratorId" + } + input_template = <<EOF +{ + "subject": "Security Hub [<severity>]: <title>", + "message": "Severity: <severity>\nAccount: <accountId>\nRegion: <region>\n\nTitle: <title>\n\nDescription: <description>\n\nResource: <resourceId>\n\nStandard: <standard>\n\nView in console: https://<region>.console.aws.amazon.com/securityhub/home?region=<region>#/findings" +} +EOF + } +} + +################################################################################ +# Finding Aggregator (Cross-Region) +################################################################################ + +resource "aws_securityhub_finding_aggregator" "main" { + count = var.enable && var.enable_finding_aggregator ? 1 : 0 + + linking_mode = length(var.aggregation_regions) > 0 ? "SPECIFIED_REGIONS" : "ALL_REGIONS" + specified_regions = length(var.aggregation_regions) > 0 ? var.aggregation_regions : null + + depends_on = [aws_securityhub_account.main] +} + +################################################################################ +# Organization Configuration +################################################################################ + +resource "aws_securityhub_organization_configuration" "main" { + count = var.enable && var.is_organization_admin ? 1 : 0 + + auto_enable = var.auto_enable_organization_members + auto_enable_standards = var.auto_enable_organization_members ? "DEFAULT" : "NONE" + + depends_on = [aws_securityhub_account.main] +} + +resource "aws_securityhub_organization_admin_account" "main" { + count = var.enable && var.is_organization_admin ? 1 : 0 + + admin_account_id = data.aws_caller_identity.current.account_id + + depends_on = [aws_securityhub_account.main] +} + +################################################################################ +# Custom Actions +################################################################################ + +resource "aws_securityhub_action_target" "custom" { + for_each = var.enable ? { for a in var.custom_actions : a.identifier => a } : {} + + name = each.value.name + identifier = each.value.identifier + description = each.value.description + + depends_on = [aws_securityhub_account.main] +} + +################################################################################ +# Product Integrations +################################################################################ + +resource "aws_securityhub_product_subscription" "inspector" { + count = var.enable && var.enable_inspector ? 1 : 0 + + product_arn = "arn:aws:securityhub:${data.aws_region.current.id}::product/aws/inspector" + + depends_on = [aws_securityhub_account.main] +} + +resource "aws_securityhub_product_subscription" "macie" { + count = var.enable && var.enable_macie ? 1 : 0 + + product_arn = "arn:aws:securityhub:${data.aws_region.current.id}::product/aws/macie" + + depends_on = [aws_securityhub_account.main] +} + +################################################################################ +# Insights (Custom Finding Queries) +################################################################################ + +resource "aws_securityhub_insight" "critical_findings" { + count = var.enable ? 1 : 0 + + name = "${var.name}-critical-findings" + + filters { + severity_label { + comparison = "EQUALS" + value = "CRITICAL" + } + workflow_status { + comparison = "EQUALS" + value = "NEW" + } + record_state { + comparison = "EQUALS" + value = "ACTIVE" + } + } + + group_by_attribute = "ResourceType" + + depends_on = [aws_securityhub_account.main] +} + +resource "aws_securityhub_insight" "failed_resources" { + count = var.enable ? 1 : 0 + + name = "${var.name}-failed-resources" + + filters { + compliance_status { + comparison = "EQUALS" + value = "FAILED" + } + record_state { + comparison = "EQUALS" + value = "ACTIVE" + } + } + + group_by_attribute = "ResourceId" + + depends_on = [aws_securityhub_account.main] +} + +resource "aws_securityhub_insight" "findings_by_account" { + count = var.enable ? 1 : 0 + + name = "${var.name}-findings-by-account" + + filters { + severity_label { + comparison = "NOT_EQUALS" + value = "INFORMATIONAL" + } + workflow_status { + comparison = "EQUALS" + value = "NEW" + } + record_state { + comparison = "EQUALS" + value = "ACTIVE" + } + } + + group_by_attribute = "AwsAccountId" + + depends_on = [aws_securityhub_account.main] +} + +################################################################################ +# Outputs +################################################################################ + +output "hub_arn" { + value = var.enable ? aws_securityhub_account.main[0].arn : null + description = "Security Hub account ARN" +} + +output "sns_topic_arn" { + value = var.enable_sns_alerts ? local.sns_topic_arn : null + description = "SNS topic for alerts" +} + +output "enabled_standards" { + value = local.enabled_standards + description = "List of enabled standards ARNs" +} + +output "finding_aggregator_id" { + value = var.enable && var.enable_finding_aggregator ? aws_securityhub_finding_aggregator.main[0].id : null + description = "Finding aggregator ID" +} + +output "custom_action_arns" { + value = var.enable ? { + for k, v in aws_securityhub_action_target.custom : k => v.arn + } : {} + description = "Custom action ARNs" +} + +output "insight_arns" { + value = var.enable ? { + critical_findings = aws_securityhub_insight.critical_findings[0].arn + failed_resources = aws_securityhub_insight.failed_resources[0].arn + by_account = aws_securityhub_insight.findings_by_account[0].arn + } : null + description = "Security Hub insight ARNs" +} + +output "enabled_features" { + value = var.enable ? { + aws_foundational = var.enable_aws_foundational + cis_benchmark = var.enable_cis_benchmark + cis_benchmark_v3 = var.enable_cis_benchmark_v3 + pci_dss = var.enable_pci_dss + nist_800_53 = var.enable_nist_800_53 + sns_alerts = var.enable_sns_alerts + finding_aggregator = var.enable_finding_aggregator + inspector = var.enable_inspector + macie = var.enable_macie + detective = var.enable_detective + } : null + description = "Enabled Security Hub features" +}