CloudTrail LakeでQuickSightのログを監査してみる

# はじめに

CloudTrailはAWSにおける全てのAPIを記録することができます。
**QuickSightのログはCloudTrailに出力されます。**

QuickSight側にログ閲覧インターフェースがあれば便利なのですが用意されていないため、残念ながらCloudTrailの仕様にも慣れる必要があります。

ログの概要を確認するだけであればCloudTrailのメニューである「イベント履歴」(直近90日間)だけで済みますが、**ログを詳細に調査しはじめると、そこから急激に深い海に足を踏み入れる**ことになります。

**深い海とはAthenaやCloudTrail Lakeを指します。**
従来、CloudTrailの詳細なログ調査は「AthenaでCloudTrail用のテーブルを作ってSQL文をかく」という方法がスタンダードでした。最近はCloudTrail LakeというSQLインターフェースができました。これによりテーブル作成の手間が省けるようにはなりましたが、中身はAthenaとほぼ変わらずSQL文を駆使していく形になります。

**CloudTrail LakeやAthenaでのログ調査は、json出力のCloudTrailイベントにSQLクエリをかけているという特性上、少し扱いづらい**ため備忘もかねて手順を記載していきます。

まずはシンプルな「イベント履歴」から説明し、次に「CloudTrail Lake」について説明します。

# 「イベント履歴」から概要をつかむ

ログの概要をつかむには、CloudTrailのコンソール画面から、左メニューの「イベント履歴」を開きます。

**「ルックアップ属性」として「イベントソース」を選択し、フィルタに「quicksight:amazonaws.com」を入力します。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/a41ccee4-4f52-26e4-4f37-bfbc41386260.png)

表示列を増やしたい場合は画面右上の歯車マークをクリックしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/8ab08259-bd58-3eb5-386a-86fec5fdd643.png)

以下のような列を追加できます。
「発信元IPアドレス」などは、よく使う列になると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/03e01fb1-afd4-6e47-8ad5-9b6fe7e98599.png)

## ログの出力内容について

QuickSightのログ情報ページをみると、重要なポイントがいくつか記載されています。
https://docs.aws.amazon.com/ja_jp/quicksight/latest/user/logging-using-cloudtrail.html

> キャプチャされたコールには、Amazon QuickSight コンソールからの呼び出しの一部と、Amazon QuickSight API オペレーションへのすべてのコード呼び出しが含まれます。

つまり1つのCloudTrailイベントのなかに、「API呼び出し」部分と、「QuickSightコンソールからの呼び出し(非API)」部分の２種類が含まれていると記載されています。これは意味が分かりづらいのですが、イベントが分離しているのではなく、**1つのイベントのなかにCloudTrailの汎用的な出力項目（API系ログ）と、QuickSight固有の出力項目がある（非API系ログ）と捉えてよさそうです。**

しかも**コンソールからの呼び出しは「一部」しか記録されない**ということです。

> ユーザーが Amazon QuickSight によってプロビジョニングされた場合、CloudTrail はユーザーを unknown と表示します。このように表示されるのは、これらのユーザーが既知の IAM ID のタイプではないためです。

また、QuickSightユーザ(IAMユーザではない)は、個別のユーザ名について識別できないことが記載されていますのでご留意ください。


### CloudTrailログ(汎用部分)の出力内容

APIレベルでの出力内容については以下を参照してください。
（出力項目の網羅性がなく、サンプルも少ないので注意です）

https://docs.aws.amazon.com/ja_jp/quicksight/latest/user/logging-using-cloudtrail.html

- 上記サイトより出力内容を以下に転記します(2023/2/15時点)。
  - userIdentity – ユーザー ID
  - eventTime – イベント時間
  - eventId – イベント ID
  - readOnly – 読み取り専用
  - eventSource (quicksight) – イベントのソース (Amazon QuickSight)
  - eventType (AwsServiceEvent) – イベントタイプ (AWS のサービスイベント)
  - recipientAccountId (ユーザーの AWS アカウント) – 受取人アカウント ID (ユーザーの AWS アカウント)

- 上記以外に私が確認したCloudTrailへの出力項目としては以下のようなものがあります（CreateDataSetを例に）
  - eventName - イベント名
  - awsRegion - AWSリージョン
  - sourceIPAddress- アクセス元IPアドレス (AWS のサービスでは、DNS 名のみが表示)
  - requestParameters - リクエストとともに送信されたパラメータ (ある場合)
  - responseElements - 変更を行うアクションのレスポンスの要素 (アクションの作成、更新、削除)。アクションが状態を変更しない場合 (例: オブジェクトの取得やリストのリクエスト)、この要素は省略される
  - managementEvent - イベントがAWSの管理イベントかどうかを識別するブール値
  - **serviceEventDetails** イベントをトリガーしたものとその結果を含むサービスイベント。(このjsonオブジェクトの中身が「非API系ログ」(=QuickSightの詳細ログ)にあたります)
  - eventCategory - AWSが識別するイベントのカテゴリ(Management,Data,Insight)

