インフラ

Dependabot alertsのREST APIで脆弱性検知をSlack通知してみた

kurochan

こんにちは!クロちゃんです!!

今回は、Dependabot alertsのREST APIを使用して、脆弱性検知をSlackへ通知する仕組みを実装したので、解説していきたいと思います。

Dependabotとは

簡単に説明しますと、リポジトリ内の依存関係をチェックして、
脆弱性の検知、通知、対策方法の提案や、更新候補をプルリクエストとして発行してくれる
GithubのBotになります。
詳細はこちらの公式ページをご覧頂ければと思います。

実装背景

当初、保守運用の都合で「Dependabotのプルリクエストをトリガーにして、
Github Actionsを使って、その内容をSlackへ通知させて確認しよう」と考えていました。
ただ、先程説明した通りDependabotは、更新候補がある場合にプルリクエストを作ってくれます。
つまり、脆弱性情報が公開され、それがDependabotに表示されていても、
更新候補がまだ無い場合はプルリクエストは作成されません。

ですが、そういった脆弱性情報も確認したい!となりました。
そこでDependabot alertsのREST APIを使用して、Slack通知を実現することにしました。

Dependabot alertsは「脆弱性が検知された時にアラートの通知を行う」という機能です。
ちなみにメールであれば、この機能を利用して通知を受け取ることが出来ます。
このAPIを使用することで、プルリクエストが作られていない脆弱性情報を含めて
更新情報を取得することができます。

Lambdaのサンプルコード(Python)

こちらが、Slack通知を実現するためのLambda(Python)のサンプルコードになります。
ブログ掲載用に編集していますので、サンプルコードを参考にされる際は、
リージョンや更新対象にする時間範囲等をご自身の都合に合わせて適宜修正頂ければと思います。
処理の概要は、次項で解説します。

import urllib3
import json
import requests
from datetime import datetime,timedelta
import boto3

#引数:リポジトリのowner/repo(文字列)、Githubのアクセストークン(文字列)
#返り値:リポジトリのDependabot alertsの更新情報を通知するためのメッセージ(文字列)
def get_dependabot_alerts(owner_repo,token):

    #実行時のタイムスタンプを取得
    timestamp = datetime.now()

    #前日の日付を文字列で取得
    yesterday = str((timestamp - timedelta(1)).date())

    #Dependabot alerts APIをコールして、対象リポジトリのDependabot alertsの一覧を受け取る
    headers = {"Accept":"application/vnd.github+json", "Authorization":"Bearer "+ token}
    res = requests.get("https://api.github.com/repos/"+owner_repo+"/dependabot/alerts", headers=headers)
    dependabot_alerts = res.json()

    #Dependabot alertsの更新があれば、そのページのURLを格納するための配列
    url_list = []

    #Dependabot alertsを1件ずつ見ていく
    for alert in dependabot_alerts:
        # 前日のAlertsがあれば、更新情報としてURLを配列に格納する
        if yesterday in alert["updated_at"]:
            url_list.append(alert["html_url"])

    #更新が1件でもあれば、Slackへ通知する文字列を生成して返す
    if len(url_list) != 0:
        url_message = ":github: "+ owner_repo +"にて、Dependabot Alertsの更新がありました。\n 以下のURLを確認してください。\n\n【Dependabot Alerts】\n" 
        for url_str in url_list:
            url_message = url_message + url_str + "\n"
        return url_message

    #更新がなければ、空文字を返す
    else:
        return ""

#引数:パラメータストアに登録したパラメータの名前(文字列)
#返り値:対象パラメータのValue(文字列 or リスト)
def get_parameters(param_key):
    # region_nameは各自の環境に応じて適宜修正
    ssm = boto3.client("ssm", region_name="ap-northeast-1")
    response = ssm.get_parameters(
        Names=[
            param_key,
        ],
        WithDecryption=True
    )
    return response['Parameters'][0]['Value']

def lambda_handler(event, context):

    #パラメータストアから、通知先Slackチャンネル、Githubのアクセストークン、対象リポジトリを取得
    parameters_store_list = get_parameters("test").split(",")

    #parameters_store_list[0]:通知先Slackチャンネル
    #parameters_store_list[1]:Slack通知用のWebhookURL
    #parameters_store_list[2]:Githubのアクセストークン
    #対象リポジトリはparameters_store_list[3]から入っている
    for parameters_store_value in parameters_store_list[3:]: 

        #Dependabot alertsの更新メッセージを取得
        message = get_dependabot_alerts(parameters_store_value,parameters_store_list[2])

        #Slackへ通知する更新メッセージがあれば、対象のチャンネルに投稿する
        if message != "":
            http = urllib3.PoolManager()
            slack_url = parameters_store_list[1]
            msg = {
                "channel": parameters_store_list[0],
                "username": "",
                "text": message,
                "icon_emoji": ""
            }
            encoded_msg = json.dumps(msg).encode('utf-8')
            resp = http.request('POST', slack_url, body=encoded_msg)

    return {
        'statusCode': 200,
        'body': json.dumps('fin')
    }

解説

ここからは、サンプルコードの内容を解説していきたいと思います!

動作説明

今回実装した処理の流れですが、簡単に図にすると以下ような感じです。
サンプルコードでは、以下の処理を定期実行して「昨日のDependabot alertsの更新情報」を朝に通知し、
出勤時から対応するといった業務フローを想定しています。

Slackへの通知は以下の通りで、昨日に更新があれば更新情報のURLも込みで通知してくれます。

Parameter Storeに設定する値

Parameter Storeに登録している情報は以下の4点です。

  1. 通知先のSlackチャンネル名
    • 例 : #general
  2. Slackへ通知する権限を持ったWebhookURL
  3. Githubのアクセストークン
    • APIの仕様が掲載されているページに、必要な権限も掲載されています
    • サンプルコードでは「List Dependabot alerts for a repository」を使用しているため、「security_events」と「public_repo」の権限が必要です
  4. 通知対象にするリポジトリのowner/repo
    • 例 : test/hogehoge

なお、Parameter Storeに格納されている情報は、以上の順に「,」(カンマ)区切りされた文字列を想定しています。
要するに、対象リポジトリが3つの場合は、以下のような文字列がParameter Storeに格納されている想定です。

#general,WebhookURL,token_hogehoge,test/hogehoge1,test/hogehoge2,test/hogehoge3

「通知先のSlackチャンネル名」と「Slackへ通知する権限を持ったWebhookURL」は③に、
「Githubのアクセストークン」と「対象リポジトリのowner/repo」は②で使用します。

検討事項

今回は、コストの観点からParameter Storeを選択していますが、
プロジェクトの要件によっては、Secrets Managerを検討しても良いと思います。

また、脆弱性情報に対応できたかどうかも漏れなく確認したかったため、
サンプルコードでは「昨日にステータスがFixedとなったもの」も更新情報として取得しています。
ステータスもREST APIから取得できるので、要件や優先順位によっては、
「ステータスがFixedのものは通知から除外する」としても良さそうです。

最後に

先日Github Actionsのブログを書いたときにも思いましたが、
本当に便利な機能が盛り沢山なサービスで、使っていて楽しいなと改めて感じました。
これからも色々な技術をしっかり楽しみながら使って身に付けていきたいと思います。
最後まで読んで頂き、ありがとうございました!!

AUTHOR
kurochan
kurochan
記事URLをコピーしました