[小ネタ] 経営層にも分かるようGuardDutyの検出結果をGeminiのCanvas機能でビジュアル化してみた

[小ネタ] 経営層にも分かるようGuardDutyの検出結果をGeminiのCanvas機能でビジュアル化してみた

積極的に図解して、脳を悦ばせましょう。
2025.07.30

こんにちは!クラウド事業本部のおつまみです。

みなさん、Gemini の Canvas 機能使っていますか?私はよく使っています。
テキストだけでは伝わりにくい内容も、Canvas 機能を使えば、簡単に図解で分かりやすく表現できます。

詳細はこちらの記事がわかりやすいので、ご参考ください。
https://dev.classmethod.jp/articles/gemini-app-canvas-creation-feature/

ところで、GuardDutyの検出結果を見て「これ、何が起きているの?」と思ったことはありませんか?
技術的な詳細が多くて、セキュリティの専門知識がないと理解しにくいですよね。

そこで今回は、Gemini の Canvas 機能を使って、GuardDutyの検出結果を視覚的にわかりやすく表現する方法を試してみました!

GuardDutyの検出内容について

GuardDutyの検出結果を取得するために、該当の検出結果を選択し、[アクション]>[JSONを表示およびエクスポート]を選択します。

CleanShot 2025-07-30 at 10.39.30@2x

今回取得した検出結果はこちらです。(一部出力内容をマスクしています)

[
  {
    "AccountId": "123456789012",
    "Arn": ":detector/f8bda9bcef2b6a701d8c8650cbaf5206/finding/9acc0b97b7e4fe0ea01d799866702e4e",
    "CreatedAt": "2025-07-17T07:00:41.545Z",
    "Description": "The EC2 instance i-xxxxxxxx is communicating outbound with a known Bitcoin-related IP address 66.23.198.161.",
    "Id": "9acc0b97b7e4fe0ea01d799866702e4e",
    "Partition": "aws",
    "Region": "ap-northeast-1",
    "Resource": {
      "InstanceDetails": {
        "AvailabilityZone": "ap-northeast-1a",
        "InstanceId": "i-xxxxxxxx",
        "InstanceState": "running",
        "InstanceType": "t2.large",
        "LaunchTime": "2017-06-08T02:00:59.000Z",
        "NetworkInterfaces": [
          {
            "Ipv6Addresses": [],
            "NetworkInterfaceId": "eni-abcdefg",
            "PrivateDnsName": "ip-10-112-1-3.ap-northeast-1.compute.internal",
            "PrivateIpAddress": "10.112.1.3",
            "PrivateIpAddresses": [
              {
                "PrivateDnsName": "ip-10-112-1-3.ap-northeast-1.compute.internal",
                "PrivateIpAddress": "10.112.1.3"
              }
            ],
            "PublicDnsName": "ec2-12-34-56-78.ap-northeast-1.compute.amazonaws.com",
            "PublicIp": "12.34.56.78",
            "SecurityGroups": [
              {
                "GroupId": "sg-d3e498b7",
                "GroupName": "test-sg"
              }
            ],
            "SubnetId": "subnet-a50235d2",
            "VpcId": "vpc-01234567"
          }
        ],
        "Platform": "windows",
        "ProductCodes": [],
        "Tags": [
          {
            "Key": "Name",
            "Value": "test"
          }
        ]
      },
      "ResourceType": "Instance"
    },
    "SchemaVersion": "2.0",
    "Service": {
      "Action": {
        "ActionType": "NETWORK_CONNECTION",
        "NetworkConnectionAction": {
          "Blocked": false,
          "ConnectionDirection": "OUTBOUND",
          "LocalPortDetails": {
            "Port": 56865,
            "PortName": "Unknown"
          },
          "Protocol": "TCP",
          "LocalIpDetails": {
            "IpAddressV4": "10.112.1.3"
          },
          "LocalNetworkInterface": "eni-abcdefg",
          "RemoteIpDetails": {
            "City": {
              "CityName": "Norristown"
            },
            "Country": {
              "CountryName": "United States"
            },
            "GeoLocation": {
              "Lat": 40.1435,
              "Lon": -75.3779
            },
            "IpAddressV4": "66.23.198.161",
            "Organization": {
              "Asn": "400304",
              "AsnOrg": "CRUNCHBITS",
              "Isp": "Crunchbits",
              "Org": "Crunchbits"
            }
          },
          "RemotePortDetails": {
            "Port": 5555,
            "PortName": "Cryptomining-Related"
          }
        }
      },
      "Evidence": {
        "ThreatIntelligenceDetails": [
          {
            "ThreatListName": "Amazon Active Threat Defense",
            "ThreatNames": [
              "suspicious:mining/stratum"
            ]
          }
        ]
      },
      "Archived": false,
      "Count": 4119,
      "DetectorId": "f8bda9bcef2b6a701d8c8650cbaf5206",
      "EventFirstSeen": "2025-07-17T06:51:07.000Z",
      "EventLastSeen": "2025-07-30T01:24:43.000Z",
      "ResourceRole": "TARGET",
      "ServiceName": "guardduty",
      "AdditionalInfo": {
        "Value": "{\"threatListName\":\"Amazon Active Threat Defense\",\"threatName\":\"suspicious:mining/stratum\"}",
        "Type": "default"
      }
    },
    "Severity": 8,
    "Title": "The EC2 instance i-xxxxxxxx is communicating with a known Bitcoin-related IP address.",
    "Type": "CryptoCurrency:EC2/BitcoinTool.B",
    "UpdatedAt": "2025-07-30T01:30:40.430Z"
  }
]

