Security Hubの検出結果をノート(note)をつけてまとめて抑制してみた

Security Hubの検出結果をノート(note)をつけてまとめて抑制してみた

Clock Icon2024.04.13

こんにちは。たかやまです。

Security Hub 100%にしていますか?

私は定期的にSecurity Hubの結果を確認して棚卸しをしていますが、新たにリソースをデプロイした場合にはどうしてもSecurity Hubの失敗コントロールが出てしまいます。

対処するべき項目はリソース設定を見直して改善するべきですが、結果の内容次第では抑制しても問題ない項目もあります。

私が抑制する場合にはよくこちらの ノート(Note)機能を使ってコメントを残して抑制しています。

ただこちらのノート機能、失敗コントロールの検出結果ごとに検出結果のID(Id)製品ARN(ProductArn)と個別のコメントをAPI(BatchUpdateFindings)から登録する必要があります。

1,2件であれば個別確認して登録でも問題ないですが、件数が多いと手間がかかります。

そこで、今回はSecurity Hubの失敗コントロールをまとめてノートを記載して抑制するスクリプトを用意したのでご紹介します。

スクリプト

import argparse
import csv
import re

import boto3

# STSクライアントの作成
sts = boto3.client("sts")

# 現在のアカウントIDを取得
account_id = sts.get_caller_identity()["Account"]

# Security Hubクライアントの作成
securityhub = boto3.client("securityhub")


def extract_control_id(finding_id):
    # Finding IDからControlIdを抽出するための正規表現パターン
    pattern = r"security-control/([^/]+)/finding"
    match = re.search(pattern, finding_id)
    if match:
        return match.group(1)
    return ""


def export_findings_to_csv(csv_file_path, workflow_status):
    # 指定されたWorkflowStatusの調査結果を取得
    findings = []
    next_token = ""

    while True:
        if next_token:
            response = securityhub.get_findings(
                Filters={
                    "ProductName": [{"Value": "Security Hub", "Comparison": "EQUALS"}],
                    "ComplianceStatus": [{"Value": "FAILED", "Comparison": "EQUALS"}],
                    "WorkflowStatus": [
                        {"Value": workflow_status, "Comparison": "EQUALS"}
                    ],
                    "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}],
                },
                NextToken=next_token,
            )
        else:
            response = securityhub.get_findings(
                Filters={
                    "ProductName": [{"Value": "Security Hub", "Comparison": "EQUALS"}],
                    "ComplianceStatus": [{"Value": "FAILED", "Comparison": "EQUALS"}],
                    "WorkflowStatus": [
                        {"Value": workflow_status, "Comparison": "EQUALS"}
                    ],
                    "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}],
                }
            )

        findings.extend(response["Findings"])

        if "NextToken" in response:
            next_token = response["NextToken"]
        else:
            break

    # 調査結果をControlIdとIDで昇順にソート
    findings.sort(key=lambda x: (extract_control_id(x["Id"]), x["Id"]))

    # CSVファイルを書き込みモードで開く
    with open(csv_file_path, "w", newline="", encoding='utf-8') as csv_file:
        # CSVライターを作成
        fieldnames = [
            "ControlId",
            "Id",
            "ProductArn",
            "ResourceId",
        ]
        if workflow_status == "NEW":
            fieldnames.append("UpdateText")
        elif workflow_status == "SUPPRESSED":
            fieldnames.extend(["Text", "UpdateText"])
        csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
        csv_writer.writeheader()

        # 各Findingの情報を抽出してCSVファイルに書き込む
        for finding in findings:
            product_arn = finding["ProductArn"]
            resource_id = (
                finding["Resources"][0]["Id"] if finding.get("Resources") else ""
            )
            control_id = extract_control_id(finding["Id"])
            row_data = {
                "ControlId": control_id,
                "Id": finding["Id"],
                "ProductArn": product_arn,
                "ResourceId": resource_id,
            }
            if workflow_status == "NEW":
                row_data["UpdateText"] = ""  # UpdateTextは空白で初期化
            elif workflow_status == "SUPPRESSED":
                suppression_reason = finding.get("Note", {}).get("Text", "")
                row_data["Text"] = suppression_reason
                row_data["UpdateText"] = ""  # UpdateTextは空白で初期化

            csv_writer.writerow(row_data)

    print(f"Findingsの情報が {csv_file_path} に出力されました。")


