chore: add VEX document and generator for Trivy (#7128)

Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: Nikita Pivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
Teppei Fukuda
2024-07-10 10:21:17 +04:00
committed by GitHub
parent f27c236d6e
commit d2f4da86a4
5 changed files with 897 additions and 3 deletions

458
.vex/trivy.openvex.json Normal file
View File

@@ -0,0 +1,458 @@
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "aquasecurity/trivy:613fd55abbc2857b5ca28b07a26f3cd4c8b0ddc4c8a97c57497a2d4c4880d7fc",
"author": "Aqua Security",
"timestamp": "2024-07-09T11:38:00.115697+04:00",
"version": 1,
"tooling": "https://github.com/aquasecurity/trivy/tree/main/magefiles/vex.go",
"statements": [
{
"vulnerability": {
"@id": "https://pkg.go.dev/vuln/GO-2024-2575",
"name": "GO-2024-2575",
"description": "Helm's Missing YAML Content Leads To Panic in helm.sh/helm/v3",
"aliases": [
"CVE-2024-26147",
"GHSA-r53h-jv2g-vpx6"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/helm.sh/helm/v3",
"identifiers": {
"purl": "pkg:golang/helm.sh/helm/v3"
}
}
]
}
],
"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-2023-1765",
"name": "GO-2023-1765",
"description": "Leaked shared secret and weak blinding in github.com/cloudflare/circl",
"aliases": [
"CVE-2023-1732",
"GHSA-2q89-485c-9j2x"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/github.com/cloudflare/circl",
"identifiers": {
"purl": "pkg:golang/github.com/cloudflare/circl"
}
}
]
}
],
"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-2024-2512",
"name": "GO-2024-2512",
"description": "Classic builder cache poisoning in github.com/docker/docker",
"aliases": [
"CVE-2024-24557",
"GHSA-xw73-rw38-6vjc"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/github.com/docker/docker",
"identifiers": {
"purl": "pkg:golang/github.com/docker/docker"
}
}
]
}
],
"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-2024-2453",
"name": "GO-2024-2453",
"description": "Timing side channel in github.com/cloudflare/circl",
"aliases": [
"GHSA-9763-4f94-gfch"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/github.com/cloudflare/circl",
"identifiers": {
"purl": "pkg:golang/github.com/cloudflare/circl"
}
}
]
}
],
"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-2023-2048",
"name": "GO-2023-2048",
"description": "Paths outside of the rootfs could be produced on Windows in github.com/cyphar/filepath-securejoin",
"aliases": [
"GHSA-6xv5-86q9-7xr8"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/github.com/cyphar/filepath-securejoin",
"identifiers": {
"purl": "pkg:golang/github.com/cyphar/filepath-securejoin"
}
}
]
}
],
"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-2024-2497",
"name": "GO-2024-2497",
"description": "Privilege escalation in github.com/moby/buildkit",
"aliases": [
"CVE-2024-23653",
"GHSA-wr6v-9f75-vh2g"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/github.com/moby/buildkit",
"identifiers": {
"purl": "pkg:golang/github.com/moby/buildkit"
}
}
]
}
],
"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-2023-2102",
"name": "GO-2023-2102",
"description": "HTTP/2 rapid reset can cause excessive work in net/http",
"aliases": [
"CVE-2023-39325",
"GHSA-4374-p667-p6c8"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/golang.org/x/net",
"identifiers": {
"purl": "pkg:golang/golang.org/x/net"
}
}
]
}
],
"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-2024-2493",
"name": "GO-2024-2493",
"description": "Host system file access in github.com/moby/buildkit",
"aliases": [
"CVE-2024-23651",
"GHSA-m3r6-h7wv-7xxv"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/github.com/moby/buildkit",
"identifiers": {
"purl": "pkg:golang/github.com/moby/buildkit"
}
}
]
}
],
"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-2024-2491",
"name": "GO-2024-2491",
"description": "Container breakout through process.cwd trickery and leaked fds in github.com/opencontainers/runc",
"aliases": [
"CVE-2024-21626",
"GHSA-xr7r-f8xq-vfvv"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/github.com/opencontainers/runc",
"identifiers": {
"purl": "pkg:golang/github.com/opencontainers/runc"
}
}
]
}
],
"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-2024-2494",
"name": "GO-2024-2494",
"description": "Host system modification in github.com/moby/buildkit",
"aliases": [
"CVE-2024-23652",
"GHSA-4v98-7qmw-rqr8"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/github.com/moby/buildkit",
"identifiers": {
"purl": "pkg:golang/github.com/moby/buildkit"
}
}
]
}
],
"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-2023-2412",
"name": "GO-2023-2412",
"description": "RAPL accessibility in github.com/containerd/containerd",
"aliases": [
"GHSA-7ww5-4wqc-m92c"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/github.com/containerd/containerd",
"identifiers": {
"purl": "pkg:golang/github.com/containerd/containerd"
}
}
]
}
],
"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-2023-1988",
"name": "GO-2023-1988",
"description": "Improper rendering of text nodes in golang.org/x/net/html",
"aliases": [
"CVE-2023-3978",
"GHSA-2wrh-6pvc-2jm9"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/golang.org/x/net",
"identifiers": {
"purl": "pkg:golang/golang.org/x/net"
}
}
]
}
],
"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-2024-2492",
"name": "GO-2024-2492",
"description": "Panic in github.com/moby/buildkit",
"aliases": [
"CVE-2024-23650",
"GHSA-9p26-698r-w4hx"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/github.com/moby/buildkit",
"identifiers": {
"purl": "pkg:golang/github.com/moby/buildkit"
}
}
]
}
],
"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-2022-0646",
"name": "GO-2022-0646",
"description": "Use of risky cryptographic algorithm in github.com/aws/aws-sdk-go",
"aliases": [
"CVE-2020-8911",
"CVE-2020-8912",
"GHSA-7f33-f4f5-xwgw",
"GHSA-f5pg-7wfw-84q9"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/github.com/aws/aws-sdk-go",
"identifiers": {
"purl": "pkg:golang/github.com/aws/aws-sdk-go"
}
}
]
}
],
"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-2023-2153",
"name": "GO-2023-2153",
"description": "Denial of service from HTTP/2 Rapid Reset in google.golang.org/grpc",
"aliases": [
"GHSA-m425-mq94-257g"
]
},
"products": [
{
"@id": "pkg:golang/github.com/aquasecurity/trivy",
"identifiers": {
"purl": "pkg:golang/github.com/aquasecurity/trivy"
},
"subcomponents": [
{
"@id": "pkg:golang/google.golang.org/grpc",
"identifiers": {
"purl": "pkg:golang/google.golang.org/grpc"
}
}
]
}
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"impact_statement": "Govulncheck determined that the vulnerable code isn't called"
}
]
}

