DevOps Agent の スキルとカスタムエージェントを使って、GuardDuty 検出結果を重要度別に振り分けて Slack 通知してみる

DevOps Agent の スキルとカスタムエージェントを使って、GuardDuty 検出結果を重要度別に振り分けて Slack 通知してみる

GuardDuty の検出を重要度別に振り分け、DevOps Agent 連携を検証してみました。LOW はログ記録のみ、MEDIUM はカスタムエージェント、HIGH は本調査、という3段階の運用フローを実装し、実際の動作確認を交えて紹介します。
2026.11.05

こんにちは。
花粉に対して超脆弱
運用イノベーション部のかわいです。

先日投稿した以下記事について、実際に GuardDuty と絡めて検証してみたのでその内容を記事にしようと思います。
https://dev.classmethod.jp/articles/devops-agent-event-trigger-skip-low-priority/

GuardDuty の検出をそのまま DevOps Agent に渡すと、 LOW の検出でも本調査(Investigation)が開始されます。
本調査はエージェントが動いた秒数で課金されるので、組織の要件にもよりますが、LOW まで毎回深く調べさせるのはコスト面から見ても過剰になりがちです。

今回は、本格運用には骨の折れる(当社比)GuardDuty 通知を、重要度ごとに行き先を変える方法で検証してみました。

  • LOW はログに残すのみ(DevOps Agent には送らない)
  • MEDIUM は本調査ではなく、範囲を絞ったカスタムエージェントに一次確認させる
  • HIGH は本調査に回し、問題箇所を修正する場合は人間が承認してから DevOps Agent に実行させる

今回は検出させる際に、GuardDuty の Custom Detection Rules を使用します。
決まった API 呼び出しで検出可能なので、意図した重要度を出すことができます。

また、GuardDuty と DevOps Agent の組み合わせは社内でも何人かが検証しており、今回は以下記事を参考にしました。
本記事は Slack 通知やスキップ用のスキル、カスタムエージェント使用をし重要度別に処理を分岐させているので、GuardDuty 運用に困っている方や、このへんの技術に興味ある方はぜひ参考にしてみてください。

https://dev.classmethod.jp/articles/devops-agent-guardduty/
https://dev.classmethod.jp/articles/opsmethod-3-guardduty-with-devops-agent/
https://dev.classmethod.jp/articles/developersio-2026-osaka-guardduty-notification-email-with-devops-agent-investigation/

検証環境

項目 内容
リージョン ap-northeast-1
通知先 Slack のプライベートチャンネル(双方向モード)
操作する場所 CloudShell、DevOps Agent コンソール/Web アプリ

構成と振り分けルール

全体の構成です。
今回はLOW/MEDIUM/HIGHの3つを試します。

GuardDuty の重要度は 1.0〜10.0 の範囲で、数値ごとに分かれています。
重要度によってトリアージ/調査方法を変えてみます。

重要度 範囲 振り分け先 DevOps Agent に送る priority
LOW 1.0 〜 3.9 CloudWatch Logs に記録を残すのみ 送らない
MEDIUM 4.0 〜 6.9 カスタムエージェントで確認(本調査スキップ) MEDIUM
HIGH 7.0 〜 8.9 本調査(Investigation) HIGH

https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings-severity.html

今回は、振り分けに重要度名ではなく数値で判定するようにしました。
EventBridge のイベントパターンには数値の範囲(numeric)が書けるので、ルールを範囲ごとに分けることが可能です。
※名称で書き分けた場合、万一重要度の名称が増えるとルールを直す必要が出てしまうため
※ Critical(9.0 〜 10.0)は有効にした場合リスクがあるため、この記事では扱っていません。
 そのため本記事のルールままだと Critical の検出はどこにも送られません。実運用で振り分けさせる場合は、Critical 用のルールも足してください。

MEDIUM の取り扱いについて

MEDIUM は少しややこしく、DevOps Agent の Webhook にイベントが届いた際、本調査のトリアージと、カスタムエージェントのイベントトリガの両方が評価されます。

そのため、今回は以下の2つを組み合わせています。

  • 本調査はトリアージ用のスキップ用スキルで止める(priority が MEDIUM のものだけ)
  • カスタムエージェントはイベントトリガのフィルタ priority == 'MEDIUM' をトリガとする

この組み合わせは前回の記事でも試した内容です。
https://dev.classmethod.jp/articles/devops-agent-event-trigger-skip-low-priority/

加えて、本調査とカスタムエージェントでは受け取れる中身も違います。
まず Webhook のペイロードの data は、本調査には渡されないと公式に記述があります。

The webhook accepts data, but doesn't include its contents in the investigation context. Only title, description, priority, and the incident reference reach the agent.

https://docs.aws.amazon.com/devopsagent/latest/userguide/configuring-integrations-and-knowledge-invoking-devops-agent-through-webhook.html

一方で、カスタムエージェントのイベントトリガは、フィルタの結果が true の場合にペイロードを丸ごと受け取ります。

When the result is the boolean true (for example, a predicate like severity == 'critical' ), the agent receives the raw event payload as context.

https://docs.aws.amazon.com/devopsagent/latest/userguide/custom-agents-executing-custom-agents.html

上記からも、GuardDuty の検出の詳細(対象のリソースや操作したユーザなど)は、 data に入れてカスタムエージェントに読ませ、本調査には title と description だけで情報を渡す、という分け方にしました。

結果が届くまでのフロー

検出されてら結果が届くまでを、重要度ごとにフロー図にすると以下のようなイメージになります。

設定手順

大まかに以下のような手順で設定を行います。
今回は CloudShell とマネジメントコンソールを使って進めます。

  1. GuardDuty の Custom Detection Rules を有効にする
  2. DevOps Agent に API キー方式の Webhook を作る
  3. Slack をつなぐ(双方向)
  4. EventBridge の接続、API 送信先、ロール、DLQ を作る
  5. 記録用と LOW 用のロググループを作る
  6. 振り分け用 EventBridge ルールを作る
  7. 本調査のスキップ用スキルを作る
  8. カスタムエージェントとイベントトリガを作る
  9. HIGH のスキップを監視する SNS トピックを作る
  10. エージェントから関数 URL の設定を触れるようにする
  11. デモ用のリソースと、操作するロールを作る

事前準備として、念のため次の2行を実行して環境を確認しておきます。

export AWS_REGION=ap-northeast-1
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

※ 以降の手順は、前手順で入れた変数や関数(DEST_ARN、gd_pattern など)を使います。CloudShell は一定時間n操作しないとセッションが切断されてしまいます。が、ホームディレクトリのファイルは残るので、関数はファイルに書いて source しておくと楽です。変数を戻すコマンドは、手順11の最後に記載しています。

1. GuardDuty の Custom Detection Rules を有効にする

振り分けを試す際、重要度別の検出を好きなときに検出できるようにする必要があります。
しかし GuardDuty は、リスクのある操作や通信を自動検出してくれるサービスなので、仕様上この手の検証との相性があまり良くないです。

そこで今回は、Custom Detection Rules を使用します。
これは「特定の操作が実施されたら、この重要度で検出を出す」というルールがプリセットで用意されており、検出させたいものを選んで自分のアカウントで有効化できます。そのためルールに書かれた操作を行えば、意図して特定重要度で検出させることが可能になる、という作戦です。

