govulncheckで脆弱性スキャナーの偽陽性を削減する

この記事は、CYBOZU SUMMER BLOG FES '26の記事です。

Cloud Platform部のpddgです。 サプライチェーン攻撃やAIによる脆弱性の検出が盛んになった昨今、迅速な影響の把握と更新の適用が求められ、ソフトウェアが抱える依存関係の脆弱性の検出はその重要性が増してきています。一方で、ナイーブに脆弱性を検出するだけでは誤検知(偽陽性)が多くなり、開発者の負担が増えることになります。

Goのプロジェクトでは、govulncheckを用いることでモジュールレベルではなく関数のシンボルレベルで影響の有無を検出でき、偽陽性を削減できます。一般的なOSSスキャナーのスキャンとgovulncheckのスキャン結果を組み合わせることで、より正確な脆弱性検出が可能になります。

背景

ソフトウェアを構成する依存関係に存在する脆弱性を早く検知し、攻撃に繋がる前に早く対応することは現代の開発において重要性が増しています。TrivyやGrype、OSV-ScannerなどのOSSの脆弱性スキャナーを通して、依存関係に含まれる脆弱なライブラリ・モジュールを検知し、開発者に通知することは有効な手段です。これらのスキャナーはモジュールレベルでの検知を行うことが多く、実際に影響を及ぼすかどうかに関わらず脆弱性ありとして報告されることがあります。

実際にはあるライブラリの脆弱性が検知されたとき、そのライブラリのうちどのような機能を利用しているかによって影響を受ける箇所は変わってきます。静的解析により当該機能を利用しているかどうかを知ることができれば、より精度の高い脆弱性検知が可能なはずです。

govulncheck

Goは脆弱性管理のためのデータベースを持っています。

このデータベースを用いて、現在のGoのプロジェクト内・もしくはバイナリにおいて依存関係に既知の脆弱性を含むかどうかを調べるのが govulncheck コマンドです。

Goのコードもしくはビルドしたバイナリを対象に解析を行い、ソースコードの場合は脆弱性の見つかった機能への到達可能性まで見て影響を受けるかどうかを判定します。そのため、単に依存関係に脆弱性の見つかったライブラリが依存関係として存在するかどうかだけで判断するよりも偽陽性を抑えられます。

脆弱性スキャナーとgovulncheckの現在

ほとんどの脆弱性スキャナーは現状では依存関係の単位で脆弱性の有無を判定しています。多くの脆弱性データベースでは対象のモジュール内のどこに脆弱性を含むのかを確認できるレベルの情報を持たないためです。 govulncheckのような言語ネイティブな脆弱性スキャナーはTrivyやGrypeには統合されておらず、OSV-Scannerのソースコードに対するスキャン時に統合されているのみでした*1。

ではTrivyやGrypeは全くgovulncheckの恩恵を受けられないかというと、そうでもありません。Trivyでは以下のissueで2022年から議論が行われてきていました。

(1回方針が決まった後にまた最近議論が再燃していますが)最終的にgovulncheckが結果をOpenVEX形式で出力することをサポートした*2ことで、Trivyのスキャン結果に対してgovulncheckの結果を組み合わせることが可能になりました。直接的にはインテグレーションせず、govulncheckが出力したGoに特化した結果をTrivyが読み取り、Trivyが出力した結果とマージするという形です。

OpenVEXとは

VEXとはVulnerability Exploitability eXchangeの略で、脆弱性の影響を受けるかどうかの情報を表現するという概念です。このVEXの仕様は複数の組織が策定しており、OpenVEXはLinux Foundation傘下のOpenSSFが策定している仕様です。

プログラミング言語非依存で、コンポーネントに対して脆弱性が影響を受けるのか受けないのかといった情報を表現できます。 以下のリポジトリでその仕様が管理されています。 https://github.com/openvex/spec

基本的な構造としてはこんな感じのJSONになっています。以下は実際に今回govulncheckで出力した結果の一部です。

