@@ -3,6 +3,8 @@ package cmd
33import (
44 "fmt"
55 "io"
6+ "net"
7+ "regexp"
68 "strings"
79
810 "github.com/VirusTotal/vt-go"
@@ -12,6 +14,12 @@ import (
1214 "github.com/spf13/viper"
1315)
1416
17+ type objectDescriptor struct {
18+ Type string `json:"type"`
19+ Id string `json:"id,omitempty"`
20+ Url string `json:"url,omitempty"`
21+ }
22+
1523var collectionCmdHelp = `Get information about one or more collections.
1624
1725This command receives one or more collection IDs and returns information about
@@ -49,6 +57,8 @@ func NewCollectionCmd() *cobra.Command {
4957 cmd .AddCommand (NewCollectionCreateCmd ())
5058 cmd .AddCommand (NewCollectionRenameCmd ())
5159 cmd .AddCommand (NewCollectionUpdateCmd ())
60+ cmd .AddCommand (NewCollectionDeleteCmd ())
61+ cmd .AddCommand (NewCollectionRemoveItemsCmd ())
5262
5363 addRelationshipCmds (cmd , "collections" , "collection" , "[collection]" )
5464 addThreadsFlag (cmd .Flags ())
@@ -200,6 +210,82 @@ func NewCollectionUpdateCmd() *cobra.Command {
200210 }
201211}
202212
213+ var removeCollectionItemsCmdHelp = `Remove items from a collection.
214+
215+ This command receives a collection ID and one of more IoCs
216+ (sha256 hashes, URLs, domains, IP addresses) and removes them from the collection.
217+
218+ If the command receives a single hypen (-) the IoCs will be read from the
219+ standard input.`
220+
221+ var removeCollectionItemsExample = ` vt collection remove [collection id] www.example.com
222+ vt collection remove [collection id] www.example.com 8.8.8.8
223+ cat list_of_iocs | vt collection remove [collection id] -`
224+
225+ // NewCollectionRemoveItemsCmd returns a command for removing items from a collection.
226+ func NewCollectionRemoveItemsCmd () * cobra.Command {
227+ return & cobra.Command {
228+ Use : "remove [collection id] [ioc]..." ,
229+ Short : "Remove items from a collection." ,
230+ Args : cobra .MinimumNArgs (2 ),
231+ Long : removeCollectionItemsCmdHelp ,
232+ Example : removeCollectionItemsExample ,
233+
234+ RunE : func (cmd * cobra.Command , args []string ) error {
235+ c , err := NewAPIClient ()
236+ if err != nil {
237+ return err
238+ }
239+ relationshipDescriptors := descriptorsFromReader (
240+ utils .StringReaderFromCmdArgs (args [1 :]))
241+ for relationshipName , descriptors := range relationshipDescriptors {
242+ url := vt .URL ("collections/%s/%s" , args [0 ], relationshipName )
243+ response , err := c .DeleteData (url , descriptors )
244+ if err != nil {
245+ return err
246+ }
247+ if response .Error .Code != "" {
248+ return response .Error
249+ }
250+ }
251+ return nil
252+ },
253+ }
254+ }
255+
256+ var deleteCollectionCmdHelp = `Delete a collection.
257+
258+ This command receives a collection ID and deletes it.`
259+
260+ var deleteCollectionExample = ` vt collection delete [collection id]`
261+
262+ // NewCollectionDeleteCmd returns a command for deleting a collection.
263+ func NewCollectionDeleteCmd () * cobra.Command {
264+ return & cobra.Command {
265+ Use : "delete [collection id]" ,
266+ Short : "Delete a collection." ,
267+ Args : cobra .MinimumNArgs (1 ),
268+ Long : deleteCollectionCmdHelp ,
269+ Example : deleteCollectionExample ,
270+
271+ RunE : func (cmd * cobra.Command , args []string ) error {
272+ c , err := NewAPIClient ()
273+ if err != nil {
274+ return err
275+ }
276+ url := vt .URL ("collections/%s" , args [0 ])
277+ response , err := c .Delete (url )
278+ if err != nil {
279+ return err
280+ }
281+ if response .Error .Code != "" {
282+ return response .Error
283+ }
284+ return nil
285+ },
286+ }
287+ }
288+
203289func rawFromReader (reader utils.StringReader ) string {
204290 var lines []string
205291 for {
@@ -211,3 +297,51 @@ func rawFromReader(reader utils.StringReader) string {
211297 }
212298 return strings .Join (lines , " " )
213299}
300+
301+ func descriptorsFromReader (reader utils.StringReader ) map [string ][]objectDescriptor {
302+ descriptors := make (map [string ][]objectDescriptor )
303+ hashPattern := regexp .MustCompile ("[0-9a-fA-F]{32,64}" )
304+ urlPattern := regexp .MustCompile ("[hH][tTxX]{2}[pP][sS]?://.*" )
305+ // At least two domain parts.
306+ domainPattern := regexp .MustCompile (".*[^.]+\\ .[^.]+.*" )
307+ for {
308+ next , err := reader .ReadString ()
309+ if err == io .EOF {
310+ break
311+ }
312+ if match := hashPattern .MatchString (next ); match {
313+ files := descriptors ["files" ]
314+ files = append (files , objectDescriptor {
315+ Type : "file" ,
316+ Id : next ,
317+ })
318+ descriptors ["files" ] = files
319+ } else if net .ParseIP (next ) != nil {
320+ if strings .Contains (next , "." ) {
321+ ipAddresses := descriptors ["ip_addresses" ]
322+ ipAddresses = append (ipAddresses , objectDescriptor {
323+ Type : "ip_address" ,
324+ Id : next ,
325+ })
326+ descriptors ["ip_addresses" ] = ipAddresses
327+ } else {
328+ // IPv6, skip.
329+ }
330+ } else if urlPattern .MatchString (next ) {
331+ urls := descriptors ["urls" ]
332+ urls = append (urls , objectDescriptor {
333+ Type : "url" ,
334+ Url : next ,
335+ })
336+ descriptors ["urls" ] = urls
337+ } else if domainPattern .MatchString (next ) {
338+ domains := descriptors ["domains" ]
339+ domains = append (domains , objectDescriptor {
340+ Type : "domain" ,
341+ Id : next ,
342+ })
343+ descriptors ["domains" ] = domains
344+ }
345+ }
346+ return descriptors
347+ }
0 commit comments