def suppress_findings_from_csv(csv_file_path, updated_by):
    # 試行するエンコーディングのリスト
    encodings = ['utf-8', 'cp932', 'shift_jis']
    
    for encoding in encodings:
        try:
            with open(csv_file_path, "r", encoding=encoding) as csv_file:
                csv_reader = csv.DictReader(csv_file)
                for row in csv_reader:
                    finding_id = row["Id"]
                    product_arn = row["ProductArn"]
                    update_text = row.get("UpdateText", "")

                    if update_text == "解除":
                        # UpdateTextが"解除"の場合、リソースの抑制を解除する
                        securityhub.batch_update_findings(
                            FindingIdentifiers=[{"Id": finding_id, "ProductArn": product_arn}],
                            Workflow={"Status": "NEW"},
                            # Noteの削除はできないため、更新理由を記載
                            Note={
                                "Text": "SUPPRESSED -> NEW へ更新",
                                "UpdatedBy": updated_by,
                            },
                        )
                        print(f"リソース {finding_id} の抑制を解除しました。")
                    elif update_text:
                        # UpdateTextが設定されている場合、リソースを抑制する
                        securityhub.batch_update_findings(
                            FindingIdentifiers=[{"Id": finding_id, "ProductArn": product_arn}],
                            Workflow={"Status": "SUPPRESSED"},
                            Note={"Text": update_text, "UpdatedBy": updated_by},
                        )
                        print(f"リソース {finding_id} を抑制しました。理由: {update_text}")
                    else:
                        # UpdateTextが空の場合は何もしない
                        continue
            # 正常に読み込めた場合はループを抜ける
            break
                    
        except UnicodeDecodeError:
            if encoding == encodings[-1]:
                # 全てのエンコーディングを試して失敗した場合
                raise Exception("CSVファイルの文字エンコーディングを特定できませんでした。")
            continue


def main():
    parser = argparse.ArgumentParser(
        description="Security Hub Findings CSVエクスポート/抑制スクリプト\n\n"
        "UpdateTextカラムを使用して、リソースの抑制/抑制解除を行えます。\n"
        "- UpdateTextが空の場合: 何もしない\n"
        "- UpdateTextに「解除」と入力: リソースの抑制を解除する\n"
        "- UpdateTextにその他の値を入力: その値をNote.Textとしてリソースを抑制する",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "--export",
        metavar="CSV_FILE",
        help="失敗コントロールのFindingsをCSVファイルにエクスポートします",
    )
    parser.add_argument(
        "--suppress",
        nargs=2,
        metavar=("CSV_FILE", "UPDATED_BY"),
        help="指定したCSVファイルからUpdateTextを読み込み、該当のリソースを抑制します",
    )
    parser.add_argument(
        "--export-suppressed",
        metavar="CSV_FILE",
        help="抑制済みのFindingsをCSVファイルにエクスポートします",
    )

    args = parser.parse_args()

    if args.export:
        export_findings_to_csv(args.export, "NEW")
    elif args.suppress:
        suppress_findings_from_csv(args.suppress[0], args.suppress[1])
    elif args.export_suppressed:
        export_findings_to_csv(args.export_suppressed, "SUPPRESSED")
    else:
        parser.print_help()


if __name__ == "__main__":
    main()

ざっくりとした使い方はこちらです。

$ python3 securityhub_findings_csv_manager.py --help
usage: securityhub_findings_csv_manager.py [-h] [--export CSV_FILE] [--suppress CSV_FILE UPDATED_BY] [--export-suppressed CSV_FILE]

Security Hub Findings CSVエクスポート/抑制スクリプト

UpdateTextカラムを使用して、リソースの抑制/抑制解除を行えます。
- UpdateTextが空の場合: 何もしない
- UpdateTextに「解除」と入力: リソースの抑制を解除する
- UpdateTextにその他の値を入力: その値をNote.Textとしてリソースを抑制する

options:
  -h, --help            show this help message and exit
  --export CSV_FILE     失敗コントロールのFindingsをCSVファイルにエクスポートします
  --suppress CSV_FILE UPDATED_BY
                        指定したCSVファイルからUpdateTextを読み込み、該当のリソースを抑制します
  --export-suppressed CSV_FILE
                        抑制済みのFindingsをCSVファイルにエクスポートします

