// Copyright 2023 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package vulncheck import ( "go/token" "os" "path/filepath" "strings" "golang.org/x/tools/go/packages" "golang.org/x/vuln/internal/govulncheck" ) // emitOSVs emits all OSV vuln entries in modVulns to handler. func emitOSVs(handler govulncheck.Handler, modVulns []*ModVulns) error { for _, mv := range modVulns { for _, v := range mv.Vulns { if err := handler.OSV(v); err != nil { return err } } } return nil } // emitModuleFindings emits module-level findings for vulnerabilities in modVulns. func emitModuleFindings(handler govulncheck.Handler, affVulns affectingVulns) error { for _, vuln := range affVulns { for _, osv := range vuln.Vulns { if err := handler.Finding(&govulncheck.Finding{ OSV: osv.ID, FixedVersion: FixedVersion(modPath(vuln.Module), modVersion(vuln.Module), osv.Affected), Trace: []*govulncheck.Frame{frameFromModule(vuln.Module)}, }); err != nil { return err } } } return nil } // emitPackageFinding emits package-level findings fod vulnerabilities in vulns. func emitPackageFindings(handler govulncheck.Handler, vulns []*Vuln) error { for _, v := range vulns { if err := handler.Finding(&govulncheck.Finding{ OSV: v.OSV.ID, FixedVersion: FixedVersion(modPath(v.Package.Module), modVersion(v.Package.Module), v.OSV.Affected), Trace: []*govulncheck.Frame{frameFromPackage(v.Package)}, }); err != nil { return err } } return nil } // emitCallFindings emits call-level findings for vulnerabilities // that have a call stack in callstacks. func emitCallFindings(handler govulncheck.Handler, callstacks map[*Vuln]CallStack) error { var vulns []*Vuln for v := range callstacks { vulns = append(vulns, v) } for _, vuln := range vulns { stack := callstacks[vuln] if stack == nil { continue } fixed := FixedVersion(modPath(vuln.Package.Module), modVersion(vuln.Package.Module), vuln.OSV.Affected) if err := handler.Finding(&govulncheck.Finding{ OSV: vuln.OSV.ID, FixedVersion: fixed, Trace: traceFromEntries(stack), }); err != nil { return err } } return nil } // traceFromEntries creates a sequence of // frames from vcs. Position of a Frame is the // call position of the corresponding stack entry. func traceFromEntries(vcs CallStack) []*govulncheck.Frame { var frames []*govulncheck.Frame for i := len(vcs) - 1; i >= 0; i-- { e := vcs[i] fr := frameFromPackage(e.Function.Package) fr.Function = e.Function.Name fr.Receiver = e.Function.Receiver() isSink := i == (len(vcs) - 1) fr.Position = posFromStackEntry(e, isSink) frames = append(frames, fr) } return frames } func posFromStackEntry(e StackEntry, sink bool) *govulncheck.Position { var p *token.Position var f *FuncNode if sink && e.Function != nil && e.Function.Pos != nil { // For sinks, i.e., vulns we take the position // of the symbol. p = e.Function.Pos f = e.Function } else if e.Call != nil && e.Call.Pos != nil { // Otherwise, we take the position of // the call statement. p = e.Call.Pos f = e.Call.Parent } if p == nil { return nil } return &govulncheck.Position{ Filename: pathRelativeToMod(p.Filename, f), Offset: p.Offset, Line: p.Line, Column: p.Column, } } // pathRelativeToMod computes a version of path // relative to the module of f. If it does not // have all the necessary information, returns // an empty string. // // The returned paths always use slash as separator // so they can work across different platforms. func pathRelativeToMod(path string, f *FuncNode) string { if path == "" || f == nil || f.Package == nil { // sanity return "" } mod := f.Package.Module if mod.Replace != nil { mod = mod.Replace // for replace directive } modDir := modDirWithVendor(mod.Dir, path, mod.Path) p, err := filepath.Rel(modDir, path) if err != nil { return "" } // make sure paths are portable. return filepath.ToSlash(p) } // modDirWithVendor returns modDir if modDir is not empty. // Otherwise, the module might be located in the vendor // directory. This function attempts to reconstruct the // vendored module directory from path and module. It // returns an empty string if reconstruction fails. func modDirWithVendor(modDir, path, module string) string { if modDir != "" { return modDir } sep := string(os.PathSeparator) vendor := sep + "vendor" + sep vendorIndex := strings.Index(path, vendor) if vendorIndex == -1 { return "" } return filepath.Join(path[:vendorIndex], "vendor", filepath.FromSlash(module)) } func frameFromPackage(pkg *packages.Package) *govulncheck.Frame { fr := &govulncheck.Frame{} if pkg != nil { fr.Module = pkg.Module.Path fr.Version = pkg.Module.Version fr.Package = pkg.PkgPath } if pkg.Module.Replace != nil { fr.Module = pkg.Module.Replace.Path fr.Version = pkg.Module.Replace.Version } return fr } func frameFromModule(mod *packages.Module) *govulncheck.Frame { fr := &govulncheck.Frame{ Module: mod.Path, Version: mod.Version, } if mod.Replace != nil { fr.Module = mod.Replace.Path fr.Version = mod.Replace.Version } return fr }