Skip to content

Commit 9d357a0

Browse files
authored
feat: make scan command look pretty (#12)
Co-authored-by: Ayoub Faouzi <ayoubfaouzi@users.noreply.github.com>
1 parent 5919c9d commit 9d357a0

11 files changed

Lines changed: 545 additions & 182 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,8 @@
11
repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v4.2.0
3+
rev: v6.0.0
44
hooks:
55
- id: trailing-whitespace
66
- id: end-of-file-fixer
77
- id: check-yaml
88
- id: check-added-large-files
9-
- repo: https://github.com/dnephin/pre-commit-golang
10-
rev: master
11-
hooks:
12-
- id: go-fmt
13-
- id: go-vet
14-
- id: go-lint
15-
- id: go-imports
16-
- id: go-cyclo
17-
args: [-over=15]
18-
- id: validate-toml
19-
- id: no-go-testing
20-
- id: golangci-lint
21-
- id: go-critic
22-
- id: go-unit-tests
23-
- id: go-build
24-
- id: go-mod-tidy

cmd/rescan.go

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,27 @@ package cmd
66

77
import (
88
"log"
9-
"runtime"
109
"strings"
10+
"sync"
1111
"time"
1212

13-
"github.com/gammazero/workerpool"
1413
"github.com/saferwall/cli/internal/util"
1514
"github.com/saferwall/cli/internal/webapi"
1615
"github.com/spf13/cobra"
1716
)
1817

1918
var (
20-
fileHash string
19+
rescanFilePath string
20+
fileHash string
2121
)
2222

2323
func init() {
24-
reScanCmd.Flags().StringVarP(&filePath, "path", "p", "",
24+
reScanCmd.Flags().StringVarP(&rescanFilePath, "path", "p", "",
2525
"File name or path containing list of SHA256 to scan")
2626
reScanCmd.Flags().StringVarP(&fileHash, "hash", "s", "",
2727
"SHA256 of the file to rescan")
28-
reScanCmd.Flags().BoolVarP(&asyncScanFlag, "async", "a", false,
29-
"Scan files in parallel")
28+
reScanCmd.Flags().IntVar(&parallelFlag, "parallel", 1,
29+
"Number of files to rescan in parallel")
3030
reScanCmd.Flags().BoolVarP(&enableDetonationFlag, "enableDetonation", "d", false,
3131
"Skip detonation")
3232
reScanCmd.Flags().IntVarP(&timeoutFlag, "timeout", "t", 15,
@@ -37,42 +37,23 @@ func init() {
3737

3838
// reScanFile re-scans a list of SHA256.
3939
func reScanFile(web webapi.Service, shaList []string, token string) error {
40+
sem := make(chan struct{}, parallelFlag)
41+
var wg sync.WaitGroup
4042

41-
if asyncScanFlag {
42-
// Create a worker pool
43-
maxWorkers := runtime.GOMAXPROCS(0)
44-
wp := workerpool.New(maxWorkers)
45-
46-
for _, sha256 := range shaList {
47-
wp.Submit(func() {
48-
log.Printf("rescanning %s", sha256)
49-
err := web.Rescan(sha256, token, osFlag, enableDetonationFlag, timeoutFlag)
50-
if err != nil {
51-
log.Fatalf("failed to rescan file: %v", sha256)
52-
}
53-
54-
time.Sleep(2 * time.Second)
55-
})
56-
}
57-
wp.StopWait()
58-
return nil
59-
}
60-
61-
// Sequentially scan the files.
6243
for _, sha256 := range shaList {
63-
64-
log.Printf("re-scanning %s", sha256)
65-
err := web.Rescan(sha256, token, osFlag, enableDetonationFlag, timeoutFlag)
66-
if err != nil {
67-
log.Fatalf("failed to rescan file: %v", sha256)
68-
}
69-
70-
if len(shaList) > 1 {
71-
time.Sleep(10 * time.Second)
72-
}
73-
44+
sem <- struct{}{}
45+
wg.Add(1)
46+
go func() {
47+
defer func() { <-sem; wg.Done() }()
48+
log.Printf("rescanning %s", sha256)
49+
err := web.Rescan(sha256, token, osFlag, enableDetonationFlag, timeoutFlag)
50+
if err != nil {
51+
log.Printf("failed to rescan file: %v", sha256)
52+
}
53+
time.Sleep(2 * time.Second)
54+
}()
7455
}
75-
56+
wg.Wait()
7657
return nil
7758
}
7859

@@ -91,8 +72,8 @@ var reScanCmd = &cobra.Command{
9172

9273
// Read the txt file containing the list of hashes to rescan.
9374
var sha256List []string
94-
if filePath != "" {
95-
data, err := util.ReadAll(filePath)
75+
if rescanFilePath != "" {
76+
data, err := util.ReadAll(rescanFilePath)
9677
if err != nil {
9778
log.Fatalf("failed to read txt file")
9879
}

cmd/root.go

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package cmd
66

77
import (
8-
"fmt"
98
"log"
109
"path/filepath"
1110

@@ -14,10 +13,6 @@ import (
1413
"github.com/spf13/cobra"
1514
)
1615

17-
const (
18-
version = "0.5.0"
19-
)
20-
2116
var cfg config.Config
2217

2318
var rootCmd = &cobra.Command{
@@ -38,16 +33,7 @@ upload, scan samples from your drive, or download samples.
3833
For more details see the github repo at https://github.com/saferwall
3934
`,
4035
Run: func(cmd *cobra.Command, args []string) {
41-
fmt.Printf("You are using version %s\n", version)
42-
},
43-
}
44-
45-
var versionCmd = &cobra.Command{
46-
Use: "version",
47-
Short: "Print the version number",
48-
Long: "Print the version number",
49-
Run: func(cmd *cobra.Command, args []string) {
50-
fmt.Printf("You are using version %s\n", version)
36+
cmd.Help()
5137
},
5238
}
5339

cmd/scan.go

Lines changed: 58 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -5,46 +5,85 @@
55
package cmd
66

77
import (
8+
"fmt"
89
"log"
910
"os"
1011
"path/filepath"
11-
"runtime"
1212
"time"
1313

14-
"github.com/gammazero/workerpool"
15-
"github.com/saferwall/cli/internal/util"
14+
tea "github.com/charmbracelet/bubbletea"
15+
"github.com/saferwall/cli/internal/entity"
1616
"github.com/saferwall/cli/internal/webapi"
1717
"github.com/spf13/cobra"
1818
)
1919

20+
const (
21+
statusQueued = 1
22+
statusScanning = 2
23+
statusCompleted = 3
24+
25+
pollInterval = 5 * time.Second
26+
)
27+
2028
// Used for flags.
21-
var filePath string
2229
var forceRescanFlag bool
23-
var asyncScanFlag bool
30+
var parallelFlag int
2431
var enableDetonationFlag bool
2532
var timeoutFlag int
2633
var osFlag string
2734

2835
func init() {
29-
scanCmd.Flags().StringVarP(&filePath, "path", "p", "",
30-
"File name or path to scan (required)")
3136
scanCmd.Flags().BoolVarP(&forceRescanFlag, "force", "f", false,
3237
"Force rescan the file if it exists")
33-
scanCmd.Flags().BoolVarP(&asyncScanFlag, "async", "a", false,
34-
"Scan files in parallel")
38+
scanCmd.Flags().IntVarP(&parallelFlag, "parallel", "p", 1,
39+
"Number of files to scan in parallel")
3540
scanCmd.Flags().BoolVarP(&enableDetonationFlag, "enableDetonation", "d", false,
3641
"Skip detonation")
3742
scanCmd.Flags().IntVarP(&timeoutFlag, "timeout", "t", 15,
3843
"Detonation duration in seconds")
3944
scanCmd.Flags().StringVarP(&osFlag, "os", "o", "win-10",
4045
"Preferred OS for detonation, choice(win-7 | win-10)")
41-
scanCmd.MarkFlagRequired("path")
46+
}
47+
48+
type scanSummary struct {
49+
SHA256 string `json:"sha256"`
50+
Classification string `json:"classification"`
51+
FileFormat string `json:"file_format"`
52+
FileExtension string `json:"file_extension"`
53+
MultiAV *avSummary `json:"multiav,omitempty"`
54+
}
55+
56+
type avSummary struct {
57+
Positives int `json:"positives"`
58+
EnginesCount int `json:"engines_count"`
59+
}
4260

61+
func buildScanSummary(file entity.File) scanSummary {
62+
s := scanSummary{
63+
SHA256: file.SHA256,
64+
Classification: file.Classification,
65+
FileFormat: file.Format,
66+
FileExtension: file.Extension,
67+
}
68+
69+
if lastScan, ok := file.MultiAV["last_scan"].(map[string]any); ok {
70+
if stats, ok := lastScan["stats"].(map[string]any); ok {
71+
av := &avSummary{}
72+
if v, ok := stats["positives"].(float64); ok {
73+
av.Positives = int(v)
74+
}
75+
if v, ok := stats["engines_count"].(float64); ok {
76+
av.EnginesCount = int(v)
77+
}
78+
s.MultiAV = av
79+
}
80+
}
81+
82+
return s
4383
}
4484

4585
// scanFile scans an individual file or a directory.
4686
func scanFile(web webapi.Service, filePath, token string) error {
47-
4887
_, err := os.Stat(filePath)
4988
if os.IsNotExist(err) {
5089
log.Printf("file path [%s] does not exists", filePath)
@@ -60,98 +99,20 @@ func scanFile(web webapi.Service, filePath, token string) error {
6099
return nil
61100
})
62101

63-
if asyncScanFlag {
64-
65-
// Create a worker pool
66-
maxWorkers := runtime.GOMAXPROCS(0)
67-
wp := workerpool.New(maxWorkers)
68-
69-
// Upload files
70-
for _, filename := range fileList {
71-
filename := filename
72-
wp.Submit(func() {
73-
74-
// Get sha256
75-
data, err := os.ReadFile(filename)
76-
if err != nil {
77-
log.Fatalf("failed to read file: %v", filename)
78-
}
79-
sha256 := util.GetSha256(data)
80-
81-
// Check if we the file exists in the DB.
82-
exists, err := web.FileExists(sha256)
83-
if err != nil {
84-
log.Fatalf("failed to check existence of file: %v", filename)
85-
}
86-
87-
// Upload the file to be scanned, this will automatically trigger a scan request.
88-
if !exists {
89-
_, err = web.Scan(filename, token, osFlag, enableDetonationFlag, timeoutFlag)
90-
if err != nil {
91-
log.Fatalf("failed to upload file: %v", filename)
92-
}
93-
} else {
94-
// Force rescan the file
95-
if forceRescanFlag {
96-
err = web.Rescan(sha256, token, osFlag, enableDetonationFlag, timeoutFlag)
97-
if err != nil {
98-
log.Fatalf("failed to rescan file: %v", filename)
99-
}
100-
}
101-
}
102-
103-
time.Sleep(2 * time.Second)
104-
})
105-
}
106-
wp.StopWait()
107-
return nil
108-
}
109-
110-
// Sequentially scan the files.
111-
for _, filename := range fileList {
112-
// Get sha256
113-
data, err := os.ReadFile(filename)
114-
if err != nil {
115-
log.Fatalf("failed to read file: %v", filename)
116-
}
117-
sha256 := util.GetSha256(data)
118-
119-
log.Printf("processing %s", sha256)
120-
121-
// Check if we the file exists in the DB.
122-
exists, err := web.FileExists(sha256)
123-
if err != nil {
124-
log.Fatalf("failed to check existence of file: %s, error: %v", filename, err)
125-
}
126-
127-
// Upload the file to be scanned, this will automatically
128-
// trigger a scan request.
129-
if !exists {
130-
body, err := web.Scan(filename, token, osFlag, enableDetonationFlag, timeoutFlag)
131-
if err != nil {
132-
log.Fatalf("failed to upload file: %s, error: %v", filename, err)
133-
}
134-
log.Print(body)
135-
time.Sleep(10 * time.Second)
136-
} else {
137-
// Force re-scan the file
138-
if forceRescanFlag {
139-
err = web.Rescan(sha256, token, osFlag, enableDetonationFlag, timeoutFlag)
140-
if err != nil {
141-
log.Fatalf("failed to re-scan file: %v", filename)
142-
}
143-
}
144-
}
145-
102+
// Launch TUI scan with the configured parallelism.
103+
model := newScanModel(fileList, web, token, parallelFlag)
104+
p := tea.NewProgram(model)
105+
if _, err := p.Run(); err != nil {
106+
return fmt.Errorf("TUI error: %w", err)
146107
}
147-
148108
return nil
149109
}
150110

151111
var scanCmd = &cobra.Command{
152-
Use: "scan",
112+
Use: "scan <path>",
153113
Short: "Submit a scan request of a file using its hash",
154114
Long: `Scans the file`,
115+
Args: cobra.ExactArgs(1),
155116
Run: func(cmd *cobra.Command, args []string) {
156117

157118
// login to saferwall web service
@@ -161,6 +122,6 @@ var scanCmd = &cobra.Command{
161122
log.Fatalf("failed to login to saferwall web service")
162123
}
163124

164-
scanFile(webSvc, filePath, token)
125+
scanFile(webSvc, args[0], token)
165126
},
166127
}

0 commit comments

Comments
 (0)