- 上記以外にも出力項目があります。CloudTrailはイベントによって出力内容が変わる部分があります。**CloudTrailが出力する項目の網羅的な情報は以下ページを参照**するとよいと思います。
   - https://docs.aws.amazon.com/ja_jp/awscloudtrail/latest/userguide/cloudtrail-event-reference-record-contents.html
     - オプションがFalseとなっているものはどんなイベントでも必ず出力される項目です。

### QuickSightログの出力内容

- CloudTrailが出力するjsonイベントの中にある **「serviceEventDetails」という項目が非API系ログ(※)に該当します。ここにQuickSightの操作ログが出力されます。**
    - ※正直なところユーザにとってはAPI系でも非API系でもどちらでもよいのですが、CloudTrailの仕様としては分類されており、公式の説明ページも分かれてしまっているので調査の際は用語を覚えておく必要があります。

- 詳しいイベント名については、以下URLを参照してください。
  - https://docs.aws.amazon.com/ja_jp/quicksight/latest/user/logging-non-api.html
  - QuickSightログ出力内容を確認する際は、こちらから見ていくのが直感的でわかりやすいと思います。


上記サイトより出力内容を以下に転記します(2023/2/15時点)。

- ユーザー管理
   - CreateAccount – アカウントの作成
   - BatchCreateUser – ユーザーの作成
   - BatchResendUserInvite – ユーザーの招待
   - UpdateGroups – グループの更新(Enterprise Edition でのみ機能)
   - UpdateSpiceCapacity – SPICE 容量の更新
   - DeleteUser – ユーザーの削除
   - Unsubscribe – ユーザーのサブスクリプション解除
- サブスクリプション
   - CreateSubscription – サブスクリプションの作成
   - UpdateSubscription – サブスクリプションの更新
   - DeleteSubscription – サブスクリプションの削除
- ダッシュボード
  - GetDashboard – ダッシュボードの取得
  - CreateDashboard – ダッシュボードの作成
  - UpdateDashboard – ダッシュボードの更新
  - UpdateDashboardAccess – ダッシュボードアクセスの更新
  - DeleteDashboard – ダッシュボードの削除
- 分析
  - GetAnalysis – 分析の取得
  - CreateAnalysis – 分析の作成
  - UpdateAnalysisAccess – 分析アクセスの更新
  - UpdateAnalysis – 分析の更新
    - RenameAnalysis – 分析の名前変更
    - CreateVisual – ビジュアルの作成
    - DeleteAnalysis – ビジュアルの名前変更
    - DeleteVisual – ビジュアルの削除
    - DeleteAnalysis – 分析の削除
- データソース
  - CreateDataSource – データソースの作成
    - FlatFile – フラットファイル
    - External – 外部
    - S3 – S3
    - ImportS3ManifestFile – S3 マニフェストファイル
    - Presto – Presto
    - RDS – RDS
    - Redshift – Redshift (手動)
  - UpdateDataSource – データソースの更新
  - DeleteDataSource – データソースの削除
- データセット
  - CreateDataSet – データセットの作成
    - CustomSQL – カスタム SQL
    - SQLTable – SQL テーブル
    - ファイル – CSV または XLSX
  - UpdateDataSet – SQL 結合データセットの更新
  - UpdateDatasetAccess – データセットアクセスの更新
  - DeleteDataSet – データセットの削除
  - Querydatabase – データセットの更新中にデータソースをクエリします。

上記のとおり、 **QuickSightへのログイン系のイベントは残念ながら出力されません。**
また、QuickSightのGUI操作における成功系のログは出力されますが、**失敗系のログは出力されません。**

## イベント履歴の一覧では調査しきれないケース

例えば監査やセキュリティを目的とした調査の場合、「QuickSightでユーザが新たなデータセットを作成していないかを調べる」のはよくあるケースかと思います。

データセット作成系を検知するイベント名としては、上記のとおり、データセット作成は「CreateDataSet」、データソース作成は「CreateDataSource」などが対象になります。


しかし「イベント履歴」の場合、ユーザ名、日時、イベント名などは出力できても、**「どのようなデータセットを作成したか」「どのようなデータソースを作成したか」まではわかりません。**

まずはイベントの詳細を見て、それらが元のjsonに出力されているかを見てみます。

## イベント履歴の詳細を確認する

### CreateDataSetのデータ例

ではイベントの詳細をコンソールから見ていきます。
「イベント履歴」で出力されたイベントから、例として「CreateDataSet」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/51482cfb-19d1-e651-db59-3c258978b509.png)