{
  // メタデータ
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "govulncheck/vex:f1fc940891127a0239bc833dddd66492ed005192c32e127037a7a5c4e42bb248",
  "author": "Unknown Author",
  "timestamp": "2026-08-03T02:55:38.512408915Z",
  "version": 1,
  "tooling": "https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck",
  // 脆弱性の影響を受けるかどうかの情報
  "statements": [
    {
      "vulnerability": {
        "@id": "https://pkg.go.dev/vuln/GO-2026-4970",
        "name": "GO-2026-4970",
        "description": "Root escape via symlink plus trailing slash in os",
        "aliases": [
          "CVE-2026-39822"
        ]
      },
      "products": [
        {
          "@id": "Unknown Product",
          "subcomponents": [{"@id": "pkg:golang/stdlib@v1.26.4"}]
        }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_in_execute_path",
      "impact_statement": "Govulncheck determined that the vulnerable code isn't called"
    },
  ]
}

ちゃんと not_affected である理由まで出力されていることがわかります。 このフォーマットはオープンな仕様であり、Trivyに限らず他の脆弱性スキャナーでもサポートされています。例えばGrypeでもOpenVEX形式の結果を読み込むことができ、Grypeのスキャン結果に対してgovulncheckの結果を組み合わせることが可能です。

実際にやってみる

今回は以下のリポジトリ内のデモ実装を利用します。 https://github.com/pddg/govulncheck-integrate-demo

実際に脆弱性のある機能の呼び出しを含む場合の実装として cmd/true-positive を、含まない場合の実装として cmd/false-positive を用意しています。 今回は golang.org/x/net/html で判明した脆弱性を検知することを想定しています。

true-positiveの実装では、実際に脆弱性の影響を受ける html.Parse を呼び出すコードが含まれています。

package main

import (
    "strings"

    "golang.org/x/net/html"
)

func main() {
    // Vulnerable
    html.Parse(strings.NewReader("<div>hello</div>"))
}

一方、false-positiveの実装では、同じバージョンの golang.org/x/net を依存関係としてgo.modに含むものの、脆弱性の影響を受ける関数は呼び出していません。

package main

import (
    "fmt"

    "golang.org/x/net/html"
)

func main() {
    // Not vulnerable
    fmt.Println(html.EscapeString("<hello>"))
}

この状態でTrivyによるソースコードのスキャンを行うと、両方の実装で脆弱性が検知されます(true-positiveの方の出力は同等なので省略)。

❯ trivy fs ./cmd/false-positive -q

Report Summary

┌────────┬───────┬─────────────────┬─────────┐
│ Target │ Type  │ Vulnerabilities │ Secrets │
├────────┼───────┼─────────────────┼─────────┤
│ go.mod │ gomod │        7        │    -    │
└────────┴───────┴─────────────────┴─────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)


go.mod (gomod)

Total: 7 (UNKNOWN: 1, LOW: 0, MEDIUM: 3, HIGH: 3, CRITICAL: 0)

┌──────────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐
│     Library      │ Vulnerability  │ Severity │ Status │ Installed Version │ Fixed Version │                            Title                             │
├──────────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ golang.org/x/net │ CVE-2026-25681 │ HIGH     │ fixed  │ v0.54.0           │ 0.55.0        │ golang.org/x/net/html: golang.org/x/net/html: Arbitrary code │
│                  │                │          │        │                   │               │ execution via Cross-Site Scripting                           │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-25681                   │
│                  ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-27136 │          │        │                   │               │ golang.org/x/net/html: golang: golang.org/x/net/html:        │
│                  │                │          │        │                   │               │ Cross-Site Scripting via HTML parsing bypass                 │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-27136                   │
│                  ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-39821 │          │        │                   │               │ golang.org/x/net/idna: golang: net/http:                     │
│                  │                │          │        │                   │               │ golang.org/x/net/idna: Privilege escalation via incorrect    │
│                  │                │          │        │                   │               │ Punycode label processing                                    │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-39821                   │
│                  ├────────────────┼──────────┤        │                   │               ├──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-25680 │ MEDIUM   │        │                   │               │ golang.org/x/net/html: golang.org/x/net/html: Denial of      │
│                  │                │          │        │                   │               │ Service due to excessive HTML parsing                        │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-25680                   │
│                  ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-42502 │          │        │                   │               │ golang.org/x/net/html: golang: golang.org/x/net/html:        │
│                  │                │          │        │                   │               │ Cross-Site Scripting via unexpected HTML tree rendering      │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-42502                   │
│                  ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-42506 │          │        │                   │               │ golang.org/x/net/html: golang.org/x/net/html: Cross-Site     │
│                  │                │          │        │                   │               │ Scripting (XSS) via arbitrary HTML parsing                   │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-42506                   │
│                  ├────────────────┼──────────┤        │                   ├───────────────┼──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-46600 │ UNKNOWN  │        │                   │ 0.56.0        │ Parsing an invalid SVCB or HTTPS RR can panic when the       │
│                  │                │          │        │                   │               │ size...                                                      │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-46600                   │
└──────────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┴──────────────────────────────────────────────────────────────┘

