Skip to content

Commit ebbc620

Browse files
authored
feat: support downloading files in async, better TUI, extract flag (#15)
Co-authored-by: Ayoub Faouzi <ayoubfaouzi@users.noreply.github.com>
1 parent fb0237d commit ebbc620

6 files changed

Lines changed: 305 additions & 58 deletions

File tree

.golangci.yaml

Lines changed: 0 additions & 13 deletions
This file was deleted.

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,15 @@ saferwall-cli rescan <sha256>
5757

5858
### Download
5959

60-
Download files by their SHA256 hash. You can also download a batch of samples from a text file.
60+
Download a sample by its SHA256 hash, or provide a text file with one hash per line to download in batch.
6161

6262
```sh
63-
saferwall-cli download --hash <sha256>
63+
# Single sample
64+
saferwall-cli download <sha256>
65+
66+
# Batch from a text file
67+
saferwall-cli download hashes.txt
68+
69+
# Extract from zip (password: infected) instead of keeping the .zip
70+
saferwall-cli download -x <sha256>
6471
```

cmd/download.go

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,88 +5,87 @@
55
package cmd
66

77
import (
8-
"bytes"
8+
"fmt"
99
"log"
1010
"os"
1111
"path/filepath"
1212
"strings"
1313

14+
tea "github.com/charmbracelet/bubbletea"
1415
"github.com/saferwall/cli/internal/util"
1516
"github.com/saferwall/cli/internal/webapi"
1617
"github.com/spf13/cobra"
1718
)
1819

19-
var sha256Flag string
20-
var txtFlag string
2120
var outputFlag string
21+
var extractFlag bool
2222

2323
func init() {
2424
ex, err := os.Executable()
2525
if err != nil {
2626
panic(err)
2727
}
2828

29-
downloadCmd.Flags().StringVarP(&sha256Flag, "hash", "s", "", "SHA256 hash to download")
30-
downloadCmd.Flags().StringVarP(&txtFlag, "txt", "t", "", "Download all hashes in a text file, separate by a line break")
3129
downloadCmd.Flags().StringVarP(&outputFlag, "output", "o", filepath.Dir(ex),
3230
"Destination directory where to save samples. (default=current dir)")
31+
downloadCmd.Flags().IntVarP(&parallelFlag, "parallel", "p", 4,
32+
"Number of files to download in parallel")
33+
downloadCmd.Flags().BoolVarP(&extractFlag, "extract", "x", false,
34+
"Extract samples from zip (password: infected)")
3335
}
3436

3537
var downloadCmd = &cobra.Command{
36-
Use: "download",
37-
Short: "Download a sample(s)",
38-
Long: `Download a binary sample given a sha256`,
38+
Use: "download <sha256|file.txt>",
39+
Short: "Download a sample (and its artifacts)",
40+
Long: `Download a binary sample given a SHA256 hash, or a batch of samples from a text file containing one hash per line.`,
41+
Args: cobra.ExactArgs(1),
3942
Run: func(cmd *cobra.Command, args []string) {
43+
arg := args[0]
4044

41-
// Login to saferwall web service
45+
// Login to saferwall web service.
4246
webSvc := webapi.New(cfg.Credentials.URL)
4347
token, err := webSvc.Login(cfg.Credentials.Username, cfg.Credentials.Password)
4448
if err != nil {
4549
log.Fatalf("failed to login to saferwall web service")
4650
}
4751

48-
// download a single binary.
49-
if sha256Flag != "" {
50-
download(sha256Flag, token, webSvc)
51-
} else if txtFlag != "" {
52-
// Download a list of sha256 hashes.
53-
data, err := util.ReadAll(txtFlag)
54-
if err != nil {
55-
log.Fatalf("failed to read to SHA256 hashes from txt file: %v", txtFlag)
56-
}
57-
58-
sha256list := strings.Split(string(data), "\n")
59-
for _, sha256 := range sha256list {
60-
if len(sha256) >= 64 {
61-
err = download(sha256, token, webSvc)
62-
if err != nil {
63-
log.Fatalf("failed to download sample (%s): %v", sha256, err)
64-
}
65-
}
66-
}
52+
hashes := collectHashes(arg)
53+
if len(hashes) == 0 {
54+
log.Fatalf("no valid SHA256 hashes found in %q", arg)
6755
}
56+
57+
downloadFiles(webSvc, token, hashes)
6858
},
6959
}
7060

71-
func download(sha256, token string, web webapi.Service) error {
72-
var err error
73-
var data bytes.Buffer
74-
var destPath string
61+
// collectHashes returns a list of SHA256 hashes from the argument.
62+
// If arg is a SHA256 hash, it returns a single-element slice.
63+
// Otherwise it treats arg as a file path and reads hashes from it.
64+
func collectHashes(arg string) []string {
65+
if sha256Re.MatchString(arg) {
66+
return []string{arg}
67+
}
7568

76-
log.Printf("downloading %s to %s", sha256, outputFlag)
77-
dataContent, err := web.Download(sha256, token)
69+
data, err := util.ReadAll(arg)
7870
if err != nil {
79-
log.Fatalf("failed to download %s, err: %v", sha256, err)
80-
return err
71+
log.Fatalf("failed to read SHA256 hashes from file: %s", arg)
8172
}
82-
data = *dataContent
8373

84-
filename := sha256 + ".zip"
85-
destPath = filepath.Join(outputFlag, filename)
86-
_, err = util.WriteBytesFile(destPath, &data)
87-
if err != nil {
88-
return err
74+
var hashes []string
75+
for _, line := range strings.Split(string(data), "\n") {
76+
line = strings.TrimSpace(line)
77+
if sha256Re.MatchString(line) {
78+
hashes = append(hashes, line)
79+
}
8980
}
81+
return hashes
82+
}
9083

91-
return nil
84+
func downloadFiles(web webapi.Service, token string, hashes []string) {
85+
model := newDownloadModel(hashes, web, token, outputFlag, parallelFlag, extractFlag)
86+
p := tea.NewProgram(model)
87+
if _, err := p.Run(); err != nil {
88+
fmt.Fprintf(os.Stderr, "TUI error: %v\n", err)
89+
os.Exit(1)
90+
}
9291
}

0 commit comments

Comments
 (0)