| // Copyright 2023 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 vulncheck |
| |
| import ( |
| "fmt" |
| "os/exec" |
| "slices" |
| "strings" |
| |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/vuln/internal" |
| "golang.org/x/vuln/internal/govulncheck" |
| "golang.org/x/vuln/internal/semver" |
| ) |
| |
| // PackageGraph holds a complete module and package graph. |
| // Its primary purpose is to allow fast access to the nodes |
| // by path and make sure all(stdlib) packages have a module. |
| type PackageGraph struct { |
| // topPkgs are top-level packages specified by the user. |
| // Empty in binary mode. |
| topPkgs []*packages.Package |
| modules map[string]*packages.Module // all modules (even replacing ones) |
| packages map[string]*packages.Package // all packages (even dependencies) |
| } |
| |
| func NewPackageGraph(goVersion string) *PackageGraph { |
| graph := &PackageGraph{ |
| modules: map[string]*packages.Module{}, |
| packages: map[string]*packages.Package{}, |
| } |
| |
| goRoot := "" |
| if out, err := exec.Command("go", "env", "GOROOT").Output(); err == nil { |
| goRoot = strings.TrimSpace(string(out)) |
| } |
| stdlibModule := &packages.Module{ |
| Path: internal.GoStdModulePath, |
| Version: semver.GoTagToSemver(goVersion), |
| Dir: goRoot, |
| } |
| graph.AddModules(stdlibModule) |
| return graph |
| } |
| |
| func (g *PackageGraph) TopPkgs() []*packages.Package { |
| return g.topPkgs |
| } |
| |
| // DepPkgs returns the number of packages that graph.TopPkgs() |
| // strictly depend on. This does not include topPkgs even if |
| // they are dependency of each other. |
| func (g *PackageGraph) DepPkgs() []*packages.Package { |
| topPkgs := g.TopPkgs() |
| tops := make(map[string]bool) |
| depPkgs := make(map[string]*packages.Package) |
| |
| for _, t := range topPkgs { |
| tops[t.PkgPath] = true |
| } |
| |
| var visit func(*packages.Package, bool) |
| visit = func(p *packages.Package, top bool) { |
| path := p.PkgPath |
| if _, ok := depPkgs[path]; ok { |
| return |
| } |
| if tops[path] && !top { |
| // A top package that is a dependency |
| // will not be in depPkgs, so we skip |
| // reiterating on it here. |
| return |
| } |
| |
| // We don't count a top-level package as |
| // a dependency even when they are used |
| // as a dependent package. |
| if !tops[path] { |
| depPkgs[path] = p |
| } |
| |
| for _, d := range p.Imports { |
| visit(d, false) |
| } |
| } |
| |
| for _, t := range topPkgs { |
| visit(t, true) |
| } |
| |
| var deps []*packages.Package |
| for _, d := range depPkgs { |
| deps = append(deps, g.GetPackage(d.PkgPath)) |
| } |
| return deps |
| } |
| |
| func (g *PackageGraph) Modules() []*packages.Module { |
| var mods []*packages.Module |
| for _, m := range g.modules { |
| mods = append(mods, m) |
| } |
| return mods |
| } |
| |
| // AddModules adds the modules and any replace modules provided. |
| // It will ignore modules that have duplicate paths to ones the |
| // graph already holds. |
| func (g *PackageGraph) AddModules(mods ...*packages.Module) { |
| for _, mod := range mods { |
| if _, found := g.modules[mod.Path]; found { |
| //TODO: check duplicates are okay? |
| continue |
| } |
| g.modules[mod.Path] = mod |
| if mod.Replace != nil { |
| g.AddModules(mod.Replace) |
| } |
| } |
| } |
| |
| // GetModule gets module at path if one exists. Otherwise, |
| // it creates a module and returns it. |
| func (g *PackageGraph) GetModule(path string) *packages.Module { |
| if mod, ok := g.modules[path]; ok { |
| return mod |
| } |
| mod := &packages.Module{ |
| Path: path, |
| Version: "", |
| } |
| g.AddModules(mod) |
| return mod |
| } |
| |
| // AddPackages adds the packages and their full graph of imported packages. |
| // It also adds the modules of the added packages. It will ignore packages |
| // that have duplicate paths to ones the graph already holds. |
| func (g *PackageGraph) AddPackages(pkgs ...*packages.Package) { |
| for _, pkg := range pkgs { |
| if _, found := g.packages[pkg.PkgPath]; found { |
| //TODO: check duplicates are okay? |
| continue |
| } |
| g.packages[pkg.PkgPath] = pkg |
| g.fixupPackage(pkg) |
| for _, child := range pkg.Imports { |
| g.AddPackages(child) |
| } |
| } |
| } |
| |
| // fixupPackage adds the module of pkg, if any, to the set |
| // of all modules in g. If packages is not assigned a module |
| // (likely stdlib package), a module set for pkg. |
| func (g *PackageGraph) fixupPackage(pkg *packages.Package) { |
| if pkg.Module != nil { |
| g.AddModules(pkg.Module) |
| return |
| } |
| pkg.Module = g.findModule(pkg.PkgPath) |
| } |
| |
| // findModule finds a module for package. |
| // It does a longest prefix search amongst the existing modules, if that does |
| // not find anything, it returns the "unknown" module. |
| func (g *PackageGraph) findModule(pkgPath string) *packages.Module { |
| //TODO: better stdlib test |
| if IsStdPackage(pkgPath) { |
| return g.GetModule(internal.GoStdModulePath) |
| } |
| for _, m := range g.modules { |
| //TODO: not first match, best match... |
| if pkgPath == m.Path || strings.HasPrefix(pkgPath, m.Path+"/") { |
| return m |
| } |
| } |
| return g.GetModule(internal.UnknownModulePath) |
| } |
| |
| // GetPackage returns the package matching the path. |
| // If the graph does not already know about the package, a new one is added. |
| func (g *PackageGraph) GetPackage(path string) *packages.Package { |
| if pkg, ok := g.packages[path]; ok { |
| return pkg |
| } |
| pkg := &packages.Package{ |
| PkgPath: path, |
| } |
| g.AddPackages(pkg) |
| return pkg |
| } |
| |
| // LoadPackages loads the packages specified by the patterns into the graph. |
| // See golang.org/x/tools/go/packages.Load for details of how it works. |
| func (g *PackageGraph) LoadPackagesAndMods(cfg *packages.Config, tags []string, patterns []string, wantSymbols bool) error { |
| if len(tags) > 0 { |
| cfg.BuildFlags = []string{fmt.Sprintf("-tags=%s", strings.Join(tags, ","))} |
| } |
| |
| addLoadMode(cfg, wantSymbols) |
| |
| pkgs, err := packages.Load(cfg, patterns...) |
| if err != nil { |
| return err |
| } |
| var perrs []packages.Error |
| packages.Visit(pkgs, nil, func(p *packages.Package) { |
| perrs = append(perrs, p.Errors...) |
| }) |
| if len(perrs) > 0 { |
| err = &packageError{perrs} |
| } |
| |
| // Add all packages, top-level ones and their imports. |
| // This will also add their respective modules. |
| g.AddPackages(pkgs...) |
| |
| // save top-level packages |
| for _, p := range pkgs { |
| g.topPkgs = append(g.topPkgs, g.GetPackage(p.PkgPath)) |
| } |
| return err |
| } |
| |
| func addLoadMode(cfg *packages.Config, wantSymbols bool) { |
| cfg.Mode |= |
| packages.NeedModule | |
| packages.NeedName | |
| packages.NeedDeps | |
| packages.NeedImports |
| if wantSymbols { |
| cfg.Mode |= packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo |
| } |
| } |
| |
| // packageError contains errors from loading a set of packages. |
| type packageError struct { |
| Errors []packages.Error |
| } |
| |
| func (e *packageError) Error() string { |
| var b strings.Builder |
| fmt.Fprintln(&b, "\nThere are errors with the provided package patterns:") |
| fmt.Fprintln(&b, "") |
| for _, e := range e.Errors { |
| fmt.Fprintln(&b, e) |
| } |
| fmt.Fprintln(&b, "\nFor details on package patterns, see https://pkg.go.dev/cmd/go#hdr-Package_lists_and_patterns.") |
| return b.String() |
| } |
| |
| func (g *PackageGraph) SBOM() *govulncheck.SBOM { |
| getMod := func(mod *packages.Module) *govulncheck.Module { |
| if mod.Replace != nil { |
| return &govulncheck.Module{ |
| Path: mod.Replace.Path, |
| Version: mod.Replace.Version, |
| } |
| } |
| |
| return &govulncheck.Module{ |
| Path: mod.Path, |
| Version: mod.Version, |
| } |
| } |
| |
| var roots []string |
| rootMods := make(map[string]*govulncheck.Module) |
| for _, pkg := range g.TopPkgs() { |
| roots = append(roots, pkg.PkgPath) |
| mod := getMod(pkg.Module) |
| rootMods[mod.Path] = mod |
| } |
| |
| // Govulncheck attempts to put the modules that correspond to the matched package patterns (i.e. the root modules) |
| // at the beginning of the SBOM.Modules message. |
| // Note: This does not guarantee that the first element is the root module. |
| var topMods, depMods []*govulncheck.Module |
| var goVersion string |
| for _, mod := range g.Modules() { |
| mod := getMod(mod) |
| |
| if mod.Path == internal.GoStdModulePath { |
| goVersion = semver.SemverToGoTag(mod.Version) |
| } |
| |
| // if the mod is not associated with a root package, add it to depMods |
| if rootMods[mod.Path] == nil { |
| depMods = append(depMods, mod) |
| } |
| } |
| |
| for _, mod := range rootMods { |
| topMods = append(topMods, mod) |
| } |
| // Sort for deterministic output |
| sortMods(topMods) |
| sortMods(depMods) |
| |
| mods := append(topMods, depMods...) |
| |
| return &govulncheck.SBOM{ |
| GoVersion: goVersion, |
| Modules: mods, |
| Roots: roots, |
| } |
| } |
| |
| // Sorts modules alphabetically by path. |
| func sortMods(mods []*govulncheck.Module) { |
| slices.SortFunc(mods, func(a, b *govulncheck.Module) int { |
| return strings.Compare(a.Path, b.Path) |
| }) |
| } |