シンプルにgovulncheckの出力を使う

govulncheckでOpenVEX形式の出力を得るには、-format openvex というオプションを付けます。

❯ govulncheck -C ./cmd/false-positive -format openvex . | tee vex/source/false-positive.raw.json
{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "govulncheck/vex:f1fc940891127a0239bc833dddd66492ed005192c32e127037a7a5c4e42bb248",
  "author": "Unknown Author",
  "timestamp": "2026-08-04T02:14:43.286694211Z",
  "version": 1,
  "tooling": "https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck",
  "statements": [
...

❯ govulncheck -C ./cmd/true-positive -format openvex . | tee vex/source/true-positive.raw.json

.statements[].statusを見ると、false-positiveでは全て not_affected であり、true-positiveでは affected も含まれています。

❯ jq -r '.statements[].status' vex/source/false-positive.raw.json 
not_affected
not_affected
not_affected
not_affected
not_affected
not_affected
not_affected
not_affected
not_affected

❯ jq -r '.statements[].status' vex/source/true-positive.raw.json 
not_affected
affected
not_affected
affected
affected
affected
affected
not_affected
not_affected

ではこのファイルをTrivyの出力結果にマージしてみましょう。Trivyは --vex というオプションでVEXファイルを指定します。想定通りであれば、false-positiveに対するスキャン結果で全脆弱性の検知が抑制されるはずです。

❯ trivy fs -q --vex vex/source/false-positive.raw.json ./cmd/false-positive

Report Summary

┌────────┬───────┬─────────────────┬─────────┐
│ Target │ Type  │ Vulnerabilities │ Secrets │
├────────┼───────┼─────────────────┼─────────┤
│ go.mod │ gomod │        7        │    -    │
└────────┴───────┴─────────────────┴─────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)


go.mod (gomod)

Total: 7 (UNKNOWN: 1, LOW: 0, MEDIUM: 3, HIGH: 3, CRITICAL: 0)

┌──────────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐
│     Library      │ Vulnerability  │ Severity │ Status │ Installed Version │ Fixed Version │                            Title                             │
├──────────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤
│ golang.org/x/net │ CVE-2026-25681 │ HIGH     │ fixed  │ v0.54.0           │ 0.55.0        │ golang.org/x/net/html: golang.org/x/net/html: Arbitrary code │
│                  │                │          │        │                   │               │ execution via Cross-Site Scripting                           │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-25681                   │
│                  ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-27136 │          │        │                   │               │ golang.org/x/net/html: golang: golang.org/x/net/html:        │
│                  │                │          │        │                   │               │ Cross-Site Scripting via HTML parsing bypass                 │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-27136                   │
│                  ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-39821 │          │        │                   │               │ golang.org/x/net/idna: golang: net/http:                     │
│                  │                │          │        │                   │               │ golang.org/x/net/idna: Privilege escalation via incorrect    │
│                  │                │          │        │                   │               │ Punycode label processing                                    │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-39821                   │
│                  ├────────────────┼──────────┤        │                   │               ├──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-25680 │ MEDIUM   │        │                   │               │ golang.org/x/net/html: golang.org/x/net/html: Denial of      │
│                  │                │          │        │                   │               │ Service due to excessive HTML parsing                        │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-25680                   │
│                  ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-42502 │          │        │                   │               │ golang.org/x/net/html: golang: golang.org/x/net/html:        │
│                  │                │          │        │                   │               │ Cross-Site Scripting via unexpected HTML tree rendering      │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-42502                   │
│                  ├────────────────┤          │        │                   │               ├──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-42506 │          │        │                   │               │ golang.org/x/net/html: golang.org/x/net/html: Cross-Site     │
│                  │                │          │        │                   │               │ Scripting (XSS) via arbitrary HTML parsing                   │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-42506                   │
│                  ├────────────────┼──────────┤        │                   ├───────────────┼──────────────────────────────────────────────────────────────┤
│                  │ CVE-2026-46600 │ UNKNOWN  │        │                   │ 0.56.0        │ Parsing an invalid SVCB or HTTPS RR can panic when the       │
│                  │                │          │        │                   │               │ size...                                                      │
│                  │                │          │        │                   │               │ https://avd.aquasec.com/nvd/cve-2026-46600                   │
└──────────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┴──────────────────────────────────────────────────────────────┘

あれぇ……変わりませんでした……

govulncheckが出力するコンポーネント識別子

問題はそれぞれのツールが異なる識別子で情報を扱っているところにあります。VEXファイルは、

  • どのコンポーネントの
  • どのサブコンポーネントが
  • どの脆弱性の影響を受けるか

というフォーマットで書かれています。正しく偽陽性を抑制するには、この「どのコンポーネント」「どのサブコンポーネント」という部分において、govulncheckとTrivyが同じ解釈をしている必要があります。

まずgovulncheckの出力を見てみます。コンポーネント、サブコンポーネントの識別子は .statements[].product["@id"] などに含まれます。

❯ jq '.statements[].products[] | {product_id: .["@id"], subcomponents: [.subcomponents[]["@id"]]}' vex/source/false-positive.raw.json
{
  "product_id": "Unknown Product",
  "subcomponents": [
    "pkg:golang/stdlib@v1.26.4"
  ]
}
{
  "product_id": "Unknown Product",
  "subcomponents": [
    "pkg:golang/golang.org%2Fx%2Fnet@v0.54.0"
  ]
}
... 以下略

サブコンポーネントの名前は出ていますが、ProductのIDは v1.6.0 現在デフォルトで Unknown Product に固定されており、変更できません。

vuln/internal/openvex/handler.go at 19b0bb6a272792b9afa8a6983c3e9b9a1816947f · golang/vuln · GitHub

vuln/internal/openvex/vex.go at 19b0bb6a272792b9afa8a6983c3e9b9a1816947f · golang/vuln · GitHub

この問題はissueとして報告されています*3が進展は全くありません。

Trivyが認識しているコンポーネント識別子

一方、Trivyではこれがどのように認識されているのかを確認しましょう。Trivyのドキュメントを確認します。

https://trivy.dev/docs/v0.72/guide/supply-chain/vex/file/#purl-matching

In the context of VEX, Package URLs (PURLs) are utilized to identify specific software packages and their versions.

PURLという形式で識別します。これは以下のリポジトリで管理されている仕様です。

実際にTrivyの出力するSBOMに今回対象としたコンポーネントのPURLが含まれているので、確認してみます。

❯ trivy fs ./cmd/false-positive -q --format cyclonedx | jq -r '.components[] | .purl?'
null
pkg:golang/github.com/pddg/govulncheck-integrate-demo/cmd/false-positive
pkg:golang/golang.org/x/net@v0.54.0

GoのモジュールのPURL表現は pkg:golang/ というprefixに、 module@version という形式で形成されるようです。

govulncheckの出力を修正する

govulncheckの素の出力と、Trivyが認識する識別子(PURL)の間には2つの問題があることが分かりました。

  • メインモジュールのidが一致しない
    • govulncheck: Unknown Product
    • Trivy: pkg:golang/github.com/pddg/govulncheck-integrate-demo/cmd/false-positive
  • サブコンポーネントのidが一致しない
    • govulncheck: pkg:golang/golang.org%2Fx%2Fnet@v0.54.0
    • Trivy: pkg:golang/golang.org/x/net@v0.54.0

前者の問題は見たままの問題ですが、後者も実は問題です。GoにおけるPURL表現はまだ改善の提案が続いており、govulncheckはpurl-specにまだマージされていない仕様に、しかも中途半端に則っています。

上記のissueで言及されているpurl-specのPR(#338) では、以下のようなフォーマットにすることを提案しています。

  • prefixは pkg:go/
  • モジュールに含まれる / はエスケープする
  • ?goos=linux などのメタデータを付けられるようにする
  • つまり pkg:go/golang.org%2Fx%2Fvuln@v1.1.3?goos=linux#cmd/govulncheck

現在のgovulncheckの出力ではこのうち / をエスケープするという部分だけが実装されています*4……なぜ……

幸いちょっと加工してあげればこれについては解決できそうです。試しに以下のようにしてgovulncheckの出力に手を入れてみます。

❯ govulncheck -C ./cmd/false-positive -format openvex . \
  | jq '.statements?[].products?[] |= if .["@id"] == "Unknown Product" then .["@id"] = "pkg:golang/github.com/pddg/govulncheck-integrate-demo/cmd/false-positive" else . end' \
  | sed -e 's/%2F/\//g' \
  > vex/source/false-positive.json

❯ jq '.statements[].products[] | {product_id: .["@id"], subcomponents: [.subcomponents[]["@id"]]}' vex/source/false-positive.json    
{
  "product_id": "pkg:golang/github.com/pddg/govulncheck-integrate-demo/cmd/false-positive",
  "subcomponents": [
    "pkg:golang/stdlib@v1.26.4"
  ]
}
{
  "product_id": "pkg:golang/github.com/pddg/govulncheck-integrate-demo/cmd/false-positive",
  "subcomponents": [
    "pkg:golang/golang.org/x/net@v0.54.0"
  ]
}
... 以下略

これでTrivyが認識する識別子と一致するはずです。

実際に抑制されることを確認する

同じようにtrue-positiveでもVEXファイルを生成し、VEXなしの場合も含めてtrivyの出力結果を見てみます。

VEXなし(true-positive・false-positiveで共通)

❯ trivy fs -q ./cmd/true-positive --table-mode summary                                    

Report Summary

┌────────┬───────┬─────────────────┬─────────┐
│ Target │ Type  │ Vulnerabilities │ Secrets │
├────────┼───────┼─────────────────┼─────────┤
│ go.mod │ gomod │        7        │    -    │
└────────┴───────┴─────────────────┴─────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)

VEXあり(true-positive)

❯ trivy fs --vex vex/source/true-positive.json -q ./cmd/true-positive --table-mode summary  

Report Summary

┌────────┬───────┬─────────────────┬─────────┐
│ Target │ Type  │ Vulnerabilities │ Secrets │
├────────┼───────┼─────────────────┼─────────┤
│ go.mod │ gomod │        5        │    -    │
└────────┴───────┴─────────────────┴─────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)

