This is handy when codifying suppressions using terraform and a map structure such as YAML.
Example of module usage
module "securityhub_batch_update_findings" {
source = "infralicious/securityhub-batchupdatefindings/aws"
# It's recommended to pin every module to a specific version
# version = "x.x.x"
findings = yamldecode(file("${path.module}/findings.yaml")).findings
default_product_arn = "arn:aws:securityhub:us-east-1:ACCOUNTID:product/ACCOUNTID/default"
default_workflow = "SUPPRESSED"
note_suffix = "\n\nAdded using terraform"
}
Example of findings.yaml
file
# findings.yaml
findings:
# Every finding should have an adequate note for the suppression.
# A single resource can have multiple findings.
# We can codify the resource either in the note or in an inline comment.
- id: "arn:aws:securityhub:us-east-1:ACCOUNTID:subscription/aws-foundational-security-best-practices/v/1.0.0/S3.11/finding/e4c171dc-12e6-433b-8a51-a382e8d24e37"
product_arn: "arn:aws:securityhub:us-east-1:ACCOUNTID:product/ACCOUNTID/default"
note:
text: "INFOSEC-1234: Suppressed since public IP ingress is for data partner"
workflow:
status: "SUPPRESSED"
The yaml file can be autogenerated from existing suppressions using this awscli
command with yq
.
Remember to add the findings
parent key.
The title
and resource_id
are for inline comments on what the control the finding was for.
aws securityhub get-findings \
--filters '{"WorkflowStatus": [{"Value": "SUPPRESSED", "Comparison": "EQUALS"}] }' \
--query 'Findings[].{
id: Id,
product_arn: ProductArn,
note: { text: Note.Text },
workflow: { status: `"SUPPRESSED"` }
title: Title,
resource_id: Resources[0].Id,
}' | yq -P . > findings.yaml
This can be done for specific controls and with more information. Additional information in the YAML is helpful and is ignored by the terraform.
Here is an example with RDS.13
, sorting by engine version, and populating the yaml with the desired fields
aws securityhub get-findings \
--filters '{"SeverityLabel": [{"Value": "HIGH", "Comparison": "EQUALS"}], "WorkflowStatus": [{"Value": "NEW", "Comparison": "EQUALS"}], "ComplianceSecurityControlId": [{"Value": "RDS.13", "Comparison": "EQUALS"}] }' \
--query 'sort_by(
Findings,
&Resources[0].Details.AwsRdsDbInstance.EngineVersion
)[
?contains(GeneratorId, `"security-control"`)
].{
id: Id,
product_arn: ProductArn,
workflow: { status: `"SUPPRESSED"` },
title: Title,
engine_version: Resources[0].Details.AwsRdsDbInstance.EngineVersion,
resource_id: Resources[0].Id
}' | yq -P . > findings.yaml
- Run a plan
- Retrieve the existing suppression for a specific finding
- Use
terraform apply -target
to suppress and add a note to the same finding - Repeat the previous retrieval to see the new result
- Compare with the old result and see if there are differences
This will give the count of suppressions in aws.
aws securityhub get-findings \
--filters '{"WorkflowStatus": [{"Value": "SUPPRESSED", "Comparison": "EQUALS"}] }' \
--query 'Findings[] | length(@)
This will give the codified suppression count.
yq '.findings | length' findings.yaml
If the counts differ, then the clickops'ed suppression(s) can be moved to the yaml file.
If the findings.yaml
file is too long, consider breaking it up by each control.
~ tree findings/
findings
├── EC2.1.yaml
├── EC2.2.yaml
├── EC2.3.yaml
└── S3.1.yaml
1 directory, 4 files
The terraform can then be modified
locals {
findings = flatten(concat([
for file in fileset(path.module, "findings/*.yaml"):
yamldecode(file("${path.module}/${file}")).findings
]))
}
module "securityhub_batch_update_findings" {
source = "infralicious/securityhub-batchupdatefindings/aws"
# It's recommended to pin every module to a specific version
# version = "x.x.x"
for_each = local.findings
findings = yamldecode(file(each.key)).findings
# ...
}
Name | Version |
---|---|
terraform | >= 1.1.0 |
null | > 1 |
Name | Version |
---|---|
null | > 1 |
Name | Type |
---|---|
null_resource.default | resource |
Name | Description | Type | Default | Required |
---|---|---|---|---|
default_product_arn | The default product ARN for each finding. This can be overridden using the key product_arn . |
string |
n/a | yes |
findings | The list of findings to run the awscli command on. | list(object({ |
n/a | yes |
awscli_additional_arguments | n/a | string |
"" |
no |
awscli_command | n/a | string |
"aws" |
no |
default_note_updated_by | The default UpdatedBy for each finding for its note if a note is provided. This can be overridden using the key note_updatedby . |
string |
"terraform" |
no |
default_workflow | The default workflow for each finding. This can be overridden using the key workflow . |
string |
"SUPPRESSED" |
no |
dryrun_enabled | Whether or not to add an echo before the command to verify the commands prior to applying. | bool |
false |
no |
note_suffix | Add a suffix to each note. | string |
"" |
no |
- https://ekantmate.medium.com/how-to-suppress-particular-findings-in-aws-security-hub-using-terraform-558bd3819b31
- hashicorp/terraform-provider-aws#29164
- https://registry.terraform.io/modules/infralicious/securityhub-batchupdatefindings/aws
- https://library.tf/modules/infralicious/securityhub-batchupdatefindings/aws/latest