イベント詳細が表示されます。画面下部のイベントレコードが、実際のイベントデータの本体(json)になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/a56a2f95-36c5-977a-b0fe-ba8fe772f92b.png)

私の環境で、SSOユーザで操作した際のjsonデータを以下に貼り付けます。(一部マスキングしてあります）

```json
{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "AROAXXXXXXXXXXXXXXXXX:user-name.beex",
        "arn": "arn:aws:sts::012345678901:assumed-role/AWSReservedSSO_AdministratorAccess_7bc3XXXXXXXXXXXX/user-name.beex",
        "accountId": "012345678901",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "AROAXXXXXXXXXXXXXXXXX",
                "arn": "arn:aws:iam::012345678901:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_AdministratorAccess_7bc3XXXXXXXXXXXX",
                "accountId": "012345678901",
                "userName": "AWSReservedSSO_AdministratorAccess_7bc3XXXXXXXXXXXX"
            },
            "webIdFederationData": {},
            "attributes": {}
        }
    },
    "eventTime": "2023-02-08T02:45:47Z",
    "eventSource": "quicksight.amazonaws.com",
    "eventName": "CreateDataSet",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "123.456.789.012",
    "requestParameters": null,
    "responseElements": null,
    "eventID": "23b478fd-1565-47dd-bed7-XXXXXXXXXXXX",
    "readOnly": false,
    "eventType": "AwsServiceEvent",
    "managementEvent": true,
    "recipientAccountId": "012345678901",
    "serviceEventDetails": {
        "eventRequestDetails": {
            "dataSetName": "vgsales",★★★★★
            "dataSetMode": "SPICE",
            "dataSetDefinition": {
                "dataSetType": "sql",
                "dataSourceId": "arn:aws:quicksight:ap-northeast-1:012345678901:datasource/407b9d0e-d498-441a-8bf8-XXXXXXXXXXXX",
                "tableDetails": {
                    "tableName": "vgsales",★★★★★
                    "tableSchema": "glue_connector_test",★★★★★
                    "columns": [
                        "Rank",
                        "Name",
                        "Platform",
                        "Year",
                        "Genre",
                        "Publisher",
                        "NA_Sales",
                        "EU_Sales",
                        "JP_Sales",
                        "Other_Sales",
                        "Global_Sales"
                    ]
                },
                "permissionsDataSetConfiguration": "null",
                "columnLevelSecurityUpdated": false
            }
        },
        "eventResponseDetails": {
            "dataSetId": "arn:aws:quicksight:ap-northeast-1:012345678901:dataset/29ddd1aa-ebde-4645-aec4-XXXXXXXXXXXX"
        }
    },
    "eventCategory": "Management"
}
```

- 上記json内に「serviceEventDetails」jsonオブジェクトの深いところ(★★★★★をつけた箇所)に、以下のようなキーバリューがあることがわかります。


| キー：バリュー | 説明 |
|:-|:-|
| "dataSetName": "vgsales"  | QuickSight上のデータセット名  |
| "tableName": "vgsales"  | 参照しているテーブル名(この場合Glue上のテーブル名)  |
|"tableSchema": "glue_connector_test"   | 参照しているスキーマ名(この場合Glue上のデータベース名)  |



### CreateDataSourceのデータ例

一方、CreateDataSourceのデータ例を以下に貼り付けます。(一部マスキングしてあります）

```json
{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "AROAXXXXXXXXXXXXXXXXX:user-name.beex",
        "arn": "arn:aws:sts::012345678901:assumed-role/AWSReservedSSO_AdministratorAccess_7bc3XXXXXXXXXXXX/user-name.beex",
        "accountId": "012345678901",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "AROAXXXXXXXXXXXXXXXXX",
                "arn": "arn:aws:iam::012345678901:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_AdministratorAccess_7bc3XXXXXXXXXXXX",
                "accountId": "012345678901",
                "userName": "AWSReservedSSO_AdministratorAccess_7bc3XXXXXXXXXXXX"
            },
            "webIdFederationData": {},
            "attributes": {}
        }
    },
    "eventTime": "2023-02-08T02:45:03Z",
    "eventSource": "quicksight.amazonaws.com",
    "eventName": "CreateDataSource",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "123.456.789.012",
    "requestParameters": null,
    "responseElements": null,
    "eventID": "1797e355-2445-4fbf-bcad-XXXXXXXXXXXX",
    "readOnly": false,
    "eventType": "AwsServiceEvent",
    "managementEvent": true,
    "recipientAccountId": "012345678901",
    "serviceEventDetails": {
        "eventRequestDetails": {
            "dataSource": {
                "dataSourceType": "ATHENA",★★★★★
                "databaseName": "ATHENA"★★★★★
            }
        },
        "eventResponseDetails": {
            "dataSourceId": "arn:aws:quicksight:ap-northeast-1:012345678901:datasource/407b9d0e-d498-441a-8bf8-XXXXXXXXXXXX"
        }
    },
    "eventCategory": "Management"
}
```

- 上記json内に「serviceEventDetails」jsonオブジェクトの深いところ(★★★★★をつけた箇所)に、以下のようなキーバリューがあることがわかります。


| キー：バリュー | 説明 |
|:-|:-|
| "dataSourceType": "ATHENA"  | QuickSight上のデータソースの種類  |
| "databaseName": "ATHENA"  | 参照しているデータベース名(※)  |

- ※データソースにAthenaを指定している場合は"ATHENA" 表記になりますが、RDSやRedshiftなど通常のDBをデータソースとして指定した場合は実際のDB内の固有のDB名が表示されます。

- また、上記JSONには「データソース名」が出力されておらず、代わりに「dataSourceId」というARNのみ判別できることがわかります。
  - つまり、UI上で作成した内容が直感的に記載されているわけではないことに注意が必要です。

- CreateDataSourceイベントの出力は、**先程のCreateDataSetと比べると[eventRequestDetails]配下の出力項目が全く異なる**ことがわかります。
  - **このように取り出したい情報の構造がQuickSightのイベントによって異なるため、まず各イベントの詳細をJSONで見る必要があることがわかります。**

## 複数のイベントから総合的に判断する

「CreateDataSet」は、データセット名やデータベース名などがわかりますが、それがどのデータソースによるものかはわかりません。データソースは「CreateDataSource」イベントに出力されます。

CloudTrail上で両者を紐付けできれば「どのデータソースを元にどのデータセットが作成されたか」がわかりそうなものです。
しかし「データソース」は使い回すことが多いため、「データセット作成」ログの直前にない場合もあります。このヒモ付けを厳密に調べようとすると大変で、CloudTrailだけでは不可能です(※)。CloudTrailでは「近しい時間帯で同一ユーザ・同一IPによるイベントか」というようなファジーな基準で調査していくとよいと思います。

- ※「コンソールでデータセット名とデータソース名を確認する」「コマンドでデータソースのARNを確認する」「CloudTrailのdataSourceIdと、コマンド結果が一致するか確認する」などのプロセスを、1データセット毎で行う形になるかと思います。
    - また、QuickSightのセッションIDもCloudTrailには出力されません。

さて、監査やセキュリティチェックを行う上で、**全てのログにたいし上記のような確認をコンソールで行うのは現実的ではないので、一覧化して全体を掴みたいところです。また、直近90日よりも古いログはSQLクエリでの調査が必要になります。そこでCloudTrail Lakeを使います。**

# 「CloudTrail Lake」で任意の項目を一覧化する

## CloudTrail Lakeについて

CloudTrail Lakeは2022年1月に[発表](https://aws.amazon.com/jp/about-aws/whats-new/2022/01/aws-cloudtrail-lake-audit-security/)された、CloudTrailログへのSQL発行などができる機能です。
CloudTrailコンソールの左メニューの「Lake」というカテゴリにあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/ae8013aa-4b79-bd41-9b97-f24f63762cd2.png)

- CloudTrail Lakeの主な特徴としては以下だと考えています。
   - **従来(Athena)よりもSQLクエリによる詳細調査の手間が少ない。**
     - Athenaのなんやかんや(テーブル作成/workgroupの概念等)に煩わされないという点においては、ユーザに優しくなっている。
     - 特に**CloudTrail Lakeではパーティション作成が不要**な点が気に入っています。
   - **マルチリージョン、Organizations内のマルチアカウントを一気に調査できる。**
     - 初期セットアップ時にデフォルトでマルチリージョンが対象になるため楽。

- その他ポイント
  - データ領域について
     - Cloudtrail LakeのSQLスキャン対象は**CloudTrail上の「証跡」で設定したS3やCloudWatchLogsの領域ではなく**、専用の「イベントデータストア」が対象になります。
       - [公式ブログ](https://aws.amazon.com/jp/blogs/news/announcing-aws-cloudtrail-lake-a-managed-audit-and-security-lake/)などでもイベントデータストアはデータが不変（イミュータブル）であることが強調されています。（AWSでイミュータブルなデータストアというと[Amazon QLDB](https://aws.amazon.com/jp/qldb/)が思い浮かびます）
   - 料金について
     - 少し分かりづらいので、[公式](https://aws.amazon.com/jp/cloudtrail/pricing/)よりも以下のサイトを参照するとよいかもしれないです。
        - https://dev.classmethod.jp/articles/cloudtrail-lake-pricing/
        - https://fun-every-day.com/2022/12/30/cloudtrail-lake-cloudtrail-log/
        - Athenaはクエリのスキャンデータ量に課金されるだけですが、CloudTrail Lakeは取り込み時料金がソコソコかかるのと、クエリ料金がわずかにかかります。
        - 以下は本記事執筆時点での利用料金です。「7年間分」のストレージ領域は事前に確保されていて、**取り込み時に、2.5ドル/1GBの料金がかかります。例えていうならホテルの入り口で料金がかかるだけで、部屋の料金はかからず、7年間滞在できるような感じ**です。私の検証環境では全リージョン利用にして取り込み料金が 3ドル/月程度でした。SQLクエリ料金は、0.005USD/1GなのでデータソースがCloudTrailだけであれば、where句で期間を絞ることであまり気にならないレベルになろうかと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/7b8b2fdf-8365-1067-6145-2e931ec35221.png)

  - また、2023年2月にはAWS以外のアプリケーション(独自アプリやGitHub等)もCloudTrailっぽく取り込むマルチソースな「統合(Integration)」機能も[発表](https://aws.amazon.com/jp/blogs/news/new-aws-cloudtrail-lake-supports-ingesting-activity-events-from-non-aws-sources/)されています。

### CloudTrail Lakeの設定

- 初期セットアップとしてCloudTrail Lake用のイベントデータストアを作成する必要があります。
   - https://docs.aws.amazon.com/ja_jp/awscloudtrail/latest/userguide/query-event-data-store-cloudtrail.html
   - https://dev.classmethod.jp/articles/aws-cloudtrail-lake-audit-security/

私は保存期間をデフォルトの7年から2年に変更しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/44d3829b-2271-35a3-ed7b-ad53e734e67f.png)

あとはすべてデフォルトで作成しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/e590f5cc-ecaa-c9a3-34fa-97da2dbfb0c2.png)

- セットアップ以前のデータを閲覧したい場合は、「証跡イベントをコピー」を選択し、過去に作成したであろう証跡から指定日時の範囲で取り込むこともできます。(手順は割愛します)

### CloudTrail Lakeの利用

イベントデータストアができたら、SQLクエリが実行できるようになります。
利用できるSQLの種類については以下を参照してください。

https://docs.aws.amazon.com/ja_jp/awscloudtrail/latest/userguide/query-limitations.html

[エディタタブ]をクリックし、先程作成したイベントデータストアを選択します。
**テーブル名はイベントデータストアIDそのもの**になりますので、コピーボタンでコピーしておきます。
あとはクエリを書きます。 FROM句にはイベントデータストアIDを貼り付けます。

実行ボタンを押すとクエリ結果に出力されます。（S3に出力することも可能です。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/466a3665-6f30-a24f-d914-92dc9f9d4436.png)

QuickSight系イベントの最新10件を表示するクエリは以下です。
(FROM句のイベントデータストアID部分はご自身のものに書き換えてください）

```sql
SELECT
  *
FROM
  a173b946-2d63-4934-bc40-XXXXXXXXXX
WHERE
  eventSource = 'quicksight.amazonaws.com'
ORDER BY
  eventTime DESC
LIMIT
  10;
```

なお、クエリでエラーが発生した場合は「クエリ結果」タブに何も表示されないことが多々あります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/4019eb52-7f87-e0fd-168d-73d475645453.png)

おかしいなと思ったら「コマンド出力」タブのほうを見る癖をつけてください。
レスポンス列に、SQLのエラー出力が確認できます。（長いことが多いので見づらいですが）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/0539efe1-3bc5-962b-cf89-eb7d887cf2f9.png)


