前回、Vulsのコードを読む その1 全体像の把握でざっくりとscanコマンド周りのそーうコードと実行時の流れの理解を行いました。
今回は、Vulsの中で利用されている go-cve-dictionary についてソースコードを読んで、理解をしていきたいと思います。
また、今回はソースコードを確認していく中でバグを発見しプルリクを出してみましたのでプルリクの投げ方についても簡単に解説していきたいと思います。
(追記 2019/11/23)
無事プルリクがマージされました!
go-cve-dictionaryの特徴
go-cve-dictionaryはNVDとJVNの脆弱性データベースの情報を取得し、SQLiteやMySQLに保存、CVE の番号から簡単に検索する機能を提供しています。
例えば
$ go-cve-dictionary fetchnvd -years 2002 2003 2016
というコマンドでNVDの2002, 2003, 2016年の脆弱性情報 (CVE) を取得し、cve.sqlite3 に保存することができます。
以下のコマンドでWebサーバーを立ち上げて保存した脆弱性情報を検索することもできます。
$ go-cve-dictionary server
$ curl http://127.0.0.1:1323/cves/CVE-2014-0160 | jq "."
CVE-2014-0160はHeartBleedの脆弱性です。
ソースコード
main.go
vulsと同様に github.com/google/subcommands を使ったコマンドラインの引数を受け付ける部分になっています。
subcommands.Register(subcommands.HelpCommand(), "")
subcommands.Register(subcommands.FlagsCommand(), "")
subcommands.Register(subcommands.CommandsCommand(), "")
subcommands.Register(&commands.ServerCmd{}, "server")
subcommands.Register(&commands.FetchJvnCmd{}, "fetchjvn")
subcommands.Register(&commands.FetchNvdCmd{}, "fetchnvd")
subcommands.Register(&commands.ListCmd{}, "list")var v = flag.Bool("v", false, "Show version")
この形を一度理解するとsubcommandsを使ったコードを読むのが楽になります。
しかし、GoでのCLIツールを作成するためのライブラリは他にも多くあり、有名なものとしては spf13/cobra や urfave/cli などあります。
書き方に多少の違いはありますが、考え方などは同じように見えるので一度どれかのライブラリで書き方や読み方を理解することが重要です。
commands/
ここに実際の各コマンドのオプションと実行の処理が書かれています。
fetchjvn.go :JVNの脆弱性情報を取得しデータベースに保存
fetchnvd.go : NVDの脆弱性情報を取得しデータベースに保存
list.go :データベースに保存された脆弱性情報のステータス確認
server.go : サーバーモードで起動し、CVEの検索
今回はfetchnvd.goに注目していきます。
commands/fetchnvd.go
NVDのFeedsよりCVEのリストを取得し、データベースに保存します。
全体の流れは大きく分けて6ステップあります。
1. データベース接続
2. meta情報の取得
3. meta情報より更新が必要かどうか判断 (過去のmeta情報と比較)
4. NVDの脆弱性情報の収集、変換
5. 脆弱性情報をデータベースへ保存
6. meta情報をデータベースへ保存
1. データベース接続
L162~L171の部分。
設定情報からどのデータベースを利用する(SQLite, MySQL等)のか、データベースのパス(ユーザー名、パスワード等)を使ってデータベースの接続を行います。
go-cve-dictionaryではSQLite3, MySQL, PostgreSQL (Redis) に対応しています。
これは、GORM というO/Rマッパーを使って実現させています。
Githubのスターの数も15,000以上ありこちらがほぼスタンダードのようになっているようです。
O/Rマッパー (ORMマッパー) とは
オブジェクト関係マッピング(英: Object-relational mapping、O/RM、ORM)とは、データベースとオブジェクト指向プログラミング言語の間の非互換なデータを変換するプログラミング技法である。オブジェクト関連マッピングとも呼ぶ。実際には、オブジェクト指向言語から使える「仮想」オブジェクトデータベースを構築する手法である。
直接データベースの操作をするのではなく、仮想データベースの操作を行うことで異なるデータベースシステムも操作できるようになるものです。
2. meta情報の取得
L173~L182の部分。
FetchLatestFeedMeta() でmeta情報の取得を行います。
FetchLatestFeedMeta() は fetcher/nvd/util.go のL91で定義されています。
どこで定義されているか確認したい場合は、関数名をダブルクリックで定義している場所や参照されている場所へGithub上で飛ぶことができます。とても便利。
FetchLatestFeedMeta() では、NVDのfeedsのmeta情報の取得とすでに取得済みのmeta情報をデータベースから取得し、変数metasにmeta情報を追加していきます。
NVDのmeta情報には以下のようなデータが含まれています。
go-cve-dictionaryではsha256のハッシュ値を使って以前取得した脆弱性情報と変更があるかどうかの判断を行なっています。
metasは models/models.go で 構造体 FeedMeta として定義されているものです。
NVDから取得したハッシュ値はLatestHashに入れ、データベースから取得したハッシュ値はHashに入れます。
3. meta情報より更新が必要かどうか判断 (過去のmeta情報と比較)
L185~L201の部分。
取得したmetasを使って、
1. 新しく作成されたものか ( Newly() )
2. 更新が必要なものか ( OutDated() )
3. 更新不要なものか (上記以外の場合)
の判断を行なっています。
1. Newly() では、データベースから取得したHashの値に何も入っていないかどうかで判断しています。
2. OutDated() ではNVDから取得したLatestHash とデータベースから取得したHashが同じかどうかで判断しています。
4. NVDの脆弱性情報の収集、変換
L203~L212の部分。
この部分で実際のNVDの脆弱性情報の収集を行なっています。
実際の処理は fetcher/nvd/json/nvd.go で定義されている FetchConvert() で行われています。
FetchFeedFile() でゴルーチンを使ってNVDヘリクエストを投げ、データの取得を行います。
取得したjsonデータ (results) をUnMarshalし、convert() を使って扱いやすい形 (CveDetail) に変換しています。
CveDetailのNVDJsonの構造
CPEに関してURIとFormattedString, WellFormedNameに変換されます。
CPEに関してはIPAの共通プラットフォーム一覧CPE概説を確認すれば基本的に問題ありませんが、この部分に関してはIPAの概説では触れられていない部分のため、MITREのCPE - Common Platform Enumeration: CPE Specificationsを確認する必要があります。
一番詳細に書かれているのは NISTIR 7695, Common Platform Enumeration: Naming Specification v2.3 | CSRCの仕様書ですのでこちらを一度確認するのがいいと思います。
NVDから取得する脆弱性情報にはURIの形式で書かれているため、URIの形式をパースし他の扱いやすい形へ変換する必要があります。
CPEの変換の処理は knqyf263/go-cpe というライブラリを利用しています。
(ソフトウェアデザイン 2019年11月号 脆弱性スキャナ特集で少し触れました福田さんが作成したライブラリのようです)
こちらのライブラリはNIST IR 7695, 7696で書かれているリファレンス実装をgoで実現したものになっています。
リファレンス実装とは
リファレンス実装(リファレンスじっそう、英: reference implementation)は、なんらかの機能を実現するハードウェアまたはソフトウェアであり、他者がそれを参考にして独自に実装することを助ける目的で作られたものを言う。参考実装(さんこうじっそう)とも呼ばれる。
NVDの脆弱性情報の収集、変換の部分はやっていることは単純ですが、並列化し高速化する処理や扱いやすいように変換する処理など、コード行数としては結構あります。
5. 脆弱性情報をデータベースへ保存
L217~L229の部分。
InsertNvdJSON() を使ってデータベースへの保存を行います。
ここでは古いデータベースの情報を削除し、新しい情報を追加していく処理を行なっています。
Cvss3、Cpesなどのデータベースのテーブル毎に処理を行なっています。
6. meta情報をデータベースへ保存
L231~L234の部分。
最後にmeta情報をデータベースに保存して次のNVDの脆弱性情報の更新時に備えます。
UpdateMeta() の中で利用されている UpsertFeedHash() でmeta情報のデータベースへの保存処理が行われています。
上記のソースコードは、UpsertFeedHash() の新規にmetaデータをデータベースに保存するか、アップデートするかを判断して処理を分けて実行している部分です。
色の付いているL1072の部分、LastModifiedDateをNVDから取得したものへとアップデートする処理を行うはずだが、metaへではなく、mへデータを渡してしまっている。
このままではLastModifiedDateがはじめに作成された日時のまま更新されない。
この部分はバグだと思われるのでプルリクエストを出して対応を行いました。
私にとっての初めてのプルリクだったため、以下のQiitaの記事を参考に行いました。
【GitHub】Pull Requestの手順 - Qiita
(追記 2019/11/23)
無事にマージされました!
マージだんですっhttps://t.co/1wRQYP5hxC
— バルスのちょんまげおじさん (@kotakanbe) November 22, 2019
コントリビューターデビューを果たしました!
参考
プルリクの出し方
【GitHub】Pull Requestの手順 - Qiitaに詳しく書かれています。
大きな流れとしては
- 対象のGitHubリポジトリをFork
- ローカルへclone(クローン)
- ローカルリポジトリで新規ブランチを作成
- 修正を加える(コミット)
- 作成したブランチをpush(プッシュ)する
- Pull RequestをGitHub上で作成
プルリク後のCIツール
実際にプルリクを出すとgo-cve-dictionaryでは以下のようなCIツールにより検証が自動で走ります。
GolangCIではgolint, errcheck, staticcheck,deadcodeなどを行なってくれるようです。
オープンソースには無料で利用可能です。
Travis CIでは .travis.yml に書かれた内容のビルドやテストを実行してくれるもののようです。
こちらもオープンソースには無料で利用可能です。
感想
今回はVulsの中で使われているgo-cve-dictionaryのコードを読んで理解してみました。
その中でdb/rdb.go の中にバグがあることを発見し、プルリクを出してみました。
コードを読むことでgoの勉強、NVDのdata feedsやCPEに関してなど多くのことを学ぶことができますし、バグを見つければプルリクを出して貢献することもできます。
また、今回バグのあったrdb.goにはテストコードがなかったためバグが残ってしまっていた、というのがひとつ考えられると思います。
rdbの部分はデータベースを扱う必要があるためにテストがしにくい部分でもあります。
gormのテストのために DATA-DOG/go-sqlmock というものがあるようなので今後こちらを使ってテストコードも書けたらいいな、と思っています。
その1、その3、その4はこちら。
Vulsのコードを読む その1 全体像の把握 - Security Index
Vulsのコードを読む その3 Vuls scanを調べてみた - Security Index
Vulsのコードを読む その4 Vuls reportを調べてみた - Security Index
実践Terraform AWSにおけるシステム設計とベストプラクティス
Twitter アカウント