src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

packages.go (8443B)


      1 // Copyright 2023 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package vulncheck
      6 
      7 import (
      8 	"fmt"
      9 	"os/exec"
     10 	"slices"
     11 	"strings"
     12 
     13 	"golang.org/x/tools/go/packages"
     14 	"golang.org/x/vuln/internal"
     15 	"golang.org/x/vuln/internal/govulncheck"
     16 	"golang.org/x/vuln/internal/semver"
     17 )
     18 
     19 // PackageGraph holds a complete module and package graph.
     20 // Its primary purpose is to allow fast access to the nodes
     21 // by path and make sure all(stdlib)  packages have a module.
     22 type PackageGraph struct {
     23 	// topPkgs are top-level packages specified by the user.
     24 	// Empty in binary mode.
     25 	topPkgs  []*packages.Package
     26 	modules  map[string]*packages.Module  // all modules (even replacing ones)
     27 	packages map[string]*packages.Package // all packages (even dependencies)
     28 }
     29 
     30 func NewPackageGraph(goVersion string) *PackageGraph {
     31 	graph := &PackageGraph{
     32 		modules:  map[string]*packages.Module{},
     33 		packages: map[string]*packages.Package{},
     34 	}
     35 
     36 	goRoot := ""
     37 	if out, err := exec.Command("go", "env", "GOROOT").Output(); err == nil {
     38 		goRoot = strings.TrimSpace(string(out))
     39 	}
     40 	stdlibModule := &packages.Module{
     41 		Path:    internal.GoStdModulePath,
     42 		Version: semver.GoTagToSemver(goVersion),
     43 		Dir:     goRoot,
     44 	}
     45 	graph.AddModules(stdlibModule)
     46 	return graph
     47 }
     48 
     49 func (g *PackageGraph) TopPkgs() []*packages.Package {
     50 	return g.topPkgs
     51 }
     52 
     53 // DepPkgs returns the number of packages that graph.TopPkgs()
     54 // strictly depend on. This does not include topPkgs even if
     55 // they are dependency of each other.
     56 func (g *PackageGraph) DepPkgs() []*packages.Package {
     57 	topPkgs := g.TopPkgs()
     58 	tops := make(map[string]bool)
     59 	depPkgs := make(map[string]*packages.Package)
     60 
     61 	for _, t := range topPkgs {
     62 		tops[t.PkgPath] = true
     63 	}
     64 
     65 	var visit func(*packages.Package, bool)
     66 	visit = func(p *packages.Package, top bool) {
     67 		path := p.PkgPath
     68 		if _, ok := depPkgs[path]; ok {
     69 			return
     70 		}
     71 		if tops[path] && !top {
     72 			// A top package that is a dependency
     73 			// will not be in depPkgs, so we skip
     74 			// reiterating on it here.
     75 			return
     76 		}
     77 
     78 		// We don't count a top-level package as
     79 		// a dependency even when they are used
     80 		// as a dependent package.
     81 		if !tops[path] {
     82 			depPkgs[path] = p
     83 		}
     84 
     85 		for _, d := range p.Imports {
     86 			visit(d, false)
     87 		}
     88 	}
     89 
     90 	for _, t := range topPkgs {
     91 		visit(t, true)
     92 	}
     93 
     94 	var deps []*packages.Package
     95 	for _, d := range depPkgs {
     96 		deps = append(deps, g.GetPackage(d.PkgPath))
     97 	}
     98 	return deps
     99 }
    100 
    101 func (g *PackageGraph) Modules() []*packages.Module {
    102 	var mods []*packages.Module
    103 	for _, m := range g.modules {
    104 		mods = append(mods, m)
    105 	}
    106 	return mods
    107 }
    108 
    109 // AddModules adds the modules and any replace modules provided.
    110 // It will ignore modules that have duplicate paths to ones the
    111 // graph already holds.
    112 func (g *PackageGraph) AddModules(mods ...*packages.Module) {
    113 	for _, mod := range mods {
    114 		if _, found := g.modules[mod.Path]; found {
    115 			//TODO: check duplicates are okay?
    116 			continue
    117 		}
    118 		g.modules[mod.Path] = mod
    119 		if mod.Replace != nil {
    120 			g.AddModules(mod.Replace)
    121 		}
    122 	}
    123 }
    124 
    125 // GetModule gets module at path if one exists. Otherwise,
    126 // it creates a module and returns it.
    127 func (g *PackageGraph) GetModule(path string) *packages.Module {
    128 	if mod, ok := g.modules[path]; ok {
    129 		return mod
    130 	}
    131 	mod := &packages.Module{
    132 		Path:    path,
    133 		Version: "",
    134 	}
    135 	g.AddModules(mod)
    136 	return mod
    137 }
    138 
    139 // AddPackages adds the packages and their full graph of imported packages.
    140 // It also adds the modules of the added packages. It will ignore packages
    141 // that have duplicate paths to ones the graph already holds.
    142 func (g *PackageGraph) AddPackages(pkgs ...*packages.Package) {
    143 	for _, pkg := range pkgs {
    144 		if _, found := g.packages[pkg.PkgPath]; found {
    145 			//TODO: check duplicates are okay?
    146 			continue
    147 		}
    148 		g.packages[pkg.PkgPath] = pkg
    149 		g.fixupPackage(pkg)
    150 		for _, child := range pkg.Imports {
    151 			g.AddPackages(child)
    152 		}
    153 	}
    154 }
    155 
    156 // fixupPackage adds the module of pkg, if any, to the set
    157 // of all modules in g. If packages is not assigned a module
    158 // (likely stdlib package), a module set for pkg.
    159 func (g *PackageGraph) fixupPackage(pkg *packages.Package) {
    160 	if pkg.Module != nil {
    161 		g.AddModules(pkg.Module)
    162 		return
    163 	}
    164 	pkg.Module = g.findModule(pkg.PkgPath)
    165 }
    166 
    167 // findModule finds a module for package.
    168 // It does a longest prefix search amongst the existing modules, if that does
    169 // not find anything, it returns the "unknown" module.
    170 func (g *PackageGraph) findModule(pkgPath string) *packages.Module {
    171 	//TODO: better stdlib test
    172 	if IsStdPackage(pkgPath) {
    173 		return g.GetModule(internal.GoStdModulePath)
    174 	}
    175 	for _, m := range g.modules {
    176 		//TODO: not first match, best match...
    177 		if pkgPath == m.Path || strings.HasPrefix(pkgPath, m.Path+"/") {
    178 			return m
    179 		}
    180 	}
    181 	return g.GetModule(internal.UnknownModulePath)
    182 }
    183 
    184 // GetPackage returns the package matching the path.
    185 // If the graph does not already know about the package, a new one is added.
    186 func (g *PackageGraph) GetPackage(path string) *packages.Package {
    187 	if pkg, ok := g.packages[path]; ok {
    188 		return pkg
    189 	}
    190 	pkg := &packages.Package{
    191 		PkgPath: path,
    192 	}
    193 	g.AddPackages(pkg)
    194 	return pkg
    195 }
    196 
    197 // LoadPackages loads the packages specified by the patterns into the graph.
    198 // See golang.org/x/tools/go/packages.Load for details of how it works.
    199 func (g *PackageGraph) LoadPackagesAndMods(cfg *packages.Config, tags []string, patterns []string, wantSymbols bool) error {
    200 	if len(tags) > 0 {
    201 		cfg.BuildFlags = []string{fmt.Sprintf("-tags=%s", strings.Join(tags, ","))}
    202 	}
    203 
    204 	addLoadMode(cfg, wantSymbols)
    205 
    206 	pkgs, err := packages.Load(cfg, patterns...)
    207 	if err != nil {
    208 		return err
    209 	}
    210 	var perrs []packages.Error
    211 	packages.Visit(pkgs, nil, func(p *packages.Package) {
    212 		perrs = append(perrs, p.Errors...)
    213 	})
    214 	if len(perrs) > 0 {
    215 		err = &packageError{perrs}
    216 	}
    217 
    218 	// Add all packages, top-level ones and their imports.
    219 	// This will also add their respective modules.
    220 	g.AddPackages(pkgs...)
    221 
    222 	// save top-level packages
    223 	for _, p := range pkgs {
    224 		g.topPkgs = append(g.topPkgs, g.GetPackage(p.PkgPath))
    225 	}
    226 	return err
    227 }
    228 
    229 func addLoadMode(cfg *packages.Config, wantSymbols bool) {
    230 	cfg.Mode |=
    231 		packages.NeedModule |
    232 			packages.NeedName |
    233 			packages.NeedDeps |
    234 			packages.NeedImports
    235 	if wantSymbols {
    236 		cfg.Mode |= packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo
    237 	}
    238 }
    239 
    240 // packageError contains errors from loading a set of packages.
    241 type packageError struct {
    242 	Errors []packages.Error
    243 }
    244 
    245 func (e *packageError) Error() string {
    246 	var b strings.Builder
    247 	fmt.Fprintln(&b, "\nThere are errors with the provided package patterns:")
    248 	fmt.Fprintln(&b, "")
    249 	for _, e := range e.Errors {
    250 		fmt.Fprintln(&b, e)
    251 	}
    252 	fmt.Fprintln(&b, "\nFor details on package patterns, see https://pkg.go.dev/cmd/go#hdr-Package_lists_and_patterns.")
    253 	return b.String()
    254 }
    255 
    256 func (g *PackageGraph) SBOM() *govulncheck.SBOM {
    257 	getMod := func(mod *packages.Module) *govulncheck.Module {
    258 		if mod.Replace != nil {
    259 			return &govulncheck.Module{
    260 				Path:    mod.Replace.Path,
    261 				Version: mod.Replace.Version,
    262 			}
    263 		}
    264 
    265 		return &govulncheck.Module{
    266 			Path:    mod.Path,
    267 			Version: mod.Version,
    268 		}
    269 	}
    270 
    271 	var roots []string
    272 	rootMods := make(map[string]*govulncheck.Module)
    273 	for _, pkg := range g.TopPkgs() {
    274 		roots = append(roots, pkg.PkgPath)
    275 		mod := getMod(pkg.Module)
    276 		rootMods[mod.Path] = mod
    277 	}
    278 
    279 	// Govulncheck attempts to put the modules that correspond to the matched package patterns (i.e. the root modules)
    280 	// at the beginning of the SBOM.Modules message.
    281 	// Note: This does not guarantee that the first element is the root module.
    282 	var topMods, depMods []*govulncheck.Module
    283 	var goVersion string
    284 	for _, mod := range g.Modules() {
    285 		mod := getMod(mod)
    286 
    287 		if mod.Path == internal.GoStdModulePath {
    288 			goVersion = semver.SemverToGoTag(mod.Version)
    289 		}
    290 
    291 		// if the mod is not associated with a root package, add it to depMods
    292 		if rootMods[mod.Path] == nil {
    293 			depMods = append(depMods, mod)
    294 		}
    295 	}
    296 
    297 	for _, mod := range rootMods {
    298 		topMods = append(topMods, mod)
    299 	}
    300 	// Sort for deterministic output
    301 	sortMods(topMods)
    302 	sortMods(depMods)
    303 
    304 	mods := append(topMods, depMods...)
    305 
    306 	return &govulncheck.SBOM{
    307 		GoVersion: goVersion,
    308 		Modules:   mods,
    309 		Roots:     roots,
    310 	}
    311 }
    312 
    313 // Sorts modules alphabetically by path.
    314 func sortMods(mods []*govulncheck.Module) {
    315 	slices.SortFunc(mods, func(a, b *govulncheck.Module) int {
    316 		return strings.Compare(a.Path, b.Path)
    317 	})
    318 }