Skip to content

infralicious/terraform-aws-securityhub-batchupdatefindings

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

terraform-aws-securityhub-batch-update-findings

This is handy when codifying suppressions using terraform and a map structure such as YAML.

Usage

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"

Misc

Generate the YAML file

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

Test

  1. Run a plan
  2. Retrieve the existing suppression for a specific finding
  3. Use terraform apply -target to suppress and add a note to the same finding
  4. Repeat the previous retrieval to see the new result
  5. Compare with the old result and see if there are differences

Compare the counts between suppressions and codified suppressions

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.

Instead of a single file, use multiple files

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

Requirements

Name Version
terraform >= 1.1.0
null > 1

Providers

Name Version
null > 1

Resources

Name Type
null_resource.default resource

Inputs

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({
id = string
note = object({
text = string
updated_by = optional(string)
})
workflow = object({
status = string
})
product_arn = optional(string)
verification_state = optional(string)
confidence = optional(number)
criticality = optional(number)
}))
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

References