From 878e33a2036b83c7eb654333e0e6bf3b3d03d0d7 Mon Sep 17 00:00:00 2001 From: Tim King Date: Thu, 21 Mar 2024 13:50:11 -0700 Subject: [PATCH] internal/report: support branch and tag expansion in fix Adds support to expand git branches and tags in Report version fields to git commit hashes during vulnreport fix. Commit hashes can then be cleaned up by FixVersions(...). Change-Id: I3d2d14983b669054c9a9e25d8ab51a6a70087929 Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/573396 Reviewed-by: Tatiana Bradley LUCI-TryBot-Result: Go LUCI --- internal/gitrepo/gitrepo.go | 10 +++ internal/report/fix.go | 1 + internal/report/git.go | 155 ++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 internal/report/git.go diff --git a/internal/gitrepo/gitrepo.go b/internal/gitrepo/gitrepo.go index ecaa0a84..80e3c5b4 100644 --- a/internal/gitrepo/gitrepo.go +++ b/internal/gitrepo/gitrepo.go @@ -60,6 +60,16 @@ func PlainClone(ctx context.Context, dir, repoURL string) (repo *git.Repository, }) } +// PlainCloneWith returns a (non-bare) repo with its history by cloning the repo with the given options opt. +func PlainCloneWith(ctx context.Context, dir string, opts *git.CloneOptions) (repo *git.Repository, err error) { + defer derrors.Wrap(&err, "gitrepo.PlainCloneWith(%q)", opts.URL) + ctx, span := observe.Start(ctx, "gitrepo.PlainCloneWith") + defer span.End() + + log.Infof(ctx, "Plain cloning repo %q at HEAD with options", opts.URL) + return git.PlainCloneContext(ctx, dir, false, opts) +} + // Open returns a repo by opening the repo at the local path dirpath. func Open(ctx context.Context, dirpath string) (repo *git.Repository, err error) { defer derrors.Wrap(&err, "gitrepo.Open(%q)", dirpath) diff --git a/internal/report/fix.go b/internal/report/fix.go index 53c86791..22241619 100644 --- a/internal/report/fix.go +++ b/internal/report/fix.go @@ -18,6 +18,7 @@ import ( ) func (r *Report) Fix(pc *proxy.Client) { + expandGitCommits(r) for _, m := range r.Modules { m.FixVersions(pc) } diff --git a/internal/report/git.go b/internal/report/git.go new file mode 100644 index 00000000..f5e1e155 --- /dev/null +++ b/internal/report/git.go @@ -0,0 +1,155 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package report + +import ( + "context" + "os" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "golang.org/x/vulndb/cmd/vulnreport/log" + "golang.org/x/vulndb/internal/derrors" + "golang.org/x/vulndb/internal/gitrepo" +) + +// expandGitCommits expands git repositories and names to commits. +// Expands versions in r of the form @ where url starts with +// one of {'git://', 'https://', 'http://', 'ssh://', 'git+ssh://'} +// and name is the name of a git branch or git tag to a git commit +// hash. Any version that is successfully expanded is replaced. +func expandGitCommits(r *Report) { + // Find repos in versions to expand + repos := make(map[string][]string) // url -> names + perVersion := func(v string) { + if b, a, f := cutRepoUrl(v); f { + repos[b] = append(repos[b], a) + } + } + for _, m := range r.Modules { + for _, vr := range m.Versions { + perVersion(vr.Introduced) + perVersion(vr.Fixed) + } + perVersion(m.VulnerableAt) + } + + if len(repos) == 0 { // no repos to expand + return + } + + log.Infof("Expanding git urls for %d repos", len(repos)) + + // Create scratch directory. + scratch, err := os.MkdirTemp("", "expand-git-references") + if err != nil { + log.Err("failed to create scratch directory for ExpandGitReferences") + return + } + defer func() { + _ = os.RemoveAll(scratch) + }() + + // expand references and compute replacements + replacements := make(map[string]string) + for repo, names := range repos { + commits, err := gitNameToCommits(scratch, repo, names) + if err != nil { + log.Infof("expandGitCommits(%v, %v) failed with: %v", repo, names, err) + continue + } + for name, c := range commits { + replacements[repo+"@"+name] = c + } + } + + if len(replacements) == 0 { // no replacements created + return + } + + // Replace all. + replaceVersion := func(v string) string { + if r, ok := replacements[v]; ok { + return r + } + return v + } + for i, m := range r.Modules { + for j, vr := range m.Versions { + m.Versions[j].Introduced = replaceVersion(vr.Introduced) + m.Versions[j].Fixed = replaceVersion(vr.Fixed) + } + r.Modules[i].VulnerableAt = replaceVersion(m.VulnerableAt) + } +} + +func cutRepoUrl(v string) (string, string, bool) { + prefixes := map[string]bool{ + "https://": true, + "http://": true, + "git://": true, + "git+ssh://": true, + "ssh://": true, + } + for p := range prefixes { + if strings.HasPrefix(v, p) { + return strings.Cut(v, "@") + } + } + return v, "", false +} + +// gitNameToCommits returns a mapping from the git repo at repoURL +// and returns a mapping for the branches and tags in names to +// a commit hash. +func gitNameToCommits(dir string, repoURL string, names []string) (_ map[string]string, err error) { + defer derrors.Wrap(&err, "gitNameToCommits(%q, %q, %v)", dir, repoURL, names) + + repoRoot, err := os.MkdirTemp(dir, "git*") + if err != nil { + return nil, err + } + + ctx := context.Background() + repo, err := gitrepo.PlainCloneWith(ctx, repoRoot, &git.CloneOptions{ + URL: repoURL, + ReferenceName: plumbing.HEAD, + SingleBranch: true, // allow branches other than master + Depth: 0, // pull in history + // Leaves Tags the default value. + }) + if err != nil { + return nil, err + } + + resolveName := func(name string) (string, bool) { + // branch name? + if b, err := repo.Branch(name); err == nil { + if ref, err := repo.Reference(b.Merge, true); err == nil { + if h := ref.Hash(); !h.IsZero() { + return h.String(), true + } + } + } + + // tag name? + if ref, err := repo.Tag(name); err == nil { + if h := ref.Hash(); !h.IsZero() { + return h.String(), true + } + } + + return "", false + } + + results := make(map[string]string) + for _, name := range names { + if r, ok := resolveName(name); ok { + results[name] = r + } + } + return results, nil +}