こんにちは、PSIRTのWaTTsonです。
去年の12月にAdvent CalendartでAWS SecurityHubの結果をSIEM on Amazon OpenSearch Serviceに取り込んだ話を書きました:
今回は、同じくSIEM on OpenSearchを使った話で、GitHubのDependabotの運用に関することを書きたいと思います。
Dependabotの運用上の課題点
Dependabotはプロジェクトで使われているライブラリの依存関係をチェックし、古いものやセキュリティ上の問題があるものをアラートするサービスです。元々は独立したサービスでしたが、2019年にGitHubに買収されて、今はGitHubの公式機能として提供されています。
freeeでは、依存ライブラリの脆弱性管理に長らくyamoryを使っていましたが、2023年1月から、GtiHub Advanced Securityを大々的に使うように方針転換して、同時にライブラリの脆弱性管理もDependabotを本格運用する形に変えました。
DependabotはGitHubの公式機能となったため、GitHubの機能との連携に優れていて、自動でアップデートのPRを作成したりする仕組みが使えるようになっています。ですが、yamoryのようなライブラリの脆弱性管理に特化したサービスと比べると、機能に劣る部分も出てきます。
yamoryでは、脆弱性への対応フローに関する機能が充実しています。脆弱性が検知されたら、それを一旦セキュリティチームで確認して、トリアージした結果を開発チームに伝える、というフローをとることができるため、効率的に脆弱性への対応を行うことが可能です。
Dependabotを使う場合、デフォルトの機能だけでこうした高度な脆弱性対応フローに対応することはできません。Dependabot運用当初は、以下のような形で脆弱性対応のフローを全て手作業で行っていました:
- 定期的にGitHubからデータを取得してスプレッドシートに整理する
- PSIRTでトリアージを行う
- 開発チームのチャンネルに、対応が必要なものを連絡する
こうした作業はある程度定型化したものになるので、手動でやるのは時間の無駄が大きいです。そこで、Dependabotを本格運用するにあたって、いい感じの脆弱性対応フローを実現するための仕組みを自前で実装しました。
実装の方針
freeeのPSIRTでは、ログをSIEM on Amazon OpenSearch Serviceに集約する運用を取っています。また、このSIEM上からSlackにアラートを飛ばして、そこからJiraチケットを自動で起票する仕組みが運用されています(参考:脅威 Intelligence と log 運用 - freee Developers Hub )。
Dependabot alertの対処についても、この仕組みに乗っかる形で、以下のような運用方針を考えました:
- dailyでDependabot alertをSIEM on Amazon OpenSearch Serviceに取り込む
- 取り込んだ内容から、新規のものをSlackにアラートする
- Slack通知からボタンクリックでJiraチケットを起票する
- 起票されたJiraチケットをベースに、定期的にPSIRTでトリアージを行う
- トリアージの結果、対処が必要と判断されたものは、Slackで各開発チームのチャンネルにメッセージを飛ばす
これらの仕組みを、既存のSlack botの運用に追加して実装していきます。
SIEM on Amazon OpenSearch Serviceへの取り込み
まずはDependabot alertをSIEMに取り込みます。SIEM on Amazon OpenSearch Serviceでは、S3 bucketにJSON形式などで保存したログを取り込むLambda関数(es-loader)が動いていて、適当な設定をしておけば自動で取り込んでくれます。なので、「GitHubからDependabot alertの情報を取得してS3bucketに保存する」という機能を持ったLambda関数を作成して、dailyで動かすようにします。
Dependabot alertの情報はGitHub APIを使って取得します。GitHubのGraphQL APIで、vulnerabilityAlertsを取ってくるようにクエリを書いていきます。今回取り込む対象とする内容はorganization内の全てのリポジトリではなく、プロダクトのコードが含まれている一部だけにしたいので、対象のリポジトリの一覧を持っておいて、それぞれ対応するvulnerabilityAlertsを取得するクエリを作って、ループで回すようにします。
{ repository(name: "REPOSITORY", owner: "ORG_NAME") { url defaultBranchRef { name } vulnerabilityAlerts(first: 100) { nodes { createdAt dependabotUpdate { error { body errorType title } pullRequest { id title url } repository { id name url } } dependencyScope dismissComment dismissReason dismissedAt dismisser { id name } number securityAdvisory { classification cvss { score vectorString } cwes(first: 100) { nodes { cweId description id name } pageInfo { hasNextPage startCursor endCursor } } description ghsaId id identifiers { type value } notificationsPermalink origin permalink publishedAt references { url } severity summary updatedAt withdrawnAt } securityVulnerability { firstPatchedVersion { identifier } package { ecosystem name } severity updatedAt vulnerableVersionRange } state vulnerableManifestFilename vulnerableManifestPath vulnerableRequirements } pageInfo { hasNextPage startCursor endCursor } } } }
なお、ここでvulnerabilityAlertsは一気に全てを取得できないので、ページネーション周りを扱う必要があります。pageInfoを取得しておいて、first: 100の箇所にafter: {endCursor}の条件を追加して繰り返し取得する、といった形で、いい感じにループして何回かのクエリで全件を取得する形にします。
取得したものをアラート単位でまとめて、JSONファイルとしてS3 bucketに保存します。このとき、リポジトリごとに通知先のSlackチャンネルIDの情報も追加しておきます。これはSlack通知するところで追加することもできますが、リポジトリ一覧を二重管理するのを避けたかったので、この最初のLambda関数の時点で全ての情報を取ってくるようにしました。yamlファイルにリポジトリ名とSlackチャンネル名を持たせておきます。対象のリポジトリを変更したり通知先を変更したりする際には、ここだけを修正すれば良いようにします:
repositories: - name: リポジトリ1 notify_channel: Slackチャンネル1 notify_channel_id: SlackチャンネルID1 - name: リポジトリ2 notify_channel: Slackチャンネル2 notify_channel_id: SlackチャンネルID2
これを追加することで、最終的に以下のようなJSONがS3 bucketに保存されます:
{ "alerts": [ { "createdAt": "2023-04-01T00:00:00Z", "dependabotUpdate": { "error": { "body": "The following git repository was unreachable and caused the update to fail: ...", "errorType": "git_dependencies_not_reachable", "title": "Dependabot failed to update your dependencies" }, "pullRequest": null, "repository": { "id": "{REPOSITORY_ID}", "name": "{REPOSITORY_NAME}", "url": "{REPOSITORY_URL}" } }, "dependencyScope": "RUNTIME", "dismissComment": null, "dismissReason": null, "dismissedAt": null, "dismisser": null, "number": 1, "securityAdvisory": { "classification": "GENERAL", "cvss": { "score": 0.0, "vectorString": null }, "cwes": { "nodes": [], "pageInfo": { "hasNextPage": false, "startCursor": null, "endCursor": null } }, "description": "Package XXX prior to 2.0.0 is vulnerable.", "ghsaId": "{GHSA_ID}", "id": "{GSA_ID}", "identifiers": [{ "type": "GHSA", "value": "{GHSA_ID}" }], "notificationsPermalink": "https://github.com/advisories/{GHSA_ID}/dependabot", "origin": "UNSPECIFIED", "permalink": "https://github.com/advisories/{GHSA_ID}", "publishedAt": "2023-04-01T00:00:00Z", "references": [ {"url": "{REFERENCE_URL}"} ], "severity": "MODERATE", "summary": "Vulnerability in XXX", "updatedAt": "2023-04-01T00:00:00Z", "withdrawnAt": null }, "securityVulnerability": { "firstPatchedVersion": { "identifier": "2.0.0" }, "package": { "ecosystem": "RUBYGEMS", "name": "XXX" }, "severity": "MODERATE", "updatedAt": "2023-04-01T00:00:00Z", "vulnerableVersionRange": "< 2.0.0" }, "state": "OPEN", "vulnerableManifestFilename": "Gemfile.lock", "vulnerableManifestPath": "Gemfile.lock", "vulnerableRequirements": "= 1.0.0", "timestamp": "2023-04-01T00:00:00.000000+09:00", "repository": { "name": "{REPOSITORY_NAME}", "notify_channel": "{NOTIFY_CHANNEL}", "notify_channel_id": "{NOTIFY_CHANNEL_ID}", "url": "{REPOSITORY_URL}" }, "id": "{REPOSITORY_NAME}/1" } ] }
この中のalertsの配列をSIEMに読み込む形で、es-loaderの設定を書いておきます。
[dependabot] index_name = log-github-dependabot index_rotation = monthly s3_key = Dependabot file_format = json json_delimiter = alerts timestamp_key = timestamp timestamp_format = iso8601
このように書いておけば、指定のS3 bucket内で"Dependabot"のprefixを持つオブジェクトからJSONが解釈されて、"alerts"の配列に入っている要素がOpenSearchのレコードとして読み込まれ、log-github-dependabotのインデックスに入れられます。
Slack botの実装
SIEMからSlackへの通知は、脅威 Intelligence と log 運用 - freee Developers Hubでも紹介されているSlack botを拡張する形で実装します。
このSlack botは、Bolt for Pythonを用いて実装されていて、AWSのECS上でソケットモードで動作させています。Boltでは以下の3つの処理を実装します:
- dailyでSIEM上のDependabot alertを確認して、新規追加分をSlack通知する処理
- Slack通知投稿中のボタンをクリックした時に、Jiraチケットを作成する処理
- Slack通知投稿中のボタンをクリックした時に、入力欄に入れたメッセージを指定のSlackチャンネルに投稿する処理
1. Dependabot alertをSlack通知する処理
dependabot alertはdailyでSIEMに読み込む形にしていますが、これはEventBridgeでLambdaを定時実行して、その時のGitHub上のDependabotのデータのスナップショットをとる形になっています。そこで、最新のSIEM取り込み分のデータの中から、publishedAtの日時が過去1日以内のものをフィルターして取得したものを、Slackに通知する形にします。また、ここではseverityがCRITICALもしくはHIGHのものをフィルターします。
SIEMからデータを取得するDSLクエリの内容は例えば以下のような形になります:
{ "size": 100, "query": { "bool": { "filter": [ { "terms": { "securityAdvisory.severity": [ "CRITICAL", "HIGH" ] } }, { "range": { "securityAdvisory.publishedAt": { "gte": "2023-03-31T15:00:00.000Z", "lte": "2023-04-01T15:00:00.000Z" } } }, { "range": { "@timestamp": { "gte": "2023-03-31T15:00:00.000Z", "lte": "2023-04-01T15:00:00.000Z", "format": "strict_date_optional_time" } } } ] } } }
このようにして取得したDependabot alertのデータを元にループを回して、それぞれ通知投稿用のSlack Blockを生成し、メッセージ投稿を行います。
メッセージの投稿は、PSIRTの管理するSlackチャンネルと、開発者の各チームが見るチャンネルのそれぞれに対して行います。ここで、PSIRT管理チャンネルへ投稿するメッセージには、Create Jira Issueボタンを付けておきます。また、開発者の各チャンネルにトリアージ内容を通知するための入力欄とボタンを配置した投稿を、スレッドで投稿するようにしておきます。
2. Jiraチケット作成
これは、既存のWAFなどのアラート通知で実装されているものを流用します。Create Jira Issueのボタンが押されたときにBolt for Pythonの@app.action()で引っかけて処理を実行します。ここではメッセージの内容からButtonブロックを省いたものを、Jiraチケットのdescriptionに入れる形で、Jira APIからチケットを作成します。
3. トリアージメッセージの送信
最初の通知投稿には、トリアージメッセージの送信に使うための入力欄とボタンを配置した投稿をスレッドで追加しておきました。
ここで、送信したいチャンネルのチェックボックスにチェックを入れて、メッセージの入力欄に伝えたい内容を入力し、「各チャンネルに連絡」ボタンを押したら、指定したチャンネルにSlack投稿されるようにします。実装は、Jiraチケット作成と同様にボタンの@app.action()でトリガーさせて、チェックボックスやテキストインプットのデータを元にSlack blockを生成して、chat_postMessage()に渡す形です。ここで使うために、チェックボックスのvalueにはslack channel IDを入れておく形にしておきました。
各チャンネルへの投稿は、最初のステップで送っていた投稿にスレッドで続ける形にして、スレッド元の投稿を辿ることによりアラートの詳細が分かるようにしています。reply_broadcastをtrueにすることで、スレッド投稿がチャンネルにも表示されるようにしています。開発チームには、このトリアージメッセージの内容を見て、それぞれ担当するリポジトリについてアップデートなどの対応を行ってもらいます。
まとめ
以上のように、SIEMとSlack bot、Jiraを使って、Dependabot alertのPSIRTによるトリアージのフローをサポートするシステムを構築しました。全体をシーケンス図に表してみると下図のようになります。
当初の運用では「Dependabot alertsを取得する」「トリアージ用にデータを整理する」「各チャンネルにトリアージ内容を伝えてまわる」というのを全部手動で行っていたのが、このシステムによってかなり作業が簡略化される形になりました。
今回はSlack botにアラートメッセージの投稿・Jiraチケットの作成・トリアージメッセージの投稿の3つの機能を実装しましたが、今後はさらに拡張して、トリアージの結果対処不要と判断したものを一括でdismissできるようにしたり、週ごとに脆弱性残数のランキングを出力したりする仕組みを作っていこうと考えています。
セキュリティ関係の業務は、アラートされるものを1つ1つ見ていくような地道な作業がかなり多かったりしますが、できる限り自動化できる部分は自動化して、効率的に業務をこなしていけるようになると良いと思います。Dependabotの運用に苦労しているセキュリティチームの方々など、今回の仕組みが参考になれば幸いです。