AWS Inspector で脆弱性診断。Jenkins で自動実行し、結果を Slack に通知する。

# 概要
新規サービスの開発に較べて、どうしても後回しになりがちな脆弱性のテストについて、ついに、ちゃんとやらねばならぬ時が来てしまいました。

AWS Inspector を使えば、手軽に、かつ、網羅的に診断を行うことが可能になっています。
[AWS Inspector ユーザガイド](https://docs.aws.amazon.com/ja_jp/inspector/latest/userguide/)

毎回、手動で実行していては忘れてしまうことがあるため、以下のように、Jenkins で定期的に自動実行し、かつ、結果を Slack に通知してくれる仕組みを構築しました。

![スクリーンショット 2016-09-18 14.27.54.png](https://qiita-image-store.s3.amazonaws.com/0/69410/72fcdf5d-74f8-9dc4-a424-b1ecc4af6009.png)

構築手順について、忘れないうちに Qiita化 しておきます。



# 設定
## 準備

まず、EC2に Agent をインストールして実行しておく必要があります。

[サポート対象OS](https://docs.aws.amazon.com/ja_jp/inspector/latest/userguide/inspector_working-with-agents.html#inspector-agent-os)

```
Amazon Linux (2015.03 以降)
Ubuntu (14.04 LTS)
Red Hat Enterprise Linux (7.2)
CentOS (7.2)
Windows Server 2008 R2 および Windows Server 2012
```
[Agent インストール手順](https://docs.aws.amazon.com/ja_jp/inspector/latest/userguide/inspector_working-with-agents.html)

```
# wget https://d1wk0tztpsntt1.cloudfront.net/linux/latest/install
# bash install
# /etc/init.d/awsagent start
```


## Inspector
AWS Inspector では、以下の4種類のルールパッケージから【複数】選択することができます。

|名前|内容|
|----|----|
|一般的な脆弱性と曝露|評価ターゲット内の EC2 インスタンス が一般的な脆弱性や曝露 (CVE) に曝露されているかどうかを確認<br>例えば OpenSSLの脆弱性「ハートブリード」 CVE-2014-0160 が含まれます|
|CIS オペレーティングシステムのセキュリティ設定ベンチマーク|組織がセキュリティを評価して強化できるよう明確に定義された、公平でコンセンサスベースの業界のベストプラクティスを提供|
|セキュリティのベストプラクティス|システムが安全に設定されているかどうかの判断|
|実行時の動作の分析|インスタンスの動作を分析し、EC2 インスタンスのセキュリティを高めるためのガイダンスを提供|

ルールパッケージ、対象となるEC2のタグ、評価時間 を選択して、一つの「評価テンプレート」を作成します。
イベントをSNSに発火するように設定しておきます。

注意

 - 評価テンプレートは、作成後は編集はできません。  
 (SNSトピックだけは、後から追加変更ができるようです)
 - 対象となるEC2インスタンスは、タグによって、評価テンプレート作成後でも増減できます。
 - 評価中のEC2インスタンスは、同時に他の評価をすることはできません。


![スクリーンショット 2016-09-18 14.51.37.png](https://qiita-image-store.s3.amazonaws.com/0/69410/f82c3262-f99a-f42d-459b-ea7eafd7fe69.png)


## SNS

以下のイベントがSNSに向けて発火されてきます。

|No|event|newstate|
|---:|-------------|----------|
|1|ASSESSMENT_RUN_STATE_CHANGED|CREATED|
|2|ASSESSMENT_RUN_STATE_CHANGED|START_DATA_COLLECTION_IN_PROGRESS|
|3|ASSESSMENT_RUN_STATE_CHANGED|START_DATA_COLLECTION_PENDING|
|4|ASSESSMENT_RUN_STATE_CHANGED|COLLECTING_DATA|
|5|ASSESSMENT_RUN_STARTED| |
|6|ASSESSMENT_RUN_STATE_CHANGED|STOP_DATA_COLLECTION_PENDING|
|7|ASSESSMENT_RUN_STATE_CHANGED|DATA_COLLECTED|
|8|ASSESSMENT_RUN_STATE_CHANGED|EVALUATING_RULES|
|9|FINDING_REPORTED| |finding|
|10|ASSESSMENT_RUN_COMPLETED| |
|11|ASSESSMENT_RUN_STATE_CHANGED|COMPLETED|

以下に対応しているようです。

|イベント|event|
|----|------|
|実行開始|ASSESSMENT_RUN_STARTED|
|実行完了|ASSESSMENT_RUN_COMPLETED|
|実行ステータスが変更されました|ASSESSMENT_RUN_STATE_CHANGED|
|報告された結果|FINDING_REPORTED|

Slack通知で ASSESSMENT_RUN_STATE_CHANGED は必要ないですね。開始/完了/結果 を通知するようにします。



## Lambda
Slackに通知する、以下の Lambda 関数を作成しました。
Node.js 4.3 を選択しています。
AWS API で Promise が使えますね。
外部 npm モジュールを使わないので、インラインで Lambda を構築できます。

```javascript
// https://nodejs.org/docs/v4.3.2/api/

// Set your slack url here.
const slack = "/services/T0DD9AQER/B0DDBKWSX/xxxxxxxxxxxxxxxxxxxxxxxx";

const AWS = require("aws-sdk");

AWS.config.apiVersions = {
  inspector: "2016-02-16"
};

const inspector = new AWS.Inspector();

const https = require('https');


//
// Slack message format.
//
function build(template, target, finding, message) {

  var events = {
    "ASSESSMENT_RUN_STARTED": "実行開始",
    "ASSESSMENT_RUN_COMPLETED": "実行完了",
    "ASSESSMENT_RUN_STATE_CHANGED": "実行ステータス変更",
    "FINDING_REPORTED": "結果報告"
  };

  var buff = [
    [
      target.name,
      template.name,
      events[message.event]
    ].join(":")
  ];

  switch (message.event) {
    case "FINDING_REPORTED":
      buff.push("```");
      buff.push("[" + finding.severity + "] " + finding.id);
      buff.push(finding.title);
      buff.push("```");
      break;
    case "ASSESSMENT_RUN_STARTED":
    case "ASSESSMENT_RUN_COMPLETED":
      break;
    case "ASSESSMENT_RUN_STATE_CHANGED":
      buff.push(message.newstate);
      break;
    default:
      buff = null; // 通知しない
      break;
  }

  return buff && buff.join("\n");
}

//
// Lambda entry point
//
exports.handler = function (event, context, callback) {
  console.log("inspector lambda start : " + new Date());

  var messages = event.Records.reduce(function (memo, v) {
    if (v.Sns && v.Sns.Message) {
      memo.push(JSON.parse(v.Sns.Message));
    }
    return memo;
  }, []);

  Promise.all([
    inspector.describeAssessmentTemplates({
      assessmentTemplateArns: pluck(messages, "template")
    }).promise(),
    inspector.describeAssessmentTargets({
      assessmentTargetArns: pluck(messages, "target")
    }).promise(),
    inspector.describeFindings({
      findingArns: pluck(messages, "finding", "")
    }).promise()
  ]).then(function (result) {
    var templates = result[0].assessmentTemplates;
    var targets = result[1].assessmentTargets;
    var findings = result[2].findings;
    return Promise.all(messages.map(function (message, idx) {
      var text = build(templates[idx], targets[idx], findings[idx], message);
      console.log(text);
      return text && request({text: text});
    }));
  }).then(function () {
    callback();
  }).catch(function (err) {
    console.log(err);
    callback(err);
  });
};

//
// Slack
//
function request(data) {
  return new Promise(function (resolve, reject) {
    var body = JSON.stringify(data);

    var req = https.request({
      hostname: "hooks.slack.com",
      port: 443,
      path: slack,
      method: "POST",
      headers: {
        'Content-Type': 'application/json; charser=UTF-8',
        "Content-Length": Buffer.byteLength(body)
      }
    }, function (res) {
      res.statusCode === 200 ? resolve(res) : reject(res);
    });
    req.end(body);
  });
}

// utility

function pluck(array, propertyName, _default) {
  return array.map(function (v) {
    return v[propertyName] !== undefined ? v[propertyName] : _default;
  });
}
```

SNS でトリガーされるように設定します。

![スクリーンショット_2016-09-18_15_49_13.png](https://qiita-image-store.s3.amazonaws.com/0/69410/c9624244-722d-aae6-e9b4-7ade60fa58f5.png)


## Slack
このような感じで、 Slack に通知されます。
![スクリーンショット_2016-09-20_14_41_16.png](https://qiita-image-store.s3.amazonaws.com/0/69410/c40da109-7260-4521-4652-8f0742b64ec5.png)


何か問題があれば「報告された結果」として通知されます。
詳細(対処方法)は Inspector の「結果」画面で確認してください。

上記の例では、次のような指摘をされています。

 - node.js が古い
 - ポート 80番 で HTTP が動作している
 - root で ssh ログイン可能な設定になっている

たしかに。ごもっとも。

 
## Jenkins

手持ちの Jenkins で定期的に実行するようにしておくと、安心ですね。

Jenkins では 「シェルの実行」を選択し、
Inspector 操作権限のあるアカウントのIDとパスワードを環境変数に設定してから、「評価テンプレート」を指定して inspector start-assessment-run を実行します。

シェルの例

```
# IAM : InspectorJenkins
export AWS_ACCESS_KEY_ID=AKIAIXXXXXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
export AWS_DEFAULT_REGION=ap-northeast-1

# テンプレート
TEMPLATE="arn:aws:inspector:ap-northeast-1:663889673734:target/0-FcroYIWk/template/0-0h8lMLYY"

/opt/pyenv/shims/aws inspector start-assessment-run --assessment-template-arn ${TEMPLATE}
```

# おわりに
何台かのEC2インスタンスで本番サービスを運用していますが、いままで後手後手になっていた脆弱性診断が自動化されて、ほっと一安心しています。
さらに、ペネトレーション的な診断をしてくれるサービスが出てくることを、強く期待しています！！