summaryrefslogtreecommitdiff
path: root/fbin-scanner.py
diff options
context:
space:
mode:
authorJon Bergli Heier <snakebite@jvnv.net>2019-04-02 20:45:55 +0200
committerJon Bergli Heier <snakebite@jvnv.net>2019-04-02 20:45:55 +0200
commit608fa9690b6961b237b9e38fc6ec7c0916f92d1b (patch)
treeb8dfae44158b42645750dbabb00d21f6ff0fbcb1 /fbin-scanner.py
parent98ba4b0acdb8ad0e4e81fc082dcb1cbdcdf1ce1f (diff)
Add support for blocking files
Files are blocked if blocked_reason is non-NULL. This value is currently not exposed publicly, instead a 404 will be returned. Files are scanned using virustotal.com's public API if scanned is False. Scans are performed by the fbin-scanner.py script. If a match is found, blocked_reason is set to the payload received. Files that are not in VT's database will be automatically submitted and the script will wait for the scan to complete before continuing.
Diffstat (limited to 'fbin-scanner.py')
-rw-r--r--fbin-scanner.py127
1 files changed, 127 insertions, 0 deletions
diff --git a/fbin-scanner.py b/fbin-scanner.py
new file mode 100644
index 0000000..d520680
--- /dev/null
+++ b/fbin-scanner.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python
+
+import argparse
+from collections import deque
+import fcntl
+import hashlib
+import logging
+import os
+import sys
+import time
+
+from flask import Flask
+import requests
+
+FILE_DELAY = 15
+SCAN_DELAY = 60
+
+LOGGING_CONFIG = {
+ 'level': logging.INFO,
+ 'style': '{',
+ 'format': '{asctime} [{levelname}] {message}',
+}
+
+logging.basicConfig(**LOGGING_CONFIG)
+
+logger = logging.getLogger('fbin-scanner')
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--lock-file', default='/tmp/fbin-scanner.lock')
+parser.add_argument('-c', '--config-file', default='fbin/fbin.cfg')
+args = parser.parse_args()
+
+lock_file = open(args.lock_file, 'w')
+try:
+ fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
+except OSError:
+ logger.error('Cannot acquire lock; another process is running')
+ sys.exit(1)
+
+def get_report(dbfile, digest):
+ logger.info('Fetching file report')
+ params = {
+ 'apikey': app.config['VIRUSTOTAL_API_KEY'],
+ 'resource': digest,
+ }
+ response = requests.get('https://www.virustotal.com/vtapi/v2/file/report', params=params)
+ response.raise_for_status()
+ data = response.json()
+ # Report found
+ if data['response_code'] == 1:
+ return data
+ scan_id = None
+ # No report, submit file for scan
+ if data['response_code'] == 0:
+ logger.info('No report, submitting file')
+ response = requests.post('https://www.virustotal.com/vtapi/v2/file/scan',
+ params={'apikey': app.config['VIRUSTOTAL_API_KEY']},
+ files={'file': (dbfile.filename, open(dbfile.get_path(), 'rb'))},
+ )
+ response.raise_for_status()
+ data = response.json()
+ if data['response_code'] != 1:
+ logger.error('Scan failed')
+ return
+ scan_id = data['scan_id']
+ logger.info('File submitted with scan_id %s, waiting for scan report', scan_id)
+ params['resource'] = scan_id
+ # File was submitted or is queued for scan
+ if scan_id or data['response_code'] == -2:
+ # TODO: consider adding a timeout here
+ while True:
+ time.sleep(SCAN_DELAY)
+ response = requests.get('https://www.virustotal.com/vtapi/v2/file/report', params=params)
+ response.raise_for_status()
+ data = response.json()
+ if data['response_code'] == 1:
+ return data
+ logger.warning('Unknown response: %s', data)
+
+def main():
+ with session_scope() as session:
+ files = deque(session.query(File).filter(File.scanned == False, File.filename != 'ZwbZh6o.gif').all())
+ while len(files):
+ dbfile = files.pop()
+ if not dbfile.exists:
+ logger.warning('Ignoring missing file %s', dbfile.get_path())
+ continue
+ if dbfile.get_size() > 2**20*32:
+ logger.info('Ignoring file %s due to size (%s)', dbfile.get_path(), dbfile.formatted_size)
+ continue
+ logger.info('Checking file %s (%s)', dbfile.get_path(), dbfile.formatted_size)
+ h = hashlib.sha256()
+ with open(dbfile.get_path(), 'rb') as f:
+ chunk = f.read(2**10*16)
+ while chunk:
+ h.update(chunk)
+ chunk = f.read(2**10*16)
+ digest = h.hexdigest()
+ logger.info('SHA-256: %s', digest)
+ try:
+ report = get_report(dbfile, digest)
+ except:
+ logger.exception('Failed to get report for %s', dbfile.get_path())
+ # Most likely an error from virustotal, so just break here and retry later.
+ break
+ dbfile.scanned = True
+ if report and any(r.get('detected', False) for r in report['scans'].values()):
+ logger.warning('Positive match')
+ dbfile.blocked_reason = report
+ else:
+ logger.info('No match')
+ session.add(dbfile)
+ session.commit()
+ break
+ time.sleep(FILE_DELAY)
+ logger.info('No more files to scan')
+
+app = Flask('scanner')
+with app.app_context():
+ app.config.from_pyfile(args.config_file)
+ from fbin.db import session_scope, File
+ config = app.config
+ main()
+
+fcntl.flock(lock_file, fcntl.LOCK_UN)
+lock_file.close()
+os.unlink(args.lock_file)