
AWS Continuum for penetration testing の検出結果から、プライベートリポジトリにコード修正 PR を作ってみた
はじめに
クラウド事業統括本部の浅野です。
AWS Continuum for penetration testing(旧AWS Security Agent)には、ペネトレーションテストで見つかった脆弱性(finding)に対して、エージェントが修正のコードを書いてプルリクエストを作る機能があります。
連携したリポジトリがパブリックだと、この修正PRは作られません。今回は、リポジトリをプライベートにして、既存のペネトレーションテストの検出結果から修正PRを作り、そのPRをマージして再検証するところまで試しました。あわせて、同じ連携で使えるPRのコードレビューコメントの挙動についても確認しました。
コード修正とは
公式ドキュメントに書かれている内容をまとめると、次のとおりです。
- 接続済みのGitHubリポジトリに、検出結果を直すためのPRを作る機能
- 対象は、ペネトレーションテストとコードレビューの検出結果(今回試したのは、ペネトレーションテストの検出結果)
- 有効にする単位は、Agent Spaceに接続したリポジトリごと
- 検出結果の画面から手動で実行できる。ペネトレーションテストの設定で自動修正を有効にすると、検出結果が確認された時点で自動でも実行される(今回は手動のみ検証)
GitHub連携には、コード修正のほかにもリポジトリに関係する機能がいくつかあり、ややこしいので、一覧で整理しておきます。
| 機能 | 内容 |
|---|---|
| ソースコードの参照 | ペネトレーションテストが、連携したリポジトリのコードを読んで検証に使う |
| コード修正 | 検出結果を直すPRを作る |
| コードレビューコメント | PRにセキュリティ上の指摘をコメントする |
なお、2026年10月時点で、コード修正はプレビューとして無料で使えます。
構成
検証対象はOWASP Juice Shop(v20.3.0)です。ペネトレーションテストの実行環境は、公式のコンテナイメージをALB + ECS Fargateにデプロイし、Route 53のサブドメインを割り当てています。構成図です。

連携するリポジトリは、Juice Shopの公式リポジトリをフォークしたものです。このフォークは、最初はパブリックの状態で連携していたので、プライベートに切り替えます。GitHub Appのインストールと、Agent Spaceへの連携は、事前に済んでいる前提です。
ペネトレーションテストで出ていた検出結果のうち、次の3件を、修正PRの対象に選びました。
| 検出結果 | 重大度 | 内容 |
|---|---|---|
/rest/memoriesの情報漏洩 |
High | 未認証で他ユーザーのメールアドレス・パスワードハッシュなどが取れる |
| 商品検索のSQLインジェクション | High | qパラメータにUNION句を足して、Usersテーブルの内容が取れる |
| ファイルアップロードのXXE | High | XMLのアップロードで、サーバー上のファイルが読める |
やってみた
リポジトリをプライベートにする
フォークしたリポジトリのSettingsを開き、Danger Zoneを見ると、「Change visibility」のボタンが押せません。「For security reasons, you cannot change the visibility of a fork.」と表示されています。

今回のリポジトリは、OWASP Juice Shopの公式リポジトリをフォークして作ったものなので、フォークのままではプライベートに切り替えられません。フォークの関係を外す「Leave fork network」を先に実行すると、プライベートに切り替えられるようになります。数分かかります。フォーク元がない通常のリポジトリでは、この手順は要りません。

コード修正を有効にする
Agent Spaceのペネトレーションテストの設定を編集すると、「接続されたリポジトリ」の欄に「是正」の列があります。この時点では、リポジトリタイプが「公開」、是正が「無効化済み」のままでした。GitHub側をプライベートにしても、この表示は変わっていません。

ここで是正をオンにして保存しましたが、保存した直後の時点では、リポジトリタイプは「公開」のまま、コード修正も有効になっていませんでした。
一方で、Agent SpaceのGitHubの欄にある「編集」→「機能を管理」の画面でも、同じコード修正の設定を操作できます。こちらでは、リポジトリタイプが変更どおり「プライベート」と表示され、コードレビューコメントとコード修正はどちらも無効になっていました。

