freeeの開発情報ポータルサイト

Dependabot alertをSlackに通知して、トリアージ運用に役立てる仕組みを作ってみた

こんにちは、PSIRTのWaTTsonです。

去年の12月にAdvent CalendartでAWS SecurityHubの結果をSIEM on Amazon OpenSearch Serviceに取り込んだ話を書きました:

developers.freee.co.jp

今回は、同じく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で動かすようにします。

EventBridgeのトリガーによりdailyでLambdaが実行される。LambdaはSecrets Managerから秘匿情報を取得する。LambdaはGitHub APIからDependabot alertsの情報を取得する。LambdaはS3 bucketに取得データを保存する。S3 bucketのPutObjectトリガーでes-loader (Lambda) が実行される。es-loaderはSIEMにデータを取り込む。
構成図: SIEM on Amazon OpenSearch Serviceへの取り込み

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つの処理を実装します:

  1. dailyでSIEM上のDependabot alertを確認して、新規追加分をSlack通知する処理
  2. Slack通知投稿中のボタンをクリックした時に、Jiraチケットを作成する処理
  3. Slack通知投稿中のボタンをクリックした時に、入力欄に入れたメッセージを指定のSlackチャンネルに投稿する処理

Slack WorkflowによりSlack投稿が行われる。投稿がトリガーになって、Slack AppからBolt for Pythonが実行される。処理はECS on FargateのContainerで行われる。Container上のSlack AppはSIEMからDependabotの情報を取得する。Slack AppはSlack投稿を行う。投稿上のボタン押下がトリガーとなってSlack Appの処理が行われる。Jiraにissueを作成する。
構成図: Slack bot

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ボタンを付けておきます。また、開発者の各チャンネルにトリアージ内容を通知するための入力欄とボタンを配置した投稿を、スレッドで投稿するようにしておきます。

Slackの#siem_alert_notificationチャンネル内のスレッド。SecurityOperatorのアプリによる「Dependabot alert: Vulnerability in XXX」のタイトルの投稿に、アラートの詳細が記述されている。2023-03-31 00:00〜2023-04-01 00:00 (filtered by ghsaId) repository: repository_1, repository_2 securityAdvisory Vulnerability in XXX severity: CRITICAL GHSA-xxxx-xxxx-xxxx GitHub Advisory Databaseへのリンク SIEM Discover 長すぎるため表示できません SIEM Dashboard 長すぎるため表示できません ボタン: Create Jira Issue 1件の返信がスレッドで続けられている。SecurityOperatorのアプリによる投稿。トリアージ チェックボックス: #tiem_1_channel Slack投稿へのリンク チェックボックス: #tiem_2_channel Slack投稿へのリンク 続きを見る
Slack投稿: アラートメッセージ

2. Jiraチケット作成

これは、既存のWAFなどのアラート通知で実装されているものを流用します。Create Jira Issueのボタンが押されたときにBolt for Pythonの@app.action()で引っかけて処理を実行します。ここではメッセージの内容からButtonブロックを省いたものを、Jiraチケットのdescriptionに入れる形で、Jira APIからチケットを作成します。

JiraのSECSENSプロジェクト内のチケット。タイトル: Vulnerability in XXX 説明: 2023-03-31 00:00〜2023-04-01 00:00 (filtered by ghsaId) repository: repository_1, repository_2 securityAdvisory Vulnerability in XXX severity: CRITICAL GHSA-xxxx-xxxx-xxxx GitHub Advisory Databaseへのリンク SIEM Discover SIEMへのリンク SIEM Dashboard SIEMへのリンク
Jiraチケット

3. トリアージメッセージの送信

最初の通知投稿には、トリアージメッセージの送信に使うための入力欄とボタンを配置した投稿をスレッドで追加しておきました。

Slackの#siem_alert_notificationチャンネル内のスレッド。上掲のアラート投稿への返信部分。トリアージ チェックボックス: #tiem_1_channel Slack投稿へのリンク チェックボックス: #tiem_2_channel Slack投稿へのリンク 区切り線 Message テキストエリア ボタン: 各チャンネルに連絡
Slack投稿: トリアージ用の入力フォーム

ここで、送信したいチャンネルのチェックボックスにチェックを入れて、メッセージの入力欄に伝えたい内容を入力し、「各チャンネルに連絡」ボタンを押したら、指定したチャンネルにSlack投稿されるようにします。実装は、Jiraチケット作成と同様にボタンの@app.action()でトリガーさせて、チェックボックスやテキストインプットのデータを元にSlack blockを生成して、chat_postMessage()に渡す形です。ここで使うために、チェックボックスのvalueにはslack channel IDを入れておく形にしておきました。

Slackの#team_1_channel内の投稿。SecurityOperatorアプリによる「Dependabot alert: Vulnerability in XXX」のタイトルの投稿に1件の返信。パッケージ「XXX」の脆弱性です。バージョン2.0.0にアップデートをお願いします。
Slack投稿: トリアージメッセージ

各チャンネルへの投稿は、最初のステップで送っていた投稿にスレッドで続ける形にして、スレッド元の投稿を辿ることによりアラートの詳細が分かるようにしています。reply_broadcastをtrueにすることで、スレッド投稿がチャンネルにも表示されるようにしています。開発チームには、このトリアージメッセージの内容を見て、それぞれ担当するリポジトリについてアップデートなどの対応を行ってもらいます。

まとめ

以上のように、SIEMとSlack bot、Jiraを使って、Dependabot alertのPSIRTによるトリアージのフローをサポートするシステムを構築しました。全体をシーケンス図に表してみると下図のようになります。

LambdaからGitHubにGraphQLクエリを投げ、Dependabot alertsを取得する。LambdaはS3にJSONを保存する。S3のPutObjectトリガーによりes-loader (Lambda)が実行される。es-loaderはS3にGetObjectを行いデータ取得する。es-loaderはSIEMにデータを取り込む。Slackのsecurity channelのメッセージをトリガーとしてECSの処理が実行される。ECSからSIEMにDSLクエリを投げ、データを取得する。ECSはSlackのsecurity channelとdeveloper channelにアラートメッセージを投稿する。Slack security channelのボタンによりECS上の処理がトリガーされ、Jiraにチケットを作成する。Slack security channelのボタンによりECS上の処理がトリガーされ、Slack developer channelにトリアージメッセージを投稿する。
シーケンス図

当初の運用では「Dependabot alertsを取得する」「トリアージ用にデータを整理する」「各チャンネルにトリアージ内容を伝えてまわる」というのを全部手動で行っていたのが、このシステムによってかなり作業が簡略化される形になりました。

今回はSlack botにアラートメッセージの投稿・Jiraチケットの作成・トリアージメッセージの投稿の3つの機能を実装しましたが、今後はさらに拡張して、トリアージの結果対処不要と判断したものを一括でdismissできるようにしたり、週ごとに脆弱性残数のランキングを出力したりする仕組みを作っていこうと考えています。

セキュリティ関係の業務は、アラートされるものを1つ1つ見ていくような地道な作業がかなり多かったりしますが、できる限り自動化できる部分は自動化して、効率的に業務をこなしていけるようになると良いと思います。Dependabotの運用に苦労しているセキュリティチームの方々など、今回の仕組みが参考になれば幸いです。