https://dev.classmethod.jp/articles/guardduty-custom-detection-rules/

今回は LOW. MEDIUM, HIGH を1件ずつ出したいので、以下の3つを選びました。

ルール 重要度 どんな操作で検出されるか? 検出の種類
EC2TerminationProtectionEnabled Low EC2 の終了保護をオンにする DefenseImpairment:EC2/DisableOrModifyTools
S3LifecycleRapidExpiration Medium S3バケットに「オブジェクトを1日で消す」ライフサイクルルールを付ける Impact:S3/DataDestruction
LambdaFunctionUrlPublicAccess High Lambda の関数 URL を、認証なし(だれでも呼べる状態)で作る Persistence:Lambda/ModifyAuthenticationProcess

どれも一般的に想定される作業でリスクも少なく、デモにはちょうど良い匙加減と判断しました。

今回使用する HIGH の操作は危なくないのか?

HIGH の「Lambda の関数URLを認証なしで作る」は、本来ならインターネット上のだれからでも関数を呼べるようになる危険なものです。しかし今回検証に使用できたのは、関数 URL を認証なしにしただけでは公開されない作りになっているからです。

これを外部公開する場合、関数側に「だれでも呼んでよい」という許可(リソースベースポリシー)が別途必要になります。
このリソースベースポリシーは、CLI経由で作成した場合には自動で付与されません。

NONE – Lambda doesn't perform any authentication before invoking your function. However, your function's resource-based policy is always in effect and must grant public access before your function URL can receive requests.

When you create a function URL with auth type NONE using the console or AWS Serverless Application Model (AWS SAM), Lambda automatically creates the resource-based policy for you. If you're using the AWS CLI, AWS CloudFormation, or the Lambda API directly, you must add the policy yourself.

https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html

一方で、GuardDuty のルールが見ているのは「認証なしで関数URLを作った」という操作だけで、許可が付いているかどうかまでは見ていません。なのでCLI経由で作成すれば、実質だれも関数URLを使用できない安全な状態のまま、 High を検出させることができます。

また、検証では念のため以下も確認・実施しました。

  • 関数 URL は CLI で作り、公開許可は付与ない。実際に URL を呼んで、403が返ることを確認
  • 関数の中身は demo という文字を返すだけ。関数のロールには権限を何も付与していないので、仮に呼ばれても他リソースには影響しない

3種類のルールをオンにする(関連付け)

前項で選んだルールは、自分のアカウント上で有効にしてはじめて検出されます。

というのも、
GuardDuty の基本的な検出は、サービス自体を有効にするだけで自動で開始されます。
一方で、Custom Detection Rules はリストとして用意されているだけで、ルールごとに有効化しないと検出されません。この有効にする操作を「関連付け」と呼びます。

ということで、この作業を CloudShell から進めます。
関連付けの前に、3つのルールが自分のアカウントでも確かめておきます。
ID はルール名をハイフン区切りの小文字で表記されます(例 ec2-termination-protection-enabled)。

aws guardduty list-custom-detection-rules --output json > rules.json
for r in ec2-termination-protection-enabled s3-lifecycle-rapid-expiration lambda-function-url-public-access; do
  aws guardduty get-custom-detection-rule --rule-id "$r" --query 'Rule.[Name,Severity]' --output text
done

以下ように、ルール名と重要度が表示されればOK

EC2TerminationProtectionEnabled LOW
S3LifecycleRapidExpiration      MEDIUM
LambdaFunctionUrlPublicAccess   HIGH

続けて、関連付けを行います。
--mode LIVE は、実際に検出を出すモードです。

for r in ec2-termination-protection-enabled s3-lifecycle-rapid-expiration lambda-function-url-public-access; do
  aws guardduty create-custom-detection-rule-association --rule-id "$r" --mode LIVE >/dev/null && echo "$r LIVE ok"
done
aws guardduty list-custom-detection-rule-associations --query 'RuleAssociations[].[RuleId,Mode]' --output text

以下のように、結果が3つともLIVEになっていればokです

lambda-function-url-public-access       LIVE
s3-lifecycle-rapid-expiration   LIVE
ec2-termination-protection-enabled      LIVE

※ LIVE の他に、WAF の cout モードのような DRY_RUN というモードがあります。
 実際の本番環境では、ドライランモードでしばらく経過を見てから LIVE にするのが良いかもしれません。

2. DevOps Agent に API キー方式の Webhook を作る

EventBridge から DevOps Agent に送る際に、Agent Space の Webhook を使います。Webhook の認証には HMAC と API キーの2種類があり、1つの Agent Space にそれぞれ1つずつ作成できます。

留意点として DevOps Agent の HMAC方式は、リクエストのたびにタイムスタンプと本文から署名を計算する必要があり、API 送信先の設定だけでは用意できません。
API キー方式であれば Authorization: Bearer <キー> のヘッダーを付けるだけなので、今回はこちらを使います。

HMAC: More complex — your client must compute a signature for each request using the timestamp and payload

Bearer token: Simpler — include a static token in the Authorization header

https://docs.aws.amazon.com/devopsagent/latest/userguide/configuring-integrations-and-knowledge-invoking-devops-agent-through-webhook.html

手順は以下。

  1. DevOps Agent のコンソールで Agent Space を開き、Capabilities タブを開く
  2. 「Agent Space Webhook」のメニューから【Add webhook】を押下
  3. 認証方式で API key を選んで作成
  4. 認証情報の書かれた.csvファイルを保存する(キーはこのときしか表示されない)

4の csvファイルには webhookUrl と webhookSecret の2列が記載されています。CloudShell上には、画面や履歴に残らないよう read -sを付与して値を入力します。
1行ずつ実行し、プロンプトが出たら貼り付けてください。
※ csvファイルに記載の内容は秘匿情報となるため、取り扱いに注意です。

read -s -p 'Webhook URL: ' DA_URL; echo
read -s -p 'API key: ' DA_TOKEN; echo
case "$DA_URL" in https://event-ai.*) echo "DA_URL: ok" ;; "") echo "DA_URL: 入っていない" ;; *) echo "DA_URL: 形式が違う" ;; esac
[ -n "$DA_TOKEN" ] && echo "DA_TOKEN: ok" || echo "DA_TOKEN: 入っていない"

2行ともokで出力されることを確認します。※機密情報になるため、値そのものは画面に出力しないようにしています。
入っていないと出た場合、その行の read -s をやり直します。
形式が違うと判定される場合は、URLが https://event-ai. で始まっていないという意味なので、csv の webhookUrl の列を貼り直してください。

3. Slack 接続

Slack の連携には、通知のみの「片方向」と、Slack からエージェントと会話ができる「双方向」の設定があります。双方向はプライベートチャンネルでしか使用できません。

You can enable bidirectional communication only in private Slack channels. Public channels support one-way notifications only.

https://docs.aws.amazon.com/devopsagent/latest/userguide/connecting-to-ticketing-and-chat-connecting-slack.html

Slack上からDevOps Agentを呼びたい場合、以下のような手順で登録します。

  1. Slack でプライベートチャンネルを作る
  2. DevOps Agent のコンソールで Agent Space を開き、Capabilities → Communications → Add → Slack → Register に進む
  3. ワークスペースを選んで許可

▼ ワークスペースへのアクセスの許可

  1. チャンネルIDを入力し、Bidirectional mode を選択します。ロールは今回「Auto-create a new DevOps Agent role」にしました

