👾

govulncheckで検出された脆弱性をGoのプログラムで扱う

2024/12/09に公開

この記事は Go Advent Calendar 2024(シーズン2) 9日目です。

https://qiita.com/advent-calendar/2024/go


govulncheckとは

Goの脆弱性を検出してくれるツールです。
アプリケーションに影響を与える可能性のある脆弱性だけにレポートしてくれて
レポートには脆弱性が修正されているバージョン、脆弱性のあるコードを呼び出しているコード行も表示してくれます。
便利😀

https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck

チュートリアルもあります

https://go.dev/doc/tutorial/govulncheck

デフォルトの出力

以下の記事でgovulncheckのオプション毎の出力結果を書きました。

https://zenn.dev/masakurapa/articles/8f6f50376c6575

脆弱性を検出するだけの用途の場合、以下のようなデフォルトの出力結果でかなり見やすいので特に処理する必要はないと思います。

$ govulncheck ./...
=== Symbol Results ===

Vulnerability #1: GO-2024-3250
    Improper error handling in ParseWithClaims and bad documentation may cause
    dangerous situations in github.com/golang-jwt/jwt
  More info: https://pkg.go.dev/vuln/GO-2024-3250
  Module: github.com/golang-jwt/jwt/v4
    Found in: github.com/golang-jwt/jwt/v4@v4.5.0
    Fixed in: github.com/golang-jwt/jwt/v4@v4.5.1
    Example traces found:
      #1: mod1.go:12:28: mod1.Run calls jwt.ParseWithClaims

Your code is affected by 1 vulnerability from 1 module.
This scan found no other vulnerabilities in packages you import or modules you
require.
Use '-show verbose' for more details.

ただしこの出力結果をプログラムで扱うのはちょっと辛いです😓
-formatオプションを指定するとJSONで出力結果を処理できるようになるため
sarif, openvex, jsonそれぞれの形式の出力結果をGoで処理してみようと思います😀

オプション別のコード

-format=sarifオプションの場合

この形式で取得できる情報

  • 脆弱性が検出されたモジュール名、バージョン
  • 脆弱性の詳細が書かれたURL
  • どこで脆弱性が存在するコードを呼び出しているか、というトレース情報

https://github.com/owenrumney/go-sarif に型定義が存在するため
このモジュールのsarif.Open()呼び出すだけなので非常に簡単です😀

だいぶ端折って書いてますがStacksの中身を解析していくと必要な情報が集められます。
以下のコードでは脆弱性が検出されたモジュール名のみを抽出しています。

package main

import (
	"bytes"
	"fmt"

	"github.com/owenrumney/go-sarif/v2/sarif"
)

func main() {
	report, _ := sarif.Open("format_sarif.json")

	out := &bytes.Buffer{}
	for _, run := range report.Runs {
		for _, ret := range run.Results {
			if *ret.Level != "error" {
				continue
			}

			// Frames[0] には脆弱性のあるコードを呼び出しているモジュール名が入っている想定
			// Frames[1] には呼び出し先(脆弱性が存在する)のモジュール名が入っている想定
			fmt.Fprintf(out, "モジュール %q で脆弱性が見つかりましたよ!!\n", *ret.Stacks[0].Frames[1].Module)
		}
	}

	fmt.Print(out.String())
}

出力は以下のようになります。

$ go run .
モジュール "github.com/golang-jwt/jwt/v4@v4.5.0" で脆弱性が見つかりましたよ!!

-format=openvexオプションの場合

この形式で取得できる情報

  • 脆弱性が検出されたモジュール
  • 脆弱性の詳細が書かれたURL

https://github.com/openvex/go-vex に型定義が存在するため
このモジュールのvex.Open()呼び出すだけなのでこちらも非常に簡単です😀

この形式の場合、モジュール名などは出力に含まれないのでIDに入っているURLで詳細を確認する必要がありそうです。

package main

import (
	"bytes"
	"fmt"

	"github.com/openvex/go-vex/pkg/vex"
)

func main() {
	doc, _ := vex.Open("format_openvex.json")

	out := &bytes.Buffer{}
	for _, statement := range doc.Statements {
		v := statement.Vulnerability
		fmt.Fprintf(out, "%s\n詳細はこちら -> %s\n", v.Description, v.ID)
	}

	fmt.Print(out.String())
}

