CoreOS/Clairのスキャン処理追ってみる(CentOS編)その2


この記事は、Vuls Advent Calendar 2016の9日目の記事です。

こんにちは、浅香光代です。今日はいい天気だね。

9日目は、Vuls Advent Calendar 2016 6日目の続きだよ。Advent Calendar2016の僕のテーマは、他の脆弱性スキャナの内部実装を追ってみて、Vuls本体の脆弱性検知精度を高めるための情報収集をする、だよ。

前回の6日目はClairでのRedHat系OSの検知の内部実装を追うつもりだったけど、その過程でRedHatのサポートサイトに良い情報を見つけちまったもんですので、そっちに目移りしましたが、今日はちゃんとおっていくよ。

ClairはRedhat系はOVALをもとに脆弱性を検知しているようだ、ということがわかったので、今日はその部分を見ていくね。

OVALとは

セキュリティ検査言語OVAL(Open Vulnerability and Assessment Language)(*1)は、コンピュータのセキュリティ設定状況を検査するための仕様です。OVALは、米国政府が推進している情報セキュリティにかかわる技術面での自動化と標準化を実現する技術仕様SCAP(Security Content Automation Protocol)(*2)の構成要素のひとつです。

OVAL置き場

MITRE:Other Repositories of OVAL Content

  • パット見でDebian, SUSE, RedHat, Ciscoあたりが使えそうだね。
  • VulsはDebian, Ubuntu, CentOSはチェンジログのパースで検知してるんだけど、上記を使えば検知精度上がるね。Ubuntuがないけど、Clairはどーやってんだろうね。それについてはまた後日Advent Calendarでおってみるよ。 では、ClairでのOVALでの検知をおってみるね。

OVALファイルのフェッチ、パース

下辺りでOVALをインターネット経由でフェッチしているね。

// OvalFetcher implements updater.Fetcher.
type OvalFetcher struct {
    // OsInfo contains specifics to each Linux Distribution (see below)
    OsInfo OSInfo
}

// RHELInfo implements oval.OsInfo interface
// See oval.OsInfo for more info on what each method is
type RHELInfo struct {
}

func (f *RHELInfo) OvalURI() string {
    return "https://www.redhat.com/security/data/oval/"
}

OVALファイルのパース

とってきたOVALをXMLパースしてDBにぶち込んでいるね。

OVALデータ保持用の構造体

フェッチ、パースしたOVALは下記構造体で表現するんだね。

type Vulnerability struct {
    Model

    Name      string
    Namespace Namespace

    Description string
    Link        string
    Severity    types.Priority

    Metadata MetadataMap

    FixedIn                        []FeatureVersion
    LayersIntroducingVulnerability []Layer

    // For output purposes. Only make sense when the vulnerability
    // is already about a specific Feature/FeatureVersion.
    FixedBy types.Version `json:",omitempty"`
}

OVAL中のパッケージバージョンのパース

OVALにはパッケージバージョンが書かれいるね。

このへんでパッケージ名とバージョンに分解しているね。
OVALに記載されているバージョン番号は、このバージョン以前に該当する脆弱性だよ、という書き方がされているんだね。

RPMパッケージのリスト取得とパース部分

RPMパッケージ取得

この辺で、/var/lib/rpm/Packages(バイナリファイル)に対して、rpm -qaを実行してインスコされているパッケージバージョン情報を取得しているね。

    out, err := utils.Exec(tmpDir, "rpm", "--dbpath", tmpDir, "-qa", "--qf", "%{NAME} %{EPOCH}:%{VERSION}-%{RELEASE}\n")

パッケージのバージョンをパース

パッケージのバージョンのパースルールはDebian Packageのバージョニングルールに従っているらしい。Clairでは、RPM系の実装もルールでパースしているんだね。

       [epoch:]upstream-version[-debian-revision]

5.6.12 Version
パッケージのバージョン番号です。書式は、 [epoch:]upstream_version[-debian_revision] です。
バージョンを構成する 3 つの要素は

epoch
これは一桁の符号なし整数です。普通は小さい数になるはずです。 ゼロと仮定して良い場合は省略できます。省略した時には、 upstream_version にコロンを含めてはいけません。
これはパッケージの古いバージョンのバージョン番号の誤りを許したり、 パッケージの以前のバージョン番号体系をそのままに残しておくためにあります。

