IAM/S3のアクションをCloudTrailから通知する

# 動機
AWSの操作についてはCloudTrailで記録できる訳ですが、セキュリティの観点から「ヤバそうな操作」は検知・通報されたほうが何かとエンタープライズな組織では安心かと思います。
AWS Configルールで打ち取る、GuardDutyを使う、[evident.io](https://evident.io/)などの3rdパーティーソリューション利用も考えられますが、お財布のご都合もあるので「まずはざっくりと拾おうぜ」という考え方もあろうかと思いやってみました。

（お財布のご都合がなければ全体的にAWS Configでいい（1項目につき\$0.003、1ルールにつき\$2.00）と思われます）

# ヤバそうな操作
IAM関連の操作（ユーザー作成、ロール作成、ポリシーアタッチなど）やS3関連操作（バケット作成、バケットポリシー変更、Website Hostingの設定）あたり。

# CloudTrail
- まず「証跡情報」(Trail)を作成
    - S3バケットにLogを送るようにすると、そのイベントをSNSトピックに送ることができる
    - 届くのは、s3bucket名、ObjectKey。これをトリガーにobjectを取りに行ってgz解凍して、という流れになりそう
    - ただ、全部通知が来るので、全量欲しい訳ではない場合は過剰になる
    - 「IAMとS3関連のアクションだけ通知したい」には不向きかも
    - 今回はCloudWatch Logsに送って、そこからフィルタしたい

# CloudTrail Logs
- メトリクスフィルタからのアラームでやると、詳細が分からない
    - 検知はできるので、詳細は別途見ればいいという考えならばこれでもOKかも
- そこでログストリーミングをサブスクリプション
- サブスクリプションは1つのロググループに1つしか指定できない
    - Lambdaか、Elastic SearchServiceか
    - 変更もできない。。（削除して作成）
- サブスクリプションフィルタ
    - フィルタパターンの書き方：https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
    - ここで大枠を絞っておき、Lambda側で詳細を絞るのが良さそう
    - 例：`{ (($.eventSource = "s3.amazonaws.com") || ($.eventSource = "iam.amazonaws.com")) && ($.eventName != "Get*")  && ($.eventName != "List*") && ($.eventName != "Describe*")}`
    - ただしログが大量になる場合にはLambdaだとつらそうなのでここで絞る案もありえる
- 参考：[セキュリティーグループが変更されたらSlackに通知する | ハンズラボエンジニアブログ](https://www.hands-lab.com/tech/entry/2102.html)

# LambdaでSNSにPublish
- 事前にPublish先のSNSトピックは作っておく
- 参考：[CloudWatch LogsのLambdaによるログ監視 | Developers.IO](https://dev.classmethod.jp/cloud/aws/notify-error-cloudwatch-logs-with-lambda/)

```log2sns/index.js
var zlib = require('zlib');
var aws = require('aws-sdk');
var sns = new aws.SNS({ region: 'ap-northeast-1' });

exports.handler = function(input, context, callback) {
    var data = new Buffer(input.awslogs.data, 'base64');
    zlib.gunzip(data, function(e, result) {
        if (e) {
        callback(e);
        } else {
            result = JSON.parse(result.toString('utf-8'));
            var events = result['logEvents'].filter(
                        function(evt) { return evt['message'].match(process.env.FILTER_PATTERN);}
                    ).filter(
                        function(evt) { return !evt['message'].match(process.env.FILTER_PATTERN_EXCLUDED) ;}
                    ).map(
                        function(evt) { return evt['message'] }
                    );
            console.log('processing ' + events.length + '/' + result['logEvents'].length + ' events.');
            if (events.length === 0) {
                callback();
                return;
            }
            if (0 < events.length) {
                // has error log
                var subject = 'CloudWatch logs by lambda';
                var payload = { default: '' };
                payload['default'] += 'NotifyAt: ' + new Date() + '\n';
                payload['default'] += 'Log: ' + result['logGroup'] + ' - ' + result['logStream'] + '\n';
                payload['default'] += 'Filter: ' + result['subscriptionFilters'] + '\n';
                payload['default'] += 'Messages:\n';
                payload['default'] += events.join('\n---\n');
                sns.publish({
                    Subject: subject,
                    Message: JSON.stringify(payload),
                    MessageStructure: 'json',
                    TargetArn: process.env.SNS_ARN
                }, function(err, data) {
                    if (err) {
                      callback(err);
                    } else {
                      callback(null, data);
                    }
                });
            }
        }
    });
};
```

- あとはこのSNSにサブスクライブすれば各種通知が可能に。

# つまづいたポイント
- IAMのTrailがストリーミングされてこない・・・
    - リージョンがus-east-1のtrailだからではないか？Trailにはあるが、Logsに出ていない。
    - 東京のCloudTrail設定が「証跡情報を全てのリージョンに適用」が「いいえ」であるためと思われる。
        - リージョン間データ転送が発生するので個人で使う範囲だと落としたい設定
        - ドキュメント：https://docs.aws.amazon.com/ja_jp/awscloudtrail/latest/userguide/how-cloudtrail-works.html
    - us-east-1のTrailを有効化、CloudWatch Logsのロググループは共通とするように設定してみる。
        - 名前は同じでもLoggroupは別になった・・・（リージョンごとにある）
    - ストリーミング先のLambdaも別になる
        - Publish先のSNSは同じものを指定できる、とはいえLambdaのロジックが二重管理となってしまう
        - そのまま内容を別のLambdaに転送するようにする、など解決策はあろうかと思われるが、シンプルさに欠ける。

# 雑感
- ”エンタープライズな組織”ならカスタムルールは入れないまでも、AWS Configの費用は誤差範囲のような気もしてきました。
- SNSを挟んでいるので、これをトリガーにSlackへ通知やConnectで電話連絡なんかもできそう。
    - さらに「本当にヤバいやつ」はLambdaを呼んで直接Killしにいくような事もできそう。
    - でもそこまでやるなら[Cloud Custodian](https://github.com/capitalone/cloud-custodian)のようなものを検討したほうがいいような気もする。。