### サンプルクエリ

CloudTrail Lakeのクエリ例としては公式サイトに少しだけ記載されています。
https://docs.aws.amazon.com/ja_jp/awscloudtrail/latest/userguide/query-lake-examples.html

そのほか、CloudTrailのコンソール上で「サンプルクエリ」というタブをクリックすると、様々なユースケースのクエリが表示されます。右側のクエリSQLをクリックすると、時間やデータストアIDが自分の環境のものに置き換えたクエリがエディタ上に表示されます。
(時間についてはUTC表記ですのでご注意ください)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/4a58e620-dac6-ddfb-f97e-b2233344e309.png)

残念ながらサンプルには本記事の執筆時点でQuickSight関連のクエリが存在しませんでした。

クエリを作るためには**CloudTrail Lakeの作法に慣れておく必要がある**ため、QuickSight以外で個人的によく使いそうなクエリをいくつかピックアップします。
(変数、時間、データストアIDなどは適宜置き換えてください)

- 特定のユーザーが指定された日付範囲で呼び出したすべての API を検索するクエリ。(以下)

```sql
SELECT
    eventID, eventName, eventSource, eventTime, userIdentity.arn AS user
FROM
    a173b946-2d63-4934-bc40-XXXXXXXXX
WHERE
    userIdentity.arn LIKE '%<username>%'
    AND eventTime > '2023-02-09 00:00:00' AND eventTime < '2023-02-12 00:00:00'

```


