Skip to content

Commit

Permalink
feat(guided remediation): support offline database in fix subcommand (#…
Browse files Browse the repository at this point in the history
…1306)

#1295
Support the `--experimental-offline` and
`--experimental-download-offline-databases` flags in `osv-scanner fix`.

This is pretty inefficient since `local.MakeRequest()` reads and parses
the zip file every time it's called. I didn't want to make too many
changes to make it more efficient since we'll probably end up
refactoring it in v2.

Also fixed the links to headings on the offline docs page.
  • Loading branch information
michaelkedar authored Oct 15, 2024
1 parent d3b75df commit cbe2f70
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 6 deletions.
32 changes: 28 additions & 4 deletions cmd/osv-scanner/fix/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,20 @@ func Command(stdout, stderr io.Writer, r *reporter.Reporter) *cli.Command {
Name: "maven-fix-management",
Usage: "(pom.xml) also remediate vulnerabilities in dependencyManagement packages that do not appear in the resolved dependency graph",
},
// Offline database flags, copied from osv-scanner scan
&cli.BoolFlag{
Name: "experimental-offline",
Usage: "checks for vulnerabilities using local databases that are already cached",
},
&cli.BoolFlag{
Name: "experimental-download-offline-databases",
Usage: "downloads vulnerability databases for offline comparison",
},
&cli.StringFlag{
Name: "experimental-local-db-path",
Usage: "sets the path that local databases should be stored",
Hidden: true,
},
},
Action: func(ctx *cli.Context) error {
var err error
Expand Down Expand Up @@ -252,13 +266,9 @@ func action(ctx *cli.Context, stdout, stderr io.Writer) (reporter.Reporter, erro
Manifest: ctx.String("manifest"),
Lockfile: ctx.String("lockfile"),
RelockCmd: ctx.String("relock-cmd"),
Client: client.ResolutionClient{
VulnerabilityClient: client.NewOSVClient(),
},
}

system := resolve.UnknownSystem

if opts.Lockfile != "" {
rw, err := lockfile.GetReadWriter(opts.Lockfile)
if err != nil {
Expand Down Expand Up @@ -314,6 +324,20 @@ func action(ctx *cli.Context, stdout, stderr io.Writer) (reporter.Reporter, erro
}
}

if ctx.Bool("experimental-offline") {
var err error
opts.Client.VulnerabilityClient, err = client.NewOSVOfflineClient(
r,
system,
ctx.Bool("experimental-download-offline-databases"),
ctx.String("experimental-local-db-path"))
if err != nil {
return nil, err
}
} else {
opts.Client.VulnerabilityClient = client.NewOSVClient()
}

if !ctx.Bool("non-interactive") {
return nil, interactiveMode(ctx.Context, opts)
}
Expand Down
6 changes: 6 additions & 0 deletions docs/guided-remediation.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@ If your project uses mirrored or private registries, you will need to use `--dat
>
> The native npm cache will store the addresses of private registries used, though not any authentication information.
### Offline Vulnerability Database

The `fix` subcommand supports the `--experimental-offline` and `--experimental-download-offline-databases` flags.

For more information, see [Offline Mode](./offline-mode.md).

## Known issues

- The subcommand does not use the `osv-scanner.toml` configuration. Use the `--ignore-vulns` flag instead.
Expand Down
4 changes: 2 additions & 2 deletions docs/offline-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ If the `OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY` environment variable is _not_ set,
1. The location returned by [`os.UserCacheDir`](https://pkg.go.dev/os#UserCacheDir)
2. The location returned by [`os.TempDir`](https://pkg.go.dev/os#TempDir)

The database can be [downloaded manually](./experimental.md#manual-database-download) or by using the [`--experimental-download-offline-databases` flag](./experimental.md#download-databases-option).
The database can be [downloaded manually](#manual-database-download) or by using the [`--experimental-download-offline-databases` flag](#download-offline-databases-option).

## Offline option

Expand Down Expand Up @@ -88,7 +88,7 @@ You can also download over HTTP via `https://osv-vulnerabilities.storage.googlea
A list of all current ecosystems is available at
[`gs://osv-vulnerabilities/ecosystems.txt`](https://osv-vulnerabilities.storage.googleapis.com/ecosystems.txt).

Set the location of your manually downloaded database by following the instructions [here](./experimental.md#specify-database-location).
Set the location of your manually downloaded database by following the instructions [here](#specify-database-location).

## Limitations

Expand Down
105 changes: 105 additions & 0 deletions internal/resolution/client/osv_offline_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package client

import (
"errors"
"fmt"
"strings"

"deps.dev/util/resolve"
"github.com/google/osv-scanner/internal/local"
"github.com/google/osv-scanner/internal/resolution/util"
"github.com/google/osv-scanner/pkg/models"
"github.com/google/osv-scanner/pkg/osv"
"github.com/google/osv-scanner/pkg/reporter"
)

type OSVOfflineClient struct {
// TODO: OSV-Scanner v2 plans to make vulnerability clients that can be used here.
localDBPath string
}

func NewOSVOfflineClient(r reporter.Reporter, system resolve.System, downloadDBs bool, localDBPath string) (*OSVOfflineClient, error) {
if system == resolve.UnknownSystem {
return nil, errors.New("osv offline client created with unknown ecosystem")
}
// Make a dummy request to the local client to log and make sure the database is downloaded without error.
q := osv.BatchedQuery{Queries: []*osv.Query{{
Package: osv.Package{
Name: "foo",
Ecosystem: string(util.OSVEcosystem[system]),
},
Version: "1.0.0",
}}}
_, err := local.MakeRequest(r, q, !downloadDBs, localDBPath)
if err != nil {
return nil, err
}

if r.HasErrored() {
return nil, errors.New("error creating osv offline client")
}

return &OSVOfflineClient{localDBPath: localDBPath}, nil
}

func (c *OSVOfflineClient) FindVulns(g *resolve.Graph) ([]models.Vulnerabilities, error) {
var query osv.BatchedQuery
query.Queries = make([]*osv.Query, len(g.Nodes)-1)
for i, node := range g.Nodes[1:] {
query.Queries[i] = &osv.Query{
Package: osv.Package{
Name: node.Version.Name,
Ecosystem: string(util.OSVEcosystem[node.Version.System]),
},
Version: node.Version.Version,
}
}

// If local.MakeRequest logs an error, it's probably fatal for guided remediation.
// Set up a reporter to capture error logs and return the logs as an error.
r := &errorReporter{}
// DB should already be downloaded, set offline to true.
hydrated, err := local.MakeRequest(r, query, true, c.localDBPath)

if err != nil {
return nil, err
}

if r.HasErrored() {
return nil, r.GetError()
}

nodeVulns := make([]models.Vulnerabilities, len(g.Nodes))
for i, res := range hydrated.Results {
nodeVulns[i+1] = res.Vulns
}

return nodeVulns, nil
}

// errorReporter is a reporter.Reporter to capture error logs and pack them into an error.
type errorReporter struct {
s strings.Builder
}

func (r *errorReporter) Errorf(format string, a ...any) {
fmt.Fprintf(&r.s, format, a...)
}

func (r *errorReporter) HasErrored() bool {
return r.s.Len() > 0
}

func (r *errorReporter) GetError() error {
str := strings.TrimSpace(r.s.String())
if str == "" {
return nil
}

return errors.New(str)
}

func (r *errorReporter) Warnf(string, ...any) {}
func (r *errorReporter) Infof(string, ...any) {}
func (r *errorReporter) Verbosef(string, ...any) {}
func (r *errorReporter) PrintResult(*models.VulnerabilityResults) error { return nil }

0 comments on commit cbe2f70

Please sign in to comment.