github.com/google/osv-scalibr@v0.4.1/enricher/govulncheck/source/govulncheck.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package source provides an enricher that uses govulncheck to scan Go source code. 16 package source 17 18 import ( 19 "context" 20 "encoding/json" 21 "errors" 22 "io" 23 "path/filepath" 24 "slices" 25 26 cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto" 27 "github.com/google/osv-scalibr/enricher" 28 "github.com/google/osv-scalibr/enricher/govulncheck/source/internal" 29 "github.com/google/osv-scalibr/extractor" 30 "github.com/google/osv-scalibr/extractor/filesystem/language/golang/gomod" 31 "github.com/google/osv-scalibr/inventory" 32 "github.com/google/osv-scalibr/inventory/vex" 33 "github.com/google/osv-scalibr/log" 34 "github.com/google/osv-scalibr/plugin" 35 vulnpb "github.com/ossf/osv-schema/bindings/go/osvschema" 36 ) 37 38 const ( 39 // Name is the unique name of this enricher. 40 Name = "reachability/go/source" 41 ) 42 43 // ErrNoGoToolchain is returned when the go toolchain is not found in the system. 44 var ErrNoGoToolchain = errors.New("no Go toolchain found") 45 46 // Enricher is an enricher that runs govulncheck on Go source code. 47 type Enricher struct { 48 client GovulncheckClient 49 } 50 51 // Name returns the name of the enricher. 52 func (e *Enricher) Name() string { 53 return Name 54 } 55 56 // Version returns the version of the enricher. 57 func (e *Enricher) Version() int { 58 return 0 59 } 60 61 // Requirements returns the requirements of the enricher. 62 func (e *Enricher) Requirements() *plugin.Capabilities { 63 return &plugin.Capabilities{ 64 Network: plugin.NetworkAny, 65 DirectFS: true, 66 RunningSystem: true, 67 } 68 } 69 70 // RequiredPlugins returns the names of the plugins required by this enricher. 71 func (e *Enricher) RequiredPlugins() []string { 72 return []string{gomod.Name} 73 } 74 75 // Enrich runs govulncheck on the Go modules in the inventory. 76 func (e *Enricher) Enrich(ctx context.Context, input *enricher.ScanInput, inv *inventory.Inventory) error { 77 if !e.client.GoToolchainAvailable(ctx) { 78 return ErrNoGoToolchain 79 } 80 81 goModVersions := make(map[string]string) 82 for _, pkg := range inv.Packages { 83 if !slices.Contains(pkg.Plugins, gomod.Name) { 84 continue 85 } 86 if pkg.Name == "stdlib" { 87 for _, l := range pkg.Locations { 88 if goModVersions[l] != "" { 89 continue 90 } 91 92 // Set GOVERSION to the Go version in go.mod. 93 goModVersions[l] = pkg.Version 94 95 continue 96 } 97 } 98 } 99 100 var vulns []*vulnpb.Vulnerability 101 for _, pv := range inv.PackageVulns { 102 vulns = append(vulns, pv.Vulnerability) 103 } 104 105 for goModLocation, goVersion := range goModVersions { 106 modDir := filepath.Dir(goModLocation) 107 absModDir := filepath.Join(input.ScanRoot.Path, modDir) 108 findings, err := e.client.RunGovulncheck(ctx, absModDir, vulns, goVersion) 109 if err != nil { 110 log.Errorf("govulncheck on %s: %v", modDir, err) 111 continue 112 } 113 114 if len(findings) == 0 { 115 continue 116 } 117 118 e.addSignals(inv, findings) 119 } 120 121 return nil 122 } 123 124 func (e *Enricher) addSignals(inv *inventory.Inventory, idToFindings map[string][]*internal.Finding) { 125 for _, pv := range inv.PackageVulns { 126 findings, exist := idToFindings[pv.Vulnerability.Id] 127 128 if !exist { 129 // The finding doesn't exist, this could mean two things: 130 // 1. The code does not import the vulnerable package. 131 // 2. The vulnerability does not have symbol information, so govulncheck ignored it. 132 if vulnHasImportsField(pv.Vulnerability, pv.Package) { 133 // If there is symbol information, then analysis has been performed. 134 // Since this finding doesn't exist, it means the code does not import the vulnerable package, 135 // so definitely not called. 136 pv.ExploitabilitySignals = append(pv.ExploitabilitySignals, &vex.FindingExploitabilitySignal{ 137 Plugin: Name, 138 Justification: vex.VulnerableCodeNotInExecutePath, 139 }) 140 } 141 142 // Otherwise, we don't know if the code is reachable or not. 143 continue 144 } 145 146 // For entries with findings, check if the code is reachable or not by whether there is a trace. 147 reachable := false 148 for _, f := range findings { 149 if len(f.Trace) > 0 && f.Trace[0].Function != "" { 150 reachable = true 151 break 152 } 153 } 154 155 if !reachable { 156 pv.ExploitabilitySignals = append(pv.ExploitabilitySignals, &vex.FindingExploitabilitySignal{ 157 Plugin: Name, 158 Justification: vex.VulnerableCodeNotInExecutePath, 159 }) 160 } 161 } 162 } 163 164 type osvHandler struct { 165 idToFindings map[string][]*internal.Finding 166 } 167 168 func (h *osvHandler) Finding(f *internal.Finding) { 169 h.idToFindings[f.OSV] = append(h.idToFindings[f.OSV], f) 170 } 171 172 func handleJSON(from io.Reader, to *osvHandler) error { 173 dec := json.NewDecoder(from) 174 for dec.More() { 175 msg := internal.Message{} 176 if err := dec.Decode(&msg); err != nil { 177 return err 178 } 179 if msg.Finding != nil { 180 to.Finding(msg.Finding) 181 } 182 } 183 184 return nil 185 } 186 187 func vulnHasImportsField(vuln *vulnpb.Vulnerability, pkg *extractor.Package) bool { 188 for _, affected := range vuln.Affected { 189 if pkg != nil { 190 // TODO(#1559): Compare versions to see if this is the correct affected element 191 // This is very unlikely to ever matter however. 192 if affected.Package.Name != pkg.Name { 193 continue 194 } 195 } 196 _, hasImportsField := affected.EcosystemSpecific.GetFields()["imports"] 197 if hasImportsField { 198 return true 199 } 200 } 201 202 return false 203 } 204 205 // New returns a new govulncheck source enricher. 206 func New(cfg *cpb.PluginConfig) enricher.Enricher { 207 return &Enricher{ 208 client: &realGovulncheckClient{}, 209 } 210 } 211 212 // NewWithClient returns a new govulncheck source enricher with a custom client. 213 func NewWithClient(cfg *cpb.PluginConfig, client GovulncheckClient) enricher.Enricher { 214 return &Enricher{ 215 client: client, 216 } 217 }