From ea2d4a782a426a38a13a22d59443d8a0634d962f Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Fri, 17 Apr 2020 19:41:07 -0400 Subject: [PATCH 01/13] first pass at removing os.Exit calls. WIP, more todo. --- configuration/parse.go | 10 +- configuration/parse_test.go | 16 +++ configuration/set.go | 3 +- customerrors/error.go | 13 ++- main.go | 135 ++++++++++++++++------- main_test.go | 183 +++++++++++++++++++++++++++++++ ossindex/internal/cache/cache.go | 1 + 7 files changed, 315 insertions(+), 46 deletions(-) diff --git a/configuration/parse.go b/configuration/parse.go index 53cb5f1a..418792ae 100644 --- a/configuration/parse.go +++ b/configuration/parse.go @@ -70,7 +70,7 @@ var unixComments = regexp.MustCompile(`#.*$`) var untilComment = regexp.MustCompile(`(until=)(.*)`) func ParseIQ(args []string) (config IqConfiguration, err error) { - iqCommand := flag.NewFlagSet("iq", flag.ExitOnError) + iqCommand := flag.NewFlagSet("iq", flag.ContinueOnError) iqCommand.BoolVar(&config.Info, "v", false, "Set log level to Info") iqCommand.BoolVar(&config.Debug, "vv", false, "Set log level to Debug") iqCommand.BoolVar(&config.Trace, "vvv", false, "Set log level to Trace") @@ -88,7 +88,8 @@ func ParseIQ(args []string) (config IqConfiguration, err error) { Options: `) iqCommand.PrintDefaults() - os.Exit(2) + //os.Exit(2) + // Avoid exit calls, but caller should use exit code 2 when handling } ConfigLocation = filepath.Join(HomeDir, types.IQServerDirName, types.IQServerConfigFileName) @@ -103,7 +104,7 @@ Options: return config, err } - return config, nil + return config, err } func loadConfigFromFile(configLocation string, config *Configuration) error { @@ -173,7 +174,8 @@ func Parse(args []string) (Configuration, error) { Options: `) flag.PrintDefaults() - os.Exit(2) + // os.Exit(2) + // Avoid exit calls, but caller should use exit code 2 when handling } ConfigLocation = filepath.Join(HomeDir, types.OssIndexDirName, types.OssIndexConfigFileName) diff --git a/configuration/parse_test.go b/configuration/parse_test.go index f5da9d04..00118494 100644 --- a/configuration/parse_test.go +++ b/configuration/parse_test.go @@ -147,3 +147,19 @@ func setup() { HomeDir = "/doesnt/exist" flag.CommandLine = flag.NewFlagSet("", flag.ContinueOnError) } + +func TestParseUsage(t *testing.T) { + setup() + _, err := Parse([]string{""}) + assert.NoError(t, err) + // should NOT call os.Exit + flag.Usage() +} + +func TestParseIQUsage(t *testing.T) { + setup() + _, err := ParseIQ([]string{""}) + assert.NoError(t, err) + // should NOT call os.Exit + flag.Usage() +} diff --git a/configuration/set.go b/configuration/set.go index 0d285d64..624ee339 100644 --- a/configuration/set.go +++ b/configuration/set.go @@ -72,9 +72,10 @@ func GetConfigFromCommandLine(stdin io.Reader) (err error) { ConfigLocation = filepath.Join(HomeDir, types.OssIndexDirName, types.OssIndexConfigFileName) err = getAndSetOSSIndexConfig(reader) case "": + // TODO this should probably return an error, because it means config setup was not completed return default: - LogLady.Info("User chose to set OSS Index config, moving forward") + LogLady.Infof("User chose invalid config type: %s, recurse madness", str) fmt.Println("Invalid value, 'iq' and 'ossindex' are accepted values, try again!") err = GetConfigFromCommandLine(stdin) } diff --git a/customerrors/error.go b/customerrors/error.go index a58d472e..bbd09b19 100644 --- a/customerrors/error.go +++ b/customerrors/error.go @@ -37,7 +37,7 @@ func (sw SwError) Exit() { os.Exit(3) } -func Check(err error, message string) { +func Check(err error, message string) error { if err != nil { location, _ := LogFileLocation() myErr := SwError{Message: message, Err: err} @@ -47,4 +47,15 @@ func Check(err error, message string) { fmt.Println("nancy version:", buildversion.BuildVersion) myErr.Exit() } + return err +} + +type ErrorExit struct { + Message string + Err error + ExitCode int +} + +func (sw ErrorExit) Error() string { + return fmt.Sprintf("exit code: %d - %s - error: %s", sw.ExitCode, sw.Message, sw.Err.Error()) } diff --git a/main.go b/main.go index ef18553e..2c0cc06c 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ import ( "errors" "flag" "fmt" + "io" "os" "path/filepath" "reflect" @@ -31,7 +32,7 @@ import ( . "github.com/sonatype-nexus-community/nancy/logger" "github.com/sonatype-nexus-community/nancy/types" - figure "github.com/common-nighthawk/go-figure" + "github.com/common-nighthawk/go-figure" "github.com/golang/dep" "github.com/sonatype-nexus-community/nancy/audit" "github.com/sonatype-nexus-community/nancy/buildversion" @@ -46,32 +47,66 @@ import ( func main() { LogLady.Info("Starting Nancy") + var err error if len(os.Args) > 1 && os.Args[1] == "iq" { - LogLady.Info("Nancy parsing config for IQ") - config, err := configuration.ParseIQ(os.Args[2:]) - if err != nil { - flag.Usage() - os.Exit(1) - } - LogLady.WithField("config", config).Info("Obtained IQ config") - processIQConfig(config) - LogLady.Info("Nancy finished parsing config for IQ") + err = doIq(os.Args[2:]) } else if len(os.Args) > 1 && os.Args[1] == "config" { - LogLady.Info("Nancy setting config via the command line") - err := configuration.GetConfigFromCommandLine(os.Stdin) - customerrors.Check(err, "Unable to set config for Nancy") - - os.Exit(0) + err = doConfig(os.Stdin) } else { - LogLady.Info("Nancy parsing config for OSS Index") - ossIndexConfig, err := configuration.Parse(os.Args[1:]) - if err != nil { - flag.Usage() - os.Exit(1) + err = doOssi(os.Args[1:]) + } + if err != nil { + if exiterr, ok := err.(*customerrors.ErrorExit); ok { + os.Exit(exiterr.ExitCode) + } else { + // really don't expect this + LogLady.WithError(err).Error("unexpected error in main") + os.Exit(4) } - processConfig(ossIndexConfig) - LogLady.Info("Nancy finished parsing config for OSS Index") } + fmt.Println("byeeeeee") // probably shouldn't get here until other exit cases above are all covered. +} + +func doOssi(ossiArgs []string) (err error) { + LogLady.Info("Nancy parsing config for OSS Index") + ossIndexConfig, err := configuration.Parse(ossiArgs) + if err != nil { + flag.Usage() + err = customerrors.ErrorExit{Err: err, ExitCode: 1} + return + } + if err = processConfig(ossIndexConfig); err != nil { + return + } + LogLady.Info("Nancy finished parsing config for OSS Index") + return +} + +func doConfig(stdin io.Reader) (err error) { + LogLady.Info("Nancy setting config via the command line") + if err = configuration.GetConfigFromCommandLine(stdin); err != nil { + return + } + if err = customerrors.Check(err, "Unable to set config for Nancy"); err != nil { + return + } + return +} + +func doIq(iqArgs []string) (err error) { + LogLady.Info("Nancy parsing config for IQ") + config, err := configuration.ParseIQ(iqArgs) + if err != nil { + flag.Usage() + err = customerrors.ErrorExit{Err: err, ExitCode: 2} // flag.Usage used to exit with code 2 + return + } + LogLady.WithField("config", config).Info("Obtained IQ config") + if err = processIQConfig(config); err != nil { + return + } + LogLady.Info("Nancy finished parsing config for IQ") + return } func printHeader(print bool) { @@ -92,11 +127,12 @@ func printHeader(print bool) { } } -func processConfig(config configuration.Configuration) { +func processConfig(config configuration.Configuration) (err error) { if config.Help { LogLady.Info("Printing usage and exiting clean") flag.Usage() - os.Exit(0) + err = customerrors.ErrorExit{ExitCode: 0} + return } if config.Version { @@ -111,7 +147,8 @@ func processConfig(config configuration.Configuration) { fmt.Println(buildversion.BuildVersion) _, _ = fmt.Printf("build time: %s\n", buildversion.BuildTime) _, _ = fmt.Printf("build commit: %s\n", buildversion.BuildCommit) - os.Exit(0) + err = customerrors.ErrorExit{ExitCode: 0} + return } if config.Info { @@ -126,33 +163,38 @@ func processConfig(config configuration.Configuration) { if config.CleanCache { LogLady.Info("Attempting to clean cache") - if err := ossindex.RemoveCacheDirectory(); err != nil { + if err = ossindex.RemoveCacheDirectory(); err != nil { LogLady.WithField("error", err).Error("Error cleaning cache") fmt.Printf("ERROR: cleaning cache: %v\n", err) - os.Exit(1) + err = customerrors.ErrorExit{Err: err, ExitCode: 1} + return } LogLady.Info("Cache cleaned") return } - printHeader((!config.Quiet && reflect.TypeOf(config.Formatter).String() == "*audit.AuditLogTextFormatter")) + printHeader(!config.Quiet && reflect.TypeOf(config.Formatter).String() == "*audit.AuditLogTextFormatter") if config.UseStdIn { LogLady.Info("Parsing config for StdIn") - doStdInAndParse(config) + if err = doStdInAndParse(config); err != nil { + return + } } if !config.UseStdIn { LogLady.Info("Parsing config for file based scan") doCheckExistenceAndParse(config) } + return } -func processIQConfig(config configuration.IqConfiguration) { +func processIQConfig(config configuration.IqConfiguration) (err error) { // TODO: a lot of this code is a duplication of the OSS Index config, probably should extract some of it if config.Help { LogLady.Info("Printing usage and exiting clean") flag.Usage() - os.Exit(0) + err = customerrors.ErrorExit{ExitCode: 0} + return } if config.Version { @@ -165,7 +207,8 @@ func processIQConfig(config configuration.IqConfiguration) { fmt.Println(buildversion.BuildVersion) _, _ = fmt.Printf("build time: %s\n", buildversion.BuildTime) _, _ = fmt.Printf("build commit: %s\n", buildversion.BuildCommit) - os.Exit(0) + err = customerrors.ErrorExit{ExitCode: 0} + return } if config.Info { @@ -181,18 +224,24 @@ func processIQConfig(config configuration.IqConfiguration) { if config.Application == "" { LogLady.Info("No application specified, printing usage and exiting clean") flag.Usage() - os.Exit(0) + err = customerrors.ErrorExit{ExitCode: 2} + return } printHeader(true) LogLady.Info("Parsing IQ config for StdIn") - doStdInAndParseForIQ(config) + if err = doStdInAndParseForIQ(config); err != nil { + return + } + return } -func doStdInAndParse(config configuration.Configuration) { +func doStdInAndParse(config configuration.Configuration) (err error) { LogLady.Info("Beginning StdIn parse for OSS Index") - checkStdIn() + if err = checkStdIn(); err != nil { + return + } LogLady.Info("Instantiating go.mod package") mod := packages.Mod{} @@ -211,22 +260,27 @@ func doStdInAndParse(config configuration.Configuration) { LogLady.Info("Auditing purls with OSS Index") checkOSSIndex(purls, nil, config) + return } -func checkStdIn() { +func checkStdIn() (err error) { stat, _ := os.Stdin.Stat() if (stat.Mode() & os.ModeCharDevice) == 0 { LogLady.Info("StdIn is valid") } else { LogLady.Error("StdIn is invalid, either empty or another reason") flag.Usage() - os.Exit(1) + err = customerrors.ErrorExit{ExitCode: 1} + return } + return } -func doStdInAndParseForIQ(config configuration.IqConfiguration) { +func doStdInAndParseForIQ(config configuration.IqConfiguration) (err error) { LogLady.Debug("Beginning StdIn parse for IQ") - checkStdIn() + if err = checkStdIn(); err != nil { + return + } LogLady.Info("Instantiating go.mod package") mod := packages.Mod{} @@ -245,6 +299,7 @@ func doStdInAndParseForIQ(config configuration.IqConfiguration) { LogLady.Info("Auditing purls with IQ Server") auditWithIQServer(purls, config.Application, config) + return } func doCheckExistenceAndParse(config configuration.Configuration) { diff --git a/main_test.go b/main_test.go index 64a125c8..08668237 100644 --- a/main_test.go +++ b/main_test.go @@ -17,7 +17,13 @@ package main import ( + "bytes" "fmt" + "github.com/sonatype-nexus-community/nancy/configuration" + "github.com/sonatype-nexus-community/nancy/customerrors" + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" "os/exec" "strings" "testing" @@ -33,3 +39,180 @@ func TestBadArgs(t *testing.T) { t.Errorf("%v", err) } } + +func TestProcessIQConfigHelp(t *testing.T) { + err := processIQConfig(configuration.IqConfiguration{Help: true}) + assert.Error(t, err) + if exiterr, ok := err.(customerrors.ErrorExit); ok { + assert.Equal(t, 0, exiterr.ExitCode) + } else { + t.Fail() + } +} + +func TestProcessIQConfigVersion(t *testing.T) { + err := processIQConfig(configuration.IqConfiguration{Version: true}) + assert.Error(t, err) + if exiterr, ok := err.(customerrors.ErrorExit); ok { + assert.Equal(t, 0, exiterr.ExitCode) + } else { + t.Fail() + } +} + +func TestProcessIQConfigApplicationMissing(t *testing.T) { + // NOTE: Usage func will not have been setup here, since configuration.ParseIQ() has not yet been called + err := processIQConfig(configuration.IqConfiguration{}) + assert.Error(t, err) + if exiterr, ok := err.(customerrors.ErrorExit); ok { + assert.Equal(t, 2, exiterr.ExitCode) + } else { + t.Fail() + } +} + +func TestDoIqExitError(t *testing.T) { + // NOTE: Usage func will be setup by call to configuration.ParseIQ() + err := doIq([]string{"server-url"}) + assert.Error(t, err) + if exiterr, ok := err.(customerrors.ErrorExit); ok { + assert.Equal(t, 2, exiterr.ExitCode) + } else { + t.Fail() + } +} + +func TestDoIqInvalidFlag(t *testing.T) { + // This case would call os.Exit() due to Flag parsing set to flag.ExitOnError + // therefore, changed to flag.ContinueOnError + err := doIq([]string{"-badflag"}) + assert.Error(t, err) + if exiterr, ok := err.(customerrors.ErrorExit); ok { + assert.Equal(t, 2, exiterr.ExitCode) + } else { + t.Fail() + } +} + +func TestDoConfigInvalidType(t *testing.T) { + var stdin bytes.Buffer + stdin.Write([]byte("yadda\n")) + err := doConfig(&stdin) + // TODO this should probably return an error + assert.NoError(t, err) +} + +func TestDoConfigInvalidHomeDir(t *testing.T) { + configuration.HomeDir = "/no/such/dir" + defer resetConfig(t) + + var stdin bytes.Buffer + stdin.Write([]byte("ossindex\nmyOssiUsername\nmyOssiToken\n")) + err := doConfig(&stdin) + assert.Error(t, err) + if exiterr, ok := err.(*os.PathError); ok { + assert.Equal(t, "mkdir", exiterr.Op) + assert.Equal(t, "/no/such/dir/.ossindex", exiterr.Path) + } else { + t.Fail() + } +} + +func setupConfig(t *testing.T) (tempDir string) { + tempDir, err := ioutil.TempDir("", "config-test") + assert.NoError(t, err) + configuration.HomeDir = tempDir + return tempDir +} + +func resetConfig(t *testing.T) { + var err error + configuration.HomeDir, err = os.UserHomeDir() + assert.NoError(t, err) +} + +func TestDoConfigOssIndex(t *testing.T) { + tempDir := setupConfig(t) + defer resetConfig(t) + defer os.RemoveAll(tempDir) // clean up + + var stdin bytes.Buffer + stdin.Write([]byte("ossindex\nmyOssiUsername\nmyOssiToken\n")) + err := doConfig(&stdin) + assert.NoError(t, err) +} + +func TestDoConfigIq(t *testing.T) { + tempDir := setupConfig(t) + defer resetConfig(t) + defer os.RemoveAll(tempDir) // clean up + + var stdin bytes.Buffer + stdin.Write([]byte("iq\nmyIqServer\nmyIqUsername\nmyIqToken\n")) + err := doConfig(&stdin) + assert.NoError(t, err) +} + +func TestProcessConfigHelp(t *testing.T) { + err := processConfig(configuration.Configuration{Help: true}) + assert.Error(t, err) + if exiterr, ok := err.(customerrors.ErrorExit); ok { + assert.Equal(t, 0, exiterr.ExitCode) + } else { + t.Fail() + } +} + +func TestProcessConfigVersion(t *testing.T) { + err := processConfig(configuration.Configuration{Version: true}) + assert.Error(t, err) + if exiterr, ok := err.(customerrors.ErrorExit); ok { + assert.Equal(t, 0, exiterr.ExitCode) + } else { + t.Fail() + } +} + +func TestProcessConfigCleanCache(t *testing.T) { + err := processConfig(configuration.Configuration{CleanCache: true}) + assert.NoError(t, err) +} + +func TestCheckStdInInvalid(t *testing.T) { + err := checkStdIn() + if exiterr, ok := err.(customerrors.ErrorExit); ok { + assert.Equal(t, 1, exiterr.ExitCode) + } else { + t.Fail() + } +} + +func TestCheckStdInValid(t *testing.T) { + tmpfile, err := ioutil.TempFile("", "fakeStdIn") + assert.Nil(t, err) + defer os.Remove(tmpfile.Name()) // clean up + + content := []byte("yadda\n") + if _, err := tmpfile.Write(content); err != nil { + assert.NoError(t, err) + } + if _, err := tmpfile.Seek(0, 0); err != nil { + assert.NoError(t, err) + } + oldStdin := os.Stdin + defer func() { os.Stdin = oldStdin }() // Restore original Stdin + os.Stdin = tmpfile + + err = checkStdIn() + assert.NoError(t, err) +} + +func TestDoOssi(t *testing.T) { + err := doOssi([]string{""}) + assert.Error(t, err) + if exiterr, ok := err.(customerrors.ErrorExit); ok { + assert.Equal(t, 1, exiterr.ExitCode) + } else { + t.Fail() + } +} diff --git a/ossindex/internal/cache/cache.go b/ossindex/internal/cache/cache.go index 8257fc86..0a7e519e 100644 --- a/ossindex/internal/cache/cache.go +++ b/ossindex/internal/cache/cache.go @@ -49,6 +49,7 @@ type DBValue struct { func (c *Cache) getDatabasePath() (dbDir string) { usr, err := user.Current() + // TODO Change this to return error, replace customerrors.Check() customerrors.Check(err, "Error getting user home") return path.Join(usr.HomeDir, types.OssIndexDirName, dbDirName, c.DBName) From ee9d02edf4f42f469990615b9c4822382b5e3c2c Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Sat, 18 Apr 2020 00:52:18 -0400 Subject: [PATCH 02/13] replace Check() calls with ErrorExit usage. WIP, more todo. --- audit/csvformatter.go | 51 ++++++++++----- audit/csvformatter_test.go | 20 ++++++ customerrors/error.go | 34 +--------- cyclonedx/cyclonedx.go | 8 ++- cyclonedx/cyclonedx_test.go | 40 +++++++++++- iq/iq.go | 106 ++++++++++++++++++++++--------- main.go | 60 +++++++++-------- main_test.go | 15 +++++ ossindex/internal/cache/cache.go | 49 ++++++++++---- ossindex/ossindex.go | 7 +- packages/mod.go | 6 +- packages/mod_test.go | 12 +--- 12 files changed, 272 insertions(+), 136 deletions(-) diff --git a/audit/csvformatter.go b/audit/csvformatter.go index 13d98817..e66ad01f 100644 --- a/audit/csvformatter.go +++ b/audit/csvformatter.go @@ -24,7 +24,6 @@ import ( "strconv" . "github.com/sirupsen/logrus" - "github.com/sonatype-nexus-community/nancy/customerrors" "github.com/sonatype-nexus-community/nancy/types" ) @@ -57,33 +56,56 @@ func (f *CsvFormatter) Format(entry *Entry) ([]byte, error) { var buf bytes.Buffer w := csv.NewWriter(&buf) - f.write(w, []string{"Summary"}) - f.write(w, summaryHeader) - f.write(w, summaryRow) + var err error + if err = f.write(w, []string{"Summary"}); err != nil { + return nil, err + } + if err = f.write(w, summaryHeader); err != nil { + return nil, err + } + if err = f.write(w, summaryRow); err != nil { + return nil, err + } if !*f.Quiet { invalidCount := len(invalidEntries) if invalidCount > 0 { - f.write(w, []string{""}) - f.write(w, []string{"Invalid Package(s)"}) - f.write(w, invalidHeader) + if err = f.write(w, []string{""}); err != nil { + return nil, err + } + if err = f.write(w, []string{"Invalid Package(s)"}); err != nil { + return nil, err + } + if err = f.write(w, invalidHeader); err != nil { + return nil, err + } for i := 1; i <= invalidCount; i++ { invalidEntry := invalidEntries[i-1] - f.write(w, []string{"[" + strconv.Itoa(i) + "/" + strconv.Itoa(invalidCount) + "]", invalidEntry.Coordinates, "Does not use SemVer"}) + if err = f.write(w, []string{"[" + strconv.Itoa(i) + "/" + strconv.Itoa(invalidCount) + "]", invalidEntry.Coordinates, "Does not use SemVer"}); err != nil { + return nil, err + } } } } if !*f.Quiet || numVulnerable > 0 { - f.write(w, []string{""}) - f.write(w, []string{"Audited Package(s)"}) - f.write(w, auditedHeader) + if err = f.write(w, []string{""}); err != nil { + return nil, err + } + if err = f.write(w, []string{"Audited Package(s)"}); err != nil { + return nil, err + } + if err = f.write(w, auditedHeader); err != nil { + return nil, err + } } for i := 1; i <= len(auditedEntries); i++ { auditEntry := auditedEntries[i-1] if auditEntry.IsVulnerable() || !*f.Quiet { jsonVulns, _ := json.Marshal(auditEntry.Vulnerabilities) - f.write(w, []string{"[" + strconv.Itoa(i) + "/" + strconv.Itoa(packageCount) + "]", auditEntry.Coordinates, strconv.FormatBool(auditEntry.IsVulnerable()), strconv.Itoa(len(auditEntry.Vulnerabilities)), string(jsonVulns)}) + if err = f.write(w, []string{"[" + strconv.Itoa(i) + "/" + strconv.Itoa(packageCount) + "]", auditEntry.Coordinates, strconv.FormatBool(auditEntry.IsVulnerable()), strconv.Itoa(len(auditEntry.Vulnerabilities)), string(jsonVulns)}); err != nil { + return nil, err + } } } @@ -96,7 +118,6 @@ func (f *CsvFormatter) Format(entry *Entry) ([]byte, error) { } -func (f *CsvFormatter) write(w *csv.Writer, line []string) { - err := w.Write(line) - customerrors.Check(err, "Failed to write data to csv") +func (f *CsvFormatter) write(w *csv.Writer, line []string) error { + return w.Write(line) } diff --git a/audit/csvformatter_test.go b/audit/csvformatter_test.go index 3177d9b2..ad32efa0 100644 --- a/audit/csvformatter_test.go +++ b/audit/csvformatter_test.go @@ -104,3 +104,23 @@ func TestCsvOutputWhenNotAuditLog(t *testing.T) { assert.NotNil(t, e) assert.Equal(t, errors.New("fields passed did not match the expected values for an audit log. You should probably look at setting the formatter to something else"), e) } + +func TestCsvFormatter_FormatNoError(t *testing.T) { + quiet := true + formatter := CsvFormatter{Quiet: &quiet} + + data := map[string]interface{}{ + "audited": []types.Coordinate{ + {Coordinates: "auditedCoordinates"}, + }, + "invalid": []types.Coordinate{ + {Coordinates: "invalidCoordinates"}, + }, + "num_audited": 0, + "num_vulnerable": 0, + "version": "theBuildVersion", + } + buf, err := formatter.Format(&Entry{Data: data}) + assert.NoError(t, err) + assert.Equal(t, "Summary\nAudited Count,Vulnerable Count,Build Version\n0,0,theBuildVersion\n", string(buf)) +} diff --git a/customerrors/error.go b/customerrors/error.go index bbd09b19..ddaca289 100644 --- a/customerrors/error.go +++ b/customerrors/error.go @@ -18,44 +18,14 @@ package customerrors import ( "fmt" - "os" - - "github.com/sonatype-nexus-community/nancy/buildversion" - . "github.com/sonatype-nexus-community/nancy/logger" ) -type SwError struct { - Message string - Err error -} - -func (sw SwError) Error() string { - return fmt.Sprintf("%s - error: %s", sw.Message, sw.Err.Error()) -} - -func (sw SwError) Exit() { - os.Exit(3) -} - -func Check(err error, message string) error { - if err != nil { - location, _ := LogFileLocation() - myErr := SwError{Message: message, Err: err} - LogLady.WithField("error", err).Error(message) - fmt.Println(myErr.Error()) - fmt.Printf("For more information, check the log file at %s\n", location) - fmt.Println("nancy version:", buildversion.BuildVersion) - myErr.Exit() - } - return err -} - type ErrorExit struct { Message string Err error ExitCode int } -func (sw ErrorExit) Error() string { - return fmt.Sprintf("exit code: %d - %s - error: %s", sw.ExitCode, sw.Message, sw.Err.Error()) +func (ee ErrorExit) Error() string { + return fmt.Sprintf("exit code: %d - %s - error: %s", ee.ExitCode, ee.Message, ee.Err.Error()) } diff --git a/cyclonedx/cyclonedx.go b/cyclonedx/cyclonedx.go index 7491b947..eb46fa01 100644 --- a/cyclonedx/cyclonedx.go +++ b/cyclonedx/cyclonedx.go @@ -33,7 +33,7 @@ const version = "1" // ProcessPurlsIntoSBOM will take a slice of packageurl.PackageURL and convert them // into a minimal 1.1 CycloneDX sbom -func ProcessPurlsIntoSBOM(results []types.Coordinate) string { +func ProcessPurlsIntoSBOM(results []types.Coordinate) (string, error) { return processPurlsIntoSBOMSchema1_1(results) } @@ -91,7 +91,9 @@ func processPurlsIntoSBOMSchema1_1(results []types.Coordinate) string { sbom := createSbomDocument() for _, v := range results { purl, err := packageurl.FromString(v.Coordinates) - customerrors.Check(err, "Error parsing purl from given coordinate") + if err != nil { + return "", err + } component := types.Component{ Type: "library", @@ -141,5 +143,5 @@ func processAndReturnSbom(sbom *types.Sbom) string { output = []byte(xml.Header + string(output)) - return string(output) + return string(output), err } diff --git a/cyclonedx/cyclonedx_test.go b/cyclonedx/cyclonedx_test.go index 19053d6b..9d042ea1 100644 --- a/cyclonedx/cyclonedx_test.go +++ b/cyclonedx/cyclonedx_test.go @@ -24,7 +24,7 @@ import ( "github.com/package-url/packageurl-go" "github.com/shopspring/decimal" "github.com/sonatype-nexus-community/nancy/types" - assert "gopkg.in/go-playground/assert.v1" + "gopkg.in/go-playground/assert.v1" ) func TestCreateSBOMFromPackageURLs(t *testing.T) { @@ -104,7 +104,7 @@ func TestCreateSBOMFromSHA1s(t *testing.T) { } func TestProcessPurlsIntoSBOM(t *testing.T) { - results := []types.Coordinate{} + var results []types.Coordinate crypto := types.Coordinate{ Coordinates: "pkg:golang/golang.org/x/crypto@v0.0.0-20190308221718-c2843e01d9a2", Reference: "https://ossindex.sonatype.org/component/pkg:golang/golang.org/x/crypto@v0.0.0-20190308221718-c2843e01d9a2", @@ -128,7 +128,8 @@ func TestProcessPurlsIntoSBOM(t *testing.T) { Reference: "https://ossindex.sonatype.org/component/pkg:golang/github.com/go-yaml/yaml@v2.2.2", Vulnerabilities: []types.Vulnerability{}, }) - result := ProcessPurlsIntoSBOM(results) + result, err := ProcessPurlsIntoSBOM(results) + assert.Equal(t, nil, err) doc := etree.NewDocument() @@ -212,3 +213,36 @@ func assertBaseXMLValid(doc *etree.Element, t *testing.T) { assert.Equal(t, doc.Attr[2].Key, "version") assert.Equal(t, doc.Attr[2].Value, "1") } + +func TestProcess1_1NoError(t *testing.T) { + var results []types.Coordinate + sbom, err := processPurlsIntoSBOMSchema1_1(results) + assert.Equal(t, nil, err) + assert.Equal(t, ` + + + `, sbom) +} + +func TestProcess1_1WithCoordinate(t *testing.T) { + results := []types.Coordinate{ + { + Coordinates: "BADpkg:golang/golang.org/x/crypto@v0.0.0-20190308221718-c2843e01d9a2", + }, + } + + sbom, err := processPurlsIntoSBOMSchema1_1(results) + assert.NotEqual(t, nil, err) + assert.Equal(t, "scheme is missing", err.Error()) + assert.Equal(t, "", sbom) +} + +func TestProcessWithError(t *testing.T) { + var results []types.Coordinate + sbom, err := ProcessPurlsIntoSBOM(results) + assert.Equal(t, nil, err) + assert.Equal(t, ` + + + `, sbom) +} diff --git a/iq/iq.go b/iq/iq.go index a2c8bdbc..0a665e11 100644 --- a/iq/iq.go +++ b/iq/iq.go @@ -65,6 +65,11 @@ type thirdPartyAPIResult struct { var statusURLResp types.StatusURLResult +type resultError struct { + finished bool + err error +} + // AuditPackages accepts a slice of purls, public application ID, and configuration, and will submit these to // Nexus IQ Server for audit, and return a struct of StatusURLResult func AuditPackages(purls []string, applicationID string, config configuration.IqConfiguration) (types.StatusURLResult, error) { @@ -86,16 +91,24 @@ func AuditPackages(purls []string, applicationID string, config configuration.Iq } resultsFromOssIndex, err := ossindex.AuditPackages(purls) //nolint deprecation - customerrors.Check(err, "There was an issue auditing packages using OSS Index") + if err != nil { + return statusURLResp, customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an issue auditing packages using OSS Index"} + } - sbom := cyclonedx.ProcessPurlsIntoSBOM(resultsFromOssIndex) + sbom, err := cyclonedx.ProcessPurlsIntoSBOM(resultsFromOssIndex) + if err != nil { + return types.StatusURLResult{}, err + } LogLady.WithField("sbom", sbom).Debug("Obtained cyclonedx SBOM") LogLady.WithFields(logrus.Fields{ "internal_id": internalID, "sbom": sbom, }).Debug("Submitting to Third Party API") - statusURL := submitToThirdPartyAPI(sbom, internalID) + statusURL, err := submitToThirdPartyAPI(sbom, internalID) + if err != nil { + return statusURLResp, customerrors.ErrorExit{ExitCode: 3, Err: err} + } if statusURL == "" { LogLady.Error("StatusURL not obtained from Third Party API") return statusURLResp, fmt.Errorf("There was an issue submitting your sbom to the Nexus IQ Third Party API, sbom: %s", sbom) @@ -103,22 +116,24 @@ func AuditPackages(purls []string, applicationID string, config configuration.Iq statusURLResp = types.StatusURLResult{} - finished := make(chan bool) + finishedChan := make(chan resultError) - go func() { + go func() resultError { for { select { - case <-finished: - return + case <-finishedChan: + return resultError{finished: true} default: - pollIQServer(fmt.Sprintf("%s/%s", localConfig.Server, statusURL), finished, localConfig.MaxRetries) + if err = pollIQServer(fmt.Sprintf("%s/%s", localConfig.Server, statusURL), finishedChan, localConfig.MaxRetries); err != nil { + return resultError{finished: false, err: err} + } time.Sleep(pollInterval) } } }() - <-finished - return statusURLResp, nil + r := <-finishedChan + return statusURLResp, r.err } func getInternalApplicationID(applicationID string) (string, error) { @@ -129,22 +144,32 @@ func getInternalApplicationID(applicationID string) (string, error) { fmt.Sprintf("%s%s%s", localConfig.Server, internalApplicationIDURL, applicationID), nil, ) - customerrors.Check(err, "Request to get internal application id failed") + if err != nil { + return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Request to get internal application id failed"} + } req.SetBasicAuth(localConfig.User, localConfig.Token) req.Header.Set("User-Agent", useragent.GetUserAgent()) resp, err := client.Do(req) - customerrors.Check(err, "There was an error communicating with Nexus IQ Server to get your internal application ID") + if err != nil { + return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an error communicating with Nexus IQ Server to get your internal application ID"} + } + //noinspection GoUnhandledErrorResult defer resp.Body.Close() if resp.StatusCode == http.StatusOK { bodyBytes, err := ioutil.ReadAll(resp.Body) - customerrors.Check(err, "There was an error retrieving the bytes of the response for getting your internal application ID from Nexus IQ Server") + if err != nil { + return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an error retrieving the bytes of the response for getting your internal application ID from Nexus IQ Server"} + } var response applicationResponse - customerrors.Check(json.Unmarshal(bodyBytes, &response), "failed to unmarshal response") + err = json.Unmarshal(bodyBytes, &response) + if err != nil { + return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "failed to unmarshal response"} + } if response.Applications != nil && len(response.Applications) > 0 { LogLady.WithFields(logrus.Fields{ @@ -166,7 +191,7 @@ func getInternalApplicationID(applicationID string) (string, error) { return "", fmt.Errorf("Unable to communicate with Nexus IQ Server, status code returned is: %d", resp.StatusCode) } -func submitToThirdPartyAPI(sbom string, internalID string) string { +func submitToThirdPartyAPI(sbom string, internalID string) (string, error) { LogLady.Debug("Beginning to submit to Third Party API") client := &http.Client{} @@ -178,26 +203,35 @@ func submitToThirdPartyAPI(sbom string, internalID string) string { url, bytes.NewBuffer([]byte(sbom)), ) - customerrors.Check(err, "Could not POST to Nexus iQ Third Party API") + if err != nil { + return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Could not POST to Nexus iQ Third Party API"} + } req.SetBasicAuth(localConfig.User, localConfig.Token) req.Header.Set("User-Agent", useragent.GetUserAgent()) req.Header.Set("Content-Type", "application/xml") resp, err := client.Do(req) - customerrors.Check(err, "There was an issue communicating with the Nexus IQ Third Party API") + if err != nil { + return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an issue communicating with the Nexus IQ Third Party API"} + } + //noinspection GoUnhandledErrorResult defer resp.Body.Close() if resp.StatusCode == http.StatusAccepted { bodyBytes, err := ioutil.ReadAll(resp.Body) LogLady.WithField("body", string(bodyBytes)).Info("Request accepted") - customerrors.Check(err, "There was an issue submitting your sbom to the Nexus IQ Third Party API") + if err != nil { + return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an issue submitting your sbom to the Nexus IQ Third Party API"} + } var response thirdPartyAPIResult err = json.Unmarshal(bodyBytes, &response) - customerrors.Check(err, "Could not unmarshal response from iQ server") - return response.StatusURL + if err != nil { + return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Could not unmarshal response from iQ server"} + } + return response.StatusURL, err } bodyBytes, err := ioutil.ReadAll(resp.Body) LogLady.WithFields(logrus.Fields{ @@ -205,12 +239,14 @@ func submitToThirdPartyAPI(sbom string, internalID string) string { "status_code": resp.StatusCode, "status": resp.Status, }).Info("Request not accepted") - customerrors.Check(err, "There was an issue submitting your sbom to the Nexus IQ Third Party API") + if err != nil { + return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an issue submitting your sbom to the Nexus IQ Third Party API"} + } - return "" + return "", err } -func pollIQServer(statusURL string, finished chan bool, maxRetries int) { +func pollIQServer(statusURL string, finished chan resultError, maxRetries int) error { LogLady.WithFields(logrus.Fields{ "attempt_number": tries, "max_retries": maxRetries, @@ -218,12 +254,14 @@ func pollIQServer(statusURL string, finished chan bool, maxRetries int) { }).Trace("Polling Nexus IQ for response") if tries > maxRetries { LogLady.Error("Maximum tries exceeded, finished polling, consider bumping up Max Retries") - finished <- true + finished <- resultError{finished: true, err: nil} } client := &http.Client{} req, err := http.NewRequest("GET", statusURL, nil) - customerrors.Check(err, "Could not poll iQ server") + if err != nil { + return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Could not poll iQ server"} + } req.SetBasicAuth(localConfig.User, localConfig.Token) @@ -232,27 +270,33 @@ func pollIQServer(statusURL string, finished chan bool, maxRetries int) { resp, err := client.Do(req) if err != nil { - finished <- true - customerrors.Check(err, "There was an error polling Nexus IQ Server") + finished <- resultError{finished: true, err: err} + return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an error polling Nexus IQ Server"} } + //noinspection GoUnhandledErrorResult defer resp.Body.Close() if resp.StatusCode == http.StatusOK { bodyBytes, err := ioutil.ReadAll(resp.Body) - customerrors.Check(err, "There was an error with processing the response from polling Nexus IQ Server") + if err != nil { + return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an error with processing the response from polling Nexus IQ Server"} + } var response types.StatusURLResult err = json.Unmarshal(bodyBytes, &response) - customerrors.Check(err, "Could not unmarshal response from iQ server") + if err != nil { + return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Could not unmarshal response from iQ server"} + } statusURLResp = response if response.IsError { - finished <- true + finished <- resultError{finished: true, err: nil} } - finished <- true + finished <- resultError{finished: true, err: nil} } tries++ fmt.Print(".") + return err } func warnUserOfBadLifeChoices() { diff --git a/main.go b/main.go index 2c0cc06c..39e1f62e 100644 --- a/main.go +++ b/main.go @@ -55,6 +55,7 @@ func main() { } else { err = doOssi(os.Args[1:]) } + if err != nil { if exiterr, ok := err.(*customerrors.ErrorExit); ok { os.Exit(exiterr.ExitCode) @@ -84,12 +85,7 @@ func doOssi(ossiArgs []string) (err error) { func doConfig(stdin io.Reader) (err error) { LogLady.Info("Nancy setting config via the command line") - if err = configuration.GetConfigFromCommandLine(stdin); err != nil { - return - } - if err = customerrors.Check(err, "Unable to set config for Nancy"); err != nil { - return - } + err = configuration.GetConfigFromCommandLine(stdin) return } @@ -183,7 +179,9 @@ func processConfig(config configuration.Configuration) (err error) { } if !config.UseStdIn { LogLady.Info("Parsing config for file based scan") - doCheckExistenceAndParse(config) + if err = doCheckExistenceAndParse(config); err != nil { + return + } } return } @@ -259,7 +257,7 @@ func doStdInAndParse(config configuration.Configuration) (err error) { }).Debug("Extracted purls") LogLady.Info("Auditing purls with OSS Index") - checkOSSIndex(purls, nil, config) + err = checkOSSIndex(purls, nil, config) return } @@ -298,11 +296,11 @@ func doStdInAndParseForIQ(config configuration.IqConfiguration) (err error) { }).Debug("Extracted purls") LogLady.Info("Auditing purls with IQ Server") - auditWithIQServer(purls, config.Application, config) + err = auditWithIQServer(purls, config.Application, config) return } -func doCheckExistenceAndParse(config configuration.Configuration) { +func doCheckExistenceAndParse(config configuration.Configuration) error { switch { case strings.Contains(config.Path, "Gopkg.lock"): workingDir := filepath.Dir(config.Path) @@ -315,33 +313,41 @@ func doCheckExistenceAndParse(config configuration.Configuration) { GOPATHs: []string{getenv}, } project, err := ctx.LoadProject() - customerrors.Check(err, fmt.Sprintf("could not read lock at path %s", config.Path)) - + if err != nil { + return customerrors.ErrorExit{ExitCode: 3, Message: fmt.Sprintf("could not read lock at path %s", config.Path)} + } if project.Lock == nil { - customerrors.Check(errors.New("dep failed to parse lock file and returned nil"), "nancy could not continue due to dep failure") + return customerrors.ErrorExit{ExitCode: 3, Err: errors.New("dep failed to parse lock file and returned nil"), Message: "nancy could not continue due to dep failure"} } purls, invalidPurls := packages.ExtractPurlsUsingDep(project) - checkOSSIndex(purls, invalidPurls, config) + return checkOSSIndex(purls, invalidPurls, config) case strings.Contains(config.Path, "go.sum"): mod := packages.Mod{} mod.GoSumPath = config.Path - if mod.CheckExistenceOfManifest() { + manifestExists, err := mod.CheckExistenceOfManifest() + if err != nil { + return err + } + if manifestExists { mod.ProjectList, _ = parse.GoSum(config.Path) var purls = mod.ExtractPurlsFromManifest() - checkOSSIndex(purls, nil, config) + return checkOSSIndex(purls, nil, config) } default: - os.Exit(3) + return customerrors.ErrorExit{ExitCode: 3} } + return nil } -func checkOSSIndex(purls []string, invalidpurls []string, config configuration.Configuration) { +func checkOSSIndex(purls []string, invalidpurls []string, config configuration.Configuration) error { var packageCount = len(purls) coordinates, err := ossindex.AuditPackagesWithOSSIndex(purls, &config) - customerrors.Check(err, "Error auditing packages") + if err != nil { + return err + } var invalidCoordinates []types.Coordinate for _, invalidpurl := range invalidpurls { @@ -349,30 +355,34 @@ func checkOSSIndex(purls []string, invalidpurls []string, config configuration.C } if count := audit.LogResults(config.Formatter, packageCount, coordinates, invalidCoordinates, config.CveList.Cves); count > 0 { - os.Exit(count) + // return error with number of vulnerable items found + return customerrors.ErrorExit{ExitCode: count} } + return nil } -func auditWithIQServer(purls []string, applicationID string, config configuration.IqConfiguration) { +func auditWithIQServer(purls []string, applicationID string, config configuration.IqConfiguration) error { LogLady.Debug("Sending purls to be Audited by IQ Server") res, err := iq.AuditPackages(purls, applicationID, config) - customerrors.Check(err, "Uh oh! There was an error with your request to Nexus IQ Server") + if err != nil { + return err + } fmt.Println() if res.IsError { LogLady.WithField("res", res).Error("An error occurred with the request to IQ Server") - customerrors.Check(errors.New(res.ErrorMessage), "Uh oh! There was an error with your request to Nexus IQ Server") + return errors.New(res.ErrorMessage) } if res.PolicyAction != "Failure" { LogLady.WithField("res", res).Debug("Successful in communicating with IQ Server") fmt.Println("Wonderbar! No policy violations reported for this audit!") fmt.Println("Report URL: ", res.ReportHTMLURL) - os.Exit(0) + return nil } else { LogLady.WithField("res", res).Debug("Successful in communicating with IQ Server") fmt.Println("Hi, Nancy here, you have some policy violations to clean up!") fmt.Println("Report URL: ", res.ReportHTMLURL) - os.Exit(1) + return customerrors.ErrorExit{ExitCode: 1} } } diff --git a/main_test.go b/main_test.go index 08668237..d349ad49 100644 --- a/main_test.go +++ b/main_test.go @@ -19,6 +19,7 @@ package main import ( "bytes" "fmt" + "github.com/sonatype-nexus-community/nancy/audit" "github.com/sonatype-nexus-community/nancy/configuration" "github.com/sonatype-nexus-community/nancy/customerrors" "github.com/stretchr/testify/assert" @@ -134,6 +135,7 @@ func resetConfig(t *testing.T) { func TestDoConfigOssIndex(t *testing.T) { tempDir := setupConfig(t) defer resetConfig(t) + //noinspection GoUnhandledErrorResult defer os.RemoveAll(tempDir) // clean up var stdin bytes.Buffer @@ -145,6 +147,7 @@ func TestDoConfigOssIndex(t *testing.T) { func TestDoConfigIq(t *testing.T) { tempDir := setupConfig(t) defer resetConfig(t) + //noinspection GoUnhandledErrorResult defer os.RemoveAll(tempDir) // clean up var stdin bytes.Buffer @@ -190,6 +193,7 @@ func TestCheckStdInInvalid(t *testing.T) { func TestCheckStdInValid(t *testing.T) { tmpfile, err := ioutil.TempFile("", "fakeStdIn") assert.Nil(t, err) + //noinspection GoUnhandledErrorResult defer os.Remove(tmpfile.Name()) // clean up content := []byte("yadda\n") @@ -207,6 +211,17 @@ func TestCheckStdInValid(t *testing.T) { assert.NoError(t, err) } +func TestCheckOSSIndexNoneVulnerable(t *testing.T) { + // TODO find way to mock ossindex url for this test, probably just move checkOSSIndex() method to ossindex package + purls := []string{"pkg:github/BurntSushi/toml@0.3.1"} + invalidPurls := []string{"invalidPurl"} + noColor := true + quiet := true + config := configuration.Configuration{Formatter: &audit.AuditLogTextFormatter{Quiet: &quiet, NoColor: &noColor}} + err := checkOSSIndex(purls, invalidPurls, config) + assert.NoError(t, err) +} + func TestDoOssi(t *testing.T) { err := doOssi([]string{""}) assert.Error(t, err) diff --git a/ossindex/internal/cache/cache.go b/ossindex/internal/cache/cache.go index 0a7e519e..96587d51 100644 --- a/ossindex/internal/cache/cache.go +++ b/ossindex/internal/cache/cache.go @@ -47,12 +47,13 @@ type DBValue struct { TTL int64 } -func (c *Cache) getDatabasePath() (dbDir string) { +func (c *Cache) getDatabasePath() (dbDir string, err error) { usr, err := user.Current() - // TODO Change this to return error, replace customerrors.Check() - customerrors.Check(err, "Error getting user home") + if err != nil { + return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Error getting user home"} + } - return path.Join(usr.HomeDir, types.OssIndexDirName, dbDirName, c.DBName) + return path.Join(usr.HomeDir, types.OssIndexDirName, dbDirName, c.DBName), err } // RemoveCache deletes the cache database @@ -63,7 +64,11 @@ func (c *Cache) RemoveCache() error { } }() - err := pudge.DeleteFile(c.getDatabasePath()) + dbDir, err := c.getDatabasePath() + if err != nil { + return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Error getting user home"} + } + err = pudge.DeleteFile(dbDir) if err == nil { return nil } @@ -71,7 +76,7 @@ func (c *Cache) RemoveCache() error { LogLady.WithField("error", err).Error("Unable to delete database, looks like it doesn't exist") return nil } - err = pudge.BackupAll(c.getDatabasePath()) + err = pudge.BackupAll(dbDir) if err != nil { return err } @@ -89,7 +94,12 @@ func (c *Cache) Insert(coordinates []types.Coordinate) (err error) { }() doSet := func(coordinate types.Coordinate) error { - err = pudge.Set(c.getDatabasePath(), strings.ToLower(coordinate.Coordinates), DBValue{Coordinates: coordinate, TTL: c.TTL.Unix()}) + dbDir, err := c.getDatabasePath() + if err != nil { + return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Error getting user home"} + } + + err = pudge.Set(dbDir, strings.ToLower(coordinate.Coordinates), DBValue{Coordinates: coordinate, TTL: c.TTL.Unix()}) if err != nil { LogLady.WithField("error", err).Error("Unable to add coordinate to cache DB") return err @@ -111,7 +121,9 @@ func (c *Cache) Insert(coordinates []types.Coordinate) (err error) { continue } if exists.TTL < time.Now().Unix() { - c.deleteKey(coord.Coordinates) + if err = c.deleteKey(coord.Coordinates); err != nil { + return + } err = doSet(coord) if err != nil { continue @@ -150,7 +162,9 @@ func (c *Cache) GetCacheValues(purls []string) ([]string, []types.Coordinate, er if item.TTL < time.Now().Unix() { newPurls = append(newPurls, purl) - c.deleteKey(item.Coordinates.Coordinates) + if err = c.deleteKey(item.Coordinates.Coordinates); err != nil { + return nil, nil, err + } continue } else { LogLady.WithField("coordinate", item.Coordinates).Info("Result found in cache, moving forward and hydrating results") @@ -161,13 +175,24 @@ func (c *Cache) GetCacheValues(purls []string) ([]string, []types.Coordinate, er return newPurls, results, nil } -func (c *Cache) deleteKey(key string) { - err := pudge.Delete(c.getDatabasePath(), strings.ToLower(key)) +func (c *Cache) deleteKey(key string) error { + dbDir, err := c.getDatabasePath() + if err != nil { + return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Error getting user home"} + } + + err = pudge.Delete(dbDir, strings.ToLower(key)) if err != nil { LogLady.WithField("error", err).Error("Unable to delete value from pudge db") } + return err } func (c *Cache) getKeyAndHydrate(key string, item *DBValue) error { - return pudge.Get(c.getDatabasePath(), strings.ToLower(key), item) + dbDir, err := c.getDatabasePath() + if err != nil { + return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Error getting user home"} + } + + return pudge.Get(dbDir, strings.ToLower(key), item) } diff --git a/ossindex/ossindex.go b/ossindex/ossindex.go index 028210c6..867b3df5 100644 --- a/ossindex/ossindex.go +++ b/ossindex/ossindex.go @@ -26,7 +26,6 @@ import ( "time" "github.com/sonatype-nexus-community/nancy/configuration" - "github.com/sonatype-nexus-community/nancy/customerrors" . "github.com/sonatype-nexus-community/nancy/logger" "github.com/sonatype-nexus-community/nancy/ossindex/internal/cache" "github.com/sonatype-nexus-community/nancy/types" @@ -83,7 +82,9 @@ func AuditPackagesWithOSSIndex(purls []string, config *configuration.Configurati func doAuditPackages(purls []string, config *configuration.Configuration) ([]types.Coordinate, error) { newPurls, results, err := dbCache.GetCacheValues(purls) - customerrors.Check(err, "Error initializing cache") + if err != nil { + return nil, err + } chunks := chunk(newPurls, MaxCoords) @@ -146,7 +147,7 @@ func doRequestToOSSIndex(jsonStr []byte, config *configuration.Configuration) (c } // Process results - if err = json.Unmarshal([]byte(body), &coordinates); err != nil { + if err = json.Unmarshal(body, &coordinates); err != nil { LogLady.WithField("error", err).Error("Error unmarshalling response from OSS Index") return } diff --git a/packages/mod.go b/packages/mod.go index b86f1779..3f8d7ecb 100644 --- a/packages/mod.go +++ b/packages/mod.go @@ -60,11 +60,11 @@ func (m Mod) ExtractPurlsFromManifestForIQ() (purls []string) { return } -func (m Mod) CheckExistenceOfManifest() bool { +func (m Mod) CheckExistenceOfManifest() (bool, error) { if _, err := os.Stat(m.GoSumPath); os.IsNotExist(err) { - customerrors.Check(err, fmt.Sprint("No go.sum found at path: "+m.GoSumPath)) + return false, customerrors.ErrorExit{ExitCode: 3, Err: err, Message: fmt.Sprint("No go.sum found at path: " + m.GoSumPath)} } - return true + return true, nil } func removeDuplicates(purls []string) (dedupedPurls []string) { diff --git a/packages/mod_test.go b/packages/mod_test.go index 5d7922a3..259be7cf 100644 --- a/packages/mod_test.go +++ b/packages/mod_test.go @@ -17,6 +17,7 @@ package packages import ( + "github.com/stretchr/testify/assert" "testing" "github.com/sonatype-nexus-community/nancy/types" @@ -66,7 +67,8 @@ func appendProject(name string, version string, projectList *types.ProjectList) func TestModCheckExistenceOfManifestExists(t *testing.T) { mod := Mod{} mod.GoSumPath = testGoSumName - exists := mod.CheckExistenceOfManifest() + exists, err := mod.CheckExistenceOfManifest() + assert.NoError(t, err) if !exists { t.Errorf("Expected existence of %s", testGoSumName) @@ -74,13 +76,9 @@ func TestModCheckExistenceOfManifestExists(t *testing.T) { } func TestModExtractPurlsFromManifest(t *testing.T) { - var err error mod := Mod{} mod.GoSumPath = testGoSumName mod.ProjectList = getProjectList() - if err != nil { - t.Error(err) - } result := mod.ExtractPurlsFromManifest() if len(result) != 5 { @@ -89,13 +87,9 @@ func TestModExtractPurlsFromManifest(t *testing.T) { } func TestModExtractPurlsFromManifestDuplicates(t *testing.T) { - var err error mod := Mod{} mod.GoSumPath = testGoSumName mod.ProjectList = getProjectListDuplicates() - if err != nil { - t.Error(err) - } result := mod.ExtractPurlsFromManifest() if len(result) != 5 { From 48a1a82bf365a5e5c97b1e23844227f036af1c94 Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Mon, 20 Apr 2020 11:55:39 -0400 Subject: [PATCH 03/13] Fix seg fault when Err struct member was nil. So much for making Jeffry NOT code on the weekend. --- customerrors/error.go | 8 +++++++- customerrors/error_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 customerrors/error_test.go diff --git a/customerrors/error.go b/customerrors/error.go index ddaca289..30146c99 100644 --- a/customerrors/error.go +++ b/customerrors/error.go @@ -27,5 +27,11 @@ type ErrorExit struct { } func (ee ErrorExit) Error() string { - return fmt.Sprintf("exit code: %d - %s - error: %s", ee.ExitCode, ee.Message, ee.Err.Error()) + var errString string + if ee.Err != nil { + errString = ee.Err.Error() + } else { + errString = "nil" + } + return fmt.Sprintf("exit code: %d - %s - error: %s", ee.ExitCode, ee.Message, errString) } diff --git a/customerrors/error_test.go b/customerrors/error_test.go new file mode 100644 index 00000000..8b4ff7ec --- /dev/null +++ b/customerrors/error_test.go @@ -0,0 +1,34 @@ +// +// Copyright 2018-present Sonatype Inc. +// +// 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 customerrors + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestError(t *testing.T) { + assert.Equal(t, "exit code: 2 - MyMessage - error: MyError", + ErrorExit{Message: "MyMessage", Err: fmt.Errorf("MyError"), ExitCode: 2}.Error()) + assert.Equal(t, "exit code: 0 - MyMessage - error: MyError", + ErrorExit{Message: "MyMessage", Err: fmt.Errorf("MyError")}.Error()) + assert.Equal(t, "exit code: 0 - MyMessage - error: nil", + ErrorExit{Message: "MyMessage"}.Error()) + assert.Equal(t, "exit code: 0 - - error: nil", + ErrorExit{}.Error()) +} From 741f98801e3c7b621fd7a2fc062fa942eb8cb7b1 Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Mon, 20 Apr 2020 17:14:00 -0400 Subject: [PATCH 04/13] minor message tweaks --- configuration/set.go | 4 ++-- main.go | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/configuration/set.go b/configuration/set.go index 624ee339..83ee7d67 100644 --- a/configuration/set.go +++ b/configuration/set.go @@ -72,7 +72,7 @@ func GetConfigFromCommandLine(stdin io.Reader) (err error) { ConfigLocation = filepath.Join(HomeDir, types.OssIndexDirName, types.OssIndexConfigFileName) err = getAndSetOSSIndexConfig(reader) case "": - // TODO this should probably return an error, because it means config setup was not completed + // TODO should this return an error, because it means config setup was not completed? return default: LogLady.Infof("User chose invalid config type: %s, recurse madness", str) @@ -180,7 +180,7 @@ func marshallAndWriteToDisk(config interface{}) (err error) { } LogLady.WithField("config_location", ConfigLocation).Info("Successfully wrote config to disk") - fmt.Printf("Successfully wrote config to: %s", ConfigLocation) + fmt.Printf("Successfully wrote config to: %s\n", ConfigLocation) return } diff --git a/main.go b/main.go index 39e1f62e..fd49fb06 100644 --- a/main.go +++ b/main.go @@ -65,7 +65,6 @@ func main() { os.Exit(4) } } - fmt.Println("byeeeeee") // probably shouldn't get here until other exit cases above are all covered. } func doOssi(ossiArgs []string) (err error) { From 38dcc0da451e9287d8c3afddc842ebff28527c14 Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Mon, 20 Apr 2020 17:30:15 -0400 Subject: [PATCH 05/13] fix pointer bug in ExitError detection --- main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index fd49fb06..0863a339 100644 --- a/main.go +++ b/main.go @@ -57,7 +57,7 @@ func main() { } if err != nil { - if exiterr, ok := err.(*customerrors.ErrorExit); ok { + if exiterr, ok := err.(customerrors.ErrorExit); ok { os.Exit(exiterr.ExitCode) } else { // really don't expect this @@ -76,6 +76,7 @@ func doOssi(ossiArgs []string) (err error) { return } if err = processConfig(ossIndexConfig); err != nil { + LogLady.Info("Nancy finished parsing config for OSS Index, vulnerability found") return } LogLady.Info("Nancy finished parsing config for OSS Index") @@ -355,6 +356,7 @@ func checkOSSIndex(purls []string, invalidpurls []string, config configuration.C if count := audit.LogResults(config.Formatter, packageCount, coordinates, invalidCoordinates, config.CveList.Cves); count > 0 { // return error with number of vulnerable items found + fmt.Printf("Found vuln count: %d\n", count) return customerrors.ErrorExit{ExitCode: count} } return nil From 33af791290c5a4dfbf1b5b945b6f845915e68e91 Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Mon, 20 Apr 2020 17:43:56 -0400 Subject: [PATCH 06/13] minor message tweak --- main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/main.go b/main.go index 0863a339..412ddc9e 100644 --- a/main.go +++ b/main.go @@ -99,6 +99,7 @@ func doIq(iqArgs []string) (err error) { } LogLady.WithField("config", config).Info("Obtained IQ config") if err = processIQConfig(config); err != nil { + LogLady.Info("Nancy finished parsing config for IQ, vulnerability found") return } LogLady.Info("Nancy finished parsing config for IQ") From 91e232b3b2a53dc3ed189a62481d5bc803517bf5 Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Mon, 20 Apr 2020 18:12:02 -0400 Subject: [PATCH 07/13] get things happy after rebase on master --- cyclonedx/cyclonedx.go | 7 +++---- cyclonedx/cyclonedx_test.go | 17 ++++++----------- iq/iq.go | 5 +---- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/cyclonedx/cyclonedx.go b/cyclonedx/cyclonedx.go index eb46fa01..3a56161a 100644 --- a/cyclonedx/cyclonedx.go +++ b/cyclonedx/cyclonedx.go @@ -21,7 +21,6 @@ import ( "encoding/xml" "github.com/package-url/packageurl-go" - "github.com/sonatype-nexus-community/nancy/customerrors" . "github.com/sonatype-nexus-community/nancy/logger" "github.com/sonatype-nexus-community/nancy/types" ) @@ -33,7 +32,7 @@ const version = "1" // ProcessPurlsIntoSBOM will take a slice of packageurl.PackageURL and convert them // into a minimal 1.1 CycloneDX sbom -func ProcessPurlsIntoSBOM(results []types.Coordinate) (string, error) { +func ProcessPurlsIntoSBOM(results []types.Coordinate) string { return processPurlsIntoSBOMSchema1_1(results) } @@ -92,7 +91,7 @@ func processPurlsIntoSBOMSchema1_1(results []types.Coordinate) string { for _, v := range results { purl, err := packageurl.FromString(v.Coordinates) if err != nil { - return "", err + return "" } component := types.Component{ @@ -143,5 +142,5 @@ func processAndReturnSbom(sbom *types.Sbom) string { output = []byte(xml.Header + string(output)) - return string(output), err + return string(output) } diff --git a/cyclonedx/cyclonedx_test.go b/cyclonedx/cyclonedx_test.go index 9d042ea1..c7f86de2 100644 --- a/cyclonedx/cyclonedx_test.go +++ b/cyclonedx/cyclonedx_test.go @@ -28,7 +28,7 @@ import ( ) func TestCreateSBOMFromPackageURLs(t *testing.T) { - results := []packageurl.PackageURL{} + var results []packageurl.PackageURL uno, _ := packageurl.FromString("pkg:golang/github.com/test/test@1.0.0") results = append(results, uno) @@ -65,7 +65,7 @@ func TestCreateSBOMFromPackageURLs(t *testing.T) { } func TestCreateSBOMFromSHA1s(t *testing.T) { - results := []types.Sha1SBOM{} + var results []types.Sha1SBOM uno := types.Sha1SBOM{Location: "/path/on/disk", Sha1: "c2843e01d9a2"} results = append(results, uno) @@ -128,8 +128,7 @@ func TestProcessPurlsIntoSBOM(t *testing.T) { Reference: "https://ossindex.sonatype.org/component/pkg:golang/github.com/go-yaml/yaml@v2.2.2", Vulnerabilities: []types.Vulnerability{}, }) - result, err := ProcessPurlsIntoSBOM(results) - assert.Equal(t, nil, err) + result := ProcessPurlsIntoSBOM(results) doc := etree.NewDocument() @@ -216,8 +215,7 @@ func assertBaseXMLValid(doc *etree.Element, t *testing.T) { func TestProcess1_1NoError(t *testing.T) { var results []types.Coordinate - sbom, err := processPurlsIntoSBOMSchema1_1(results) - assert.Equal(t, nil, err) + sbom := processPurlsIntoSBOMSchema1_1(results) assert.Equal(t, ` @@ -231,16 +229,13 @@ func TestProcess1_1WithCoordinate(t *testing.T) { }, } - sbom, err := processPurlsIntoSBOMSchema1_1(results) - assert.NotEqual(t, nil, err) - assert.Equal(t, "scheme is missing", err.Error()) + sbom := processPurlsIntoSBOMSchema1_1(results) assert.Equal(t, "", sbom) } func TestProcessWithError(t *testing.T) { var results []types.Coordinate - sbom, err := ProcessPurlsIntoSBOM(results) - assert.Equal(t, nil, err) + sbom := ProcessPurlsIntoSBOM(results) assert.Equal(t, ` diff --git a/iq/iq.go b/iq/iq.go index 0a665e11..fa40634b 100644 --- a/iq/iq.go +++ b/iq/iq.go @@ -95,10 +95,7 @@ func AuditPackages(purls []string, applicationID string, config configuration.Iq return statusURLResp, customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an issue auditing packages using OSS Index"} } - sbom, err := cyclonedx.ProcessPurlsIntoSBOM(resultsFromOssIndex) - if err != nil { - return types.StatusURLResult{}, err - } + sbom := cyclonedx.ProcessPurlsIntoSBOM(resultsFromOssIndex) LogLady.WithField("sbom", sbom).Debug("Obtained cyclonedx SBOM") LogLady.WithFields(logrus.Fields{ From ccc94e18a38e0781cbc912a074a6024d1c5bb50c Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Tue, 21 Apr 2020 16:57:55 -0400 Subject: [PATCH 08/13] restore "check the log" messages using error "constructor" that prints helpful message about where to look. Other cleanups from PR feedback. --- audit/csvformatter.go | 6 +++++- configuration/parse.go | 4 ---- configuration/set.go | 2 +- customerrors/error.go | 20 +++++++++++++++++++- customerrors/error_test.go | 13 +++++++++++-- cyclonedx/cyclonedx.go | 2 ++ iq/iq.go | 28 ++++++++++++++-------------- main.go | 14 ++++++++------ main_test.go | 11 ++++++++--- ossindex/internal/cache/cache.go | 2 +- ossindex/ossindex.go | 3 ++- packages/mod.go | 2 +- 12 files changed, 72 insertions(+), 35 deletions(-) diff --git a/audit/csvformatter.go b/audit/csvformatter.go index e66ad01f..62a5addb 100644 --- a/audit/csvformatter.go +++ b/audit/csvformatter.go @@ -21,6 +21,7 @@ import ( "encoding/csv" "encoding/json" "errors" + "github.com/sonatype-nexus-community/nancy/customerrors" "strconv" . "github.com/sirupsen/logrus" @@ -119,5 +120,8 @@ func (f *CsvFormatter) Format(entry *Entry) ([]byte, error) { } func (f *CsvFormatter) write(w *csv.Writer, line []string) error { - return w.Write(line) + if err := w.Write(line); err != nil { + return customerrors.NewErrorExitPrintHelp(err, "Failed to write data to csv") + } + return nil } diff --git a/configuration/parse.go b/configuration/parse.go index 418792ae..6ef54ecf 100644 --- a/configuration/parse.go +++ b/configuration/parse.go @@ -88,8 +88,6 @@ func ParseIQ(args []string) (config IqConfiguration, err error) { Options: `) iqCommand.PrintDefaults() - //os.Exit(2) - // Avoid exit calls, but caller should use exit code 2 when handling } ConfigLocation = filepath.Join(HomeDir, types.IQServerDirName, types.IQServerConfigFileName) @@ -174,8 +172,6 @@ func Parse(args []string) (Configuration, error) { Options: `) flag.PrintDefaults() - // os.Exit(2) - // Avoid exit calls, but caller should use exit code 2 when handling } ConfigLocation = filepath.Join(HomeDir, types.OssIndexDirName, types.OssIndexConfigFileName) diff --git a/configuration/set.go b/configuration/set.go index 83ee7d67..903051b0 100644 --- a/configuration/set.go +++ b/configuration/set.go @@ -75,7 +75,7 @@ func GetConfigFromCommandLine(stdin io.Reader) (err error) { // TODO should this return an error, because it means config setup was not completed? return default: - LogLady.Infof("User chose invalid config type: %s, recurse madness", str) + LogLady.Infof("User chose invalid config type: %s, will retry", str) fmt.Println("Invalid value, 'iq' and 'ossindex' are accepted values, try again!") err = GetConfigFromCommandLine(stdin) } diff --git a/customerrors/error.go b/customerrors/error.go index 30146c99..b4a91d98 100644 --- a/customerrors/error.go +++ b/customerrors/error.go @@ -18,6 +18,8 @@ package customerrors import ( "fmt" + "github.com/sonatype-nexus-community/nancy/buildversion" + . "github.com/sonatype-nexus-community/nancy/logger" ) type ErrorExit struct { @@ -31,7 +33,23 @@ func (ee ErrorExit) Error() string { if ee.Err != nil { errString = ee.Err.Error() } else { - errString = "nil" + errString = "" } return fmt.Sprintf("exit code: %d - %s - error: %s", ee.ExitCode, ee.Message, errString) } + +func NewErrorExitPrintHelp(errCause error, message string) ErrorExit { + myErr := ErrorExit{message, errCause, 3} + LogLady.WithField("error", errCause).Error(message) + fmt.Println(myErr.Error()) + + var logFile string + var logFileErr error + if logFile, logFileErr = LogFileLocation(); logFileErr != nil { + logFile = "unknown" + } + + fmt.Printf("For more information, check the log file at %s\n", logFile) + fmt.Println("nancy version:", buildversion.BuildVersion) + return myErr +} diff --git a/customerrors/error_test.go b/customerrors/error_test.go index 8b4ff7ec..a5b500b7 100644 --- a/customerrors/error_test.go +++ b/customerrors/error_test.go @@ -27,8 +27,17 @@ func TestError(t *testing.T) { ErrorExit{Message: "MyMessage", Err: fmt.Errorf("MyError"), ExitCode: 2}.Error()) assert.Equal(t, "exit code: 0 - MyMessage - error: MyError", ErrorExit{Message: "MyMessage", Err: fmt.Errorf("MyError")}.Error()) - assert.Equal(t, "exit code: 0 - MyMessage - error: nil", + assert.Equal(t, "exit code: 0 - MyMessage - error: ", ErrorExit{Message: "MyMessage"}.Error()) - assert.Equal(t, "exit code: 0 - - error: nil", + assert.Equal(t, "exit code: 0 - - error: ", ErrorExit{}.Error()) } + +func TestNewErrorExitPrintHelp(t *testing.T) { + assert.Equal(t, "exit code: 3 - MyMessage - error: MyError", + NewErrorExitPrintHelp(fmt.Errorf("MyError"), "MyMessage").Error()) + assert.Equal(t, "exit code: 3 - MyMessage - error: ", + NewErrorExitPrintHelp(nil, "MyMessage").Error()) + assert.Equal(t, "exit code: 3 - - error: ", + NewErrorExitPrintHelp(nil, "").Error()) +} diff --git a/cyclonedx/cyclonedx.go b/cyclonedx/cyclonedx.go index 3a56161a..4c4e698f 100644 --- a/cyclonedx/cyclonedx.go +++ b/cyclonedx/cyclonedx.go @@ -19,6 +19,7 @@ package cyclonedx import ( "encoding/xml" + "github.com/sonatype-nexus-community/nancy/customerrors" "github.com/package-url/packageurl-go" . "github.com/sonatype-nexus-community/nancy/logger" @@ -91,6 +92,7 @@ func processPurlsIntoSBOMSchema1_1(results []types.Coordinate) string { for _, v := range results { purl, err := packageurl.FromString(v.Coordinates) if err != nil { + _ = customerrors.NewErrorExitPrintHelp(err, "Error parsing purl from given coordinate") return "" } diff --git a/iq/iq.go b/iq/iq.go index fa40634b..1c9e080a 100644 --- a/iq/iq.go +++ b/iq/iq.go @@ -92,7 +92,7 @@ func AuditPackages(purls []string, applicationID string, config configuration.Iq resultsFromOssIndex, err := ossindex.AuditPackages(purls) //nolint deprecation if err != nil { - return statusURLResp, customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an issue auditing packages using OSS Index"} + return statusURLResp, customerrors.NewErrorExitPrintHelp(err, "There was an issue auditing packages using OSS Index") } sbom := cyclonedx.ProcessPurlsIntoSBOM(resultsFromOssIndex) @@ -142,7 +142,7 @@ func getInternalApplicationID(applicationID string) (string, error) { nil, ) if err != nil { - return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Request to get internal application id failed"} + return "", customerrors.NewErrorExitPrintHelp(err, "Request to get internal application id failed") } req.SetBasicAuth(localConfig.User, localConfig.Token) @@ -150,7 +150,7 @@ func getInternalApplicationID(applicationID string) (string, error) { resp, err := client.Do(req) if err != nil { - return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an error communicating with Nexus IQ Server to get your internal application ID"} + return "", customerrors.NewErrorExitPrintHelp(err, "There was an error communicating with Nexus IQ Server to get your internal application ID") } //noinspection GoUnhandledErrorResult @@ -159,13 +159,13 @@ func getInternalApplicationID(applicationID string) (string, error) { if resp.StatusCode == http.StatusOK { bodyBytes, err := ioutil.ReadAll(resp.Body) if err != nil { - return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an error retrieving the bytes of the response for getting your internal application ID from Nexus IQ Server"} + return "", customerrors.NewErrorExitPrintHelp(err, "There was an error retrieving the bytes of the response for getting your internal application ID from Nexus IQ Server") } var response applicationResponse err = json.Unmarshal(bodyBytes, &response) if err != nil { - return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "failed to unmarshal response"} + return "", customerrors.NewErrorExitPrintHelp(err, "failed to unmarshal response") } if response.Applications != nil && len(response.Applications) > 0 { @@ -201,7 +201,7 @@ func submitToThirdPartyAPI(sbom string, internalID string) (string, error) { bytes.NewBuffer([]byte(sbom)), ) if err != nil { - return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Could not POST to Nexus iQ Third Party API"} + return "", customerrors.NewErrorExitPrintHelp(err, "Could not POST to Nexus iQ Third Party API") } req.SetBasicAuth(localConfig.User, localConfig.Token) @@ -210,7 +210,7 @@ func submitToThirdPartyAPI(sbom string, internalID string) (string, error) { resp, err := client.Do(req) if err != nil { - return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an issue communicating with the Nexus IQ Third Party API"} + return "", customerrors.NewErrorExitPrintHelp(err, "There was an issue communicating with the Nexus IQ Third Party API") } //noinspection GoUnhandledErrorResult @@ -220,13 +220,13 @@ func submitToThirdPartyAPI(sbom string, internalID string) (string, error) { bodyBytes, err := ioutil.ReadAll(resp.Body) LogLady.WithField("body", string(bodyBytes)).Info("Request accepted") if err != nil { - return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an issue submitting your sbom to the Nexus IQ Third Party API"} + return "", customerrors.NewErrorExitPrintHelp(err, "There was an issue submitting your sbom to the Nexus IQ Third Party API") } var response thirdPartyAPIResult err = json.Unmarshal(bodyBytes, &response) if err != nil { - return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Could not unmarshal response from iQ server"} + return "", customerrors.NewErrorExitPrintHelp(err, "Could not unmarshal response from iQ server") } return response.StatusURL, err } @@ -237,7 +237,7 @@ func submitToThirdPartyAPI(sbom string, internalID string) (string, error) { "status": resp.Status, }).Info("Request not accepted") if err != nil { - return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an issue submitting your sbom to the Nexus IQ Third Party API"} + return "", customerrors.NewErrorExitPrintHelp(err, "There was an issue submitting your sbom to the Nexus IQ Third Party API") } return "", err @@ -257,7 +257,7 @@ func pollIQServer(statusURL string, finished chan resultError, maxRetries int) e client := &http.Client{} req, err := http.NewRequest("GET", statusURL, nil) if err != nil { - return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Could not poll iQ server"} + return customerrors.NewErrorExitPrintHelp(err, "Could not poll iQ server") } req.SetBasicAuth(localConfig.User, localConfig.Token) @@ -268,7 +268,7 @@ func pollIQServer(statusURL string, finished chan resultError, maxRetries int) e if err != nil { finished <- resultError{finished: true, err: err} - return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an error polling Nexus IQ Server"} + return customerrors.NewErrorExitPrintHelp(err, "There was an error polling Nexus IQ Server") } //noinspection GoUnhandledErrorResult @@ -277,13 +277,13 @@ func pollIQServer(statusURL string, finished chan resultError, maxRetries int) e if resp.StatusCode == http.StatusOK { bodyBytes, err := ioutil.ReadAll(resp.Body) if err != nil { - return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "There was an error with processing the response from polling Nexus IQ Server"} + return customerrors.NewErrorExitPrintHelp(err, "There was an error with processing the response from polling Nexus IQ Server") } var response types.StatusURLResult err = json.Unmarshal(bodyBytes, &response) if err != nil { - return customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Could not unmarshal response from iQ server"} + return customerrors.NewErrorExitPrintHelp(err, "Could not unmarshal response from iQ server") } statusURLResp = response if response.IsError { diff --git a/main.go b/main.go index 412ddc9e..ada20e44 100644 --- a/main.go +++ b/main.go @@ -85,7 +85,9 @@ func doOssi(ossiArgs []string) (err error) { func doConfig(stdin io.Reader) (err error) { LogLady.Info("Nancy setting config via the command line") - err = configuration.GetConfigFromCommandLine(stdin) + if err = configuration.GetConfigFromCommandLine(stdin); err != nil { + err = customerrors.NewErrorExitPrintHelp(err, "Unable to set config for Nancy") + } return } @@ -315,10 +317,10 @@ func doCheckExistenceAndParse(config configuration.Configuration) error { } project, err := ctx.LoadProject() if err != nil { - return customerrors.ErrorExit{ExitCode: 3, Message: fmt.Sprintf("could not read lock at path %s", config.Path)} + return customerrors.NewErrorExitPrintHelp(err, fmt.Sprintf("could not read lock at path %s", config.Path)) } if project.Lock == nil { - return customerrors.ErrorExit{ExitCode: 3, Err: errors.New("dep failed to parse lock file and returned nil"), Message: "nancy could not continue due to dep failure"} + return customerrors.NewErrorExitPrintHelp(errors.New("dep failed to parse lock file and returned nil"), "nancy could not continue due to dep failure") } purls, invalidPurls := packages.ExtractPurlsUsingDep(project) @@ -347,7 +349,7 @@ func checkOSSIndex(purls []string, invalidpurls []string, config configuration.C var packageCount = len(purls) coordinates, err := ossindex.AuditPackagesWithOSSIndex(purls, &config) if err != nil { - return err + return customerrors.NewErrorExitPrintHelp(err, "Error auditing packages") } var invalidCoordinates []types.Coordinate @@ -367,13 +369,13 @@ func auditWithIQServer(purls []string, applicationID string, config configuratio LogLady.Debug("Sending purls to be Audited by IQ Server") res, err := iq.AuditPackages(purls, applicationID, config) if err != nil { - return err + return customerrors.NewErrorExitPrintHelp(err, "Uh oh! There was an error with your request to Nexus IQ Server") } fmt.Println() if res.IsError { LogLady.WithField("res", res).Error("An error occurred with the request to IQ Server") - return errors.New(res.ErrorMessage) + return customerrors.NewErrorExitPrintHelp(errors.New(res.ErrorMessage), "Uh oh! There was an error with your request to Nexus IQ Server") } if res.PolicyAction != "Failure" { diff --git a/main_test.go b/main_test.go index d349ad49..424ae041 100644 --- a/main_test.go +++ b/main_test.go @@ -111,9 +111,14 @@ func TestDoConfigInvalidHomeDir(t *testing.T) { stdin.Write([]byte("ossindex\nmyOssiUsername\nmyOssiToken\n")) err := doConfig(&stdin) assert.Error(t, err) - if exiterr, ok := err.(*os.PathError); ok { - assert.Equal(t, "mkdir", exiterr.Op) - assert.Equal(t, "/no/such/dir/.ossindex", exiterr.Path) + if exiterr, ok := err.(customerrors.ErrorExit); ok { + assert.Equal(t, "Unable to set config for Nancy", exiterr.Message) + if errCause, ok := exiterr.Err.(*os.PathError); ok { + assert.Equal(t, "mkdir", errCause.Op) + assert.Equal(t, "/no/such/dir/.ossindex", errCause.Path) + } else { + t.Fail() + } } else { t.Fail() } diff --git a/ossindex/internal/cache/cache.go b/ossindex/internal/cache/cache.go index 96587d51..003c49ae 100644 --- a/ossindex/internal/cache/cache.go +++ b/ossindex/internal/cache/cache.go @@ -50,7 +50,7 @@ type DBValue struct { func (c *Cache) getDatabasePath() (dbDir string, err error) { usr, err := user.Current() if err != nil { - return "", customerrors.ErrorExit{ExitCode: 3, Err: err, Message: "Error getting user home"} + return "", customerrors.NewErrorExitPrintHelp(err, "Error getting user home") } return path.Join(usr.HomeDir, types.OssIndexDirName, dbDirName, c.DBName), err diff --git a/ossindex/ossindex.go b/ossindex/ossindex.go index 867b3df5..f8f47a48 100644 --- a/ossindex/ossindex.go +++ b/ossindex/ossindex.go @@ -21,6 +21,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/sonatype-nexus-community/nancy/customerrors" "io/ioutil" "net/http" "time" @@ -83,7 +84,7 @@ func AuditPackagesWithOSSIndex(purls []string, config *configuration.Configurati func doAuditPackages(purls []string, config *configuration.Configuration) ([]types.Coordinate, error) { newPurls, results, err := dbCache.GetCacheValues(purls) if err != nil { - return nil, err + return nil, customerrors.NewErrorExitPrintHelp(err, "Error initializing cache") } chunks := chunk(newPurls, MaxCoords) diff --git a/packages/mod.go b/packages/mod.go index 3f8d7ecb..8d474410 100644 --- a/packages/mod.go +++ b/packages/mod.go @@ -62,7 +62,7 @@ func (m Mod) ExtractPurlsFromManifestForIQ() (purls []string) { func (m Mod) CheckExistenceOfManifest() (bool, error) { if _, err := os.Stat(m.GoSumPath); os.IsNotExist(err) { - return false, customerrors.ErrorExit{ExitCode: 3, Err: err, Message: fmt.Sprint("No go.sum found at path: " + m.GoSumPath)} + return false, customerrors.NewErrorExitPrintHelp(err, fmt.Sprint("No go.sum found at path: "+m.GoSumPath)) } return true, nil } From 40136f89cc72fea0f94c01d43c5abd873ce3b019 Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Tue, 21 Apr 2020 17:21:12 -0400 Subject: [PATCH 09/13] try to detect cause of intermittent logger test failures. --- logger/logger_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/logger/logger_test.go b/logger/logger_test.go index 80a2bf7e..ebdc54f9 100644 --- a/logger/logger_test.go +++ b/logger/logger_test.go @@ -28,7 +28,8 @@ import ( ) func TestLogger(t *testing.T) { - location, _ := LogFileLocation() + location, err := LogFileLocation() + assert.NoError(t, err) if !strings.Contains(location, TestLogfilename) { t.Errorf("Nancy test file not in log file location. args: %+v", os.Args) } From f369b29cac1ad4f99b9f906218e0ef98d43ade2c Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Wed, 22 Apr 2020 10:52:52 -0400 Subject: [PATCH 10/13] change log message to match exit with error code --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index ada20e44..a021bfaa 100644 --- a/main.go +++ b/main.go @@ -223,7 +223,7 @@ func processIQConfig(config configuration.IqConfiguration) (err error) { } if config.Application == "" { - LogLady.Info("No application specified, printing usage and exiting clean") + LogLady.Info("No application specified, printing usage and exiting with error") flag.Usage() err = customerrors.ErrorExit{ExitCode: 2} return From 1036360a7e2b490d2920e38200bf629f84e107cc Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Wed, 22 Apr 2020 11:02:07 -0400 Subject: [PATCH 11/13] show the user more info in the console about missing IQ app id. --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index a021bfaa..d884fab9 100644 --- a/main.go +++ b/main.go @@ -225,7 +225,7 @@ func processIQConfig(config configuration.IqConfiguration) (err error) { if config.Application == "" { LogLady.Info("No application specified, printing usage and exiting with error") flag.Usage() - err = customerrors.ErrorExit{ExitCode: 2} + err = customerrors.NewErrorExitPrintHelp(fmt.Errorf("no IQ application id specified"), "Missing IQ application ID") return } From 7a019532f9c19a8eb03034e560c99b506b09ba6b Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Wed, 22 Apr 2020 11:09:04 -0400 Subject: [PATCH 12/13] tests doing their job --- main_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main_test.go b/main_test.go index 424ae041..97470fce 100644 --- a/main_test.go +++ b/main_test.go @@ -66,7 +66,7 @@ func TestProcessIQConfigApplicationMissing(t *testing.T) { err := processIQConfig(configuration.IqConfiguration{}) assert.Error(t, err) if exiterr, ok := err.(customerrors.ErrorExit); ok { - assert.Equal(t, 2, exiterr.ExitCode) + assert.Equal(t, 3, exiterr.ExitCode) } else { t.Fail() } From a119470ca235e438748926dac714aaa20556c62c Mon Sep 17 00:00:00 2001 From: Dan Rollo Date: Wed, 22 Apr 2020 11:12:53 -0400 Subject: [PATCH 13/13] tests doing their job --- main_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main_test.go b/main_test.go index 97470fce..28e036b9 100644 --- a/main_test.go +++ b/main_test.go @@ -77,7 +77,7 @@ func TestDoIqExitError(t *testing.T) { err := doIq([]string{"server-url"}) assert.Error(t, err) if exiterr, ok := err.(customerrors.ErrorExit); ok { - assert.Equal(t, 2, exiterr.ExitCode) + assert.Equal(t, 3, exiterr.ExitCode) } else { t.Fail() }