はじめに
弊社は、監視アラートをプロジェクト管理ツールであるBacklogの課題を自動起票する形で、お客様に通知している案件が多いです。
その為、GuardDutyやInspectorの検知結果もBacklogの課題として自動起票する形で通知したいといったニーズがあるとかないとか。
このBacklog課題の自動起票ですが、AWS SNSとBacklogのメールによる課題登録機能を組み合わせれば簡単に実現はできるのですが、GuardDutyやInspectorの検知結果がJSON形式で無慈悲に課題の本文に記載される為、まったく内容が頭に入ってきません。

EventBridgeの入力トランスフォーマーを使えば少しはマッシになりますが、担当者やお知らせユーザを柔軟に変更できない、課題のフォーマットを自由に設定できない等の難点があります。
何より、SNSのサブスクライブを解除するリンクがまじで罠です(リンク踏んでも解除されないようにはできるが、そもそも罠を置くのがダサい)。

そんなあなたに朗報です!
EventBridge API DestinationsでBacklogの課題追加 APIを叩けば良いですよ!
そうすれば、こんな感じで任意のフォーマットでBacklog課題が起票できます。


イメージ図
GuardDuty

Inspector

設定手順
GuardDutyやInspectorのFindingをEventBridgeで拾う方法は本筋と逸れるので割愛します。
EventBridge API DestinationsでBacklog課題を起票する方法は、そこら中に転がっているので、肝となる部分の設定例を抜粋して、Terraformコードベースで解説します。
EventBridge 入力トランスフォーマー
EventBridgeでGuardDutyやInspectorのFindingを検知すると、以下のような色々な情報が格納されたJSONがEventBridgeでキャッチされます。

