// Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package scan import ( "context" "fmt" "io" "os/exec" "path" "path/filepath" "runtime/debug" "strings" "time" "golang.org/x/telemetry/counter" "golang.org/x/vuln/internal/client" "golang.org/x/vuln/internal/govulncheck" "golang.org/x/vuln/internal/openvex" "golang.org/x/vuln/internal/sarif" ) // RunGovulncheck performs main govulncheck functionality and exits the // program upon success with an appropriate exit status. Otherwise, // returns an error. func RunGovulncheck(ctx context.Context, env []string, r io.Reader, stdout io.Writer, stderr io.Writer, args []string) error { cfg := &config{env: env} if err := parseFlags(cfg, stderr, args); err != nil { return err } client, err := client.NewClient(cfg.db, nil) if err != nil { return fmt.Errorf("creating client: %w", err) } prepareConfig(ctx, cfg, client) var handler govulncheck.Handler switch cfg.format { case formatJSON: handler = govulncheck.NewJSONHandler(stdout) case formatSarif: handler = sarif.NewHandler(stdout) case formatOpenVEX: handler = openvex.NewHandler(stdout) default: th := NewTextHandler(stdout) cfg.show.Update(th) handler = th } if err := handler.Config(&cfg.Config); err != nil { return err } incTelemetryFlagCounters(cfg) switch cfg.ScanMode { case govulncheck.ScanModeSource: dir := filepath.FromSlash(cfg.dir) err = runSource(ctx, handler, cfg, client, dir) case govulncheck.ScanModeBinary: err = runBinary(ctx, handler, cfg, client) case govulncheck.ScanModeExtract: return runExtract(cfg, stdout) case govulncheck.ScanModeQuery: err = runQuery(ctx, handler, cfg, client) case govulncheck.ScanModeConvert: err = govulncheck.HandleJSON(r, handler) } if err != nil { return err } return Flush(handler) } func prepareConfig(ctx context.Context, cfg *config, client *client.Client) { cfg.ProtocolVersion = govulncheck.ProtocolVersion cfg.DB = cfg.db if cfg.ScanMode == govulncheck.ScanModeSource && cfg.GoVersion == "" { const goverPrefix = "GOVERSION=" for _, env := range cfg.env { if val := strings.TrimPrefix(env, goverPrefix); val != env { cfg.GoVersion = val } } if cfg.GoVersion == "" { if out, err := exec.Command("go", "env", "GOVERSION").Output(); err == nil { cfg.GoVersion = strings.TrimSpace(string(out)) } } } if bi, ok := debug.ReadBuildInfo(); ok { scannerVersion(cfg, bi) } if mod, err := client.LastModifiedTime(ctx); err == nil { cfg.DBLastModified = &mod } } // scannerVersion reconstructs the current version of // this binary used from the build info. func scannerVersion(cfg *config, bi *debug.BuildInfo) { if bi.Path != "" { cfg.ScannerName = path.Base(bi.Path) } if bi.Main.Version != "" && bi.Main.Version != "(devel)" { cfg.ScannerVersion = bi.Main.Version return } // TODO(https://go.dev/issue/29228): we need to manually construct the // version string when it is "(devel)" until #29228 is resolved. var revision, at string for _, s := range bi.Settings { if s.Key == "vcs.revision" { revision = s.Value } if s.Key == "vcs.time" { at = s.Value } } buf := strings.Builder{} buf.WriteString("v0.0.0") if revision != "" { buf.WriteString("-") buf.WriteString(revision[:12]) } if at != "" { // commit time is of the form 2023-01-25T19:57:54Z p, err := time.Parse(time.RFC3339, at) if err == nil { buf.WriteString("-") buf.WriteString(p.Format("20060102150405")) } } cfg.ScannerVersion = buf.String() } func incTelemetryFlagCounters(cfg *config) { counter.Inc(fmt.Sprintf("govulncheck/mode:%s", cfg.ScanMode)) counter.Inc(fmt.Sprintf("govulncheck/scan:%s", cfg.ScanLevel)) counter.Inc(fmt.Sprintf("govulncheck/format:%s", cfg.format)) if len(cfg.show) == 0 { counter.Inc("govulncheck/show:none") } for _, s := range cfg.show { counter.Inc(fmt.Sprintf("govulncheck/show:%s", s)) } } func Flush(h govulncheck.Handler) error { if th, ok := h.(interface{ Flush() error }); ok { return th.Flush() } return nil }