NVDを元に脆弱性DBを構築した話

はじめに
どうも、SST研究開発部の小野里です。弊社は10月末、EASMサービスのβ版をリリースいたしました。
https://www.securesky-tech.com/2023/10/26/6723/
弊社EASMサービス(以下EASMと呼称します)では、お客様のアセット内で見つかった技術から既知の脆弱性を検索するため、NVD等の脆弱性データベースの情報を元に構築したデータベースをバックエンドで使用しています。このデータベースはAWSのRDS上に構築しており、毎日NVDから差分を取得して更新しています。この脆弱性データベース、ただクローンを作っただけじゃないの?と思われるかもしれませんが、実は構築に結構苦労しました。今回はそんな苦労したポイントを振り返ってみようと思います。
※本記事における意見は、筆者の個人的な意見であり、所属団体や関与するプロジェクト等の意見を代表するものではありません。
前提知識
NVDとは
National Vulnerability Databaseの略で、米国国立標準技術研究所(NIST)が運営する既知脆弱性のデータベースです。基本的には世界で一番充実している脆弱性データベースと思ってもらえればいいかと思います。世界中のソフトウェアの公開された脆弱性情報はここに集まります。
CVEとは
https://www.ipa.go.jp/security/vuln/scap/cve.html
Common Vulnerabilities and Exposuresの略で、ある製品中の脆弱性に対して付与される一意の番号です。米MITRE社を中心に管理されており、NVDの情報は大元であるCVEのデータベースとほぼ同期されています。
CPEとは
https://www.ipa.go.jp/security/vuln/scap/cpe.html
Common Platform Enumerationの略で、ハードウェア・ソフトウェアなど、ある製品を識別するために付与される規格化された名前です。NVDにおいては、どの製品のどのバージョンに脆弱性が存在するかを識別するために使用されます。
何故独自に脆弱性DBを構築するのか
先述の通り、EASMではNVDのデータを取得し、そのデータをAWSのRDS上に構築したデータベースに保存しています。ここでいくつかの疑問をお持ちの方もいらっしゃるかもしれないので、そもそも何故そのような構成になっているかをそれぞれの疑問にお答えする形で説明していきます。
API経由で都度データを取得すればいいのでは?
データが取得できるということは、当然NVDにはデータ取得用のAPIがあります。
https://nvd.nist.gov/developers/vulnerabilities
ならば、脆弱性情報を利用する際にそのAPIからデータを取得すればいいのでは?とお考えの方もいらっしゃるかもしれません。そうせずにローカルにデータを持っておく理由は、大きく分けて2つあります。
NISTから推奨されている方法だから
こちらのガイドのBest Practicesに記載されている通り、NVDのデータを全てダウンロードし、都度更新をかけていくというフローはNISTから推奨されたNVDの使い方です。このようなフローが推奨されているのは、恐らくこの次の項目が理由でしょう。
APIのレスポンスが非常に遅いから
NVDには現在、約23万件以上のCVE情報が登録されています。この膨大なデータから脆弱性情報を検索するので、APIリクエストに対するレスポンスに数秒程度の時間がかかることはごく一般的です(この記事を書いている間に何回かAPIリクエストしましたが、レスポンスまでに最長8秒かかりました)。また、一定期間内にリクエストが集中したなどの要因により、レスポンスがタイムアウトすることも頻繁に発生します。
EASMではお客様が自社のアセットに対してどんなリスクがあるかを調べるたびに脆弱性情報を参照するので、その都度NVD APIにリクエストを送っていると、サービスとして非常に動作が遅く、不安定なものになってしまいます。そのため、EASMサービスの中に脆弱性データベースを構築しておく必要がありました。
既存の脆弱性データベース作成ツールを使えばいいのでは?
NVDのローカルコピーを作成するツールとして、go-cve-dictionaryなど多くのソフトウェアが存在します。
https://github.com/vulsio/go-cve-dictionary
今回、それらを使用せずに独自にデータベースを構築したのは、主に以下の理由によるものです。
他のデータと連携するため
EASMサービスで利用するRDS上にはお客様のアセット情報を始めとする様々なデータが格納されており、それらはPrismaでスキーマ管理されています。脆弱性情報もこのPrismaで管理した上で、アセット情報等とのリレーションなども行いたいという要件がありました。
要件に合わせてカラムを変えるため
EASMで構築している脆弱性データベースはNVDの純粋なクローンというわけではなく、元々の脆弱性情報の意味を失わない範囲でEASMの要件に合わせてカラムを追加・変更しています。一例を挙げると、脆弱性情報の概要を示すdescriptionカラムの他に、EASM側でdescriptionの内容を機械翻訳した内容を保持するjpDescriptionカラムを追加するなどの変更を行っています。そのため、こちらの要件に合った形でDBを作成する必要がありました。
NVDからデータを取得する上でのつまずきポイント
NVDには現在、CVE情報が約23万件、CPE情報が約120万件、有効なCPEのリストを取得するためのMatch Criteriaという情報が約42万件以上存在します。このうちEASMではCVEとMatch Criteriaを利用しているので、約65万件以上のデータを保持しています。日常的に大規模なデータベースを管理している方からすればなんてことはない件数かもしれませんが、私自身今まで大規模データベースの運用経験はなく、また自社で生成したものではないデータを格納するデータベースという条件下特有のトラブルもありましたので、そのあたりのつまずいたポイントやその解決策をご紹介していきます。
メモリが枯渇する
脆弱性情報をダウンロードしてRDSに登録するコードはDocker上に構築し、AWSのECSで動かしています。データベースの初期構築時、実行中にDockerコンテナに割り当てられたメモリを使い切ってOut of Memoryエラーにより異常終了するという問題が発生しました。原因は当然ながらデータ量が膨大なためですが、
- 初期構築処理にかかる時間が膨大なので、データのダウンロードとデータ登録はなるべく並行して進めたい
- だが、データのダウンロードよりデータ登録の方が時間がかかるため、ダウンロード済みで登録待ちのデータがメモリ上にどんどん溜まっていく
という状況で、解決が難しそうでした。
結果としては多少力業ではありますが、初期構築時のECSタスクに割り当てるメモリ量を増やして対応しました。メモリが枯渇するのは初期構築時のみで、毎日のデータ更新ではデータ量が少ないため問題ないと思われること、コードの最適化などに時間をかけるよりも相対的にコストが安そうだったことなどが理由です。
とここまで書いて開発環境のログを眺めていたところ、毎日の自動更新でも一時的にデータ量が多い日があったようで、同じくメモリが枯渇するエラーが出ていたことに気が付きました……。緊急で修正しますが、やはり定期的な更新でもある程度のメモリは確保しておく方が良さそうです。
データの登録時にPrismaクライアントがタイムアウトする
同じくデータベースの初期構築時、Prismaクライアントがタイムアウトしてデータの登録に失敗するという問題が発生しました。これは1回のデータ登録クエリの実行に時間がかかるため、実行待ちのクエリがコネクションプールにどんどん溜まっていき、やがてデフォルトのタイムアウト時間である10秒を超えて実行されないクエリがタイムアウトし、データの登録に失敗するという流れでした。そもそもデータ登録のクエリに時間がかかるのは登録時にリレーションも構築しているためですが、この辺りの最適化はやはり時間がかかりそうです。
これを解決するために、公式ドキュメントを参考にコネクションプールのタイムアウトを無効にしました。この設定には注意が必要ですが、初期構築中に他に接続してくるクライアントは無いこと、前述の設定により(少なくとも初期構築中には)メモリの枯渇は発生しない前提であることから、問題ないと判断しました。
NVD APIで定義されているスキーマと実態のデータが合っていない
NVDに登録されている元データですが、これはNIST(かMITRE?)の中の人が人力で入力していると思われます。ですので、必ずしも完璧ではなく、時に間違っていることもあります。脆弱性内容そのものの間違いについてはNVD の脆弱性情報を活用する上で気をつけたいこと | yamory Blog などの記事で指摘されていますが、APIドキュメントで公開されているJSONスキーマと実際に返されるデータとの間でデータ型の齟齬も発生したりしています。例えば、CVE APIのレスポンスの中にconfigurations > nodes > cpeMatchという項目があります。この項目は、NISTにより公開されているCVE APIレスポンスのJSONスキーマでは以下のように定義されています。
"node": {
"description": "Defines a configuration node in an NVD applicability statement.",
"properties": {
"operator": {"type": "string", "enum": [ "AND", "OR"] },
"negate": {"type": "boolean"},
"cpeMatch": {
"type": "array",
"items": {"$ref": "#/definitions/cpe_match"}
}
},
"required": ["operator", "cpeMatch"],
"additionalProperties": false
},
requiredにcpeMatchが含まれていることから分かるようにこの項目は必須とされていますが、開発当時CVE-2021-43753のデータにおいてはこの値が存在しませんでした(現在は修正されているようです)。
このような事態が発生するということは恐らくNIST側でのデータ登録時にスキーマ定義との整合性のチェックはされていない可能性があり、今後も起こり得るかもしれません。この時はデータをNullableなものとして扱うことで対処しましたが、必ずしもレスポンスを信用せず、例外が発生する可能性は常に考えておいた方が良さそうです。
CPEとMatch Criteriaの違いが分からない
NISTにより公開されているProducts APIでは、CPE APIとMatch Criteria APIの2つのAPIがあります。
https://nvd.nist.gov/developers/products
CPEについては前提知識の欄で書いた通り明確な定義があるので分かりますが、Match Criteriaとは何でしょうか?APIの説明には
The CPE Match Criteria API is used to easily retrieve the complete list of valid CPE Match Strings. Unlike a CPE Name, match strings and match string ranges do not require a value in the part, vendor, product, or version components.
とあります。訳すと
CPE Match Criteria API は、有効な CPE Match Strings の完全なリストを簡単に取得するために使用します。CPE Name と異なり、マッチ文字列およびマッチ文字列の範囲は、part、vendor、product、version コンポーネントに値を必要としません。
というような意味です。完全なリストを簡単に取得とはどういうことでしょうか?いまいち意味が掴めません。そこで、CVE, CPE, Match Criteriaの関係について調査しました。以下に調査の結果分かったことの一例を挙げます。
- どのCPEにも紐づかないMatch CriteriaにCVEは紐づいているか?
紐づいている。例としてMatchCriteriaID:D21D57EA-DF58-429B-9FBE-F0080085B62EはどのCPEにも紐づいていないが、このMatch Criteriaに紐づくCVEはCVE-2003-0849,CVE-2004-1701,CVE-2004-1702,CVE-2005-2960の4件ある。これらのCVEにはCPE情報を含む他のMatch Criteriaも紐づいているが、それらはversion情報まで記載のMatch Criteriaであり、update情報まで記載されているのはD21D57EA-DF58-429B-9FBE-F0080085B62Eのみだった。以上より、NVDのCPE辞書ではupdate以下の項目を考慮していない可能性がある(未検証)。 - CPEからではなく、Match Criteria情報からでしか逆引きできないCVE情報は存在するか?
存在する可能性がある。例としてCVE-2005-2960ではcpe:2.3:a:gnu:cfengine:2.1.7:p1:*:*:*:*:*:*というMatch Criteria情報が影響を受けるソフトウェアとして挙げられているが、これはCPE辞書のCPEレコードとは紐づけられていないため、バージョン2.1.7の該当ソフトウェアのCPE情報からCVE情報は検索できない。 - どのMatch Criteriaにも紐づかないCVEは存在するか?
2023/10/06の時点で13974件存在するが、そのほとんどのステータスがRejected, Awaiting Analysis, Undergoing Analysisであるため、恐らく有効なCVEでどのMatch Criteriaにも紐づかないという場合は無いと思われる。 - どのCPEにも紐づかないMatch Criteria(matches欄が空のMatch Criteria)はどれぐらいあるのか?
2023/10/06時点で146278件。
上記の調査結果から、Match CriteriaはCPE情報とCVE情報を繋ぐ中間テーブルのような役割を果たしていると考えられます。CPEはソフトウェアの細かいバージョン違い等で1つのCVE情報に関連するCPEが膨大になったりするので、それをある程度まとめてCPEとCVE間にMany to Manyのリレーションを構築しているようです。しかしながら完全な中間テーブルの役割というわけでもなく、上記調査結果のようにそもそもMatch Criteria情報のみで肝心のCPE情報が一切無いというパターンもあります。必ずしもCPE情報からMatch Criteriaを経由して関連するCVE情報が逆引きできるわけではないようです。
EASMではCPEから得られる情報としてCPE文字列、venderやproduct名、バージョン番号が必要で、それらはMatch Criteria APIのcriteria要素からも取得可能でした。そのため、あえてCPE APIからはデータを取得せずMatch Criteria APIからのみデータを取得し、それをCPEの代わりとして使っています。
まとめ
ここに書けなかったことも含めて脆弱性情報の取得と活用には様々な障害がありましたが、何とか乗り越えて現在はEASMサービスの一部として日々稼働しています。もしもNVDのデータをダウンロードしてローカルにデータベースを構築することになってしまった方がいらっしゃった場合、この記事が何かの助けになれば幸いです。
引き続きバグの修正とか動作の最適化とか新機能の追加とか頑張っていかないとなと思っていたところ、PMからSlackでこのようなメッセージが届きました。

脆弱性情報DBを巡る戦いはもうしばらく続きそうです。