やってみる

--export : 失敗コントロールのFindingsをCSVファイルにエクスポート

以下のコマンドで失敗コントロールのFindingsをCSVファイルにエクスポートします。

python3 securityhub_findings_csv_manager.py --export failed-findings.csv

実行例

$ python3 securityhub_findings_csv_manager.py --export failed-findings.csv
Findingsの情報が failed-findings.csv に出力されました。

指定した名前のCSVファイルが同じディレクトリに作成されます。
CSVファイルには以下のような形で失敗コントロールのFindingsが記録されています。

Id,ProductArn,ResourceId,UpdateText
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/SecretsManager.1/finding/8211ced4-a69a-4ac8-9629-7f853af6cd0e,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:secretsmanager:ap-northeast-1:xxxxxxxxxxxx:secret:dev/hogehoge,
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/SecretsManager.4/finding/39a63a81-78d4-441f-b9ed-b7882d003d09,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:secretsmanager:ap-northeast-1:xxxxxxxxxxxx:secret:events!connection/fugafuga/a740b061-ff5e-4323-a671-97e7e17ccdf8-PTCfaw,

こちらのCSVファイルは --suppress オプションで使用します。

--export-suppressed : 抑制済みのFindingsをCSVファイルにエクポート

以下のコマンドで抑制済みのFindingsをCSVファイルにエクスポートします。

python3 securityhub_findings_csv_manager.py --export-suppressed suppressed-findings.csv

実行例

$ python3 securityhub_findings_csv_manager.py --export-suppressed suppressed-findings.csv
Findingsの情報が suppressed-findings.csv に出力されました。

指定した名前のCSVファイルが同じディレクトリに作成されます。
CSVファイルには以下のような形で抑制済みのFindingsが記録されています。

Id,ProductArn,ResourceId,Text,UpdateText
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/APIGateway.1/finding/64b6b59e-06dd-4c78-841a-ca7dd68c8fb7,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:apigateway:ap-northeast-1::/restapis/tm7up9scmd/stages/dev,ロギングは必須ではないため抑制,
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/APIGateway.4/finding/ac4eff05-2ed1-491f-b54f-11020a5459b0,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:apigateway:ap-northeast-1::/restapis/znf7gv0ryb/stages/prod,CloudFrontでWAF設定をしているため抑制,
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/APIGateway.4/finding/d9b83249-a8f2-449a-a75c-cd0fc9a3a0ff,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:apigateway:ap-northeast-1::/restapis/tm7up9scmd/stages/dev,WAFの利用が必要なリソースではないため抑制,
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/DynamoDB.1/finding/493cb557-c041-4834-a101-33e0ae5507a1,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:dynamodb:ap-northeast-1:xxxxxxxxxxxx:table/aha-DynamoDBTable-197F6M18IOT28,AHAリソースのため抑制,
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/DynamoDB.2/finding/3453e357-ffe8-4125-850b-bf175025aa72,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:dynamodb:ap-northeast-1:xxxxxxxxxxxx:table/aha-DynamoDBTable-197F6M18IOT28,AHAリソースのため抑制,
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/DynamoDB.6/finding/4bc89acf-aa48-43ce-bf43-914e66bf3b5c,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:dynamodb:ap-northeast-1:xxxxxxxxxxxx:table/aha-DynamoDBTable-197F6M18IOT28,AHAリソースのため抑制,

こちらのCSVファイルも --suppress オプションで使用します。

--suppress : 指定したCSVファイルからTextを読み込み、該当のリソースを抑制

こちらのオプションでは、CSVの UpdateText カラムに記載された内容をノートに記載してリソースを抑制します。
最後の UpdateText には抑制理由を記載して保存します。

Id,ProductArn,ResourceId,UpdateText
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/SecretsManager.1/finding/8211ced4-a69a-4ac8-9629-7f853af6cd0e,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:secretsmanager:ap-northeast-1:xxxxxxxxxxxx:secret:dev/hogehoge,ローテーション対応は不要のため抑制
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/SecretsManager.4/finding/39a63a81-78d4-441f-b9ed-b7882d003d09,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:secretsmanager:ap-northeast-1:xxxxxxxxxxxx:secret:events!connection/fugafuga/a740b061-ff5e-4323-a671-97e7e17ccdf8-PTCfaw,ローテーション対応は不要のため抑制

