mirror of https://github.com/CIRCL/url-abuse
367 lines
11 KiB
Python
367 lines
11 KiB
Python
import json
|
|
import os
|
|
from pathlib import Path
|
|
import uuid
|
|
|
|
from flask import Flask, render_template, request, Response, redirect, url_for
|
|
from flask_mail import Mail, Message
|
|
from flask_bootstrap import Bootstrap
|
|
from flask_wtf import FlaskForm
|
|
from wtforms import StringField, SubmitField
|
|
from wtforms.widgets import TextInput
|
|
from wtforms.validators import Required
|
|
|
|
import logging
|
|
from logging.handlers import RotatingFileHandler
|
|
from logging import Formatter
|
|
|
|
from redis import Redis
|
|
|
|
from urlabuse.helpers import get_socket_path, get_homedir
|
|
from urlabuse.urlabuse import Query
|
|
|
|
import configparser
|
|
from .proxied import ReverseProxied
|
|
|
|
|
|
config_dir = Path('config')
|
|
|
|
|
|
class AngularTextInput(TextInput):
|
|
|
|
def __call__(self, field, **kwargs):
|
|
kwargs['ng-model'] = 'input_url'
|
|
return super(AngularTextInput, self).__call__(field, **kwargs)
|
|
|
|
|
|
class URLForm(FlaskForm):
|
|
url = StringField('URL Field',
|
|
description='Enter the URL you want to lookup here.',
|
|
validators=[Required()], widget=AngularTextInput())
|
|
|
|
submit_button = SubmitField('Run lookup')
|
|
|
|
|
|
def make_dict(parser, section):
|
|
to_return = {}
|
|
entries = parser.items(section)
|
|
for k, v in entries:
|
|
to_return[k] = v.split(',')
|
|
return to_return
|
|
|
|
|
|
def prepare_auth():
|
|
if not os.path.exists('users.key'):
|
|
return None
|
|
to_return = {}
|
|
with open('users.key', 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
user, password = line.split('=')
|
|
to_return[user] = password
|
|
return to_return
|
|
|
|
|
|
app = Flask(__name__)
|
|
handler = RotatingFileHandler('urlabuse.log', maxBytes=10000, backupCount=5)
|
|
handler.setFormatter(Formatter('%(asctime)s %(message)s'))
|
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
|
app.logger.addHandler(handler)
|
|
app.logger.setLevel(logging.INFO)
|
|
Bootstrap(app)
|
|
queue = Redis(unix_socket_path=get_socket_path('cache'), db=0,
|
|
decode_responses=True)
|
|
urlabuse_query = Query()
|
|
|
|
# Mail Config
|
|
app.config['MAIL_SERVER'] = 'localhost'
|
|
app.config['MAIL_PORT'] = 25
|
|
mail = Mail(app)
|
|
|
|
secret_file_path = get_homedir() / 'website' / 'secret_key'
|
|
|
|
if not secret_file_path.exists() or secret_file_path.stat().st_size < 64:
|
|
with open(secret_file_path, 'wb') as f:
|
|
f.write(os.urandom(64))
|
|
|
|
with open(secret_file_path, 'rb') as f:
|
|
app.config['SECRET_KEY'] = f.read()
|
|
|
|
app.config['BOOTSTRAP_SERVE_LOCAL'] = True
|
|
app.config['configfile'] = config_dir / 'config.ini'
|
|
|
|
parser = configparser.SafeConfigParser()
|
|
parser.read(app.config['configfile'])
|
|
|
|
replacelist = make_dict(parser, 'replacelist')
|
|
auth_users = prepare_auth()
|
|
ignorelist = [i.strip()
|
|
for i in parser.get('abuse', 'ignore').split('\n')
|
|
if len(i.strip()) > 0]
|
|
autosend_threshold = 5
|
|
|
|
|
|
def _get_user_ip(request):
|
|
ip = request.headers.get('X-Forwarded-For')
|
|
if ip is None:
|
|
ip = request.remote_addr
|
|
return ip
|
|
|
|
|
|
@app.route('/', methods=['GET', 'POST'])
|
|
def index():
|
|
if request.method == 'HEAD':
|
|
# Just returns ack if the webserver is running
|
|
return 'Ack'
|
|
form = URLForm()
|
|
return render_template('index.html', form=form)
|
|
|
|
|
|
@app.route('/urlreport', methods=['GET'])
|
|
def url_report():
|
|
return render_template('url-report.html')
|
|
|
|
|
|
@app.errorhandler(404)
|
|
def page_not_found(e):
|
|
ip = request.headers.get('X-Forwarded-For')
|
|
if ip is None:
|
|
ip = request.remote_addr
|
|
if request.path != '/_result/':
|
|
app.logger.info('404 of {} on {}'.format(ip, request.path))
|
|
return render_template('404.html'), 404
|
|
|
|
|
|
def authenticate():
|
|
"""Sends a 401 response that enables basic auth"""
|
|
return Response('Could not verify your access level for that URL.\n'
|
|
'You have to login with proper credentials', 401,
|
|
{'WWW-Authenticate': 'Basic realm="Login Required"'})
|
|
|
|
|
|
def check_auth(username, password):
|
|
"""This function is called to check if a username /
|
|
password combination is valid.
|
|
"""
|
|
if auth_users is None:
|
|
return False
|
|
else:
|
|
db_pass = auth_users.get(username)
|
|
return db_pass == password
|
|
|
|
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
auth = request.authorization
|
|
if not auth or not check_auth(auth.username, auth.password):
|
|
return authenticate()
|
|
return redirect(url_for('index'))
|
|
|
|
|
|
@app.route("/_result/<job_key>", methods=['GET'])
|
|
def check_valid(job_key):
|
|
if not job_key or not queue.exists(job_key):
|
|
return Response(json.dumps(None), mimetype='application/json'), 200
|
|
if not queue.hexists(job_key, 'result'):
|
|
return Response(json.dumps('Nay!'), mimetype='application/json'), 202
|
|
result = queue.hget(job_key, 'result')
|
|
queue.delete(job_key)
|
|
return Response(result, mimetype='application/json'), 200
|
|
|
|
|
|
def enqueue(method, data):
|
|
job_id = str(uuid.uuid4())
|
|
p = queue.pipeline()
|
|
p.hmset(job_id, {'method': method, 'data': json.dumps(data)})
|
|
p.sadd('to_process', job_id)
|
|
p.execute()
|
|
return job_id
|
|
|
|
|
|
@app.route('/start', methods=['POST'])
|
|
def run_query():
|
|
data = request.get_json(force=True)
|
|
url = data["url"]
|
|
ip = _get_user_ip(request)
|
|
app.logger.info(f'{ip} {url}')
|
|
if urlabuse_query.get_submissions(url) and urlabuse_query.get_submissions(url) >= autosend_threshold:
|
|
send(url, '', True)
|
|
return enqueue('is_valid_url', {'url': url})
|
|
|
|
|
|
@app.route('/lookyloo', methods=['POST'])
|
|
def lookyloo():
|
|
data = request.get_json(force=True)
|
|
return enqueue('lookyloo', {'url': data["url"]})
|
|
|
|
|
|
@app.route('/urls', methods=['POST'])
|
|
def urls():
|
|
data = request.get_json(force=True)
|
|
return enqueue('url_list', {'url': data["url"]})
|
|
|
|
|
|
@app.route('/resolve', methods=['POST'])
|
|
def resolve():
|
|
data = request.get_json(force=True)
|
|
return enqueue('dns_resolve', {'url': data["url"]})
|
|
|
|
|
|
def read_auth(name):
|
|
key = config_dir / f'{name}.key'
|
|
if not key.exists():
|
|
return ''
|
|
with open(key) as f:
|
|
to_return = []
|
|
for line in f.readlines():
|
|
to_return.append(line.strip())
|
|
return to_return
|
|
|
|
|
|
@app.route('/phishtank', methods=['POST'])
|
|
def phishtank():
|
|
auth = read_auth('phishtank')
|
|
if not auth:
|
|
return ''
|
|
data = request.get_json(force=True)
|
|
return enqueue('phish_query', {'url': parser.get("PHISHTANK", "url"),
|
|
'key': auth[0], 'query': data["query"]})
|
|
|
|
|
|
@app.route('/virustotal_report', methods=['POST'])
|
|
def vt():
|
|
auth = read_auth('virustotal')
|
|
if not auth:
|
|
return ''
|
|
data = request.get_json(force=True)
|
|
return enqueue('vt_query_url', {'url': parser.get("VIRUSTOTAL", "url_report"),
|
|
'url_up': parser.get("VIRUSTOTAL", "url_upload"),
|
|
'key': auth[0], 'query': data["query"]})
|
|
|
|
|
|
@app.route('/googlesafebrowsing', methods=['POST'])
|
|
def gsb():
|
|
auth = read_auth('googlesafebrowsing')
|
|
if not auth:
|
|
return ''
|
|
key = auth[0]
|
|
data = request.get_json(force=True)
|
|
url = parser.get("GOOGLESAFEBROWSING", "url").format(key)
|
|
return enqueue('gsb_query', {'url': url,
|
|
'query': data["query"]})
|
|
|
|
|
|
'''
|
|
@app.route('/urlquery', methods=['POST'])
|
|
def urlquery():
|
|
auth = read_auth('urlquery')
|
|
if not auth:
|
|
return ''
|
|
key = auth[0]
|
|
data = json.loads(request.data.decode())
|
|
url = parser.get("URLQUERY", "url")
|
|
query = data["query"]
|
|
u = q.enqueue_call(func=urlquery_query, args=(url, key, query,), result_ttl=500)
|
|
return u.get_id()
|
|
|
|
@app.route('/ticket', methods=['POST'])
|
|
def ticket():
|
|
if not request.authorization:
|
|
return ''
|
|
data = json.loads(request.data.decode())
|
|
server = parser.get("SPHINX", "server")
|
|
port = int(parser.get("SPHINX", "port"))
|
|
url = parser.get("ITS", "url")
|
|
query = data["query"]
|
|
u = q.enqueue_call(func=sphinxsearch, args=(server, port, url, query,),
|
|
result_ttl=500)
|
|
return u.get_id()
|
|
'''
|
|
|
|
|
|
@app.route('/whois', methods=['POST'])
|
|
def whoismail():
|
|
data = request.get_json(force=True)
|
|
return enqueue('whois', {'server': parser.get("WHOIS", "server"),
|
|
'port': parser.getint("WHOIS", "port"),
|
|
'domain': data["query"],
|
|
'ignorelist': ignorelist, 'replacelist': replacelist})
|
|
|
|
|
|
@app.route('/eupi', methods=['POST'])
|
|
def eu():
|
|
auth = read_auth('eupi')
|
|
if not auth:
|
|
return ''
|
|
data = request.get_json(force=True)
|
|
return enqueue('eupi', {'url': parser.get("EUPI", "url"),
|
|
'key': auth[0], 'q': data["query"]})
|
|
|
|
|
|
@app.route('/pdnscircl', methods=['POST'])
|
|
def dnscircl():
|
|
auth = read_auth('pdnscircl')
|
|
if not auth:
|
|
return ''
|
|
user, password = auth
|
|
url = parser.get("PDNS_CIRCL", "url")
|
|
data = request.get_json(force=True)
|
|
return enqueue('pdnscircl', {'url': url, 'user': user.strip(),
|
|
'passwd': password.strip(), 'q': data["query"]})
|
|
|
|
|
|
@app.route('/bgpranking', methods=['POST'])
|
|
def bgpr():
|
|
data = request.get_json(force=True)
|
|
return enqueue('bgpranking', {'ip': data["query"]})
|
|
|
|
|
|
@app.route('/psslcircl', methods=['POST'])
|
|
def sslcircl():
|
|
auth = read_auth('psslcircl')
|
|
if not auth:
|
|
return ''
|
|
user, password = auth
|
|
url = parser.get("PSSL_CIRCL", "url")
|
|
data = request.get_json(force=True)
|
|
return enqueue('psslcircl', {'url': url, 'user': user.strip(),
|
|
'passwd': password.strip(), 'q': data["query"]})
|
|
|
|
|
|
@app.route('/get_cache', methods=['POST'])
|
|
def get_cache():
|
|
data = request.get_json(force=True)
|
|
url = data["query"]
|
|
if 'digest' in data:
|
|
digest = data["digest"]
|
|
else:
|
|
digest = False
|
|
data = urlabuse_query.cached(url, digest)
|
|
return Response(json.dumps(data), mimetype='application/json')
|
|
|
|
|
|
def send(url, ip='', autosend=False):
|
|
if not urlabuse_query.get_mail_sent(url):
|
|
data = urlabuse_query.cached(url, digest=True)
|
|
if not autosend:
|
|
subject = 'URL Abuse report from ' + ip
|
|
else:
|
|
subject = 'URL Abuse report sent automatically'
|
|
msg = Message(subject, sender='urlabuse@circl.lu', recipients=["info@circl.lu"])
|
|
msg.body = data['digest'][0]
|
|
msg.body += '\n\n'
|
|
msg.body += json.dumps(data['result'], sort_keys=True, indent=2)
|
|
mail.send(msg)
|
|
urlabuse_query.set_mail_sent(url)
|
|
|
|
|
|
@app.route('/submit', methods=['POST'])
|
|
def send_mail():
|
|
data = request.get_json(force=True)
|
|
url = data["url"]
|
|
if not urlabuse_query.get_mail_sent(url):
|
|
ip = _get_user_ip(request)
|
|
send(url, ip)
|
|
form = URLForm()
|
|
return render_template('index.html', form=form)
|