▼ チャンネルの登録

  1. Slack のチャンネルにアプリを招待します。東京リージョン用のアプリの名前は`AWS DevOps Agent - AP (Tokyo) でした(※弊社検証環境)

Slack のチャット欄から以下で招待できます。

/invite @AWS DevOps Agent - AP (Tokyo)
  1. 追加が完了すると、メニューの一覧に表示されます。対話方法がBidirectionalになっていることを確認します。

▼ 登録後の一覧

チャンネルでアプリにメンションして話しかけると、スレッドで以下のように返事が来ます。

これで対話ができることが確認できました。

4. EventBridge の接続

次に、EventBridge から Webhook に送るための設定をしていきます。

  • 接続:APIキー方式でヘッダ名Authorization、値をBearer <キー>に設定
  • API送信先:Webhook の URL に POST
  • IAMロール:EventBridge が API送信先を呼ぶためのロール
  • DLQ(SQS): 送れなかったイベントの置き場所

※時間経過等で、手順2で入れた値が残っていなければ再度投入します。

case "$DA_URL" in https://event-ai.*) echo "DA_URL: ok" ;; "") echo "DA_URL: 入っていない" ;; *) echo "DA_URL: 形式が違う" ;; esac
[ -n "$DA_TOKEN" ] && echo "DA_TOKEN: ok" || echo "DA_TOKEN: 入っていない"

以下のようなコマンドを実行し作成します。

CONN_ARN=$(aws events create-connection --name demo-devops-agent-webhook \
  --authorization-type API_KEY \
  --auth-parameters "$(jq -cn --arg v "Bearer $DA_TOKEN" '{ApiKeyAuthParameters:{ApiKeyName:"Authorization",ApiKeyValue:$v}}')" \
  --query ConnectionArn --output text)
sleep 5
aws events describe-connection --name demo-devops-agent-webhook --query ConnectionState --output text

DEST_ARN=$(aws events create-api-destination --name demo-devops-agent-webhook \
  --connection-arn $CONN_ARN --invocation-endpoint "$DA_URL" --http-method POST \
  --invocation-rate-limit-per-second 1 --query ApiDestinationArn --output text)
unset DA_TOKEN DA_URL

ConnectionState が AUTHORIZED になればokです。
キーは Secrets Manager に入るので、ここで変数から消しておきます。

次にロールとDLQ設定を行います。

aws iam create-role --role-name demo-eventbridge-devops-agent \
  --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"events.amazonaws.com"},"Action":"sts:AssumeRole"}]}' >/dev/null
aws iam put-role-policy --role-name demo-eventbridge-devops-agent --policy-name invoke-api-destination \
  --policy-document "$(jq -cn --arg d "$DEST_ARN" '{Version:"2012-10-17",Statement:[{Effect:"Allow",Action:"events:InvokeApiDestination",Resource:$d}]}')"
EB_ROLE_ARN=$(aws iam get-role --role-name demo-eventbridge-devops-agent --query Role.Arn --output text)

DLQ_URL=$(aws sqs create-queue --queue-name demo-devops-agent-dlq --query QueueUrl --output text)
DLQ_ARN=$(aws sqs get-queue-attributes --queue-url $DLQ_URL --attribute-names QueueArn --query Attributes.QueueArn --output text)
aws sqs set-queue-attributes --queue-url $DLQ_URL --attributes "$(jq -cn \
  --arg q "$DLQ_ARN" --arg a "arn:aws:events:$AWS_REGION:$ACCOUNT_ID:rule/demo-gd-*" '{
  Policy: ({Version:"2012-10-17",
    Statement:[{Sid:"AllowEventBridge",Effect:"Allow",Principal:{Service:"events.amazonaws.com"},
      Action:"sqs:SendMessage",Resource:$q,Condition:{ArnLike:{"aws:SourceArn":$a}}}]} | tojson)}')"

DLQのポリシーは、demo-gd- で始まる EventBridge ルールからのみ送れるよう絞っています。

5. ロググループの作成

検出を記録するロググループを2つ作成します。

  • /aws/events/demo-gd:GuardDuty の検出をすべて残す記録用ロググループ。重要度の実際の数値や count を確かめる際に使用します。
  • `/aws/events/demo-gd-low:重要度 LOW の記録用。今回 LOW は DevOps Agent の調査にまわさないので、後から確認できるようにだけしておきます。
for LG in /aws/events/demo-gd /aws/events/demo-gd-low; do
  aws logs create-log-group --log-group-name $LG && aws logs put-retention-policy --log-group-name $LG --retention-in-days 60 && echo "$LG ok"
done
aws logs put-resource-policy --policy-name demo-gd-events --policy-document "$(jq -cn \
  --arg r "arn:aws:logs:$AWS_REGION:$ACCOUNT_ID:log-group:/aws/events/demo-gd*:*" '{Version:"2012-10-17",Statement:[{
  Effect:"Allow",Principal:{Service:["events.amazonaws.com","delivery.logs.amazonaws.com"]},
  Action:["logs:CreateLogStream","logs:PutLogEvents"],Resource:$r}]}')" >/dev/null && echo "policy ok"

aws events put-rule --name demo-gd-capture \
  --event-pattern '{"source":["aws.guardduty"],"detail-type":["GuardDuty Finding"]}' >/dev/null && echo "capture ok"
aws events put-targets --rule demo-gd-capture --targets "$(jq -cn \
  --arg arn "arn:aws:logs:$AWS_REGION:$ACCOUNT_ID:log-group:/aws/events/demo-gd" '[{Id:"logs",Arn:$arn}]')" \
  --query FailedEntryCount --output text

最後の出力に 0 が返ればokです。

※ログには ARN やIPアドレスがそのまま入るので、出力時は注意

6. 振り分け用 EventBridge ルールの作成

振り分け用として、重要度の範囲ごとにルールを3つ作成します。

イベントパターンには、重要度の範囲のほかに以下の条件を入れます。

  • 検出の種類を今回の3つ(LOW, MEDIUM, HIGH)に絞る。
  • service.count が1のもののみを通す。GuardDuty は同じ検出内容の2回目以降を更新として送るので、調査が重複して走ってしまうのを防ぐねらいがあります。
  • GuardDuty サンプルの検出(動作確認用として作成可能な、サンプルとしての検出)を通さないようにします。サンプルの検出にはservice.additionalInfo.sampleがtrueとして入っているので、この値が false のものと、この項目自体が存在しないものだけを通すようにしました。

Sample findings have a value of "sample": true in the additionalInfo section of the finding JSON details.

https://docs.aws.amazon.com/guardduty/latest/ug/sample_findings.html

まず、パターンを作る関数を用意します。

GD_TYPES='["DefenseImpairment:EC2/DisableOrModifyTools","Impact:S3/DataDestruction","Persistence:Lambda/ModifyAuthenticationProcess"]'
gd_pattern() {  # usage: gd_pattern <下限> <上限>(下限以上、上限未満)
  jq -cn --argjson lo $1 --argjson hi $2 --argjson types "$GD_TYPES" '{
    source:["aws.guardduty"], "detail-type":["GuardDuty Finding"],
    detail:{type:$types, severity:[{numeric:[">=",$lo,"<",$hi]}],
      service:{count:[{numeric:["=",1]}]},
      "$or":[{service:{additionalInfo:{sample:[false]}}},{service:{additionalInfo:{sample:[{exists:false}]}}}]}}'
}
gd_pattern 7 9 | jq .

