├── .gitignore ├── README.md └── vt.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | virustotal 2 | ========== 3 | 4 | Command-line utility to automatically lookup on VirusTotal all files recursively contained in a directory. 5 | You can also specify the path to a single file directly: 6 | 7 | nex@localhost:~$ python vt.py --key downloads/ 8 | [*] (downloads/44491d810062e6ad517914f442d44368550c87a3ccafe593185d06b571253037 9 | downloads/duplicate_example): FOUND 10 | \_ Results: 43/46 DETECTED 11 | SHA256: 44491d810062e6ad517914f442d44368550c87a3ccafe593185d06b571253037 12 | Scan Date: 2013-05-13 05:20:45 13 | Signatures: 14 | Win32.Worm.Downadup.Gen 15 | Worm/W32.Kido.157798 16 | Win32.Worm.Conficker.B.3 17 | Artemis!FBD8778D87C0 18 | Riskware 19 | Riskware 20 | W32/Kido.ih 21 | Trojan.Win32.Kido.ieisa 22 | W32/Conficker!Generic 23 | W32.Downadup.B 24 | Conficker.FA 25 | Win32/Kido!generic 26 | WORM_DOWNAD.AD 27 | Win32:Rootkit-gen [Rtk] 28 | Win32.Conficker.worm 29 | [...] 30 | [*] (downloads/0f4a6e4132d55949b1b7257411fde6ba1caae6c155d564589e01832c0d7f99a3): FOUND 31 | \_ Results: 45/47 DETECTED 32 | SHA256: 0f4a6e4132d55949b1b7257411fde6ba1caae6c155d564589e01832c0d7f99a3 33 | Scan Date: 2013-05-30 04:56:13 34 | Signatures: 35 | Worm/W32.Kido.165141 36 | Win32.Worm.Conficker.B.3 37 | Artemis!7C84915A299F 38 | Worm.Conficker 39 | Trojan 40 | NetWorm 41 | W32/Kido.fk 42 | Trojan.Win32.Kido.cdstu 43 | W32/Conficker!Generic 44 | W32.Downadup.B 45 | Conficker.HQ 46 | Win32/Kido!generic 47 | WORM_DOWNAD.AD 48 | Win32:Rootkit-gen [Rtk] 49 | Win32.Banker 50 | Trojan.Dropper-18535 51 | Trojan.Win32.Genome.vnvd 52 | Worm.Generic.63025 53 | [...] 54 | [*] (downloads/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855): FOUND 55 | [*] (downloads/cfc5bef5b3a8bd21d5b9748832db14f6966154867c946564e003e0febf2b6c92): FOUND 56 | \_ Results: 45/47 DETECTED 57 | SHA256: cfc5bef5b3a8bd21d5b9748832db14f6966154867c946564e003e0febf2b6c92 58 | Scan Date: 2013-05-30 05:11:03 59 | Signatures: 60 | Worm.Generic.393285 61 | Worm/W32.Kido.165025 62 | Worm.Conficker.Gen 63 | W32/Conficker.worm.gen.a 64 | Worm.Conficker 65 | Riskware 66 | Riskware 67 | Trojan/Downloader.Kido.bj 68 | Trojan.Win32.Shadow.bdjonf 69 | W32/Conficker!Generic 70 | W32.Downadup.B 71 | Conficker.ESK 72 | Win32/Conficker 73 | WORM_DOWNAD.AD 74 | Win32:Confi [Wrm] 75 | Worm.Kido-25 76 | Net-Worm.Win32.Kido.ih 77 | Worm.Generic.393285 78 | Worm.Kido!Dya6ZOjm14U 79 | [...] -------------------------------------------------------------------------------- /vt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import json 5 | import urllib 6 | import urllib2 7 | import hashlib 8 | import argparse 9 | 10 | VIRUSTOTAL_FILE_URL = 'https://www.virustotal.com/vtapi/v2/file/report' 11 | API_KEY = '' 12 | 13 | TPL_SECTION = "[*] ({0}):" 14 | TPL_MATCH = "\t\_ Results: {0}/{1} {2}\n\t SHA256: {3}\n\t Scan Date: {4}" 15 | TPL_SIGNATURES = "\t Signatures:\n\t\t{0}" 16 | 17 | def color(text, color_code): 18 | if sys.platform == "win32" and os.getenv("TERM") != "xterm": 19 | return text 20 | 21 | return '\x1b[%dm%s\x1b[0m' % (color_code, text) 22 | 23 | def red(text): 24 | return color(text, 31) 25 | 26 | def yellow(text): 27 | return color(text, 33) 28 | 29 | class Hash(object): 30 | def __init__(self, path): 31 | self.path = path 32 | self.md5 = '' 33 | self.sha256 = '' 34 | 35 | def get_chunks(self): 36 | fd = open(self.path, 'rb') 37 | while True: 38 | chunk = fd.read(16 * 1024) 39 | if not chunk: 40 | break 41 | 42 | yield chunk 43 | fd.close() 44 | 45 | def calculate(self): 46 | md5 = hashlib.md5() 47 | sha256 = hashlib.sha256() 48 | 49 | for chunk in self.get_chunks(): 50 | md5.update(chunk) 51 | sha256.update(chunk) 52 | 53 | self.md5 = md5.hexdigest() 54 | self.sha256 = sha256.hexdigest() 55 | 56 | class Scanner(object): 57 | def __init__(self, key, path): 58 | self.key = key 59 | self.path = path 60 | self.list = [] 61 | 62 | def populate(self): 63 | paths = [] 64 | 65 | if os.path.isfile(self.path): 66 | paths.append(self.path) 67 | else: 68 | for root, folders, files in os.walk(self.path): 69 | for file_name in files: 70 | # Skip hidden files, might need an option for this. 71 | if file_name.startswith('.'): 72 | continue 73 | 74 | file_path = os.path.join(root, file_name) 75 | if os.path.exists(file_path): 76 | paths.append(file_path) 77 | 78 | for path in paths: 79 | hashes = Hash(path) 80 | hashes.calculate() 81 | 82 | self.list.append({ 83 | 'path' : path, 84 | 'md5' : hashes.md5, 85 | 'sha256' : hashes.sha256 86 | }) 87 | 88 | def scan(self): 89 | hashes = [] 90 | for entry in self.list: 91 | if entry['sha256'] not in hashes: 92 | hashes.append(entry['sha256']) 93 | 94 | data = urllib.urlencode({ 95 | 'resource' : ','.join(hashes), 96 | 'apikey' : self.key 97 | }) 98 | 99 | try: 100 | request = urllib2.Request(VIRUSTOTAL_FILE_URL, data) 101 | response = urllib2.urlopen(request) 102 | report = json.loads(response.read()) 103 | except Exception as e: 104 | print(red("[!] ERROR: Cannot obtain results from VirusTotal: {0}\n".format(e))) 105 | return 106 | 107 | results = [] 108 | if type(report) is dict: 109 | results.append(report) 110 | elif type(report) is list: 111 | results = report 112 | 113 | for entry in results: 114 | sha256 = entry['resource'] 115 | 116 | entry_paths = [] 117 | for item in self.list: 118 | if item['sha256'] == sha256: 119 | if item['path'] not in entry_paths: 120 | entry_paths.append(item['path']) 121 | 122 | print(TPL_SECTION.format('\n '.join(entry_paths))), 123 | 124 | if entry['response_code'] == 0: 125 | print('NOT FOUND') 126 | else: 127 | print(yellow('FOUND')) 128 | 129 | signatures = [] 130 | for av, scan in entry['scans'].items(): 131 | if scan['result']: 132 | signatures.append(scan['result']) 133 | 134 | if entry['positives'] > 0: 135 | print(TPL_MATCH.format( 136 | entry['positives'], 137 | entry['total'], 138 | red('DETECTED'), 139 | entry['resource'], 140 | entry['scan_date'] 141 | )) 142 | 143 | if entry['positives'] > 0: 144 | print(TPL_SIGNATURES.format('\n\t\t'.join(signatures))) 145 | 146 | def run(self): 147 | if not self.key: 148 | print(red("[!] ERROR: You didn't specify a valid VirusTotal API key.\n")) 149 | return 150 | 151 | if not os.path.exists(self.path): 152 | print(red("[!] ERROR: The target path {0} does not exist.\n".format(self.path))) 153 | return 154 | 155 | self.populate() 156 | self.scan() 157 | 158 | if __name__ == '__main__': 159 | parser = argparse.ArgumentParser() 160 | parser.add_argument('path', type=str, help='Path to the file or folder to lookup on VirusTotal') 161 | parser.add_argument('--key', type=str, action='store', default=API_KEY, help='VirusTotal API key') 162 | 163 | try: 164 | args = parser.parse_args() 165 | except IOError as e: 166 | parser.error(e) 167 | sys.exit() 168 | 169 | scan = Scanner(args.key, args.path) 170 | scan.run() 171 | --------------------------------------------------------------------------------