Files
terraform-foundation/terraform/modules/security-hub/main.tf
Greg Hendrickson 7e8ef83390 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.
2026-02-06 20:05:03 +00:00

553 lines
16 KiB
HCL

################################################################################
# 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"
}