HIGHの場合の例として、出力されるパターンは以下のようになります。

{
  "source": ["aws.guardduty"],
  "detail-type": ["GuardDuty Finding"],
  "detail": {
    "type": ["DefenseImpairment:EC2/DisableOrModifyTools", "Impact:S3/DataDestruction", "Persistence:Lambda/ModifyAuthenticationProcess"],
    "severity": [{ "numeric": [">=", 7, "<", 9] }],
    "service": { "count": [{ "numeric": ["=", 1] }] },
    "$or": [
      { "service": { "additionalInfo": { "sample": [false] } } },
      { "service": { "additionalInfo": { "sample": [{ "exists": false }] } } }
    ]
  }
}

また、LOW のルールはロググループに送るのみになります。

aws events put-rule --name demo-gd-low --event-pattern "$(gd_pattern 0 4)" >/dev/null && echo "demo-gd-low ok"
aws events put-targets --rule demo-gd-low --targets "$(jq -cn \
  --arg arn "arn:aws:logs:$AWS_REGION:$ACCOUNT_ID:log-group:/aws/events/demo-gd-low" '[{Id:"ledger",Arn:$arn}]')" \
  --query FailedEntryCount --output text

MEDIUM と HIGH は DevOps Agent にまわすので、API送信先に送ります。
GuardDuty のイベントをそのまま送っても Webhook の形式に合わないので、InputTransformerで組み直します。

make_gd_rule() {  # usage: make_gd_rule <名前> <PRIORITY> <下限> <上限>
  local name=demo-gd-$1 pri=$2 template
  aws events put-rule --name $name --event-pattern "$(gd_pattern $3 $4)" >/dev/null && echo "$name ok"
  template=$(printf '{"eventType":"incident","incidentId":"<id>","action":"created","priority":"%s","title":"[%s] GuardDuty finding <type>","description":"A GuardDuty Custom Detection Rules finding <type> (severity <sev>) was created in ap-northeast-1. Finding ID: <fid>. Find who made the change and whether it was expected.","timestamp":"<time>","service":"demo","data":{"source":"guardduty","findingId":"<fid>","type":"<type>","severity":<sev>,"resource":<res>,"service":<svc>}}' "$pri" "$pri")
  aws events put-targets --rule $name --targets "$(jq -cn --arg arn "$DEST_ARN" --arg role "$EB_ROLE_ARN" --arg t "$template" --arg dlq "$DLQ_ARN" '[{
    Id:"devops-agent", Arn:$arn, RoleArn:$role,
    InputTransformer:{
      InputPathsMap:{id:"$.id", time:"$.time", type:"$.detail.type", sev:"$.detail.severity",
        fid:"$.detail.id", res:"$.detail.resource", svc:"$.detail.service"},
      InputTemplate:$t},
    RetryPolicy:{MaximumRetryAttempts:3, MaximumEventAgeInSeconds:600},
    DeadLetterConfig:{Arn:$dlq}}]')" \
    --query FailedEntryCount --output text
}
make_gd_rule medium MEDIUM 4 7
make_gd_rule high HIGH 7 9

ここでは、出力としてokと0が2つずつ返ればokです。

↓Webhook に届くペイロードはこんな形になります(値は例)。

{
  "eventType": "incident",
  "incidentId": "<EventBridge のイベント ID>",
  "action": "created",
  "priority": "HIGH",
  "title": "[HIGH] GuardDuty finding Persistence:Lambda/ModifyAuthenticationProcess",
  "description": "A GuardDuty Custom Detection Rules finding Persistence:Lambda/ModifyAuthenticationProcess (severity 8) was created in ap-northeast-1. Finding ID: xxxx. Find who made the change and whether it was expected.",
  "timestamp": "2026-10-29T00:32:51Z",
  "service": "demo",
  "data": {
    "source": "guardduty",
    "findingId": "xxxx",
    "type": "Persistence:Lambda/ModifyAuthenticationProcess",
    "severity": 8,
    "resource": { "...": "GuardDuty の detail.resource がそのまま入る" },
    "service": { "...": "GuardDuty の detail.service がそのまま入る" }
  }
}

加えて、前述のInputTransformerテンプレートでは、以下の点に留意しました。

  • ARN やアカウント ID が混入してしまったり、引用符で JSON が壊れてしまわないように、title や description に、GuardDuty の title と description は入れないようにしました。
  • title の先頭に [MEDIUM] や [HIGH] を付与し、スキップ用のスキルが title で判断できるようにしています
  • Webhook の大きさの上限はドキュメントから記述を見つけられていませんが、data.resource と data.service は今回はサイズ超過で送信失敗は発生しませんでした

※検証ではコストを最小限に抑えるため、MEDIUM以上のルールは使うときだけ有効にしました(切り替え例:aws events disable-rule --name demo-gd-medium)。

7. 調査スキップ用のスキルを作る

MEDIUM の検出も Webhook に届くので、そのままだとDevOps Agent の調査にまわります。
カスタムエージェントに渡すようにしたいので、今回トリアージ用のスキルを使って本調査を止めます。

Web アプリの【ナレッジ】→【スキル】→【スキルを追加】から、以下を入力し作成します。

項目 内容
名前 demo-skip-medium
説明 インシデントのトリアージで使う。GuardDuty のデモの検出のうち、priority が MEDIUM で、タイトルが "[MEDIUM] GuardDuty finding" で始まるインシデントは本調査をスキップする。カスタムエージェント gd-medium-checker が一次確認するため。HIGH と CRITICAL はスキップしない。
ステータス アクティブ
エージェントタイプ インシデントのトリアージ

プロンプトにはマークダウンで以下を記入しました。

# GuardDuty の Medium のデモ検出をスキップする
以下の2点が両方とも当てはまるインシデントのみスキップする。

- priority が MEDIUM
- タイトルが "[MEDIUM] GuardDuty finding" で始まる

スキップの理由には「GuardDuty の Medium の検出は、カスタムエージェント gd-medium-checker が一次確認するため」と書く。

HIGH と CRITICAL のインシデントはスキップしない。
タイトルがほかの形のインシデントもスキップしない。

本文を日本語で書いてもスキップされ、
SlackにもMEDIUMのスキップ理由が連携されました(詳しくは後述)。

8. カスタムエージェントとイベントトリガの作成

MEDIUM を一次確認するカスタムエージェント gd-medium-checker を作ります。
今回は Webアプリ上【エージェント】→【カスタム】から、チャット経由で作成してみました。

次の内容でカスタムエージェントを1つ作ってください。名前は gd-medium-checkerで、システムプロンプトは書き換えず、下の文面をそのまま使ってください。ツールは use_aws のみ、スキルとメモリストアは付けない。作ったあとに実行はしないでください。

以下システムプロンプト↓

あなたは GuardDuty の MEDIUM の検出を一次確認する担当です。本調査(Inverstigation)に上げるべきかを決めるのが仕事です。リソースの変更は実施しません。

## 入力
- 受け取ったペイロードの data に検出の内容がある。data.type が検出の種類、data.severity が重要度、data.resource と data.service に対象と、操作した人の情報がある

