VEX
Takumi のイメージに対する脆弱性スキャンは、検出感度を高めるために疑わしい検出結果も残します。
そのため、報告のなかには実際には該当しないもの、すでに修正済みのもの、または脆弱性としての扱いに争いがあるものも含まれます。
VEX(Vulnerability Exploitability eXchange)は、検出結果ごとの判断と根拠を、機械が読める形で公開する仕組みです。
Takumi Images では OpenVEX 形式で、not_affected、fixed、affected の判断を公開しています。
公開している VEX は、単なる抑制リストではありません。
affected も含めて公開し、利用者が「どの検出結果が、どの理由で、どのイメージに対して判断済みなのか」を確認できるようにしています。
Takumi Images は現在ベータ版です。 仕様や挙動が予告なく変更される可能性があります。
公開している経路
Takumi Images は、同じ VEX の判断を2つの経路で公開しています。
署名付き attestation
イメージの digest に紐づく OpenVEX attestation です。 SBOM や SLSA Provenance と同じ keyless 署名の identity で検証でき、pull した成果物に対する証跡として使います。 VEX はアーキテクチャごとの子 manifest ではなく、原則としてマルチアーキテクチャの index に添付します。
公開 VEX repository
https://vex.images.flatt.tech で公開している VEX repository です。
VEX Repository Specification に沿った layout で、対応するスキャナや監査用のツールが参照できます。
イメージを pull する前に VEX を取得したい場合や、grype にまとめて OpenVEX ファイルを渡したい場合はこちらを使います。
どちらも、Takumi Images の vex/takumi.openvex.json を元に生成します。
元の文書は Nix のコンポーネント単位の pkg:nix/... を product として持ちますが、公開時にはイメージ単位の pkg:oci/... に変換します。
公開 VEX repository の構造
本番の discovery manifest は次の URL で公開しています。
https://vex.images.flatt.tech/.well-known/vex-repository.json
manifest は、VEX repository のデータ本体として v1/vex-data.tar.gz を指します。
これは仕様上、repository client が archive を取得して展開するためです。
HTTP 上のディレクトリをたどって各ファイルを探すのではなく、archive の root に index.json と pkg/... を置きます。
https://vex.images.flatt.tech/.well-known/vex-repository.json
https://vex.images.flatt.tech/v1/vex-data.tar.gz
https://vex.images.flatt.tech/v1/index.json
https://vex.images.flatt.tech/v1/pkg/oci/images.flatt.tech/takumi/<image>/vex.json
https://vex.images.flatt.tech/v1/all.openvex.json
それぞれの役割は次のとおりです。
.well-known/vex-repository.json
VEX repository の discovery manifest です。
spec_version: "0.1"、データ archive の URL、update_interval: "24h" を広告します。
v1/vex-data.tar.gz
スキャナ向けの配布 archive です。
archive の中には index.json と pkg/oci/.../vex.json が入ります。
v1/index.json
PURL から VEX 文書の location への対応表です。
PURL 文字列を key として引くため、repository_url の / は %2F に percent-encode された形で入ります。
v1/pkg/oci/images.flatt.tech/takumi/<image>/vex.json
イメージごとの OpenVEX 文書です。
たとえば busybox の VEX は次の URL から取得できます。
https://vex.images.flatt.tech/v1/pkg/oci/images.flatt.tech/takumi/busybox/vex.json
v1/all.openvex.json
全イメージ分をまとめた OpenVEX 文書です。 grype にローカル VEX ファイルとして渡す場合や、人間が全体を監査する場合に使います。
データは Cloudflare R2 の public custom domain から直接配信しています。
VEX の更新は日次の publish workflow が R2 objects を書き換えるだけで完了し、worker の deploy は必要ありません。
書き込み順序は、per-image の leaf data、aggregate、archive、index、.well-known manifest の順です。
最後に pointer を更新することで、途中の publish によって「index はあるが参照先 がない」という状態を見せないようにしています。
ステートメントの内容
OpenVEX の statement には、脆弱性 ID、対象 product、status、根拠を記録します。
脆弱性 ID は CVE に限られず、GHSA や OSV ID、ベンダー advisory ID なども同じ vulnerability.name に入ります。
justification は OpenVEX が定める機械可読な区分で、impact_statement や action_statement は読者が判断を検証するための説明です。
次は、公開中の busybox VEX から短縮した例です。
{
"vulnerability": {
"name": "CVE-2019-1010022",
"description": "glibc stack guard protection bypass -- DISPUTED by upstream."
},
"products": [
{
"@id": "pkg:oci/busybox@sha256:<index-digest>?repository_url=images.flatt.tech/takumi&tag=latest"
},
{
"@id": "pkg:oci/busybox?repository_url=images.flatt.tech/takumi"
}
],
"status": "not_affected",
"justification": "vulnerable_code_cannot_be_controlled_by_adversary",
"impact_statement": "Disputed by the glibc maintainers ..."
}
この例では、1つの statement に2つの product ID が入っています。 これは互換性のためです。
1つ目: digest に固定した canonical ID
pkg:oci/<image>@sha256:<index-digest>?repository_url=images.flatt.tech/takumi&tag=latest
これは署名付き attestation と同じ、監査用の基準 ID です。
どの image index に対する判断なのかを digest で固定します。
タグだけに依存しないため、後から latest が別の digest を指しても判断の対象は変わりません。
2つ目: grype 向けの ID
pkg:oci/<image>?repository_url=images.flatt.tech/takumi
grype が作る OCI PURL は、image 名を PURL の name とし、repository_url には registry と namespace だけを入れます。
さらに grype は per-arch manifest digest を version として扱うため、こちらも version を省略しています。
tag=latest は canonical ID にだけ残します。
grype が生成する PURL には tag qualifier が入らないため、grype との照合には2つ目の versionless ID を使います。
attestation として検証する
pull したイメージに紐づく VEX を検証する場合は、openvex attestation を取得します。
cosign verify-attestation \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
--certificate-identity-regexp 'https://github.com/flatt-security/takumi-images/.github/workflows/release.yml@.*' \
--type openvex \
images.flatt.tech/takumi/busybox:latest \
| jq -r '.payload' | base64 -d | jq '.predicate'
この検証で確認できるのは、取得した OpenVEX 文書が対象 digest に対して署名され、署名時点から改ざんされていないことです。 署名の identity や attestation 全般の検証方法は、Attestation を参照してください。
grype で使う
grype では aggregate 文書を取得し、--vex で渡します。
grype は VEX 文書を読むだけでは finding を ignore しないため、not_affected と fixed を抑制対象にする ignore rule も設定します。
affected は抑制してはいけません。
# grype-vex.yaml
ignore:
- vex-status: not_affected
- vex-status: fixed
curl -fsSLo takumi.openvex.json \
https://vex.images.flatt.tech/v1/all.openvex.json
grype registry:images.flatt.tech/takumi/busybox:latest-amd64 \
--vex takumi.openvex.json \
--config grype-vex.yaml \
--show-suppressed
この設定では、VEX が not_affected または fixed と判断した finding は suppressed として扱われます。
未修正として公開している affected は残るため、レポートから除外せずに対応判断の対象にしてください。
トリアージとの関係
VEX は、スキャン結果を見かけ上少なくするための後処理ではありません。 上流リリースへの追従、パッケージ単位のパッチ、実際に は該当しない検出結果の判断を区別し、それぞれを検証可能な形で残すための記録です。 このトリアージの考え方は、既知脆弱性への対応 で説明しています。