よくやることとして、とりあえずClaude Desktop で Claude Sonnet 4に聞いてみました。

このGuardDutyの検出内容を教えて

このGuardDutyの検出内容を分析すると、重要なセキュリティインシデントが発生していることがわかります。

## 検出の概要
**検出タイプ**: `CryptoCurrency:EC2/BitcoinTool.B`
**重要度**: 8(高)
**ステータス**: アクティブ(アーカイブされていない)

## インシデントの詳細

**影響を受けるリソース**:
- EC2インスタンス: `i-xxxxxxxx` (t2.large, Windows)
- リージョン: ap-northeast-1a
- プライベートIP: 10.112.1.3
- パブリックIP: 12.34.56.78

**悪意のある通信**:
- **接続先IP**: 66.23.198.161(米国ペンシルベニア州ノリスタウン)
- **プロトコル**: TCP
- **ローカルポート**: 56865
- **リモートポート**: 5555(暗号通貨マイニング関連)
- **通信方向**: アウトバウンド(インスタンスから外部へ)

## 脅威の詳細

このインシデントは**暗号通貨マイニング**に関連する脅威です:

- **脅威名**: `suspicious:mining/stratum`
- **検出回数**: 4,119回(2025年7月17日〜7月30日)
- **最初の検出**: 2025年7月17日 06:51:07
- **最新の検出**: 2025年7月30日 01:24:43

接続先のポート5555は「Cryptomining-Related」として分類されており、Stratumプロトコル(マイニングプールとの通信に使用)を示唆しています。

## 推奨対応

1. **即座の対応**:
   - 該当インスタンスのネットワークアクセスを制限
   - インスタンスの詳細な調査を実施

2. **調査項目**:
   - 不正なマイニングソフトウェアの有無
   - システムの侵害経路の特定
   - 他のインスタンスへの影響範囲の確認

3. **予防策**:
   - セキュリティグループの見直し
   - アウトバウンド通信の監視強化
   - 定期的なセキュリティスキャンの実施

この検出は継続的な活動(約2週間にわたり4,000回以上の通信)を示しており、早急な対応が必要です。