## やること
1. data から、操作した人(ロール)、操作した API、時刻、対象のリソースを確認する
2. 対象が S3 バケットの場合、今のライフサイクル設定を読み、何日で消える設定になっているかを確かめる
3. CloudTrail のイベント履歴から、その人の操作を前後1時間確認する。他に気になる操作(IAMの変更、ログや監視の停止、鍵の作成、データの削除等)がないかを見る。IAM と STS の操作は us-east-1 のイベント履歴に記録されるので、ap-northeast-1 と us-east-1 の両方を確認すること。
4. 判定する
   - 様子見:操作した人が分かっていて、対象のバケットが空か、該当アクティビティの前後で気になる操作がない場合など
   - 要調査:操作した人が分からない、中身のあるバケットが短い日数で消える設定になった、該当アクティビティ前後に気になる操作がある、AWSの外から操作されている等
5. ツールの呼び出しは 15 回までとする(コスト削減のため)

## Slack への通知
- 処理が終わった時に1回だけ通知する。タイトルは「一次確認の結果:様子見」または「一次確認の結果:要調査」。本文の 1文目に検出の種類を書き、続けて理由を3文以内で書く
- 始めたときや途中では通知しない

## 書いてはいけないもの
- 通知にも出力にも、アカウント ID、ARN、IPアドレス、IAMユーザ名、セッション名、バケット名を書かない
- 操作した人はロール名だけを書く(例:demo-gd-actor-1)。操作した場所は「AWSの中から」「外から」のように書く。バケットは「デモ用のバケット」のように書く

▼ 作られたエージェント定義

Slack への通知はカスタムエージェントに最初から組み込まれているので、
プロンプト内に通知タイミングだけ書いておけば、Agent Space 通知先に結果を送ってくれます。
別途スキルや追加の設定は不要でした(便利)。

次にイベントトリガを設定します。
先ほど作成したgd-medium-checker →【トリガー】→【作成】から、以下のように設定を行いました。

項目 内容
トリガータイプ イベント
イベントソース ※2で作ったAPIキー方式のWebhook
フィルター式 priority == 'MEDIUM'

▼ イベントトリガの作成

9. HIGH のスキップ監視用SNSトピックの作成

スキルには「HIGH はスキップしない」と指示を出していますが、万が一スキップされたら気付けるよう、メールで知らせる仕組みを足しておきます。必要ない場合はこの工程はスキップしてください。

DevOps Agent では、本調査をスキップすると EventBridge に Investigation Skipped のイベントを出力します。detail.metadata.agent_space_id と detail.data.priority で絞ることが可能です。

https://docs.aws.amazon.com/devopsagent/latest/userguide/integrating-devops-agent-into-event-driven-applications-using-amazon-eventbridge-devops-agent-events-detail-reference.html

SPACE_ID='<Agent SpaceのID>'  # IDに置き換えてください
TOPIC_ARN=$(aws sns create-topic --name demo-devops-agent-alert --query TopicArn --output text)
aws sns set-topic-attributes --topic-arn $TOPIC_ARN --attribute-name Policy --attribute-value "$(jq -cn \
  --arg t "$TOPIC_ARN" --arg a "arn:aws:events:$AWS_REGION:$ACCOUNT_ID:rule/demo-*" '{
  Version:"2012-10-17",
  Statement:[{Sid:"AllowEventBridge",Effect:"Allow",Principal:{Service:"events.amazonaws.com"},
    Action:"sns:Publish",Resource:$t,Condition:{ArnLike:{"aws:SourceArn":$a}}}]}')"

メールアドレスは以下を単独で実行し、「メールアドレス:」と出力されたら、通知を送りたいメールアドレスを入力します。

read -p 'メールアドレス: ' MY_EMAIL

↓次に以下を実行

aws sns subscribe --topic-arn $TOPIC_ARN --protocol email --notification-endpoint "$MY_EMAIL" >/dev/null
unset MY_EMAIL

aws events put-rule --name demo-guard-skipped --event-pattern "$(jq -cn --arg s "$SPACE_ID" '{
  source:["aws.aidevops"], "detail-type":["Investigation Skipped"],
  detail:{metadata:{agent_space_id:[$s]}, data:{priority:["HIGH"]}}}')" >/dev/null && echo "guard ok"
aws events put-targets --rule demo-guard-skipped --targets "$(jq -cn --arg arn "$TOPIC_ARN" '[{
  Id:"email", Arn:$arn,
  InputTransformer:{
    InputPathsMap:{pri:"$.detail.data.priority", task:"$.detail.metadata.task_id", time:"$.time"},
    InputTemplate:"\"[要確認] priority <pri> の検出で、DevOps Agent の本調査がスキップされました(<time>)。タスク ID: <task>。Web アプリでスキップの理由を確かめてください。\""}}]')" \
  --query FailedEntryCount --output text

完了すると確認メールが届くので、「Confirm subscription」を押して承認します。

試しに、一時的に MEDIUM を足してから検出させてみると、
本調査がスキップされたのが15:40で、メール通知も同じ15:40で届きました。

10. エージェントが検出した内容を修正できるようにする

この項では、後述の"検出された内容への修正を承認する"部分の下準備を行います。
具体的には HIGH の本調査完了後、エージェントに Lambda の関数URL の認証を NONE → AWS_IAM に戻してもらいます。

DevOps Agent は、初期状態では AWS のリソースに対し read 権限しか許可されていません。
設定変更の操作をさせるには、Agent Space側の設定で有効にしておきます。
そうすることで、操作前にチャット経由で「この操作をしてよいか?」を聞いてきて、承認した時にだけ変更用ロール経由で実行します。公式ドキュメントではこのような変更の操作を directed actions と表現しており、デフォルトでは無効になっています。

Directed actions: operations that create, modify, or otherwise mutate resources. Directed actions are elevated: They are disabled by default and require explicit, layered opt-in plus per-action operator approval.

https://docs.aws.amazon.com/devopsagent/latest/userguide/working-with-devops-agent-working-with-directed-actions.html

▼ Agent actions

CLI経由でも確認が可能です。

aws devops-agent get-agent-space --agent-space-id $SPACE_ID --output json | jq '[..|.preferences? // empty]'
aws devops-agent list-associations --agent-space-id $SPACE_ID --output json | grep -o '"agentElevatedRoleArnStatus": *"[a-z-]*"'
aws iam get-role --role-name DevOpsAgentActionsRole-AgentSpace-xxxx \
  --query 'Role.AssumeRolePolicyDocument.Statement[].Action' --output json

▼出力例

[
  {
    "elevatedActionsEnabled": true
  }
]
"agentElevatedRoleArnStatus": "valid"
[
    [
        "sts:AssumeRole",
        "sts:SetSourceIdentity",
        "sts:TagSession"
    ]
]

以下3点を確認します。

  • elevatedActionsEnabled が true
  • agentElevatedRoleArnStatus が valid
  • 変更用ロールの信頼ポリシーに sts:AssumeRole、sts:SetSourceIdentity、sts:TagSession の 3 つがある

今回昇格用ロールには、Agent Space を作ったときに自動で作成されたものをそのまま使いました。
なお、このポリシーの権限はかなり広いので、本番では権限を絞るよう推奨されています。

AIDevOpsAgentActionsPolicy is a broad default. For a production role, scope the permission policy to the actions that your operators intend to approve.

