この記事は 富士通クラウドテクノロジーズ Advent Calendar 2017 の10日目です。

昨日は @sato-mh さんの 「[python] attrs ライブラリに自動バリデーション機能をデコレータで追加してみた」 でした。 attrs、便利そうなライブラリですね。自分も Python を書くことが結構多いので、今度使ってみようと思います!

サーバ脆弱性スキャンの定期実行・結果通知を自動化してみた

この世に存在しているサーバは常に悪意のある攻撃者からの攻撃対象になっています。完全に攻撃から守る方法はなかなか難しいかもしれませんが、少しでもリスクを低減させるために定期的な脆弱性のスキャンが必要になるかと思われます。

ニフクラのエンジニアリングパーツにはすでに脆弱性スキャンサービスが存在しており、ニフクラタイマーと組み合わせると定期的にスキャンを実行することを自動化することが可能です。しかし、そのスキャン結果の通知機能までは提供されていません。そこで、自分の好きな動作を仕込むことができるニフクラスクリプトとニフクラタイマーを組み合わせることで、定期実行したスキャン結果の取得・通知が可能になります。

ということで本記事では、二フクラのエンジニアリングパーツ 3 つを組み合わせて、「サーバの脆弱性スキャンを定期実行し、スキャン結果を Slack に自動通知してくれるもの」を作成してみたので紹介してみようと思います。

ちなみにサブテーマとして、二フクラスクリプトで外部ライブラリを追加する方法についても本記事内で述べます!

今回利用する二フクラのエンジニアリングパーツ

今回は、下記 3 つのエンジニアリングパーツを組み合わせて使用することで、脆弱性スキャンの定期実行、スキャン結果の通知を自動化してみようと思います。

  • 脆弱性スキャン

    • Nessus を使用して、サーバの脆弱性をスキャンしてくれるサービスです。
    • 2017 年 12 月現在、上述の通りスキャン完了時の結果通知機能が提供されていないため、本記事では他のパーツと組み合わせることによりスキャン結果の自動通知を実現します。(脆弱性スキャンサービスに標準で通知機能が標準で実装されたらとてもうれしいですね!)
  • タイマー

    • cron 感覚で定期的なジョブを実行することができるサービスです。
    • 今回は脆弱性スキャンとスクリプトを定期的にキックするために使用します。
  • スクリプト

    • Node.js で記述したスクリプトを API やコンパネ、タイマーから実行できるサービスです。
    • 今回は脆弱性スキャンの結果取得、 Slack への通知処理をスクリプトで実行します。

本題

それでは実際にパーツを組み合わせて設定していきます。

大まかな手順としては、

  1. 脆弱性スキャンでスキャンテンプレートを作成
  2. タイマーで定期的にスキャンを実行するように設定
  3. スキャン結果を取得、通知するスクリプトを作成
  4. タイマーでスキャンが完了したあとくらいにスクリプトを実行するように設定

になります。

スキャンテンプレートの作成、定期スキャンの設定

まずは

  • 脆弱性スキャン
  • タイマー

を利用して、脆弱性スキャンを定期実行するようにしてみましょう。ここでの設定はすべてコンパネ上から操作できます。

スキャンテンプレートの設定

ここでは例として、以下のようなスキャンテンプレートを設定してみます。設定の詳細は 脆弱性スキャン ユーザーガイド を御覧ください。

scan-template.png

スキャンテンプレートに対して鍵やパスフレーズ等のクレデンシャル情報の設定が必要になります。こちらも適宜設定してください。

タイマーの設定

では、上記で作成したスキャンテンプレートを定期的に実行するようにタイマーを設定してみましょう。

timer001-1.png

ここでは例として、先程作成したスキャンテンプレートを毎日 4 時に実行するように設定しています。このタイマーを作成することにより、脆弱性スキャンを定期的に実行することが可能になります。簡単!!!

スキャン結果を取得し、Slack に通知するスクリプトを作成

続いてニフクラスクリプトを利用して、スキャン結果を取得し、結果を Slack に投稿する Node.js のスクリプトを作成していきます。

スキャン結果は脆弱性スキャンサービスの API を呼び出すことで取得できます。脆弱性スキャンサービスは API の認証方式が Signature V4 のため、自前で実装するのは少し気が引けます。そこで Node.js の AWS SDK (aws-sdk-js) を利用し、 Signature V4 の計算は aws-sdk-js に任せることにします。

二フクラスクリプトで追加の外部ライブラリをインストールする

上述の通り、 aws-sdk-js をスクリプト内で使用したいのですが、二フクラスクリプトではスクリプト内で利用できるライブラリが限定されています。しかし、スクリプト内で exec を利用して npm コマンドを実行することによって外部ライブラリを追加で利用することが可能です。

exec_sample.js
const exec = require('child_process').exec;

module.exports = (req, res) => {
  const command = 'mkdir -p /tmp/work && npm install --prefix /tmp/work aws-sdk';
  exec(command, (err, stdout, stderr) => {
    if (err) res.send(JSON.stringify(err));
    const AWS = require("/tmp/work/node_modules/aws-sdk"); 
    res.send(AWS.VERSION);
  });
};

ここでの要点は、

  • 各スクリプトは /tmp に対してアクセス権限があるが、 /tmp/node_modules はデフォルトでインストールされているパッケージが存在しており、 /tmp/node_modules だけはアクセス権限がなくインストールできない。
  • そこで、新たに /tmp/work というディレクトリを作成し、ここに追加したいライブラリをインストールする。

    • ここにインストールしたものを利用したい際は下記のように require すれば OK です。
    const AWS = require("/tmp/work/node_modules/aws-sdk");
    