- 過去 1 週間でコンソールに最も多くサインインしたユーザーを検索するクエリ。(以下)
  - サンプルが動かなかったのでGROUP BY句を少し修正しました。

```sql
SELECT
    userIdentity.arn AS user, COUNT(*) AS loginCount
FROM
    a173b946-2d63-4934-bc40-XXXXXXXXX
WHERE
    eventName = 'ConsoleLogin'
    AND eventTime > '2023-02-09 00:00:00'
GROUP
    BY userIdentity.arn
ORDER
    BY count(*) DESC
```

- 過去 1 週間以内に手動で作成されたすべてのリソースを検索するクエリ(以下)
  - バックアップ系ロールなども含まれてしまったので、除外する行を追加しました。
     - 手動といってもcloudformation系の処理を除外しているだけですので、他にも不要な出力があるかもしれません。
  - 私の環境だと出力結果が想定より少なかったので、「イベントデータストア」の設定に「データイベント」として「CloudTrail」を追加するとよいかもしれません。

```sql
SELECT
    userIdentity.arn AS user, userIdentity, eventTime,
    eventSource, eventName, awsRegion, requestParameters,
    resources, requestID, eventID
FROM
    a173b946-2d63-4934-bc40-XXXXXXXXX
WHERE
    (eventName LIKE '%Create%')
    AND resources IS NOT NULL
    AND userIdentity.sessioncontext.sessionissuer.username NOT LIKE 'AWSServiceRole%'
    AND userIdentity.sessioncontext.sessionissuer.username NOT LIKE 'AWSBackupDefaultServiceRole%'
    AND userIdentity.sessioncontext.sessionissuer.username IS NOT NULL
    AND sourceIpAddress != 'cloudformation.amazonaws.com'
    AND eventTime > '2023-02-09 00:00:00'
ORDER
    BY eventTime DESC
```

