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 }