になります。

exec_sample.js を実際にスクリプトサービス上で実行してみると、

script-exec-result.png

のように SDK のバージョンが表示され、特にエラーが発生せず aws-sdk-js を require できていることがわかります。

注意

  • この方法はドキュメント等には記載されていない非公式な方法です。
  • スクリプト内で npm install するため、スクリプト実行時間が大幅に増えてしまいます。
  • 仕様変更によりこの方法が使用できなくなる可能性もあるのでご了承ください。

スクリプト本体

脆弱性スキャンサービスの API リファレンスと Slack のリファレンスを読みながら

  • aws-sdk-js をインストール
  • 脆弱性スキャンサービスの API を呼び出し、スキャン結果を取得
    • 現在時刻から 1 日前までのスキャン履歴一覧を DescribeScanHistories で取得
    • 最新のスキャンの HistroyId を DescribeScanResults に指定することでスキャン結果を取得
  • Slack の incoming webhook を利用し、結果を Slack に通知

という流れのスクリプトを書いてみました。

fetch_and_notify_scan_result.js
const { exec } = require('child_process');
const request = require('superagent');
const moment  = require('moment');

class VssClient {
  constructor(accessKeyId, secretAccessKey) {
    this.host     = 'vss.api.cloud.nifty.com';
    this.endpoint = `https://${this.host}/`;
    this.version  = '2017-02-23';
    this.credentials = {
      accessKeyId,
      secretAccessKey
    };
  }

  doAction(action, body) {
    const AWS = require('/tmp/work/node_modules/aws-sdk');

    const awsreq = new AWS.HttpRequest(this.endpoint, 'jp-east-1');
    awsreq.headers = {
      host:         this.host,
      'content-type': 'application/json',
      'x-amz-target': `${this.version}.${action}`
    };
    awsreq.body = JSON.stringify(body);

    const signer = new AWS.Signers.V4(awsreq, 'vss');
    signer.addAuthorization(this.credentials, new Date());

    return new Promise((resolve, reject) => {
      request.post(this.endpoint)
        .set(awsreq.headers)
        .send(awsreq.body)
        .end((err, response) => {
          if (err) reject(response);
          resolve(response);
        });
    });
  }

  describeScanHistories(templateName, start, end) {
    const body = {
      ScanTemplateName: templateName,
      StartTime:        start,
      EndTime:          end
    };
    return this.doAction('DescribeScanHistories', body);
  }

  describeScanResults(historyId) {
    const body = {
      ScanHistoryUUID: historyId
    };
    return this.doAction('DescribeScanResults', body);
  }
}

class SlackClient {
  constructor(url) {
    this.url = url;
  }

  notify(body) {
    return new Promise((resolve, reject) => {
      request.post(this.url)
        .send(body)
        .end((err, response) => {
          if (err) reject(response);
          resolve(response);
        });
    });
  }
}

module.exports = (req, res) => {
  const command = 'mkdir -p /tmp/work && npm install --prefix /tmp/work aws-sdk';
  exec(command, (err, stdout, stderr) => {
    if (err) res.send(`${JSON.stringify(err)} ${stdout} ${stderr}`);

    const vss = new VssClient(
      "accessKeyId",
      "secretAccessKey"
    );
    const slack = new SlackClient("https://yourslackincomingwebhookurl.com");

    const start = moment().utc().subtract(1, 'days').format();
    const end = moment().utc().format();
    vss.describeScanHistories('sample', start, end).then((result) => {
      if (result.length === 0) res.send("no scan histories");
      const target = result.body.ScanHistories[0];
      return vss.describeScanResults(target.ScanHistoryUUID);
    }).then((result) => {
      const fields = result.body.ScanResults.map((r) => ({
        title: r.Rule.RuleName,
        value: `Synopsis: ${r.Rule.Synopsis}\nSeverity: ${r.Severity}   Count: ${r.Count}`,
        short: false
      }))
      const attachments = [
        {
          color:  "#36a64f",
          title:  "脆弱性スキャン実行結果通知",
          footer: "vss notification bot",
          fields
        }
      ];
      const body = {
        icon_emoji: ":innocent",
        username:   "vss-bot",
        channel:    "#notify_channel",
        attachments
      };
      return slack.notify(body);
    }).then((result) => {
      res.send(JSON.stringify(result.body));
    }).catch((error) => {
      res.send(JSON.stringify(error));
    });
  });
};

Node.js は初心者なので書き方的におかしいところがあるかもしれないです… :innocent:

スクリプトを定期実行するようにタイマーを設定

下記画面のように、作成したスクリプトをスキャンが完了していそうな時刻に定期実行するように設定しておきます。ここでは毎日 6 時にスキャン結果を取得し、 Slack に通知するようにしてみます。

image.png

これですべての設定は完了です…!

実行結果

image.png

こんな感じでタイマーに設定した時刻に通知をしてくれました! (幾つか結果が出ちゃってますが、設定ミスかな… Severity が 0 だから一旦は大丈夫そう)

まとめ

本記事では、サーバの脆弱性スキャンを定期実行しそのスキャン結果を自動で Slack に投稿してくれるものを、二フクラのエンジニアリングパーツを組み合わせることで実現してみました。

各サービス単体だとできることが限られてしまいがちなので、このように複数のサービスを組み合わせていくと実現できることが増えて面白いですね…!

さて、明日は @yaaamaaaguuu がここ 1 年での知見を書いてくれるみたいです。はたして彼はここ 1 年でどんな知見を獲得することができたのでしょうか…!?楽しみですね!

では!

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.