- 過去 1 週間以内に表示または変更したすべての Glue データベースとテーブルを検索するクエリ(以下)
  - 元のクエリはLakeFormationの「データベース管理者」を絞ったものでしたが、私はLakeFormationを使っていないので行を削りました。
  - また、元のクエリは eventTimeの比較に単純なミスがあったので、そこも修正しました。
```sql
SELECT
    element_at(requestParameters, 'databaseName') AS DatabaseName,
    element_at(requestParameters, 'name') AS TableName,
    eventName, userIdentity.arn AS user
FROM
    a173b946-2d63-4934-bc40-XXXXXXXXX
WHERE
    eventSource ='glue.amazonaws.com'
    AND element_at(requestParameters, 'databaseName') IS NOT NULL
    AND element_at(requestParameters, 'name') IS NOT NULL
    AND eventTime > '2023-02-09 00:00:00' 
```

- サンプルにはセキュリティ・監査系のクエリも多く含まれています。
- AWSのベストプラクティスとしてはSecurityHubやGuardDuty,AWS Configなどの有効化が推奨されていますから、それらで検知したうえで、セキュリティインシデントであればAmazon Detectiveなどで検索する形が多いと思います。
- しかしイベントドリブン調査ではない場合や、インシデント周辺含め網羅的に調査を行う場合は、CloudTrail LakeでのSQLなどを使うケースはあると思います。

## QuickSight系イベントの構造

JSONオブジェクトの出力形式をみながら、QuickSightログを調べていきます。

### CloudTrail LakeにおけるJSONオブジェクトの出力について

さきほどのサンプルクエリを見てみると、selectに```element_at```が使われているものと、そうでないものがあります。

element_atはSQLの関数です。実例をみないとわかりづらいので、クエリ出力の例としてCreateDataSetイベントを取り上げてみます。