VEXあり(false-positive)

❯ trivy fs --vex vex/source/false-positive.json -q ./cmd/false-positive --table-mode summary 

Report Summary

┌────────┬───────┬─────────────────┬─────────┐
│ Target │ Type  │ Vulnerabilities │ Secrets │
├────────┼───────┼─────────────────┼─────────┤
│ go.mod │ gomod │        0        │    -    │
└────────┴───────┴─────────────────┴─────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)

true-positiveでも無関係な脆弱性2件は抑制され、false-positiveに至っては全件影響なしという結果が得られました*5。

イメージスキャンとの接続

ソースコードレベルでのスキャンではなく、本番環境で実行されているコンテナイメージに対するスキャンを行い、検出された脆弱性に対する警告を行うということがままあるのではないかと思います。 この場合でもgovulncheckが出力するVEXファイルを用いて偽陽性を抑制できます。 ただし、Goでコンパイルしたコンポーネントを含むコンテナイメージでは、scratchやdistrolessイメージの中にバイナリだけをコピーしておく手法が主流です。そのため、govulncheckのソースコード解析をそのまま流用できるとは限りません。

TrivyやGrypeなどがイメージに含まれるGoのバイナリをスキャンするように、govulncheckもバイナリを解析してVEXファイルを出力できます。今回は以下のようなケースを想定し、govulncheckでバイナリをスキャンして得られたVEXファイルを用いてイメージスキャン結果をフィルタリングします。

  • コンテナイメージにはGoのバイナリのみが含まれ、ソースコードは存在しない
  • ソースコードには容易にはアクセスできず、コンテナイメージ本体・脆弱性スキャナー・govulncheck・dockerなど何らかのOCIイメージ操作ツールのみが利用可能である。

