-
Notifications
You must be signed in to change notification settings - Fork 95
feat: Govulncheck source call analysis enricher #1555
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
another-rex
wants to merge
13
commits into
main
Choose a base branch
from
go_call_analysis
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
00f9ebd
feat: add the govulncheck source enricher
another-rex 4d62fe7
Cleanup: Support new cfg, add to list, rename govulncheck source
another-rex 1c9b941
offline vulndb
another-rex 0b531a9
Address PR comments
another-rex 18839b9
Use tabular tests, fix issue with non present vulns
another-rex 6f1971b
Rename back to source, set network to offline if using local db, add …
another-rex ea5797d
Fix lints
another-rex 2dde31f
Add a comment and issue for the TODO.
another-rex 48c867c
Address PR comments 2
another-rex 793dc9b
Refactor to use vuln matcher
another-rex f6ea8f7
Fix linting
another-rex a213e26
Copy over url package
another-rex 777b450
Fix lints
another-rex File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,273 @@ | ||
| // Copyright 2025 Google LLC | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| // Package source provides an enricher that uses govulncheck to scan Go source code. | ||
| package source | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "slices" | ||
|
|
||
| cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto" | ||
| "github.com/google/osv-scalibr/enricher" | ||
| "github.com/google/osv-scalibr/enricher/govulncheck/source/internal" | ||
| "github.com/google/osv-scalibr/enricher/govulncheck/source/internal/url" | ||
| "github.com/google/osv-scalibr/extractor" | ||
| "github.com/google/osv-scalibr/extractor/filesystem/language/golang/gomod" | ||
| "github.com/google/osv-scalibr/inventory" | ||
| "github.com/google/osv-scalibr/inventory/vex" | ||
| "github.com/google/osv-scalibr/log" | ||
| "github.com/google/osv-scalibr/plugin" | ||
| "github.com/ossf/osv-schema/bindings/go/osvschema" | ||
| "golang.org/x/vuln/scan" | ||
| "google.golang.org/protobuf/encoding/protojson" | ||
| ) | ||
|
|
||
| const ( | ||
| // Name is the unique name of this enricher. | ||
| Name = "reachability/go/source" | ||
| ) | ||
|
|
||
| // ErrNoGoToolchain is returned when the go toolchain is not found in the system. | ||
| var ErrNoGoToolchain = errors.New("no Go toolchain found") | ||
|
|
||
| // Enricher is an enricher that runs govulncheck on Go source code. | ||
| type Enricher struct{} | ||
|
|
||
| // Name returns the name of the enricher. | ||
| func (e *Enricher) Name() string { | ||
| return Name | ||
| } | ||
|
|
||
| // Version returns the version of the enricher. | ||
| func (e *Enricher) Version() int { | ||
| return 0 | ||
| } | ||
|
|
||
| // Requirements returns the requirements of the enricher. | ||
| func (e *Enricher) Requirements() *plugin.Capabilities { | ||
| return &plugin.Capabilities{ | ||
| Network: plugin.NetworkAny, | ||
| DirectFS: true, | ||
| RunningSystem: true, | ||
| } | ||
| } | ||
|
|
||
| // RequiredPlugins returns the names of the plugins required by this enricher. | ||
| func (e *Enricher) RequiredPlugins() []string { | ||
| return []string{gomod.Name} | ||
| } | ||
|
|
||
| // Enrich runs govulncheck on the Go modules in the inventory. | ||
| func (e *Enricher) Enrich(ctx context.Context, input *enricher.ScanInput, inv *inventory.Inventory) error { | ||
| cmd := exec.CommandContext(ctx, "go", "version") | ||
| _, err := cmd.Output() | ||
| if err != nil { | ||
| return ErrNoGoToolchain | ||
| } | ||
|
|
||
| goModVersions := make(map[string]string) | ||
| for _, pkg := range inv.Packages { | ||
| if !slices.Contains(pkg.Plugins, gomod.Name) { | ||
| continue | ||
| } | ||
| if pkg.Name == "stdlib" { | ||
| for _, l := range pkg.Locations { | ||
| if goModVersions[l] != "" { | ||
| continue | ||
| } | ||
|
|
||
| // Set GOVERSION to the Go version in go.mod. | ||
| goModVersions[l] = pkg.Version | ||
|
|
||
| continue | ||
| } | ||
| } | ||
| } | ||
|
|
||
| var vulns []*osvschema.Vulnerability | ||
| for _, pv := range inv.PackageVulns { | ||
| vulns = append(vulns, pv.Vulnerability) | ||
| } | ||
|
|
||
| for goModLocation, goVersion := range goModVersions { | ||
| modDir := filepath.Dir(goModLocation) | ||
| absModDir := filepath.Join(input.ScanRoot.Path, modDir) | ||
| findings, err := e.runGovulncheck(ctx, absModDir, vulns, goVersion) | ||
| if err != nil { | ||
| log.Errorf("govulncheck on %s: %v", modDir, err) | ||
| continue | ||
| } | ||
|
|
||
| if len(findings) == 0 { | ||
| continue | ||
| } | ||
|
|
||
| e.addSignals(inv, findings) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (e *Enricher) addSignals(inv *inventory.Inventory, idToFindings map[string][]*internal.Finding) { | ||
| for _, pv := range inv.PackageVulns { | ||
| findings, exist := idToFindings[pv.Vulnerability.Id] | ||
|
|
||
| if !exist { | ||
| // The finding doesn't exist, this could mean two things: | ||
| // 1. The code does not import the vulnerable package. | ||
| // 2. The vulnerability does not have symbol information, so govulncheck ignored it. | ||
| if vulnHasImportsField(pv.Vulnerability, pv.Package) { | ||
| // If there is symbol information, then analysis has been performed. | ||
| // Since this finding doesn't exist, it means the code does not import the vulnerable package, | ||
| // so definitely not called. | ||
| pv.ExploitabilitySignals = append(pv.ExploitabilitySignals, &vex.FindingExploitabilitySignal{ | ||
| Plugin: Name, | ||
| Justification: vex.VulnerableCodeNotInExecutePath, | ||
| }) | ||
| } | ||
|
|
||
| // Otherwise, we don't know if the code is reachable or not. | ||
| continue | ||
| } | ||
|
|
||
| // For entries with findings, check if the code is reachable or not by whether there is a trace. | ||
| reachable := false | ||
| for _, f := range findings { | ||
| if len(f.Trace) > 0 && f.Trace[0].Function != "" { | ||
| reachable = true | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if !reachable { | ||
| pv.ExploitabilitySignals = append(pv.ExploitabilitySignals, &vex.FindingExploitabilitySignal{ | ||
| Plugin: Name, | ||
| Justification: vex.VulnerableCodeNotInExecutePath, | ||
| }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func (e *Enricher) runGovulncheck(ctx context.Context, absModDir string, vulns []*osvschema.Vulnerability, goVersion string) (map[string][]*internal.Finding, error) { | ||
| // Create a temporary directory containing all the vulnerabilities that | ||
| // are passed in to check against govulncheck. | ||
| // | ||
| // This enables OSV scanner to supply the OSV vulnerabilities to run | ||
| // against govulncheck and manage the database separately from vuln.go.dev. | ||
| dbdir, err := os.MkdirTemp("", "") | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer func() { | ||
| rerr := os.RemoveAll(dbdir) | ||
| if err == nil { | ||
| err = rerr | ||
| } | ||
| }() | ||
|
|
||
| for _, vuln := range vulns { | ||
| dat, err := protojson.Marshal(vuln) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if err := os.WriteFile(fmt.Sprintf("%s/%s.json", dbdir, vuln.GetId()), dat, 0600); err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
|
|
||
| // this only errors if the file path is not absolute, | ||
| // which paths from os.MkdirTemp should always be | ||
| dbdirURL, err := url.FromFilePath(dbdir) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // Run govulncheck on the module at moddir and vulnerability database that | ||
| // was just created. | ||
| cmd := scan.Command(ctx, "-db", dbdirURL.String(), "-C", absModDir, "-json", "-mode", "source", "./...") | ||
| var b bytes.Buffer | ||
| cmd.Stdout = &b | ||
| cmd.Env = append(os.Environ(), "GOVERSION=go"+goVersion) | ||
| if err := cmd.Start(); err != nil { | ||
| return nil, err | ||
| } | ||
| if err := cmd.Wait(); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // Group the output of govulncheck based on the OSV ID. | ||
| h := &osvHandler{ | ||
| idToFindings: map[string][]*internal.Finding{}, | ||
| } | ||
| if err := handleJSON(bytes.NewReader(b.Bytes()), h); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return h.idToFindings, nil | ||
| } | ||
|
|
||
| type osvHandler struct { | ||
| idToFindings map[string][]*internal.Finding | ||
| } | ||
|
|
||
| func (h *osvHandler) Finding(f *internal.Finding) { | ||
| h.idToFindings[f.OSV] = append(h.idToFindings[f.OSV], f) | ||
| } | ||
|
|
||
| func handleJSON(from io.Reader, to *osvHandler) error { | ||
| dec := json.NewDecoder(from) | ||
| for dec.More() { | ||
| msg := internal.Message{} | ||
| if err := dec.Decode(&msg); err != nil { | ||
| return err | ||
| } | ||
| if msg.Finding != nil { | ||
| to.Finding(msg.Finding) | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func vulnHasImportsField(vuln *osvschema.Vulnerability, pkg *extractor.Package) bool { | ||
| for _, affected := range vuln.Affected { | ||
| if pkg != nil { | ||
| // TODO(#1559): Compare versions to see if this is the correct affected element | ||
| // This is very unlikely to ever matter however. | ||
| if affected.Package.Name != pkg.Name { | ||
| continue | ||
| } | ||
| } | ||
| _, hasImportsField := affected.EcosystemSpecific.GetFields()["imports"] | ||
| if hasImportsField { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| // New returns a new govulncheck source enricher. | ||
| func New(cfg *cpb.PluginConfig) enricher.Enricher { | ||
| return &Enricher{} | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.