AWS Security Hubにて統合されたコントロールの検出結果の設定を切り替えた時に、抑制済みステータスを一括で引き継ぐスクリプトを作成しました

2023.10.24

こんにちはカスタマーソリューション部のこーへいです!

AWS Security Hub(以降Security Hub)の設定項目の一つである「統合されたコントロールの検出結果」は、設定切り替え時に既存の検出結果がアーカイブされ、別Idで検出結果が作成されます。

そのため、新しく作成された検出結果ではWorkflowStatus(以降ワークフローステータス)がSUPPRESSED(以降抑制済み)ではなくNEWになります。

新しく作成された検出結果のワークフローステータスを抑制済みに更新しなければいけないのですが、手作業では大変なので今回は一括で更新できるスクリプトを作成しました。

※各ワークフローステータスの説明は結果のワークフローステータスを設定するを参照してください。

統合されたコントロールの検出結果とは

Security Hub上の設定項目の1つです。

簡単に説明すると以下の画面のように複数のセキュリティ基準を有効化している場合において、同じチェック項目の検出結果の値を統一させる設定です。

例えばコントロール項目「CloudTrail.2」は「AWS 基礎セキュリティのベストプラクティス v1.0.0」と「CIS AWS Foundations Benchmark v1.2.0」のどちらもチェック対象に含まれています。

ですがそれぞれのセキュリティ基準でチェックしているため、検出結果のフィールドや値の一部が異なります。

本設定を有効化するとセキュリティ基準間での検出結果が統一されるので、利用者目線で結果をより便利に見やすくなります。

統合されたコントロールの検出結果に関する詳細は以下をご覧ください。

今回具体的に解決したい事象

元々、統合されたコントロールの検出結果がオフの状態でSecurity Hubを利用していたのですが、先日オン状態に切り替える作業を行いました。

すると、ワークフローステータスが抑制済みの検出結果(コントロール項目「S3.13」のとあるS3バケット)にて、抑制済みステータスが外れてしまい(NEWになってしまい)、失敗状態になっていることに気が付きました。

詳細は下記記事を参照していただければと思いますが、「統合されたコントロールの検出結果」をオフからオンに切り替えると既存の検出結果とは別に新しいIdで検出結果が作成されるため抑制済みの状態が外れてしまう(新規作成のため外れるという表現は厳密に言えば正しくない)ということでした。

つまり新しく作成された検出結果に対して、ワークフローステータスを抑制済みに更新する必要があります。

対象の検出結果が数個であれば手作業での更新でも問題ないですが、数十個以上となると手作業ではきついものがあるので今回はスクリプトを作成し一括で抑制済みに更新しようという目的です。

基本的にはSecurity Hubのセキュリティ基準「AWS 基礎セキュリティのベストプラクティス v1.0.0」のみを有効化している場合を想定しておりますが、それ以外のセキュリティ基準を有効化している場合も利用可能です。
セキュリティ基準「CIS AWS Foundations Benchmark v1.2.0」のみControlIdのパラメータがなく代わりにRuleIdのパラメータが使用されているため、本ブログ記事のスクリプトでは対応しておりません。

参考:コントロールの結果の ProductFields 詳細

手順

抑制済みの検出結果一覧を取得する

まず統合されたコントロールの検出結果がオフ状態なのを確認します。

その後CloudShellを開き、下記コマンドを実行します(CloudShellの使い方はこちら)。

aws securityhub get-findings \
--page-size 100 \
--filters '{"ProductName":[{"Value": "Security Hub","Comparison":"EQUALS"}], "RecordState":[{"Value": "ACTIVE","Comparison":"EQUALS"}], "WorkflowStatus":[{"Value": "SUPPRESSED","Comparison":"EQUALS"}]}' \
--query 'Findings[].{"Id": Id, "ControlId": ProductFields.ControlId, "Resource_Id": Resources[0].Id, "GeneratorId": GeneratorId}' > output.txt

コマンドを実行すると「output.txt」が作成されているので中身を確認し、抽出された検出結果の一覧を確認します。

