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  }