バイナリをgovulncheckでスキャン

まずはコンテナイメージを用意します。デモで用いているリポジトリにビルドするための bake.hcl があるのでこれを使ってDocker bakeでビルドします。

❯ docker buildx bake -f bake.hcl

次に、スキャンする対象のバイナリをイメージ内から取り出します。今回はpushしていないイメージを対象とするため、 docker buildx build --output=path/to/out を使います。 --output オプションを使うことで、コンテナイメージの中身をローカルファイルシステム上に全て展開できます。リモートに存在するイメージを使う場合は crane export などが便利です*6。 一つのDockerfileでステージを分けてイメージを作っているので、true-positive/false-positiveそれぞれで出力します。

❯ docker buildx build --output=bin --target=true-positive .
❯ docker buildx build --output=bin --target=false-positive .
❯ ls bin/   
false-positive  true-positive
❯ file bin/false-positive 
bin/false-positive: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=_zwajW1HOeQ4NOKBD5PK/DDbDU-q78iwTtMFLb8jt/c0FKIcCY1wrCXkpA61Tj/MPezm3KFIR-WUVL778Jj, BuildID[sha1]=9a848ef902c62b59d4a0ef9cfbc6b972bbdd4f99, with debug_info, not stripped

こうして得られたバイナリに対してgovulncheckを実行します。 -mode binary を付けることで、バイナリモードでのスキャンになります。

