| // 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 ( |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "os" |
| "strings" |
| |
| "golang.org/x/tools/go/buildutil" |
| "golang.org/x/vuln/internal/govulncheck" |
| ) |
| |
| type config struct { |
| govulncheck.Config |
| patterns []string |
| db string |
| dir string |
| tags buildutil.TagsFlag |
| test bool |
| show ShowFlag |
| format FormatFlag |
| env []string |
| } |
| |
| func parseFlags(cfg *config, stderr io.Writer, args []string) error { |
| var version bool |
| var json bool |
| var scanFlag ScanFlag |
| var modeFlag ModeFlag |
| flags := flag.NewFlagSet("", flag.ContinueOnError) |
| flags.SetOutput(stderr) |
| flags.BoolVar(&json, "json", false, "output JSON (Go compatible legacy flag, see format flag)") |
| flags.BoolVar(&cfg.test, "test", false, "analyze test files (only valid for source mode, default false)") |
| flags.StringVar(&cfg.dir, "C", "", "change to `dir` before running govulncheck") |
| flags.StringVar(&cfg.db, "db", "https://vuln.go.dev", "vulnerability database `url`") |
| flags.Var(&modeFlag, "mode", "supports 'source', 'binary', and 'extract' (default 'source')") |
| flags.Var(&cfg.tags, "tags", "comma-separated `list` of build tags") |
| flags.Var(&cfg.show, "show", "enable display of additional information specified by the comma separated `list`\nThe supported values are 'traces','color', 'version', and 'verbose'") |
| flags.Var(&cfg.format, "format", "specify format output\nThe supported values are 'text', 'json', 'sarif', and 'openvex' (default 'text')") |
| flags.BoolVar(&version, "version", false, "print the version information") |
| flags.Var(&scanFlag, "scan", "set the scanning level desired, one of 'module', 'package', or 'symbol' (default 'symbol')") |
| |
| // We don't want to print the whole usage message on each flags |
| // error, so we set to a no-op and do the printing ourselves. |
| flags.Usage = func() {} |
| usage := func() { |
| fmt.Fprint(flags.Output(), `Govulncheck reports known vulnerabilities in dependencies. |
| |
| Usage: |
| |
| govulncheck [flags] [patterns] |
| govulncheck -mode=binary [flags] [binary] |
| |
| `) |
| flags.PrintDefaults() |
| fmt.Fprintf(flags.Output(), "\n%s\n", detailsMessage) |
| } |
| |
| if err := flags.Parse(args); err != nil { |
| if err == flag.ErrHelp { |
| usage() // print usage only on help |
| return errHelp |
| } |
| return errUsage |
| } |
| cfg.patterns = flags.Args() |
| if version { |
| cfg.show = append(cfg.show, "version") |
| } |
| cfg.ScanLevel = govulncheck.ScanLevel(scanFlag) |
| cfg.ScanMode = govulncheck.ScanMode(modeFlag) |
| if err := validateConfig(cfg, json); err != nil { |
| fmt.Fprintln(flags.Output(), err) |
| return errUsage |
| } |
| return nil |
| } |
| |
| func validateConfig(cfg *config, json bool) error { |
| // take care of default values |
| if cfg.ScanMode == "" { |
| cfg.ScanMode = govulncheck.ScanModeSource |
| } |
| if cfg.ScanLevel == "" { |
| cfg.ScanLevel = govulncheck.ScanLevelSymbol |
| } |
| if json { |
| if cfg.format != formatUnset { |
| return fmt.Errorf("the -json flag cannot be used with -format flag") |
| } |
| cfg.format = formatJSON |
| } else { |
| if cfg.format == formatUnset { |
| cfg.format = formatText |
| } |
| } |
| |
| // show flag is only supported with text output |
| if cfg.format != formatText && len(cfg.show) > 0 { |
| return fmt.Errorf("the -show flag is not supported for %s output", cfg.format) |
| } |
| |
| switch cfg.ScanMode { |
| case govulncheck.ScanModeSource: |
| if len(cfg.patterns) == 1 && isFile(cfg.patterns[0]) { |
| return fmt.Errorf("%q is a file.\n\n%v", cfg.patterns[0], errNoBinaryFlag) |
| } |
| if cfg.ScanLevel == govulncheck.ScanLevelModule && len(cfg.patterns) != 0 { |
| return fmt.Errorf("patterns are not accepted for module only scanning") |
| } |
| case govulncheck.ScanModeBinary: |
| if cfg.test { |
| return fmt.Errorf("the -test flag is not supported in binary mode") |
| } |
| if len(cfg.tags) > 0 { |
| return fmt.Errorf("the -tags flag is not supported in binary mode") |
| } |
| if len(cfg.patterns) != 1 { |
| return fmt.Errorf("only 1 binary can be analyzed at a time") |
| } |
| if !isFile(cfg.patterns[0]) { |
| return fmt.Errorf("%q is not a file", cfg.patterns[0]) |
| } |
| case govulncheck.ScanModeExtract: |
| if cfg.test { |
| return fmt.Errorf("the -test flag is not supported in extract mode") |
| } |
| if len(cfg.tags) > 0 { |
| return fmt.Errorf("the -tags flag is not supported in extract mode") |
| } |
| if len(cfg.patterns) != 1 { |
| return fmt.Errorf("only 1 binary can be extracted at a time") |
| } |
| if cfg.format == formatJSON { |
| return fmt.Errorf("the json format must be off in extract mode") |
| } |
| if !isFile(cfg.patterns[0]) { |
| return fmt.Errorf("%q is not a file (source extraction is not supported)", cfg.patterns[0]) |
| } |
| case govulncheck.ScanModeConvert: |
| if len(cfg.patterns) != 0 { |
| return fmt.Errorf("patterns are not accepted in convert mode") |
| } |
| if cfg.dir != "" { |
| return fmt.Errorf("the -C flag is not supported in convert mode") |
| } |
| if cfg.test { |
| return fmt.Errorf("the -test flag is not supported in convert mode") |
| } |
| if len(cfg.tags) > 0 { |
| return fmt.Errorf("the -tags flag is not supported in convert mode") |
| } |
| case govulncheck.ScanModeQuery: |
| if cfg.test { |
| return fmt.Errorf("the -test flag is not supported in query mode") |
| } |
| if len(cfg.tags) > 0 { |
| return fmt.Errorf("the -tags flag is not supported in query mode") |
| } |
| if cfg.format != formatJSON { |
| return fmt.Errorf("the json format must be set in query mode") |
| } |
| for _, pattern := range cfg.patterns { |
| // Parse the input here so that we can catch errors before |
| // outputting the Config. |
| if _, _, err := parseModuleQuery(pattern); err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| |
| func isFile(path string) bool { |
| s, err := os.Stat(path) |
| if err != nil { |
| return false |
| } |
| return !s.IsDir() |
| } |
| |
| var errFlagParse = errors.New("see -help for details") |
| |
| // ShowFlag is used for parsing and validation of |
| // govulncheck -show flag. |
| type ShowFlag []string |
| |
| var supportedShows = map[string]bool{ |
| "traces": true, |
| "color": true, |
| "verbose": true, |
| "version": true, |
| } |
| |
| func (v *ShowFlag) Set(s string) error { |
| if s == "" { |
| return nil |
| } |
| for _, show := range strings.Split(s, ",") { |
| sh := strings.TrimSpace(show) |
| if _, ok := supportedShows[sh]; !ok { |
| return errFlagParse |
| } |
| *v = append(*v, sh) |
| } |
| return nil |
| } |
| |
| func (v *ShowFlag) Get() interface{} { return *v } |
| func (v *ShowFlag) String() string { return "" } |
| |
| // Update the text handler h with values of the flag. |
| func (v ShowFlag) Update(h *TextHandler) { |
| for _, show := range v { |
| switch show { |
| case "traces": |
| h.showTraces = true |
| case "color": |
| h.showColor = true |
| case "version": |
| h.showVersion = true |
| case "verbose": |
| h.showVerbose = true |
| } |
| } |
| } |
| |
| // FormatFlag is used for parsing and validation of |
| // govulncheck -format flag. |
| type FormatFlag string |
| |
| const ( |
| formatUnset = "" |
| formatJSON = "json" |
| formatText = "text" |
| formatSarif = "sarif" |
| formatOpenVEX = "openvex" |
| ) |
| |
| var supportedFormats = map[string]bool{ |
| formatJSON: true, |
| formatText: true, |
| formatSarif: true, |
| formatOpenVEX: true, |
| } |
| |
| func (f *FormatFlag) Get() interface{} { return *f } |
| func (f *FormatFlag) Set(s string) error { |
| if _, ok := supportedFormats[s]; !ok { |
| return errFlagParse |
| } |
| *f = FormatFlag(s) |
| return nil |
| } |
| func (f *FormatFlag) String() string { return "" } |
| |
| // ModeFlag is used for parsing and validation of |
| // govulncheck -mode flag. |
| type ModeFlag string |
| |
| var supportedModes = map[string]bool{ |
| govulncheck.ScanModeSource: true, |
| govulncheck.ScanModeBinary: true, |
| govulncheck.ScanModeConvert: true, |
| govulncheck.ScanModeQuery: true, |
| govulncheck.ScanModeExtract: true, |
| } |
| |
| func (f *ModeFlag) Get() interface{} { return *f } |
| func (f *ModeFlag) Set(s string) error { |
| if _, ok := supportedModes[s]; !ok { |
| return errFlagParse |
| } |
| *f = ModeFlag(s) |
| return nil |
| } |
| func (f *ModeFlag) String() string { return "" } |
| |
| // ScanFlag is used for parsing and validation of |
| // govulncheck -scan flag. |
| type ScanFlag string |
| |
| var supportedLevels = map[string]bool{ |
| govulncheck.ScanLevelModule: true, |
| govulncheck.ScanLevelPackage: true, |
| govulncheck.ScanLevelSymbol: true, |
| } |
| |
| func (f *ScanFlag) Get() interface{} { return *f } |
| func (f *ScanFlag) Set(s string) error { |
| if _, ok := supportedLevels[s]; !ok { |
| return errFlagParse |
| } |
| *f = ScanFlag(s) |
| return nil |
| } |
| func (f *ScanFlag) String() string { return "" } |