| // Copyright 2022 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 vulns provides utilities to interact with vuln APIs. |
| package vuln |
| |
| import ( |
| "context" |
| "fmt" |
| "go/token" |
| "strings" |
| |
| "golang.org/x/pkgsite/internal/osv" |
| "golang.org/x/pkgsite/internal/stdlib" |
| vers "golang.org/x/pkgsite/internal/version" |
| ) |
| |
| // A Vuln contains information to display about a vulnerability. |
| type Vuln struct { |
| // The vulndb ID. |
| ID string |
| // A description of the vulnerability, or the problem in obtaining it. |
| Details string |
| } |
| |
| // VulnsForPackage obtains vulnerability information for the given package. |
| // If packagePath is empty, it returns all entries for the module at version. |
| // If there is an error, VulnsForPackage returns a single Vuln that describes the error. |
| func VulnsForPackage(ctx context.Context, modulePath, version, packagePath string, vc *Client) []Vuln { |
| if vc == nil { |
| return nil |
| } |
| |
| // Handle special module paths. |
| if modulePath == stdlib.ModulePath { |
| // Stdlib pages requested at master will map to a pseudo version |
| // that puts all vulns in range. |
| // We can't really tell you're at master so version.IsPseudo |
| // is the best we can do. The result is vulns won't be reported for a |
| // pseudoversion that refers to a commit that is in a vulnerable range. |
| switch { |
| case vers.IsPseudo(version): |
| return nil |
| case strings.HasPrefix(packagePath, "cmd/"): |
| modulePath = osv.GoCmdModulePath |
| default: |
| modulePath = osv.GoStdModulePath |
| } |
| } |
| |
| // Get all the vulns for this package/version. |
| entries, err := vc.ByPackage(ctx, &PackageRequest{Module: modulePath, Package: packagePath, Version: version}) |
| if err != nil { |
| return []Vuln{{Details: fmt.Sprintf("could not get vulnerability data: %v", err)}} |
| } |
| |
| return toVulns(entries) |
| } |
| |
| func toVulns(entries []*osv.Entry) []Vuln { |
| if len(entries) == 0 { |
| return nil |
| } |
| |
| vulns := make([]Vuln, len(entries)) |
| for i, e := range entries { |
| vulns[i] = Vuln{ |
| ID: e.ID, |
| Details: e.Summary, |
| } |
| } |
| |
| return vulns |
| } |
| |
| // AffectedComponent holds information about a module/package affected by a certain vulnerability. |
| type AffectedComponent struct { |
| Path string |
| Versions string |
| // Lists of affected symbols (for packages). |
| // If both of these lists are empty, all symbols in the package are affected. |
| ExportedSymbols []string |
| UnexportedSymbols []string |
| } |
| |
| // A pair is like an osv.Range, but each pair is a self-contained 2-tuple |
| // (introduced version, fixed version). |
| type pair struct { |
| intro, fixed string |
| } |
| |
| // collectRangePairs turns a slice of osv Ranges into a more manageable slice of |
| // formatted version pairs. |
| func collectRangePairs(a osv.Affected) []pair { |
| var ( |
| ps []pair |
| p pair |
| prefix string |
| ) |
| if stdlib.Contains(a.Module.Path) { |
| prefix = "go" |
| } else { |
| prefix = "v" |
| } |
| for _, r := range a.Ranges { |
| isSemver := r.Type == osv.RangeTypeSemver |
| for _, v := range r.Events { |
| if v.Introduced != "" { |
| // We expected Introduced and Fixed to alternate, but if |
| // p.intro != "", then they don't. |
| // Keep going in that case, ignoring the first Introduced. |
| p.intro = v.Introduced |
| if p.intro == "0" { |
| p.intro = "" |
| } |
| if isSemver && p.intro != "" { |
| p.intro = prefix + p.intro |
| } |
| } |
| if v.Fixed != "" { |
| p.fixed = v.Fixed |
| if isSemver && p.fixed != "" { |
| p.fixed = prefix + p.fixed |
| } |
| ps = append(ps, p) |
| p = pair{} |
| } |
| } |
| } |
| return ps |
| } |
| |
| // AffectedComponents extracts information about affected packages (and // modules, if there are any with no package information) from the given osv.Entry. |
| func AffectedComponents(e *osv.Entry) (pkgs, modsNoPkgs []*AffectedComponent) { |
| for _, a := range e.Affected { |
| pairs := collectRangePairs(a) |
| var vs []string |
| for _, p := range pairs { |
| var s string |
| if p.intro == "" && p.fixed == "" { |
| // If neither field is set, the vuln applies to all versions. |
| // Leave it blank, the template will render it properly. |
| s = "" |
| } else if p.intro == "" { |
| s = "before " + p.fixed |
| } else if p.fixed == "" { |
| s = p.intro + " and later" |
| } else { |
| s = "from " + p.intro + " before " + p.fixed |
| } |
| vs = append(vs, s) |
| } |
| if len(a.EcosystemSpecific.Packages) == 0 { |
| modsNoPkgs = append(modsNoPkgs, &AffectedComponent{ |
| Path: a.Module.Path, |
| Versions: strings.Join(vs, ", "), |
| }) |
| } |
| for _, p := range a.EcosystemSpecific.Packages { |
| exported, unexported := affectedSymbols(p.Symbols) |
| pkgs = append(pkgs, &AffectedComponent{ |
| Path: p.Path, |
| Versions: strings.Join(vs, ", "), |
| ExportedSymbols: exported, |
| UnexportedSymbols: unexported, |
| // TODO(hyangah): where to place GOOS/GOARCH info |
| }) |
| } |
| } |
| return pkgs, modsNoPkgs |
| } |
| |
| func affectedSymbols(in []string) (e, u []string) { |
| for _, s := range in { |
| exported := true |
| for _, part := range strings.Split(s, ".") { |
| if !token.IsExported(part) { |
| exported = false // exported only if all parts of the symbol name are exported. |
| } |
| } |
| if exported { |
| e = append(e, s) |
| } else { |
| u = append(u, s) |
| } |
| } |
| return e, u |
| } |