以下のコマンドで指定したCSVファイルからUpdateTextを読み込み、該当のリソースを抑制します。
第二引数には更新者名を指定してください。

python3 securityhub_findings_csv_manager.py --suppress failed-findings.csv cm-takayama.kotaro

実行例

$ python3 securityhub_findings_csv_manager.py --suppress failed-findings.csv cm-takayama.kotaro
リソース arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/SecretsManager.1/finding/8211ced4-a69a-4ac8-9629-7f853af6cd0e を抑制しました。理由: ローテーション対応は不要のため抑制
リソース arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/SecretsManager.4/finding/39a63a81-78d4-441f-b9ed-b7882d003d09 を抑制しました。理由: ローテーション対応は不要のため抑制-850b-bf175025aa72

--export-suppressed オプションでエクスポートしたCSVファイルをベースにコメントを編集したり抑制を解除することも可能です。

以下では以下の設定をしています。

  • 3行目 : リソースの抑制を解除
  • 5行目 : コメント更新
Id,ProductArn,ResourceId,Text,UpdateText
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/APIGateway.1/finding/64b6b59e-06dd-4c78-841a-ca7dd68c8fb7,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:apigateway:ap-northeast-1::/restapis/tm7up9scmd/stages/dev,ロギングは必須ではないため抑制,
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/APIGateway.4/finding/ac4eff05-2ed1-491f-b54f-11020a5459b0,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:apigateway:ap-northeast-1::/restapis/znf7gv0ryb/stages/prod,CloudFrontでWAF設定をしているため抑制,解除
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/APIGateway.4/finding/d9b83249-a8f2-449a-a75c-cd0fc9a3a0ff,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:apigateway:ap-northeast-1::/restapis/tm7up9scmd/stages/dev,WAFの利用が必要なリソースではないため抑制,
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/DynamoDB.1/finding/493cb557-c041-4834-a101-33e0ae5507a1,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:dynamodb:ap-northeast-1:xxxxxxxxxxxx:table/aha-DynamoDBTable-197F6M18IOT28,AHAリソースのため抑制,コメント更新
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/DynamoDB.2/finding/3453e357-ffe8-4125-850b-bf175025aa72,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:dynamodb:ap-northeast-1:xxxxxxxxxxxx:table/aha-DynamoDBTable-197F6M18IOT28,AHAリソースのため抑制,
arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/DynamoDB.6/finding/4bc89acf-aa48-43ce-bf43-914e66bf3b5c,arn:aws:securityhub:ap-northeast-1::product/aws/securityhub,arn:aws:dynamodb:ap-northeast-1:xxxxxxxxxxxx:table/aha-DynamoDBTable-197F6M18IOT28,AHAリソースのため抑制,

--export-suppressed で出力したファイルを対象に以下のコマンドを実行します。

python3 securityhub_findings_csv_manager.py --suppress suppressed-findings.csv cm-takayama.kotaro

UpdateText解除 が設定されているリソースは抑制を解除し、それ以外のリソースは Text に記載された内容でコメントの更新を行います。

$ python3 securityhub_findings_csv_manager.py --suppress suppressed-findings.csv cm-takayama.kotaro
リソース arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/APIGateway.4/finding/ac4eff05-2ed1-491f-b54f-11020a5459b0 の抑制を解除しました。
リソース arn:aws:securityhub:ap-northeast-1:xxxxxxxxxxxx:security-control/DynamoDB.1/finding/493cb557-c041-4834-a101-33e0ae5507a1 を抑制しました。理由: コメント更新

最後に

Security Hubの失敗コントロールを抑制するスクリプトをご紹介しました。

100%の維持はなかなか骨が折れますが、スクリプトなどを使って作業を効率化していきましょう!

抑制して良い項目か判断が難しい、または対処方法がわからないという場合にはこちらのブログ/ドキュメントで弊社としての推奨対応を公開しているので参考にしてみてください。

以上、たかやま(@nyan_kotaroo)でした。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.