これでも十分なのですが、やはり図にしたいですよね。
先人も「図、つかう。脳、悦ぶ。」と教えてくれました。(参考:ブログ夏祭り2020_チバユキ_脳 - Speaker Deck

ということで、Gemini の Canvas 機能で出番です。

Canvas 機能使ってみた

Google Geminiにアクセスし、先ほどと同じプロンプト(例:以下のGuardDuty検出結果について教えてください[検出結果をコピペ])を投げます。
この時に[Canvas]をONにしておきます。

CleanShot 2025-07-30 at 10.55.30@2x

しばらくするとGuardDutyセキュリティ検出結果の詳細の文章が出力されるので、[作成] > [ウェブページ] を選択します。

CleanShot 2025-07-30 at 10.56.16@2x

すると以下のようなページが作成されました!なかなか分かりやすいのではないでしょうか?

上部:サマリ、脅威の分析
CleanShot 2025-07-30 at 11.00.22@2x

中部:影響を受けるリソース、通信アクティビティの推移
CleanShot 2025-07-30 at 11.00.46@2x

下部:推奨される対応策
CleanShot 2025-07-30 at 11.01.02@2x

コードでも出力がされるので、文章や構成を書き換えることも可能です。

出力されたhtmlコード
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GuardDuty 検出結果ダッシュボード</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">
    <!-- Chosen Palette: Calm Harmony -->
    <!-- Application Structure Plan: A dashboard-style SPA with collapsible sections. The structure guides the user from a high-level alert summary (Overview) to a visual explanation of the threat (Threat Analysis), details on the compromised asset (Affected Resource), a visualization of the activity over time (Communication Activity), and finally an interactive checklist for mitigation (Recommended Actions). This structure was chosen to prioritize quick understanding and a clear path to resolution, which is more effective for a security alert than a linear report. -->
    <!-- Visualization & Content Choices: 
        - Overview: Report Info (Severity, Count, Dates) -> Goal (Inform) -> Viz (Styled cards) -> Interaction (None) -> Justification (Quick, scannable summary) -> Method (HTML/Tailwind).
        - Threat Flow: Report Info (Source/Destination IP, Direction) -> Goal (Organize/Relate) -> Viz (Flow diagram) -> Interaction (None) -> Justification (Visually clarifies the attack path simply) -> Method (HTML/Tailwind).
        - Activity Chart: Report Info (Count, First/Last Seen) -> Goal (Show Change) -> Viz (Line Chart) -> Interaction (Hover tooltips) -> Justification (Visualizes the persistence and frequency of the threat) -> Library (Chart.js/Canvas).
        - Action Plan: Report Info (Recommended Actions) -> Goal (Organize/Guide) -> Viz (Checklist) -> Interaction (Click to mark complete, Click for LLM details) -> Justification (Makes the response process interactive and trackable, provides deeper insights via LLM) -> Method (HTML/JS, Gemini API).
        - Threat Summary: Report Info (Threat Name) -> Goal (Inform/Explain) -> Viz (Text block) -> Interaction (Click for LLM summary) -> Justification (Provides plain-language explanation of complex threat via LLM) -> Method (HTML/JS, Gemini API).
    -->
    <!-- CONFIRMATION: NO SVG graphics used. NO Mermaid JS used. -->
    <style>
        body {
            font-family: 'Noto Sans JP', sans-serif;
            background-color: #f8f9fa;
        }
        .severity-high {
            background-color: #dc3545;
            color: white;
        }
        .action-item:not(.completed):hover {
            background-color: #e9ecef;
        }
        .action-item.completed {
            background-color: #d1e7dd;
            text-decoration: line-through;
            color: #0f5132;
        }
        .chart-container {
            position: relative;
            width: 100%;
            max-width: 800px;
            margin-left: auto;
            margin-right: auto;
            height: 350px;
            max-height: 400px;
        }
        @media (min-width: 768px) {
            .chart-container {
                height: 400px;
            }
        }
    </style>
</head>
<body class="text-gray-800">

    <div id="app" class="container mx-auto p-4 md:p-8 max-w-7xl">

        <header class="mb-8 p-6 bg-white rounded-lg shadow-md border-l-8 border-red-600">
            <h1 id="finding-title" class="text-2xl md:text-3xl font-bold text-gray-900"></h1>
            <p id="finding-description" class="mt-2 text-gray-600"></p>
        </header>

        <main class="space-y-8">

            <section id="overview">
                <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
                    <div class="bg-white p-5 rounded-lg shadow-md text-center">
                        <h3 class="text-lg font-semibold text-gray-500 mb-2">重大度</h3>
                        <div id="severity" class="text-4xl font-bold inline-block px-4 py-2 rounded-md"></div>
                    </div>
                    <div class="bg-white p-5 rounded-lg shadow-md text-center">
                        <h3 class="text-lg font-semibold text-gray-500 mb-2">検出回数</h3>
                        <p id="count" class="text-4xl font-bold text-blue-600"></p>
                    </div>
                    <div class="bg-white p-5 rounded-lg shadow-md text-center">
                        <h3 class="text-lg font-semibold text-gray-500 mb-2">初回検出日時 (UTC)</h3>
                        <p id="first-seen" class="text-2xl font-medium text-gray-700"></p>
                    </div>
                    <div class="bg-white p-5 rounded-lg shadow-md text-center">
                        <h3 class="text-lg font-semibold text-gray-500 mb-2">最終検出日時 (UTC)</h3>
                        <p id="last-seen" class="text-2xl font-medium text-gray-700"></p>
                    </div>
                </div>
            </section>

            <section id="threat-analysis" class="p-6 bg-white rounded-lg shadow-md">
                <h2 class="text-2xl font-bold mb-4">脅威の分析</h2>
                <p class="mb-6 text-gray-600">
                    このセクションでは、検出された脅威の性質と通信経路を視覚的に示します。お客様のEC2インスタンスから、暗号通貨マイニングに関連すると特定された外部のIPアドレスへのアウトバウンド通信が確認されました。
                </p>
                <div class="flex flex-col md:flex-row items-center justify-center gap-4 text-center p-4 rounded-lg bg-gray-50">
                    <div class="w-full md:w-1/3 p-4 border border-gray-200 rounded-lg bg-white">
                        <div class="text-blue-500 text-4xl mb-2">🖥️</div>
                        <h4 class="font-bold">影響を受けるリソース (ターゲット)</h4>
                        <p id="local-ip" class="text-sm font-mono"></p>
                        <p class="text-xs text-gray-500">お客様のEC2インスタンス</p>
                    </div>
                    <div class="text-4xl font-thin text-red-500 transform rotate-90 md:rotate-0">→</div>
                    <div class="w-full md:w-1/3 p-4 border border-red-200 rounded-lg bg-red-50">
                        <div class="text-red-500 text-4xl mb-2">🌍</div>
                        <h4 class="font-bold">既知の脅威 (ソース)</h4>
                        <p id="remote-ip" class="text-sm font-mono"></p>
                        <p class="text-xs text-gray-500">ビットコイン関連IP</p>
                    </div>
                </div>
                <div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
                    <div class="bg-gray-50 p-4 rounded-lg">
                        <h4 class="font-semibold text-lg mb-2">脅威インテリジェンス</h4>
                        <p class="font-mono bg-gray-200 text-gray-800 px-2 py-1 rounded inline-block" id="threat-name"></p>
                        <p class="mt-2 text-sm text-gray-600">Amazonの脅威インテリジェンスリストにより、この通信が暗号通貨マイニングプロトコル「Stratum」に関連する疑いがあることが特定されました。</p>
                        <div class="mt-4 text-center">
                            <button id="threat-summary-btn" class="bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded-full shadow-lg transition duration-300">
                                脅威の詳細を理解する ✨
                            </button>
                            <div id="threat-summary-loading" class="hidden mt-2 text-sm text-gray-500">生成中...</div>
                            <div id="threat-summary-content" class="hidden mt-4 p-4 bg-gray-100 rounded-lg text-left text-gray-700 border border-gray-200 whitespace-pre-wrap"></div>
                            <div id="threat-summary-error" class="hidden mt-2 text-sm text-red-500"></div>
                        </div>
                    </div>
                    <div class="bg-gray-50 p-4 rounded-lg">
                        <h4 class="font-semibold text-lg mb-2">リモートIP情報</h4>
                        <p class="text-sm"><strong class="font-medium">国:</strong> <span id="remote-country"></span></p>
                        <p class="text-sm"><strong class="font-medium">都市:</strong> <span id="remote-city"></span></p>
                        <p class="text-sm"><strong class="font-medium">組織:</strong> <span id="remote-org"></span></p>
                    </div>
                </div>
            </section>

            <section id="affected-resource" class="p-6 bg-white rounded-lg shadow-md">
                <h2 class="text-2xl font-bold mb-4">影響を受けるリソース</h2>
                <p class="mb-6 text-gray-600">
                    以下のEC2インスタンスが、このセキュリティイベントのターゲットとして特定されました。インスタンスの詳細を確認し、対応計画に役立ててください。
                </p>
                <div class="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-r-lg">
                    <div class="grid grid-cols-2 md:grid-cols-3 gap-4">
                        <div><strong class="block text-sm text-gray-500">インスタンスID</strong><span id="instance-id" class="font-mono"></span></div>
                        <div><strong class="block text-sm text-gray-500">インスタンスタイプ</strong><span id="instance-type" class="font-mono"></span></div>
                        <div><strong class="block text-sm text-gray-500">プラットフォーム</strong><span id="platform" class="font-mono"></span></div>
                        <div><strong class="block text-sm text-gray-500">VPC ID</strong><span id="vpc-id" class="font-mono"></span></div>
                        <div><strong class="block text-sm text-gray-500">サブネットID</strong><span id="subnet-id" class="font-mono"></span></div>
                        <div><strong class="block text-sm text-gray-500">パブリックIP</strong><span id="public-ip" class="font-mono"></span></div>
                    </div>
                </div>
            </section>

            <section id="activity-chart" class="p-6 bg-white rounded-lg shadow-md">
                <h2 class="text-2xl font-bold mb-4">通信アクティビティの推移</h2>
                <p class="mb-6 text-gray-600">
                    このグラフは、初回検出から最終検出までの期間における、疑わしい通信の発生頻度(シミュレーション)を示しています。アクティビティが継続的であったことがわかります。
                </p>
                <div class="chart-container">
                    <canvas id="communicationsChart"></canvas>
                </div>
            </section>

            <section id="recommendations" class="p-6 bg-white rounded-lg shadow-md">
                <h2 class="text-2xl font-bold mb-4">推奨される対応策</h2>
                <p class="mb-6 text-gray-600">
                    この脅威は重大度が高いため、迅速な対応が不可欠です。以下のステップに従って、調査と緩和を進めてください。各項目をクリックすると、完了済みとしてマークできます。
                </p>
                <div id="action-list" class="space-y-3">
                </div>
            </section>

        </main>

        <footer class="text-center mt-12 text-sm text-gray-500">
            <p>GuardDuty Interactive Dashboard | 生成日時: <span id="generation-date"></span></p>
        </footer>
    </div>

    <script>
        const findingData = {
            "Title": "EC2インスタンスが既知のビットコイン関連IPアドレスと通信しています。",
            "Description": "EC2インスタンス i-xxxxxxxx は、既知のビットコイン関連IPアドレス 66.23.198.161 とアウトバウンド通信を行っています。",
            "Severity": 8,
            "Count": 4119,
            "EventFirstSeen": "2025-07-17T06:51:07.000Z",
            "EventLastSeen": "2025-07-30T01:24:43.000Z",
            "Resource": {
                "InstanceDetails": {
                    "InstanceId": "i-xxxxxxxx",
                    "InstanceType": "t2.large",
                    "Platform": "windows",
                    "PublicIp": "12.34.56.78",
                    "VpcId": "vpc-01234567",
                    "SubnetId": "subnet-a50235d2"
                }
            },
            "Service": {
                "Action": {
                    "NetworkConnectionAction": {
                        "LocalIpDetails": { "IpAddressV4": "10.112.1.3" },
                        "RemoteIpDetails": {
                            "IpAddressV4": "66.23.198.161",
                            "Country": { "CountryName": "United States" },
                            "City": { "CityName": "Norristown" },
                            "Organization": { "Org": "Crunchbits" }
                        }
                    }
                },
                "Evidence": {
                    "ThreatIntelligenceDetails": [{
                        "ThreatNames": ["suspicious:mining/stratum"]
                    }]
                }
            },
            "RecommendedActions": [
                { "title": "インスタンスの隔離", "description": "セキュリティグループを変更し、インスタンスをネットワークから隔離して被害の拡大を防ぎます。" },
                { "title": "詳細な調査", "description": "インスタンスにログインし、不審なプロセス、ファイル、ネットワーク接続がないかを確認します。" },
                { "title": "スナップショットの取得", "description": "フォレンジック調査のために、インスタンスのボリュームのスナップショットを取得します。" },
                { "title": "マルウェアスキャン", "description": "インスタンス上でマルウェアスキャンを実行し、感染の有無と種類を特定します。" },
                { "title": "認証情報のローテーション", "description": "関連するIAMロールやユーザーの認証情報をローテーション(変更)します。" },
                { "title": "根本原因の特定", "description": "どのようにしてインスタンスが侵害されたのか、根本原因を特定します(例:脆弱なアプリ、設定ミス)。" },
                { "title": "ネットワークルールの確認", "description": "セキュリティグループとNACLを見直し、不要なアウトバウンド通信を制限します。" },
                { "title": "GuardDutyの継続的な監視", "description": "対応後もGuardDutyを監視し、同様のアクティビティが再発しないことを確認します。" }
            ]
        };

        document.addEventListener('DOMContentLoaded', () => {
            renderFinding();
            createActivityChart();
            setupActionList();
            setupGeminiFeatures();
        });

        async function exponentialBackoffFetch(url, options, retries = 3, delay = 1000) {
            for (let i = 0; i < retries; i++) {
                try {
                    const response = await fetch(url, options);
                    if (response.ok) {
                        return response;
                    } else if (response.status === 429 || response.status >= 500) {
                        await new Promise(res => setTimeout(res, delay * Math.pow(2, i)));
                    } else {
                        throw new Error(`HTTP error! status: ${response.status}`);
                    }
                } catch (error) {
                    if (i === retries - 1) throw error;
                    await new Promise(res => setTimeout(res, delay * Math.pow(2, i)));
                }
            }
            throw new Error('Max retries exceeded');
        }

        async function callGeminiAPI(prompt) {
            const chatHistory = [];
            chatHistory.push({ role: "user", parts: [{ text: prompt }] });
            const payload = { contents: chatHistory };
            const apiKey = "";
            const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`;

            try {
                const response = await exponentialBackoffFetch(apiUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(payload)
                });
                const result = await response.json();
                if (result.candidates && result.candidates.length > 0 &&
                    result.candidates[0].content && result.candidates[0].content.parts &&
                    result.candidates[0].content.parts.length > 0) {
                    return result.candidates[0].content.parts[0].text;
                } else {
                    return "エラー: 応答の解析に失敗しました。";
                }
            } catch (error) {
                return "エラー: API呼び出し中に問題が発生しました。";
            }
        }

        function showLoading(elementId) {
            document.getElementById(elementId).classList.remove('hidden');
        }

        function hideLoading(elementId) {
            document.getElementById(elementId).classList.add('hidden');
        }

        function displayError(elementId, message) {
            const errorEl = document.getElementById(elementId);
            errorEl.textContent = `エラー: ${message}`;
            errorEl.classList.remove('hidden');
        }

        function hideError(elementId) {
            document.getElementById(elementId).classList.add('hidden');
        }

        function renderFinding() {
            document.getElementById('finding-title').textContent = findingData.Title;
            document.getElementById('finding-description').textContent = findingData.Description;

            const severityEl = document.getElementById('severity');
            severityEl.textContent = findingData.Severity;
            if (findingData.Severity >= 7) {
                severityEl.classList.add('severity-high');
            }

            document.getElementById('count').textContent = findingData.Count.toLocaleString();
            document.getElementById('first-seen').textContent = new Date(findingData.EventFirstSeen).toLocaleString('ja-JP');
            document.getElementById('last-seen').textContent = new Date(findingData.EventLastSeen).toLocaleString('ja-JP');

            const netAction = findingData.Service.Action.NetworkConnectionAction;
            document.getElementById('local-ip').textContent = netAction.LocalIpDetails.IpAddressV4;
            document.getElementById('remote-ip').textContent = netAction.RemoteIpDetails.IpAddressV4;
            document.getElementById('remote-country').textContent = netAction.RemoteIpDetails.Country.CountryName;
            document.getElementById('remote-city').textContent = netAction.RemoteIpDetails.City.CityName;
            document.getElementById('remote-org').textContent = netAction.RemoteIpDetails.Organization.Org;

            document.getElementById('threat-name').textContent = findingData.Service.Evidence.ThreatIntelligenceDetails[0].ThreatNames[0];

            const instance = findingData.Resource.InstanceDetails;
            document.getElementById('instance-id').textContent = instance.InstanceId;
            document.getElementById('instance-type').textContent = instance.InstanceType;
            document.getElementById('platform').textContent = instance.Platform;
            document.getElementById('vpc-id').textContent = instance.VpcId;
            document.getElementById('subnet-id').textContent = instance.SubnetId;
            document.getElementById('public-ip').textContent = instance.PublicIp;

            document.getElementById('generation-date').textContent = new Date().toLocaleString('ja-JP');
        }

        function setupActionList() {
            const listEl = document.getElementById('action-list');
            findingData.RecommendedActions.forEach((action, index) => {
                const item = document.createElement('div');
                item.className = 'action-item p-4 border rounded-lg transition-colors duration-200 flex flex-col items-start gap-2';

                const mainContent = document.createElement('div');
                mainContent.className = 'flex items-start gap-4 w-full cursor-pointer';
                mainContent.innerHTML = `
                    <div class="mt-1 w-5 h-5 border-2 border-gray-300 rounded-full flex-shrink-0 flex items-center justify-center">
                        <span class="check-icon hidden text-green-600 font-bold">✓</span>
                    </div>
                    <div class="flex-grow">
                        <h4 class="font-semibold">${action.title}</h4>
                        <p class="text-sm text-gray-600">${action.description}</p>
                    </div>
                `;

                mainContent.addEventListener('click', (event) => {
                    item.classList.toggle('completed');
                    mainContent.querySelector('.check-icon').classList.toggle('hidden');
                    mainContent.querySelector('.border-2').classList.toggle('border-gray-300');
                    mainContent.querySelector('.border-2').classList.toggle('border-green-600');
                });

                item.appendChild(mainContent);

                item.innerHTML += `
                    <div class="w-full mt-2 text-right">
                         <button class="action-details-btn bg-green-500 hover:bg-green-600 text-white text-xs font-bold py-1 px-3 rounded-full" data-index="${index}">
                            詳細を見る ✨
                        </button>
                    </div>
                    <div id="action-loading-${index}" class="action-details-loading hidden mt-2 text-sm text-gray-500 w-full text-right">生成中...</div>
                    <div id="action-content-${index}" class="action-details-content hidden mt-2 p-3 bg-blue-50 rounded-lg text-left text-gray-700 border border-blue-200 w-full whitespace-pre-wrap"></div>
                    <div id="action-error-${index}" class="action-details-error hidden mt-2 text-sm text-red-500 w-full text-right"></div>
                `;

                listEl.appendChild(item);
            });
        }

        function createActivityChart() {
            const ctx = document.getElementById('communicationsChart').getContext('2d');

            const startDate = new Date(findingData.EventFirstSeen);
            const endDate = new Date(findingData.EventLastSeen);
            const labels = [];
            const dataPoints = [];
            let currentDate = new Date(startDate);

            while (currentDate <= endDate) {
                labels.push(currentDate.toLocaleDateString('ja-JP', { month: '2-digit', day: '2-digit' }));
                const randomFactor = 0.5 + Math.random();
                const totalDays = (endDate - startDate) / (1000 * 60 * 60 * 24);
                const baseCount = totalDays > 0 ? findingData.Count / totalDays : findingData.Count;
                dataPoints.push(Math.round(baseCount * randomFactor));
                currentDate.setDate(currentDate.getDate() + 1);
            }
            if (labels.length === 0) {
                 labels.push(startDate.toLocaleDateString('ja-JP', { month: '2-digit', day: '2-digit' }));
                 dataPoints.push(findingData.Count);
            }

            new Chart(ctx, {
                type: 'line',
                data: {
                    labels: labels,
                    datasets: [{
                        label: '検出回数 (シミュレーション)',
                        data: dataPoints,
                        borderColor: '#dc3545',
                        backgroundColor: 'rgba(220, 53, 69, 0.1)',
                        fill: true,
                        tension: 0.3,
                        borderWidth: 2
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    scales: {
                        y: {
                            beginAtZero: true,
                            title: { display: true, text: '回数' }
                        },
                        x: {
                           title: { display: true, text: '日付' }
                        }
                    },
                    plugins: {
                        legend: { display: false },
                        tooltip: {
                            callbacks: {
                                title: function(tooltipItems) {
                                    const item = tooltipItems[0];
                                    let label = item.chart.data.labels[item.dataIndex];
                                    return Array.isArray(label) ? label.join(' ') : label;
                                },
                                label: function(context) {
                                    return ` ${context.dataset.label}: ${context.parsed.y} 回`;
                                }
                            }
                        }
                    }
                }
            });
        }

        function setupGeminiFeatures() {
            const threatSummaryBtn = document.getElementById('threat-summary-btn');
            threatSummaryBtn.addEventListener('click', async () => {
                const threatName = findingData.Service.Evidence.ThreatIntelligenceDetails[0].ThreatNames[0];
                const prompt = `GuardDutyの脅威名「${threatName}」について、セキュリティ専門家でない人にも分かるように、これが何を意味し、どのようなリスクがあるのかを具体的に説明してください。日本語でお願いします。`;

                hideError('threat-summary-error');
                document.getElementById('threat-summary-content').classList.add('hidden');
                showLoading('threat-summary-loading');

                try {
                    const response = await callGeminiAPI(prompt);
                    document.getElementById('threat-summary-content').textContent = response;
                    document.getElementById('threat-summary-content').classList.remove('hidden');
                } catch (error) {
                    displayError('threat-summary-error', '脅威の詳細取得に失敗しました。');
                } finally {
                    hideLoading('threat-summary-loading');
                }
            });

            document.querySelectorAll('.action-details-btn').forEach(button => {
                button.addEventListener('click', async (event) => {
                    const index = event.target.dataset.index;
                    const action = findingData.RecommendedActions[index];
                    const prompt = `AWS環境におけるセキュリティインシデント対応として、「${action.title}」というアクションを実行する際の、具体的な手順やコマンド例、注意点を箇条書きで詳しく説明してください。対象はWindows EC2インスタンスです。日本語でお願いします。`;

                    const loadingEl = document.getElementById(`action-loading-${index}`);
                    const contentEl = document.getElementById(`action-content-${index}`);
                    const errorEl = document.getElementById(`action-error-${index}`);

                    hideError(errorEl.id);
                    contentEl.classList.add('hidden');
                    loadingEl.classList.remove('hidden');

                    try {
                        const response = await callGeminiAPI(prompt);
                        contentEl.textContent = response;
                        contentEl.classList.remove('hidden');
                    } catch (error) {
                        displayError(errorEl.id, '詳細ステップの取得に失敗しました。');
                    } finally {
                        loadingEl.classList.add('hidden');
                    }
                });
            });
        }
    </script>
</body>
</html>

図解することで、経営層や非エンジニアにも理解しやすい形になりますね!

最後に

今回はGuardDutyの検出結果をGeminiのCanvas機能で可視化する方法についてご紹介しました。
今回はGuardDutyを題材にしましたが、他にもログの可視化などがしやすくなるのではないでしょうか?

みなさんも、GeminiのCanvas機能を使った可視化にチャレンジしてみてくださいね!

最後までお読みいただきありがとうございました!

以上、おつまみ(@AWS11077)でした!

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.