コードレビューコメントとコード修正を両方オンにして保存しました。コードレビューコメントをオンにすると、どのPRでレビューを走らせるかを決めるトリガーのフィルターを設定できます。トリガーイベントには、次の3つの選択肢があります。
- プルリクエストのレビューの準備ができました: レビューの準備が整ったPRが開かれたとき、再オープンされたとき、コードが更新されたとき(ドラフトが終了したときも含む)
- プルリクエストの下書きが作成されました: ドラフトのPRが開かれたとき、再オープンされたとき、コードが更新されたとき
- プルリクエストのラベルが追加されました: 一致するラベルが追加されたとき(ラベルの条件が必要)
今回は、PRを開いて、レビューを受けられる状態になったときにレビューが走ってほしいので、「プルリクエストのレビューの準備ができました」を選びました。また、条件としてターゲットブランチも設定できます。今回はmasterにしました。この設定はコードレビューコメントを走らせるPRの条件で、コード修正のPRの作成先を決める設定ではありません。


保存すると、Agent Spaceの画面に「統合リソースが追加されました」と表示され、リポジトリのコード修正が「有効化済み」になりました。

AWS CLIのlist-integrated-resourcesで連携の状態を見ると、「機能を管理」を保存する前後で次のように変わっていました。
$ aws securityagent list-integrated-resources \
--region ap-northeast-1 \
--agent-space-id <agent-space-id> \
--query 'integratedResourceSummaries[].{repo:resource.githubRepository.name,accessType:resource.githubRepository.accessType,capabilities:capabilities.github}'
| 状態 | accessType |
remediateCode |
|---|---|---|
| GitHub側をプライベートにした直後 | PUBLIC |
False |
| ペネトレーションテストの設定で是正をオンにして保存 | PUBLIC |
False |
| 「機能を管理」でコード修正をオンにして保存 | PRIVATE |
True |
今回の結果では、「機能を管理」でコード修正をオンにして保存した場合に、accessTypeがPRIVATE、remediateCodeがTrueになりました。この機能を確実に有効にするには、この「機能を管理」の画面から行ったほうがよさそうです。
ペネトレーションテストの設定の「是正」と、「機能を管理」の「コード修正」は、同じ設定でした。片方を変更すると、もう片方にも反映されました。

検出結果から「コードを修正」を実行する
以前実行したペネトレーションテストの結果の中から、まず/rest/Memoriesの検出結果を対象にしました。
この検出結果は、次のような脆弱性です。Juice Shopには、ユーザーが写真を投稿して一覧で見られる「Photo Wall」という機能があり、その一覧を返すAPIがGET /rest/memoriesです。このAPIは、ログインなしで呼べるうえに、写真と一緒に、投稿したユーザーの情報をまるごと返していました。含まれるのは、メールアドレス、ソルトなしのMD5のパスワードハッシュ、ロール、会員トークンなどです。ログインしていない誰でも、他のユーザーのこれらの情報を取得できます。
検出結果には、推奨される対応も書かれています。
- このAPIで返すユーザー情報を、IDや表示名など、必要なものだけに限定する
- ユーザーモデルの側で、パスワードなどの機微な属性が、既定ではレスポンスに入らないようにする
- このAPIにも認証を求める
- 漏れた会員トークンをローテーションし、パスワードのリセットを求める。MD5のハッシュは、ソルト付きの方式に移行する
ペネトレーションテストの実行を開き、検出結果の詳細に出ている「コードを修正」を押します。エージェントがどういうPRを作るのかは、このあと確認します。

「コード修正が開始されました」というバナーが出ます。

押した直後の画面には、進捗も完了も表示されませんでした。再読み込みしても変わりません。

進捗は、AWS CLIのbatch-get-findingsが返すcodeRemediationTaskで確認できます。
$ aws securityagent batch-get-findings \
--region ap-northeast-1 \
--agent-space-id <agent-space-id> \
--finding-ids <finding-id> \
--query 'findings[0].codeRemediationTask'
実行中は次のように返ります。
{"status": "IN_PROGRESS", "statusReason": "Code remediation in progress"}
完了すると、PRのリンクが入ります。
{
"status": "COMPLETED",
"statusReason": "1 remediation(s) in COMPLETED status",
"taskDetails": [
{
"repoName": "<owner>/juice-shop",
"pullRequestLink": "https://github.com/<owner>/juice-shop/pull/1"
}
]
}
ボタンを押してから約2分で、リポジトリにPRが作られました。押した直後の画面は更新されませんでしたが、この検出結果を開き直すと、詳細の下のほうに「コード修正」の欄があり、ステータスとPRのリンクが表示されていました。

