1414package cmd
1515
1616import (
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+
2774type 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
3282func (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.
70129This command receives one or more file paths and uploads them to VirusTotal for
71130scanning. It returns the file paths followed by their corresponding analysis IDs.
72131You 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
75135If the command receives a single hypen (-) the file paths are read from the standard
76136input, 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
123192type 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
128200func (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
142223var scanURLCmdHelp = `Scan one or more URLs.
143224
144225This command receives one or more URLs and scan them. It returns the URLs followed
145226by 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
148230If the command receives a single hypen (-) the URLs are read from the standard
149231input, 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