[
    {
        "Id": "arn:aws:securityhub:ap-northeast-1:XXXXXXXXXXXX:subscription/aws-foundational-security-best-practices/v/1.0.0/SNS.2/finding/XXXXXXXXXXXX",
        "ControlId": "SNS.2",
        "Resource_Id": "arn:aws:sns:ap-northeast-1:XXXXXXXXXXXX:test",
        "GeneratorId": "aws-foundational-security-best-practices/v/1.0.0/SNS.2"
    },
    {
        "Id": "arn:aws:securityhub:ap-northeast-1:XXXXXXXXXXXX:subscription/aws-foundational-security-best-practices/v/1.0.0/EC2.2/finding/XXXXXXXXXXXX",
        "ControlId": "EC2.2",
        "Resource_Id": "arn:aws:ec2:ap-northeast-1:XXXXXXXXXXXX:security-group/sg-XXXXXXXXXXXX",
        "GeneratorId": "aws-foundational-security-best-practices/v/1.0.0/EC2.2"
    }
]

上記2つの検出結果が、抑制済みになっている引き継ぎ元の検出結果となります。

コマンドの解説

page-sizeオプション
--page-size 100

--page-sizeオプションを使用することで、get-findingsで取得できるデータ量が多すぎる場合でも、一回のAPIで呼び出すデータを100に調整しその分APIを内部的に叩く回数を増やすことでエラーを回避します(※出力には影響しません)。

参考:--page-size パラメータの使用方法

filtersオプション
--filters '{"ProductName":[{"Value": "Security Hub","Comparison":"EQUALS"}], "RecordState":[{"Value": "ACTIVE","Comparison":"EQUALS"}], "WorkflowStatus":[{"Value": "SUPPRESSED","Comparison":"EQUALS"}]}'

--filtersオプションを使用することで、抑制済みステータスを引き継ぎたい検証結果の一覧を取得することができます。

"ProductName":[{"Value": "Security Hub","Comparison":"EQUALS"}] では、Security Hub由来の検出結果であることを条件としています。GuardDutyなどの統合製品由来の検出結果は条件に含んでいません。

"RecordState":[{"Value": "ACTIVE","Comparison":"EQUALS"}] では、検出結果がACTIVE状態であること(ARCHIVED状態でないこと)を条件としています。ARCHIVED状態の検出結果はRecordStateに記載の通り、「統合されたコントロールの検出結果」をオンに切り替えた際に生成される検出結果が、抑制済みステータスを引き継ぐ必要がないためです。

ARCHIVED 状態は、結果がビューで非表示になるべきであることを示します。アーカイブされた結果はすぐに削除されません。検索やレビュー、レポートを行うことができます。関連付けられたリソースが削除された場合、リソースが存在しない場合、またはコントロールが無効になっている場合、Security Hub でコントロールベースの結果が自動的にアーカイブされます。

"WorkflowStatus":[{"Value": "SUPPRESSED","Comparison":"EQUALS"}] では、ワークフローステータスが抑制済みであることを条件としています。

queryオプション
--query 'Findings[].{"Id": Id, "ControlId": ProductFields.ControlId, "Resource_Id": Resources[0].Id, "GeneratorId": GeneratorId}' > output.txt

--queryオプションにて、出力項目を指定します。

  • Id:検出結果のId、引き継ぎ元の検出結果を識別するために必要
  • ControlIdとResource_Id:引き継ぎ先の検出結果を紐付け取得するために必要
  • GeneratorId:検出結果を生成したセキュリティ基準を確認するために必要

--filtersオプションや--queryオプションを変更すると、対象の検出結果や出力情報をカスタマイズできるのでこちらを参考にお好きにカスタマイズしていただければと思います。

検出結果の抑制済み状態を引き継ぐ

統合されたコントロールの検出結果をオン状態に更新後、1日程度待機すると新たな検出結果が作成されます。

新たに作成された検出結果のワークフローステータスを一括で抑制済みに更新していきましょう。

echo '#!/bin/bash

while IFS= read -r line
do
  controlId=$(echo $line | jq -r '.ControlId')
  resourceId=$(echo $line | jq -r '.Resource_Id')

  read -r Id ProductArn <<< $(aws securityhub get-findings \
  --filters "{\"RecordState\":[{\"Value\": \"ACTIVE\",\"Comparison\":\"EQUALS\"}], \"GeneratorId\":[{\"Value\": \"security-control/$controlId\",\"Comparison\":\"EQUALS\"}], \"ResourceId\":[{\"Value\": \"$resourceId\",\"Comparison\":\"EQUALS\"}]}" \
  --query 'Findings[0].[Id,ProductArn]' --output text)

  if [ ! -z "$Id" ] && [ ! -z "$ProductArn" ]; then
    echo "$Id" | tee -a results.txt

    result=$(aws securityhub batch-update-findings \
    --finding-identifiers Id="$Id",ProductArn="$ProductArn" \
    --workflow Status="SUPPRESSED" 2>&1)

    echo "$result" | tee -a results.txt

    if echo "$result" | grep -q '"ErrorCode"'; then
      echo "Error detected in UnprocessedFindings." | tee -a results.txt
    fi

    echo "======================" | tee -a results.txt

  fi