出力は以下のようになります。

$ go run .
Improper error handling in ParseWithClaims and bad documentation may cause dangerous situations in github.com/golang-jwt/jwt
詳細はこちら -> https://pkg.go.dev/vuln/GO-2024-3250

-format=jsonオプションの場合

この形式で取得できる情報

  • 脆弱性が検出されたモジュール名、バージョン
  • 脆弱性が修正されているバージョン
  • 脆弱性の詳細が書かれたURL
  • どこで脆弱性が存在するコードを呼び出しているか、というトレース情報

この形式の出力結果は config, progress, osv, finding 毎に別々のJSONが出力されます。
そのためこのままではプログラムで扱うのは難しそうです。

$ govulncheck -format=json ./...
{
  "config": {
    "protocol_version": "v1.0.0",
    "scanner_name": "govulncheck",
    "scanner_version": "v1.1.3",
    "db": "https://vuln.go.dev",
    "db_last_modified": "2024-11-27T19:16:39Z",
    "go_version": "go1.23.3",
    "scan_level": "symbol",
    "scan_mode": "source"
  }
}
{
  "progress": {
    "message": "Scanning your code and 112 packages across 2 dependent modules for known vulnerabilities..."
  }
}
... 略

プログラムで処理しやすいように改行等を取り除きます。
jq -c '.' で再変換し直すと以下のような形になります。
この状態であれば1行ずつ読み取って処理することが可能そうです。

{"config":{"protocol_version":"v1.0.0","scanner_name":"govulncheck","scanner_version":"v1.1.3","db":"https://vuln.go.dev","db_last_modified":"2024-11-27T19:16:39Z","go_version":"go1.23.3","scan_level":"symbol","scan_mode":"source"}}
{"progress":{"message":"Scanning your code and 112 packages across 2 dependent modules for known vulnerabilities..."}}
{"progress":{"message":"Fetching vulnerabilities from the database..."}}
{"osv":{"schema_version":"1.3.1","id":"GO-2024-3250","modified":"2024-11-12T14:50:10Z","published":"2024-11-12T13:55:08Z","aliases":["CVE-2024-51744","GHSA-29wx-vh33-7x7r"],"summary":"Improper error handling in ParseWithClaims and bad documentation may cause dangerous situations in github.com/golang-jwt/jwt","details":"Improper error handling in ParseWithClaims and bad documentation may cause dangerous situations in github.com/golang-jwt/jwt","affected":[{"package":{"name":"github.com/golang-jwt/jwt/v4","ecosystem":"Go"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"4.5.1"}]}],"ecosystem_specific":{"imports":[{"path":"github.com/golang-jwt/jwt/v4","symbols":["Parse","ParseWithClaims","Parser.Parse","Parser.ParseWithClaims"]}]}}],"references":[{"type":"ADVISORY","url":"https://github.com/golang-jwt/jwt/security/advisories/GHSA-29wx-vh33-7x7r"},{"type":"FIX","url":"https://github.com/golang-jwt/jwt/commit/7b1c1c00a171c6c79bbdb40e4ce7d197060c1c2c"}],"database_specific":{"url":"https://pkg.go.dev/vuln/GO-2024-3250","review_status":"REVIEWED"}}}
~~~中略(osvが大量に出力されます)~~~
{"progress":{"message":"Checking the code against the vulnerabilities..."}}
~~~中略(findingが1~N行出力されます)~~~
{"finding":{"osv":"GO-2024-3250","fixed_version":"v4.5.1","trace":[{"module":"github.com/golang-jwt/jwt/v4","version":"v4.5.0","package":"github.com/golang-jwt/jwt/v4","function":"ParseWithClaims","position":{"filename":"token.go","offset":4856,"line":113,"column":6}},{"module":"sample/mod1","package":"sample/mod1","function":"Run","position":{"filename":"mod1.go","offset":212,"line":12,"column":28}}]}}

このJSONをまとめて処理する構造体は以下のようになります。

type JsonFormat struct {
	Config   *Config   `json:"config,omitempty"`
	Progress *Progress `json:"progress,omitempty"`
	Osv      *Osv      `json:"osv,omitempty"`
	Finding  *Finding  `json:"finding,omitempty"`
}