https://docs.aws.amazon.com/devopsagent/latest/userguide/working-with-devops-agent-working-with-directed-actions.html

11. デモ用リソースと操作用ロールの作成

GuardDuty に検出させるための、操作される側のリソースを3つ作ります。
この項で下準備は最後です。

リソース 使用場面 備考
EC2インスタンス(t4g.nano) LOW(終了保護の変更) 検証目的のため作ってすぐ停止。※止めてもEBS分で若干のコストが発生します
空の S3 バケット MEDIUM(ライフサイクルの設定) なんでもいいですが、今回は名前にランダムな文字列を付与
Lambda 関数 HIGH(関数 URL の認証を NONE に設定) 固定の文字列を返すのみ。実行ロールには何の権限も与えない

手始めに、EC2インスタンスを作成して即停止します。

AMI=$(aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64 \
  --query Parameter.Value --output text)
INSTANCE_ID=$(aws ec2 run-instances --image-id $AMI --instance-type t4g.nano \
  --metadata-options HttpTokens=required \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=demo-gd-target}]' \
  --query 'Instances[0].InstanceId' --output text)
aws ec2 wait instance-running --instance-ids $INSTANCE_ID
aws ec2 stop-instances --instance-ids $INSTANCE_ID --query 'StoppingInstances[0].CurrentState.Name' --output text

つづいて、S3 バケットと Lambda 関数

BUCKET=demo-gd-lc-$(openssl rand -hex 4)
aws s3api create-bucket --bucket $BUCKET --create-bucket-configuration LocationConstraint=$AWS_REGION

aws iam create-role --role-name demo-gd-url-fn-role \
  --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
mkdir -p fn && cat > fn/app.py <<'EOF'
def handler(event, context):
    return {"statusCode": 200, "body": "demo"}
EOF
python3 -c "import zipfile; z=zipfile.ZipFile('fn.zip','w'); z.write('fn/app.py','app.py'); z.close()"
sleep 15
FN=demo-gd-url-fn
aws lambda create-function --function-name $FN --runtime python3.12 --architectures arm64 \
  --handler app.handler --zip-file fileb://fn.zip \
  --role arn:aws:iam::$ACCOUNT_ID:role/demo-gd-url-fn-role --query State --output text

※sleep 15 はロールの反映待ちとして入れています

次に、操作専用のロールを作ります。デモ用の3つの操作だけを許可します。
同じロールで同じ操作を2回行うと、GuardDuty が1つの検出にまとめてしまうので(後述)、demo-gd-actor-1、demo-gd-actor-2 のように番号を変えて複数作成できるよう、関数化しておきます。

make_actor() {  # 使い方: make_actor <番号>
  local r=demo-gd-actor-$1
  aws iam create-role --role-name $r --assume-role-policy-document "$(jq -cn --arg a "arn:aws:iam::$ACCOUNT_ID:root" \
    '{Version:"2012-10-17",Statement:[{Effect:"Allow",Principal:{AWS:$a},Action:"sts:AssumeRole"}]}')" >/dev/null &&
  aws iam put-role-policy --role-name $r --policy-name demo-actions --policy-document "$(jq -cn \
    --arg i "arn:aws:ec2:$AWS_REGION:$ACCOUNT_ID:instance/$INSTANCE_ID" \
    --arg b "arn:aws:s3:::$BUCKET" \
    --arg f "arn:aws:lambda:$AWS_REGION:$ACCOUNT_ID:function:$FN" '{Version:"2012-10-17",Statement:[
      {Effect:"Allow",Action:"ec2:ModifyInstanceAttribute",Resource:$i},
      {Effect:"Allow",Action:"s3:PutLifecycleConfiguration",Resource:$b},
      {Effect:"Allow",Action:["lambda:CreateFunctionUrlConfig","lambda:UpdateFunctionUrlConfig"],Resource:$f}]}')" && echo "$r ok"
}
make_actor 1
make_actor 2

ロールを受けてコマンドを打つ関数も作成しておきます。
一時的な認証情報は環境変数で渡すのみで、ファイルには残しません。

as_actor() {  # 使い方: as_actor <番号> <aws のサブコマンド>
  local n=$1; shift
  local c
  c=$(aws sts assume-role --role-arn arn:aws:iam::$ACCOUNT_ID:role/demo-gd-actor-$n \
    --role-session-name demo-run-$n --query Credentials --output json) || return 1
  AWS_ACCESS_KEY_ID=$(jq -r .AccessKeyId <<<"$c") AWS_SECRET_ACCESS_KEY=$(jq -r .SecretAccessKey <<<"$c") \
    AWS_SESSION_TOKEN=$(jq -r .SessionToken <<<"$c") aws "$@"
}

CloudShell のセッションが切れたときは、最初の2行を実行した後で、以下のコマンドで変数を戻します。
名前から引き直すだけなので、値をファイルに残す必要がありません。

export AWS_REGION=ap-northeast-1
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
DEST_ARN=$(aws events describe-api-destination --name demo-devops-agent-webhook --query ApiDestinationArn --output text)
EB_ROLE_ARN=$(aws iam get-role --role-name demo-eventbridge-devops-agent --query Role.Arn --output text)
DLQ_ARN=$(aws sqs get-queue-attributes --attribute-names QueueArn --query Attributes.QueueArn --output text \
  --queue-url $(aws sqs get-queue-url --queue-name demo-devops-agent-dlq --query QueueUrl --output text))
TOPIC_ARN=arn:aws:sns:$AWS_REGION:$ACCOUNT_ID:demo-devops-agent-alert
INSTANCE_ID=$(aws ec2 describe-instances --filters Name=tag:Name,Values=demo-gd-target \
  Name=instance-state-name,Values=running,stopped --query 'Reservations[0].Instances[0].InstanceId' --output text)
BUCKET=$(aws s3api list-buckets --prefix demo-gd-lc- --query 'Buckets[0].Name' --output text)
FN=demo-gd-url-fn
SPACE_NAME='<Agent Space の名前>'  # 自分の Agent Space の名前に置き換える
SPACE_ID=$(aws devops-agent list-agent-spaces --query "agentSpaces[?name=='$SPACE_NAME'].agentSpaceId | [0]" --output text)
for v in DEST_ARN EB_ROLE_ARN DLQ_ARN INSTANCE_ID BUCKET SPACE_ID; do
  case "${!v}" in ""|None) echo "$v: 見つからない" ;; *) echo "$v: ok" ;; esac
done
aws sns get-topic-attributes --topic-arn "$TOPIC_ARN" >/dev/null 2>&1 && echo "TOPIC_ARN: ok" || echo "TOPIC_ARN: 見つからない"

前手順と同様に値そのものは出力させず、okと出すようにします。

DEST_ARN: ok
EB_ROLE_ARN: ok
DLQ_ARN: ok
INSTANCE_ID: ok
BUCKET: ok
SPACE_ID: ok
TOPIC_ARN: ok

見つからない場合はリソース名が見つかっていないので、名前が合っているかを見直してみてください。
SPACE_IDであれば Agent Space の名前を見直してください。

※変数が空のまま make_actor を実行すると、操作するリソースが正しく入っていないロールが作成されてしまいます。
 そのロールで操作すると権限エラーで失敗するため、make_actor は、変数がそろっているのを確かめてから実行してください。

