From 39dd2021dd06dfac06f55e2990bf524c407e6029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Thu, 4 Feb 2021 19:51:43 +0100 Subject: [PATCH] chg: Complete rework of the login system, add UI for MISP Push --- lookyloo/lookyloo.py | 5 +- lookyloo/modules.py | 5 +- poetry.lock | 20 ++-- pyproject.toml | 2 +- website/web/__init__.py | 132 ++++++++++++++++------ website/web/static/tree.css | 6 + website/web/templates/misp_push_view.html | 18 +++ website/web/templates/tree.html | 32 ++++++ 8 files changed, 171 insertions(+), 49 deletions(-) create mode 100644 website/web/templates/misp_push_view.html diff --git a/lookyloo/lookyloo.py b/lookyloo/lookyloo.py index 0179f65..6746355 100644 --- a/lookyloo/lookyloo.py +++ b/lookyloo/lookyloo.py @@ -953,7 +953,8 @@ class Lookyloo(): obj.comment = f'Redirect {nb}' self.__misp_add_ips_to_URLObject(obj, ct.root_hartree.hostname_tree) redirects.append(obj) - obj.comment = f'Last redirect ({nb})' + if redirects: + redirects[-1].comment = f'Last redirect ({nb})' if redirects: prec_object = initial_url @@ -977,7 +978,7 @@ class Lookyloo(): screenshot: MISPAttribute = event.add_attribute('attachment', 'screenshot_landing_page.png', data=self.get_screenshot(capture_uuid), disable_correlation=True) # type: ignore try: - fo = FileObject(pseudofile=ct.root_hartree.rendered_node.body, filename='body_response.html') + fo = FileObject(pseudofile=ct.root_hartree.rendered_node.body, filename=ct.root_hartree.rendered_node.filename) fo.comment = 'Content received for the final redirect (before rendering)' fo.add_reference(event.objects[-1], 'loaded-by', 'URL loading that content') fo.add_reference(screenshot, 'rendered-as', 'Screenshot of the page') diff --git a/lookyloo/modules.py b/lookyloo/modules.py index c8b762a..f237ed4 100644 --- a/lookyloo/modules.py +++ b/lookyloo/modules.py @@ -44,11 +44,14 @@ class MISP(): self.enable_lookup = True if config.get('enable_push'): self.enable_push = True - self.default_tags: List[str] = config.get('default_tags') # type: ignore + self.default_tags: List[str] = config.get('default_tags') # type: ignore self.auto_publish = config.get('auto_publish') self.storage_dir_misp = get_homedir() / 'misp' self.storage_dir_misp.mkdir(parents=True, exist_ok=True) + def get_fav_tags(self): + return self.client.tags(pythonify=True, favouritesOnly=1) + def push(self, event: MISPEvent) -> Union[MISPEvent, Dict]: if self.available and self.enable_push: for tag in self.default_tags: diff --git a/poetry.lock b/poetry.lock index 70908c6..4bf7619 100644 --- a/poetry.lock +++ b/poetry.lock @@ -280,9 +280,9 @@ docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx- dotenv = ["python-dotenv"] [[package]] -name = "flask-httpauth" -version = "4.2.0" -description = "Basic and Digest HTTP authentication for Flask routes" +name = "flask-login" +version = "0.5.0" +description = "User session management for Flask" category = "main" optional = false python-versions = "*" @@ -744,7 +744,7 @@ requests = ">=2.22.0,<3.0.0" [[package]] name = "pymisp" -version = "2.4.137.3" +version = "2.4.137.4" description = "Python API for MISP." category = "main" optional = false @@ -1147,7 +1147,7 @@ misp = ["python-magic", "pydeep"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "b3e7ae942355125f62268270e7888093f64b33035d04d574d49ad39189c48f40" +content-hash = "51d0bb7529658d3b5323d7c87d7676c1126d88c2b927af77a91c426f842a9249" [metadata.files] aiohttp = [ @@ -1354,9 +1354,9 @@ flask = [ {file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"}, {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"}, ] -flask-httpauth = [ - {file = "Flask-HTTPAuth-4.2.0.tar.gz", hash = "sha256:8c7e49e53ce7dc14e66fe39b9334e4b7ceb8d0b99a6ba1c3562bb528ef9da84a"}, - {file = "Flask_HTTPAuth-4.2.0-py2.py3-none-any.whl", hash = "sha256:3fcedb99a03985915335a38c35bfee6765cbd66d7f46440fa3b42ae94a90fac7"}, +flask-login = [ + {file = "Flask-Login-0.5.0.tar.gz", hash = "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b"}, + {file = "Flask_Login-0.5.0-py2.py3-none-any.whl", hash = "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0"}, ] gunicorn = [ {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"}, @@ -1724,8 +1724,8 @@ pylookyloo = [ {file = "pylookyloo-1.3.tar.gz", hash = "sha256:3996798b68203c1ebfdb4744d029a2c741e3a7d4e5d98541afd0c9b4a20eee86"}, ] pymisp = [ - {file = "pymisp-2.4.137.3-py3-none-any.whl", hash = "sha256:433a58cd5d6ec8bee9394c43bb679dc0422bec68450f7642f7fd51b870e97118"}, - {file = "pymisp-2.4.137.3.tar.gz", hash = "sha256:d3a8074e3276a698d25bcd56f4ce3913d375b05a04fb36f4c8cfe732f4aaefd9"}, + {file = "pymisp-2.4.137.4-py3-none-any.whl", hash = "sha256:814c3e5cd3218ba885edab7b8808f45dbe16bbfccb3cd9d19bf062b1ced70fc0"}, + {file = "pymisp-2.4.137.4.tar.gz", hash = "sha256:ea029360d7e76646403571b479c8208a678c8b51b9e683d51b0ce95d6ebe3274"}, ] pyopenssl = [ {file = "pyOpenSSL-20.0.1-py2.py3-none-any.whl", hash = "sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"}, diff --git a/pyproject.toml b/pyproject.toml index 278aa63..95146d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ bootstrap-flask = "^1.5.1" cloudscraper = "^1.2.56" defang = "^0.5.3" vt-py = "^0.6.1" -Flask-HTTPAuth = "^4.2.0" pyeupi = "^1.0" scrapysplashwrapper = "^1.3" pysanejs = "^1.3" @@ -56,6 +55,7 @@ python-magic = {version = "^0.4.18", optional = true} pydeep = {version = "^0.4", optional = true} Pillow = "^8.1.0" lief = "^0.11.0" +Flask-Login = "^0.5.0" [tool.poetry.extras] misp = ['python-magic', 'pydeep'] diff --git a/website/web/__init__.py b/website/web/__init__.py index aad0821..18f4678 100644 --- a/website/web/__init__.py +++ b/website/web/__init__.py @@ -16,7 +16,7 @@ import hashlib from flask import Flask, render_template, request, send_file, redirect, url_for, Response, flash, jsonify from flask_bootstrap import Bootstrap # type: ignore -from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth, MultiAuth # type: ignore +import flask_login # type: ignore from werkzeug.security import generate_password_hash, check_password_hash from pymisp import MISPEvent @@ -44,9 +44,8 @@ app.config['SESSION_COOKIE_NAME'] = 'lookyloo' app.debug = False # Auth stuff -basic_auth = HTTPBasicAuth() -token_auth = HTTPTokenAuth('LookylooToken') -auth = MultiAuth(basic_auth, token_auth) +login_manager = flask_login.LoginManager() +login_manager.init_app(app) try: # Use legacy user mgmt users = get_config('generic', 'cache_clean_user') @@ -79,17 +78,61 @@ for username, authstuff in users_table.items(): keys_table[authstuff['authkey']] = username -@basic_auth.verify_password -def verify_password(username, password): - if users_table.get(username): - if check_password_hash(users_table['username']['password'], password): - return username +class User(flask_login.UserMixin): + pass -@token_auth.verify_token -def verify_token(token): - if token in keys_table: - return keys_table[token] +@login_manager.user_loader +def user_loader(username): + if username not in users_table: + return None + user = User() + user.id = username + return user + + +@login_manager.request_loader +def load_user_from_request(request): + api_key = request.headers.get('Authorization') + if not api_key: + return None + user = User() + api_key = api_key.strip() + if api_key in keys_table: + user.id = keys_table[api_key] + return user + return None + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'GET': + return ''' +
+ + + +
+ ''' + + username = request.form['username'] + if username in users_table and check_password_hash(users_table[username]['password'], request.form['password']): + user = User() + user.id = username + flask_login.login_user(user) + flash(f'Logged in as: {flask_login.current_user.id}', 'success') + else: + flash(f'Unable to login as: {username}', 'error') + + return redirect(url_for('index')) + + +@app.route('/logout') +@flask_login.login_required +def logout(): + flask_login.logout_user() + flash('Successfully logged out.', 'success') + return redirect(url_for('index')) # Config @@ -212,7 +255,7 @@ def hostnode_popup(tree_uuid: str, node_uuid: str): # ##### Tree level Methods ##### @app.route('/tree//rebuild') -@auth.login_required +@flask_login.login_required def rebuild_tree(tree_uuid: str): try: lookyloo.remove_pickle(tree_uuid) @@ -361,7 +404,7 @@ def export(tree_uuid: str): @app.route('/tree//hide', methods=['GET']) -@auth.login_required +@flask_login.login_required def hide_capture(tree_uuid: str): lookyloo.hide_capture(tree_uuid) return redirect(url_for('tree', tree_uuid=tree_uuid)) @@ -383,6 +426,7 @@ def send_mail(tree_uuid: str): email = '' comment: str = request.form.get('comment') if request.form.get('comment') else '' # type: ignore lookyloo.send_mail(tree_uuid, email, comment) + flash("Email notification sent", 'success') return redirect(url_for('tree', tree_uuid=tree_uuid)) @@ -426,7 +470,6 @@ def tree(tree_uuid: str, node_uuid: Optional[str]=None): except IndexError as e: print(e) pass - return render_template('tree.html', tree_json=ct.to_json(), start_time=ct.start_time.isoformat(), user_agent=ct.user_agent, root_url=ct.root_url, @@ -437,6 +480,7 @@ def tree(tree_uuid: str, node_uuid: Optional[str]=None): enable_context_by_users=enable_context_by_users, enable_categorization=enable_categorization, enable_bookmark=enable_bookmark, + misp_push=lookyloo.misp.enable_push, blur_screenshot=blur_screenshot, urlnode_uuid=hostnode_to_highlight, auto_trigger_modules=auto_trigger_modules, has_redirects=True if cache.redirects else False) @@ -446,7 +490,7 @@ def tree(tree_uuid: str, node_uuid: Optional[str]=None): @app.route('/tree//mark_as_legitimate', methods=['POST']) -@auth.login_required +@flask_login.login_required def mark_as_legitimate(tree_uuid: str): if request.data: legitimate_entries = request.get_json(force=True) @@ -502,7 +546,7 @@ def index(): @app.route('/hidden', methods=['GET']) -@auth.login_required +@flask_login.login_required def index_hidden(): return index_generic(show_hidden=True) @@ -538,14 +582,14 @@ def categories(): @app.route('/rebuild_all') -@auth.login_required +@flask_login.login_required def rebuild_all(): lookyloo.rebuild_all() return redirect(url_for('index')) @app.route('/rebuild_cache') -@auth.login_required +@flask_login.login_required def rebuild_cache(): lookyloo.rebuild_cache() return redirect(url_for('index')) @@ -733,7 +777,7 @@ def hashes_urlnode(tree_uuid: str, node_uuid: str): @app.route('/tree//url//add_context', methods=['POST']) -@auth.login_required +@flask_login.login_required def add_context(tree_uuid: str, node_uuid: str): if not enable_context_by_users: return redirect(url_for('ressources')) @@ -766,25 +810,43 @@ def add_context(tree_uuid: str, node_uuid: str): return redirect(url_for('ressources')) -@app.route('/tree//misp_push', methods=['GET']) -@auth.login_required -def web_misp_push(tree_uuid: str): +@app.route('/tree//misp_push', methods=['GET', 'POST']) +@flask_login.login_required +def web_misp_push_view(tree_uuid: str): + error = False if not lookyloo.misp.available: flash('MISP module not available.', 'error') + error = True elif not lookyloo.misp.enable_push: flash('Push not enabled in MISP module.', 'error') + error = True else: event = lookyloo.misp_export(tree_uuid) if isinstance(event, dict): flash(f'Unable to generate the MISP export: {event}', 'error') - else: - event = lookyloo.misp.push(event) - if isinstance(event, MISPEvent): - flash(f'MISP event {event.id} created on {lookyloo.misp.client.root_url}', 'success') - else: - flash(f'Unable to create event: {event}', 'error') + error = True + if error: + return redirect(url_for('tree', tree_uuid=tree_uuid)) - return redirect(url_for('tree', tree_uuid=tree_uuid)) + if request.method == 'POST': + # event is a MISPEvent at this point + # Submit the event + tags = request.form.getlist('tags') + for tag in tags: + event.add_tag(tag) # type: ignore + event = lookyloo.misp.push(event) # type: ignore + if isinstance(event, MISPEvent): + flash(f'MISP event {event.id} created on {lookyloo.misp.client.root_url}', 'success') + else: + flash(f'Unable to create event: {event}', 'error') + return redirect(url_for('tree', tree_uuid=tree_uuid)) + + fav_tags = lookyloo.misp.get_fav_tags() + + return render_template('misp_push_view.html', tree_uuid=tree_uuid, + event=event, fav_tags=fav_tags, + auto_publish=lookyloo.misp.auto_publish, + default_tags=lookyloo.misp.default_tags) # Query API @@ -793,9 +855,9 @@ def web_misp_push(tree_uuid: str): def json_get_token(): auth = request.get_json(force=True) if 'username' in auth and 'password' in auth: # Expected keys in json - username = verify_password(auth['username'], auth['password']) - if username == auth['username']: - return jsonify({'authkey': users_table[username]['authkey']}) + if (auth['username'] in users_table + and check_password_hash(users_table[auth['username']]['password'], auth['password'])): + return jsonify({'authkey': users_table[auth['username']]['authkey']}) return jsonify({'error': 'User/Password invalid.'}) @@ -830,7 +892,7 @@ def misp_export(tree_uuid: str): @app.route('/json//misp_push', methods=['GET']) -@auth.login_required +@flask_login.login_required def misp_push(tree_uuid: str): to_return: Dict = {} if not lookyloo.misp.available: diff --git a/website/web/static/tree.css b/website/web/static/tree.css index 9c39d21..1b07294 100644 --- a/website/web/static/tree.css +++ b/website/web/static/tree.css @@ -14,6 +14,12 @@ stroke-width: 2px; } +.flashed-messages { + position: fixed; + top: 5px; + text-align: center; +} + #menu_container { position: fixed; top: 5px; diff --git a/website/web/templates/misp_push_view.html b/website/web/templates/misp_push_view.html new file mode 100644 index 0000000..758c525 --- /dev/null +++ b/website/web/templates/misp_push_view.html @@ -0,0 +1,18 @@ +
+

Event to push: {{event.info}}

+

Auto Publish: {{auto_publish}}

+

Default tags: {{', '.join(default_tags)}}

+
+
+ +
+ +
+
+ +
+
diff --git a/website/web/templates/tree.html b/website/web/templates/tree.html index aac1d0c..a53f12a 100644 --- a/website/web/templates/tree.html +++ b/website/web/templates/tree.html @@ -65,6 +65,13 @@ modal.find('.modal-body').load(button.data("remote")); }); +