😀

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

2022/11/28に公開約11,700字

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

専任のセキュリティ管理者がいればいいですが兼任してたりで中々継続的に調べる事も難しい事が多々あると思います。
そこでそういうものはAWS Guard Dutyにお任せしましょう。

AWS Guard Dutyとは?

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

【レポート】自動セキュリティ分析サービスGuardDutyのよくわかる解説 #reinvent # SID218

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

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

です。

料金について

普段の業務では一般的な

  • ELB
  • EC2
  • RDS
  • S3

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

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

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

Serverless Framework内部はCloudFormationで管理してくれるのでサービス停止忘れとかがないし、設定わかりやすいしドキュメントが丁寧なのでLambda使うならマジおすすめします。

環境

  • MacOSX
  • Node8.10
  • Serverless 1.47.0

Gurad Dutyを有効化にする

【re:invent2017】新サービス Amazon 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:
        - "*"

は不要です。

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行位なので素の感じで

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
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

mkdir events
touch events/guard-durty-sample.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を実行できます

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

開発用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分程すると結果画面にサンプルの驚異検出をしてくれます

結果サンプルの生成

下記のような感じです

結果サンプル結果

Slack側

slack

本番用のLambdaをDeployする

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

npm run deploy:prod

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

補足

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

npm run watch:log:dev

最後に

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

Discussion

ログインするとコメントできます