❯ govulncheck -mode binary -format openvex ./bin/true-positive \
  | jq '.statements?[].products?[] |= if .["@id"] == "Unknown Product" then .["@id"] = "pkg:golang/github.com/pddg/govulncheck-integrate-demo/cmd/true-positive" else . end' \
  | sed -e 's/%2F/\//g' \
  > vex/binary/true-positive.json
❯ govulncheck -mode binary -format openvex ./bin/false-positive \
  | jq '.statements?[].products?[] |= if .["@id"] == "Unknown Product" then .["@id"] = "pkg:golang/github.com/pddg/govulncheck-integrate-demo/cmd/false-positive" else . end' \
  | sed -e 's/%2F/\//g' \
  > vex/binary/false-positive.json

> -mode=binaryの差分(クリックして展開)

--- vex/source/false-positive.json      2026-08-04 02:56:16.376053087 +0000
+++ vex/binary/false-positive.json      2026-08-04 04:16:25.387458474 +0000
@@ -1,36 +1,13 @@
 {
   "@context": "https://openvex.dev/ns/v0.2.0",
-  "@id": "govulncheck/vex:f1fc940891127a0239bc833dddd66492ed005192c32e127037a7a5c4e42bb248",
+  "@id": "govulncheck/vex:43d713b5ea18afa8548793af71ca3c4ec3e3d939d92b1fb9ae698fc006d05db8",
   "author": "Unknown Author",
-  "timestamp": "2026-08-04T02:56:16.363228271Z",
+  "timestamp": "2026-08-04T04:16:25.387255427Z",
   "version": 1,
   "tooling": "https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck",
   "statements": [
     {
       "vulnerability": {
-        "@id": "https://pkg.go.dev/vuln/GO-2026-4970",
-        "name": "GO-2026-4970",
-        "description": "Root escape via symlink plus trailing slash in os",
-        "aliases": [
-          "CVE-2026-39822"
-        ]
-      },
-      "products": [
-        {
-          "@id": "pkg:golang/github.com/pddg/govulncheck-integrate-demo/cmd/false-positive",
-          "subcomponents": [
-            {
-              "@id": "pkg:golang/stdlib@v1.26.4"
-            }
-          ]
-        }
-      ],
-      "status": "not_affected",
-      "justification": "vulnerable_code_not_in_execute_path",
-      "impact_statement": "Govulncheck determined that the vulnerable code isn't called"
-    },
-    {
-      "vulnerability": {
         "@id": "https://pkg.go.dev/vuln/GO-2026-5025",
         "name": "GO-2026-5025",
         "description": "Invoking incorrect handling of namespaced elements in foreign content in golang.org/x/net/html",
@@ -168,29 +145,6 @@
       "impact_statement": "Govulncheck determined that the vulnerable code isn't called"
     },
     {
-      "vulnerability": {
-        "@id": "https://pkg.go.dev/vuln/GO-2026-5856",
-        "name": "GO-2026-5856",
-        "description": "Invoking Encrypted Client Hello privacy leak in crypto/tls",
-        "aliases": [
-          "CVE-2026-42505"
-        ]
-      },
-      "products": [
-        {
-          "@id": "pkg:golang/github.com/pddg/govulncheck-integrate-demo/cmd/false-positive",
-          "subcomponents": [
-            {
-              "@id": "pkg:golang/stdlib@v1.26.4"
-            }
-          ]
-        }
-      ],
-      "status": "not_affected",
-      "justification": "vulnerable_code_not_present",
-      "impact_statement": "Govulncheck determined that the vulnerable code isn't called"
-    },
-    {
       "vulnerability": {
         "@id": "https://pkg.go.dev/vuln/GO-2026-5942",
         "name": "GO-2026-5942",
--- vex/source/true-positive.json       2026-08-03 02:55:35.529863811 +0000
+++ vex/binary/true-positive.json       2026-08-04 04:16:13.907384041 +0000
@@ -1,36 +1,13 @@
 {
   "@context": "https://openvex.dev/ns/v0.2.0",
-  "@id": "govulncheck/vex:b3ba2dae3ca9103a981804ec952e3cc406998b159d8b013c769b5ef23d20063a",
+  "@id": "govulncheck/vex:a999d6041943a2b7b70994591c9e6eff01e09b801067a15972e96c55d4f4d10e",
   "author": "Unknown Author",
-  "timestamp": "2026-08-03T02:55:35.519744688Z",
+  "timestamp": "2026-08-04T04:16:13.910305431Z",
   "version": 1,
   "tooling": "https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck",
   "statements": [
     {
       "vulnerability": {
-        "@id": "https://pkg.go.dev/vuln/GO-2026-4970",
-        "name": "GO-2026-4970",
-        "description": "Root escape via symlink plus trailing slash in os",
-        "aliases": [
-          "CVE-2026-39822"
-        ]
-      },
-      "products": [
-        {
-          "@id": "pkg:golang/github.com/pddg/govulncheck-integrate-demo/cmd/true-positive",
-          "subcomponents": [
-            {
-              "@id": "pkg:golang/stdlib@v1.26.4"
-            }
-          ]
-        }
-      ],
-      "status": "not_affected",
-      "justification": "vulnerable_code_not_in_execute_path",
-      "impact_statement": "Govulncheck determined that the vulnerable code isn't called"
-    },
-    {
-      "vulnerability": {
         "@id": "https://pkg.go.dev/vuln/GO-2026-5025",
         "name": "GO-2026-5025",
         "description": "Invoking incorrect handling of namespaced elements in foreign content in golang.org/x/net/html",
@@ -159,29 +136,6 @@
     },
     {
       "vulnerability": {
-        "@id": "https://pkg.go.dev/vuln/GO-2026-5856",
-        "name": "GO-2026-5856",
-        "description": "Invoking Encrypted Client Hello privacy leak in crypto/tls",
-        "aliases": [
-          "CVE-2026-42505"
-        ]
-      },
-      "products": [
-        {
-          "@id": "pkg:golang/github.com/pddg/govulncheck-integrate-demo/cmd/true-positive",
-          "subcomponents": [
-            {
-              "@id": "pkg:golang/stdlib@v1.26.4"
-            }
-          ]
-        }
-      ],
-      "status": "not_affected",
-      "justification": "vulnerable_code_not_present",
-      "impact_statement": "Govulncheck determined that the vulnerable code isn't called"
-    },
-    {
-      "vulnerability": {
         "@id": "https://pkg.go.dev/vuln/GO-2026-5942",
         "name": "GO-2026-5942",
         "description": "Parsing an invalid SVCB or HTTPS RR can panic in golang.org/x/net/dns/dnsmessage",

ちなみにビルド時に -ldflags '-s -w' などを付与してシンボル情報を削除していると、govulncheckが必要とするモジュール内のシンボルの情報を得られません。結果として、脆弱性ありと判定されたモジュールを依存に含むだけで、その全ての脆弱性の影響を受けうる( affected )として判定します。 バイナリスキャンを使いたい場合、これらのオプションは付与しないことをおすすめします。

イメージをスキャン

同様にVEXありなしなどで結果が変わるかを見ます。

VEXなし(true-positive・false-positiveで共通)

❯ trivy image --table-mode summary -q govulncheck-integrate-demo/true-positive:latest

Report Summary

┌───────────────┬──────────┬─────────────────┬─────────┐
│    Target     │   Type   │ Vulnerabilities │ Secrets │
├───────────────┼──────────┼─────────────────┼─────────┤
│ true-positive │ gobinary │        7        │    -    │
└───────────────┴──────────┴─────────────────┴─────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)

VEXあり(true-positive)

❯ trivy image --table-mode summary -q --vex vex/binary/true-positive.json govulncheck-integrate-demo/true-positive:latest

Report Summary

┌───────────────┬──────────┬─────────────────┬─────────┐
│    Target     │   Type   │ Vulnerabilities │ Secrets │
├───────────────┼──────────┼─────────────────┼─────────┤
│ true-positive │ gobinary │        5        │    -    │
└───────────────┴──────────┴─────────────────┴─────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)

VEXあり(false-positive)

❯ trivy image --table-mode summary -q --vex vex/binary/false-positive.json govulncheck-integrate-demo/false-positive:latest

Report Summary

┌────────────────┬──────────┬─────────────────┬─────────┐
│     Target     │   Type   │ Vulnerabilities │ Secrets │
├────────────────┼──────────┼─────────────────┼─────────┤
│ false-positive │ gobinary │        0        │    -    │
└────────────────┴──────────┴─────────────────┴─────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)

今回はイメージスキャン+バイナリスキャンの場合でもソースコードレベルでのスキャンと同様の結果を得ました。govulncheckを併用することで、Goバイナリのイメージで検出される脆弱性について大幅に偽陽性を抑制できる可能性があります。

Grypeのイメージスキャンでも今回用いたVEXファイルを使って偽陽性を抑制できることが分かっています。OSV-ScannerはVEXファイルを使うオプションがなく、現状イメージスキャンでは対応できないようでした。

更なる統合を目指して

現在は人が介入してバイナリを取り出し、govulncheckにかけ、結果をフィルタリングしています。しかし、毎回これをやろうとするのは大変です。スキャナに完全に取り込まれれば嬉しいですが、多数のバイナリを含む場合などにはスキャンにかかる時間が膨大になってしまう懸念もあり、容易には進まないかもしれません。

そこで、ここではどういうことができそうかだけ軽く共有しておこうかなと思います。まず、イメージスキャンして得られたSBOMの中から、Goのバイナリの一覧を入手できます。

❯ trivy image --format cyclonedx -q govulncheck-integrate-demo/false-positive:latest \
  | jq -r '.components[] 
    | select(.properties[]? | .name=="aquasecurity:trivy:Type" and .value=="gobinary") 
    | .name'
false-positive

これにより、そのイメージ中でgovulncheckをかけるべきバイナリが分かります。よって以下のような手順を取れば、コンテナイメージスキャンからgovulncheckによる偽陽性の抑制を自動化できるはずです。

  1. コンテナイメージをスキャンする
  2. イメージ内に含まれるGoバイナリのパスを特定する
  3. govulncheckで2をスキャンしてVEXファイルを生成
  4. 3のファイルを用いて結果を抑制

こういった仕組みを実際に落とし込んだものとしてDataDog/trivy-plugin-govulncheckもあります*7。

おわりに

govulncheckとTrivyを代表とする脆弱性スキャナーのインテグレーションについて解説してきました。まだ何も知らなくてもすんなりgovulncheckが有効に使えるという状態にはなく、活用までには大きなギャップがあるという感想です。

この記事の内容自体、1年半前に調べた内容をベースに書き始めたのですが、govulncheckによるsubcomponentsのPURL形式の変更などギャップは更に増える方向になってしまっていました。 各種issueもあまり活発とは言えず、今後の方向性を少し心配しています。

脆弱性スキャナーは問題検出において非常に有効である反面、偽陽性の抑制をしないと「アラート疲れ」ならぬ「脆弱性疲れ」を引き起こしてしまいます。脆弱性スキャナーを有効に活用するために、こういった現実的なフィルタリング手法は常に考えていきたいところです。この記事が脆弱性と戦う方々の、何かの役に立てば幸いです。


*1:OSV-Scanner v1.3.0から実装されているようです https://github.com/google/osv-scanner/blob/main/CHANGELOG.md#v130

*2:https://github.com/golang/go/issues/62486

*3:x/vuln: OpenVEX report lacks affected product · Issue #68152 · golang/go · GitHub

*4:vuln/internal/openvex/purl.go at v1.6.0 · golang/vuln · GitHub

*5:--show-suppressedというオプションを付与すると、検出されたが抑制された脆弱性の一覧を確認できます

*6:https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane_export.md

*7:試した限りtrivy-plugin-govulncheckはローカルのDockerイメージにはアクセスできず、必ずpullしようとしました。大規模に実行すると大量にpullしてしまう可能性があります。