From ca90fa1bd0e29539c3f4b8c948ffe2c996e99c8d Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Thu, 13 Aug 2020 19:55:23 -0400 Subject: [PATCH 1/6] separate `sleuth` command for 'oss index' --- cmd/iq.go | 16 ++- cmd/root.go | 58 +++-------- cmd/root_test.go | 145 +++------------------------ cmd/sleuth.go | 59 +++++++++++ cmd/sleuth_test.go | 150 ++++++++++++++++++++++++++++ go.mod | 1 + internal/customerrors/error.go | 2 +- internal/customerrors/error_test.go | 4 +- 8 files changed, 257 insertions(+), 178 deletions(-) create mode 100644 cmd/sleuth.go create mode 100644 cmd/sleuth_test.go diff --git a/cmd/iq.go b/cmd/iq.go index eaf30b22..4abf31e7 100644 --- a/cmd/iq.go +++ b/cmd/iq.go @@ -18,6 +18,7 @@ package cmd import ( "errors" "fmt" + "github.com/spf13/pflag" "os" "path" @@ -162,17 +163,26 @@ func bindViperIq(cmd *cobra.Command) { bindViper(rootCmd) // Bind viper to the flags passed in via the command line, so it will override config from file - if err := viper.BindPFlag(configuration.ViperKeyIQUsername, cmd.Flags().Lookup(flagNameIqUsername)); err != nil { + if err := viper.BindPFlag(configuration.ViperKeyIQUsername, lookupFlagNotNil(flagNameIqUsername, cmd)); err != nil { panic(err) } - if err := viper.BindPFlag(configuration.ViperKeyIQToken, cmd.Flags().Lookup(flagNameIqToken)); err != nil { + if err := viper.BindPFlag(configuration.ViperKeyIQToken, lookupFlagNotNil(flagNameIqToken, cmd)); err != nil { panic(err) } - if err := viper.BindPFlag(configuration.ViperKeyIQServer, cmd.Flags().Lookup(flagNameIqServerUrl)); err != nil { + if err := viper.BindPFlag(configuration.ViperKeyIQServer, lookupFlagNotNil(flagNameIqServerUrl, cmd)); err != nil { panic(err) } } +func lookupFlagNotNil(flagName string, cmd *cobra.Command) *pflag.Flag { + // see: https://github.com/spf13/viper/pull/949 + foundFlag := cmd.Flags().Lookup(flagName) + if foundFlag == nil { + panic(fmt.Errorf("flag lookup for name: '%s' returned nil", flagName)) + } + return foundFlag +} + func initIQConfig() { var cfgFileToCheck string if cfgFileIQ != "" { diff --git a/cmd/root.go b/cmd/root.go index e907cf48..f18a1953 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,6 +19,7 @@ package cmd import ( "bufio" "fmt" + "github.com/spf13/pflag" "os" "path" "path/filepath" @@ -39,7 +40,6 @@ import ( "github.com/sonatype-nexus-community/nancy/buildversion" "github.com/sonatype-nexus-community/nancy/internal/audit" "github.com/sonatype-nexus-community/nancy/internal/customerrors" - "github.com/sonatype-nexus-community/nancy/internal/logger" "github.com/sonatype-nexus-community/nancy/packages" "github.com/sonatype-nexus-community/nancy/parse" "github.com/sonatype-nexus-community/nancy/types" @@ -112,39 +112,9 @@ var rootCmd = &cobra.Command{ Long: `nancy is a tool to check for vulnerabilities in your Golang dependencies, powered by the 'Sonatype OSS Index', and as well, works with Nexus IQ Server, allowing you a smooth experience as a Golang developer, using the best tools in the market!`, - PreRun: func(cmd *cobra.Command, args []string) { bindViper(cmd) }, - RunE: doOSSI, -} - -//noinspection GoUnusedParameter -func doOSSI(cmd *cobra.Command, args []string) (err error) { - defer func() { - if r := recover(); r != nil { - var ok bool - err, ok = r.(error) - if !ok { - err = fmt.Errorf("pkg: %v", r) - } - err = customerrors.ErrorShowLogPath{Err: err} - } - }() - - logLady = logger.GetLogger("", configOssi.LogLevel) - logLady.Info("Nancy parsing config for OSS Index") - - err = processConfig() - if err != nil { - if errExit, ok := err.(customerrors.ErrorExit); ok { - logLady.Info(fmt.Sprintf("Nancy finished parsing config for OSS Index, vulnerability found. exit code: %d", errExit.ExitCode)) - os.Exit(errExit.ExitCode) - } else { - logLady.WithError(err).Error("unexpected error in root cmd") - panic(err) - } - } - - logLady.Info("Nancy finished parsing config for OSS Index") - return + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Usage() + }, } func Execute() (err error) { @@ -171,28 +141,32 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&configOssi.Version, "version", "V", false, "Get the version") rootCmd.PersistentFlags().BoolVarP(&configOssi.Quiet, "quiet", "q", true, "indicate output should contain only packages with vulnerabilities") rootCmd.PersistentFlags().BoolVar(&configOssi.Loud, "loud", false, "indicate output should include non-vulnerable packages") - rootCmd.Flags().BoolVarP(&configOssi.NoColor, "no-color", "n", false, "indicate output should not be colorized") - rootCmd.Flags().BoolVarP(&configOssi.CleanCache, "clean-cache", "c", false, "Deletes local cache directory") - rootCmd.Flags().VarP(&configOssi.CveList, "exclude-vulnerability", "e", "Comma separated list of CVEs to exclude") - rootCmd.Flags().StringVarP(&configOssi.Path, "path", "p", "", "Specify a path to a dep Gopkg.lock file for scanning") + rootCmd.PersistentFlags().BoolVarP(&configOssi.CleanCache, "clean-cache", "c", false, "Deletes local cache directory") rootCmd.PersistentFlags().StringVarP(&configOssi.Username, flagNameOssiUsername, "u", "", "Specify OSS Index username for request") rootCmd.PersistentFlags().StringVarP(&configOssi.Token, flagNameOssiToken, "t", "", "Specify OSS Index API token for request") - rootCmd.Flags().StringVarP(&excludeVulnerabilityFilePath, "exclude-vulnerability-file", "x", defaultExcludeFilePath, "Path to a file containing newline separated CVEs to be excluded") - rootCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Styling for output format. json, json-pretty, text, csv") } func bindViper(cmd *cobra.Command) { // need to defer bind call until command is run. see: https://github.com/spf13/viper/issues/233 // Bind viper to the flags passed in via the command line, so it will override config from file - if err := viper.BindPFlag(configuration.ViperKeyUsername, cmd.PersistentFlags().Lookup(flagNameOssiUsername)); err != nil { + if err := viper.BindPFlag(configuration.ViperKeyUsername, lookupPersistentFlagNotNil(flagNameOssiUsername, cmd)); err != nil { panic(err) } - if err := viper.BindPFlag(configuration.ViperKeyToken, cmd.PersistentFlags().Lookup(flagNameOssiToken)); err != nil { + if err := viper.BindPFlag(configuration.ViperKeyToken, lookupPersistentFlagNotNil(flagNameOssiToken, cmd)); err != nil { panic(err) } } +func lookupPersistentFlagNotNil(flagName string, cmd *cobra.Command) *pflag.Flag { + // see: https://github.com/spf13/viper/pull/949 + foundFlag := cmd.PersistentFlags().Lookup(flagName) + if foundFlag == nil { + panic(fmt.Errorf("persisent flag lookup for name: '%s' returned nil", flagName)) + } + return foundFlag +} + const configTypeYaml = "yaml" func initConfig() { diff --git a/cmd/root_test.go b/cmd/root_test.go index aae568c6..4a4cfd44 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -54,8 +54,7 @@ func executeCommand(root *cobra.Command, args ...string) (output string, err err func TestRootCommandNoArgs(t *testing.T) { _, err := executeCommand(rootCmd, "") - assert.NotNil(t, err) - assert.Equal(t, customerrors.ErrorShowLogPath{Err: stdInInvalid}, err) + assert.Nil(t, err) } func TestRootCommandUnknownCommand(t *testing.T) { @@ -66,18 +65,14 @@ func TestRootCommandUnknownCommand(t *testing.T) { assert.Contains(t, err.Error(), "unknown command \"one\" for \"nancy\"") } -func TestRootCommandInvalidPath(t *testing.T) { - _, err := executeCommand(rootCmd, "--path", "invalidPath") - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid path value. must point to 'Gopkg.lock' file. path: ") -} - func TestProcessConfigInvalidStdIn(t *testing.T) { origConfig := configOssi defer func() { configOssi = origConfig }() configOssi = types.Configuration{} + logLady, _ = test.NewNullLogger() + err := processConfig() assert.Equal(t, stdInInvalid, err) } @@ -271,150 +266,39 @@ func validateConfigOssi(t *testing.T, expectedConfig types.Configuration, args . assert.Equal(t, expectedConfig, configOssi) } -var defaultAuditLogFormatter = audit.AuditLogTextFormatter{Quiet: true} - func TestRootCommandLogVerbosity(t *testing.T) { - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter}, "") - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter, LogLevel: 1}, "-v") - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter, LogLevel: 2}, "-vv") - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter, LogLevel: 3}, "-vvv") -} - -func TestConfigOssi_defaults(t *testing.T) { - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter}, []string{}...) -} - -func TestConfigOssi_no_color(t *testing.T) { - validateConfigOssi(t, types.Configuration{NoColor: true, Formatter: audit.AuditLogTextFormatter{NoColor: true, Quiet: true}}, []string{"--no-color"}...) -} + logLady, _ = test.NewNullLogger() -func TestConfigOssi_quiet(t *testing.T) { - validateConfigOssi(t, types.Configuration{Quiet: true, Formatter: audit.AuditLogTextFormatter{Quiet: true}}, []string{"--quiet"}...) + validateConfigOssi(t, types.Configuration{}, "") + validateConfigOssi(t, types.Configuration{LogLevel: 1}, "-v") + validateConfigOssi(t, types.Configuration{LogLevel: 2}, "-vv") + validateConfigOssi(t, types.Configuration{LogLevel: 3}, "-vvv") } -func TestConfigOssi_loud(t *testing.T) { - validateConfigOssi(t, types.Configuration{Loud: true, Formatter: audit.AuditLogTextFormatter{Quiet: false}}, []string{"--loud"}...) +func TestConfigOssi_defaults(t *testing.T) { + validateConfigOssi(t, types.Configuration{}, []string{}...) } func TestConfigOssi_version(t *testing.T) { validateConfigOssi(t, types.Configuration{Version: true, Formatter: logrus.Formatter(nil)}, []string{"--version"}...) } -func TestConfigOssi_exclude_vulnerabilities(t *testing.T) { - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"CVE123", "CVE988"}}, Formatter: defaultAuditLogFormatter}, []string{"--exclude-vulnerability=CVE123,CVE988"}...) -} - -func TestConfigOssi_stdIn_as_input(t *testing.T) { - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter}, []string{}...) -} - -const testdataDir = "../internal/configuration/testdata" - -func TestConfigOssi_exclude_vulnerabilities_with_sane_file(t *testing.T) { - file, _ := os.Open(testdataDir + "/normalIgnore") - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"CVF-000", "CVF-123", "CVF-9999"}}, Formatter: defaultAuditLogFormatter}, []string{"--exclude-vulnerability-file=" + file.Name()}...) -} - -func TestConfigOssi_exclude_vulnerabilities_when_file_empty(t *testing.T) { - emptyFile, _ := os.Open(testdataDir + "/emptyFile") - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{}, Formatter: defaultAuditLogFormatter}, []string{"--exclude-vulnerability-file=" + emptyFile.Name()}...) -} - -func TestConfigOssi_exclude_vulnerabilities_when_has_tons_of_newlines(t *testing.T) { - lotsOfRandomNewlinesFile, _ := os.Open(testdataDir + "/lotsOfRandomWhitespace") - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"CVN-111", "CVN-123", "CVN-543"}}, Formatter: defaultAuditLogFormatter}, []string{"--exclude-vulnerability-file=" + lotsOfRandomNewlinesFile.Name()}...) -} - -func TestConfigOssi_exclude_vulnerabilities_are_combined_with_file_and_args_values(t *testing.T) { - lotsOfRandomNewlinesFile, _ := os.Open(testdataDir + "/lotsOfRandomWhitespace") - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"CVE123", "CVE988", "CVN-111", "CVN-123", "CVN-543"}}, Formatter: defaultAuditLogFormatter}, []string{"--exclude-vulnerability=CVE123,CVE988", "--exclude-vulnerability-file=" + lotsOfRandomNewlinesFile.Name()}...) -} - -func TestConfigOssi_exclude_vulnerabilities_file_not_found_does_not_matter(t *testing.T) { - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{}, Formatter: defaultAuditLogFormatter}, []string{"--exclude-vulnerability-file=/blah-blah-doesnt-exists"}...) -} - -func TestConfigOssi_exclude_vulnerabilities_passed_as_directory_does_not_matter(t *testing.T) { - dir, _ := ioutil.TempDir("", "prefix") - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{}, Formatter: defaultAuditLogFormatter}, []string{"--exclude-vulnerability-file=" + dir}...) -} - -func TestConfigOssi_exclude_vulnerabilities_does_not_need_to_be_passed_if_default_value_is_used(t *testing.T) { - defaultFileName := ".nancy-ignore" - err := ioutil.WriteFile(defaultFileName, []byte("DEF-111\nDEF-222"), 0644) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = os.Remove(defaultFileName) - }() - - // reset exclude file path, is changed by prior tests - origExcludeVulnerabilityFilePath := excludeVulnerabilityFilePath - defer func() { - excludeVulnerabilityFilePath = origExcludeVulnerabilityFilePath - }() - excludeVulnerabilityFilePath = defaultExcludeFilePath - - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"DEF-111", "DEF-222"}}, Formatter: defaultAuditLogFormatter}, []string{}...) -} - -func TestConfigOssi_exclude_vulnerabilities_when_has_comments(t *testing.T) { - commentedFile, _ := os.Open(testdataDir + "/commented") - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"CVN-111", "CVN-123", "CVN-543"}}, Formatter: defaultAuditLogFormatter}, []string{"--exclude-vulnerability-file=" + commentedFile.Name()}...) -} - -func TestConfigOssi_exclude_vulnerabilities_when_has_untils(t *testing.T) { - untilsFile, _ := os.Open(testdataDir + "/untilsAndComments") - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"NO-UNTIL-888", "MUST-BE-IGNORED-999", "MUST-BE-IGNORED-1999"}}, Formatter: defaultAuditLogFormatter}, []string{"--exclude-vulnerability-file=" + untilsFile.Name()}...) -} - -func TestConfigOssi_exclude_vulnerabilities_when_has_invalid_value_in_untils(t *testing.T) { - invalidUntilsFile, _ := os.Open(testdataDir + "/untilsInvaild") - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{}, Formatter: defaultAuditLogFormatter}, []string{"--exclude-vulnerability-file=" + invalidUntilsFile.Name()}...) -} - -func TestConfigOssi_exclude_vulnerabilities_when_has_invalid_date_in_untils(t *testing.T) { - invalidDateUntilsFile, _ := os.Open(testdataDir + "/untilsBadDateFormat") - validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{}, Formatter: defaultAuditLogFormatter}, []string{"--exclude-vulnerability-file=" + invalidDateUntilsFile.Name()}...) -} - -func TestConfigOssi_output_of_json(t *testing.T) { - validateConfigOssi(t, types.Configuration{Formatter: audit.JsonFormatter{}}, []string{"--output=json"}...) -} - -func TestConfigOssi_output_of_json_pretty_print(t *testing.T) { - validateConfigOssi(t, types.Configuration{Formatter: audit.JsonFormatter{PrettyPrint: true}}, []string{"--output=json-pretty"}...) -} - -func TestConfigOssi_output_of_csv(t *testing.T) { - validateConfigOssi(t, types.Configuration{Formatter: audit.CsvFormatter{Quiet: true}}, []string{"--output=csv"}...) -} - -func TestConfigOssi_output_of_text(t *testing.T) { - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter}, []string{"--output=text"}...) -} - -func TestConfigOssi_output_of_bad_value(t *testing.T) { - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter}, []string{"--output=aintgonnadoit"}...) -} - func TestConfigOssi_log_level_of_info(t *testing.T) { - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter, LogLevel: 1}, []string{"-v"}...) + validateConfigOssi(t, types.Configuration{LogLevel: 1}, []string{"-v"}...) } func TestConfigOssi_log_level_of_debug(t *testing.T) { - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter, LogLevel: 2}, []string{"-vv"}...) + validateConfigOssi(t, types.Configuration{LogLevel: 2}, []string{"-vv"}...) } func TestConfigOssi_log_level_of_trace(t *testing.T) { flag.CommandLine = flag.NewFlagSet("", flag.ContinueOnError) - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter, LogLevel: 3}, []string{"-vvv"}...) + validateConfigOssi(t, types.Configuration{LogLevel: 3}, []string{"-vvv"}...) } func TestConfigOssi_cleanCache(t *testing.T) { - validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter, CleanCache: true}, []string{"--clean-cache"}...) + validateConfigOssi(t, types.Configuration{CleanCache: true}, []string{"--clean-cache"}...) } func setupConfig(t *testing.T) (tempDir string) { @@ -607,6 +491,7 @@ func TestOssiCreatorOptions(t *testing.T) { defer func() { ossiCreator = origCreator }() + logLady, _ = test.NewNullLogger() ossIndex := ossiCreator.create() ossIndexServer, ok := ossIndex.(*ossindex.Server) diff --git a/cmd/sleuth.go b/cmd/sleuth.go new file mode 100644 index 00000000..c7a0b361 --- /dev/null +++ b/cmd/sleuth.go @@ -0,0 +1,59 @@ +package cmd + +import ( + "fmt" + "github.com/sonatype-nexus-community/nancy/internal/customerrors" + "github.com/sonatype-nexus-community/nancy/internal/logger" + "github.com/spf13/cobra" + "os" +) + +func init() { + rootCmd.AddCommand(sleuthCmd) + + sleuthCmd.Flags().BoolVarP(&configOssi.NoColor, "no-color", "n", false, "indicate output should not be colorized") + sleuthCmd.Flags().VarP(&configOssi.CveList, "exclude-vulnerability", "e", "Comma separated list of CVEs to exclude") + sleuthCmd.Flags().StringVarP(&configOssi.Path, "path", "p", "", "Specify a path to a dep Gopkg.lock file for scanning") + sleuthCmd.Flags().StringVarP(&excludeVulnerabilityFilePath, "exclude-vulnerability-file", "x", defaultExcludeFilePath, "Path to a file containing newline separated CVEs to be excluded") + sleuthCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Styling for output format. json, json-pretty, text, csv") +} + +var sleuthCmd = &cobra.Command{ + Use: "sleuth", + Example: ` go list -json -m all | nancy sleuth --` + flagNameOssiUsername + ` your_user --` + flagNameOssiToken + ` your_token`, + Short: "Check for vulnerabilities in your Golang dependencies using Sonatype's OSS Index", + Long: `'nancy iq' is a command to check for vulnerabilities in your Golang dependencies, powered by 'Sonatype's Nexus IQ IQServer', allowing you a smooth experience as a Golang developer, using the best tools in the market!`, + PreRun: func(cmd *cobra.Command, args []string) { bindViper(rootCmd) }, + RunE: doOSSI, +} + +//noinspection GoUnusedParameter +func doOSSI(cmd *cobra.Command, args []string) (err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = fmt.Errorf("pkg: %v", r) + } + err = customerrors.ErrorShowLogPath{Err: err} + } + }() + + logLady = logger.GetLogger("", configOssi.LogLevel) + logLady.Info("Nancy parsing config for OSS Index") + + err = processConfig() + if err != nil { + if errExit, ok := err.(customerrors.ErrorExit); ok { + logLady.Info(fmt.Sprintf("Nancy finished parsing config for OSS Index, vulnerability found. exit code: %d", errExit.ExitCode)) + os.Exit(errExit.ExitCode) + } else { + logLady.WithError(err).Error("unexpected error in root cmd") + panic(err) + } + } + + logLady.Info("Nancy finished parsing config for OSS Index") + return +} diff --git a/cmd/sleuth_test.go b/cmd/sleuth_test.go new file mode 100644 index 00000000..ec8c6682 --- /dev/null +++ b/cmd/sleuth_test.go @@ -0,0 +1,150 @@ +package cmd + +import ( + "github.com/sonatype-nexus-community/nancy/internal/audit" + "github.com/sonatype-nexus-community/nancy/internal/customerrors" + "github.com/sonatype-nexus-community/nancy/types" + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "testing" +) + +func TestSleuthCommandNoArgs(t *testing.T) { + _, err := executeCommand(rootCmd, sleuthCmd.Use) + assert.NotNil(t, err) + assert.Equal(t, customerrors.ErrorShowLogPath{Err: stdInInvalid}, err) +} + +func TestSleuthCommandInvalidPath(t *testing.T) { + _, err := executeCommand(rootCmd, sleuthCmd.Use, "--path", "invalidPath") + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid path value. must point to 'Gopkg.lock' file. path: ") +} + +func TestConfigOssi_no_color(t *testing.T) { + validateConfigOssi(t, types.Configuration{NoColor: true, Formatter: audit.AuditLogTextFormatter{NoColor: true, Quiet: true}}, []string{sleuthCmd.Use, "--no-color"}...) +} + +func TestConfigOssi_quiet(t *testing.T) { + validateConfigOssi(t, types.Configuration{Quiet: true, Formatter: audit.AuditLogTextFormatter{Quiet: true}}, + []string{sleuthCmd.Use, "--quiet"}...) +} + +func TestConfigOssi_loud(t *testing.T) { + validateConfigOssi(t, types.Configuration{Loud: true, Formatter: audit.AuditLogTextFormatter{Quiet: false}}, + []string{sleuthCmd.Use, "--loud"}...) +} + +var defaultAuditLogFormatter = audit.AuditLogTextFormatter{Quiet: true} + +func TestConfigOssi_exclude_vulnerabilities(t *testing.T) { + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"CVE123", "CVE988"}}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--exclude-vulnerability=CVE123,CVE988"}...) +} + +const testdataDir = "../internal/configuration/testdata" + +func TestConfigOssi_exclude_vulnerabilities_with_sane_file(t *testing.T) { + file, _ := os.Open(testdataDir + "/normalIgnore") + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"CVF-000", "CVF-123", "CVF-9999"}}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--exclude-vulnerability-file=" + file.Name()}...) +} + +func TestConfigOssi_exclude_vulnerabilities_when_file_empty(t *testing.T) { + emptyFile, _ := os.Open(testdataDir + "/emptyFile") + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--exclude-vulnerability-file=" + emptyFile.Name()}...) +} + +func TestConfigOssi_exclude_vulnerabilities_when_has_tons_of_newlines(t *testing.T) { + lotsOfRandomNewlinesFile, _ := os.Open(testdataDir + "/lotsOfRandomWhitespace") + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"CVN-111", "CVN-123", "CVN-543"}}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--exclude-vulnerability-file=" + lotsOfRandomNewlinesFile.Name()}...) +} + +func TestConfigOssi_exclude_vulnerabilities_are_combined_with_file_and_args_values(t *testing.T) { + lotsOfRandomNewlinesFile, _ := os.Open(testdataDir + "/lotsOfRandomWhitespace") + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"CVE123", "CVE988", "CVN-111", "CVN-123", "CVN-543"}}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--exclude-vulnerability=CVE123,CVE988", "--exclude-vulnerability-file=" + lotsOfRandomNewlinesFile.Name()}...) +} + +func TestConfigOssi_exclude_vulnerabilities_file_not_found_does_not_matter(t *testing.T) { + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--exclude-vulnerability-file=/blah-blah-doesnt-exists"}...) +} + +func TestConfigOssi_exclude_vulnerabilities_passed_as_directory_does_not_matter(t *testing.T) { + dir, _ := ioutil.TempDir("", "prefix") + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--exclude-vulnerability-file=" + dir}...) +} + +func TestConfigOssi_exclude_vulnerabilities_does_not_need_to_be_passed_if_default_value_is_used(t *testing.T) { + defaultFileName := ".nancy-ignore" + err := ioutil.WriteFile(defaultFileName, []byte("DEF-111\nDEF-222"), 0644) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = os.Remove(defaultFileName) + }() + + // reset exclude file path, is changed by prior tests + origExcludeVulnerabilityFilePath := excludeVulnerabilityFilePath + defer func() { + excludeVulnerabilityFilePath = origExcludeVulnerabilityFilePath + }() + excludeVulnerabilityFilePath = defaultExcludeFilePath + + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"DEF-111", "DEF-222"}}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use}...) +} + +func TestConfigOssi_exclude_vulnerabilities_when_has_comments(t *testing.T) { + commentedFile, _ := os.Open(testdataDir + "/commented") + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"CVN-111", "CVN-123", "CVN-543"}}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--exclude-vulnerability-file=" + commentedFile.Name()}...) +} + +func TestConfigOssi_exclude_vulnerabilities_when_has_untils(t *testing.T) { + untilsFile, _ := os.Open(testdataDir + "/untilsAndComments") + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{Cves: []string{"NO-UNTIL-888", "MUST-BE-IGNORED-999", "MUST-BE-IGNORED-1999"}}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--exclude-vulnerability-file=" + untilsFile.Name()}...) +} + +func TestConfigOssi_exclude_vulnerabilities_when_has_invalid_value_in_untils(t *testing.T) { + invalidUntilsFile, _ := os.Open(testdataDir + "/untilsInvaild") + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--exclude-vulnerability-file=" + invalidUntilsFile.Name()}...) +} + +func TestConfigOssi_exclude_vulnerabilities_when_has_invalid_date_in_untils(t *testing.T) { + invalidDateUntilsFile, _ := os.Open(testdataDir + "/untilsBadDateFormat") + validateConfigOssi(t, types.Configuration{CveList: types.CveListFlag{}, Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--exclude-vulnerability-file=" + invalidDateUntilsFile.Name()}...) +} + +func TestConfigOssi_output_of_json(t *testing.T) { + validateConfigOssi(t, types.Configuration{Formatter: audit.JsonFormatter{}}, []string{sleuthCmd.Use, "--output=json"}...) +} + +func TestConfigOssi_output_of_json_pretty_print(t *testing.T) { + validateConfigOssi(t, types.Configuration{Formatter: audit.JsonFormatter{PrettyPrint: true}}, + []string{sleuthCmd.Use, "--output=json-pretty"}...) +} + +func TestConfigOssi_output_of_csv(t *testing.T) { + validateConfigOssi(t, types.Configuration{Formatter: audit.CsvFormatter{Quiet: true}}, + []string{sleuthCmd.Use, "--output=csv"}...) +} + +func TestConfigOssi_output_of_text(t *testing.T) { + validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--output=text"}...) +} + +func TestConfigOssi_output_of_bad_value(t *testing.T) { + validateConfigOssi(t, types.Configuration{Formatter: defaultAuditLogFormatter}, + []string{sleuthCmd.Use, "--output=aintgonnadoit"}...) +} diff --git a/go.mod b/go.mod index 5ed99900..45281129 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/sonatype-nexus-community/go-sona-types v0.0.6 github.com/spf13/afero v1.2.2 // indirect github.com/spf13/cobra v1.0.0 + github.com/spf13/pflag v1.0.3 github.com/spf13/viper v1.6.3 github.com/stretchr/testify v1.5.1 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect diff --git a/internal/customerrors/error.go b/internal/customerrors/error.go index 4bcc7607..5b4fea0e 100644 --- a/internal/customerrors/error.go +++ b/internal/customerrors/error.go @@ -35,7 +35,7 @@ func (es ErrorShowLogPath) Error() string { errString = "" } - return errString + "\n" + getLogFileMessage() + return errString + "\n\n" + getLogFileMessage() } type ErrorExit struct { diff --git a/internal/customerrors/error_test.go b/internal/customerrors/error_test.go index bb7e1f77..0c0c85e9 100644 --- a/internal/customerrors/error_test.go +++ b/internal/customerrors/error_test.go @@ -43,8 +43,8 @@ func TestNewErrorExitPrintHelp(t *testing.T) { } func TestErrorShowLogPath(t *testing.T) { - assert.Equal(t, "MyError\n"+getLogFileMessage(), + assert.Equal(t, "MyError\n\n"+getLogFileMessage(), ErrorShowLogPath{Err: fmt.Errorf("MyError")}.Error()) - assert.Equal(t, "\n"+getLogFileMessage(), + assert.Equal(t, "\n\n"+getLogFileMessage(), ErrorShowLogPath{}.Error()) } From 3baf8d122ba618c6fcf6e70fbc232a1caf7c4dc4 Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Fri, 14 Aug 2020 12:14:11 -0400 Subject: [PATCH 2/6] fix integration test to use new 'sleuth' command --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 99b59f13..2bbaa8e1 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,6 @@ test: build $(GOTEST) -v ./... 2>&1 integration-test: build - cd packages/testdata && GOPATH=. ../../$(BINARY_NAME) -p Gopkg.lock && cd - - go list -m all | ./$(BINARY_NAME) - go list -m all > deps.out && ./$(BINARY_NAME) < deps.out + cd packages/testdata && GOPATH=. ../../$(BINARY_NAME) sleuth -p Gopkg.lock && cd - + go list -m all | ./$(BINARY_NAME) sleuth + go list -m all > deps.out && ./$(BINARY_NAME) sleuth < deps.out From 1f14321e0cecc90a742049d2497b64ac250cd8f7 Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Mon, 17 Aug 2020 13:56:20 -0400 Subject: [PATCH 3/6] support --path flag (pointing to dep Gopkg.lock file) both ossi and iq --- cmd/iq.go | 49 +++++++++++++++++++++++++++++----------------- cmd/iq_test.go | 35 ++++++++++++++++++++++++++++++--- cmd/root.go | 48 +++++++++++++++++++++++++++++++-------------- cmd/root_test.go | 5 +++-- cmd/sleuth.go | 1 - cmd/sleuth_test.go | 12 ++++++++++-- 6 files changed, 109 insertions(+), 41 deletions(-) diff --git a/cmd/iq.go b/cmd/iq.go index 4abf31e7..c3126d1f 100644 --- a/cmd/iq.go +++ b/cmd/iq.go @@ -22,12 +22,11 @@ import ( "os" "path" - "github.com/sonatype-nexus-community/nancy/internal/configuration" - "github.com/sonatype-nexus-community/nancy/internal/logger" - "github.com/mitchellh/go-homedir" "github.com/sonatype-nexus-community/go-sona-types/iq" + "github.com/sonatype-nexus-community/nancy/internal/configuration" "github.com/sonatype-nexus-community/nancy/internal/customerrors" + "github.com/sonatype-nexus-community/nancy/internal/logger" "github.com/sonatype-nexus-community/nancy/packages" "github.com/sonatype-nexus-community/nancy/parse" "github.com/sonatype-nexus-community/nancy/types" @@ -103,20 +102,8 @@ func doIQ(cmd *cobra.Command, args []string) (err error) { printHeader(!configOssi.Quiet) - if err = checkStdIn(); err != nil { - logLady.WithError(err).Error("unexpected error in iq cmd") - panic(err) - } - - mod := packages.Mod{} - - mod.ProjectList, err = parse.GoListAgnostic(os.Stdin) - if err != nil { - logLady.WithError(err).Error("unexpected error in iq cmd") - panic(err) - } - - var purls = mod.ExtractPurlsFromManifest() + var purls []string + purls, err = getPurls() err = auditWithIQServer(purls, configIQ.IQApplication) if err != nil { @@ -131,6 +118,32 @@ func doIQ(cmd *cobra.Command, args []string) (err error) { return } +func getPurls() (purls []string, err error) { + if configOssi.Path != "" { + var invalidPurls []string + if purls, invalidPurls, err = getPurlsFromPath(configOssi.Path); err != nil { + panic(err) + } + invalidCoordinates := convertInvalidPurlsToCoordinates(invalidPurls) + logLady.WithField("invalid", invalidCoordinates).Info("") + } else { + if err = checkStdIn(); err != nil { + logLady.WithError(err).Error("unexpected error in iq cmd") + panic(err) + } + + mod := packages.Mod{} + + mod.ProjectList, err = parse.GoListAgnostic(os.Stdin) + if err != nil { + logLady.WithError(err).Error("unexpected error in iq cmd") + panic(err) + } + purls = mod.ExtractPurlsFromManifest() + } + return purls, err +} + const ( flagNameIqUsername = "iq-username" flagNameIqToken = "iq-token" @@ -143,7 +156,7 @@ func init() { cobra.OnInitialize(initIQConfig) iqCmd.Flags().StringVarP(&configIQ.IQUsername, flagNameIqUsername, "l", "admin", "Specify Nexus IQ username for request") - iqCmd.Flags().StringVarP(&configIQ.IQToken, flagNameIqToken, "p", "admin123", "Specify Nexus IQ token for request") + iqCmd.Flags().StringVarP(&configIQ.IQToken, flagNameIqToken, "k", "admin123", "Specify Nexus IQ token for request") iqCmd.Flags().StringVarP(&configIQ.IQStage, flagNameIqStage, "s", "develop", "Specify Nexus IQ stage for request") iqCmd.Flags().StringVarP(&configIQ.IQApplication, flagNameIqApplication, "a", "", "Specify Nexus IQ public application ID for request") diff --git a/cmd/iq_test.go b/cmd/iq_test.go index fdca8430..24f72d99 100644 --- a/cmd/iq_test.go +++ b/cmd/iq_test.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "os" "path" + "strings" "testing" "github.com/sirupsen/logrus" @@ -35,18 +36,46 @@ import ( ) func TestIqApplicationFlagMissing(t *testing.T) { - output, err := executeCommand(rootCmd, "iq") + output, err := executeCommand(rootCmd, iqCmd.Use) assert.Contains(t, output, "Error: required flag(s) \""+flagNameIqApplication+"\" not set") assert.NotNil(t, err) assert.Contains(t, err.Error(), "required flag(s) \""+flagNameIqApplication+"\" not set") } func TestIqHelp(t *testing.T) { - output, err := executeCommand(rootCmd, "iq", "--help") + output, err := executeCommand(rootCmd, iqCmd.Use, "--help") assert.Contains(t, output, "go list -json -m all | nancy iq --"+flagNameIqApplication+" your_public_application_id --"+flagNameIqServerUrl+" ") assert.Nil(t, err) } +func TestIqCommandPathInvalidName(t *testing.T) { + origConfig := configOssi + defer func() { + configOssi = origConfig + }() + // not sure why calling executeCommand() fails as part of test suite, but is fine individually. + //_, err := executeCommand(rootCmd, iqCmd.Use, "--path", "invalidPath", "-a", "appId") + configOssi = types.Configuration{Path: "invalidPath"} + err := doIQ(iqCmd, []string{}) + + assert.Error(t, err) + assert.Contains(t, err.Error(), fmt.Sprintf("invalid path value. must point to '%s' file. path: ", GopkgLockFilename)) +} + +func TestIqCommandPathInvalidFile(t *testing.T) { + origConfig := configOssi + defer func() { + configOssi = origConfig + }() + // not sure why calling executeCommand() fails as part of test suite, but is fine individually. + //_, err := executeCommand(rootCmd, iqCmd.Use, "--path", GopkgLockFilename, "-a", "appId") + configOssi = types.Configuration{Path: GopkgLockFilename} + err := doIQ(iqCmd, []string{}) + + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "could not find project"), err.Error()) +} + func setupIQConfigFile(t *testing.T, tempDir string) { cfgDirIQ := path.Join(tempDir, types.IQServerDirName) assert.Nil(t, os.Mkdir(cfgDirIQ, 0700)) @@ -253,7 +282,7 @@ func TestDoIqWithIqServerError(t *testing.T) { typedErrorCause, ok := typedError.Err.(*iq.ServerError) assert.True(t, ok) - assert.Contains(t, typedErrorCause.Error(), "There was an error communicating with Nexus IQ Server to get your internal application ID") + assert.Contains(t, typedErrorCause.Error(), "There was an error communicating with Nexus IQ Server to get your internal application ID", typedErrorCause.Error()) } func TestDoIqHappyPath(t *testing.T) { diff --git a/cmd/root.go b/cmd/root.go index f18a1953..cd2c097b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -132,6 +132,8 @@ const defaultExcludeFilePath = "./.nancy-ignore" const ( flagNameOssiUsername = "username" flagNameOssiToken = "token" + + GopkgLockFilename = "Gopkg.lock" ) func init() { @@ -144,6 +146,7 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&configOssi.CleanCache, "clean-cache", "c", false, "Deletes local cache directory") rootCmd.PersistentFlags().StringVarP(&configOssi.Username, flagNameOssiUsername, "u", "", "Specify OSS Index username for request") rootCmd.PersistentFlags().StringVarP(&configOssi.Token, flagNameOssiToken, "t", "", "Specify OSS Index API token for request") + rootCmd.PersistentFlags().StringVarP(&configOssi.Path, "path", "p", "", "Specify a path to a dep "+GopkgLockFilename+" file for scanning") } func bindViper(cmd *cobra.Command) { @@ -257,12 +260,6 @@ func processConfig() (err error) { */ if configOssi.Path != "" { - logLady.Info("Parsing config for file based scan") - if !strings.Contains(configOssi.Path, "Gopkg.lock") { - err = fmt.Errorf("invalid path value. must point to 'Gopkg.lock' file. path: %s", configOssi.Path) - logLady.WithField("error", err).Error("Path error in file based scan") - return - } if err = doDepAndParse(ossIndex, configOssi.Path); err != nil { logLady.WithField("error", err).Error("Error in file based scan") return @@ -281,7 +278,14 @@ func getIsQuiet() bool { return !configOssi.Loud } -func doDepAndParse(ossIndex ossindex.IServer, path string) (err error) { +func getPurlsFromPath(path string) (purls []string, invalidPurls []string, err error) { + logLady.Info("Parsing config for file based scan") + if !strings.Contains(path, GopkgLockFilename) { + err = fmt.Errorf("invalid path value. must point to '%s' file. path: %s", GopkgLockFilename, path) + logLady.WithField("error", err).Error("Path error in file based scan") + return + } + workingDir := filepath.Dir(path) if workingDir == "." { workingDir, _ = os.Getwd() @@ -291,7 +295,9 @@ func doDepAndParse(ossIndex ossindex.IServer, path string) (err error) { WorkingDir: workingDir, GOPATHs: []string{getenv}, } - project, err := ctx.LoadProject() + + var project *dep.Project + project, err = ctx.LoadProject() if err != nil { return } @@ -301,10 +307,17 @@ func doDepAndParse(ossIndex ossindex.IServer, path string) (err error) { return } - purls, invalidPurls := packages.ExtractPurlsUsingDep(project) + purls, invalidPurls = packages.ExtractPurlsUsingDep(project) + return +} - err = checkOSSIndex(ossIndex, purls, invalidPurls) - if err != nil { +func doDepAndParse(ossIndex ossindex.IServer, path string) (err error) { + var purls, invalidPurls []string + if purls, invalidPurls, err = getPurlsFromPath(path); err != nil { + return + } + + if err = checkOSSIndex(ossIndex, purls, invalidPurls); err != nil { return } @@ -414,10 +427,7 @@ func checkOSSIndex(ossIndex ossindex.IServer, purls []string, invalidpurls []str return } - var invalidCoordinates []ossIndexTypes.Coordinate - for _, invalidpurl := range invalidpurls { - invalidCoordinates = append(invalidCoordinates, ossIndexTypes.Coordinate{Coordinates: invalidpurl, InvalidSemVer: true}) - } + invalidCoordinates := convertInvalidPurlsToCoordinates(invalidpurls) if count := audit.LogResults(configOssi.Formatter, packageCount, coordinates, invalidCoordinates, configOssi.CveList.Cves); count > 0 { err = customerrors.ErrorExit{ExitCode: count} @@ -426,6 +436,14 @@ func checkOSSIndex(ossIndex ossindex.IServer, purls []string, invalidpurls []str return } +func convertInvalidPurlsToCoordinates(invalidPurls []string) []ossIndexTypes.Coordinate { + var invalidCoordinates []ossIndexTypes.Coordinate + for _, invalidPurl := range invalidPurls { + invalidCoordinates = append(invalidCoordinates, ossIndexTypes.Coordinate{Coordinates: invalidPurl, InvalidSemVer: true}) + } + return invalidCoordinates +} + func checkStdIn() (err error) { stat, _ := os.Stdin.Stat() if (stat.Mode() & os.ModeCharDevice) == 0 { diff --git a/cmd/root_test.go b/cmd/root_test.go index 4a4cfd44..81ae38af 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -103,7 +103,7 @@ func TestProcessConfigPath(t *testing.T) { defer func() { configOssi = origConfig }() - configOssi = types.Configuration{Path: "../packages/testdata/Gopkg.lock"} + configOssi = types.Configuration{Path: "../packages/testdata/" + GopkgLockFilename} logLady, _ = test.NewNullLogger() configOssi.Formatter = &logrus.TextFormatter{} @@ -207,7 +207,8 @@ func validateFormatterVolume(t *testing.T, testConfig types.Configuration, expec } func TestDoDepAndParseInvalidPath(t *testing.T) { - err := doDepAndParse(ossiFactoryMock{}.create(), "bogusPath") + logLady, _ = test.NewNullLogger() + err := doDepAndParse(ossiFactoryMock{}.create(), GopkgLockFilename) assert.Error(t, err) assert.True(t, strings.Contains(err.Error(), "could not find project")) } diff --git a/cmd/sleuth.go b/cmd/sleuth.go index c7a0b361..ce212d2c 100644 --- a/cmd/sleuth.go +++ b/cmd/sleuth.go @@ -13,7 +13,6 @@ func init() { sleuthCmd.Flags().BoolVarP(&configOssi.NoColor, "no-color", "n", false, "indicate output should not be colorized") sleuthCmd.Flags().VarP(&configOssi.CveList, "exclude-vulnerability", "e", "Comma separated list of CVEs to exclude") - sleuthCmd.Flags().StringVarP(&configOssi.Path, "path", "p", "", "Specify a path to a dep Gopkg.lock file for scanning") sleuthCmd.Flags().StringVarP(&excludeVulnerabilityFilePath, "exclude-vulnerability-file", "x", defaultExcludeFilePath, "Path to a file containing newline separated CVEs to be excluded") sleuthCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Styling for output format. json, json-pretty, text, csv") } diff --git a/cmd/sleuth_test.go b/cmd/sleuth_test.go index ec8c6682..a3e4f444 100644 --- a/cmd/sleuth_test.go +++ b/cmd/sleuth_test.go @@ -1,12 +1,14 @@ package cmd import ( + "fmt" "github.com/sonatype-nexus-community/nancy/internal/audit" "github.com/sonatype-nexus-community/nancy/internal/customerrors" "github.com/sonatype-nexus-community/nancy/types" "github.com/stretchr/testify/assert" "io/ioutil" "os" + "strings" "testing" ) @@ -16,10 +18,16 @@ func TestSleuthCommandNoArgs(t *testing.T) { assert.Equal(t, customerrors.ErrorShowLogPath{Err: stdInInvalid}, err) } -func TestSleuthCommandInvalidPath(t *testing.T) { +func TestSleuthCommandPathInvalidName(t *testing.T) { _, err := executeCommand(rootCmd, sleuthCmd.Use, "--path", "invalidPath") assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid path value. must point to 'Gopkg.lock' file. path: ") + assert.Contains(t, err.Error(), fmt.Sprintf("invalid path value. must point to '%s' file. path: ", GopkgLockFilename)) +} + +func TestSleuthCommandPathInvalidFile(t *testing.T) { + _, err := executeCommand(rootCmd, sleuthCmd.Use, "--path", GopkgLockFilename) + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), "could not find project")) } func TestConfigOssi_no_color(t *testing.T) { From 10f787bff53c41ca2f9a263205fc3b6b13e875aa Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Mon, 17 Aug 2020 14:55:59 -0400 Subject: [PATCH 4/6] update docs for `sleuth` command. --- README.md | 168 ++++++++++++++++++++++++++++---------------------- cmd/root.go | 6 +- cmd/sleuth.go | 2 +- 3 files changed, 99 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 8d115316..ca954f41 100644 --- a/README.md +++ b/README.md @@ -34,75 +34,99 @@ ``` ~ > nancy --help - nancy is a tool to check for vulnerabilities in your Golang dependencies, - powered by the 'Sonatype OSS Index', and as well, works with Nexus IQ Server, allowing you - a smooth experience as a Golang developer, using the best tools in the market! - - Usage: - nancy [flags] - nancy [command] - - Examples: - Typical usage will pipe the output of 'go list -json -m all' to 'nancy': - go list -json -m all | nancy [flags] - go list -json -m all | nancy iq [flags] - go list -m all | nancy [flags] - go list -m all | nancy iq [flags] - - Available Commands: - config Setup credentials to use when connecting to services - help Help about any command - iq Check for vulnerabilities in your Golang dependencies using 'Sonatype's Nexus IQ IQServer' - - Flags: - -v, -- count Set log level, multiple v's is more verbose - -c, --clean-cache Deletes local cache directory - -e, --exclude-vulnerability CveListFlag Comma separated list of CVEs to exclude (default []) - -x, --exclude-vulnerability-file string Path to a file containing newline separated CVEs to be excluded (default "./.nancy-ignore") - -h, --help help for nancy - --loud indicate output should include non-vulnerable packages - -n, --no-color indicate output should not be colorized - -o, --output string Styling for output format. json, json-pretty, text, csv (default "text") - -p, --path string Specify a path to a dep Gopkg.lock file for scanning - -q, --quiet indicate output should contain only packages with vulnerabilities (default true) - -t, --token string Specify OSS Index API token for request - -u, --username string Specify OSS Index username for request - -V, --version Get the version - - Use "nancy [command] --help" for more information about a command. +nancy is a tool to check for vulnerabilities in your Golang dependencies, +powered by the 'Sonatype OSS Index', and as well, works with Nexus IQ Server, allowing you +a smooth experience as a Golang developer, using the best tools in the market! + +Usage: + nancy [flags] + nancy [command] + +Examples: + Typical usage will pipe the output of 'go list -json -m all' to 'nancy': + go list -json -m all | nancy sleuth [flags] + go list -json -m all | nancy iq [flags] + +Available Commands: + config Setup credentials to use when connecting to services + help Help about any command + iq Check for vulnerabilities in your Golang dependencies using 'Sonatype's Nexus IQ IQServer' + sleuth Check for vulnerabilities in your Golang dependencies using Sonatype's OSS Index + +Flags: + -v, -- count Set log level, multiple v's is more verbose + -c, --clean-cache Deletes local cache directory + -h, --help help for nancy + --loud indicate output should include non-vulnerable packages + -p, --path string Specify a path to a dep Gopkg.lock file for scanning + -q, --quiet indicate output should contain only packages with vulnerabilities (default true) + -t, --token string Specify OSS Index API token for request + -u, --username string Specify OSS Index username for request + -V, --version Get the version + +Use "nancy [command] --help" for more information about a command. + + +$ > nancy sleuth --help +'nancy sleuth' is a command to check for vulnerabilities in your Golang dependencies, powered by the 'Sonatype OSS Index'. + +Usage: + nancy sleuth [flags] + +Examples: + go list -json -m all | nancy sleuth --username your_user --token your_token + +Flags: + -e, --exclude-vulnerability CveListFlag Comma separated list of CVEs to exclude (default []) + -x, --exclude-vulnerability-file string Path to a file containing newline separated CVEs to be excluded (default "./.nancy-ignore") + -h, --help help for sleuth + -n, --no-color indicate output should not be colorized + -o, --output string Styling for output format. json, json-pretty, text, csv (default "text") + +Global Flags: + -v, -- count Set log level, multiple v's is more verbose + -c, --clean-cache Deletes local cache directory + --loud indicate output should include non-vulnerable packages + -p, --path string Specify a path to a dep Gopkg.lock file for scanning + -q, --quiet indicate output should contain only packages with vulnerabilities (default true) + -t, --token string Specify OSS Index API token for request + -u, --username string Specify OSS Index username for request + -V, --version Get the version $ > nancy iq --help - 'nancy iq' is a command to check for vulnerabilities in your Golang dependencies, powered by 'Sonatype's Nexus IQ IQServer', allowing you a smooth experience as a Golang developer, using the best tools in the market! - - Usage: - nancy iq [flags] - - Examples: - go list -json -m all | nancy iq --iq-application your_public_application_id --iq-server-url http://your_iq_server_url:port --iq-username your_user --iq-token your_token --iq-stage develop - - Flags: - -h, --help help for iq - -a, --iq-application string Specify Nexus IQ public application ID for request - -x, --iq-server-url string Specify Nexus IQ server url for request (default "http://localhost:8070") - -s, --iq-stage string Specify Nexus IQ stage for request (default "develop") - -p, --iq-token string Specify Nexus IQ token for request (default "admin123") - -l, --iq-username string Specify Nexus IQ username for request (default "admin") - - Global Flags: - -v, -- count Set log level, multiple v's is more verbose - --loud indicate output should include non-vulnerable packages - -q, --quiet indicate output should contain only packages with vulnerabilities (default true) - -t, --token string Specify OSS Index API token for request - -u, --username string Specify OSS Index username for request - -V, --version Get the version +'nancy iq' is a command to check for vulnerabilities in your Golang dependencies, powered by 'Sonatype's Nexus IQ IQServer', allowing you a smooth experience as a Golang developer, using the best tools in the market! + +Usage: + nancy iq [flags] + +Examples: + go list -json -m all | nancy iq --iq-application your_public_application_id --iq-server-url http://your_iq_server_url:port --iq-username your_user --iq-token your_token --iq-stage develop + +Flags: + -h, --help help for iq + -a, --iq-application string Specify Nexus IQ public application ID for request + -x, --iq-server-url string Specify Nexus IQ server url for request (default "http://localhost:8070") + -s, --iq-stage string Specify Nexus IQ stage for request (default "develop") + -k, --iq-token string Specify Nexus IQ token for request (default "admin123") + -l, --iq-username string Specify Nexus IQ username for request (default "admin") + +Global Flags: + -v, -- count Set log level, multiple v's is more verbose + -c, --clean-cache Deletes local cache directory + --loud indicate output should include non-vulnerable packages + -p, --path string Specify a path to a dep Gopkg.lock file for scanning + -q, --quiet indicate output should contain only packages with vulnerabilities (default true) + -t, --token string Specify OSS Index API token for request + -u, --username string Specify OSS Index username for request + -V, --version Get the version ``` #### What is the best usage of Nancy? The preferred way to use Nancy is: -- `go list -json -m all | nancy` -- `nancy -p /path/to/Gopkg.lock` +- `go list -json -m all | nancy sleuth` +- `nancy sleuth -p /path/to/Gopkg.lock` #### Homebrew usage @@ -140,7 +164,7 @@ We publish a few different flavors for convenience: If you start using Nancy extensively, you might run into Rate Limiting from OSS Index! Don't worry, we've got your back! -If you run into Rate Limiting you should recieve an error that will give you instructions on how to register on OSS Index: +If you run into Rate Limiting you should receive an error that will give you instructions on how to register on OSS Index: ``` You have been rate limited by OSS Index. @@ -154,7 +178,7 @@ After setting this config, you'll be gifted a nice new higher rate limit. If you You can also set the user and token via the command line like so: -`nancy --username auser@anemailaddress.com --token A4@k3@p1T0k3n` +`nancy sleuth --username auser@anemailaddress.com --token A4@k3@p1T0k3n` This can be handy for testing your account out, or if you want to override your set config with a different user. @@ -163,8 +187,8 @@ This can be handy for testing your account out, or if you want to override your By default, `nancy` runs in a "quiet" mode, only displaying a list of vulnerable components. You can run `nancy` in a loud manner, showing all components by running: -* `./nancy --loud -p /path/to/your/Gopkg.lock` -* `go list -json -m all | ./nancy --loud` +* `./nancy sleuth --loud -p /path/to/your/Gopkg.lock` +* `go list -json -m all | ./nancy sleuth --loud` #### Exclude vulnerabilities @@ -176,15 +200,15 @@ Vulnerabilities excluded will then be silenced and not show up in the output or We support exclusion of vulnerability either by CVE-ID (ex: `CVE-2018-20303`) or via the OSS Index ID (ex: `a8c20c84-1f6a-472a-ba1b-3eaedb2a2a14`) as not all vulnerabilities have a CVE-ID. ##### Via CLI flag -* `./nancy --exclude-vulnerability CVE-789,bcb0c38d-0d35-44ee-b7a7-8f77183d1ae2 -p /path/to/your/Gopkg.lock` -* `go list -json -m all | ./nancy --exclude-vulnerability CVE-789,bcb0c38d-0d35-44ee-b7a7-8f77183d1ae2` +* `./nancy sleuth --exclude-vulnerability CVE-789,bcb0c38d-0d35-44ee-b7a7-8f77183d1ae2 -p /path/to/your/Gopkg.lock` +* `go list -json -m all | ./nancy sleuth --exclude-vulnerability CVE-789,bcb0c38d-0d35-44ee-b7a7-8f77183d1ae2` ##### Via file By default if a file named `.nancy-ignore` exists in the same directory that nancy is run it will use it, will no other options need to be passed. If you would like to define the path to the file you can use the following -* `./nancy --exclude-vulnerability-file=/path/to/your/exclude-file -p /path/to/your/Gopkg.lock` -* `go list -json -m all | ./nancy --exclude-vulnerability-file=/path/to/your/exclude-file` +* `./nancy sleuth --exclude-vulnerability-file=/path/to/your/exclude-file -p /path/to/your/Gopkg.lock` +* `go list -json -m all | ./nancy sleuth --exclude-vulnerability-file=/path/to/your/exclude-file` The file format requires each vulnerability that you want to exclude to be on a separate line. Comments are allowed in the file as well to help provide context when needed. See an example file below. @@ -388,7 +412,7 @@ Options for stage are as follows: `build, develop, stage-release, release` -By default `-stage` will be `develop`. +By default `--iq-stage` will be `develop`. Successful submissions to Nexus IQ Server will result in either an OS exit of 0, meaning all is clear and a response akin to: @@ -414,7 +438,7 @@ Uh oh! There was an error with your request to Nexus IQ Server: #### Persistent Nexus IQ Server Config -Nancy let's you set the Nexus IQ Server Address, User and Token as persistent config (application and stage are generally per project so we do not let you set these globally). +Nancy lets you set the Nexus IQ Server Address, User and Token as persistent config (application and stage are generally per project so we do not let you set these globally). To set your Nexus IQ Server config run: @@ -441,7 +465,7 @@ This project is called `nancy` as like the great detective herself, it looks for ## Installation -At current time you have a few options: +At the current time you have a few options: * Build from source * Download release binary from [here on GitHub](https://github.com/sonatype-nexus-community/nancy/releases) diff --git a/cmd/root.go b/cmd/root.go index cd2c097b..42c08af0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -104,10 +104,8 @@ var rootCmd = &cobra.Command{ Version: buildversion.BuildVersion, Use: "nancy", Example: ` Typical usage will pipe the output of 'go list -json -m all' to 'nancy': - go list -json -m all | nancy [flags] - go list -json -m all | nancy iq [flags] - go list -m all | nancy [flags] - go list -m all | nancy iq [flags]`, + go list -json -m all | nancy sleuth [flags] + go list -json -m all | nancy iq [flags]`, Short: "Check for vulnerabilities in your Golang dependencies using Sonatype's OSS Index", Long: `nancy is a tool to check for vulnerabilities in your Golang dependencies, powered by the 'Sonatype OSS Index', and as well, works with Nexus IQ Server, allowing you diff --git a/cmd/sleuth.go b/cmd/sleuth.go index ce212d2c..40ac8723 100644 --- a/cmd/sleuth.go +++ b/cmd/sleuth.go @@ -21,7 +21,7 @@ var sleuthCmd = &cobra.Command{ Use: "sleuth", Example: ` go list -json -m all | nancy sleuth --` + flagNameOssiUsername + ` your_user --` + flagNameOssiToken + ` your_token`, Short: "Check for vulnerabilities in your Golang dependencies using Sonatype's OSS Index", - Long: `'nancy iq' is a command to check for vulnerabilities in your Golang dependencies, powered by 'Sonatype's Nexus IQ IQServer', allowing you a smooth experience as a Golang developer, using the best tools in the market!`, + Long: `'nancy sleuth' is a command to check for vulnerabilities in your Golang dependencies, powered by the 'Sonatype OSS Index'.`, PreRun: func(cmd *cobra.Command, args []string) { bindViper(rootCmd) }, RunE: doOSSI, } From cbcbbe3c19ab9600abf386ca9bfa11ca2554a710 Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Tue, 18 Aug 2020 11:43:51 -0400 Subject: [PATCH 5/6] add TODO comments --- cmd/iq_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/iq_test.go b/cmd/iq_test.go index 24f72d99..62bc49f2 100644 --- a/cmd/iq_test.go +++ b/cmd/iq_test.go @@ -53,7 +53,7 @@ func TestIqCommandPathInvalidName(t *testing.T) { defer func() { configOssi = origConfig }() - // not sure why calling executeCommand() fails as part of test suite, but is fine individually. + // TODO debug side effects. calling executeCommand() fails as part of test suite, but is fine when run individually. //_, err := executeCommand(rootCmd, iqCmd.Use, "--path", "invalidPath", "-a", "appId") configOssi = types.Configuration{Path: "invalidPath"} err := doIQ(iqCmd, []string{}) @@ -67,7 +67,7 @@ func TestIqCommandPathInvalidFile(t *testing.T) { defer func() { configOssi = origConfig }() - // not sure why calling executeCommand() fails as part of test suite, but is fine individually. + // TODO debug side effects. calling executeCommand() fails as part of test suite, but is fine when run individually. //_, err := executeCommand(rootCmd, iqCmd.Use, "--path", GopkgLockFilename, "-a", "appId") configOssi = types.Configuration{Path: GopkgLockFilename} err := doIQ(iqCmd, []string{}) From abc47d17185802a016252ed38f595229a9881897 Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Tue, 18 Aug 2020 16:56:49 -0400 Subject: [PATCH 6/6] fix vulnerability: CVE-2020-15114 in etcd v3.3.13+incompatible --- Makefile | 2 ++ go.mod | 3 +++ go.sum | 1 + 3 files changed, 6 insertions(+) diff --git a/Makefile b/Makefile index 2bbaa8e1..2e508a94 100644 --- a/Makefile +++ b/Makefile @@ -44,5 +44,7 @@ test: build integration-test: build cd packages/testdata && GOPATH=. ../../$(BINARY_NAME) sleuth -p Gopkg.lock && cd - + go list -json -m all | ./$(BINARY_NAME) sleuth go list -m all | ./$(BINARY_NAME) sleuth + go list -json -m all > deps.out && ./$(BINARY_NAME) sleuth < deps.out go list -m all > deps.out && ./$(BINARY_NAME) sleuth < deps.out diff --git a/go.mod b/go.mod index 45281129..b884f086 100644 --- a/go.mod +++ b/go.mod @@ -37,4 +37,7 @@ replace golang.org/x/crypto => golang.org/x/crypto v0.0.0-20200604202706-70a84ac // fix vulnerability: CVE-2020-14040 in v0.3.0 replace golang.org/x/text => golang.org/x/text v0.3.3 +// fix vulnerability: CVE-2020-15114 in etcd v3.3.13+incompatible +replace github.com/coreos/etcd => github.com/coreos/etcd v3.3.24+incompatible + go 1.13 diff --git a/go.sum b/go.sum index 12f375bd..438073c9 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,7 @@ github.com/common-nighthawk/go-figure v0.0.0-20190529165535-67e0ed34491a/go.mod github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.24+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=