done < <(jq -c '.[]' output.txt) 2>&1' > update_findings_status.sh

上記のechoコマンドでスクリプトファイルを作成し、下記で実行します。

bash update_findings_status.sh

Cloudshell上で、スクリプト「update_findings_status.sh」を作成して「output.txt」が存在するディレクトリにてスクリプト実行します。

update_findings_status.shの中身
#!/bin/bash

while IFS= read -r line
do
  controlId=$(echo $line | jq -r '.ControlId')
  resourceId=$(echo $line | jq -r '.Resource_Id')

  read -r Id ProductArn <<< $(aws securityhub get-findings \
  --filters "{\"RecordState\":[{\"Value\": \"ACTIVE\",\"Comparison\":\"EQUALS\"}], \"GeneratorId\":[{\"Value\": \"security-control/$controlId\",\"Comparison\":\"EQUALS\"}], \"ResourceId\":[{\"Value\": \"$resourceId\",\"Comparison\":\"EQUALS\"}]}" \
  --query 'Findings[0].[Id,ProductArn]' --output text)

  if [ ! -z "$Id" ] && [ ! -z "$ProductArn" ]; then
    echo "$Id" | tee -a results.txt

    result=$(aws securityhub batch-update-findings \
    --finding-identifiers Id="$Id",ProductArn="$ProductArn" \
    --workflow Status="SUPPRESSED" 2>&1)

    echo "$result" | tee -a results.txt

    if echo "$result" | grep -q '"ErrorCode"'; then
      echo "Error detected in UnprocessedFindings." | tee -a results.txt
    fi

    echo "======================" | tee -a results.txt

  fi

done < <(jq -c '.[]' output.txt) 2>&1
【おまけ】先に抑制済みに更新する検出結果の一覧を確認したい場合のサンプル
#!/bin/bash

while IFS= read -r line
do
  controlId=$(echo $line | jq -r '.ControlId')
  resourceId=$(echo $line | jq -r '.Resource_Id')

  aws securityhub get-findings \
  --filters "{\"RecordState\":[{\"Value\": \"ACTIVE\",\"Comparison\":\"EQUALS\"}], \"GeneratorId\":[{\"Value\": \"security-control/$controlId\",\"Comparison\":\"EQUALS\"}], \"ResourceId\":[{\"Value\": \"$resourceId\",\"Comparison\":\"EQUALS\"}]}" \
  --query 'Findings[].{"Id": Id, "GeneratorId": GeneratorId, "Resource_Id": Resources[0].Id}'

done < <(jq -c '.[]' output.json) > results.txt

出力先は「results.txt」ファイルなので、ダウンロード等を行い中身を確認しましょう。

arn:aws:securityhub:ap-northeast-1:XXXXXXXXXXXX:security-control/SNS.2/finding/XXXXXXXXXXXX
{
    "ProcessedFindings": [
        {
            "Id": "arn:aws:securityhub:ap-northeast-1:XXXXXXXXXXXX:security-control/SNS.2/finding/XXXXXXXXXXXX",
            "ProductArn": "arn:aws:securityhub:ap-northeast-1::product/aws/securityhub"
        }
    ],
    "UnprocessedFindings": []
}
======================
arn:aws:securityhub:ap-northeast-1:XXXXXXXXXXXX:security-control/EC2.2/finding/XXXXXXXXXXXX
{
    "ProcessedFindings": [
        {
            "Id": "arn:aws:securityhub:ap-northeast-1:XXXXXXXXXXXX:security-control/EC2.2/finding/XXXXXXXXXXXX",
            "ProductArn": "arn:aws:securityhub:ap-northeast-1::product/aws/securityhub"
        }
    ],
    "UnprocessedFindings": []
}
======================

