From f3769fbaeb4c6bffcb7ead0cab7c3e80ae3024d9 Mon Sep 17 00:00:00 2001 From: piravinth Date: Wed, 13 May 2026 17:09:14 +0100 Subject: [PATCH 1/3] intro SH module --- .../modules/security-hub/context.tf | 365 ++++++++++++++++++ infrastructure/modules/security-hub/main.tf | 117 ++++++ .../modules/security-hub/outputs.tf | 24 ++ infrastructure/modules/security-hub/readme.md | 89 +++++ .../modules/security-hub/variables.tf | 96 +++++ 5 files changed, 691 insertions(+) create mode 100644 infrastructure/modules/security-hub/context.tf create mode 100644 infrastructure/modules/security-hub/main.tf create mode 100644 infrastructure/modules/security-hub/outputs.tf create mode 100644 infrastructure/modules/security-hub/readme.md create mode 100644 infrastructure/modules/security-hub/variables.tf diff --git a/infrastructure/modules/security-hub/context.tf b/infrastructure/modules/security-hub/context.tf new file mode 100644 index 0000000..5eb0bc6 --- /dev/null +++ b/infrastructure/modules/security-hub/context.tf @@ -0,0 +1,365 @@ +# +# ONLY EDIT THIS FILE IN github.com/NHSDigital/screening-terraform-modules-aws/infrastructure/modules/tags +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/NHSDigital/screening-terraform-modules-aws/blob/master/infrastructure/modules/tags/exports/context.tf +# and then place it in your Terraform module to automatically get +# tag module standard configuration inputs suitable for passing +# to other modules. +# +# curl -sL https://raw.githubusercontent.com/NHSDigital/screening-terraform-modules-aws/master/infrastructure/modules/tags/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags?ref=feature/BCSS-23189-add-new-modules-to-suppport-bcss" + + service = var.service + project = var.project + region = var.region + environment = var.environment + stack = var.stack + workspace = var.workspace + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of screening-terraform-modules-aws/tags/variables.tf here +# tflint-ignore: terraform_unused_declarations +variable "aws_region" { + type = string + description = "The AWS region" + default = "eu-west-2" + validation { + condition = contains(["eu-west-1", "eu-west-2", "us-east-1"], var.aws_region) + error_message = "AWS Region must be one of eu-west-1, eu-west-2, us-east-1" + } +} + +variable "context" { + type = any + default = { + enabled = true + service = null + project = null + region = null + environment = null + stack = null + workspace = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "service" { + type = string + default = null + description = "ID element. Usually an abbreviation of your service directorate name, e.g. 'bcss' or 'csms', to help ensure generated IDs are globally unique" +} + +variable "region" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. Usually an abbreviation of the selected AWS region e.g. 'uw2', 'ew2' or 'gbl' for resources like IAM roles that have no region" +} + +variable "project" { + type = string + default = null + description = "ID element. A project identifier, indicating the name or role of the project the resource is for, such as `website` or `api`" +} +variable "stack" { + type = string + default = null + description = "ID element. The name of the stack/component, e.g. `database`, `web`, `waf`, `eks`" +} +variable "workspace" { + type = string + default = null + description = "ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces" +} +variable "environment" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +variable "owner" { + type = string + description = "The name and or NHS.net email address of the service owner" + default = "None" +} + +variable "tag_version" { + type = string + description = "Used to identify the tagging version in use" + default = "1.0" +} + +variable "data_classification" { + type = string + description = "Used to identify the data classification of the resource, e.g 1-5" + default = "n/a" + validation { + condition = contains(["n/a", "1", "2", "3", "4", "5"], var.data_classification) + error_message = "Data Classification must be \"n/a\" or between 1-5" + } +} + +variable "data_type" { + type = string + description = "The tag data_type" + default = "None" + validation { + condition = contains(["None", "PCD", "PID", "Anonymised", "UserAccount", "Audit"], var.data_type) + error_message = "Data Type must be one of None, PCD, PID, Anonymised, UserAccount, Audit" + } +} + + +variable "public_facing" { + type = bool + description = "Whether this resource is public facing" + default = false +} + +variable "service_category" { + type = string + description = "The tag service_category" + default = "n/a" + validation { + condition = contains(["n/a", "Bronze", "Silver", "Gold", "Platinum"], var.service_category) + error_message = "The Service Category must be one of n/a, Bronze, Silver, Gold, Platinum" + } +} +variable "on_off_pattern" { + type = string + description = "Used to turn resources on and off based on a time pattern" + default = "n/a" +} + +variable "application_role" { + type = string + description = "The role the application is performing" + default = "General" +} + +variable "tool" { + type = string + description = "The tool used to deploy the resource" + default = "Terraform" +} + +#### End of copy of screening-terraform-modules-aws/tags/variables.tf diff --git a/infrastructure/modules/security-hub/main.tf b/infrastructure/modules/security-hub/main.tf new file mode 100644 index 0000000..f199fef --- /dev/null +++ b/infrastructure/modules/security-hub/main.tf @@ -0,0 +1,117 @@ +################################################################ +# Security Hub +# +# Enables AWS Security Hub in the current account/region, +# subscribes to the requested standards, optionally enables +# cross-region finding aggregation, and (optionally) wires +# imported findings to an existing SNS topic via EventBridge. +# +# This pairs with the GuardDuty module: GuardDuty findings are +# automatically ingested by Security Hub once both services are +# enabled in the same account/region. +################################################################ + +data "aws_partition" "current" {} +data "aws_region" "current" {} + +################################################################ +# Account-level Security Hub enablement +################################################################ + +resource "aws_securityhub_account" "this" { + count = module.this.enabled ? 1 : 0 + + enable_default_standards = var.enable_default_standards + control_finding_generator = var.control_finding_generator + auto_enable_controls = var.auto_enable_controls +} + +################################################################ +# Standards subscriptions +################################################################ + +locals { + standards_arns = { + for s in var.enabled_standards : + s => startswith(s, "arn:") ? s : format( + "arn:%s:securityhub:%s::%s", + data.aws_partition.current.partition, + data.aws_region.current.name, + s, + ) + } +} + +resource "aws_securityhub_standards_subscription" "this" { + for_each = module.this.enabled ? local.standards_arns : {} + + standards_arn = each.value + + depends_on = [aws_securityhub_account.this] +} + +################################################################ +# Finding aggregator (cross-region aggregation) +################################################################ + +resource "aws_securityhub_finding_aggregator" "this" { + count = module.this.enabled && var.finding_aggregator_enabled ? 1 : 0 + + linking_mode = var.finding_aggregator_linking_mode + specified_regions = contains( + ["SPECIFIED_REGIONS", "ALL_REGIONS_EXCEPT_SPECIFIED"], + var.finding_aggregator_linking_mode, + ) ? var.finding_aggregator_regions : null + + depends_on = [aws_securityhub_account.this] + + lifecycle { + precondition { + condition = !contains( + ["SPECIFIED_REGIONS", "ALL_REGIONS_EXCEPT_SPECIFIED"], + var.finding_aggregator_linking_mode, + ) || length(var.finding_aggregator_regions) > 0 + error_message = "finding_aggregator_regions must be set when finding_aggregator_linking_mode is SPECIFIED_REGIONS or ALL_REGIONS_EXCEPT_SPECIFIED." + } + } +} + +################################################################ +# CloudWatch Event Rule -> SNS forwarding for imported findings +# +# The SNS topic itself is created by the separate alerting module. +# Pass the topic ARN via `findings_notification_arn` to wire +# imported Security Hub findings into it. +################################################################ + +# Sub-label for the imported-findings EventBridge rule so its +# name/tags are derived from the same context but disambiguated +# from the account-level resources. +module "imported_findings_label" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags?ref=feature/BCSS-23189-add-new-modules-to-suppport-bcss" + context = module.this.context + + attributes = concat(module.this.attributes, ["imported-findings"]) +} + +resource "aws_cloudwatch_event_rule" "imported_findings" { + count = module.this.enabled && var.enable_cloudwatch ? 1 : 0 + + name = module.imported_findings_label.id + description = "Forward Security Hub imported findings to SNS for alerting." + + event_pattern = jsonencode({ + source = ["aws.securityhub"] + detail-type = [var.cloudwatch_event_rule_pattern_detail_type] + }) + + tags = module.imported_findings_label.tags +} + +resource "aws_cloudwatch_event_target" "imported_findings" { + count = module.this.enabled && var.enable_cloudwatch && var.findings_notification_arn != null ? 1 : 0 + + rule = aws_cloudwatch_event_rule.imported_findings[0].name + target_id = module.imported_findings_label.id + arn = var.findings_notification_arn +} diff --git a/infrastructure/modules/security-hub/outputs.tf b/infrastructure/modules/security-hub/outputs.tf new file mode 100644 index 0000000..1084ba7 --- /dev/null +++ b/infrastructure/modules/security-hub/outputs.tf @@ -0,0 +1,24 @@ +output "account_id" { + description = "The AWS account ID that Security Hub has been enabled in." + value = try(aws_securityhub_account.this[0].id, null) +} + +output "account_arn" { + description = "The ARN of the Security Hub hub resource for this account." + value = try(aws_securityhub_account.this[0].arn, null) +} + +output "enabled_standards_subscriptions" { + description = "Map of subscribed Security Hub standards keyed by the input identifier, with the resulting subscription ARN as the value." + value = { for k, v in aws_securityhub_standards_subscription.this : k => v.id } +} + +output "finding_aggregator_arn" { + description = "ARN of the Security Hub finding aggregator, if created." + value = try(aws_securityhub_finding_aggregator.this[0].arn, null) +} + +output "cloudwatch_event_rule_arn" { + description = "ARN of the CloudWatch (EventBridge) rule forwarding Security Hub imported findings, if created." + value = try(aws_cloudwatch_event_rule.imported_findings[0].arn, null) +} diff --git a/infrastructure/modules/security-hub/readme.md b/infrastructure/modules/security-hub/readme.md new file mode 100644 index 0000000..3599246 --- /dev/null +++ b/infrastructure/modules/security-hub/readme.md @@ -0,0 +1,89 @@ +# Security Hub + + + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider_aws) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [imported_findings_label](#module_imported_findings_label) | `git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags` | feature/BCSS-23189-add-new-modules-to-suppport-bcss | +| [this](#module_this) | `git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags` | feature/BCSS-23189-add-new-modules-to-suppport-bcss | + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_event_rule.imported_findings](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | +| [aws_cloudwatch_event_target.imported_findings](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | +| [aws_securityhub_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_account) | resource | +| [aws_securityhub_finding_aggregator.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_finding_aggregator) | resource | +| [aws_securityhub_standards_subscription.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_standards_subscription) | resource | +| [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 | +|------|-------------|------|---------|:--------:| +| [additional_tag_map](#input_additional_tag_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. | `map(string)` | `{}` | no | +| [application_role](#input_application_role) | The role the application is performing | `string` | `"General"` | no | +| [attributes](#input_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, in the order they appear in the list. | `list(string)` | `[]` | no | +| [auto_enable_controls](#input_auto_enable_controls) | Whether new controls added to enabled standards are automatically enabled. | `bool` | `true` | no | +| [aws_region](#input_aws_region) | The AWS region | `string` | `"eu-west-2"` | no | +| [cloudwatch_event_rule_pattern_detail_type](#input_cloudwatch_event_rule_pattern_detail_type) | The detail-type pattern used to match Security Hub events for the CloudWatch rule. | `string` | `"Security Hub Findings - Imported"` | no | +| [context](#input_context) | Single object for setting entire context at once. See description of individual variables for details. | `any` | see `context.tf` | no | +| [control_finding_generator](#input_control_finding_generator) | How Security Hub generates findings for security checks. Valid values: SECURITY_CONTROL (consolidated, recommended) or STANDARD_CONTROL (one finding per standard). | `string` | `"SECURITY_CONTROL"` | no | +| [data_classification](#input_data_classification) | Used to identify the data classification of the resource, e.g 1-5 | `string` | `"n/a"` | no | +| [data_type](#input_data_type) | The tag data_type | `string` | `"None"` | no | +| [delimiter](#input_delimiter) | Delimiter to be used between ID elements. Defaults to `-` (hyphen). | `string` | `null` | no | +| [descriptor_formats](#input_descriptor_formats) | Describe additional descriptors to be output in the `descriptors` output map. | `any` | `{}` | no | +| [enable_cloudwatch](#input_enable_cloudwatch) | Create a CloudWatch (EventBridge) rule that forwards Security Hub imported findings. The SNS topic itself is created by the separate alerting module. | `bool` | `true` | no | +| [enable_default_standards](#input_enable_default_standards) | Whether to enable the AWS-recommended default standards (AWS Foundational Security Best Practices and CIS AWS Foundations Benchmark) when Security Hub is first enabled in this account/region. | `bool` | `true` | no | +| [enabled](#input_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [enabled_standards](#input_enabled_standards) | A list of Security Hub standards/rulesets to subscribe to. Values can be a short identifier or a full ARN. | `list(string)` | `[]` | no | +| [environment](#input_environment) | ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat' | `string` | `null` | no | +| [finding_aggregator_enabled](#input_finding_aggregator_enabled) | Whether to create a Security Hub finding aggregator to consolidate findings across regions. | `bool` | `false` | no | +| [finding_aggregator_linking_mode](#input_finding_aggregator_linking_mode) | Linking mode for the finding aggregator. One of: ALL_REGIONS, ALL_REGIONS_EXCEPT_SPECIFIED, SPECIFIED_REGIONS. | `string` | `"ALL_REGIONS"` | no | +| [finding_aggregator_regions](#input_finding_aggregator_regions) | List of regions used by the finding aggregator. Required when `finding_aggregator_linking_mode` is `SPECIFIED_REGIONS` or `ALL_REGIONS_EXCEPT_SPECIFIED`. | `list(string)` | `[]` | no | +| [findings_notification_arn](#input_findings_notification_arn) | ARN of an existing SNS topic that Security Hub imported findings should be forwarded to. Leave null to skip target wiring. | `string` | `null` | no | +| [id_length_limit](#input_id_length_limit) | Limit `id` to this many characters (minimum 6). | `number` | `null` | no | +| [label_key_case](#input_label_key_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module. | `string` | `null` | no | +| [label_order](#input_label_order) | The order in which the labels (ID elements) appear in the `id`. | `list(string)` | `null` | no | +| [label_value_case](#input_label_value_case) | Controls the letter case of ID elements (labels) as included in `id`, set as tag values, and output by this module individually. | `string` | `null` | no | +| [labels_as_tags](#input_labels_as_tags) | Set of labels (ID elements) to include as tags in the `tags` output. | `set(string)` | `["default"]` | no | +| [name](#input_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. | `string` | `null` | no | +| [on_off_pattern](#input_on_off_pattern) | Used to turn resources on and off based on a time pattern | `string` | `"n/a"` | no | +| [owner](#input_owner) | The name and or NHS.net email address of the service owner | `string` | `"None"` | no | +| [project](#input_project) | ID element. A project identifier, indicating the name or role of the project the resource is for. | `string` | `null` | no | +| [public_facing](#input_public_facing) | Whether this resource is public facing | `bool` | `false` | no | +| [regex_replace_chars](#input_regex_replace_chars) | Terraform regular expression (regex) string. Characters matching the regex will be removed from the ID elements. | `string` | `null` | no | +| [region](#input_region) | ID element. Short region abbreviation e.g. 'uw2', 'ew2'. | `string` | `null` | no | +| [service](#input_service) | ID element. Service directorate abbreviation, e.g. 'bcss'. | `string` | `null` | no | +| [service_category](#input_service_category) | The tag service_category | `string` | `"n/a"` | no | +| [stack](#input_stack) | ID element. The name of the stack/component, e.g. `database`, `web`, `waf`, `eks`. | `string` | `null` | no | +| [tag_version](#input_tag_version) | Used to identify the tagging version in use | `string` | `"1.0"` | no | +| [tags](#input_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). | `map(string)` | `{}` | no | +| [tool](#input_tool) | The tool used to deploy the resource | `string` | `"Terraform"` | no | +| [workspace](#input_workspace) | ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces. | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [account_arn](#output_account_arn) | The ARN of the Security Hub hub resource for this account. | +| [account_id](#output_account_id) | The AWS account ID that Security Hub has been enabled in. | +| [cloudwatch_event_rule_arn](#output_cloudwatch_event_rule_arn) | ARN of the CloudWatch (EventBridge) rule forwarding Security Hub imported findings, if created. | +| [enabled_standards_subscriptions](#output_enabled_standards_subscriptions) | Map of subscribed Security Hub standards keyed by the input identifier, with the resulting subscription ARN as the value. | +| [finding_aggregator_arn](#output_finding_aggregator_arn) | ARN of the Security Hub finding aggregator, if created. | + + diff --git a/infrastructure/modules/security-hub/variables.tf b/infrastructure/modules/security-hub/variables.tf new file mode 100644 index 0000000..3e23bb1 --- /dev/null +++ b/infrastructure/modules/security-hub/variables.tf @@ -0,0 +1,96 @@ +################################################################ +# Security Hub-specific inputs. +# +# Naming, tagging and the master `enabled` switch come from +# `context.tf` via `module.this` — see that file for the full +# list of inherited inputs (service, project, environment, +# stack, name, owner, data_classification, tags, etc.). +################################################################ + +variable "enable_default_standards" { + description = "Whether to enable the AWS-recommended default standards (AWS Foundational Security Best Practices and CIS AWS Foundations Benchmark) when Security Hub is first enabled in this account/region." + type = bool + default = true +} + +variable "control_finding_generator" { + description = "How Security Hub generates findings for security checks. Valid values: SECURITY_CONTROL (consolidated, recommended) or STANDARD_CONTROL (one finding per standard)." + type = string + default = "SECURITY_CONTROL" + + validation { + condition = contains(["SECURITY_CONTROL", "STANDARD_CONTROL"], var.control_finding_generator) + error_message = "control_finding_generator must be one of SECURITY_CONTROL or STANDARD_CONTROL." + } +} + +variable "auto_enable_controls" { + description = "Whether new controls added to enabled standards are automatically enabled." + type = bool + default = true +} + +################################################################ +# Standards +################################################################ + +variable "enabled_standards" { + description = <<-EOT + A list of Security Hub standards/rulesets to subscribe to. Values can be a + short identifier (e.g. `standards/aws-foundational-security-best-practices/v/1.0.0`) + or a full ARN. When a short identifier is supplied, the partition and current + region are prepended automatically. See: + https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_standards_subscription + EOT + type = list(string) + default = [] +} + +################################################################ +# Finding aggregator +################################################################ + +variable "finding_aggregator_enabled" { + description = "Whether to create a Security Hub finding aggregator to consolidate findings across regions." + type = bool + default = false +} + +variable "finding_aggregator_linking_mode" { + description = "Linking mode for the finding aggregator. One of: ALL_REGIONS, ALL_REGIONS_EXCEPT_SPECIFIED, SPECIFIED_REGIONS." + type = string + default = "ALL_REGIONS" + + validation { + condition = contains(["ALL_REGIONS", "ALL_REGIONS_EXCEPT_SPECIFIED", "SPECIFIED_REGIONS"], var.finding_aggregator_linking_mode) + error_message = "finding_aggregator_linking_mode must be one of ALL_REGIONS, ALL_REGIONS_EXCEPT_SPECIFIED, SPECIFIED_REGIONS." + } +} + +variable "finding_aggregator_regions" { + description = "List of regions used by the finding aggregator. Required when `finding_aggregator_linking_mode` is `SPECIFIED_REGIONS` or `ALL_REGIONS_EXCEPT_SPECIFIED`." + type = list(string) + default = [] +} + +################################################################ +# CloudWatch Event -> SNS forwarding +################################################################ + +variable "enable_cloudwatch" { + description = "Create a CloudWatch (EventBridge) rule that forwards Security Hub imported findings. The SNS topic itself is created by the separate alerting module." + type = bool + default = true +} + +variable "cloudwatch_event_rule_pattern_detail_type" { + description = "The detail-type pattern used to match Security Hub events for the CloudWatch rule." + type = string + default = "Security Hub Findings - Imported" +} + +variable "findings_notification_arn" { + description = "ARN of an existing SNS topic that Security Hub imported findings should be forwarded to. Leave null to skip target wiring." + type = string + default = null +} From 2e1f83fe687064e7e8db241105a6578321a0c52c Mon Sep 17 00:00:00 2001 From: piravinth Date: Mon, 18 May 2026 17:30:09 +0100 Subject: [PATCH 2/3] consume upstream module --- infrastructure/modules/security-hub/main.tf | 124 +++---------- .../modules/security-hub/outputs.tf | 28 +-- infrastructure/modules/security-hub/readme.md | 168 +++++++++--------- .../modules/security-hub/variables.tf | 42 ++--- 4 files changed, 125 insertions(+), 237 deletions(-) diff --git a/infrastructure/modules/security-hub/main.tf b/infrastructure/modules/security-hub/main.tf index f199fef..5c16774 100644 --- a/infrastructure/modules/security-hub/main.tf +++ b/infrastructure/modules/security-hub/main.tf @@ -1,117 +1,33 @@ ################################################################ # Security Hub # -# Enables AWS Security Hub in the current account/region, -# subscribes to the requested standards, optionally enables -# cross-region finding aggregation, and (optionally) wires -# imported findings to an existing SNS topic via EventBridge. +# Thin wrapper around the upstream `cloudposse/security-hub/aws` +# module, pinned to a specific version. Naming and tagging are +# derived from `context.tf` via `module.this` and forwarded to +# the upstream module so resources composed by sibling screening +# modules stay consistent. # -# This pairs with the GuardDuty module: GuardDuty findings are -# automatically ingested by Security Hub once both services are -# enabled in the same account/region. -################################################################ - -data "aws_partition" "current" {} -data "aws_region" "current" {} - -################################################################ -# Account-level Security Hub enablement -################################################################ - -resource "aws_securityhub_account" "this" { - count = module.this.enabled ? 1 : 0 - - enable_default_standards = var.enable_default_standards - control_finding_generator = var.control_finding_generator - auto_enable_controls = var.auto_enable_controls -} - -################################################################ -# Standards subscriptions -################################################################ - -locals { - standards_arns = { - for s in var.enabled_standards : - s => startswith(s, "arn:") ? s : format( - "arn:%s:securityhub:%s::%s", - data.aws_partition.current.partition, - data.aws_region.current.name, - s, - ) - } -} - -resource "aws_securityhub_standards_subscription" "this" { - for_each = module.this.enabled ? local.standards_arns : {} - - standards_arn = each.value - - depends_on = [aws_securityhub_account.this] -} +# Reference: +# https://registry.terraform.io/modules/cloudposse/security-hub/aws/latest -################################################################ -# Finding aggregator (cross-region aggregation) ################################################################ -resource "aws_securityhub_finding_aggregator" "this" { - count = module.this.enabled && var.finding_aggregator_enabled ? 1 : 0 +module "security_hub" { + source = "cloudposse/security-hub/aws" + version = "0.12.2" - linking_mode = var.finding_aggregator_linking_mode - specified_regions = contains( - ["SPECIFIED_REGIONS", "ALL_REGIONS_EXCEPT_SPECIFIED"], - var.finding_aggregator_linking_mode, - ) ? var.finding_aggregator_regions : null + enabled_standards = var.enabled_standards + enable_default_standards = var.enable_default_standards - depends_on = [aws_securityhub_account.this] - - lifecycle { - precondition { - condition = !contains( - ["SPECIFIED_REGIONS", "ALL_REGIONS_EXCEPT_SPECIFIED"], - var.finding_aggregator_linking_mode, - ) || length(var.finding_aggregator_regions) > 0 - error_message = "finding_aggregator_regions must be set when finding_aggregator_linking_mode is SPECIFIED_REGIONS or ALL_REGIONS_EXCEPT_SPECIFIED." - } - } -} + finding_aggregator_enabled = var.finding_aggregator_enabled + finding_aggregator_linking_mode = var.finding_aggregator_linking_mode + finding_aggregator_regions = var.finding_aggregator_regions -################################################################ -# CloudWatch Event Rule -> SNS forwarding for imported findings -# -# The SNS topic itself is created by the separate alerting module. -# Pass the topic ARN via `findings_notification_arn` to wire -# imported Security Hub findings into it. -################################################################ + # SNS topic ownership stays with the alerting module — we just + # point the upstream CloudWatch event rule at an existing topic. + create_sns_topic = false + imported_findings_notification_arn = var.findings_notification_arn + cloudwatch_event_rule_pattern_detail_type = var.cloudwatch_event_rule_pattern_detail_type -# Sub-label for the imported-findings EventBridge rule so its -# name/tags are derived from the same context but disambiguated -# from the account-level resources. -module "imported_findings_label" { - source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags?ref=feature/BCSS-23189-add-new-modules-to-suppport-bcss" context = module.this.context - - attributes = concat(module.this.attributes, ["imported-findings"]) -} - -resource "aws_cloudwatch_event_rule" "imported_findings" { - count = module.this.enabled && var.enable_cloudwatch ? 1 : 0 - - name = module.imported_findings_label.id - description = "Forward Security Hub imported findings to SNS for alerting." - - event_pattern = jsonencode({ - source = ["aws.securityhub"] - detail-type = [var.cloudwatch_event_rule_pattern_detail_type] - }) - - tags = module.imported_findings_label.tags -} - -resource "aws_cloudwatch_event_target" "imported_findings" { - count = module.this.enabled && var.enable_cloudwatch && var.findings_notification_arn != null ? 1 : 0 - - rule = aws_cloudwatch_event_rule.imported_findings[0].name - target_id = module.imported_findings_label.id - arn = var.findings_notification_arn } diff --git a/infrastructure/modules/security-hub/outputs.tf b/infrastructure/modules/security-hub/outputs.tf index 1084ba7..a1de479 100644 --- a/infrastructure/modules/security-hub/outputs.tf +++ b/infrastructure/modules/security-hub/outputs.tf @@ -1,24 +1,14 @@ -output "account_id" { - description = "The AWS account ID that Security Hub has been enabled in." - value = try(aws_securityhub_account.this[0].id, null) +output "enabled_subscriptions" { + description = "List of Security Hub standards subscriptions enabled by the upstream module." + value = module.security_hub.enabled_subscriptions } -output "account_arn" { - description = "The ARN of the Security Hub hub resource for this account." - value = try(aws_securityhub_account.this[0].arn, null) +output "sns_topic" { + description = "The SNS topic that the upstream module created (null when `create_sns_topic` is false, which is the default for this wrapper)." + value = module.security_hub.sns_topic } -output "enabled_standards_subscriptions" { - description = "Map of subscribed Security Hub standards keyed by the input identifier, with the resulting subscription ARN as the value." - value = { for k, v in aws_securityhub_standards_subscription.this : k => v.id } -} - -output "finding_aggregator_arn" { - description = "ARN of the Security Hub finding aggregator, if created." - value = try(aws_securityhub_finding_aggregator.this[0].arn, null) -} - -output "cloudwatch_event_rule_arn" { - description = "ARN of the CloudWatch (EventBridge) rule forwarding Security Hub imported findings, if created." - value = try(aws_cloudwatch_event_rule.imported_findings[0].arn, null) +output "sns_topic_subscriptions" { + description = "Any SNS topic subscriptions that the upstream module created." + value = module.security_hub.sns_topic_subscriptions } diff --git a/infrastructure/modules/security-hub/readme.md b/infrastructure/modules/security-hub/readme.md index 3599246..04fd833 100644 --- a/infrastructure/modules/security-hub/readme.md +++ b/infrastructure/modules/security-hub/readme.md @@ -1,89 +1,91 @@ # Security Hub +NHS Screening wrapper around the +[`cloudposse/security-hub/aws`](https://registry.terraform.io/modules/cloudposse/security-hub/aws/latest) +module (pinned to `0.12.2`) so screening services can enable AWS +Security Hub with consistent naming and tagging via the shared +`context.tf`. + +This wraps the upstream module in the same way as +[`inspector`](../inspector) wraps `cloudposse/inspector/aws`. + +## What this module enforces + +| Control | How it is enforced | +| ------------------------------- | ------------------------------------------------------------------------ | +| Consistent naming & tagging | `context = module.this.context` forwarded to the upstream module | +| `enabled` switch | Honoured via `module.this.context.enabled` | +| Default standards on by default | `var.enable_default_standards = true` (AWS FSBP + CIS AWS Foundations) | +| Single source of SNS truth | `create_sns_topic = false`; findings forwarded to an existing topic ARN | + +## Pairing with GuardDuty + +GuardDuty findings are automatically ingested by Security Hub once both +services are enabled in the same account/region. Both the +[`guardduty`](../guardduty) and `security-hub` modules forward findings to a +shared SNS topic created by the separate alerting module via the +`findings_notification_arn` input. + +## Usage + +### Minimal: enable Security Hub with the default standards + +```hcl +module "security_hub" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/security-hub?ref=main" + + service = "bcss" + project = "platform" + environment = "prod" + name = "security-hub" +} +``` + +### Subscribe to extra standards and aggregate findings across regions + +```hcl +module "security_hub" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/security-hub?ref=main" + + service = "bcss" + project = "platform" + environment = "prod" + name = "security-hub" + + enabled_standards = [ + "standards/pci-dss/v/3.2.1", + ] + + finding_aggregator_enabled = true +} +``` + +### Forward imported findings to the shared alerting SNS topic + +```hcl +module "security_hub" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/security-hub?ref=main" + + service = "bcss" + project = "platform" + environment = "prod" + name = "security-hub" + + findings_notification_arn = module.alerting.sns_topic_arn +} +``` + +## What this module does NOT do + +* Create the SNS topic that receives findings. That is owned by the alerting + module — pass its ARN via `findings_notification_arn`. +* Create a KMS key. If the alerting SNS topic is KMS-encrypted, configure that + inside the alerting module. +* Manage Organization-wide Security Hub administration / member accounts. Those + belong in a separate account-scope module. + -## Requirements - -No requirements. - -## Providers - -| Name | Version | -|------|---------| -| [aws](#provider_aws) | n/a | - -## Modules - -| Name | Source | Version | -|------|--------|---------| -| [imported_findings_label](#module_imported_findings_label) | `git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags` | feature/BCSS-23189-add-new-modules-to-suppport-bcss | -| [this](#module_this) | `git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/tags` | feature/BCSS-23189-add-new-modules-to-suppport-bcss | - -## Resources - -| Name | Type | -|------|------| -| [aws_cloudwatch_event_rule.imported_findings](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | -| [aws_cloudwatch_event_target.imported_findings](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | -| [aws_securityhub_account.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_account) | resource | -| [aws_securityhub_finding_aggregator.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_finding_aggregator) | resource | -| [aws_securityhub_standards_subscription.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_standards_subscription) | resource | -| [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 | -|------|-------------|------|---------|:--------:| -| [additional_tag_map](#input_additional_tag_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. | `map(string)` | `{}` | no | -| [application_role](#input_application_role) | The role the application is performing | `string` | `"General"` | no | -| [attributes](#input_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, in the order they appear in the list. | `list(string)` | `[]` | no | -| [auto_enable_controls](#input_auto_enable_controls) | Whether new controls added to enabled standards are automatically enabled. | `bool` | `true` | no | -| [aws_region](#input_aws_region) | The AWS region | `string` | `"eu-west-2"` | no | -| [cloudwatch_event_rule_pattern_detail_type](#input_cloudwatch_event_rule_pattern_detail_type) | The detail-type pattern used to match Security Hub events for the CloudWatch rule. | `string` | `"Security Hub Findings - Imported"` | no | -| [context](#input_context) | Single object for setting entire context at once. See description of individual variables for details. | `any` | see `context.tf` | no | -| [control_finding_generator](#input_control_finding_generator) | How Security Hub generates findings for security checks. Valid values: SECURITY_CONTROL (consolidated, recommended) or STANDARD_CONTROL (one finding per standard). | `string` | `"SECURITY_CONTROL"` | no | -| [data_classification](#input_data_classification) | Used to identify the data classification of the resource, e.g 1-5 | `string` | `"n/a"` | no | -| [data_type](#input_data_type) | The tag data_type | `string` | `"None"` | no | -| [delimiter](#input_delimiter) | Delimiter to be used between ID elements. Defaults to `-` (hyphen). | `string` | `null` | no | -| [descriptor_formats](#input_descriptor_formats) | Describe additional descriptors to be output in the `descriptors` output map. | `any` | `{}` | no | -| [enable_cloudwatch](#input_enable_cloudwatch) | Create a CloudWatch (EventBridge) rule that forwards Security Hub imported findings. The SNS topic itself is created by the separate alerting module. | `bool` | `true` | no | -| [enable_default_standards](#input_enable_default_standards) | Whether to enable the AWS-recommended default standards (AWS Foundational Security Best Practices and CIS AWS Foundations Benchmark) when Security Hub is first enabled in this account/region. | `bool` | `true` | no | -| [enabled](#input_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | -| [enabled_standards](#input_enabled_standards) | A list of Security Hub standards/rulesets to subscribe to. Values can be a short identifier or a full ARN. | `list(string)` | `[]` | no | -| [environment](#input_environment) | ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat' | `string` | `null` | no | -| [finding_aggregator_enabled](#input_finding_aggregator_enabled) | Whether to create a Security Hub finding aggregator to consolidate findings across regions. | `bool` | `false` | no | -| [finding_aggregator_linking_mode](#input_finding_aggregator_linking_mode) | Linking mode for the finding aggregator. One of: ALL_REGIONS, ALL_REGIONS_EXCEPT_SPECIFIED, SPECIFIED_REGIONS. | `string` | `"ALL_REGIONS"` | no | -| [finding_aggregator_regions](#input_finding_aggregator_regions) | List of regions used by the finding aggregator. Required when `finding_aggregator_linking_mode` is `SPECIFIED_REGIONS` or `ALL_REGIONS_EXCEPT_SPECIFIED`. | `list(string)` | `[]` | no | -| [findings_notification_arn](#input_findings_notification_arn) | ARN of an existing SNS topic that Security Hub imported findings should be forwarded to. Leave null to skip target wiring. | `string` | `null` | no | -| [id_length_limit](#input_id_length_limit) | Limit `id` to this many characters (minimum 6). | `number` | `null` | no | -| [label_key_case](#input_label_key_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module. | `string` | `null` | no | -| [label_order](#input_label_order) | The order in which the labels (ID elements) appear in the `id`. | `list(string)` | `null` | no | -| [label_value_case](#input_label_value_case) | Controls the letter case of ID elements (labels) as included in `id`, set as tag values, and output by this module individually. | `string` | `null` | no | -| [labels_as_tags](#input_labels_as_tags) | Set of labels (ID elements) to include as tags in the `tags` output. | `set(string)` | `["default"]` | no | -| [name](#input_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. | `string` | `null` | no | -| [on_off_pattern](#input_on_off_pattern) | Used to turn resources on and off based on a time pattern | `string` | `"n/a"` | no | -| [owner](#input_owner) | The name and or NHS.net email address of the service owner | `string` | `"None"` | no | -| [project](#input_project) | ID element. A project identifier, indicating the name or role of the project the resource is for. | `string` | `null` | no | -| [public_facing](#input_public_facing) | Whether this resource is public facing | `bool` | `false` | no | -| [regex_replace_chars](#input_regex_replace_chars) | Terraform regular expression (regex) string. Characters matching the regex will be removed from the ID elements. | `string` | `null` | no | -| [region](#input_region) | ID element. Short region abbreviation e.g. 'uw2', 'ew2'. | `string` | `null` | no | -| [service](#input_service) | ID element. Service directorate abbreviation, e.g. 'bcss'. | `string` | `null` | no | -| [service_category](#input_service_category) | The tag service_category | `string` | `"n/a"` | no | -| [stack](#input_stack) | ID element. The name of the stack/component, e.g. `database`, `web`, `waf`, `eks`. | `string` | `null` | no | -| [tag_version](#input_tag_version) | Used to identify the tagging version in use | `string` | `"1.0"` | no | -| [tags](#input_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). | `map(string)` | `{}` | no | -| [tool](#input_tool) | The tool used to deploy the resource | `string` | `"Terraform"` | no | -| [workspace](#input_workspace) | ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces. | `string` | `null` | no | - -## Outputs - -| Name | Description | -|------|-------------| -| [account_arn](#output_account_arn) | The ARN of the Security Hub hub resource for this account. | -| [account_id](#output_account_id) | The AWS account ID that Security Hub has been enabled in. | -| [cloudwatch_event_rule_arn](#output_cloudwatch_event_rule_arn) | ARN of the CloudWatch (EventBridge) rule forwarding Security Hub imported findings, if created. | -| [enabled_standards_subscriptions](#output_enabled_standards_subscriptions) | Map of subscribed Security Hub standards keyed by the input identifier, with the resulting subscription ARN as the value. | -| [finding_aggregator_arn](#output_finding_aggregator_arn) | ARN of the Security Hub finding aggregator, if created. | + diff --git a/infrastructure/modules/security-hub/variables.tf b/infrastructure/modules/security-hub/variables.tf index 3e23bb1..24b3953 100644 --- a/infrastructure/modules/security-hub/variables.tf +++ b/infrastructure/modules/security-hub/variables.tf @@ -2,9 +2,8 @@ # Security Hub-specific inputs. # # Naming, tagging and the master `enabled` switch come from -# `context.tf` via `module.this` — see that file for the full -# list of inherited inputs (service, project, environment, -# stack, name, owner, data_classification, tags, etc.). +# `context.tf` via `module.this` and are forwarded to the +# upstream `cloudposse/security-hub/aws` module as `context`. ################################################################ variable "enable_default_standards" { @@ -13,36 +12,19 @@ variable "enable_default_standards" { default = true } -variable "control_finding_generator" { - description = "How Security Hub generates findings for security checks. Valid values: SECURITY_CONTROL (consolidated, recommended) or STANDARD_CONTROL (one finding per standard)." - type = string - default = "SECURITY_CONTROL" - - validation { - condition = contains(["SECURITY_CONTROL", "STANDARD_CONTROL"], var.control_finding_generator) - error_message = "control_finding_generator must be one of SECURITY_CONTROL or STANDARD_CONTROL." - } -} - -variable "auto_enable_controls" { - description = "Whether new controls added to enabled standards are automatically enabled." - type = bool - default = true -} - ################################################################ # Standards ################################################################ variable "enabled_standards" { description = <<-EOT - A list of Security Hub standards/rulesets to subscribe to. Values can be a - short identifier (e.g. `standards/aws-foundational-security-best-practices/v/1.0.0`) - or a full ARN. When a short identifier is supplied, the partition and current - region are prepended automatically. See: + A list of Security Hub standards/rulesets to subscribe to (in addition to or + instead of the defaults). Pass either short identifiers + (e.g. `standards/aws-foundational-security-best-practices/v/1.0.0`) or full + ARNs. The upstream module resolves identifiers per partition/region. See: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_standards_subscription EOT - type = list(string) + type = list(any) default = [] } @@ -75,14 +57,12 @@ variable "finding_aggregator_regions" { ################################################################ # CloudWatch Event -> SNS forwarding +# +# The SNS topic itself is created by the separate alerting +# module. Pass its ARN via `findings_notification_arn` to wire +# imported Security Hub findings into it. ################################################################ -variable "enable_cloudwatch" { - description = "Create a CloudWatch (EventBridge) rule that forwards Security Hub imported findings. The SNS topic itself is created by the separate alerting module." - type = bool - default = true -} - variable "cloudwatch_event_rule_pattern_detail_type" { description = "The detail-type pattern used to match Security Hub events for the CloudWatch rule." type = string From 920e125bb7f6aef94c31e30e42f1364d692e6126 Mon Sep 17 00:00:00 2001 From: piravinth Date: Mon, 18 May 2026 17:37:12 +0100 Subject: [PATCH 3/3] fix english fmt --- infrastructure/modules/security-hub/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infrastructure/modules/security-hub/readme.md b/infrastructure/modules/security-hub/readme.md index 04fd833..697edcc 100644 --- a/infrastructure/modules/security-hub/readme.md +++ b/infrastructure/modules/security-hub/readme.md @@ -16,7 +16,7 @@ This wraps the upstream module in the same way as | Consistent naming & tagging | `context = module.this.context` forwarded to the upstream module | | `enabled` switch | Honoured via `module.this.context.enabled` | | Default standards on by default | `var.enable_default_standards = true` (AWS FSBP + CIS AWS Foundations) | -| Single source of SNS truth | `create_sns_topic = false`; findings forwarded to an existing topic ARN | +| Single source of SNS truth | `create_sns_topic = false`; findings forwarded to an existing topic arn | ## Pairing with GuardDuty @@ -78,7 +78,7 @@ module "security_hub" { ## What this module does NOT do * Create the SNS topic that receives findings. That is owned by the alerting - module — pass its ARN via `findings_notification_arn`. + module — pass its arn via `findings_notification_arn`. * Create a KMS key. If the alerting SNS topic is KMS-encrypted, configure that inside the alerting module. * Manage Organization-wide Security Hub administration / member accounts. Those