- 以下のようにクエリの結果は連想配列のような形で出力されていますが、**フィールドの一部にmap型({key=value,key=value}形式)のものが含まれている**ことがわかります。(Qiitaのテーブルの仕様変更で見づらくなったため、一部の列のみ抜粋します）例えば「userIdentity」カラムなどがそれに該当します。

|eventVersion|userIdentity|eventTime|
|:----|:----|:----|
|1.08|{type=AssumedRole, principalid=AROAXXXXXXXXXXXXXX:user-name.beex, arn=arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_AdministratorAccess_XXXXXXXXXXXXXX/user-name.beex, accountid=123456789012, accesskeyid=null, username=null, sessioncontext={attributes={creationdate=null, mfaauthenticated=null}, sessionissuer={type=Role, principalid=AROAXXXXXXXXXXXXXX, arn=arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_AdministratorAccess_XXXXXXXXXXXXXX, accountid=123456789012, username=AWSReservedSSO_AdministratorAccess_XXXXXXXXXXXXXX}, webidfederationdata={federatedprovider=null, attributes=null}, sourceidentity=null, ec2roledelivery=null, ec2issuedinvpc=null}, invokedby=null, identityprovider=null}|2023-02-15 15:01:05.000|

- 上記のmap型で出力されるものは、CloudTrailのJSONでみると(以下再掲)、第一階層でオブジェクトがネストされているものが該当します。
   - 以下に★★★★★をつけた userIdentity、serviceEventDetails のところです。


```json
{
    "eventVersion": "1.08",
    "userIdentity": {　　　　　　　★★★★★
        "type": "AssumedRole",
        "principalId": "AROAXXXXXXXXXXXXXXXXX:user-name.beex",
        "arn": "arn:aws:sts::012345678901:assumed-role/AWSReservedSSO_AdministratorAccess_7bc3XXXXXXXXXXXX/user-name.beex",
        "accountId": "012345678901",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "AROAXXXXXXXXXXXXXXXXX",
                "arn": "arn:aws:iam::012345678901:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_AdministratorAccess_7bc3XXXXXXXXXXXX",
                "accountId": "012345678901",
                "userName": "AWSReservedSSO_AdministratorAccess_7bc3XXXXXXXXXXXX"
            },
            "webIdFederationData": {},
            "attributes": {}
        }
    },
    "eventTime": "2023-02-08T02:45:47Z",
    "eventSource": "quicksight.amazonaws.com",
    "eventName": "CreateDataSet",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "123.456.789.012",
    "requestParameters": null,
    "responseElements": null,
    "eventID": "23b478fd-1565-47dd-bed7-XXXXXXXXXXXX",
    "readOnly": false,
    "eventType": "AwsServiceEvent",
    "managementEvent": true,
    "recipientAccountId": "012345678901",
    "serviceEventDetails": {              ★★★★★
        "eventRequestDetails": {
            "dataSetName": "vgsales",
            "dataSetMode": "SPICE",
            "dataSetDefinition": {
                "dataSetType": "sql",
                "dataSourceId": "arn:aws:quicksight:ap-northeast-1:012345678901:datasource/407b9d0e-d498-441a-8bf8-XXXXXXXXXXXX",
                "tableDetails": {
                    "tableName": "vgsales",
                    "tableSchema": "glue_connector_test",
                    "columns": [
                        "Rank",
                        "Name",
                        "Platform",
                        "Year",
                        "Genre",
                        "Publisher",
                        "NA_Sales",
                        "EU_Sales",
                        "JP_Sales",
                        "Other_Sales",
                        "Global_Sales"
                    ]
                },
                "permissionsDataSetConfiguration": "null",
                "columnLevelSecurityUpdated": false
            }
        },
        "eventResponseDetails": {
            "dataSetId": "arn:aws:quicksight:ap-northeast-1:012345678901:dataset/29ddd1aa-ebde-4645-aec4-XXXXXXXXXXXX"
        }
    },
    "eventCategory": "Management"
}
```

### map型の取り出し方

#### map型の取り出し方(通常)

- 通常のフィールドは、以下の ```eventID```のように何も気にすることなく取得できます。
- map型のフィールドについては、(おそらく)第二階層までなら、以下の```userIdentity.arn```のようにドットでつなげることで取得できます。

```sql
SELECT
    eventID, userIdentity.arn
FROM
    a173b946-2d63-4934-bc40-XXXXXXXXX
```

#### map型の取り出し方(ネスト)

- map型のフィールドでネストされている場合は、**まずelement_at関数で連想配列の要素をjsonオブジェクトとして取り出します。**
  - element_at関数の使い方についてはAthenaのページが参考になると思います。
    - https://docs.aws.amazon.com/ja_jp/athena/latest/ug/accessing-array-elements.html
  - 以下は"serviceEventDetails"の直下にある"eventRequestDetails"を取り出しています。
    - 第１引数としてカラム名を指定し、第２引数には直下の要素名を文字列で指定しています。

```sql
element_at(serviceEventDetails, 'eventRequestDetails')
```

- element_at関数を実行すると要素の取得ができます。この場合jsonデータが返ってきます。今度はjsonデータに対し**json_extract_scalar関数を使って、任意のパスの文字列を取り出します。**
  - json_extract_scalar関数についてもAthenaのページが参考になると思います。
     - https://docs.aws.amazon.com/ja_jp/athena/latest/ug/extracting-data-from-JSON.html
  - **jsonとして取り出した時点で、JSONPathが使えます。**
    - 指定方法は、$がjsonのルートディレクトリとなっており、そこからドットをつなげることでルートからの第１階層、第２階層というようにたどることができます。
  - JSONPathの使い方はkinesisのページが個人的にわかりやすかったです。
    - https://docs.aws.amazon.com/ja_jp/kinesisanalytics/latest/dev/about-json-path.html
  - 以下は、見やすさのため第一引数を"jsonデータ"と省略していますが、jsonルート直下の「dataSetName」を文字列として取得する例です。



```sql
json_extract_scalar( <jsonデータ>,'$.dataSetName')
```

上記の関数を組み合わせると以下のような形になります。

```sql
  json_extract_scalar(
    element_at(serviceEventDetails, 'eventRequestDetails'),
    '$.dataSetName'
  )
```

これでJSONオブジェクトを以下のようにたどることができました。
**serviceEventDetails -> eventRequestDetails -> dataSetName**

- もう少しJSON階層の深いところを取得する場合は、先に説明したようにドットで要素をつなぎます。
  - 以下は(データセット名ではなく)データソースのタイプを取得する例です。

```sql
json_extract_scalar(
  element_at(serviceEventDetails, 'eventRequestDetails'),
  '$.dataSource.dataSourceType'
)
```

### CloudTrail Lakeの関数について

CloudTrail Lakeがサポートする関数は、内部で使われている Presto 0.266 に基づくようです。

https://docs.aws.amazon.com/ja_jp/awscloudtrail/latest/userguide/query-limitations.html#query-supported-functions

- 上記ページから本記事執筆時点での関数一覧を以下に引用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/b0b889ff-3c18-2834-cc44-439ab29bcba8.png)　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/c854ea15-9cef-d345-991b-49bd486f82cd.png)