ここまでで事前準備完了です。
以降は実際に検出の挙動を試してみます。

LOWのテスト

actor-1 のロールで、EC2 の終了保護を有効にしてみます。

as_actor 1 ec2 modify-instance-attribute --instance-id $INSTANCE_ID --disable-api-termination

結果は以下のようになりました。

イベント 経過時間
GuardDuty の検出(重要度2.0) 約3分42秒
EventBridge に届く 検出から約28秒
LOW のロググループに記録 操作から4-5分後

想定通り DevOps Agent のタスクは実行されず、Slack にも何も届きませんでした。
ロググループは、ARN などを出さないよう、種類や重要度、時刻だけを出して確かめました。

aws logs filter-log-events --log-group-name /aws/events/demo-gd-low \
  --start-time $(( ($(date +%s) - 120*60) * 1000 )) --query 'events[].message' --output text |
  jq -r '[.time, .detail.type, (.detail.severity|tostring), "count=" + (.detail.service.count|tostring)] | @tsv'
# 出力結果例
2026-10-28T06:27:00Z    DefenseImpairment:EC2/DisableOrModifyTools      2       count=1

加えて、別ロール(actor-2 や actor-3)で同様の操作をすると、それぞれ別の検出が行われました。
操作した人が違った場合、検出も別モノとして扱われることが確認できました。

2026-10-28T06:27:00Z    DefenseImpairment:EC2/DisableOrModifyTools      2       count=1
2026-10-28T06:36:05Z    DefenseImpairment:EC2/DisableOrModifyTools      2       count=1
2026-10-28T06:36:10Z    DefenseImpairment:EC2/DisableOrModifyTools      2       count=1

MEDIUMのテスト

S3バケットのライフサイクルを1日後に設定し、MEDIUMを検出させてみます。

as_actor 1 s3api put-bucket-lifecycle-configuration --bucket $BUCKET --lifecycle-configuration \
  '{"Rules":[{"ID":"demo-rapid-expire","Status":"Enabled","Filter":{"Prefix":""},"Expiration":{"Days":1}}]}'
イベント 経過時間
GuardDuty 検出(重要度5.0) 約4分45秒後
本調査のタスク作成 → SKIPPED 約9秒でスキップ
カスタムエージェントのタスク作成 → 完了 1分27秒
Slackに結果通知 操作から6〜7分

Slack には本調査スキップの通知と、カスタムエージェントの「run started」の投稿が並びました。
また、調査結果は run started のスレッドの中に返ってました。
カスタムエージェントが概要を解説してくれるので、Slackからすぐに検出内容を把握できます。

▼ Slack通知(カスタムエージェントのスレッド)

結果の中身は以下。

  • 判定は「要調査」
  • demo-gd-actor-1 が、1日で全オブジェクトを消すライフサイクルを設定した
  • 前後1時間に、他の気になる操作はなかった
  • バケットの中身は確認できていない

バケット名とアカウントID は出ていません。
プロンプトで指定した「書いてはいけないもの」の部分は守られていることが確認できました。

スレッド内の会話から本調査を実行できる

カスタムエージェント概要を確認し、調査した方がいいと判断されたものを調査にまわすことができます。

ここからが bidirectinal communication を活用する一番のメリットで、
結果通知のスレッドからアプリにメンションし、「調査(Investigation)を起票して深掘りしてほしい」や「S3の設定値を適切なものに変更してほしい」と頼むと、本調査を開始できます。

▼ スレッドから本調査を依頼する

▼ ほどなくして、本調査が開始されました

返信は英語で返ってきたものの、スレッドから返信するだけで本調査が開始できました。
これはAWS環境にログインすることなく実行できるので、初動がスムーズになりそうです。

▼進行中の本調査

調査時間は設定や内容によって変わりますが、10数分経過して、以下のような結果がSlackに投稿されました。

おそらくログなどからデモ用に実施したテストと判定しており、精度が高かったです。
バケットに関する問題点も的確に指摘されており、再発防止案も提案されていました。
※この調査にかかったコストは約$6.6ほどでした。

この挙動は GuardDuty だけでなく、色々なアラームにも応用できそうです。

HIGHのテスト

認証なしのLambda関数URLを作成します。
前述の通り、関数URL は CLI経由で作ります。
検証ではMEDIUM 検証のすぐ後に試したので、actor-5 を新しく作り、1分ほど経ってから操作しました。

make_actor 5
sleep 15  # ロール作成待ちのため念のため15秒だけ待機
as_actor 5 lambda create-function-url-config --function-name $FN --auth-type NONE --query AuthType --output text
イベント 経過時間
GuardDuty 検出(重要度8.0) 作成から約5分後
本調査開始 検出から約12秒
本調査完了通知(Slack) 開始から約8分

Slack には1件通知が届き、スレッド内で調査内容が更新されていきます。

▼ 本調査の途中経過(Slack)

初回は英語で返信が来たため、その後日本語で返すよう指示を出すと直りました。

もちろんWebアプリ側でも進捗が確認できます。

▼ 途中経過(Webアプリ)

↓最終的に、「認可されたエンジニアによる意図的な検出のテストで、侵害ではない」と結果が返りました。
今回もMEDIUM同様に的確な分析結果でした。

根拠として挙げられていたのは以下のポイント

  • actor-1〜5 のロールが連番で作られている
  • actor-5 は作成されてから65秒後、1回だけ操作している
  • インラインポリシーが3つのリソースに絞られている
  • 同一のIAMユーザが、同じCloudShellからロール作成、URLのパブリック化、GuardDuty 検出結果の確認、のアクションを順に実行している

修正案としても、「URLを維持したまま認証方式のみをAWS_IAMに変更し、認証なしの公開エクスポージャーを解消する」と提案がありました。

修正案を承認してみる

Slack から Lambda 関数へのタグ付けを依頼してみる

試しに、Lambda にタグを付ける操作を依頼してみます。

しかしながら、

  • Slack で頼むと「タグを付けます」と返ってくるものの、承認ボタンなどは表示されず、書き込み用の API も呼ばれなかった
  • スレッドで聞き直すと「承認フローで問題が発生し、タグ付けが実行されませんでした」と返ってきた
  • Slack用ロールの管理ポリシー(AIDevOpsChannelAccessPolicy)に、承認用の API(aidevops:UpdateApprovalAction)が入っていなかった
  • 承認に必要な権限をインラインで足して試しても、結果は変わらなかった

公式ドキュメントの directed actions のページには、承認の依頼について以下の記述がありました。

AWS DevOps Agent surfaces operator approval requests only in chat. If the agent invokes a mutating tool outside chat, for example during an autonomous investigation, the call fails instead of presenting an approval request.

https://docs.aws.amazon.com/devopsagent/latest/userguide/working-with-devops-agent-working-with-directed-actions.html

が、これは本調査に関する記述なので、リソースに変更を加える内容とは少し違いそう。
なので、あくまでも今回の検証ではうまく実行されなかった結果として残しています。今後の改善に期待。

Web アプリのチャットから承認する

Webアプリ上、左ペインの「新しいチャット」から依頼すると選択肢が表示されました。

Lambda 関数 demo-gd-url-fn の関数 URL の認証を、NONE から AWS_IAM に変えてください。関数 URL もロールも削除しないでください。ほかの変更もしないでください。

まず、試しに1回目は拒否してみました。

