Skip to content

Commit b6a0c30

Browse files
authored
feat(scan): Add a flag to wait for analysis completion (#71)
1 parent e5524bd commit b6a0c30

File tree

1 file changed

+110
-14
lines changed

1 file changed

+110
-14
lines changed

cmd/scan.go

Lines changed: 110 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
package cmd
1515

1616
import (
17+
"context"
1718
"fmt"
1819
"os"
20+
"strings"
21+
"time"
1922

2023
"github.com/VirusTotal/vt-cli/utils"
2124
vt "github.com/VirusTotal/vt-go"
@@ -24,9 +27,56 @@ import (
2427
"github.com/spf13/viper"
2528
)
2629

30+
const (
31+
// PollFrequency defines the interval in which requests are sent to the
32+
// VT API to check if the analysis is completed.
33+
PollFrequency = 10 * time.Second
34+
// TimeoutLimit defines the maximum amount of minutes to wait for an
35+
// analysis' results.
36+
TimeoutLimit = 10 * time.Minute
37+
)
38+
39+
// waitForAnalysisResults calls every PollFrequency seconds to the VT API and
40+
// checks whether an analysis is completed or not. When the analysis is completed
41+
// it is returned.
42+
func waitForAnalysisResults(cli *utils.APIClient, analysisId string, ds *utils.DoerState) (*vt.Object, error) {
43+
ds.Progress = "Waiting for analysis completion..."
44+
ticker := time.NewTicker(PollFrequency)
45+
defer ticker.Stop()
46+
ctx, cancel := context.WithTimeout(context.Background(), TimeoutLimit)
47+
defer cancel()
48+
i := 1
49+
50+
for {
51+
select {
52+
case <-ctx.Done():
53+
return nil, ctx.Err()
54+
case <-ticker.C:
55+
ds.Progress = fmt.Sprintf("Waiting for analysis completion...%s", strings.Repeat(".", i))
56+
i++
57+
if obj, err := cli.GetObject(vt.URL(fmt.Sprintf("analyses/%s", analysisId))); err != nil {
58+
// If the API returned an error 503 (transient error) retry; otherwise just return
59+
// the error to the user.
60+
if e, ok := err.(*vt.Error); !ok || e.Code != "TransientError" {
61+
ds.Progress = ""
62+
return nil, fmt.Errorf("error retrieving analysis result: %v", err)
63+
}
64+
} else if status, _ := obj.Get("status"); status == "completed" {
65+
ds.Progress = ""
66+
// Request the full object report and return it instead of just
67+
// the analysis results.
68+
return cli.GetObject(vt.URL(fmt.Sprintf("analyses/%s/item", analysisId)))
69+
}
70+
}
71+
}
72+
}
73+
2774
type fileScanner struct {
28-
scanner *vt.FileScanner
29-
showInVT bool
75+
scanner *vt.FileScanner
76+
cli *utils.APIClient
77+
printer *utils.Printer
78+
showInVT bool
79+
waitForCompletion bool
3080
}
3181

3282
func (s *fileScanner) Do(path interface{}, ds *utils.DoerState) string {
@@ -46,22 +96,31 @@ func (s *fileScanner) Do(path interface{}, ds *utils.DoerState) string {
4696

4797
f, err := os.Open(path.(string))
4898
if err != nil {
49-
return fmt.Sprintf("%s", err)
99+
return err.Error()
50100
}
51101
defer f.Close()
52102

53103
analysis, err := s.scanner.ScanFile(f, progressCh)
54104
if err != nil {
55-
return fmt.Sprintf("%s", err)
105+
return err.Error()
56106
}
57107

58108
if s.showInVT {
59-
// Return the analysis URL in VT so users can visit it
109+
// Return the analysis URL in VT so users can visit it.
60110
return fmt.Sprintf(
61111
"%s https://www.virustotal.com/gui/file-analysis/%s",
62112
path.(string), analysis.ID())
63113
}
64114

115+
if s.waitForCompletion {
116+
analysisResult, err := waitForAnalysisResults(s.cli, analysis.ID(), ds)
117+
if err != nil {
118+
return err.Error()
119+
}
120+
s.printer.PrintObject(analysisResult)
121+
return ""
122+
}
123+
65124
return fmt.Sprintf("%s %s", path.(string), analysis.ID())
66125
}
67126

@@ -70,7 +129,8 @@ var scanFileCmdHelp = `Scan one or more files.
70129
This command receives one or more file paths and uploads them to VirusTotal for
71130
scanning. It returns the file paths followed by their corresponding analysis IDs.
72131
You can use the "vt analysis" command for retrieving information about the
73-
analyses.
132+
analyses or you can use the --wait flag to see the results when the
133+
analysis is completed.
74134
75135
If the command receives a single hypen (-) the file paths are read from the standard
76136
input, one per line.
@@ -105,45 +165,67 @@ func NewScanFileCmd() *cobra.Command {
105165
if err != nil {
106166
return err
107167
}
168+
p, err := NewPrinter(cmd)
169+
if err != nil {
170+
return err
171+
}
108172
s := &fileScanner{
109-
scanner: client.NewFileScanner(),
110-
showInVT: viper.GetBool("open")}
173+
scanner: client.NewFileScanner(),
174+
showInVT: viper.GetBool("open"),
175+
waitForCompletion: viper.GetBool("wait"),
176+
printer: p,
177+
cli: client}
111178
c.DoWithStringsFromReader(s, argReader)
112179
return nil
113180
},
114181
}
115182

116183
addThreadsFlag(cmd.Flags())
117184
addOpenInVTFlag(cmd.Flags())
185+
addWaitForCompletionFlag(cmd.Flags())
186+
addIncludeExcludeFlags(cmd.Flags())
118187
cmd.MarkZshCompPositionalArgumentFile(1)
119188

120189
return cmd
121190
}
122191

123192
type urlScanner struct {
124-
scanner *vt.URLScanner
125-
showInVT bool
193+
scanner *vt.URLScanner
194+
cli *utils.APIClient
195+
printer *utils.Printer
196+
showInVT bool
197+
waitForCompletion bool
126198
}
127199

128200
func (s *urlScanner) Do(url interface{}, ds *utils.DoerState) string {
129201
analysis, err := s.scanner.Scan(url.(string))
130202
if err != nil {
131-
return fmt.Sprintf("%s", err)
203+
return err.Error()
132204
}
133205

134206
if s.showInVT {
135207
return fmt.Sprintf(
136208
"%s https://www.virustotal.com/gui/url-analysis/%s", url, analysis.ID())
137209
}
138210

211+
if s.waitForCompletion {
212+
analysisResult, err := waitForAnalysisResults(s.cli, analysis.ID(), ds)
213+
if err != nil {
214+
return err.Error()
215+
}
216+
s.printer.PrintObject(analysisResult)
217+
return ""
218+
}
219+
139220
return fmt.Sprintf("%s %s", url, analysis.ID())
140221
}
141222

142223
var scanURLCmdHelp = `Scan one or more URLs.
143224
144225
This command receives one or more URLs and scan them. It returns the URLs followed
145226
by their corresponding analysis IDs. You can use the "vt analysis" command for
146-
retrieving information about the analyses.
227+
retrieving information about the analyses or you can use the --wait
228+
flag to see the results when the analysis is completed.
147229
148230
If the command receives a single hypen (-) the URLs are read from the standard
149231
input, one per line.`
@@ -174,16 +256,24 @@ func NewScanURLCmd() *cobra.Command {
174256
if err != nil {
175257
return err
176258
}
259+
p, err := NewPrinter(cmd)
260+
if err != nil {
261+
return err
262+
}
177263
s := &urlScanner{
178-
scanner: client.NewURLScanner(),
179-
showInVT: viper.GetBool("open")}
264+
scanner: client.NewURLScanner(),
265+
showInVT: viper.GetBool("open"),
266+
waitForCompletion: viper.GetBool("wait"),
267+
printer: p,
268+
cli: client}
180269
c.DoWithStringsFromReader(s, argReader)
181270
return nil
182271
},
183272
}
184273

185274
addThreadsFlag(cmd.Flags())
186275
addOpenInVTFlag(cmd.Flags())
276+
addWaitForCompletionFlag(cmd.Flags())
187277

188278
return cmd
189279
}
@@ -212,3 +302,9 @@ func addOpenInVTFlag(flags *pflag.FlagSet) {
212302
"open", "o", false,
213303
"Return an URL to see the analysis report at the VirusTotal web GUI")
214304
}
305+
306+
func addWaitForCompletionFlag(flags *pflag.FlagSet) {
307+
flags.BoolP(
308+
"wait", "w", false,
309+
"Wait until the analysis is completed and show the analysis results")
310+
}

0 commit comments

Comments
 (0)