type Config struct {
	ProtocolVersion string    `json:"protocol_version"`
	ScannerName     string    `json:"scanner_name"`
	ScannerVersion  string    `json:"scanner_version"`
	Db              string    `json:"db"`
	DbLastModified  time.Time `json:"db_last_modified"`
	GoVersion       string    `json:"go_version"`
	ScanLevel       string    `json:"scan_level"`
	ScanMode        string    `json:"scan_mode"`
}

type Progress struct {
	Message string `json:"message"`
}

type Osv struct {
	SchemaVersion string    `json:"schema_version"`
	Id            string    `json:"id"`
	Modified      time.Time `json:"modified"`
	Published     time.Time `json:"published"`
	Aliases       []string  `json:"aliases"`
	Summary       string    `json:"summary"`
	Details       string    `json:"details"`
	Affected      []struct {
		Package struct {
			Name      string `json:"name"`
			Ecosystem string `json:"ecosystem"`
		} `json:"package"`
		Ranges []struct {
			Type   string `json:"type"`
			Events []struct {
				Introduced string `json:"introduced,omitempty"`
				Fixed      string `json:"fixed,omitempty"`
			} `json:"events"`
		} `json:"ranges"`
		EcosystemSpecific struct {
			Imports []struct {
				Path    string   `json:"path"`
				Symbols []string `json:"symbols"`
			} `json:"imports"`
		} `json:"ecosystem_specific"`
	} `json:"affected"`
	References []struct {
		Type string `json:"type"`
		Url  string `json:"url"`
	} `json:"references"`
	DatabaseSpecific struct {
		Url          string `json:"url"`
		ReviewStatus string `json:"review_status"`
	} `json:"database_specific"`
}

type Finding struct {
	Osv          string `json:"osv"`
	FixedVersion string `json:"fixed_version"`
	Trace        []struct {
		Module  string `json:"module"`
		Version string `json:"version"`
	} `json:"trace"`
}

この構造体を使って脆弱性の検出結果を処理すると以下のようになります。

bufio.NewScannerを使って1行ずつJSONを処理しています。

osvは大量に出力され、かつfindingで検出される脆弱性ではないものも含まれます。
なので一度全ての出力を回してosvとfindingのリストに分けてから処理するようにしています。
(osv -> findingの順番に必ず出力されるようなので、一度の読み込みで全て処理することもできると思います)

package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"time"
)

func main() {
	f, _ := os.Open("format_json.json")
	scanner := bufio.NewScanner(f)

	osvList := make(map[string]*Osv)
	findingList := make([]*Finding, 0)

	out := &bytes.Buffer{}
	for scanner.Scan() {
		line := scanner.Bytes()
		var ret JsonFormat
		_ = json.Unmarshal(line, &ret)

		if ret.Osv != nil {
			osvList[ret.Osv.Id] = ret.Osv
		}

		// Traceが複数存在する場合、対応が必要な脆弱性であると判断しています
		if ret.Finding != nil && len(ret.Finding.Trace) > 1 {
			findingList = append(findingList, ret.Finding)
		}
	}

	for _, finding := range findingList {
		osv, ok := osvList[finding.Osv]
		if !ok {
			continue
		}

		fmt.Fprintf(
			out,
			"モジュール %q で脆弱性が見つかりましたよ!\nfinding version: %s\nfixed version: v%s\n",
			finding.Trace[0].Module,
			finding.Trace[0].Version,
			osv.Affected[0].Ranges[0].Events[1].Fixed,
		)
	}

	fmt.Print(out.String())
}

出力は以下のようになります。

$ go run .
モジュール "github.com/golang-jwt/jwt/v4" で脆弱性が見つかりましたよ!
finding version: v4.5.0
fixed version: v4.5.1

まとめ

text, sarif, openvex, json それぞれの形式で取得できる内容が異なります。
(例えば修正バージョンはjson以外には含まれないなど)

用途に応じて-formatオプションの出力形式を選択するのが良さそうです😀

text以外の形式は全てJSONで扱うことができるので
検出された脆弱性の内容をSlackに送信したり、Google Apps Scriptなど通してスプレッドシートに蓄積したりなど色々な用途に使えそうです😀

GitHubで編集を提案

Discussion