blob: 772cba46ff73924ca5a88e2e27a87f4517765b8d [file] [log] [blame]
// 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 <url>@<name> 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 *Version) {
if v == nil {
return
}
if b, a, f := cutRepoUrl(v.Version); f {
repos[b] = append(repos[b], a)
}
}
for _, m := range r.Modules {
for _, v := range m.Versions {
perVersion(v)
}
perVersion((*Version)(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 *Version) {
if r, ok := replacements[v.Version]; ok {
v.Version = r
}
}
for _, m := range r.Modules {
for _, v := range m.Versions {
replaceVersion(v)
}
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) {
// commit hash?
if c, err := repo.CommitObject(plumbing.NewHash(name)); err == nil {
return c.Hash.String(), true
}
// 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
}