AWS
CircleCI
lambda
serverless

Ubuntuの脆弱性レベルがわかりにくいので一覧、通知を作ってみた


はじめに

Ubuntuパッケージの脆弱性情報はUbuntu security notice(USN)で公開されます。

https://usn.ubuntu.com/

緊急度の高い脆弱性から速やかに対応したいところですが、USNのページを見ても脆弱性レベル(Priority)の記載がありません。ReferencesのCVE情報をひとつひとつたどらなければなりません。

そこで、月毎に一覧表示する機能とCritical/HighをSlackに通知する機能を実装しました。


プロダクト


一覧表示Web

選択した月の脆弱性情報をレベル順に表示します。16.04 LTSと18.04 LTSのみ対象としました。

usn-web.png

https://oke-py.github.io/usn-web/


Slack通知

脆弱性情報の新規登録時にCriticalまたはHighのものをSlackに通知します。

(注)なかなか出ないのでキャプチャ用にMediumのものを通知しました。

usn-notification.png


GitHubリポジトリ


使用技術


  • usn-web


    • GitHub Pages

    • Bootstrap

    • jQuery



  • usn-api


    • Amazon API Gateway

    • AWS Lambda

    • Amazon DynamoDB

    • Golang



  • usn-batch


    • AWS Lambda

    • Amazon DynamoDB

    • Golang



  • usn-notification


    • Amazon DynamoDB stream

    • AWS Lambda

    • Slack

    • Node.js



  • デプロイ


    • Serverless Framework

    • CircleCI




意識、工夫した点


サーバーレス構成で管理コストを低減する

サーバーレス構成(API Gateway + Lambda + DynamoDB)にすることで、セキュリティパッチ適用などの運用コスト低減を目指しました。また、EC2などのインスタンスを常時稼働しておく必要もないため、料金も安く抑えられます(そんなにアクセスされるとも思わないし、0円でいけそう)。


手動デプロイをやめる

他の個人開発プロダクトでNode.jsのLambda関数をServerless Frameworkを利用して手動デプロイしていたところ、npm installを忘れてpackage.jsonとズレが発生するなど面倒なことがありました。そこで、手動デプロイはやめてCircleCIによる自動デプロイを導入しました。

また、開発(テスト)環境と本番環境を分けるため、プッシュされたブランチによってデプロイ先を振り分けるようにしました。


.circleci/config.yml

jobs:

build:
docker:
- image: circleci/node
steps:
- checkout
- ...
- deploy:
name: sls-deploy
command: |
if [ "${CIRCLE_BRANCH}" == "master" ]; then
sls deploy --stage prod --verbose
else
sls deploy --stage dev --verbose
fi


serverless.yml

provider:

name: aws
runtime: nodejs10.x
stage: ${opt:stage, "dev"}
...

sls deploy時に--stageオプションを指定し、serverless.ymlでその値を利用しています。


ライブラリを基準に言語選定して省力化する

DynamoDBを操作するapi、batchではPuerkitoBio/goqueryを利用するためGolangを選択しました。notificationでは他のプロダクトで利用実績のある@slack/webhookを利用するためNode.jsを選択しました。

一応上記の理由はありますが、最終的にはGolangとNode.jsの勉強をしたかったというのも言語選定に大きく影響しました。


フロントエンドは凝らない

昔からフロントエンドは疎く、今回もあまり凝らないことにしました。CSSはBootstrapにお任せ、JavaScriptもjQueryで済ませました。React/Vue.js/Nuxt.jsも気になりましたが、背伸びせず自分にできる範囲でまずは動くものを作ることを優先しました。


ページ遷移せず、#の変更に追従する

凝らないフロントエンドで唯一目標としたのが、ページ遷移せずにテーブルを書き換えることです。ハッシュフラグメントの変更を検知してAPIをたたいて描画しています。こんな書き方でよいのやら・・・


index.html

<script>

...
$(() => {
updateView();
});

$(window).on('hashchange', updateView);
</script>



苦労した点


DynamoDBのテーブル設計

初めてDynamoDBを利用したのでソートキーの使い方などがなかなかつかめず、どういう設計ならやりたいことが実現できるのか検討するのに時間がかかりました。さらに言えば、数週間ストップして開発やめそうでした。


API Gatewayのクエリ文字列をLambdaで使う方法がわからない

選択した月のデータを取得するため、API GatewayのGETパラメータにyyyy-mm形式の値を指定して、Lambdaで使おうとしました。しかし、これといってズバッと解決するドキュメントが見当たりませんでした。

Lambda(Golang)でAPI Gatewayのクエリ文字列を使用するにサンプルコードを記載しました。


Slackに通知する前に処理が終わってしまう

Promise/async/awaitを十分に理解できていない証拠です。await Promise.all()とすることで解決しました。


handler.js

'use strict';

module.exports.run = async (event) => {
const { IncomingWebhook } = require('@slack/webhook');
const message = require('./src/message');

await Promise.all(event.Records.map(async record => {
console.log('event type:', record.eventName);
console.log('DynamoDB Record: %j', record.dynamodb);

if (record.eventName === 'INSERT') {
const newItem = record.dynamodb.NewImage;

if (['Critical', 'High'].includes(newItem.severity.S)) {
const webhook = new IncomingWebhook(process.env.SLACK_WEBHOOK_URL);
const body = message.create(newItem);
console.log('to be sent: %j', body);

const res = await webhook.send(body);
console.log(res);
}
}
}));

return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};



改善したい点

やらない可能性が高いですが、覚えているうちに一応列挙しておきます。


  • フロントエンドのテストを書く

  • Reactでフロントエンドを書き直す

  • GitHub Pagesはmasterを直接いじっているので、開発環境と本番環境に分ける

  • フロントエンドをS3 + Cloudfrontに移行する

  • DynamoDBをInfrastructure as Code化する

  • DynamoDB streamのARNべた書きをなくす


まとめ

Ubuntuの脆弱性情報収集を楽にしたい、AWS使ってみたい、という地点からはじまり、なんとか動くものを作り上げることができました。サーバーレス構成オススメです。


参考