diff --git a/cli.go b/cli.go index a8a54f1..c5cc795 100755 --- a/cli.go +++ b/cli.go @@ -5,6 +5,8 @@ package main import ( + "os" + "github.com/saferwall/cli/cmd" ) @@ -16,5 +18,7 @@ var ( func main() { cmd.SetVersionInfo(version, commit, date) - cmd.Execute() + if err := cmd.Execute(); err != nil { + os.Exit(1) + } } diff --git a/cmd/download.go b/cmd/download.go index 328240a..85a359a 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -6,9 +6,6 @@ package cmd import ( "fmt" - "log" - "os" - "path/filepath" "strings" tea "github.com/charmbracelet/bubbletea" @@ -21,13 +18,8 @@ var outputFlag string var extractFlag bool func init() { - ex, err := os.Executable() - if err != nil { - panic(err) - } - - downloadCmd.Flags().StringVarP(&outputFlag, "output", "o", filepath.Dir(ex), - "Destination directory where to save samples. (default=current dir)") + downloadCmd.Flags().StringVarP(&outputFlag, "output", "o", ".", + "Destination directory where to save samples") downloadCmd.Flags().IntVarP(¶llelFlag, "parallel", "p", 4, "Number of files to download in parallel") downloadCmd.Flags().BoolVarP(&extractFlag, "extract", "x", false, @@ -39,30 +31,33 @@ var downloadCmd = &cobra.Command{ Short: "Download a sample (and its artifacts)", Long: `Download a binary sample given a SHA256 hash, or a batch of samples from a text file containing one hash per line.`, Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { arg := args[0] webSvc := webapi.New(cfg.Credentials.URL) - hashes := collectHashes(arg) + hashes, err := collectHashes(arg) + if err != nil { + return err + } if len(hashes) == 0 { - log.Fatalf("no valid SHA256 hashes found in %q", arg) + return fmt.Errorf("no valid SHA256 hashes found in %q", arg) } - downloadFiles(webSvc, cfg.Credentials.APIKey, hashes) + return downloadFiles(webSvc, cfg.Credentials.APIKey, hashes) }, } // collectHashes returns a list of SHA256 hashes from the argument. // If arg is a SHA256 hash, it returns a single-element slice. // Otherwise it treats arg as a file path and reads hashes from it. -func collectHashes(arg string) []string { +func collectHashes(arg string) ([]string, error) { if sha256Re.MatchString(arg) { - return []string{arg} + return []string{arg}, nil } data, err := util.ReadAll(arg) if err != nil { - log.Fatalf("failed to read SHA256 hashes from file: %s", arg) + return nil, fmt.Errorf("failed to read SHA256 hashes from file %s: %w", arg, err) } var hashes []string @@ -72,14 +67,14 @@ func collectHashes(arg string) []string { hashes = append(hashes, line) } } - return hashes + return hashes, nil } -func downloadFiles(web webapi.Service, token string, hashes []string) { +func downloadFiles(web webapi.Service, token string, hashes []string) error { model := newDownloadModel(hashes, web, token, outputFlag, parallelFlag, extractFlag) p := tea.NewProgram(model) if _, err := p.Run(); err != nil { - fmt.Fprintf(os.Stderr, "TUI error: %v\n", err) - os.Exit(1) + return fmt.Errorf("TUI error: %w", err) } + return nil } diff --git a/cmd/rescan.go b/cmd/rescan.go index ddd29b2..01742f9 100644 --- a/cmd/rescan.go +++ b/cmd/rescan.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" - "log" "regexp" "strings" @@ -44,8 +43,7 @@ var reScanCmd = &cobra.Command{ Short: "Rescan an existing file using its hash", Long: `Rescans one or more files. Pass a SHA256 hash to rescan a single file, or a path to a text file with one hash per line to rescan in batch.`, Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - + RunE: func(cmd *cobra.Command, args []string) error { webSvc := webapi.New(cfg.Credentials.URL) arg := args[0] @@ -55,7 +53,7 @@ var reScanCmd = &cobra.Command{ } else { data, err := util.ReadAll(arg) if err != nil { - log.Fatalf("failed to read file: %s", arg) + return fmt.Errorf("failed to read file %s: %w", arg, err) } for _, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(line) @@ -65,6 +63,6 @@ var reScanCmd = &cobra.Command{ } } - reScanFile(webSvc, sha256List, cfg.Credentials.APIKey) + return reScanFile(webSvc, sha256List, cfg.Credentials.APIKey) }, } diff --git a/cmd/root.go b/cmd/root.go index 8f33ff3..e22f18a 100755 --- a/cmd/root.go +++ b/cmd/root.go @@ -35,6 +35,8 @@ For more details see the github repo at https://github.com/saferwall Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, + // Runtime errors are reported by Execute; don't dump usage on top of them. + SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // Skip config loading for the init command. if cmd.Name() == "init" { diff --git a/cmd/scan.go b/cmd/scan.go index 8325827..65f4519 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -6,13 +6,12 @@ package cmd import ( "fmt" - "log" "os" - "path/filepath" "time" tea "github.com/charmbracelet/bubbletea" "github.com/saferwall/cli/internal/entity" + "github.com/saferwall/cli/internal/util" "github.com/saferwall/cli/internal/webapi" "github.com/spf13/cobra" ) @@ -94,20 +93,18 @@ func buildScanSummary(file entity.File) scanSummary { // scanFile scans an individual file or a directory. func scanFile(web webapi.Service, filePath, token string) error { - _, err := os.Stat(filePath) - if os.IsNotExist(err) { - log.Printf("file path [%s] does not exists", filePath) - return err + if _, err := os.Stat(filePath); err != nil { + return fmt.Errorf("cannot access %s: %w", filePath, err) } - // Walk over directory. - fileList := []string{} - filepath.Walk(filePath, func(path string, f os.FileInfo, err error) error { - if !f.IsDir() { - fileList = append(fileList, path) - } - return nil - }) + // Walk over the file or directory. + fileList, err := util.WalkAllFilesInDir(filePath) + if err != nil { + return fmt.Errorf("failed walking %s: %w", filePath, err) + } + if len(fileList) == 0 { + return fmt.Errorf("no files found in %s", filePath) + } // Launch TUI scan with the configured parallelism. model := newScanModel(fileList, web, token, parallelFlag) @@ -123,9 +120,8 @@ var scanCmd = &cobra.Command{ Short: "Submit a scan request of a file using its hash", Long: `Scans the file`, Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - + RunE: func(cmd *cobra.Command, args []string) error { webSvc := webapi.New(cfg.Credentials.URL) - scanFile(webSvc, args[0], cfg.Credentials.APIKey) + return scanFile(webSvc, args[0], cfg.Credentials.APIKey) }, } diff --git a/cmd/scan_test.go b/cmd/scan_test.go index 6209ded..7060737 100644 --- a/cmd/scan_test.go +++ b/cmd/scan_test.go @@ -92,12 +92,21 @@ func TestSha256Re(t *testing.T) { } func TestCollectHashesSingleHash(t *testing.T) { - got := collectHashes(testHash) + got, err := collectHashes(testHash) + if err != nil { + t.Fatalf("collectHashes() error = %v", err) + } if !reflect.DeepEqual(got, []string{testHash}) { t.Errorf("collectHashes() = %v, want [%s]", got, testHash) } } +func TestCollectHashesMissingFile(t *testing.T) { + if _, err := collectHashes(filepath.Join(t.TempDir(), "nope.txt")); err == nil { + t.Error("collectHashes() expected error for unreadable file, got nil") + } +} + func TestCollectHashesFromFile(t *testing.T) { other := "0000000000000000000000000000000000000000000000000000000000000000" path := filepath.Join(t.TempDir(), "hashes.txt") @@ -109,7 +118,10 @@ func TestCollectHashesFromFile(t *testing.T) { t.Fatal(err) } - got := collectHashes(path) + got, err := collectHashes(path) + if err != nil { + t.Fatalf("collectHashes() error = %v", err) + } want := []string{testHash, other} if !reflect.DeepEqual(got, want) { t.Errorf("collectHashes() = %v, want %v", got, want) diff --git a/cmd/search.go b/cmd/search.go index aed663e..603a007 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" - "log" "strings" "time" @@ -30,13 +29,14 @@ Examples: saferwall-cli search 'fs>2026 and tag=upx' --per-page 50 saferwall-cli search 'extension=sys and positives>=10' --page 2`, Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { webSvc := webapi.New(cfg.Credentials.URL) result, err := webSvc.SearchFiles(args[0], cfg.Credentials.APIKey, searchPage, searchPerPage) if err != nil { - log.Fatalf("search failed: %v", err) + return err } printSearchResults(result, searchPage, searchPerPage) + return nil }, } diff --git a/cmd/souk.go b/cmd/souk.go index d806ee2..ebe5f4e 100755 --- a/cmd/souk.go +++ b/cmd/souk.go @@ -36,9 +36,8 @@ var genCmd = &cobra.Command{ Short: "Generate malware souk markdown for the entire corpus", Long: `Generates markdown source code for the entire corpus of saferwall's malware souk database`, - Run: func(cmd *cobra.Command, args []string) { - - generateMalwareSoukDB() + RunE: func(cmd *cobra.Command, args []string) error { + return generateMalwareSoukDB() }, } @@ -47,9 +46,9 @@ var addCmd = &cobra.Command{ Short: "Add a new malware family to the malware souk database", Long: `Generates markdown source code for a new malware family for saferwall's malware souk database`, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { familyYamlPath := filepath.Join(soukFlag, familyYamlFlag) - addFamilyToSouk(familyYamlPath) + return addFamilyToSouk(familyYamlPath) }, } diff --git a/cmd/view.go b/cmd/view.go index f1efc1c..ca0921d 100644 --- a/cmd/view.go +++ b/cmd/view.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" - "log" "sort" "strings" "time" @@ -22,16 +21,17 @@ var viewCmd = &cobra.Command{ Short: "View scan results for a file by its SHA256 hash", Long: `Fetches and displays the scan results summary for a file, including AV detections.`, Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { sha256 := strings.ToLower(args[0]) webSvc := webapi.New(cfg.Credentials.URL) var file entity.File if err := webSvc.GetFile(sha256, &file); err != nil { - log.Fatalf("failed to get file: %v", err) + return fmt.Errorf("failed to get file: %w", err) } printFileReport(file, webSvc) + return nil }, } diff --git a/internal/util/utils.go b/internal/util/utils.go index d99edba..bb7a062 100755 --- a/internal/util/utils.go +++ b/internal/util/utils.go @@ -22,27 +22,7 @@ func GetSha256(b []byte) string { // ReadAll reads the entire file into memory. func ReadAll(filePath string) ([]byte, error) { - // Start by getting a file descriptor over the file - file, err := os.Open(filePath) - if err != nil { - return nil, err - } - defer file.Close() - - // Get the file size to know how much we need to allocate - fileinfo, err := file.Stat() - if err != nil { - return nil, err - } - filesize := fileinfo.Size() - buffer := make([]byte, filesize) - - // Read the whole binary - _, err = file.Read(buffer) - if err != nil { - return nil, err - } - return buffer, nil + return os.ReadFile(filePath) } // WriteBytesFile write Bytes to a File.