| // 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 | |
| } |