From d2f4da86a4d666f8d894149cfc74ea571dc78c6f Mon Sep 17 00:00:00 2001 From: Teppei Fukuda Date: Wed, 10 Jul 2024 10:21:17 +0400 Subject: [PATCH] chore: add VEX document and generator for Trivy (#7128) Signed-off-by: knqyf263 Co-authored-by: Nikita Pivkin --- .vex/trivy.openvex.json | 458 ++++++++++++++++++++++++++++++++++++++++ go.mod | 4 +- go.sum | 8 +- magefiles/magefile.go | 5 + magefiles/vex.go | 425 +++++++++++++++++++++++++++++++++++++ 5 files changed, 897 insertions(+), 3 deletions(-) create mode 100644 .vex/trivy.openvex.json create mode 100644 magefiles/vex.go diff --git a/.vex/trivy.openvex.json b/.vex/trivy.openvex.json new file mode 100644 index 0000000000..21af61db7d --- /dev/null +++ b/.vex/trivy.openvex.json @@ -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" + } + ] +} diff --git a/go.mod b/go.mod index fb7cca2025..222511f8ee 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ed9263ecb2..0eedca0480 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/magefiles/magefile.go b/magefiles/magefile.go index b23dde046e..7ce148d885 100644 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -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) +} diff --git a/magefiles/vex.go b/magefiles/vex.go new file mode 100644 index 0000000000..f5eb52e2ef --- /dev/null +++ b/magefiles/vex.go @@ -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 +}