ServerlessでシャッとAWS Guard Dutyの驚異検出（重要度中以上）をSlackに通知する(JS)

昨今騒がれているアレとは違いますが中々セキュリティ監視って大変ですよね。

専任のセキュリティ管理者がいればいいですが兼任してたりで中々継続的に調べる事も難しい事が多々あると思います。
そこでそういうものは[AWS Guard Duty](https://aws.amazon.com/jp/guardduty/)にお任せしましょう。

## AWS Guard Dutyとは？

詳しい方にご説明は譲ります

[【レポート】自動セキュリティ分析サービスGuardDutyのよくわかる解説 #reinvent # SID218](https://dev.classmethod.jp/cloud/aws/aws-reinvent2017-guardduty-intro-sid218/)

上のサマリーで一言でいうと

> ログを機械学習に突っ込んで役に立つ脅威情報を使える状態で出すよ

です。

## 料金について

普段の業務では一般的な

* ELB
* EC2
* RDS
* S3

みたいな構成でサービスを組んでいてEC2インスタンス自体は約30台あって
料金は月3.25$でした。
AWS Guard Dutyの検出頻度だったりEC2の台数だったりVPCの数で料金は変わってくると思うので参考程度にお願いしますmm


## AWS Guard DutyをONにするだけではそれを毎回見なくてはならないです

辛いですよね。
そこで自分が大好きな[Serverless Framework](https://serverless.com/)を使ってGuard Dutyが驚異(重要度中以上)を検知したらSlackに通知をしてみたいと思います。

[Serverless Framework](https://serverless.com/)内部は[CloudFormation](https://aws.amazon.com/jp/cloudformation/)で管理してくれるのでサービス停止忘れとかがないし、設定わかりやすいしドキュメントが丁寧なのでLambda使うならマジおすすめします。

## 環境

* MacOSX
* Node8.10
* Serverless 1.47.0


## Gurad Dutyを有効化にする

[【re:invent2017】新サービス Amazon GuardDutyを触ってみた](http://blog.serverworks.co.jp/tech/2017/11/30/guardduty/)

ココらへんを参考にしてください。

難しい事はありません。ボタンを押すだけです。

## ServerlessでTemplateを作成する

今回はこんな感じのLambda Functionを作成します

* Function名
    * guard-duty-watcher-dev-handler (開発)
    * guard-duty-watcher-prod-handler (本番)

```
npm install -g serverless
serverless create --template aws-nodejs --path guard-duty-watcher
```

下記のようなディレクトリができると思います。

```
guard-duty-watcher
├── handler.js
├── package-lock.json
├── package.json
├── serverless.yml
```

## @slack/clientとaws-sdkのインストール

SlackのWebHookURLをsecret managerに保管しているので使わない方はaws-sdkのインストールは不要です。


```
npm i @slack/client aws-sdk --save
```

## serverless.ymlの修正

こんな感じです。

* secret managerを使っていない方はiamRoleStatementsの

```
    - Effect: "Allow"
      Action:
        - "secretsmanager:*"
      Resource:
        - "*"
```

は不要です。


```yml:serverless.yml
service: guard-duty-watcher
provider:
  name: aws
  runtime: nodejs8.10
  region: ${self:custom.defaultRegion}
  stage:  ${opt:stage, self:custom.defaultStage}
  timeout: 180
  memorySize: 128
  logRetentionInDays: 7
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "cloudwatch:ListMetrics"
        - "cloudwatch:GetMetricStatistics"
        - "cloudwatch:Describe*"
      Resource:
        - "*"
    - Effect: "Allow"
      Action:
        - "logs:*"
        - "logs:CreateLogGroup"
        - "logs:CreateLogStream"
        - "logs:PutLogEvents"
      Resource:
        - "*"
    - Effect: "Allow"
      Action:
        - "secretsmanager:*"
      Resource:
        - "*"
custom:
  defaultRegion: ap-northeast-1

functions:
  handler:
    handler: handler.handle
    description: "GuardDurtyのセキュリティ脅威を通知します(重要度中以上のもの)"
    environment:
      SERVERLESS_ENV_STAGE: "${opt:stage, 'dev'}"
    events:
      - cloudwatchEvent:
          event:
            source:
              - 'aws.guardduty'
            detail-type:
              - 'GuardDuty Finding'
```

## handler.jsを書く

さらっと50行位なので素の感じで

```js:handler.js
'use strict';
const SlackClient = require('./src/slack/client');

module.exports.handle = async (event) => {
  // 時間がLambdaとlocalで違うので設定する
  process.env.TZ = 'Asia/Tokyo';
  const slackClient = new SlackClient();
  if (event.detail.severity < 3.9) {
    return {
      statusCode: 304,
      message: 'guard-duty Notify Fire',
    };
  }

  let slackChannel = 'sandbox';
  let severityText = '中';

  if (event.detail.severity > 3.9) {
    if (process.env.SERVERLESS_ENV_STAGE != 'dev') slackChannel = 'warn';
    const attachments = [];
    // 脅威度中の場合
    if (event.detail.severity <= 6.9) {
      attachments.push({
        color: "warning",
        fields: [
          {
            "title": event.detail.title,
            "value": event.detail.description,
            "short": false
          }
        ]
      });
    // 脅威度高の場合
    } else if (event.detail.severity > 6.9) {
      if (process.env.SERVERLESS_ENV_STAGE != 'dev') slackChannel = 'emergency';
      severityText = '高';
      attachments.push({
        color: "danger",
        fields: [
          {
            "title": event.detail.title,
            "value": event.detail.description,
            "short": false
          }
        ]
      });

    }
    const sendOptions = {
      channel: slackChannel,
      icon_emoji: ':guardduty:', # 適当に良しななアイコンを設定してください
      text: `AWS Guard Durtyで重要度: 【${severityText}】以上セキュリティの驚異を検出しました。
      https://ap-northeast-1.console.aws.amazon.com/guardduty/home?region=ap-northeast-1#/findings?macros=current
      で驚異の確認をしてください
      `,
      attachments: attachments,
      username: 'AWS Guard Durty Watcher'
    };
    await slackClient.send(sendOptions);
  }

  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'guard-duty Notify Fire',input: event},null,2)
  };
};
```

## SlackClientを書く

```
mkdir src
touch src/slack/client.js
```

```js:src/slack/client.js
'use strict';

const { IncomingWebhook }    = require('@slack/client'),
        AWS                  = require('aws-sdk');

class SlackClient {
  constructor() {
    this._awsSecretManagerEndpoint = 'https://secretsmanager.ap-northeast-1.amazonaws.com';
    this._awsSecretManagerWebhookURLSecretName = 'xxxx/xxxxxxx'; # 設定したsecret managerのkeyを設定してください
  }
  async send(options) {
    const client = new AWS.SecretsManager({
      endpoint: this._awsSecretManagerEndpoint,
      region: 'ap-northeast-1'
    });
    try {
      let response = await client.getSecretValue({ SecretId: this._awsSecretManagerWebhookURLSecretName }).promise();
      const webhookURL = JSON.parse(response.SecretString).slackWebhookURL;
      const slackClient = new IncomingWebhook(webhookURL);
      response = await slackClient.send(options);
      console.log(`slack response: ${response.text}`);
    } catch (err) {
      console.error(err);
    }
  }
}
module.exports = SlackClient;
```

## SampleEventのjsonを追加する

実際のeventをエミュレーションするために必要です。
sample値はAWS Console CloudWatchの画面からサンプル値を取得できます

![Cloudwatch Event Rules](https://raw.githubusercontent.com/ryurock/qiita_images/master/guard_duty1.png)

```
mkdir events
touch events/guard-durty-sample.json
```

```json:guard-durty-sample.json
{
  "version": "0",
  "id": "c8c4daa7-a20c-2f03-0070-b7393dd542ad",
  "detail-type": "GuardDuty Finding",
  "source": "aws.guardduty",
  "account": "123456789012",
  "time": "1970-01-01T00:00:00Z",
  "region": "us-east-1",
  "resources": [],
  "detail": {
    "schemaVersion": "2.0",
    "accountId": "123456789012",
    "region": "us-east-1",
    "partition": "aws",
    "id": "16afba5c5c43e07c9e3e5e2e544e95df",
    "arn": "arn:aws:guardduty:us-east-1:123456789012:detector/123456789012/finding/16afba5c5c43e07c9e3e5e2e544e95df",
    "type": "Canary:EC2/Stateless.IntegTest",
    "resource": {
      "resourceType": "Instance",
      "instanceDetails": {
        "instanceId": "i-05746eb48123455e0",
        "instanceType": "t2.micro",
        "launchTime": 1492735675000,
        "productCodes": [],
        "networkInterfaces": [
          {
            "ipv6Addresses": [],
            "privateDnsName": "ip-0-0-0-0.us-east-1.compute.internal",
            "privateIpAddress": "0.0.0.0",
            "privateIpAddresses": [
              {
                "privateDnsName": "ip-0-0-0-0.us-east-1.compute.internal",
                "privateIpAddress": "0.0.0.0"
              }
            ],
            "subnetId": "subnet-d58b7123",
            "vpcId": "vpc-34865123",
            "securityGroups": [
              {
                "groupName": "launch-wizard-1",
                "groupId": "sg-9918a123"
              }
            ],
            "publicDnsName": "ec2-11-111-111-1.us-east-1.compute.amazonaws.com",
            "publicIp": "11.111.111.1"
          }
        ],
        "tags": [
          {
            "key": "Name",
            "value": "ssh-22-open"
          }
        ],
        "instanceState": "running",
        "availabilityZone": "us-east-1b",
        "imageId": "ami-4836a123",
        "imageDescription": "Amazon Linux AMI 2017.03.0.20170417 x86_64 HVM GP2"
      }
    },
    "service": {
      "serviceName": "guardduty",
      "detectorId": "3caf4e0aaa46ce4ccbcef949a8785353",
      "action": {
        "actionType": "NETWORK_CONNECTION",
        "networkConnectionAction": {
          "connectionDirection": "OUTBOUND",
          "remoteIpDetails": {
            "ipAddressV4": "0.0.0.0",
            "organization": {
              "asn": -1,
              "isp": "GeneratedFindingISP",
              "org": "GeneratedFindingORG"
            },
            "country": {
              "countryName": "United States"
            },
            "city": {
              "cityName": "GeneratedFindingCityName"
            },
            "geoLocation": {
              "lat": 0,
              "lon": 0
            }
          },
          "remotePortDetails": {
            "port": 22,
            "portName": "SSH"
          },
          "localPortDetails": {
            "port": 2000,
            "portName": "Unknown"
          },
          "protocol": "TCP",
          "blocked": false
        }
      },
      "resourceRole": "TARGET",
      "additionalInfo": {
        "unusualProtocol": "UDP",
        "threatListName": "GeneratedFindingCustomerListName",
        "unusual": 22
      },
      "eventFirstSeen": "2017-10-31T23:16:23Z",
      "eventLastSeen": "2017-10-31T23:16:23Z",
      "archived": false,
      "count": 1
    },
    "severity": 5,
    "createdAt": "2017-10-31T23:16:23.824Z",
    "updatedAt": "2017-10-31T23:16:23.824Z",
    "title": "Canary:EC2/Stateless.IntegTest",
    "description": "Canary:EC2/Stateless.IntegTest"
  }
}
```

## package.jsonにrun scriptを書く

```
npm run invoke:local:dev
```

でローカルでlambdaを実行できます

```json:package.json
{
  "name": "guraddurty-event-watcher",
  "version": "1.0.0",
  "description": "",
  "main": "handler.js",
  "scripts": {
    "invoke:local:dev": "serverless invoke local --stage dev --function handler --type event --path events/guard-durty-sample.json",
    "invoke:lambda:dev": "serverless invoke --stage dev  --function handler",
    "invoke:lambda:prod": "serverless invoke --stage prod  --function handler",
    "deploy:dev": "serverless deploy --stage dev",
    "deploy:prod": "serverless deploy --stage prod",
    "watch:log:dev": "serverless logs  --stage dev --function handler --tail --no-color --startTime",
    "watch:log:prod": "serverless logs  --stage prod --function handler --tail --no-color --startTime"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@slack/client": "^5.0.1",
    "aws-sdk": "^2.494.0"
  },
  "devDependencies": {
    "serverless": "^1.47.0"
  }
}
```

## ローカルで試す

```
npm run invoke:local:dev
```

下記のような通知が出てきたらOKです

![Slack通知1](https://raw.githubusercontent.com/ryurock/qiita_images/master/guard_duty2.png)

## 開発用Lambdaをデプロイ

内部的にCkoudFormationのCreate Stackを実行して必要なAWS Resourceをデプロイしてくれます(Serverlessが)

```
npm run deploy:dev
```

## Guard dutyのテスト

https://ap-northeast-1.console.aws.amazon.com/guardduty/home?region=ap-northeast-1#/settings

に「結果サンプルの生成」のボタンがあるのでポチッとおします。

5分程すると結果画面にサンプルの驚異検出をしてくれます

![結果サンプルの生成](https://raw.githubusercontent.com/ryurock/qiita_images/master/guard_duty3.png)

下記のような感じです

![結果サンプル結果](https://raw.githubusercontent.com/ryurock/qiita_images/master/guard_duty4.png)

Slack側

![slack](https://raw.githubusercontent.com/ryurock/qiita_images/master/guard_duty5.png)

## 本番用のLambdaをDeployする

本番用のデプロイします。

```
npm run deploy:prod
```

出来上がりです。(これで5分おきにLambdaがキックされて新しい驚異があった場合に通知してくれます)

## 補足

Lambdaのデバッグ用にcloudwatchのlogをtailするrun scriptがありますので動かなかったらlogを覗いてください

```
npm run watch:log:dev
```

## 最後に

Guard Duty Lambda連携でお手軽Slack通知ができます。
みなさんも是非試してみて下さい。