次に、同じチャット上で「さっきの変更を、もう一度実行してください。今度は承認します」と送り、2回目は Approve してみました。

承認してからすぐに AWS_IAM に変わりました。CloudTrail には、次のようにログが記録されました。

時刻(UTC) イベント 実行した内容
01:55:47 UpdateApprovalAction 拒否
02:00:23 UpdateApprovalAction 承認
02:00:25 UpdateFunctionUrlConfig 昇格用ロール DevOpsAgentActionsRole-AgentSpace-xxxx

ユーザが選択した際も UpdateApprovalAction として残るので、「だれが、いつ、何を承認したか?」を後から追跡可能です(以下でログを確認しました)。

aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=UpdateApprovalAction \
  --start-time $(date -u -d '-60 min' +%FT%TZ) --query 'Events[].[EventTime,EventName]' --output text

破壊的作業を頼んでみる

ついでに、同じチャットで「この関数 URL を削除してください」と頼んでみます。
Agentの回答は「申し訳ありませんが、AWS リソースの削除はお受けできません。コンソールか CLI から直接操作してください」と断られました。

この挙動は、公式ドキュメントの以下記述に合致しています。

Delete resources. The agent refuses delete-class operations, for example deleting an instance, bucket, table, function, or stack. The operator deletes resources themselves with their own credentials.

https://docs.aws.amazon.com/devopsagent/latest/userguide/working-with-devops-agent-working-with-directed-actions.html#operations-the-agent-will-not-perform

その他

同じ人が2回同じ操作をしても、どこにも通知されない

MEDIUM の検証中、3回目に同じロールで同じ内容を2回実施しましたが、検出は最初から count 2 として作成されました。
2回の操作が、検出が出る前に1件にまとめられたようです。

これは、GuardDuty が関連するシグナルを1つの検出にまとめるためと考えられます。
同じ内容であれば、重複を避けるために辻褄の合った動きと理解しました。

GuardDuty aggregates related signals into a single finding.

https://docs.aws.amazon.com/guardduty/latest/ug/custom-detection-rules.html

カスタムエージェントの履歴に、ペイロードがすべて出力される

Web アプリ上エージェントの履歴を開くと、受け取ったペイロードがそのまま出ます。

今回はdataに GuardDuty のfindingを丸ごと入れているので、アカウントIDやロールの固有ID、一時的なアクセスキーID、送信元のIPアドレス、バケット名などが全部見えてしまいます。閲覧できる権限やメンバー、外部に見せる際などには注意が必要と思いました。

費用について

DevOps Agent の料金は、エージェントが動いた秒数ごと課金で $0.0083/秒です(1時間あたり約 $29.88)。

https://aws.amazon.com/devops-agent/pricing/

今回の検証での目安です。時間は get-account-usage の差分から出しています。

重要度 DevOps Agent で動かしたもの 時間目安 費用目安
LOW なし 0 $0
MEDIUM 本調査のトリアージ(スキップ) 約6秒 約 $0.05
MEDIUM カスタムエージェント実行 1〜1.5 分 使用量に出てこなかったため未確認(秒数で計算すると $0.5〜0.75)
MEDIUM スレッドから頼んだ本調査 約13分 約$6.6
HIGH 本調査とステアリング 約12分 約$5.8

本調査は 1 回で 10 分前後、費用にすると $6 前後になる、と見ておくとよさそうです。
特に検出数の多いアカウントでは、LOW を DevOps Agent に送らないだけで大きな費用削減になると思います。

検証の後片付け

今回作ったものを削除する際の手順です。
EventBridge のルールは、ターゲットを外してから削除します。

export AWS_REGION=ap-northeast-1
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
for r in demo-gd-low demo-gd-medium demo-gd-high demo-gd-capture demo-guard-skipped; do
  ids=$(aws events list-targets-by-rule --rule $r --query 'Targets[].Id' --output text)
  [ -n "$ids" ] && aws events remove-targets --rule $r --ids $ids >/dev/null
  aws events delete-rule --name $r && echo "$r deleted"
done
aws events delete-api-destination --name demo-devops-agent-webhook
aws events delete-connection --name demo-devops-agent-webhook
aws iam delete-role-policy --role-name demo-eventbridge-devops-agent --policy-name invoke-api-destination
aws iam delete-role --role-name demo-eventbridge-devops-agent
aws sqs delete-queue --queue-url $(aws sqs get-queue-url --queue-name demo-devops-agent-dlq --query QueueUrl --output text)
aws sns delete-topic --topic-arn arn:aws:sns:$AWS_REGION:$ACCOUNT_ID:demo-devops-agent-alert && echo "topic deleted"
for LG in /aws/events/demo-gd /aws/events/demo-gd-low; do aws logs delete-log-group --log-group-name $LG; done
aws logs delete-resource-policy --policy-name demo-gd-events
for r in ec2-termination-protection-enabled s3-lifecycle-rapid-expiration lambda-function-url-public-access; do
  A=$(aws guardduty list-custom-detection-rule-associations --rule-id "$r" --query 'RuleAssociations[0].AssociationId' --output text)
  aws guardduty delete-custom-detection-rule-association --rule-id "$r" --association-id "$A"
done

次にリソースなどを片付けます。
$INSTANCE_ID、$BUCKET、$FN を使うので、もしCloudShellのセッションが切れていたら、手順11の変数を戻すコマンドを先に実行してください。終了保護は自身の権限で無効に戻します。

aws ec2 modify-instance-attribute --instance-id $INSTANCE_ID --no-disable-api-termination
aws ec2 terminate-instances --instance-ids $INSTANCE_ID --query 'TerminatingInstances[0].CurrentState.Name' --output text
aws s3api delete-bucket --bucket $BUCKET
aws lambda delete-function --function-name $FN
aws iam delete-role --role-name demo-gd-url-fn-role
for n in 1 2 3 4 5; do  # 作った番号に合わせる
  aws iam delete-role-policy --role-name demo-gd-actor-$n --policy-name demo-actions &&
  aws iam delete-role --role-name demo-gd-actor-$n && echo "demo-gd-actor-$n deleted"
done

※S3バケットは空のままであれば、そのまま消せます。関数 URL はLambda関数と一緒に消えます。

その他、以下不要であれば手動で削除します。

  • DevOps Agent のカスタムエージェント gd-medium-checker とトリガ、スキル demo-skip-medium(Web アプリ)
  • APIキー方式の Webhook と、ダウンロードしたcsvファイル
  • Slack連携解除と、双方向用に作られたロール
  • SNSのメールの購読(トピックを消せば一緒に消えます)
  • CloudShell に残った rules.json(検出のルールの中身)と fn/、fn.zip
  • デモで発生した GuardDuty 検出

まとめ的なもの

今回、EventBridge のルールを重要度の数値範囲で分けて、MEDIUM だけスキップ用のスキルとイベントトリガで受ける方法を用いつつ、LOW、MEDIUM、HIGH で重要度別の振り分けが可能になりました。

Slackアプリから実施できる内容がわかった点や、実運用にも取り込めそうな挙動が確認できて、おもしろい検証になりました。実際のケースに当てはめるにはまだまだ詰めが必要ですが、特にカスタムエージェントを使った運用は色々な場面で応用できそうだなと思いました。

今後も時間がある時に検証シてみたいと思います。

完

参考

この記事をシェアする

関連記事