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 }