AWS Security Agent 検証 — 自動コード修正(Code Remediation)の品質評価
目次
はじめに
第1回では REST API に対するペネトレーションテストで 5 件中 4 件の脆弱性を検出し、第2回ではソースコード提供による検出数の変化を、第3回では GraphQL API に対する検出能力を、第4回では認証フロー対応と検出範囲の変化を検証した。
第1〜4回は全て「検出」に焦点を当てていた。AWS Security Agent にはペンテストで検出した脆弱性に対して自動的にコード修正を生成し、GitHub リポジトリに PR を作成する Code Remediation 機能がある。本記事では第4回と同じ Flask アプリに対してペンテストを実行し、検出された脆弱性に対する自動修正の品質を検証する。
公式ドキュメント: Remediate a penetration test finding
前提条件と制約
Code Remediation には以下の制約がある。
- us-east-1 のみ — Code Remediation は現時点で us-east-1 リージョンでのみ利用可能
- GitHub 連携が必須 — AWS Security Agent GitHub App をインストールし、リポジトリを Agent Space に接続する必要がある。Private リポジトリの場合は GitHub に直接 PR が作成される。Public リポジトリの場合はセキュリティ上の理由から PR ではなく diff ファイルのダウンロードとなる
- VPC から GitHub へのアクセスが必要 — Agent のテスト環境からソースコードを取得するため、NAT Gateway 等でインターネットアクセスを確保する必要がある
第1〜4回は ap-northeast-1 で実施したが、本記事は us-east-1 で実施する。
検証環境
第4回と同じ Flask + pyotp アプリ(5つの脆弱性を埋め込み済み)を使用する。
- アプリ: Flask + pyotp + SQLite、TOTP 2FA 有効
- GitHub リポジトリ:
shinyaz/auth-vuln-app(Private) - リージョン: us-east-1
- 認証: Secrets Manager 経由で testuser のクレデンシャルを提供
ソースコード全文(app.py)
Flask + pyotp で構築。TOTP_ENABLED=true 環境変数で 2FA を有効化する。TOTP Secret は全アカウント共通で JBSWY3DPEHPK3PXP(RFC 6238 のテスト値)を使用している。
from flask import Flask, request, jsonify, session, redirect, render_template_string
import sqlite3
import os
import pyotp
app = Flask(__name__)
app.secret_key = "insecure-secret-key-for-testing"
DB_PATH = "/tmp/auth_vuln.db"
TOTP_SECRET = "JBSWY3DPEHPK3PXP"
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY, username TEXT UNIQUE,
password TEXT, role TEXT DEFAULT 'user', email TEXT, phone TEXT);
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY, name TEXT, category TEXT, price REAL);
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY, user_id INTEGER,
content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
DELETE FROM users; DELETE FROM products; DELETE FROM comments;
INSERT INTO users (id, username, password, role, email, phone) VALUES
(1, 'admin', 'admin123', 'admin', 'admin@example.com', '555-0100'),
(2, 'testuser', 'testpass123', 'user', 'test@example.com', '555-0200'),
(3, 'alice', 'alice123', 'user', 'alice@example.com', '555-0300');
INSERT INTO products (id, name, category, price) VALUES
(1, 'Laptop', 'electronics', 999.99), (2, 'Phone', 'electronics', 699.99),
(3, 'Desk', 'furniture', 299.99), (4, 'Chair', 'furniture', 199.99);
INSERT INTO comments (user_id, content) VALUES
(1, 'Welcome to the dashboard!'), (2, 'This is a test comment.');
""")
conn.commit()
conn.close()
LAYOUT_HEAD = """<!DOCTYPE html><html><head><title>Auth Vuln App</title></head><body>
<nav>
<a href="/">Home</a> | <a href="/search">Search</a> |
<a href="/api/products">Products API</a> |
{% if session.get('user_id') %}
<a href="/dashboard">Dashboard</a> |
<a href="/profile/{{ session['user_id'] }}">Profile</a> |
{% if session.get('role') == 'admin' %}<a href="/admin/users">Admin</a> |{% endif %}
<a href="/logout">Logout ({{ session['username'] }})</a>
{% else %}<a href="/login">Login</a>{% endif %}
</nav><hr>"""
LAYOUT_FOOT = "</body></html>"
HOME_PAGE = LAYOUT_HEAD + """
<h1>Auth Vuln App</h1>
<h2>Public Pages</h2>
<ul>
<li><a href="/search">Search Products</a></li>
<li><a href="/api/products">Products API</a></li>
<li><a href="/api/products?category=electronics">Electronics</a></li>
</ul>
<h2>Authenticated Pages</h2>
<ul>
<li><a href="/dashboard">Dashboard</a> (requires login)</li>
<li><a href="/profile/1">User Profile</a> (requires login)</li>
<li><a href="/admin/users">Admin Panel</a> (requires admin)</li>
</ul>""" + LAYOUT_FOOT
LOGIN_PAGE = LAYOUT_HEAD + """
<h1>Login</h1>
{% if error %}<p style="color:red">{{ error }}</p>{% endif %}
<form method="POST" action="/login">
<label>Username: <input type="text" name="username"></label><br>
<label>Password: <input type="password" name="password"></label><br>
{% if totp_required %}
<label>2FA Code: <input type="text" name="totp_code"></label><br>
{% endif %}
<button type="submit">Login</button>
</form>""" + LAYOUT_FOOT
SEARCH_PAGE = LAYOUT_HEAD + """
<h1>Search Products</h1>
<form method="GET" action="/search">
<input type="text" name="q" value="{{ query }}">
<button type="submit">Search</button>
</form>
{% if query %}
<h2>Results for: {{ query | safe }}</h2>
<ul>{% for p in results %}<li>{{ p['name'] }} - ${{ p['price'] }}</li>{% endfor %}</ul>
{% endif %}""" + LAYOUT_FOOT
DASHBOARD_PAGE = LAYOUT_HEAD + """
<h1>Dashboard</h1>
<p>Welcome, {{ session['username'] }}!</p>
<h2>Comments</h2>
<form method="POST" action="/dashboard/comment">
<textarea name="content" rows="3" cols="40"></textarea><br>
<button type="submit">Post Comment</button>
</form>
<ul>{% for c in comments %}<li>{{ c['content'] | safe }}</li>{% endfor %}</ul>
""" + LAYOUT_FOOT
PROFILE_PAGE = LAYOUT_HEAD + """
<h1>User Profile</h1>
<table>
<tr><td>ID</td><td>{{ user['id'] }}</td></tr>
<tr><td>Username</td><td>{{ user['username'] }}</td></tr>
<tr><td>Email</td><td>{{ user['email'] }}</td></tr>
<tr><td>Phone</td><td>{{ user['phone'] }}</td></tr>
<tr><td>Role</td><td>{{ user['role'] }}</td></tr>
</table>""" + LAYOUT_FOOT
ADMIN_PAGE = LAYOUT_HEAD + """
<h1>Admin - User Management</h1>
<table border="1">
<tr><th>ID</th><th>Username</th><th>Email</th><th>Role</th></tr>
{% for u in users %}
<tr><td>{{ u['id'] }}</td><td><a href="/profile/{{ u['id'] }}">{{ u['username'] }}</a></td>
<td>{{ u['email'] }}</td><td>{{ u['role'] }}</td></tr>
{% endfor %}
</table>""" + LAYOUT_FOOT
@app.route("/")
def index():
return render_template_string(HOME_PAGE)
@app.route("/search")
def search():
query = request.args.get("q", "")
results = []
if query:
conn = get_db()
results = conn.execute("SELECT * FROM products WHERE name LIKE ?", (f"%{query}%",)).fetchall()
conn.close()
return render_template_string(SEARCH_PAGE, query=query, results=results)
@app.route("/api/products")
def api_products():
category = request.args.get("category", "")
conn = get_db()
if category:
query = f"SELECT * FROM products WHERE category = '{category}'"
try:
results = conn.execute(query).fetchall()
except Exception as e:
conn.close()
return jsonify({"error": str(e)}), 500
else:
results = conn.execute("SELECT * FROM products").fetchall()
conn.close()
return jsonify([dict(r) for r in results])
TOTP_ENABLED = os.environ.get("TOTP_ENABLED", "false").lower() == "true"
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "GET":
return render_template_string(LOGIN_PAGE, error=None, totp_required=TOTP_ENABLED)
username = request.form.get("username", "")
password = request.form.get("password", "")
conn = get_db()
user = conn.execute("SELECT * FROM users WHERE username = ? AND password = ?",
(username, password)).fetchone()
conn.close()
if not user:
return render_template_string(LOGIN_PAGE, error="Invalid credentials",
totp_required=TOTP_ENABLED), 401
if TOTP_ENABLED:
totp_code = request.form.get("totp_code", "")
totp = pyotp.TOTP(TOTP_SECRET)
if not totp.verify(totp_code, valid_window=1):
return render_template_string(LOGIN_PAGE, error="Invalid 2FA code",
totp_required=True), 401
session["user_id"] = user["id"]
session["username"] = user["username"]
session["role"] = user["role"]
return redirect("/dashboard")
@app.route("/logout")
def logout():
session.clear()
return redirect("/")
@app.route("/profile/<int:user_id>")
def profile(user_id):
if "user_id" not in session:
return redirect("/login")
conn = get_db()
user = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
conn.close()
if not user:
return "User not found", 404
return render_template_string(PROFILE_PAGE, user=dict(user))
@app.route("/dashboard")
def dashboard():
if "user_id" not in session:
return redirect("/login")
conn = get_db()
comments = conn.execute("SELECT * FROM comments ORDER BY created_at DESC").fetchall()
conn.close()
return render_template_string(DASHBOARD_PAGE, comments=comments)
@app.route("/dashboard/comment", methods=["POST"])
def post_comment():
if "user_id" not in session:
return redirect("/login")
content = request.form.get("content", "")
conn = get_db()
conn.execute("INSERT INTO comments (user_id, content) VALUES (?, ?)",
(session["user_id"], content))
conn.commit()
conn.close()
return redirect("/dashboard")
@app.route("/admin/users")
def admin_users():
if "user_id" not in session:
return redirect("/login")
conn = get_db()
users = conn.execute("SELECT * FROM users").fetchall()
conn.close()
return render_template_string(ADMIN_PAGE, users=users)
@app.route("/.well-known/aws/securityagent-domain-verification.json")
def verify():
return jsonify({"token": "<verification-token>"})
if __name__ == "__main__":
init_db()
app.run(host="0.0.0.0", port=5000, debug=False)環境構築手順(EC2 + GitHub 連携 + NAT Gateway)
REGION=us-east-1
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
AGENT_SPACE_ID=<your-agent-space-id>
# 1. GitHub リポジトリ作成・プッシュ
gh repo create auth-vuln-app --private
cd /tmp && mkdir auth-vuln-app-repo && cd auth-vuln-app-repo
git init
# app.py と requirements.txt を配置
git add . && git commit -m "Initial commit"
git remote add origin git@github.com:<your-org>/auth-vuln-app.git
git push -u origin main
# 2. EC2 起動(パブリックサブネット)
AMI_ID=$(aws ec2 describe-images --region $REGION --owners amazon \
--filters "Name=name,Values=al2023-ami-2023*-x86_64" "Name=state,Values=available" \
--query "sort_by(Images, &CreationDate)[-1].ImageId" --output text)
SG_ID=$(aws ec2 create-security-group --region $REGION \
--group-name "remediation-test-sg" --description "Remediation test" \
--vpc-id <your-vpc-id> --query "GroupId" --output text)
aws ec2 authorize-security-group-ingress --region $REGION \
--group-id "$SG_ID" --protocol tcp --port 80 --cidr <vpc-cidr>
# SG 自己参照ルール(Agent コンテナ → EC2)
aws ec2 authorize-security-group-ingress --region $REGION \
--group-id "$SG_ID" --protocol tcp --port 80 --source-group "$SG_ID"
INSTANCE_ID=$(aws ec2 run-instances --region $REGION \
--image-id "$AMI_ID" --instance-type t3.small \
--subnet-id <your-public-subnet-id> --security-group-ids "$SG_ID" \
--iam-instance-profile Name=<your-ssm-profile> \
--associate-public-ip-address \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=remediation-test-target}]' \
--query "Instances[0].InstanceId" --output text)
# 3. アプリデプロイ(SSM 経由)
aws ssm send-command --region $REGION --instance-ids "$INSTANCE_ID" \
--document-name "AWS-RunShellScript" \
--parameters commands='["dnf install -y python3-pip",
"pip3 install flask pyotp gunicorn",
"mkdir -p /opt/app",
"# app.py を base64 でデコードして配置",
"cd /opt/app && TOTP_ENABLED=true python3 -c \"from app import init_db; init_db()\"",
"cd /opt/app && TOTP_ENABLED=true gunicorn -w 4 -b 0.0.0.0:80 --timeout 120 -e TOTP_ENABLED=true --daemon app:app"]'
# 4. NAT Gateway 作成(Agent コンテナから GitHub へのアクセス用)
EIP_ALLOC=$(aws ec2 allocate-address --region $REGION --domain vpc --query "AllocationId" --output text)
NAT_GW=$(aws ec2 create-nat-gateway --region $REGION \
--subnet-id <your-public-subnet-id> --allocation-id "$EIP_ALLOC" \
--query "NatGateway.NatGatewayId" --output text)
aws ec2 wait nat-gateway-available --region $REGION --nat-gateway-ids "$NAT_GW"
# 5. プライベートサブネット作成(Agent コンテナ用)
PRIVATE_SUBNET=$(aws ec2 create-subnet --region $REGION \
--vpc-id <your-vpc-id> --cidr-block "172.31.128.0/24" \
--availability-zone <your-az> \
--query "Subnet.SubnetId" --output text)
PRIVATE_RT=$(aws ec2 create-route-table --region $REGION \
--vpc-id <your-vpc-id> --query "RouteTable.RouteTableId" --output text)
aws ec2 create-route --region $REGION \
--route-table-id "$PRIVATE_RT" --destination-cidr-block "0.0.0.0/0" \
--nat-gateway-id "$NAT_GW"
aws ec2 associate-route-table --region $REGION \
--route-table-id "$PRIVATE_RT" --subnet-id "$PRIVATE_SUBNET"
# 6. Target Domain 作成・検証
PRIVATE_DNS=$(aws ec2 describe-instances --region $REGION \
--instance-ids "$INSTANCE_ID" \
--query "Reservations[0].Instances[0].PrivateDnsName" --output text)
aws securityagent create-target-domain --region $REGION \
--target-domain-name "$PRIVATE_DNS" --verification-method HTTP_ROUTE
# Private VPC ドメインの場合、UNREACHABLE が正常な検証状態
# 7. Secrets Manager
aws secretsmanager create-secret --region $REGION \
--name "security-agent/remediation-test/testuser" \
--secret-string '{"username":"testuser","password":"testpass123","totpSecret":"JBSWY3DPEHPK3PXP"}'
# 8. Agent Space 更新(プライベートサブネット + Secret + Target Domain)
aws securityagent update-agent-space --region $REGION \
--agent-space-id "$AGENT_SPACE_ID" \
--target-domain-ids <target-domain-id> \
--aws-resources '{
"vpcs": [{"vpcArn":"<vpc-id>","securityGroupArns":["'$SG_ID'"],"subnetArns":["'$PRIVATE_SUBNET'"]}],
"secretArns": ["<secret-arn>"],
"iamRoles": ["<role-arn>"]
}'
# 9. GitHub 連携(AWS Console で実施)
# - Security Agent → Agent Space → Integrations → Connect GitHub
# - GitHub App インストール → リポジトリ選択
# - Penetration test タブ → Pentest remediation enabled
# 10. ペンテスト作成(GitHub リポジトリ + AUTOMATIC Remediation)
aws securityagent create-pentest --region $REGION \
--agent-space-id "$AGENT_SPACE_ID" \
--title "remediation-test-auth" \
--code-remediation-strategy AUTOMATIC \
--assets '{
"endpoints": [{"uri": "http://'"$PRIVATE_DNS"'"}],
"actors": [{"identifier":"testuser","uris":["http://'"$PRIVATE_DNS"'"],
"authentication":{"providerType":"SECRETS_MANAGER","value":"<secret-arn>"},
"description":"Navigate to /login. Enter username and password. Enter TOTP code. Click Login."}],
"integratedRepositories": [{"integrationId":"<integration-id>","providerResourceId":"<github-repo-id>"}]
}' \
--service-role "<role-arn>" \
--vpc-config '{"vpcArn":"<vpc-id>","securityGroupArns":["'$SG_ID'"],"subnetArns":["'$PRIVATE_SUBNET'"]}'
# 11. ペンテストジョブ開始
aws securityagent start-pentest-job --region $REGION \
--agent-space-id "$AGENT_SPACE_ID" --pentest-id <pentest-id>セットアップで遭遇した問題
環境構築では以下の問題に遭遇した。
- DB 未初期化 — gunicorn 経由では
init_db()が実行されない。手動でpython3 -c "from app import init_db; init_db()"を実行する必要がある。これを忘れると全 DB 依存エンドポイントが 500 エラーを返し、ペンテストが FAILED になる - VPC から GitHub へのアクセス — Agent のテスト環境は Agent Space に設定したサブネットで起動する。このサブネットから GitHub にアクセスできないとソースコードを取得できず、
Unable to reach GitHub repositoryエラーでジョブが失敗する。今回はプライベートサブネットを作成し、NAT Gateway 経由でインターネットアクセスを確保した - ペンテスト作成時にリポジトリを紐づける必要がある —
create-pentestのassets.integratedRepositoriesでリポジトリを指定する。後からupdate-pentestで追加しても、既に実行中のジョブには反映されない
検証結果
7 件の Finding を検出し、全件に対して GitHub PR が自動生成された。 code-remediation-strategy を AUTOMATIC に設定したため、Finding が確認されるたびに PR が順次生成された。ペンテストジョブの完了を待つ必要はなく、Finding の検出から数分後に対応する PR が GitHub リポジトリに作成された。
| # | Finding | リスクタイプ | 深刻度 | 確信度 | PR |
|---|---|---|---|---|---|
| 1 | SQL Injection in /api/products | SQL_INJECTION | CRITICAL | HIGH | #1 |
| 2 | Reflected XSS in /search | CROSS_SITE_SCRIPTING | MEDIUM | LOW | #2 |
| 3 | Hardcoded Flask Secret Key | SESSION_TOKEN_VULNERABILITIES | CRITICAL | HIGH | #3 |
| 4 | Default Credentials + Plaintext Passwords | DEFAULT_CREDENTIALS | CRITICAL | HIGH | #4 |
| 5 | IDOR on /profile | INSECURE_DIRECT_OBJECT_REFERENCE | MEDIUM | HIGH | #5 |
| 6 | Privilege Escalation /admin/users | PRIVILEGE_ESCALATION | MEDIUM | HIGH | #6 |
| 7 | Stored XSS in Comments | CROSS_SITE_SCRIPTING | MEDIUM | LOW | #7 |
第4回(条件 B)では 6 件だったが、今回は 7 件検出された。第4回では検出されなかった Hardcoded Secret Key(#3)と Default Credentials(#4)が新たに検出されている。今回は GitHub リポジトリとしてソースコードが提供されており、コードレベルの分析が加わった可能性がある。
PR の修正内容
各 PR の修正内容を分析する。
PR #1: SQL Injection(CRITICAL)— f-string → パラメータ化クエリ(+4/-4)
query = f"SELECT * FROM products WHERE category = '{category}'"
results = conn.execute(query).fetchall()results = conn.execute(
"SELECT * FROM products WHERE category = ?", (category,)
).fetchall()f-string による文字列結合を SQLite のパラメータ化クエリ(? プレースホルダ)に置き換えている。加えて、例外ハンドラの str(e) を汎用メッセージに変更し、エラー詳細の漏洩も防いでいる。教科書的な修正であり、そのまま適用可能。
なお、各 PR の description には Pentest ID、Finding ID、CWE 番号、根本原因の説明、修正内容の詳細、影響範囲が構造化されて記載されている。コードレビュー時に Finding の詳細を別途確認する必要がなく、PR 単体で修正の妥当性を判断できる。
PR #2: Reflected XSS(MEDIUM)— |safe フィルタ除去(+1/-1)
<h2>Results for: {{ query | safe }}</h2><h2>Results for: {{ query }}</h2>Jinja2 の |safe フィルタを除去し、デフォルトの自動エスケープを有効にしている。1行の変更で正確に脆弱性を解消している。
PR #3: Hardcoded Secret Key(CRITICAL)— 環境変数 / ランダム生成(+5/-1)
ハードコードされた app.secret_key = "insecure-secret-key-for-testing" を、環境変数 FLASK_SECRET_KEY から取得するように変更。未設定の場合は os.urandom(32) でランダム生成し、警告を出力する。セッション偽造を防ぐ実用的な修正。
PR #4: Default Credentials + Plaintext(CRITICAL)— 包括的リファクタリング(+70/-30)
最も大規模な修正。以下の変更を含む。
- 平文パスワード →
werkzeug.security.generate_password_hashによるハッシュ化 - 共通 TOTP Secret →
pyotp.random_base32()による per-user TOTP Secret 生成 - 起動時の毎回シード →
SELECT COUNT(*)で空テーブルの場合のみシード - ログイン処理を
check_password_hashに変更 - users テーブルに
totp_secretカラムを追加
単一の脆弱性修正を超えて、認証基盤全体をリファクタリングしている。
PR #5: IDOR(MEDIUM)— 所有者チェック追加(+2/-1)
if session["user_id"] != user_id and session.get("role") != "admin":
return "Forbidden", 403自分自身のプロフィールまたは admin ロールの場合のみアクセスを許可する認可チェックを追加。admin の例外を考慮している点が実用的。
PR #6: Privilege Escalation(MEDIUM)— ロールベース認可チェック(+3/-1)
if session.get("role") != "admin":
return "Forbidden", 403/admin/users エンドポイントに admin ロールチェックを追加。シンプルだが正確な修正。
PR #7: Stored XSS(MEDIUM)— |safe フィルタ除去(+1/-1)
PR #2 と同じパターン。コメント表示の {{ c['content'] | safe }} から |safe を除去。
修正品質の評価
| # | 脆弱性 | 深刻度 | 修正の正確性 | そのまま適用可能か | 変更量 |
|---|---|---|---|---|---|
| 1 | SQL Injection | CRITICAL | ✅ 正確 | ✅ | +4/-4 |
| 2 | Reflected XSS | MEDIUM | ✅ 正確 | ✅ | +1/-1 |
| 3 | Hardcoded Secret Key | CRITICAL | ✅ 正確 | ✅ | +5/-1 |
| 4 | Default Credentials | CRITICAL | ✅ 正確 | ⚠️ 要テスト | +70/-30 |
| 5 | IDOR | MEDIUM | ✅ 正確 | ✅ | +2/-1 |
| 6 | Privilege Escalation | MEDIUM | ✅ 正確 | ✅ | +3/-1 |
| 7 | Stored XSS | MEDIUM | ✅ 正確 | ✅ | +1/-1 |
全 7 件の修正が技術的に正確だった。PR #4(Default Credentials)のみ +70/-30 の大規模変更であり、テーブルスキーマの変更を含むため適用前にテストが必要だが、修正の方向性は適切である。
まとめ
- 全 Finding に対して PR が自動生成された — 7 件の脆弱性に対して 7 件の PR が作成され、全て技術的に正確な修正だった。XSS の
|safe除去(1行)から認証基盤のリファクタリング(70行)まで、脆弱性の複雑さに応じた修正が生成された - 修正は「検出」の延長線上にある — PR の description には Finding のタイトル、CWE 番号、根本原因、修正内容、影響範囲が記載されており、コードレビューに必要な情報が揃っている。ペンテストの Finding を読んで手動で修正するよりも効率的である
- NAT Gateway が必要 — Agent のテスト環境はプライベートサブネットで起動するため、GitHub へのアクセスには NAT Gateway が必要。これは追加コスト(NAT Gateway の時間課金 + データ転送)を伴う
- us-east-1 限定は実用上の制約 — 既存の Agent Space が別リージョンにある場合、Remediation のためだけに us-east-1 で環境を再構築する必要がある
クリーンアップ
リソース削除手順
REGION=us-east-1
# EC2
aws ec2 terminate-instances --region $REGION --instance-ids <instance-id>
# NAT Gateway + EIP
aws ec2 delete-nat-gateway --region $REGION --nat-gateway-id <nat-gw-id>
# NAT Gateway 削除完了を待ってから EIP 解放
aws ec2 release-address --region $REGION --allocation-id <eip-alloc-id>
# プライベートサブネット + ルートテーブル
aws ec2 delete-subnet --region $REGION --subnet-id <private-subnet-id>
aws ec2 delete-route-table --region $REGION --route-table-id <private-rt-id>
# Security Group
aws ec2 delete-security-group --region $REGION --group-id <sg-id>
# Secrets Manager
aws secretsmanager delete-secret --region $REGION \
--secret-id <secret-name> --force-delete-without-recovery
# Target Domain
aws securityagent delete-target-domain --region $REGION \
--target-domain-id <target-domain-id>
# GitHub リポジトリ
gh repo delete <your-org>/auth-vuln-app --yes