github.com/google/osv-scalibr@v0.4.1/detector/govulncheck/binary/binary.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 binary implements a detector that uses govulncheck to scan for vulns on Go binaries found
    16  // on the filesystem.
    17  package binary
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"path"
    25  	"slices"
    26  
    27  	"github.com/google/osv-scalibr/detector"
    28  	"github.com/google/osv-scalibr/extractor/filesystem/language/golang/gobinary"
    29  	scalibrfs "github.com/google/osv-scalibr/fs"
    30  	"github.com/google/osv-scalibr/inventory"
    31  	"github.com/google/osv-scalibr/log"
    32  	"github.com/google/osv-scalibr/packageindex"
    33  	"github.com/google/osv-scalibr/plugin"
    34  	"github.com/google/osv-scalibr/purl"
    35  	"golang.org/x/vuln/scan"
    36  
    37  	cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto"
    38  	osvpb "github.com/ossf/osv-schema/bindings/go/osvschema"
    39  )
    40  
    41  const (
    42  	// Name is the unique name of this detector.
    43  	Name = "govulncheck/binary"
    44  )
    45  
    46  // Detector is a SCALIBR Detector that uses govulncheck to scan for vulns on Go binaries found
    47  // on the filesystem.
    48  type Detector struct {
    49  	offlineVulnDBPath string
    50  }
    51  
    52  // New returns a detector.
    53  func New(cfg *cpb.PluginConfig) detector.Detector {
    54  	d := &Detector{}
    55  	specific := plugin.FindConfig(cfg, func(c *cpb.PluginSpecificConfig) *cpb.GovulncheckConfig { return c.GetGovulncheck() })
    56  	d.offlineVulnDBPath = specific.GetOfflineVulnDbPath()
    57  	return d
    58  }
    59  
    60  // Name of the detector.
    61  func (Detector) Name() string { return Name }
    62  
    63  // Version of the detector.
    64  func (Detector) Version() int { return 0 }
    65  
    66  // Requirements of the detector.
    67  func (d Detector) Requirements() *plugin.Capabilities {
    68  	net := plugin.NetworkOnline
    69  	if d.offlineVulnDBPath == "" {
    70  		net = plugin.NetworkAny
    71  	}
    72  	return &plugin.Capabilities{Network: net, DirectFS: true}
    73  }
    74  
    75  // RequiredExtractors returns the go binary extractor.
    76  func (Detector) RequiredExtractors() []string {
    77  	return []string{gobinary.Name}
    78  }
    79  
    80  // DetectedFinding returns generic vulnerability information about what is detected.
    81  // TODO: b/428851334 - For now, we do not return any advisories. But we want to be able to propagate
    82  // detection capabilities here.
    83  func (d Detector) DetectedFinding() inventory.Finding {
    84  	return inventory.Finding{}
    85  }
    86  
    87  // Scan takes the go binaries gathered in the extraction phase and runs govulncheck on them.
    88  func (d Detector) Scan(ctx context.Context, scanRoot *scalibrfs.ScanRoot, px *packageindex.PackageIndex) (inventory.Finding, error) {
    89  	result := inventory.Finding{}
    90  	scanned := make(map[string]bool)
    91  	var allErrs error
    92  	for _, p := range px.GetAllOfType(purl.TypeGolang) {
    93  		// We only look at Go binaries (no source code).
    94  		if !slices.Contains(p.Plugins, gobinary.Name) {
    95  			continue
    96  		}
    97  		for _, l := range p.Locations {
    98  			if scanned[l] {
    99  				continue
   100  			}
   101  			scanned[l] = true
   102  			if ctx.Err() != nil {
   103  				return result, appendError(allErrs, ctx.Err())
   104  			}
   105  			out, err := d.runGovulncheck(ctx, l, scanRoot.Path)
   106  			if err != nil {
   107  				allErrs = appendError(allErrs, fmt.Errorf("d.runGovulncheck(%s): %w", l, err))
   108  				continue
   109  			}
   110  			r, err := parseVulnsFromOutput(out)
   111  			if err != nil {
   112  				allErrs = appendError(allErrs, fmt.Errorf("d.parseVulnsFromOutput(%v, %s): %w", out, l, err))
   113  				continue
   114  			}
   115  			result.PackageVulns = append(result.PackageVulns, r...)
   116  		}
   117  	}
   118  	return result, allErrs
   119  }
   120  
   121  func (d Detector) runGovulncheck(ctx context.Context, binaryPath, scanRoot string) (*bytes.Buffer, error) {
   122  	fullPath := path.Join(scanRoot, binaryPath)
   123  	log.Debugf("Running govulncheck on go binary %v", fullPath)
   124  	args := []string{"--mode=binary", "--json"}
   125  	if d.offlineVulnDBPath != "" {
   126  		args = append(args, "-db=file://"+d.offlineVulnDBPath)
   127  	}
   128  	args = append(args, fullPath)
   129  	cmd := scan.Command(ctx, args...)
   130  	var out bytes.Buffer
   131  	cmd.Stdout = &out
   132  	if err := cmd.Start(); err != nil {
   133  		return nil, err
   134  	}
   135  	if err := cmd.Wait(); err != nil {
   136  		return nil, err
   137  	}
   138  	log.Debugf("govulncheck complete")
   139  	return &out, nil
   140  }
   141  
   142  func parseVulnsFromOutput(out *bytes.Buffer) ([]*inventory.PackageVuln, error) {
   143  	var result []*inventory.PackageVuln
   144  	allOSVs := make(map[string]*osvpb.Vulnerability)
   145  	detectedOSVs := make(map[string]struct{}) // osvs detected at the symbol level
   146  	dec := json.NewDecoder(bytes.NewReader(out.Bytes()))
   147  	for dec.More() {
   148  		msg := govulncheckMessage{}
   149  		if err := dec.Decode(&msg); err != nil {
   150  			return nil, err
   151  		}
   152  		if msg.OSV != nil {
   153  			allOSVs[msg.OSV.Id] = msg.OSV
   154  		}
   155  		if msg.Finding != nil {
   156  			trace := msg.Finding.Trace
   157  			if len(trace) != 0 && trace[0].Function != "" {
   158  				// symbol findings
   159  				detectedOSVs[msg.Finding.OSV] = struct{}{}
   160  			}
   161  		}
   162  	}
   163  
   164  	// create scalibr findings for detected govulncheck findings
   165  	for osvID := range detectedOSVs {
   166  		osv := allOSVs[osvID]
   167  		result = append(result, &inventory.PackageVuln{Vulnerability: osv})
   168  	}
   169  	return result, nil
   170  }
   171  
   172  func appendError(err1, err2 error) error {
   173  	if err1 == nil {
   174  		return err2
   175  	}
   176  	return fmt.Errorf("%w\n%w", err1, err2)
   177  }