diff --git a/go.mod b/go.mod index b019c6abc66..a26ddc5677b 100644 --- a/go.mod +++ b/go.mod @@ -25,8 +25,7 @@ require ( golang.org/x/sync v0.6.0 golang.org/x/sys v0.16.0 golang.org/x/text v0.14.0 - golang.org/x/tools v0.17.0 - golang.org/x/vuln v1.0.4 + golang.org/x/tools v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index fb2c69e0bb4..31a58bfb1f3 100644 --- a/go.sum +++ b/go.sum @@ -68,10 +68,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I= -golang.org/x/vuln v1.0.4/go.mod h1:NbJdUQhX8jY++FtuhrXs2Eyx0yePo9pF7nPlIjo9aaQ= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/internal/golangorgx/gopls/cache/mod_vuln.go b/internal/golangorgx/gopls/cache/mod_vuln.go deleted file mode 100644 index cf2653f0e66..00000000000 --- a/internal/golangorgx/gopls/cache/mod_vuln.go +++ /dev/null @@ -1,389 +0,0 @@ -// 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 cache - -import ( - "context" - "fmt" - "io" - "os" - "sort" - "strings" - "sync" - - "cuelang.org/go/internal/golangorgx/gopls/cache/metadata" - "cuelang.org/go/internal/golangorgx/gopls/protocol" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/govulncheck" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/osv" - isem "cuelang.org/go/internal/golangorgx/gopls/vulncheck/semver" - "cuelang.org/go/internal/golangorgx/tools/memoize" - "golang.org/x/mod/semver" - "golang.org/x/sync/errgroup" - "golang.org/x/tools/go/packages" - "golang.org/x/vuln/scan" -) - -// ModVuln returns import vulnerability analysis for the given go.mod URI. -// Concurrent requests are combined into a single command. -func (s *Snapshot) ModVuln(ctx context.Context, modURI protocol.DocumentURI) (*vulncheck.Result, error) { - s.mu.Lock() - entry, hit := s.modVulnHandles.Get(modURI) - s.mu.Unlock() - - type modVuln struct { - result *vulncheck.Result - err error - } - - // Cache miss? - if !hit { - handle := memoize.NewPromise("modVuln", func(ctx context.Context, arg interface{}) interface{} { - result, err := modVulnImpl(ctx, arg.(*Snapshot)) - return modVuln{result, err} - }) - - entry = handle - s.mu.Lock() - s.modVulnHandles.Set(modURI, entry, nil) - s.mu.Unlock() - } - - // Await result. - v, err := s.awaitPromise(ctx, entry) - if err != nil { - return nil, err - } - res := v.(modVuln) - return res.result, res.err -} - -// GoVersionForVulnTest is an internal environment variable used in gopls -// testing to examine govulncheck behavior with a go version different -// than what `go version` returns in the system. -const GoVersionForVulnTest = "_GOPLS_TEST_VULNCHECK_GOVERSION" - -// modVulnImpl queries the vulndb and reports which vulnerabilities -// apply to this snapshot. The result contains a set of packages, -// grouped by vuln ID and by module. This implements the "import-based" -// vulnerability report on go.mod files. -func modVulnImpl(ctx context.Context, snapshot *Snapshot) (*vulncheck.Result, error) { - // TODO(hyangah): can we let 'govulncheck' take a package list - // used in the workspace and implement this function? - - // We want to report the intersection of vulnerable packages in the vulndb - // and packages transitively imported by this module ('go list -deps all'). - // We use snapshot.AllMetadata to retrieve the list of packages - // as an approximation. - // - // TODO(hyangah): snapshot.AllMetadata is a superset of - // `go list all` - e.g. when the workspace has multiple main modules - // (multiple go.mod files), that can include packages that are not - // used by this module. Vulncheck behavior with go.work is not well - // defined. Figure out the meaning, and if we decide to present - // the result as if each module is analyzed independently, make - // gopls track a separate build list for each module and use that - // information instead of snapshot.AllMetadata. - allMeta, err := snapshot.AllMetadata(ctx) - if err != nil { - return nil, err - } - - // TODO(hyangah): handle vulnerabilities in the standard library. - - // Group packages by modules since vuln db is keyed by module. - packagesByModule := map[metadata.PackagePath][]*metadata.Package{} - for _, mp := range allMeta { - modulePath := metadata.PackagePath(osv.GoStdModulePath) - if mi := mp.Module; mi != nil { - modulePath = metadata.PackagePath(mi.Path) - } - packagesByModule[modulePath] = append(packagesByModule[modulePath], mp) - } - - var ( - mu sync.Mutex - // Keys are osv.Entry.ID - osvs = map[string]*osv.Entry{} - findings []*govulncheck.Finding - ) - - goVersion := snapshot.Options().Env[GoVersionForVulnTest] - if goVersion == "" { - goVersion = snapshot.GoVersionString() - } - - stdlibModule := &packages.Module{ - Path: osv.GoStdModulePath, - Version: goVersion, - } - - // GOVULNDB may point the test db URI. - db := GetEnv(snapshot, "GOVULNDB") - - var group errgroup.Group - group.SetLimit(10) // limit govulncheck api runs - for _, mps := range packagesByModule { - mps := mps - group.Go(func() error { - effectiveModule := stdlibModule - if m := mps[0].Module; m != nil { - effectiveModule = m - } - for effectiveModule.Replace != nil { - effectiveModule = effectiveModule.Replace - } - ver := effectiveModule.Version - if ver == "" || !isem.Valid(ver) { - // skip invalid version strings. the underlying scan api is strict. - return nil - } - - // TODO(hyangah): batch these requests and add in-memory cache for efficiency. - vulns, err := osvsByModule(ctx, db, effectiveModule.Path+"@"+ver) - if err != nil { - return err - } - if len(vulns) == 0 { // No known vulnerability. - return nil - } - - // set of packages in this module known to gopls. - // This will be lazily initialized when we need it. - var knownPkgs map[metadata.PackagePath]bool - - // Report vulnerabilities that affect packages of this module. - for _, entry := range vulns { - var vulnerablePkgs []*govulncheck.Finding - fixed := fixedVersion(effectiveModule.Path, entry.Affected) - - for _, a := range entry.Affected { - if a.Module.Ecosystem != osv.GoEcosystem || a.Module.Path != effectiveModule.Path { - continue - } - for _, imp := range a.EcosystemSpecific.Packages { - if knownPkgs == nil { - knownPkgs = toPackagePathSet(mps) - } - if knownPkgs[metadata.PackagePath(imp.Path)] { - vulnerablePkgs = append(vulnerablePkgs, &govulncheck.Finding{ - OSV: entry.ID, - FixedVersion: fixed, - Trace: []*govulncheck.Frame{ - { - Module: effectiveModule.Path, - Version: effectiveModule.Version, - Package: imp.Path, - }, - }, - }) - } - } - } - if len(vulnerablePkgs) == 0 { - continue - } - mu.Lock() - osvs[entry.ID] = entry - findings = append(findings, vulnerablePkgs...) - mu.Unlock() - } - return nil - }) - } - if err := group.Wait(); err != nil { - return nil, err - } - - // Sort so the results are deterministic. - sort.Slice(findings, func(i, j int) bool { - x, y := findings[i], findings[j] - if x.OSV != y.OSV { - return x.OSV < y.OSV - } - return x.Trace[0].Package < y.Trace[0].Package - }) - ret := &vulncheck.Result{ - Entries: osvs, - Findings: findings, - Mode: vulncheck.ModeImports, - } - return ret, nil -} - -// TODO(rfindley): this function was exposed during refactoring. Reconsider it. -func GetEnv(snapshot *Snapshot, key string) string { - val, ok := snapshot.Options().Env[key] - if ok { - return val - } - return os.Getenv(key) -} - -// toPackagePathSet transforms the metadata to a set of package paths. -func toPackagePathSet(mds []*metadata.Package) map[metadata.PackagePath]bool { - pkgPaths := make(map[metadata.PackagePath]bool, len(mds)) - for _, md := range mds { - pkgPaths[md.PkgPath] = true - } - return pkgPaths -} - -func fixedVersion(modulePath string, affected []osv.Affected) string { - fixed := latestFixed(modulePath, affected) - if fixed != "" { - fixed = versionString(modulePath, fixed) - } - return fixed -} - -// latestFixed returns the latest fixed version in the list of affected ranges, -// or the empty string if there are no fixed versions. -func latestFixed(modulePath string, as []osv.Affected) string { - v := "" - for _, a := range as { - if a.Module.Path != modulePath { - continue - } - for _, r := range a.Ranges { - if r.Type == osv.RangeTypeSemver { - for _, e := range r.Events { - if e.Fixed != "" && (v == "" || - semver.Compare(isem.CanonicalizeSemverPrefix(e.Fixed), isem.CanonicalizeSemverPrefix(v)) > 0) { - v = e.Fixed - } - } - } - } - } - return v -} - -// versionString prepends a version string prefix (`v` or `go` -// depending on the modulePath) to the given semver-style version string. -func versionString(modulePath, version string) string { - if version == "" { - return "" - } - v := "v" + version - // These are internal Go module paths used by the vuln DB - // when listing vulns in standard library and the go command. - if modulePath == "stdlib" || modulePath == "toolchain" { - return semverToGoTag(v) - } - return v -} - -// semverToGoTag returns the Go standard library repository tag corresponding -// to semver, a version string without the initial "v". -// Go tags differ from standard semantic versions in a few ways, -// such as beginning with "go" instead of "v". -func semverToGoTag(v string) string { - if strings.HasPrefix(v, "v0.0.0") { - return "master" - } - // Special case: v1.0.0 => go1. - if v == "v1.0.0" { - return "go1" - } - if !semver.IsValid(v) { - return fmt.Sprintf("", v) - } - goVersion := semver.Canonical(v) - prerelease := semver.Prerelease(goVersion) - versionWithoutPrerelease := strings.TrimSuffix(goVersion, prerelease) - patch := strings.TrimPrefix(versionWithoutPrerelease, semver.MajorMinor(goVersion)+".") - if patch == "0" { - versionWithoutPrerelease = strings.TrimSuffix(versionWithoutPrerelease, ".0") - } - goVersion = fmt.Sprintf("go%s", strings.TrimPrefix(versionWithoutPrerelease, "v")) - if prerelease != "" { - // Go prereleases look like "beta1" instead of "beta.1". - // "beta1" is bad for sorting (since beta10 comes before beta9), so - // require the dot form. - i := finalDigitsIndex(prerelease) - if i >= 1 { - if prerelease[i-1] != '.' { - return fmt.Sprintf("", v) - } - // Remove the dot. - prerelease = prerelease[:i-1] + prerelease[i:] - } - goVersion += strings.TrimPrefix(prerelease, "-") - } - return goVersion -} - -// finalDigitsIndex returns the index of the first digit in the sequence of digits ending s. -// If s doesn't end in digits, it returns -1. -func finalDigitsIndex(s string) int { - // Assume ASCII (since the semver package does anyway). - var i int - for i = len(s) - 1; i >= 0; i-- { - if s[i] < '0' || s[i] > '9' { - break - } - } - if i == len(s)-1 { - return -1 - } - return i + 1 -} - -// osvsByModule runs a govulncheck database query. -func osvsByModule(ctx context.Context, db, moduleVersion string) ([]*osv.Entry, error) { - var args []string - args = append(args, "-mode=query", "-json") - if db != "" { - args = append(args, "-db="+db) - } - args = append(args, moduleVersion) - - ir, iw := io.Pipe() - handler := &osvReader{} - - var g errgroup.Group - g.Go(func() error { - defer iw.Close() // scan API doesn't close cmd.Stderr/cmd.Stdout. - cmd := scan.Command(ctx, args...) - cmd.Stdout = iw - // TODO(hakim): Do we need to set cmd.Env = getEnvSlices(), - // or is the process environment good enough? - if err := cmd.Start(); err != nil { - return err - } - return cmd.Wait() - }) - g.Go(func() error { - return govulncheck.HandleJSON(ir, handler) - }) - - if err := g.Wait(); err != nil { - return nil, err - } - return handler.entry, nil -} - -// osvReader implements govulncheck.Handler. -type osvReader struct { - entry []*osv.Entry -} - -func (h *osvReader) OSV(entry *osv.Entry) error { - h.entry = append(h.entry, entry) - return nil -} - -func (h *osvReader) Config(config *govulncheck.Config) error { - return nil -} - -func (h *osvReader) Finding(finding *govulncheck.Finding) error { - return nil -} - -func (h *osvReader) Progress(progress *govulncheck.Progress) error { - return nil -} diff --git a/internal/golangorgx/gopls/cache/session.go b/internal/golangorgx/gopls/cache/session.go index 4714a2f499b..6fe624cb23e 100644 --- a/internal/golangorgx/gopls/cache/session.go +++ b/internal/golangorgx/gopls/cache/session.go @@ -24,7 +24,6 @@ import ( "cuelang.org/go/internal/golangorgx/gopls/util/bug" "cuelang.org/go/internal/golangorgx/gopls/util/persistent" "cuelang.org/go/internal/golangorgx/gopls/util/slices" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck" "cuelang.org/go/internal/golangorgx/tools/event" "cuelang.org/go/internal/golangorgx/tools/gocommand" "cuelang.org/go/internal/golangorgx/tools/imports" @@ -249,11 +248,9 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, * parseModHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), parseWorkHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), modTidyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), - modVulnHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), modWhyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), pkgIndex: typerefs.NewPackageIndex(), moduleUpgrades: new(persistent.Map[protocol.DocumentURI, map[string]string]), - vulns: new(persistent.Map[protocol.DocumentURI, *vulncheck.Result]), } // Snapshots must observe all open files, as there are some caching diff --git a/internal/golangorgx/gopls/cache/snapshot.go b/internal/golangorgx/gopls/cache/snapshot.go index 7feef3fb576..a469b128030 100644 --- a/internal/golangorgx/gopls/cache/snapshot.go +++ b/internal/golangorgx/gopls/cache/snapshot.go @@ -40,7 +40,6 @@ import ( "cuelang.org/go/internal/golangorgx/gopls/util/pathutil" "cuelang.org/go/internal/golangorgx/gopls/util/persistent" "cuelang.org/go/internal/golangorgx/gopls/util/slices" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck" "cuelang.org/go/internal/golangorgx/tools/event" "cuelang.org/go/internal/golangorgx/tools/event/label" "cuelang.org/go/internal/golangorgx/tools/event/tag" @@ -180,7 +179,6 @@ type Snapshot struct { // the view's go.mod file. modTidyHandles *persistent.Map[protocol.DocumentURI, *memoize.Promise] // *memoize.Promise[modTidyResult] modWhyHandles *persistent.Map[protocol.DocumentURI, *memoize.Promise] // *memoize.Promise[modWhyResult] - modVulnHandles *persistent.Map[protocol.DocumentURI, *memoize.Promise] // *memoize.Promise[modVulnResult] // importGraph holds a shared import graph to use for type-checking. Adding // more packages to this import graph can speed up type checking, at the @@ -197,9 +195,6 @@ type Snapshot struct { // Each modfile has a map of module name to upgrade version. moduleUpgrades *persistent.Map[protocol.DocumentURI, map[string]string] - // vulns maps each go.mod file's URI to its known vulnerabilities. - vulns *persistent.Map[protocol.DocumentURI, *vulncheck.Result] - // gcOptimizationDetails describes the packages for which we want // optimization details to be included in the diagnostics. gcOptimizationDetails map[metadata.PackageID]unit @@ -246,11 +241,9 @@ func (s *Snapshot) decref() { s.parseModHandles.Destroy() s.parseWorkHandles.Destroy() s.modTidyHandles.Destroy() - s.modVulnHandles.Destroy() s.modWhyHandles.Destroy() s.unloadableFiles.Destroy() s.moduleUpgrades.Destroy() - s.vulns.Destroy() s.done() } } @@ -1675,7 +1668,7 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f // TODO(rfindley): reorganize this function to make the derivation of // needsDiagnosis clearer. - needsDiagnosis := len(changed.GCDetails) > 0 || len(changed.ModuleUpgrades) > 0 || len(changed.Vulns) > 0 + needsDiagnosis := len(changed.GCDetails) > 0 || len(changed.ModuleUpgrades) > 0 bgCtx, cancel := context.WithCancel(bgCtx) result := &Snapshot{ @@ -1700,11 +1693,9 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f parseWorkHandles: cloneWithout(s.parseWorkHandles, changedFiles, &needsDiagnosis), modTidyHandles: cloneWithout(s.modTidyHandles, changedFiles, &needsDiagnosis), modWhyHandles: cloneWithout(s.modWhyHandles, changedFiles, &needsDiagnosis), - modVulnHandles: cloneWithout(s.modVulnHandles, changedFiles, &needsDiagnosis), importGraph: s.importGraph, pkgIndex: s.pkgIndex, moduleUpgrades: cloneWith(s.moduleUpgrades, changed.ModuleUpgrades), - vulns: cloneWith(s.vulns, changed.Vulns), } // Compute the new set of packages for which we want gc details, after @@ -1896,7 +1887,6 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f // // TODO(rfindley): no tests fail if I delete the line below. result.modWhyHandles.Clear() - result.modVulnHandles.Clear() } } diff --git a/internal/golangorgx/gopls/cache/view.go b/internal/golangorgx/gopls/cache/view.go index db2f1d2587e..843e7ea3228 100644 --- a/internal/golangorgx/gopls/cache/view.go +++ b/internal/golangorgx/gopls/cache/view.go @@ -23,7 +23,6 @@ import ( "sort" "strings" "sync" - "time" "cuelang.org/go/internal/golangorgx/gopls/cache/metadata" "cuelang.org/go/internal/golangorgx/gopls/file" @@ -32,7 +31,6 @@ import ( "cuelang.org/go/internal/golangorgx/gopls/util/maps" "cuelang.org/go/internal/golangorgx/gopls/util/pathutil" "cuelang.org/go/internal/golangorgx/gopls/util/slices" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck" "cuelang.org/go/internal/golangorgx/tools/event" "cuelang.org/go/internal/golangorgx/tools/gocommand" "cuelang.org/go/internal/golangorgx/tools/imports" @@ -768,7 +766,6 @@ type StateChange struct { Modifications []file.Modification // if set, the raw modifications originating this change Files map[protocol.DocumentURI]file.Handle ModuleUpgrades map[protocol.DocumentURI]map[string]string - Vulns map[protocol.DocumentURI]*vulncheck.Result GCDetails map[metadata.PackageID]bool // package -> whether or not we want details } @@ -1183,40 +1180,6 @@ func (s *Snapshot) ModuleUpgrades(modfile protocol.DocumentURI) map[string]strin return upgrades } -// MaxGovulncheckResultsAge defines the maximum vulnerability age considered -// valid by gopls. -// -// Mutable for testing. -var MaxGovulncheckResultAge = 1 * time.Hour - -// Vulnerabilities returns known vulnerabilities for the given modfile. -// -// Results more than an hour old are excluded. -// -// TODO(suzmue): replace command.Vuln with a different type, maybe -// https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary? -// -// TODO(rfindley): move to snapshot.go -func (s *Snapshot) Vulnerabilities(modfiles ...protocol.DocumentURI) map[protocol.DocumentURI]*vulncheck.Result { - m := make(map[protocol.DocumentURI]*vulncheck.Result) - now := time.Now() - - s.mu.Lock() - defer s.mu.Unlock() - - if len(modfiles) == 0 { // empty means all modfiles - modfiles = s.vulns.Keys() - } - for _, modfile := range modfiles { - vuln, _ := s.vulns.Get(modfile) - if vuln != nil && now.Sub(vuln.AsOf) > MaxGovulncheckResultAge { - vuln = nil - } - m[modfile] = vuln - } - return m -} - // GoVersion returns the effective release Go version (the X in go1.X) for this // view. func (v *View) GoVersion() int { diff --git a/internal/golangorgx/gopls/cmd/cmd.go b/internal/golangorgx/gopls/cmd/cmd.go index aa5e35314c5..639a5b1b4e0 100644 --- a/internal/golangorgx/gopls/cmd/cmd.go +++ b/internal/golangorgx/gopls/cmd/cmd.go @@ -272,9 +272,7 @@ func (app *Application) mainCommands() []tool.Application { } func (app *Application) internalCommands() []tool.Application { - return []tool.Application{ - &vulncheck{app: app}, - } + return []tool.Application{} } func (app *Application) featureCommands() []tool.Application { diff --git a/internal/golangorgx/gopls/cmd/vulncheck.go b/internal/golangorgx/gopls/cmd/vulncheck.go deleted file mode 100644 index d5f2593dc52..00000000000 --- a/internal/golangorgx/gopls/cmd/vulncheck.go +++ /dev/null @@ -1,47 +0,0 @@ -// 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 cmd - -import ( - "context" - "flag" - "fmt" - "os" - - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/scan" -) - -// vulncheck implements the vulncheck command. -// TODO(hakim): hide from the public. -type vulncheck struct { - app *Application -} - -func (v *vulncheck) Name() string { return "vulncheck" } -func (v *vulncheck) Parent() string { return v.app.Name() } -func (v *vulncheck) Usage() string { return "" } -func (v *vulncheck) ShortHelp() string { - return "run vulncheck analysis (internal-use only)" -} -func (v *vulncheck) DetailedHelp(f *flag.FlagSet) { - fmt.Fprint(f.Output(), ` - WARNING: this command is for internal-use only. - - By default, the command outputs a JSON-encoded - cuelang.org/go/internal/golangorgx/gopls/protocol/command.VulncheckResult - message. - Example: - $ gopls vulncheck - -`) -} - -func (v *vulncheck) Run(ctx context.Context, args ...string) error { - if err := scan.Main(ctx, args...); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - return nil -} diff --git a/internal/golangorgx/gopls/mod/code_lens.go b/internal/golangorgx/gopls/mod/code_lens.go index b756798e1f1..ba3a279088f 100644 --- a/internal/golangorgx/gopls/mod/code_lens.go +++ b/internal/golangorgx/gopls/mod/code_lens.go @@ -24,7 +24,6 @@ func LensFuncs() map[command.Command]golang.LensFunc { command.UpgradeDependency: upgradeLenses, command.Tidy: tidyLens, command.Vendor: vendorLens, - command.RunGovulncheck: vulncheckLenses, } } @@ -165,29 +164,3 @@ func firstRequireRange(fh file.Handle, pm *cache.ParsedModule) (protocol.Range, } return pm.Mapper.OffsetRange(start.Byte, end.Byte) } - -func vulncheckLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { - pm, err := snapshot.ParseMod(ctx, fh) - if err != nil || pm.File == nil { - return nil, err - } - // Place the codelenses near the module statement. - // A module may not have the require block, - // but vulnerabilities can exist in standard libraries. - uri := fh.URI() - rng, err := moduleStmtRange(fh, pm) - if err != nil { - return nil, err - } - - vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ - URI: uri, - Pattern: "./...", - }) - if err != nil { - return nil, err - } - return []protocol.CodeLens{ - {Range: rng, Command: &vulncheck}, - }, nil -} diff --git a/internal/golangorgx/gopls/mod/diagnostics.go b/internal/golangorgx/gopls/mod/diagnostics.go index 25c7b8e18c4..3a58221c0d4 100644 --- a/internal/golangorgx/gopls/mod/diagnostics.go +++ b/internal/golangorgx/gopls/mod/diagnostics.go @@ -18,8 +18,6 @@ import ( "cuelang.org/go/internal/golangorgx/gopls/file" "cuelang.org/go/internal/golangorgx/gopls/protocol" "cuelang.org/go/internal/golangorgx/gopls/protocol/command" - "cuelang.org/go/internal/golangorgx/gopls/settings" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/govulncheck" "cuelang.org/go/internal/golangorgx/tools/event" "golang.org/x/mod/modfile" "golang.org/x/mod/semver" @@ -51,15 +49,6 @@ func UpgradeDiagnostics(ctx context.Context, snapshot *cache.Snapshot) (map[prot return collectDiagnostics(ctx, snapshot, ModUpgradeDiagnostics) } -// VulnerabilityDiagnostics returns vulnerability diagnostics for the active modules in the -// workspace with known vulnerabilities. -func VulnerabilityDiagnostics(ctx context.Context, snapshot *cache.Snapshot) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { - ctx, done := event.Start(ctx, "mod.VulnerabilityDiagnostics", snapshot.Labels()...) - defer done() - - return collectDiagnostics(ctx, snapshot, ModVulnerabilityDiagnostics) -} - func collectDiagnostics(ctx context.Context, snapshot *cache.Snapshot, diagFn func(context.Context, *cache.Snapshot, file.Handle) ([]*cache.Diagnostic, error)) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { g, ctx := errgroup.WithContext(ctx) cpulimit := runtime.GOMAXPROCS(0) @@ -176,256 +165,6 @@ func ModUpgradeDiagnostics(ctx context.Context, snapshot *cache.Snapshot, fh fil const upgradeCodeActionPrefix = "Upgrade to " -// ModVulnerabilityDiagnostics adds diagnostics for vulnerabilities in individual modules -// if the vulnerability is recorded in the view. -func ModVulnerabilityDiagnostics(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) (vulnDiagnostics []*cache.Diagnostic, err error) { - pm, err := snapshot.ParseMod(ctx, fh) - if err != nil { - // Don't return an error if there are parse error diagnostics to be shown, but also do not - // continue since we won't be able to show the vulnerability diagnostics. - if pm != nil && len(pm.ParseErrors) != 0 { - return nil, nil - } - return nil, err - } - - diagSource := cache.Govulncheck - vs := snapshot.Vulnerabilities(fh.URI())[fh.URI()] - if vs == nil && snapshot.Options().Vulncheck == settings.ModeVulncheckImports { - vs, err = snapshot.ModVuln(ctx, fh.URI()) - if err != nil { - return nil, err - } - diagSource = cache.Vulncheck - } - if vs == nil || len(vs.Findings) == 0 { - return nil, nil - } - - suggestRunOrResetGovulncheck, err := suggestGovulncheckAction(diagSource == cache.Govulncheck, fh.URI()) - if err != nil { - // must not happen - return nil, err // TODO: bug report - } - vulnsByModule := make(map[string][]*govulncheck.Finding) - - for _, finding := range vs.Findings { - if vuln, typ := foundVuln(finding); typ == vulnCalled || typ == vulnImported { - vulnsByModule[vuln.Module] = append(vulnsByModule[vuln.Module], finding) - } - } - for _, req := range pm.File.Require { - mod := req.Mod.Path - findings := vulnsByModule[mod] - if len(findings) == 0 { - continue - } - // note: req.Syntax is the line corresponding to 'require', which means - // req.Syntax.Start can point to the beginning of the "require" keyword - // for a single line require (e.g. "require golang.org/x/mod v0.0.0"). - start := req.Syntax.Start.Byte - if len(req.Syntax.Token) == 3 { - start += len("require ") - } - rng, err := pm.Mapper.OffsetRange(start, req.Syntax.End.Byte) - if err != nil { - return nil, err - } - // Map affecting vulns to 'warning' level diagnostics, - // others to 'info' level diagnostics. - // Fixes will include only the upgrades for warning level diagnostics. - var warningFixes, infoFixes []cache.SuggestedFix - var warningSet, infoSet = map[string]bool{}, map[string]bool{} - for _, finding := range findings { - // It is possible that the source code was changed since the last - // govulncheck run and information in the `vulns` info is stale. - // For example, imagine that a user is in the middle of updating - // problematic modules detected by the govulncheck run by applying - // quick fixes. Stale diagnostics can be confusing and prevent the - // user from quickly locating the next module to fix. - // Ideally we should rerun the analysis with the updated module - // dependencies or any other code changes, but we are not yet - // in the position of automatically triggering the analysis - // (govulncheck can take a while). We also don't know exactly what - // part of source code was changed since `vulns` was computed. - // As a heuristic, we assume that a user upgrades the affecting - // module to the version with the fix or the latest one, and if the - // version in the require statement is equal to or higher than the - // fixed version, skip generating a diagnostic about the vulnerability. - // Eventually, the user has to rerun govulncheck. - if finding.FixedVersion != "" && semver.IsValid(req.Mod.Version) && semver.Compare(finding.FixedVersion, req.Mod.Version) <= 0 { - continue - } - switch _, typ := foundVuln(finding); typ { - case vulnImported: - infoSet[finding.OSV] = true - case vulnCalled: - warningSet[finding.OSV] = true - } - // Upgrade to the exact version we offer the user, not the most recent. - if fixedVersion := finding.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { - cmd, err := getUpgradeCodeAction(fh, req, fixedVersion) - if err != nil { - return nil, err // TODO: bug report - } - sf := cache.SuggestedFixFromCommand(cmd, protocol.QuickFix) - switch _, typ := foundVuln(finding); typ { - case vulnImported: - infoFixes = append(infoFixes, sf) - case vulnCalled: - warningFixes = append(warningFixes, sf) - } - } - } - - if len(warningSet) == 0 && len(infoSet) == 0 { - continue - } - // Remove affecting osvs from the non-affecting osv list if any. - if len(warningSet) > 0 { - for k := range infoSet { - if warningSet[k] { - delete(infoSet, k) - } - } - } - // Add an upgrade for module@latest. - // TODO(suzmue): verify if latest is the same as fixedVersion. - latest, err := getUpgradeCodeAction(fh, req, "latest") - if err != nil { - return nil, err // TODO: bug report - } - sf := cache.SuggestedFixFromCommand(latest, protocol.QuickFix) - if len(warningFixes) > 0 { - warningFixes = append(warningFixes, sf) - } - if len(infoFixes) > 0 { - infoFixes = append(infoFixes, sf) - } - if len(warningSet) > 0 { - warning := sortedKeys(warningSet) - warningFixes = append(warningFixes, suggestRunOrResetGovulncheck) - vulnDiagnostics = append(vulnDiagnostics, &cache.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: protocol.SeverityWarning, - Source: diagSource, - Message: getVulnMessage(req.Mod.Path, warning, true, diagSource == cache.Govulncheck), - SuggestedFixes: warningFixes, - }) - } - if len(infoSet) > 0 { - info := sortedKeys(infoSet) - infoFixes = append(infoFixes, suggestRunOrResetGovulncheck) - vulnDiagnostics = append(vulnDiagnostics, &cache.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: protocol.SeverityInformation, - Source: diagSource, - Message: getVulnMessage(req.Mod.Path, info, false, diagSource == cache.Govulncheck), - SuggestedFixes: infoFixes, - }) - } - } - - // TODO(hyangah): place this diagnostic on the `go` directive or `toolchain` directive - // after https://go.dev/issue/57001. - const diagnoseStdLib = false - - // If diagnosing the stdlib, add standard library vulnerability diagnostics - // on the module declaration. - // - // Only proceed if we have a valid module declaration on which to position - // the diagnostics. - if diagnoseStdLib && pm.File.Module != nil && pm.File.Module.Syntax != nil { - // Add standard library vulnerabilities. - stdlibVulns := vulnsByModule["stdlib"] - if len(stdlibVulns) == 0 { - return vulnDiagnostics, nil - } - - // Put the standard library diagnostic on the module declaration. - rng, err := pm.Mapper.OffsetRange(pm.File.Module.Syntax.Start.Byte, pm.File.Module.Syntax.End.Byte) - if err != nil { - return vulnDiagnostics, nil // TODO: bug report - } - - var warningSet, infoSet = map[string]bool{}, map[string]bool{} - for _, finding := range stdlibVulns { - switch _, typ := foundVuln(finding); typ { - case vulnImported: - infoSet[finding.OSV] = true - case vulnCalled: - warningSet[finding.OSV] = true - } - } - if len(warningSet) > 0 { - warning := sortedKeys(warningSet) - fixes := []cache.SuggestedFix{suggestRunOrResetGovulncheck} - vulnDiagnostics = append(vulnDiagnostics, &cache.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: protocol.SeverityWarning, - Source: diagSource, - Message: getVulnMessage("go", warning, true, diagSource == cache.Govulncheck), - SuggestedFixes: fixes, - }) - - // remove affecting osvs from the non-affecting osv list if any. - for k := range infoSet { - if warningSet[k] { - delete(infoSet, k) - } - } - } - if len(infoSet) > 0 { - info := sortedKeys(infoSet) - fixes := []cache.SuggestedFix{suggestRunOrResetGovulncheck} - vulnDiagnostics = append(vulnDiagnostics, &cache.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: protocol.SeverityInformation, - Source: diagSource, - Message: getVulnMessage("go", info, false, diagSource == cache.Govulncheck), - SuggestedFixes: fixes, - }) - } - } - - return vulnDiagnostics, nil -} - -type vulnFindingType int - -const ( - vulnUnknown vulnFindingType = iota - vulnCalled - vulnImported - vulnRequired -) - -// foundVuln returns the frame info describing discovered vulnerable symbol/package/module -// and how this vulnerability affects the analyzed package or module. -func foundVuln(finding *govulncheck.Finding) (*govulncheck.Frame, vulnFindingType) { - // finding.Trace is sorted from the imported vulnerable symbol to - // the entry point in the callstack. - // If Function is set, then Package must be set. Module will always be set. - // If Function is set it was found in the call graph, otherwise if Package is set - // it was found in the import graph, otherwise it was found in the require graph. - // See the documentation of govulncheck.Finding. - if len(finding.Trace) == 0 { // this shouldn't happen, but just in case... - return nil, vulnUnknown - } - vuln := finding.Trace[0] - if vuln.Package == "" { - return vuln, vulnRequired - } - if vuln.Function == "" { - return vuln, vulnImported - } - return vuln, vulnCalled -} - func sortedKeys(m map[string]bool) []string { ret := make([]string, 0, len(m)) for k := range m { @@ -435,60 +174,6 @@ func sortedKeys(m map[string]bool) []string { return ret } -// suggestGovulncheckAction returns a code action that suggests either run govulncheck -// for more accurate investigation (if the present vulncheck diagnostics are based on -// analysis less accurate than govulncheck) or reset the existing govulncheck result -// (if the present vulncheck diagnostics are already based on govulncheck run). -func suggestGovulncheckAction(fromGovulncheck bool, uri protocol.DocumentURI) (cache.SuggestedFix, error) { - if fromGovulncheck { - resetVulncheck, err := command.NewResetGoModDiagnosticsCommand("Reset govulncheck result", command.ResetGoModDiagnosticsArgs{ - URIArg: command.URIArg{URI: uri}, - DiagnosticSource: string(cache.Govulncheck), - }) - if err != nil { - return cache.SuggestedFix{}, err - } - return cache.SuggestedFixFromCommand(resetVulncheck, protocol.QuickFix), nil - } - vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck to verify", command.VulncheckArgs{ - URI: uri, - Pattern: "./...", - }) - if err != nil { - return cache.SuggestedFix{}, err - } - return cache.SuggestedFixFromCommand(vulncheck, protocol.QuickFix), nil -} - -func getVulnMessage(mod string, vulns []string, used, fromGovulncheck bool) string { - var b strings.Builder - if used { - switch len(vulns) { - case 1: - fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", mod, vulns[0]) - default: - fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", mod, strings.Join(vulns, ", ")) - } - } else { - if fromGovulncheck { - switch len(vulns) { - case 1: - fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", mod, vulns[0]) - default: - fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", mod, strings.Join(vulns, ", ")) - } - } else { - switch len(vulns) { - case 1: - fmt.Fprintf(&b, "%v has a vulnerability %v.", mod, vulns[0]) - default: - fmt.Fprintf(&b, "%v has known vulnerabilities %v.", mod, strings.Join(vulns, ", ")) - } - } - } - return b.String() -} - // href returns the url for the vulnerability information. // Eventually we should retrieve the url embedded in the osv.Entry. // While vuln.go.dev is under development, this always returns diff --git a/internal/golangorgx/gopls/mod/hover.go b/internal/golangorgx/gopls/mod/hover.go index 3fc5688403b..db6fcf9ff9a 100644 --- a/internal/golangorgx/gopls/mod/hover.go +++ b/internal/golangorgx/gopls/mod/hover.go @@ -8,19 +8,14 @@ import ( "bytes" "context" "fmt" - "sort" "strings" "cuelang.org/go/internal/golangorgx/gopls/cache" "cuelang.org/go/internal/golangorgx/gopls/file" "cuelang.org/go/internal/golangorgx/gopls/protocol" "cuelang.org/go/internal/golangorgx/gopls/settings" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/govulncheck" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/osv" "cuelang.org/go/internal/golangorgx/tools/event" "golang.org/x/mod/modfile" - "golang.org/x/mod/semver" ) func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) { @@ -50,10 +45,6 @@ func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, positi return nil, fmt.Errorf("computing cursor position: %w", err) } - // If the cursor position is on a module statement - if hover, ok := hoverOnModuleStatement(ctx, pm, offset, snapshot, fh); ok { - return hover, nil - } return hoverOnRequireStatement(ctx, pm, offset, snapshot, fh) } @@ -83,19 +74,6 @@ func hoverOnRequireStatement(ctx context.Context, pm *cache.ParsedModule, offset return nil, nil } - // Get the vulnerability info. - fromGovulncheck := true - vs := snapshot.Vulnerabilities(fh.URI())[fh.URI()] - if vs == nil && snapshot.Options().Vulncheck == settings.ModeVulncheckImports { - var err error - vs, err = snapshot.ModVuln(ctx, fh.URI()) - if err != nil { - return nil, err - } - fromGovulncheck = false - } - affecting, nonaffecting, osvs := lookupVulns(vs, req.Mod.Path, req.Mod.Version) - // Get the `go mod why` results for the given file. why, err := snapshot.ModWhy(ctx, fh) if err != nil { @@ -117,55 +95,16 @@ func hoverOnRequireStatement(ctx context.Context, pm *cache.ParsedModule, offset isPrivate := snapshot.IsGoPrivatePath(req.Mod.Path) header := formatHeader(req.Mod.Path, options) explanation = formatExplanation(explanation, req, options, isPrivate) - vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck) return &protocol.Hover{ Contents: protocol.MarkupContent{ Kind: options.PreferredContentFormat, - Value: header + vulns + explanation, + Value: header + explanation, }, Range: rng, }, nil } -func hoverOnModuleStatement(ctx context.Context, pm *cache.ParsedModule, offset int, snapshot *cache.Snapshot, fh file.Handle) (*protocol.Hover, bool) { - module := pm.File.Module - if module == nil { - return nil, false // no module stmt - } - if offset < module.Syntax.Start.Byte || offset > module.Syntax.End.Byte { - return nil, false // cursor not in module stmt - } - - rng, err := pm.Mapper.OffsetRange(module.Syntax.Start.Byte, module.Syntax.End.Byte) - if err != nil { - return nil, false - } - fromGovulncheck := true - vs := snapshot.Vulnerabilities(fh.URI())[fh.URI()] - - if vs == nil && snapshot.Options().Vulncheck == settings.ModeVulncheckImports { - vs, err = snapshot.ModVuln(ctx, fh.URI()) - if err != nil { - return nil, false - } - fromGovulncheck = false - } - modpath := "stdlib" - goVersion := snapshot.View().GoVersionString() - affecting, nonaffecting, osvs := lookupVulns(vs, modpath, goVersion) - options := snapshot.Options() - vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck) - - return &protocol.Hover{ - Contents: protocol.MarkupContent{ - Kind: options.PreferredContentFormat, - Value: vulns, - }, - Range: rng, - }, true -} - func formatHeader(modpath string, options *settings.Options) string { var b strings.Builder // Write the heading as an H3. @@ -178,66 +117,6 @@ func formatHeader(modpath string, options *settings.Options) string { return b.String() } -func lookupVulns(vulns *vulncheck.Result, modpath, version string) (affecting, nonaffecting []*govulncheck.Finding, osvs map[string]*osv.Entry) { - if vulns == nil || len(vulns.Entries) == 0 { - return nil, nil, nil - } - for _, finding := range vulns.Findings { - vuln, typ := foundVuln(finding) - if vuln.Module != modpath { - continue - } - // It is possible that the source code was changed since the last - // govulncheck run and information in the `vulns` info is stale. - // For example, imagine that a user is in the middle of updating - // problematic modules detected by the govulncheck run by applying - // quick fixes. Stale diagnostics can be confusing and prevent the - // user from quickly locating the next module to fix. - // Ideally we should rerun the analysis with the updated module - // dependencies or any other code changes, but we are not yet - // in the position of automatically triggering the analysis - // (govulncheck can take a while). We also don't know exactly what - // part of source code was changed since `vulns` was computed. - // As a heuristic, we assume that a user upgrades the affecting - // module to the version with the fix or the latest one, and if the - // version in the require statement is equal to or higher than the - // fixed version, skip the vulnerability information in the hover. - // Eventually, the user has to rerun govulncheck. - if finding.FixedVersion != "" && semver.IsValid(version) && semver.Compare(finding.FixedVersion, version) <= 0 { - continue - } - switch typ { - case vulnCalled: - affecting = append(affecting, finding) - case vulnImported: - nonaffecting = append(nonaffecting, finding) - } - } - - // Remove affecting elements from nonaffecting. - // An OSV entry can appear in both lists if an OSV entry covers - // multiple packages imported but not all vulnerable symbols are used. - // The current wording of hover message doesn't clearly - // present this case well IMO, so let's skip reporting nonaffecting. - if len(affecting) > 0 && len(nonaffecting) > 0 { - affectingSet := map[string]bool{} - for _, f := range affecting { - affectingSet[f.OSV] = true - } - n := 0 - for _, v := range nonaffecting { - if !affectingSet[v.OSV] { - nonaffecting[n] = v - n++ - } - } - nonaffecting = nonaffecting[:n] - } - sort.Slice(nonaffecting, func(i, j int) bool { return nonaffecting[i].OSV < nonaffecting[j].OSV }) - sort.Slice(affecting, func(i, j int) bool { return affecting[i].OSV < affecting[j].OSV }) - return affecting, nonaffecting, vulns.Entries -} - func fixedVersion(fixed string) string { if fixed == "" { return "No fix is available." @@ -245,88 +124,6 @@ func fixedVersion(fixed string) string { return "Fixed in " + fixed + "." } -func formatVulnerabilities(affecting, nonaffecting []*govulncheck.Finding, osvs map[string]*osv.Entry, options *settings.Options, fromGovulncheck bool) string { - if len(osvs) == 0 || (len(affecting) == 0 && len(nonaffecting) == 0) { - return "" - } - byOSV := func(findings []*govulncheck.Finding) map[string][]*govulncheck.Finding { - m := make(map[string][]*govulncheck.Finding) - for _, f := range findings { - m[f.OSV] = append(m[f.OSV], f) - } - return m - } - affectingByOSV := byOSV(affecting) - nonaffectingByOSV := byOSV(nonaffecting) - - // TODO(hyangah): can we use go templates to generate hover messages? - // Then, we can use a different template for markdown case. - useMarkdown := options.PreferredContentFormat == protocol.Markdown - - var b strings.Builder - - if len(affectingByOSV) > 0 { - // TODO(hyangah): make the message more eyecatching (icon/codicon/color) - if len(affectingByOSV) == 1 { - fmt.Fprintf(&b, "\n**WARNING:** Found %d reachable vulnerability.\n", len(affectingByOSV)) - } else { - fmt.Fprintf(&b, "\n**WARNING:** Found %d reachable vulnerabilities.\n", len(affectingByOSV)) - } - } - for id, findings := range affectingByOSV { - fix := fixedVersion(findings[0].FixedVersion) - pkgs := vulnerablePkgsInfo(findings, useMarkdown) - osvEntry := osvs[id] - - if useMarkdown { - fmt.Fprintf(&b, "- [**%v**](%v) %v%v\n%v\n", id, href(id), osvEntry.Summary, pkgs, fix) - } else { - fmt.Fprintf(&b, " - [%v] %v (%v) %v%v\n", id, osvEntry.Summary, href(id), pkgs, fix) - } - } - if len(nonaffecting) > 0 { - if fromGovulncheck { - fmt.Fprintf(&b, "\n**Note:** The project imports packages with known vulnerabilities, but does not call the vulnerable code.\n") - } else { - fmt.Fprintf(&b, "\n**Note:** The project imports packages with known vulnerabilities. Use `govulncheck` to check if the project uses vulnerable symbols.\n") - } - } - for k, findings := range nonaffectingByOSV { - fix := fixedVersion(findings[0].FixedVersion) - pkgs := vulnerablePkgsInfo(findings, useMarkdown) - osvEntry := osvs[k] - - if useMarkdown { - fmt.Fprintf(&b, "- [%v](%v) %v%v\n%v\n", k, href(k), osvEntry.Summary, pkgs, fix) - } else { - fmt.Fprintf(&b, " - [%v] %v (%v) %v\n%v\n", k, osvEntry.Summary, href(k), pkgs, fix) - } - } - b.WriteString("\n") - return b.String() -} - -func vulnerablePkgsInfo(findings []*govulncheck.Finding, useMarkdown bool) string { - var b strings.Builder - seen := map[string]bool{} - for _, f := range findings { - p := f.Trace[0].Package - if !seen[p] { - seen[p] = true - if useMarkdown { - b.WriteString("\n * `") - } else { - b.WriteString("\n ") - } - b.WriteString(p) - if useMarkdown { - b.WriteString("`") - } - } - } - return b.String() -} - func formatExplanation(text string, req *modfile.Require, options *settings.Options, isPrivate bool) string { text = strings.TrimSuffix(text, "\n") splt := strings.Split(text, "\n") diff --git a/internal/golangorgx/gopls/protocol/command/command_gen.go b/internal/golangorgx/gopls/protocol/command/command_gen.go index b2e52da5069..abaddc538b9 100644 --- a/internal/golangorgx/gopls/protocol/command/command_gen.go +++ b/internal/golangorgx/gopls/protocol/command/command_gen.go @@ -30,7 +30,6 @@ const ( CheckUpgrades Command = "check_upgrades" DiagnoseFiles Command = "diagnose_files" EditGoDirective Command = "edit_go_directive" - FetchVulncheckResult Command = "fetch_vulncheck_result" GCDetails Command = "gc_details" Generate Command = "generate" GoGetPackage Command = "go_get_package" @@ -42,7 +41,6 @@ const ( RemoveDependency Command = "remove_dependency" ResetGoModDiagnostics Command = "reset_go_mod_diagnostics" RunGoWorkCommand Command = "run_go_work_command" - RunGovulncheck Command = "run_govulncheck" RunTests Command = "run_tests" StartDebugging Command = "start_debugging" StartProfile Command = "start_profile" @@ -66,7 +64,6 @@ var Commands = []Command{ CheckUpgrades, DiagnoseFiles, EditGoDirective, - FetchVulncheckResult, GCDetails, Generate, GoGetPackage, @@ -78,7 +75,6 @@ var Commands = []Command{ RemoveDependency, ResetGoModDiagnostics, RunGoWorkCommand, - RunGovulncheck, RunTests, StartDebugging, StartProfile, @@ -143,12 +139,6 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.EditGoDirective(ctx, a0) - case "gopls.fetch_vulncheck_result": - var a0 URIArg - if err := UnmarshalArgs(params.Arguments, &a0); err != nil { - return nil, err - } - return s.FetchVulncheckResult(ctx, a0) case "gopls.gc_details": var a0 protocol.DocumentURI if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -207,12 +197,6 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.RunGoWorkCommand(ctx, a0) - case "gopls.run_govulncheck": - var a0 VulncheckArgs - if err := UnmarshalArgs(params.Arguments, &a0); err != nil { - return nil, err - } - return s.RunGovulncheck(ctx, a0) case "gopls.run_tests": var a0 RunTestsArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -379,18 +363,6 @@ func NewEditGoDirectiveCommand(title string, a0 EditGoDirectiveArgs) (protocol.C }, nil } -func NewFetchVulncheckResultCommand(title string, a0 URIArg) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ - Title: title, - Command: "gopls.fetch_vulncheck_result", - Arguments: args, - }, nil -} - func NewGCDetailsCommand(title string, a0 protocol.DocumentURI) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { @@ -523,18 +495,6 @@ func NewRunGoWorkCommandCommand(title string, a0 RunGoWorkArgs) (protocol.Comman }, nil } -func NewRunGovulncheckCommand(title string, a0 VulncheckArgs) (protocol.Command, error) { - args, err := MarshalArgs(a0) - if err != nil { - return protocol.Command{}, err - } - return protocol.Command{ - Title: title, - Command: "gopls.run_govulncheck", - Arguments: args, - }, nil -} - func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { diff --git a/internal/golangorgx/gopls/protocol/command/interface.go b/internal/golangorgx/gopls/protocol/command/interface.go index 2edb1e98cba..bf4d7f84541 100644 --- a/internal/golangorgx/gopls/protocol/command/interface.go +++ b/internal/golangorgx/gopls/protocol/command/interface.go @@ -16,7 +16,6 @@ import ( "context" "cuelang.org/go/internal/golangorgx/gopls/protocol" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck" ) // Interface defines the interface gopls exposes for the @@ -167,16 +166,6 @@ type Interface interface { // runner. StopProfile(context.Context, StopProfileArgs) (StopProfileResult, error) - // RunGovulncheck: Run vulncheck - // - // Run vulnerability check (`govulncheck`). - RunGovulncheck(context.Context, VulncheckArgs) (RunVulncheckResult, error) - - // FetchVulncheckResult: Get known vulncheck result - // - // Fetch the result of latest vulnerability check (`govulncheck`). - FetchVulncheckResult(context.Context, URIArg) (map[protocol.DocumentURI]*vulncheck.Result, error) - // MemStats: Fetch memory statistics // // Call runtime.GC multiple times and return memory statistics as reported by diff --git a/internal/golangorgx/gopls/server/command.go b/internal/golangorgx/gopls/server/command.go index 61899c0af46..205b8264e39 100644 --- a/internal/golangorgx/gopls/server/command.go +++ b/internal/golangorgx/gopls/server/command.go @@ -29,11 +29,8 @@ import ( "cuelang.org/go/internal/golangorgx/gopls/progress" "cuelang.org/go/internal/golangorgx/gopls/protocol" "cuelang.org/go/internal/golangorgx/gopls/protocol/command" - "cuelang.org/go/internal/golangorgx/gopls/settings" "cuelang.org/go/internal/golangorgx/gopls/telemetry" "cuelang.org/go/internal/golangorgx/gopls/util/bug" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/scan" "cuelang.org/go/internal/golangorgx/tools/diff" "cuelang.org/go/internal/golangorgx/tools/event" "cuelang.org/go/internal/golangorgx/tools/gocommand" @@ -315,9 +312,6 @@ func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, args command ModuleUpgrades: map[protocol.DocumentURI]map[string]string{ deps.fh.URI(): nil, }, - Vulns: map[protocol.DocumentURI]*vulncheck.Result{ - deps.fh.URI(): nil, - }, }) }) }) @@ -930,95 +924,6 @@ func (c *commandHandler) StopProfile(ctx context.Context, args command.StopProfi return result, nil } -func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.URIArg) (map[protocol.DocumentURI]*vulncheck.Result, error) { - ret := map[protocol.DocumentURI]*vulncheck.Result{} - err := c.run(ctx, commandConfig{forURI: arg.URI}, func(ctx context.Context, deps commandDeps) error { - if deps.snapshot.Options().Vulncheck == settings.ModeVulncheckImports { - for _, modfile := range deps.snapshot.View().ModFiles() { - res, err := deps.snapshot.ModVuln(ctx, modfile) - if err != nil { - return err - } - ret[modfile] = res - } - } - // Overwrite if there is any govulncheck-based result. - for modfile, result := range deps.snapshot.Vulnerabilities() { - ret[modfile] = result - } - return nil - }) - return ret, err -} - -func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.VulncheckArgs) (command.RunVulncheckResult, error) { - if args.URI == "" { - return command.RunVulncheckResult{}, errors.New("VulncheckArgs is missing URI field") - } - - // Return the workdone token so that clients can identify when this - // vulncheck invocation is complete. - // - // Since the run function executes asynchronously, we use a channel to - // synchronize the start of the run and return the token. - tokenChan := make(chan protocol.ProgressToken, 1) - err := c.run(ctx, commandConfig{ - async: true, // need to be async to be cancellable - progress: "govulncheck", - requireSave: true, - forURI: args.URI, - }, func(ctx context.Context, deps commandDeps) error { - tokenChan <- deps.work.Token() - - workDoneWriter := progress.NewWorkDoneWriter(ctx, deps.work) - dir := filepath.Dir(args.URI.Path()) - pattern := args.Pattern - - result, err := scan.RunGovulncheck(ctx, pattern, deps.snapshot, dir, workDoneWriter) - if err != nil { - return err - } - - snapshot, release, err := c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{ - Vulns: map[protocol.DocumentURI]*vulncheck.Result{args.URI: result}, - }) - if err != nil { - return err - } - defer release() - c.s.diagnoseSnapshot(snapshot, nil, 0) - - affecting := make(map[string]bool, len(result.Entries)) - for _, finding := range result.Findings { - if len(finding.Trace) > 1 { // at least 2 frames if callstack exists (vulnerability, entry) - affecting[finding.OSV] = true - } - } - if len(affecting) == 0 { - showMessage(ctx, c.s.client, protocol.Info, "No vulnerabilities found") - return nil - } - affectingOSVs := make([]string, 0, len(affecting)) - for id := range affecting { - affectingOSVs = append(affectingOSVs, id) - } - sort.Strings(affectingOSVs) - - showMessage(ctx, c.s.client, protocol.Warning, fmt.Sprintf("Found %v", strings.Join(affectingOSVs, ", "))) - - return nil - }) - if err != nil { - return command.RunVulncheckResult{}, err - } - select { - case <-ctx.Done(): - return command.RunVulncheckResult{}, ctx.Err() - case token := <-tokenChan: - return command.RunVulncheckResult{Token: token}, nil - } -} - // MemStats implements the MemStats command. It returns an error as a // future-proof API, but the resulting error is currently always nil. func (c *commandHandler) MemStats(ctx context.Context) (command.MemStatsResult, error) { diff --git a/internal/golangorgx/gopls/server/diagnostics.go b/internal/golangorgx/gopls/server/diagnostics.go index 493bc70c46d..963e534c300 100644 --- a/internal/golangorgx/gopls/server/diagnostics.go +++ b/internal/golangorgx/gopls/server/diagnostics.go @@ -356,13 +356,6 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa } store("diagnosing go.mod upgrades", upgradeReports, upgradeErr) - // Diagnose vulnerabilities. - vulnReports, vulnErr := mod.VulnerabilityDiagnostics(ctx, snapshot) - if ctx.Err() != nil { - return nil, ctx.Err() - } - store("diagnosing vulnerabilities", vulnReports, vulnErr) - workspacePkgs, err := snapshot.WorkspaceMetadata(ctx) if s.shouldIgnoreError(snapshot, err) { return diagnostics, ctx.Err() diff --git a/internal/golangorgx/gopls/settings/default.go b/internal/golangorgx/gopls/settings/default.go index fdb32f6de8b..baff0772c7e 100644 --- a/internal/golangorgx/gopls/settings/default.go +++ b/internal/golangorgx/gopls/settings/default.go @@ -73,7 +73,6 @@ func DefaultOptions(overrides ...func(*Options)) *Options { Inline: true, Nil: true, }, - Vulncheck: ModeVulncheckOff, DiagnosticsDelay: 1 * time.Second, DiagnosticsTrigger: DiagnosticsOnEdit, AnalysisProgressReporting: true, @@ -103,7 +102,6 @@ func DefaultOptions(overrides ...func(*Options)) *Options { string(command.GCDetails): false, string(command.UpgradeDependency): true, string(command.Vendor): true, - // TODO(hyangah): enable command.RunGovulncheck. }, }, }, diff --git a/internal/golangorgx/gopls/settings/settings.go b/internal/golangorgx/gopls/settings/settings.go index 67ef360c96f..e025fb19e2e 100644 --- a/internal/golangorgx/gopls/settings/settings.go +++ b/internal/golangorgx/gopls/settings/settings.go @@ -342,9 +342,6 @@ type DiagnosticOptions struct { // that should be reported by the gc_details command. Annotations map[Annotation]bool `status:"experimental"` - // Vulncheck enables vulnerability scanning. - Vulncheck VulncheckMode `status:"experimental"` - // DiagnosticsDelay controls the amount of time that gopls waits // after the most recent file modification before computing deep diagnostics. // Simple diagnostics (parsing and type-checking) are always run immediately @@ -653,18 +650,6 @@ const ( Structured HoverKind = "Structured" ) -type VulncheckMode string - -const ( - // Disable vulnerability analysis. - ModeVulncheckOff VulncheckMode = "Off" - // In Imports mode, `gopls` will report vulnerabilities that affect packages - // directly and indirectly used by the analyzed main module. - ModeVulncheckImports VulncheckMode = "Imports" - - // TODO: VulncheckRequire, VulncheckCallgraph -) - type DiagnosticsTrigger string const ( @@ -833,9 +818,6 @@ func (o *Options) enableAllExperimentMaps() { if _, ok := o.Codelenses[string(command.GCDetails)]; !ok { o.Codelenses[string(command.GCDetails)] = true } - if _, ok := o.Codelenses[string(command.RunGovulncheck)]; !ok { - o.Codelenses[string(command.RunGovulncheck)] = true - } if _, ok := o.Analyses[unusedvariable.Analyzer.Name]; !ok { o.Analyses[unusedvariable.Analyzer.Name] = true } @@ -1000,14 +982,6 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "annotations": result.setAnnotationMap(&o.Annotations) - case "vulncheck": - if s, ok := result.asOneOf( - string(ModeVulncheckOff), - string(ModeVulncheckImports), - ); ok { - o.Vulncheck = VulncheckMode(s) - } - case "codelenses", "codelens": var lensOverrides map[string]bool result.setBoolMap(&lensOverrides) diff --git a/internal/golangorgx/gopls/vulncheck/govulncheck/govulncheck.go b/internal/golangorgx/gopls/vulncheck/govulncheck/govulncheck.go deleted file mode 100644 index ef2d8b6069c..00000000000 --- a/internal/golangorgx/gopls/vulncheck/govulncheck/govulncheck.go +++ /dev/null @@ -1,160 +0,0 @@ -// 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. - -// Code generated by copying from golang.org/x/vuln@v1.0.1 (go run copier.go); DO NOT EDIT. - -// Package govulncheck contains the JSON output structs for govulncheck. -package govulncheck - -import ( - "time" - - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/osv" -) - -const ( - // ProtocolVersion is the current protocol version this file implements - ProtocolVersion = "v1.0.0" -) - -// Message is an entry in the output stream. It will always have exactly one -// field filled in. -type Message struct { - Config *Config `json:"config,omitempty"` - Progress *Progress `json:"progress,omitempty"` - OSV *osv.Entry `json:"osv,omitempty"` - Finding *Finding `json:"finding,omitempty"` -} - -// Config must occur as the first message of a stream and informs the client -// about the information used to generate the findings. -// The only required field is the protocol version. -type Config struct { - // ProtocolVersion specifies the version of the JSON protocol. - ProtocolVersion string `json:"protocol_version"` - - // ScannerName is the name of the tool, for example, govulncheck. - // - // We expect this JSON format to be used by other tools that wrap - // govulncheck, which will have a different name. - ScannerName string `json:"scanner_name,omitempty"` - - // ScannerVersion is the version of the tool. - ScannerVersion string `json:"scanner_version,omitempty"` - - // DB is the database used by the tool, for example, - // vuln.go.dev. - DB string `json:"db,omitempty"` - - // LastModified is the last modified time of the data source. - DBLastModified *time.Time `json:"db_last_modified,omitempty"` - - // GoVersion is the version of Go used for analyzing standard library - // vulnerabilities. - GoVersion string `json:"go_version,omitempty"` - - // ScanLevel instructs govulncheck to analyze at a specific level of detail. - // Valid values include module, package and symbol. - ScanLevel ScanLevel `json:"scan_level,omitempty"` -} - -// Progress messages are informational only, intended to allow users to monitor -// the progress of a long running scan. -// A stream must remain fully valid and able to be interpreted with all progress -// messages removed. -type Progress struct { - // A time stamp for the message. - Timestamp *time.Time `json:"time,omitempty"` - - // Message is the progress message. - Message string `json:"message,omitempty"` -} - -// Vuln represents a single OSV entry. -type Finding struct { - // OSV is the id of the detected vulnerability. - OSV string `json:"osv,omitempty"` - - // FixedVersion is the module version where the vulnerability was - // fixed. This is empty if a fix is not available. - // - // If there are multiple fixed versions in the OSV report, this will - // be the fixed version in the latest range event for the OSV report. - // - // For example, if the range events are - // {introduced: 0, fixed: 1.0.0} and {introduced: 1.1.0}, the fixed version - // will be empty. - // - // For the stdlib, we will show the fixed version closest to the - // Go version that is used. For example, if a fix is available in 1.17.5 and - // 1.18.5, and the GOVERSION is 1.17.3, 1.17.5 will be returned as the - // fixed version. - FixedVersion string `json:"fixed_version,omitempty"` - - // Trace contains an entry for each frame in the trace. - // - // Frames are sorted starting from the imported vulnerable symbol - // until the entry point. The first frame in Frames should match - // Symbol. - // - // In binary mode, trace will contain a single-frame with no position - // information. - // - // When a package is imported but no vulnerable symbol is called, the trace - // will contain a single-frame with no symbol or position information. - Trace []*Frame `json:"trace,omitempty"` -} - -// Frame represents an entry in a finding trace. -type Frame struct { - // Module is the module path of the module containing this symbol. - // - // Importable packages in the standard library will have the path "stdlib". - Module string `json:"module"` - - // Version is the module version from the build graph. - Version string `json:"version,omitempty"` - - // Package is the import path. - Package string `json:"package,omitempty"` - - // Function is the function name. - Function string `json:"function,omitempty"` - - // Receiver is the receiver type if the called symbol is a method. - // - // The client can create the final symbol name by - // prepending Receiver to FuncName. - Receiver string `json:"receiver,omitempty"` - - // Position describes an arbitrary source position - // including the file, line, and column location. - // A Position is valid if the line number is > 0. - Position *Position `json:"position,omitempty"` -} - -// Position represents arbitrary source position. -type Position struct { - Filename string `json:"filename,omitempty"` // filename, if any - Offset int `json:"offset"` // byte offset, starting at 0 - Line int `json:"line"` // line number, starting at 1 - Column int `json:"column"` // column number, starting at 1 (byte count) -} - -// ScanLevel represents the detail level at which a scan occurred. -// This can be necessary to correctly interpret the findings, for instance if -// a scan is at symbol level and a finding does not have a symbol it means the -// vulnerability was imported but not called. If the scan however was at -// "package" level, that determination cannot be made. -type ScanLevel string - -const ( - scanLevelModule = "module" - scanLevelPackage = "package" - scanLevelSymbol = "symbol" -) - -// WantSymbols can be used to check whether the scan level is one that is able -// to generate symbols called findings. -func (l ScanLevel) WantSymbols() bool { return l == scanLevelSymbol } diff --git a/internal/golangorgx/gopls/vulncheck/govulncheck/handler.go b/internal/golangorgx/gopls/vulncheck/govulncheck/handler.go deleted file mode 100644 index 43882e7d569..00000000000 --- a/internal/golangorgx/gopls/vulncheck/govulncheck/handler.go +++ /dev/null @@ -1,61 +0,0 @@ -// 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. - -// Code generated by copying from golang.org/x/vuln@v1.0.1 (go run copier.go); DO NOT EDIT. - -package govulncheck - -import ( - "encoding/json" - "io" - - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/osv" -) - -// Handler handles messages to be presented in a vulnerability scan output -// stream. -type Handler interface { - // Config communicates introductory message to the user. - Config(config *Config) error - - // Progress is called to display a progress message. - Progress(progress *Progress) error - - // OSV is invoked for each osv Entry in the stream. - OSV(entry *osv.Entry) error - - // Finding is called for each vulnerability finding in the stream. - Finding(finding *Finding) error -} - -// HandleJSON reads the json from the supplied stream and hands the decoded -// output to the handler. -func HandleJSON(from io.Reader, to Handler) error { - dec := json.NewDecoder(from) - for dec.More() { - msg := Message{} - // decode the next message in the stream - if err := dec.Decode(&msg); err != nil { - return err - } - // dispatch the message - var err error - if msg.Config != nil { - err = to.Config(msg.Config) - } - if msg.Progress != nil { - err = to.Progress(msg.Progress) - } - if msg.OSV != nil { - err = to.OSV(msg.OSV) - } - if msg.Finding != nil { - err = to.Finding(msg.Finding) - } - if err != nil { - return err - } - } - return nil -} diff --git a/internal/golangorgx/gopls/vulncheck/govulncheck/jsonhandler.go b/internal/golangorgx/gopls/vulncheck/govulncheck/jsonhandler.go deleted file mode 100644 index bae74b42273..00000000000 --- a/internal/golangorgx/gopls/vulncheck/govulncheck/jsonhandler.go +++ /dev/null @@ -1,46 +0,0 @@ -// 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. - -// Code generated by copying from golang.org/x/vuln@v1.0.1 (go run copier.go); DO NOT EDIT. - -package govulncheck - -import ( - "encoding/json" - - "io" - - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/osv" -) - -type jsonHandler struct { - enc *json.Encoder -} - -// NewJSONHandler returns a handler that writes govulncheck output as json. -func NewJSONHandler(w io.Writer) Handler { - enc := json.NewEncoder(w) - enc.SetIndent("", " ") - return &jsonHandler{enc: enc} -} - -// Config writes config block in JSON to the underlying writer. -func (h *jsonHandler) Config(config *Config) error { - return h.enc.Encode(Message{Config: config}) -} - -// Progress writes a progress message in JSON to the underlying writer. -func (h *jsonHandler) Progress(progress *Progress) error { - return h.enc.Encode(Message{Progress: progress}) -} - -// OSV writes an osv entry in JSON to the underlying writer. -func (h *jsonHandler) OSV(entry *osv.Entry) error { - return h.enc.Encode(Message{OSV: entry}) -} - -// Finding writes a finding in JSON to the underlying writer. -func (h *jsonHandler) Finding(finding *Finding) error { - return h.enc.Encode(Message{Finding: finding}) -} diff --git a/internal/golangorgx/gopls/vulncheck/osv/osv.go b/internal/golangorgx/gopls/vulncheck/osv/osv.go deleted file mode 100644 index 08e18abf87d..00000000000 --- a/internal/golangorgx/gopls/vulncheck/osv/osv.go +++ /dev/null @@ -1,240 +0,0 @@ -// 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. - -// Code generated by copying from golang.org/x/vuln@v1.0.1 (go run copier.go); DO NOT EDIT. - -// Package osv implements the Go OSV vulnerability format -// (https://go.dev/security/vuln/database#schema), which is a subset of -// the OSV shared vulnerability format -// (https://ossf.github.io/osv-schema), with database and -// ecosystem-specific meanings and fields. -// -// As this package is intended for use with the Go vulnerability -// database, only the subset of features which are used by that -// database are implemented (for instance, only the SEMVER affected -// range type is implemented). -package osv - -import "time" - -// RangeType specifies the type of version range being recorded and -// defines the interpretation of the RangeEvent object's Introduced -// and Fixed fields. -// -// In this implementation, only the "SEMVER" type is supported. -// -// See https://ossf.github.io/osv-schema/#affectedrangestype-field. -type RangeType string - -// RangeTypeSemver indicates a semantic version as defined by -// SemVer 2.0.0, with no leading "v" prefix. -const RangeTypeSemver RangeType = "SEMVER" - -// Ecosystem identifies the overall library ecosystem. -// In this implementation, only the "Go" ecosystem is supported. -type Ecosystem string - -// GoEcosystem indicates the Go ecosystem. -const GoEcosystem Ecosystem = "Go" - -// Pseudo-module paths used to describe vulnerabilities -// in the Go standard library and toolchain. -const ( - // GoStdModulePath is the pseudo-module path string used - // to describe vulnerabilities in the Go standard library. - GoStdModulePath = "stdlib" - // GoCmdModulePath is the pseudo-module path string used - // to describe vulnerabilities in the go command. - GoCmdModulePath = "toolchain" -) - -// Module identifies the Go module containing the vulnerability. -// Note that this field is called "package" in the OSV specification. -// -// See https://ossf.github.io/osv-schema/#affectedpackage-field. -type Module struct { - // The Go module path. Required. - // For the Go standard library, this is "stdlib". - // For the Go toolchain, this is "toolchain." - Path string `json:"name"` - // The ecosystem containing the module. Required. - // This should always be "Go". - Ecosystem Ecosystem `json:"ecosystem"` -} - -// RangeEvent describes a single module version that either -// introduces or fixes a vulnerability. -// -// Exactly one of Introduced and Fixed must be present. Other range -// event types (e.g, "last_affected" and "limit") are not supported in -// this implementation. -// -// See https://ossf.github.io/osv-schema/#affectedrangesevents-fields. -type RangeEvent struct { - // Introduced is a version that introduces the vulnerability. - // A special value, "0", represents a version that sorts before - // any other version, and should be used to indicate that the - // vulnerability exists from the "beginning of time". - Introduced string `json:"introduced,omitempty"` - // Fixed is a version that fixes the vulnerability. - Fixed string `json:"fixed,omitempty"` -} - -// Range describes the affected versions of the vulnerable module. -// -// See https://ossf.github.io/osv-schema/#affectedranges-field. -type Range struct { - // Type is the version type that should be used to interpret the - // versions in Events. Required. - // In this implementation, only the "SEMVER" type is supported. - Type RangeType `json:"type"` - // Events is a list of versions representing the ranges in which - // the module is vulnerable. Required. - // The events should be sorted, and MUST represent non-overlapping - // ranges. - // There must be at least one RangeEvent containing a value for - // Introduced. - // See https://ossf.github.io/osv-schema/#examples for examples. - Events []RangeEvent `json:"events"` -} - -// Reference type is a reference (link) type. -type ReferenceType string - -const ( - // ReferenceTypeAdvisory is a published security advisory for - // the vulnerability. - ReferenceTypeAdvisory = ReferenceType("ADVISORY") - // ReferenceTypeArticle is an article or blog post describing the vulnerability. - ReferenceTypeArticle = ReferenceType("ARTICLE") - // ReferenceTypeReport is a report, typically on a bug or issue tracker, of - // the vulnerability. - ReferenceTypeReport = ReferenceType("REPORT") - // ReferenceTypeFix is a source code browser link to the fix (e.g., a GitHub commit). - ReferenceTypeFix = ReferenceType("FIX") - // ReferenceTypePackage is a home web page for the package. - ReferenceTypePackage = ReferenceType("PACKAGE") - // ReferenceTypeEvidence is a demonstration of the validity of a vulnerability claim. - ReferenceTypeEvidence = ReferenceType("EVIDENCE") - // ReferenceTypeWeb is a web page of some unspecified kind. - ReferenceTypeWeb = ReferenceType("WEB") -) - -// Reference is a reference URL containing additional information, -// advisories, issue tracker entries, etc., about the vulnerability. -// -// See https://ossf.github.io/osv-schema/#references-field. -type Reference struct { - // The type of reference. Required. - Type ReferenceType `json:"type"` - // The fully-qualified URL of the reference. Required. - URL string `json:"url"` -} - -// Affected gives details about a module affected by the vulnerability. -// -// See https://ossf.github.io/osv-schema/#affected-fields. -type Affected struct { - // The affected Go module. Required. - // Note that this field is called "package" in the OSV specification. - Module Module `json:"package"` - // The module version ranges affected by the vulnerability. - Ranges []Range `json:"ranges,omitempty"` - // Details on the affected packages and symbols within the module. - EcosystemSpecific EcosystemSpecific `json:"ecosystem_specific"` -} - -// Package contains additional information about an affected package. -// This is an ecosystem-specific field for the Go ecosystem. -type Package struct { - // Path is the package import path. Required. - Path string `json:"path,omitempty"` - // GOOS is the execution operating system where the symbols appear, if - // known. - GOOS []string `json:"goos,omitempty"` - // GOARCH specifies the execution architecture where the symbols appear, if - // known. - GOARCH []string `json:"goarch,omitempty"` - // Symbols is a list of function and method names affected by - // this vulnerability. Methods are listed as .. - // - // If included, only programs which use these symbols will be marked as - // vulnerable by `govulncheck`. If omitted, any program which imports this - // package will be marked vulnerable. - Symbols []string `json:"symbols,omitempty"` -} - -// EcosystemSpecific contains additional information about the vulnerable -// module for the Go ecosystem. -// -// See https://go.dev/security/vuln/database#schema. -type EcosystemSpecific struct { - // Packages is the list of affected packages within the module. - Packages []Package `json:"imports,omitempty"` -} - -// Entry represents a vulnerability in the Go OSV format, documented -// in https://go.dev/security/vuln/database#schema. -// It is a subset of the OSV schema (https://ossf.github.io/osv-schema). -// Only fields that are published in the Go Vulnerability Database -// are supported. -type Entry struct { - // SchemaVersion is the OSV schema version used to encode this - // vulnerability. - SchemaVersion string `json:"schema_version,omitempty"` - // ID is a unique identifier for the vulnerability. Required. - // The Go vulnerability database issues IDs of the form - // GO--. - ID string `json:"id"` - // Modified is the time the entry was last modified. Required. - Modified time.Time `json:"modified,omitempty"` - // Published is the time the entry should be considered to have - // been published. - Published time.Time `json:"published,omitempty"` - // Withdrawn is the time the entry should be considered to have - // been withdrawn. If the field is missing, then the entry has - // not been withdrawn. - Withdrawn *time.Time `json:"withdrawn,omitempty"` - // Aliases is a list of IDs for the same vulnerability in other - // databases. - Aliases []string `json:"aliases,omitempty"` - // Summary gives a one-line, English textual summary of the vulnerability. - // It is recommended that this field be kept short, on the order of no more - // than 120 characters. - Summary string `json:"summary,omitempty"` - // Details contains additional English textual details about the vulnerability. - Details string `json:"details"` - // Affected contains information on the modules and versions - // affected by the vulnerability. - Affected []Affected `json:"affected"` - // References contains links to more information about the - // vulnerability. - References []Reference `json:"references,omitempty"` - // Credits contains credits to entities that helped find or fix the - // vulnerability. - Credits []Credit `json:"credits,omitempty"` - // DatabaseSpecific contains additional information about the - // vulnerability, specific to the Go vulnerability database. - DatabaseSpecific *DatabaseSpecific `json:"database_specific,omitempty"` -} - -// Credit represents a credit for the discovery, confirmation, patch, or -// other event in the life cycle of a vulnerability. -// -// See https://ossf.github.io/osv-schema/#credits-fields. -type Credit struct { - // Name is the name, label, or other identifier of the individual or - // entity being credited. Required. - Name string `json:"name"` -} - -// DatabaseSpecific contains additional information about the -// vulnerability, specific to the Go vulnerability database. -// -// See https://go.dev/security/vuln/database#schema. -type DatabaseSpecific struct { - // The URL of the Go advisory for this vulnerability, of the form - // "https://pkg.go.dev/GO-YYYY-XXXX". - URL string `json:"url,omitempty"` -} diff --git a/internal/golangorgx/gopls/vulncheck/scan/command.go b/internal/golangorgx/gopls/vulncheck/scan/command.go deleted file mode 100644 index bbabe228a3f..00000000000 --- a/internal/golangorgx/gopls/vulncheck/scan/command.go +++ /dev/null @@ -1,168 +0,0 @@ -// 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. - -//go:build go1.18 -// +build go1.18 - -package scan - -import ( - "bytes" - "context" - "fmt" - "io" - "os" - "os/exec" - "sort" - "time" - - "cuelang.org/go/internal/golangorgx/gopls/cache" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/govulncheck" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/osv" - "golang.org/x/sync/errgroup" - "golang.org/x/vuln/scan" -) - -// Main implements gopls vulncheck. -func Main(ctx context.Context, args ...string) error { - // wrapping govulncheck. - cmd := scan.Command(ctx, args...) - if err := cmd.Start(); err != nil { - return err - } - return cmd.Wait() -} - -// RunGovulncheck implements the codelens "Run Govulncheck" -// that runs 'gopls vulncheck' and converts the output to gopls's internal data -// used for diagnostics and hover message construction. -// -// TODO(rfindley): this should accept a *View (which exposes) Options, rather -// than a snapshot. -func RunGovulncheck(ctx context.Context, pattern string, snapshot *cache.Snapshot, dir string, log io.Writer) (*vulncheck.Result, error) { - vulncheckargs := []string{ - "vulncheck", "--", - "-json", - "-mode", "source", - "-scan", "symbol", - } - if dir != "" { - vulncheckargs = append(vulncheckargs, "-C", dir) - } - if db := cache.GetEnv(snapshot, "GOVULNDB"); db != "" { - vulncheckargs = append(vulncheckargs, "-db", db) - } - vulncheckargs = append(vulncheckargs, pattern) - // TODO: support -tags. need to compute tags args from opts.BuildFlags. - // TODO: support -test. - - ir, iw := io.Pipe() - handler := &govulncheckHandler{logger: log, osvs: map[string]*osv.Entry{}} - - stderr := new(bytes.Buffer) - var g errgroup.Group - // We run the govulncheck's analysis in a separate process as it can - // consume a lot of CPUs and memory, and terminates: a separate process - // is a perfect garbage collector and affords us ways to limit its resource usage. - g.Go(func() error { - defer iw.Close() - - cmd := exec.CommandContext(ctx, os.Args[0], vulncheckargs...) - cmd.Env = getEnvSlices(snapshot) - if goversion := cache.GetEnv(snapshot, cache.GoVersionForVulnTest); goversion != "" { - // Let govulncheck API use a different Go version using the (undocumented) hook - // in https://go.googlesource.com/vuln/+/v1.0.1/internal/scan/run.go#76 - cmd.Env = append(cmd.Env, "GOVERSION="+goversion) - } - cmd.Stderr = stderr // stream vulncheck's STDERR as progress reports - cmd.Stdout = iw // let the other goroutine parses the result. - - if err := cmd.Start(); err != nil { - return fmt.Errorf("failed to start govulncheck: %v", err) - } - if err := cmd.Wait(); err != nil { - return fmt.Errorf("failed to run govulncheck: %v", err) - } - return nil - }) - g.Go(func() error { - return govulncheck.HandleJSON(ir, handler) - }) - if err := g.Wait(); err != nil { - if stderr.Len() > 0 { - log.Write(stderr.Bytes()) - } - return nil, fmt.Errorf("failed to read govulncheck output: %v", err) - } - - findings := handler.findings // sort so the findings in the result is deterministic. - sort.Slice(findings, func(i, j int) bool { - x, y := findings[i], findings[j] - if x.OSV != y.OSV { - return x.OSV < y.OSV - } - return x.Trace[0].Package < y.Trace[0].Package - }) - result := &vulncheck.Result{ - Mode: vulncheck.ModeGovulncheck, - AsOf: time.Now(), - Entries: handler.osvs, - Findings: findings, - } - return result, nil -} - -type govulncheckHandler struct { - logger io.Writer // forward progress reports to logger. - - osvs map[string]*osv.Entry - findings []*govulncheck.Finding -} - -// Config implements vulncheck.Handler. -func (h *govulncheckHandler) Config(config *govulncheck.Config) error { - if config.GoVersion != "" { - fmt.Fprintf(h.logger, "Go: %v\n", config.GoVersion) - } - if config.ScannerName != "" { - scannerName := fmt.Sprintf("Scanner: %v", config.ScannerName) - if config.ScannerVersion != "" { - scannerName += "@" + config.ScannerVersion - } - fmt.Fprintln(h.logger, scannerName) - } - if config.DB != "" { - dbInfo := fmt.Sprintf("DB: %v", config.DB) - if config.DBLastModified != nil { - dbInfo += fmt.Sprintf(" (DB updated: %v)", config.DBLastModified.String()) - } - fmt.Fprintln(h.logger, dbInfo) - } - return nil -} - -// Finding implements vulncheck.Handler. -func (h *govulncheckHandler) Finding(finding *govulncheck.Finding) error { - h.findings = append(h.findings, finding) - return nil -} - -// OSV implements vulncheck.Handler. -func (h *govulncheckHandler) OSV(entry *osv.Entry) error { - h.osvs[entry.ID] = entry - return nil -} - -// Progress implements vulncheck.Handler. -func (h *govulncheckHandler) Progress(progress *govulncheck.Progress) error { - if progress.Message != "" { - fmt.Fprintf(h.logger, "%v\n", progress.Message) - } - return nil -} - -func getEnvSlices(snapshot *cache.Snapshot) []string { - return append(os.Environ(), snapshot.Options().EnvSlice()...) -} diff --git a/internal/golangorgx/gopls/vulncheck/semver/semver.go b/internal/golangorgx/gopls/vulncheck/semver/semver.go deleted file mode 100644 index 67c4fe8a39e..00000000000 --- a/internal/golangorgx/gopls/vulncheck/semver/semver.go +++ /dev/null @@ -1,48 +0,0 @@ -// 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. - -//go:build go1.18 -// +build go1.18 - -// Package semver provides shared utilities for manipulating -// Go semantic versions. -package semver - -import ( - "strings" - - "golang.org/x/mod/semver" -) - -// addSemverPrefix adds a 'v' prefix to s if it isn't already prefixed -// with 'v' or 'go'. This allows us to easily test go-style SEMVER -// strings against normal SEMVER strings. -func addSemverPrefix(s string) string { - if !strings.HasPrefix(s, "v") && !strings.HasPrefix(s, "go") { - return "v" + s - } - return s -} - -// removeSemverPrefix removes the 'v' or 'go' prefixes from go-style -// SEMVER strings, for usage in the public vulnerability format. -func removeSemverPrefix(s string) string { - s = strings.TrimPrefix(s, "v") - s = strings.TrimPrefix(s, "go") - return s -} - -// CanonicalizeSemverPrefix turns a SEMVER string into the canonical -// representation using the 'v' prefix, as used by the OSV format. -// Input may be a bare SEMVER ("1.2.3"), Go prefixed SEMVER ("go1.2.3"), -// or already canonical SEMVER ("v1.2.3"). -func CanonicalizeSemverPrefix(s string) string { - return addSemverPrefix(removeSemverPrefix(s)) -} - -// Valid returns whether v is valid semver, allowing -// either a "v", "go" or no prefix. -func Valid(v string) bool { - return semver.IsValid(CanonicalizeSemverPrefix(v)) -} diff --git a/internal/golangorgx/gopls/vulncheck/types.go b/internal/golangorgx/gopls/vulncheck/types.go deleted file mode 100644 index ec825f2169c..00000000000 --- a/internal/golangorgx/gopls/vulncheck/types.go +++ /dev/null @@ -1,47 +0,0 @@ -// 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. - -// go:generate go run copier.go - -package vulncheck - -import ( - "time" - - gvc "cuelang.org/go/internal/golangorgx/gopls/vulncheck/govulncheck" - "cuelang.org/go/internal/golangorgx/gopls/vulncheck/osv" -) - -// Result is the result of vulnerability scanning. -type Result struct { - // Entries contains all vulnerabilities that are called or imported by - // the analyzed module. Keys are Entry.IDs. - Entries map[string]*osv.Entry - // Findings are vulnerabilities found by vulncheck or import-based analysis. - // Ordered by the OSV IDs and the package names. - Findings []*gvc.Finding - - // Mode contains the source of the vulnerability info. - // Clients of the gopls.fetch_vulncheck_result command may need - // to interpret the vulnerabilities differently based on the - // analysis mode. For example, Vuln without callstack traces - // indicate a vulnerability that is not used if the result was - // from 'govulncheck' analysis mode. On the other hand, Vuln - // without callstack traces just implies the package with the - // vulnerability is known to the workspace and we do not know - // whether the vulnerable symbols are actually used or not. - Mode AnalysisMode `json:",omitempty"` - - // AsOf describes when this Result was computed using govulncheck. - // It is valid only with the govulncheck analysis mode. - AsOf time.Time `json:",omitempty"` -} - -type AnalysisMode string - -const ( - ModeInvalid AnalysisMode = "" // zero value - ModeGovulncheck AnalysisMode = "govulncheck" - ModeImports AnalysisMode = "imports" -)