upstream_version
これがバージョン番号の主要部分です。通常支障ない場合は .deb ファイルが作られたオリジナルの ("上流の") パッケージのバージョン番号になります。 普通は上流の作者によって定められたものと同じ形式になりますが、 パッケージ管理システムと比較手法に沿って修正を加えなければならないかもしれません。
upstream_version に関するパッケージ管理システムの比較の挙動については次節で述べます。 バージョン番号中で、この upstream_version の部分は必須です。
upstream_version は英数字 [37] と文字 . + - : ~ (ピリオド、プラス、ハイフン、コロン、チルド) だけから構成されており、数字で始まるようにすべきです。 ただし、debian_revision がない場合、ハイフンは許されません。 また、epoch がない場合、コロンは許されません。

debian_revision
バージョンのこの部分は、そのパッケージを Debian バイナリパッケージにするためにほどこした修正のバージョン番号を表わしています。 これは英数字と + . ~ (プラス、ピリオド およびチルド) の三記号のみからなり、upstream_version と同じやり方で比較されます。
この部分はオプションです。 debian-revision を持たない場合には、 upstream-version はハイフンを含んでいてはいけません。 この debian-revision を持たない形式のものは Debian パッケージとして特別に書かれたソフトウェアであることを示しています。 その場合、Debian パッケージソースは元のソースと常に同一の筈ですから、レビジョンの追加は必要ありません。
upstream_version が増加するたびに、 debian_revision を 1 に戻すのが慣習となっています。
パッケージ管理システムは文字列中の最後のハイフン (あれば) のところでバージョン番号を upstream_version と debian_revision とに分割しようとします。 debian_revision がないものは、debian_revision が 0 と等価です。

二つのバージョン番号を比較する場合には、まず epoch 値が比較され、次に epoch が同じである場合には upstream_version が比較され、さらに upstream_version も同じである場合には debian_revision が比較されます。 epoch は数字として比較されます。 upstream_version と debian_revision の部分はパッケージ管理システムによって、以下記載のアルゴリズムを用いて比較されます。

文字列は左から右へと比較されます。
最初に、比較対象となる2つの文字列の中で、全て非数字で構成される最初の部分を決定します。 両方の文字列に対する、この数字でない部分 (そのうちの一つは空であってもかまいません) を辞書順で比較します。 もし違いが見つかれば、それを返します。 ここでの辞書順とは、すべての文字が非文字より先に来る様に修正し、 更にチルドがもっとも前に来る修正 (行末の空文字列より更に前) を加えた ASCII 順です。 例えば、以下の各部分は早いほうから遅いほうへの順でソートされます。 ~~, ~~a, ~, 空部分, a[38]。

次に、二つの文字列の残りの部分から、全て数字で構成される最初の部分を決定します。 この二つの数値を比較し、比較結果として見つかった違いを返します。 このとき、空文字列 (比較している一方もしくは双方のバージョン文字列の末尾においてのみ生じる) は 0 として数えます。

違いが見つかるか、双方の文字列を調べ尽くすかするまで、この二つのステップを (先頭から、最初の非数字の文字列と最初の数字の文字列を比較し、切り離しながら) 繰り返します。

epoch の目的はバージョン番号付けのミスをそのままにできるようにするため、またバージョン番号の付け方を変更した場合に、 それをうまく扱えるようにするためだということに注意してください。 パッケージ管理システムが解釈することのできない文字 (ALPHA や pre- など) から成る文字列を含むバージョン番号や、思慮の浅い順序付け[39] をうまく処理するためでは ありません。

あたしゃ、epochの意味始めて知ったよ。
バージョン体系を変更する時に使うんだね。
upstream_versionとdebian_versionの意味も初めて知ったよ。
今まではパット見でバージョニングルール統一されてねぇじゃん、クソだな、と思っていたけど、ちゃんとルールがあるんだね。(当然っちゃ当然だけど)誤解していてごめんね。
Changelog比較とOVALベースを併用すれば、Vulsの検知精度が間違いなく向上するね。
あたしゃ嬉しいね。アドベントカレンダー参加して良かったよ。

  • パッケージのVersionを表現する構造体

OVALとパッケージバージョンを比較している部分

上記のルールをゴリゴリ実装しているね。
in-outはテストケース見たらわかるね。

Vulsからclairをimportして使ったら怒られるかな。
ClairはApache v2か。OSSって最高だね!あたしゃ車輪の再発明はメンゴだよ!

というわけで

RedHat系のOVALを用いての脆弱性検知の仕組みがだいたいわかったよ。近い将来Vulsに組み込むから楽しみにしていてね。
Changelogパースと併用することで、Vulsは検知精度でナンバーワンを目指すよ。これでまた世界が平和に近づくね。

Vuls Advent Calendar 10日目はcosign930さんです。よろしくお願いします。

TODO

OVALでの比較はバックポートにも対応してるのかな?また調べる。