4
go.mod
View File

@@ -122,6 +122,7 @@ require (
golang.org/x/sync v0.7.0
golang.org/x/term v0.21.0
golang.org/x/text v0.16.0
golang.org/x/vuln v1.1.2
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v3 v3.0.1
@@ -355,8 +356,9 @@ require (
go.uber.org/zap v1.27.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/api v0.172.0 // indirect
google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect

8
go.sum
View File

@@ -2506,6 +2506,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0=
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -2624,8 +2626,10 @@ golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/vuln v1.1.2 h1:UkLxe+kAMcrNBpGrFbU0Mc5l7cX97P2nhy21wx5+Qbk=
golang.org/x/vuln v1.1.2/go.mod h1:2o3fRKD8Uz9AraAL3lwd/grWBv+t+SeJnPcqBUJrY24=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -469,3 +469,8 @@ type CloudActions mg.Namespace
func (CloudActions) Generate() error {
return sh.RunWith(ENV, "go", "run", "-tags=mage_cloudactions", "./magefiles")
}
// VEX generates a VEX document for Trivy
func VEX(_ context.Context, dir string) error {
return sh.RunWith(ENV, "go", "run", "-tags=mage_vex", "./magefiles/vex.go", "--dir", dir)
}

425
magefiles/vex.go Normal file
View File

@@ -0,0 +1,425 @@
//go:build mage_vex
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path"
"strings"
"time"
"github.com/openvex/go-vex/pkg/vex"
"github.com/package-url/packageurl-go"
"github.com/samber/lo"
"golang.org/x/vuln/scan"
"github.com/aquasecurity/go-version/pkg/version"
"github.com/aquasecurity/trivy/pkg/log"
)
const (
repoURL = "https://github.com/aquasecurity/trivy"
minVersion = "0.40.0"
)
var (
minVer, _ = version.Parse(minVersion)
// Product ID for Trivy
productID = &packageurl.PackageURL{
Type: packageurl.TypeGolang,
// According to https://github.com/package-url/purl-spec/issues/63,
// It's probably better to leave namespace empty and put a module name into `name`.
Namespace: "",
Name: "github.com/aquasecurity/trivy",
}
)
// VulnerabilityFinding is for parsing govulncheck JSON output
type VulnerabilityFinding struct {
Finding Finding `json:"finding"`
}
type Finding struct {
OSV string `json:"osv"`
FixedVersion string `json:"fixed_version"`
Trace []Trace `json:"trace"`
}
type Trace struct {
Module string `json:"module"`
Version string `json:"version"`
Package string `json:"package"`
}
// UniqueKey is used to identify unique vulnerability-subcomponent pairs
type UniqueKey struct {
VulnerabilityID vex.VulnerabilityID
SubcomponentID string
}
func main() {
if err := run(); err != nil {
log.Fatal("Fatal error", log.Err(err))
}
}
// run is the main entry point for the VEX generator
func run() error {
log.InitLogger(false, false)
// Parse command-line flags
cloneDir := flag.String("dir", "trivy", "Directory to clone the repository")
output := flag.String("output", ".vex/trivy.openvex.json", "Output file")
flag.Parse()
ctx := context.Background()
// Clone or pull the Trivy repository
if _, err := cloneOrPullRepo(ctx, *cloneDir); err != nil {
return err
}
defer func() {
// Ensure we are on the main branch after processing
_, _ = checkoutMain(ctx, *cloneDir)
}()
// Save the current working directory
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory: %w", err)
}
// Change to the target directory as govulncheck doesn't support Dir
// cf. https://github.com/golang/go/blob/6d89b38ed86e0bfa0ddaba08dc4071e6bb300eea/src/os/exec/exec.go#L171-L174
if err = os.Chdir(*cloneDir); err != nil {
return fmt.Errorf("failed to change to directory %s: %w", *cloneDir, err)
}
// Get the latest tags from the repository
tags, err := getLatestTags(ctx)
if err != nil {
return err
}
log.Info("Latest tags", log.Any("tags", tags))
// Maps to store "not_affected" statements across Trivy versions
notAffectedVulns := make(map[UniqueKey][]vex.Statement)
// Indicate one or more Trivy versions are affected by the vulnerability.
// This means that the version cannot be omitted later.
affectedVulns := make(map[UniqueKey]struct{})
// Process each tag
for _, tag := range tags {
notAffected, affected, err := processTag(ctx, tag)
if err != nil {
return err
}
log.Info("Processed tag", log.String("tag", tag),
log.Int("not_affected", len(notAffected)), log.Int("affected", len(affected)))
lo.Assign(affectedVulns, affected)
for k, v := range notAffected {
notAffectedVulns[k] = append(notAffectedVulns[k], v)
}
}
// Change back to the original directory
if err = os.Chdir(wd); err != nil {
return fmt.Errorf("failed to change back to original directory: %w", err)
}
// Generate the final VEX document
if err = updateVEX(*output, combineDocs(notAffectedVulns, affectedVulns)); err != nil {
return err
}
return nil
}
// cloneOrPullRepo clones the Trivy repository or pulls updates if it already exists
func cloneOrPullRepo(ctx context.Context, dir string) ([]byte, error) {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return runCommandWithTimeout(ctx, 20*time.Minute, "git", "clone", repoURL, dir)
}
if _, err := checkoutMain(ctx, dir); err != nil {
return nil, fmt.Errorf("failed to checkout main: %w", err)
}
return runCommandWithTimeout(ctx, 2*time.Minute, "git", "-C", dir, "pull", "--tags")
}
// checkoutMain checks out the main branch of the repository
func checkoutMain(ctx context.Context, dir string) ([]byte, error) {
return runCommandWithTimeout(ctx, 1*time.Minute, "git", "-C", dir, "checkout", "main")
}
// getLatestTags retrieves and sorts the latest tags from the repository
func getLatestTags(ctx context.Context) ([]string, error) {
output, err := runCommandWithTimeout(ctx, 1*time.Minute, "git", "tag")
if err != nil {
return nil, fmt.Errorf("failed to get tags: %w", err)
}
tags := strings.Split(strings.TrimSpace(string(output)), "\n")
versions := make([]string, 0, len(tags))
for _, tag := range tags {
v, err := version.Parse(tag)
if err != nil {
continue
}
if v.GreaterThanOrEqual(minVer) {
versions = append(versions, tag)
}
}
return versions, nil
}
// processTag processes a single tag, running govulncheck and generating VEX statements
func processTag(ctx context.Context, tag string) (map[UniqueKey]vex.Statement, map[UniqueKey]struct{}, error) {
log.Info("Processing tag...", log.String("tag", tag))
if _, err := runCommandWithTimeout(ctx, 1*time.Minute, "git", "checkout", tag); err != nil {
return nil, nil, fmt.Errorf("failed to checkout tag %s: %w", tag, err)
}
// Run govulncheck and generate VEX document
vexDoc, err := generateVEX(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to run govulncheck: %w", err)
}
// Run govulncheck and generate JSON result
// Need to generate JSON as well as OpenVEX for the following reasons:
// - Subcomponent
// - OpenVEX from govulncheck doesn't fill in subcomponents.
// - Status
// - govulncheck uses "not_affected" for all vulnerabilities. Need to determine "fixed" vulnerabilities.
// cf. https://github.com/golang/go/issues/68338
findings, err := generateJSON(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to run govulncheck: %w", err)
}
product := *productID // Clone Trivy PURL
product.Version = tag
notAffected := make(map[UniqueKey]vex.Statement)
affected := make(map[UniqueKey]struct{})
// Update VEX document generated by govulncheck
for _, stmt := range vexDoc.Statements {
finding, ok := findings[stmt.Vulnerability.Name]
if !ok {
// Considered as "fixed" vulnerabilities
// cf. https://github.com/golang/go/issues/68338
continue
} else if len(finding.Finding.Trace) == 0 {
continue
}
namespace, name := path.Split(finding.Finding.Trace[0].Module)
subcomponent := &packageurl.PackageURL{
Type: packageurl.TypeGolang,
Namespace: namespace,
Name: name,
}
key := UniqueKey{
VulnerabilityID: stmt.Vulnerability.Name,
SubcomponentID: subcomponent.String(),
}
if stmt.Status == vex.StatusAffected {
affected[key] = struct{}{}
continue
} else if stmt.Status != vex.StatusNotAffected {
continue
}
// Update the statement with product and subcomponent information
stmt.Products = []vex.Product{
{
// Fill in components manually
// cf. https://github.com/golang/go/issues/68152
Component: vex.Component{
ID: product.String(),
Identifiers: map[vex.IdentifierType]string{
vex.PURL: product.String(),
},
},
Subcomponents: []vex.Subcomponent{
{
Component: vex.Component{
ID: key.SubcomponentID,
Identifiers: map[vex.IdentifierType]string{
vex.PURL: key.SubcomponentID,
},
},
},
},
},
}
notAffected[key] = stmt
}
return notAffected, affected, nil
}
// generateVEX runs govulncheck with OpenVEX format and parses the output
func generateVEX(ctx context.Context) (*vex.VEX, error) {
buf, err := runGovulncheck(ctx, "openvex")
if err != nil {
return nil, fmt.Errorf("failed to run govulncheck: %w", err)
}
vexDoc, err := vex.Parse(buf.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to parse govulncheck output: %w", err)
}
return vexDoc, nil
}
// generateJSON runs govulncheck with JSON format and parses the output
func generateJSON(ctx context.Context) (map[vex.VulnerabilityID]VulnerabilityFinding, error) {
buf, err := runGovulncheck(ctx, "json")
if err != nil {
return nil, fmt.Errorf("failed to run govulncheck: %w", err)
}
decoder := json.NewDecoder(buf)
findings := make(map[vex.VulnerabilityID]VulnerabilityFinding)
for {
var finding VulnerabilityFinding
if err := decoder.Decode(&finding); err == io.EOF {
break
} else if err != nil {
return nil, fmt.Errorf("failed to decode govulncheck output: %w", err)
}
findings[vex.VulnerabilityID(finding.Finding.OSV)] = finding
}
return findings, nil
}
// runGovulncheck executes the govulncheck command with the specified format
func runGovulncheck(ctx context.Context, format string) (*bytes.Buffer, error) {
var buf bytes.Buffer
cmd := scan.Command(ctx, "-format", format, "./...")
cmd.Stdout = &buf
log.Info("Running govulncheck", log.String("format", format))
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to start govulncheck: %w", err)
}
if err := cmd.Wait(); err != nil {
return nil, fmt.Errorf("failed to run govulncheck: %w", err)
}
return &buf, nil
}
// combineDocs merges the VEX statements from all processed tags
func combineDocs(notAffected map[UniqueKey][]vex.Statement, affected map[UniqueKey]struct{}) []vex.Statement {
log.Info("Combining VEX documents")
statements := make(map[UniqueKey]vex.Statement)
for key, stmts := range notAffected {
for _, stmt := range stmts {
if _, ok := affected[key]; !ok {
// All versions are "not_affected" or "fixed" by the vulnerability, omitting a version in PURL
// => pkg:golang/github.com/aquasecurity/trivy
stmt.Products[0].ID = productID.String()
stmt.Products[0].Identifiers[vex.PURL] = productID.String()
statements[key] = stmt
break
}
// At least one version is "affected" by the vulnerability, so we need to include the version in PURL.
// => pkg:golang/github.com/aquasecurity/trivy@0.52.0
if s, ok := statements[key]; ok {
s.Products = append(s.Products, stmt.Products...)
statements[key] = s
} else {
statements[key] = stmt
}
}
}
return lo.Values(statements)
}
// runCommandWithTimeout executes a command with a specified timeout
func runCommandWithTimeout(ctx context.Context, timeout time.Duration, name string, args ...string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
cmd := exec.CommandContext(ctx, name, args...)
log.Info("Executing command", log.String("cmd", cmd.String()))
output, err := cmd.CombinedOutput()
if err != nil {
return output, fmt.Errorf("%w, output: %s", err, string(output))
}
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, fmt.Errorf("command timed out after %v", timeout)
}
return output, nil
}
// updateVEX updates the final VEX document with the combined statements
func updateVEX(output string, statements []vex.Statement) error {
doc, err := vex.Load(output)
if errors.Is(err, os.ErrNotExist) {
doc = &vex.VEX{}
} else if err != nil {
return err
}
vex.SortStatements(statements, time.Now())
d := &vex.VEX{
Metadata: vex.Metadata{
Context: "https://openvex.dev/ns/v0.2.0",
Author: "Aqua Security",
Timestamp: lo.ToPtr(time.Now()),
Version: doc.Version + 1,
Tooling: "https://github.com/aquasecurity/trivy/tree/main/magefiles/vex.go",
},
Statements: statements,
}
h, err := hashVEX(d)
if err != nil {
return err
}
d.ID = "aquasecurity/trivy:" + h
f, err := os.Create(output)
if err != nil {
return err
}
defer f.Close()
e := json.NewEncoder(f)
e.SetIndent("", " ")
if err = e.Encode(d); err != nil {
return err
}
return err
}
func hashVEX(d *vex.VEX) (string, error) {
out, err := json.Marshal(d)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", sha256.Sum256(out)), nil
}