Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package main

import (
"os"

"github.com/saferwall/cli/cmd"
)

Expand All @@ -16,5 +18,7 @@ var (

func main() {
cmd.SetVersionInfo(version, commit, date)
cmd.Execute()
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}
37 changes: 16 additions & 21 deletions cmd/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ package cmd

import (
"fmt"
"log"
"os"
"path/filepath"
"strings"

tea "github.com/charmbracelet/bubbletea"
Expand All @@ -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(&parallelFlag, "parallel", "p", 4,
"Number of files to download in parallel")
downloadCmd.Flags().BoolVarP(&extractFlag, "extract", "x", false,
Expand All @@ -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
Expand All @@ -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
}
8 changes: 3 additions & 5 deletions cmd/rescan.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package cmd

import (
"fmt"
"log"
"regexp"
"strings"

Expand Down Expand Up @@ -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]

Expand All @@ -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)
Expand All @@ -65,6 +63,6 @@ var reScanCmd = &cobra.Command{
}
}

reScanFile(webSvc, sha256List, cfg.Credentials.APIKey)
return reScanFile(webSvc, sha256List, cfg.Credentials.APIKey)
},
}
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
30 changes: 13 additions & 17 deletions cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
},
}
16 changes: 14 additions & 2 deletions cmd/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions cmd/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package cmd

import (
"fmt"
"log"
"strings"
"time"

Expand All @@ -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
},
}

Expand Down
9 changes: 4 additions & 5 deletions cmd/souk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
},
}

Expand All @@ -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)
},
}

Expand Down
6 changes: 3 additions & 3 deletions cmd/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package cmd

import (
"fmt"
"log"
"sort"
"strings"
"time"
Expand All @@ -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
},
}

Expand Down
22 changes: 1 addition & 21 deletions internal/util/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down