抑制済みにされた検出結果のIdが「results.txt」に出力されています。

「update_findings_status.sh」の解説

#!/bin/bash

while IFS= read -r line
do
  controlId=$(echo $line | jq -r '.ControlId')
  resourceId=$(echo $line | jq -r '.Resource_Id')

省略

done < <(jq -c '.[]' output.txt) 2>&1 | tee results.txt

先ほどの手順で出力した「output.txt」をファイルをインポートし、引き継ぎ元の検出結果のcontrolIdとresourceIdを読み込んで変数に格納します。

  read -r Id ProductArn <<< $(aws securityhub get-findings \
  --filters "{\"RecordState\":[{\"Value\": \"ACTIVE\",\"Comparison\":\"EQUALS\"}], \"GeneratorId\":[{\"Value\": \"security-control/$controlId\",\"Comparison\":\"EQUALS\"}], \"ResourceId\":[{\"Value\": \"$resourceId\",\"Comparison\":\"EQUALS\"}]}" \
  --query 'Findings[0].[Id,ProductArn]' --output text)

こちらの箇所では、格納したcontrolIdとresourceIdを元に、統合されたコントロールの検出結果で当てはまる(引き継ぎ先)検出結果を抽出し、IdとProductArnを変数に格納します。

  if [ ! -z "$Id" ] && [ ! -z "$ProductArn" ]; then
    echo "$Id" | tee -a results.txt

    result=$(aws securityhub batch-update-findings \
    --finding-identifiers Id="$Id",ProductArn="$ProductArn" \
    --workflow Status="SUPPRESSED" 2>&1)

    echo "$result" | tee -a results.txt

    if echo "$result" | grep -q '"ErrorCode"'; then
      echo "Error detected in UnprocessedFindings." | tee -a results.txt
    fi

    echo "======================" | tee -a results.txt

  fi

done < <(jq -c '.[]' output.txt) 2>&1 | tee results.txt

最後にIdとProductArnの変数が格納されている場合、その値を元に対象の検出結果を抑制済みに更新します。

batch-update-findingsにて、cliコマンドのエラーが発生した場合や、UnprocessedFindingsが発生し更新がうまくいかなかった場合は、その結果がファイルと画面に出力されるため、個別対応していただければと思います。

cliコマンドのエラーが発生した場合の出力サンプル
======================
arn:aws:securityhub:us-west-2:XXXXXXXXXXXX:subscription/aws-foundational-security-best-practices/v/1.0.0/EC2.7/finding/XXXXXXXXXXXX

An error occurred (InvalidInputException) when calling the BatchUpdateFindings operation: Invalid parameter 'FindingIdentifiers'. data[0].ProductArn should NOT be shorter than 12 characters, data[0].ProductArn should match pattern "^arn:(aws|aws-cn|aws-us-gov):[A-Za-z0-9\-]{1,63}:[a-z0-9\-]*:([0-9]{12})?:.+$"
======================
UnprocessedFindingsが発生した場合の出力サンプル
arn:aws:securityhub:us-west-2:XXXXXXXXXXXX:subscription/aws-foundational-security-best-practices/v/1.0.0/EC2.7/finding/XXXXXXXXXXXX
{
    "ProcessedFindings": [],
    "UnprocessedFindings": [
        {
            "FindingIdentifier": {
                "Id": "arn:aws:securityhub:us-west-2:XXXXXXXXXXXX:subscription/aws-foundational-security-best-practices/v/1.0.0/EC2.7/finding/XXXXXXXXXXXX",
                "ProductArn": "arn:aws:securityhub:us-west-2::product/aws/securityhub"
            },
            "ErrorCode": "FindingNotFound",
            "ErrorMessage": "Finding Not Found"
        }
    ]
}
Error detected in UnprocessedFindings.
======================

確認

引き継ぎ先の新規作成された検出結果のワークフローステータスが抑制済みになったのか確認します。

左のナビゲーションバーにて「検出結果」を選択し、フィルターを追加から「Id」にて先ほどのresults.txtで出力されたIdを指定し、出てきた検出結果(Idは固有なため1つのみ)のタイトルを選択する。

SNS.2もEC2.2の検出結果が無事に抑制済みになっていることが確認できました。

まとめ

手動対応では辛い量の検出結果を抑制済みにしている場合に、お役に立てば幸いです。