作られたPRの中身を見る
作成者はaws-security-agent[bot]で、ブランチ名はaws-continuum-<数字>、マージ先はmaster(このフォークのデフォルトブランチ)です。1コミットで、コミットメッセージの先頭に[skip ci]が付いています。[skip ci]は、コミットメッセージにこの文字列を入れておくと、そのコミットではGitHub Actionsなどのワークフローが起動されなくなる記法です。つまり、このPRのコミットでは、リポジトリのCIが動きません。PRに含まれるテストの書き換えが通るかどうかは、自分で確認する必要があります。

PRの本文は、先頭の「Pentest Information」と、そのあとの「Summary / Problem / Fix / Files Changed」(何が問題で、どう直したか)でできています。
「Pentest Information」には、このPRがどの検出結果から作られたかを示す情報が入っていました。
| 項目 | 内容 |
|---|---|
| Pentest Title | ペネトレーションテストの名前 |
| Finding Title | 検出結果のタイトル |
| Web URL | コンソールの、該当するジョブの「検出結果」タブへのリンク |
| Agent Space ID | Agent SpaceのID |
| Pentest ID | ペネトレーションテストのID |
| Pentest Job ID | 検出結果が属する実行(ジョブ)のID |
| Finding ID | 検出結果のID |
PRからコンソールへは、Web URLか、これらのIDでたどれました。コンソールからPRへは、検出結果の「コード修正」の欄のリンクと、先ほどのCLIの出力に出ていたtaskDetails[].pullRequestLinkでたどれました。
変更はroutes/memory.tsの1行です。GET /rest/memoriesの処理は、Sequelize(Node.jsのORM)のfindAllで写真の一覧を取り、includeで、投稿したユーザーのモデル(UserModel)を結合して返しています。修正前は、include: [UserModel]とモデルだけを渡していたので、ユーザーの全カラム(パスワードハッシュ、会員トークン、TOTPシークレット、ロール、最終ログインIPなど)が、そのままレスポンスに入っていました。修正後は、includeに{ model: UserModel, attributes: [...] }の形でオプションを渡し、attributesに返すカラムをid・email・usernameの3つだけ指定しています。

- const memories = await MemoryModel.findAll({ include: [UserModel] })
+ const memories = await MemoryModel.findAll({ include: [{ model: UserModel, attributes: ['id', 'email', 'username'] }] })
PRの本文には、id・email・usernameは「Photo Wallのフロントエンドが必要とする最小限の項目」と書かれていましたし、パスワードなども抜けているので修正対応できていそうです。
複数の検出結果をまとめて指定する
ここまでは、コンソールの「コードを修正」ボタンから、検出結果を1件ずつ実行しました。今度は、StartCodeRemediationのAPIで、2件の検出結果をまとめて指定してみます。このAPIは、検出結果のIDをリストで受け取ります。
指定したのは、ペネトレーションテストで見つかった、次の2件です。どちらも重大度はHighです。
商品検索のSQLインジェクション
GET /rest/products/searchのqパラメータは、検索語をそのままSQLに文字列として連結して、商品テーブルへのクエリを組み立てています。'))でLIKE句を閉じて、あとにUNION SELECT ... FROM Usersを続けると、ログインなしで、全ユーザーのメールアドレスとパスワードハッシュを取得できます。検出結果が推奨している対応は、次の3つです。
- 検索語をパラメータ化したクエリやORMのバインドで渡して、常にデータとして扱う
- 生のSQLやスタックトレースをクライアントに返さず、汎用のエラーにする
- MD5を、ソルト付きの遅いハッシュ(bcryptやargon2)に移行する
ファイルアップロードのXXE
POST /file-uploadは、ログインなしでXMLファイルをアップロードできます。このXMLを読み込むときに、XMLの中に書かれた「外部のファイルを取り込む指定」(外部エンティティ)を、そのまま処理してしまいます。そのため、file:///etc/passwdのようなサーバー上のファイルを指定したXMLをアップロードすると、そのファイルの中身が、エラーの本文に入って返ってきます。データを大量に展開させる攻撃(Billion Laughs)で、サービスを止められる問題もあります。検出結果が推奨している対応は、主に次の3つです。
- XMLパーサーで、外部エンティティの展開とDTDの読み込みを無効にする(これだけで、ファイルの読み取りもサービス停止も防げる)
- パースした内容を、エラーメッセージに入れてクライアントに返さない
- 使われていない(非推奨の)XMLアップロードの経路は、廃止するか、認証の後ろに置く
この2件を、1回の呼び出しで指定しました。AWS CLIでは、次のコマンドになります。
$ aws securityagent start-code-remediation \
--region ap-northeast-1 \
--agent-space-id <agent-space-id> \
--pentest-job-id <pentest-job-id> \
--finding-ids <sqli-finding-id> <xxe-finding-id>
レスポンスは空で、エラーなく受理されました。結果は、検出結果ごとに別のPRです。2件で2本のPRができました。
作成者はどちらもaws-security-agent[bot]で、本文の構成は先ほどのPRと同じです。