このJSONをそのままBacklogに渡してしまうとカオスなので、必要な情報だけを抜き出し、Backlog課題追加 APIの仕様に従って、整形します。
# EventBridge Target
resource "aws_cloudwatch_event_target" "guardduty" {
event_bus_name = aws_cloudwatch_event_bus.guardduty.name
rule = aws_cloudwatch_event_rule.guardduty.name
role_arn = aws_iam_role.guardduty_eventbridge_invoke_api_destination.arn
arn = aws_cloudwatch_event_api_destination.guardduty.arn
retry_policy {
maximum_event_age_in_seconds = 60
maximum_retry_attempts = 3
}
input_transformer {
input_paths = {
accountId = "$.detail.accountId"
severity = "$.detail.severity"
time = "$.time"
findingType = "$.detail.type"
region = "$.region"
findingDescription = "$.detail.description"
findingId = "$.detail.id"
}
input_template = <<EOT
{
"projectId": ${local.guardduty_notify_backlog_param.project_id},
"summary": "[セキュリティアラート検知] AWS GuardDuty Finding <time>",
"description": "関係者各位\n\nセキュリティアラートを検知しましたのでご連絡させていただきます。\n**アラート詳細\n|~検出結果タイプ||\n|~重要度||\n|~検出時間|<time>(UTC)|\n|~アカウントID||\n|~リージョン||\n|~説明||\n\n[[マネジメントコンソール:https://console.aws.amazon.com/guardduty/home?region=#/findings?search=id%3D&fId=]]から詳細をご確認ください。",
"issueTypeId": ${local.guardduty_notify_backlog_param.issuetype_id},
"priorityId": ${local.guardduty_notify_backlog_param.priority_id},
"assigneeId": ${local.guardduty_notify_backlog_param.assignee_id},
%{for idx in range(length(local.guardduty_notify_backlog_param.notified_user_ids))}"notifiedUserId[${idx}]": ${local.guardduty_notify_backlog_param.notified_user_ids[idx]}%{if idx < length(local.guardduty_notify_backlog_param.notified_user_ids) - 1},
%{endif}%{endfor}
}
EOT
}
}
input_transformerでは、生のJSONから抜き出したい情報のpathに別名をつけます。
その別名を使いつつ、input_templateにJSON形式で必要なパラメータの値を設定します。
必要なパラメータはBacklog課題追加 APIに書いてありますが、何個か補足解説を入れます。
・projectId
課題を起票するBacklogプロジェクトのIDを設定
IDはBacklogの課題一覧ページで自動挿入されるクエリパラメータから確認可能
・description
Markdown or Backlog記法で課題の本文を設定する
改行コードを使いつつ、一行で設定する必要があるので、少し大変
・issueTypeId
課題の種別IDを設定
IDはプロジェクト設定の種別の編集ページで自動挿入されるクエリパラメータから確認可能
・assigneeId、notifiedUserId
課題起票時の担当者とお知らせユーザをそれぞれBacklogユーザIDで設定する
このBacklogユーザIDを調べる方法がBacklog APIを用いた方法しかない為、少し曲者
設定例では、Terraformのhttpリソースを用いて、プロジェクトに参加してるユーザ一覧をJSONで抜く → それをパース → Backlogユーザ名で検索 → IDをlocalに格納するといった手法を取っている
# Backlog Project User List
# https://developer.nulab.com/ja/docs/backlog/api/2/get-project-user-list/
data "http" "backlog_project_user_lists" {
url = "https://XXX.backlog.jp/api/v2/projects/XXXXX/users?apiKey=${data.aws_ssm_parameter.this["/backlog/apiKey/issues"].value}"
}
# Guardduty Parameter
locals {
~ Snip~
guardduty_notify_backlog_param = {
project_id = local.backlog_project.project_id
issuetype_id = local.backlog_project.issuetype_id
priority_id = 3 # 中
assignee_id = local.backlog_user_info.iret.hayabusa[0].id
notified_user_ids = [
local.backlog_user_info.iret.falcon[0].id
]
}
}
# Notify Backlog Parameter
locals {
backlog_project = {
project_id = XXXXXXXXXX # https://XXX.backlog.jp/projects/XXXXX
issuetype_id = XXXXXXXXXX # https://XXX.backlog.jp/EditIssueType.action?issueType.id=XXXXXXXXXX&issueType.projectId= XXXXXXXXXX
}
backlog_user_info = {
iret = {
hayabusa = [for user in jsondecode(data.http.backlog_project_user_lists.response_body) : user if user.name == "ハヤブサ"]
falcon = [for user in jsondecode(data.http.backlog_project_user_lists.response_body) : user if user.name == "ファルコン"]
}
}
}
EventBridge API Destination
特に解説することはないので、設定例だけを添付します。
# EventBridge API Destination
resource "aws_cloudwatch_event_api_destination" "guardduty" {
name = local.guardduty_eventbridge_api_destination_name
description = null
invocation_endpoint = "https://XXX.backlog.jp/api/v2/issues"
http_method = "POST"
invocation_rate_limit_per_second = 300
connection_arn = aws_cloudwatch_event_connection.guardduty.arn
}
EventBridge Connection
認証タイプがAPIキーだとAPIキーヘッダーの設定が必須ですが、Backlog APIの仕様だと認証情報はクエリパラメータとして渡す必要があるので、APIキーヘッダーには適当にdumyを設定します。
ConnectionのAuthパラメータは、Connection作成時にシークレットマネージャのパラメータとして自動登録されます。
設定例だと、APIキーのベタ書きを避ける為、SSMパラメータストアに保存しているAPIキーを呼び出してますが、これだとシークレットマネージャーとSSMパラメータストアとで、二重管理する形になるので、ConnectionのAuthパラメータに、既存のシークレットマネージャーのパラメータを使用できるのであれば、そうした方が良いでしょう。
# EventBridge Connection
resource "aws_cloudwatch_event_connection" "guardduty" {
name = local.guardduty_eventbridge_connection_name
description = null
authorization_type = "API_KEY"
auth_parameters {
api_key {
key = "dummy"
value = "dummy"
}
invocation_http_parameters {
query_string {
key = "apiKey"
value = data.aws_ssm_parameter.this["/backlog/apiKey/issues"].value
}
}
}
}
CloudWatchAlarm
Backlogの起票が失敗した時に感知できないのは良くないので、FailedInvocationsを監視するCloudWatchAlarmを作成しといた方が良いでしょう。
※文字数制限?の関係で、設定例は割愛します。すいません。
最後に
これを見れば設定可能!っといった記事ではありませんが、何かの参考になれたら幸いです。