- 上記表の見方として、例えばelement_at関数では以下の形となっており、右矢印が返り値を示すようです。
  - 第１引数：Map型もしくはArray型
  - 第２引数：Object型もしくは数値型
  - 返り値：Object型


- 上記ページでは、関数詳細の参照先としてPresto本家のリンク(以下)が貼られているのですが、正直見づらいので先程のようにAthenaのページから関数を探したほうがよいと思います。
   - https://prestodb.io/docs/current/functions/json.html


### QuickSight系ログのクエリ例

以上のような知識を組み合わせて、監査用のクエリを作ってみます。

- 「データセット作成」あるいは「データソース作成」イベントを実行した**ソースIP**などを調査するクエリ。

```sql
SELECT
  userIdentity.arn AS USER,
  eventName,
  eventTime,
  sourceIPAddress
FROM
  a173b946-2d63-4934-bc40-XXXXXXXXX
WHERE
  eventSource = 'quicksight.amazonaws.com'
  AND (
    eventName = 'CreateDataSet'
    OR eventName = 'CreateDataSource'
  )
ORDER BY
  eventTime DESC
LIMIT
  10;
```

上記クエリの出力例は以下です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/35ddc45c-4c90-48b4-b384-c98196d1f08b.png)


- 「データセット作成」あるいは「データソース作成」イベントを対象にして、ユーザ名、イベント名、時間、**データセット名、データソースタイプを表示**するクエリ例

```sql
SELECT
  userIdentity.arn AS USER,
  eventName,
  eventTime,
  json_extract_scalar(
    element_at(serviceEventDetails, 'eventRequestDetails'),
    '$.dataSetName'
  ) AS dataSetName,
  json_extract_scalar(
    element_at(serviceEventDetails, 'eventRequestDetails'),
    '$.dataSource.dataSourceType'
  ) AS dataSourceType
FROM
  a173b946-2d63-4934-bc40-XXXXXXXXX
WHERE
  eventSource = 'quicksight.amazonaws.com'
  AND (
    eventName = 'CreateDataSet'
    OR eventName = 'CreateDataSource'
  )
ORDER BY
  eventTime DESC
LIMIT
  10;
```

上記クエリの出力例は以下です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538058/8afc708b-b0fe-c8c9-bad5-e9ec02cea6d3.png)

- 上記クエリ結果の説明
  - CreateDataSetイベント行はdataSetNameが出力されます。
  - CreateDataSourceイベント行はdataSourceTypeが出力されます。
  - 両者を厳密に紐づけることはできませんが、時間的な近さからみて特定のユーザが、QuickSight上でAthena用データソースを作成し、特定の名前のデータセットを作成した、というような操作を追うことができます。
  - これによりQuickSightで意図しないデータが共有されてしまった場合などに、データセットを生成したユーザを探し出すことができます。

# 終わりに

だいぶん長い記事になってしまいましたが、QuickSightのログ出力詳細や、CloudTrail Lakeクエリの詳細に言及した記事を見かけなかったため、個人的な備忘のために詳しく書いてみました。

json型へのクエリはもっと良い方法があるかもしれません。
今回記載したクエリは一例です。応用の一助となれば幸いです。

- 雑まとめ
  - CloudTrailの「イベント履歴」ではQuickSightログの詳細までは確認できない。
  - QuickSightの操作イベントはCloudTrailにも記録されないものがある。
  - 詳細を一覧出力するにはCloudTrail LakeまたはAthenaを用い、SQLクエリを書く必要がある。
  - SQLクエリでネストされたオブジェクトをselectするには、複数の関数を使ってjsonオブジェクトから要素を取り出す必要がある。

以上です。