| 検出結果 | PR | 作成までの時間 | 変更 |
|---|---|---|---|
| 商品検索のSQLインジェクション | #3 | 約3分 | 3ファイル(+45 / −88) |
| ファイルアップロードのXXE | #4 | 約4分 | 4ファイル(+92 / −22) |
PRは、検出結果に対して1:1で作られます。PRの「Pentest Information」にも、その1件の検出結果のIDが入っていて、ジョブIDは、--pentest-job-idに指定したものと同じです。
2つのPRの差分を読んでみました。
SQLインジェクションのPR(#3)は、3ファイルを変更しています。実コードはroutes/search.tsの1ファイルで、検索語をSQLの文字列に直接埋め込んでいたところを、値をエスケープして埋め込む形に変えています。残りの2ファイルは、テストコードでした(test/api/search.test.tsとtest/cypress/e2e/search.spec.ts)。
Juice Shopは、脆弱性を学ぶためのアプリなので、既存のテストは「攻撃が成功すること」を確かめる内容でした。たとえば、SQLインジェクションで構文エラーが返ること、UNIONでUsersテーブルのメールアドレスとパスワードハッシュが取れること、といったものです。脆弱性を直すと、これらのテストは失敗します。そこでエージェントは、テストも「攻撃文字列が検索語として扱われて、0件が返ること」を確かめる形に書き換えています。このように修正したコードの背景に合わせてテストコードまで柔軟に変更してくれました。

XXEのPR(#4)は、4ファイルを変更しています。実コードはlib/xml.tsとroutes/fileUpload.ts、テストコードはtest/api/file-upload.test.tsとtest/server/xml.unit.test.tsです。
主にroutes/fileUpload.tsに変更がありました。Juice Shopには、攻撃に成功すると「チャレンジ達成」と判定される仕組みがあります。XXEのチャレンジは、攻撃で読めたファイルの中身を見て、達成を判定していました。修正で攻撃が成功しなくなると、この判定ができなくなります。そこでエージェントは、アップロードされたXMLの文字列を正規表現で調べて、/etc/passwdなどを狙った内容なら達成と判定するコードを足していました。脆弱性を直すだけなら要らないコードですが、PRの本文には、この達成判定を保つためと書かれていました。このように単純に検知した脆弱性を修正するだけじゃなく、アプリの目的・コンテキストも加味した修正を提案してくれていました。

PRをマージして再検証する
#1(/rest/memories)と#3(SQLインジェクション)の2つのPRを、フォークのmasterにマージしました。
マージしたあとのmasterから、リポジトリのDockerfileでイメージ(arm64)をビルドし、ECRにpushして、ECSのサービスを再デプロイしました。
再検証は、検出結果の詳細画面で「この検出結果を選択して再検証する」にチェックを入れ、「1個の検出結果を再検証する」を押します。今回は2件を、1件ずつ別のジョブで実行しました。


2つの再検証ジョブが、同時に進行中になっています。

| 対象の検出結果 | PR | 期間(実時間) | タスク時間 | 判定 |
|---|---|---|---|---|
| 商品検索のSQLインジェクション | #3 | 37分 | 0.449h | Resolved |
/rest/memoriesの情報漏洩 |
#1 | 27分 | 0.321h | Resolved |
再検証の結果、SQLインジェクションは再現せず、検出結果は解決済み(Resolved)になりました。再検証のジョブには、「NOT reproducible; endpoint is parameterized」(再現しない。エンドポイントはパラメータ化されている)というタイトルの検出結果が記録されていました。

説明には、エージェントが確かめた内容が書かれています。
- 元の指摘にあった、エラーメッセージからクエリが漏れるプローブ(
q='()が、500とクエリの表示ではなく、200と空の配列({"status":"success","data":[]})を返すようになった q='は、名前が先頭にアポストロフィで始まる実在の商品を返す。引用符が、SQLの構文ではなく、検索語の一部として扱われている- 約30種類のSQLインジェクションのペイロードを試して、元の攻撃(UsersテーブルのUNION)も含め、データは返らなかった
このように、入力がSQLの構文ではなくデータとして扱われることが確認できて、#3の修正で、脆弱性が解決したと判定されました。
/rest/memoriesのほうも、再検証で解決済みになりました。再検証のジョブには、「Memories endpoint no longer leaks credential material」(認証情報の漏洩はなくなった)というタイトルの検出結果が記録されていました。

こちらの説明には、返されるユーザー情報が{id, email, username}だけになり、パスワードハッシュ・会員トークン・TOTPシークレット・ロールは含まれなくなったことが書かれていました。emailは残っていますが、エージェントは「残っているのはメールアドレスとユーザー名だけで、検出結果が主張していた認証情報の漏洩には当たらない」と判断しています。
PRでemailを残した修正でも、検出結果が根拠にしていたパスワードハッシュと会員トークンが消えたので、Resolvedになりました。
プルリクエストのコードレビューの挙動を確認する
コードレビューコメントの機能そのものは、以前の記事で試しています。今回確認したいのは、コード修正と同じGitHub連携の上で、エージェントが作った修正PRにも、コードレビューのコメントが付くかどうかです。付くなら、エージェントが書いた修正も、人が書いたコードと同じように、マージする前にセキュリティの観点でレビューにかけられます。比較のために、人が作ったPRでの動きも確認しました。
コードレビューコメントは、先ほど「機能を管理」で、コード修正と一緒に有効にしました。コードレビューのタブでも、有効になっていることを確認できます。

人が作ったPR
脆弱性を含むコードを適当に書いたPRを、試しに作成してみました(master宛、1ファイル、ドラフトではない通常のPR)。
PRを開いてから約23秒で、aws-security-agent[bot]が「AWS Security Agent is reviewing your pull request and will post feedback shortly.」とコメントしました。約4分後にレビューが終わり、「2 security findings: 2 critical」というコメントと、該当の行へのインラインコメントが2件付きました。

インラインコメントには、重大度(CRITICAL)、何が問題か、証拠となるペイロード、推奨する修正が書かれています。「このファイルはまだどのルートにも接続されていないので、いまは休眠しているが、接続された時点で悪用できる」という注記まで読み取っていました。また、コメントの末尾には、自動修正を依頼する場合は、PRの会話に@AWS-Security-Agent fix all findingsとコメントするよう書かれています。ドキュメントにも、コード修正をオンにしていれば、PRの作成者がこのコメントで、エージェントにレビューの指摘を直させられる、と書かれています(ドキュメントでは、末尾にピリオドが付いた@AWS-Security-Agent fix all findings.の形で載っています)。
案内どおりに、PRの会話にコメントしてみました。

約15分待っても、エージェントの反応はありませんでした。小文字の書き方、末尾のピリオドを付けた書き方、インラインコメントへの返信でも試しましたが、同じでした。手動でレビューを起動するコメント(@AWS-Continuum-Pentesting review)も、このPRで試しましたが、反応はありませんでした。色々調べましたが理由は特定できませんでした。
Security Agentが作ったPR
一方で、Security Agentが作った修正PR(#1)には、コードレビューのコメントが付きませんでした。手動で起動するコメント(@AWS-Continuum-Pentesting review)をPRの会話に投稿しても、反応はありませんでした。
そこで、XXEの修正PR(#4)を、いったんDraftに戻してから、もう一度「Ready for review」にして開き直してみました。



すると、コメントが付きました。人が作ったPRのときのようなレビューの形式のコメントではなく、「No issues identified.」(指摘なし)という通常のコメントが1件です。

エージェントが自動で作ったPRにはコメントが付かず、開き直したときに付いたのは、トリガーのイベントが「プルリクエストのレビューの準備ができました」なので、PRを開き直す操作で、このイベントが発生したからだと思います。
コードレビューに関しては一部ドキュメントと異なる挙動だったり、ドキュメントだけでは読み取れない挙動をしました。
最後に
今回は、プライベートリポジトリで、既存の検出結果からコード修正のPRを作るところから、マージ、再検証までを試し、さらにAgentが自動で作成した修正PRにコードレビューコメントがつくのかまで確認しました。設定の確認先が2か所にあることと、コード修正PR作成中はコンソール画面に進捗が出ないこと(CLIで確認する)は、事前に知っておくと便利だと思います。
今回のケースで分かったこととして、コード修正のPR精度は高いものであり、単に脆弱性の修正だけでなくアプリの目的やコードの意図を汲み取ってテストコードまで修正してくれるなどの挙動が確認できました。
エージェントが作成してくれたPRの中身をそのまま適用しても脆弱性は修正されましたが、運用では必ず人間の手で差分を読むことをおすすめします。
今回は以上です。










