From 7bf0b78754ee254245d1e66d2308bf52fa3d33a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Fri, 4 Jun 2021 17:30:14 -0700 Subject: [PATCH 01/18] new: Use flask-restx for the API --- poetry.lock | 117 +++++++++---- pyproject.toml | 2 +- website/web/__init__.py | 376 ++++++++++++++++++++++++++-------------- 3 files changed, 335 insertions(+), 160 deletions(-) diff --git a/poetry.lock b/poetry.lock index 852e9c48..773ff23f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -17,6 +17,17 @@ yarl = ">=1.0,<2.0" [package.extras] speedups = ["aiodns", "brotlipy", "cchardet"] +[[package]] +name = "aniso8601" +version = "9.0.1" +description = "A library for parsing ISO 8601 strings." +category = "main" +optional = false +python-versions = "*" + +[package.extras] +dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] + [[package]] name = "appnope" version = "0.1.2" @@ -137,20 +148,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "click" -version = "8.0.1" +version = "7.1.2" description = "Composable command line interface toolkit" category = "main" optional = false -python-versions = ">=3.6" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -252,20 +260,21 @@ python-versions = "*" [[package]] name = "flask" -version = "2.0.1" +version = "1.1.4" description = "A simple framework for building complex web applications." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] -click = ">=7.1.2" -itsdangerous = ">=2.0" -Jinja2 = ">=3.0" -Werkzeug = ">=2.0" +click = ">=5.1,<8.0" +itsdangerous = ">=0.24,<2.0" +Jinja2 = ">=2.10.1,<3.0" +Werkzeug = ">=0.15,<2.0" [package.extras] -async = ["asgiref (>=3.2)"] +dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] +docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] dotenv = ["python-dotenv"] [[package]] @@ -279,6 +288,27 @@ python-versions = "*" [package.dependencies] Flask = "*" +[[package]] +name = "flask-restx" +version = "0.4.0" +description = "Fully featured framework for fast, easy and documented API development with Flask" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +aniso8601 = {version = ">=0.82", markers = "python_version >= \"3.5\""} +Flask = ">=0.8,<2.0.0" +jsonschema = "*" +pytz = "*" +six = ">=1.3.0" +werkzeug = "<2.0.0" + +[package.extras] +dev = ["blinker", "Faker (==2.0.0)", "mock (==3.0.5)", "pytest-benchmark (==3.2.2)", "pytest-cov (==2.7.1)", "pytest-flask (==0.15.1)", "pytest-mock (==1.10.4)", "pytest-profiling (==1.7.0)", "tzlocal", "invoke (==1.3.0)", "readme-renderer (==24.0)", "twine (==1.15.0)", "tox", "pytest (==4.6.5)", "pytest (==5.4.1)", "ossaudit", "black"] +doc = ["alabaster (==0.7.12)", "Sphinx (==2.1.2)", "sphinx-issues (==1.2.0)"] +test = ["blinker", "Faker (==2.0.0)", "mock (==3.0.5)", "pytest-benchmark (==3.2.2)", "pytest-cov (==2.7.1)", "pytest-flask (==0.15.1)", "pytest-mock (==1.10.4)", "pytest-profiling (==1.7.0)", "tzlocal", "invoke (==1.3.0)", "readme-renderer (==24.0)", "twine (==1.15.0)", "pytest (==4.6.5)", "pytest (==5.4.1)", "ossaudit"] + [[package]] name = "gunicorn" version = "20.1.0" @@ -437,11 +467,11 @@ w3lib = ">=1.17.0" [[package]] name = "itsdangerous" -version = "2.0.1" -description = "Safely pass data to untrusted environments and back." +version = "1.1.0" +description = "Various helpers to pass data to untrusted environments and back." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "jedi" @@ -460,17 +490,17 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] [[package]] name = "jinja2" -version = "3.0.1" +version = "2.11.3" description = "A very fast and expressive template engine." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] -MarkupSafe = ">=2.0" +MarkupSafe = ">=0.23" [package.extras] -i18n = ["Babel (>=2.7)"] +i18n = ["Babel (>=0.8)"] [[package]] name = "jmespath" @@ -860,6 +890,14 @@ category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pytz" +version = "2021.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "queuelib" version = "1.6.1" @@ -1102,13 +1140,14 @@ python-versions = "*" [[package]] name = "werkzeug" -version = "2.0.1" +version = "1.0.1" description = "The comprehensive WSGI web application library." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] +dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] watchdog = ["watchdog"] [[package]] @@ -1150,7 +1189,7 @@ misp = ["python-magic", "pydeep"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "e8e3df6d57b274c350ed4df996c608cdf1ecb06ffd3f985c3ed0912a42da310b" +content-hash = "27c09f95d480f1517a425ed98847d3096d0ec22b7cccf0727189a31e4206a9ed" [metadata.files] aiohttp = [ @@ -1192,6 +1231,10 @@ aiohttp = [ {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, ] +aniso8601 = [ + {file = "aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f"}, + {file = "aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"}, +] appnope = [ {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, @@ -1300,8 +1343,8 @@ chardet = [ {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -1351,13 +1394,17 @@ filetype = [ {file = "filetype-1.0.7.tar.gz", hash = "sha256:da393ece8d98b47edf2dd5a85a2c8733e44b769e32c71af4cd96ed8d38d96aa7"}, ] flask = [ - {file = "Flask-2.0.1-py3-none-any.whl", hash = "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9"}, - {file = "Flask-2.0.1.tar.gz", hash = "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55"}, + {file = "Flask-1.1.4-py2.py3-none-any.whl", hash = "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22"}, + {file = "Flask-1.1.4.tar.gz", hash = "sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196"}, ] 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"}, ] +flask-restx = [ + {file = "flask-restx-0.4.0.tar.gz", hash = "sha256:8bdbfd8ad2949383c490b3b180481c0e9f70163230a5b567a56592c82f7c5cb0"}, + {file = "flask_restx-0.4.0-py2.py3-none-any.whl", hash = "sha256:780ac57ebda25e9f17ffccf3ff1b47a2c4efe5787cebddbbba1a91f53560d79f"}, +] gunicorn = [ {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] @@ -1406,16 +1453,16 @@ itemloaders = [ {file = "itemloaders-1.0.4.tar.gz", hash = "sha256:1277cd8ca3e4c02dcdfbc1bcae9134ad89acfa6041bd15b4561c6290203a0c96"}, ] itsdangerous = [ - {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"}, - {file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"}, + {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, + {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, ] jedi = [ {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, ] jinja2 = [ - {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, - {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, + {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, + {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, ] jmespath = [ {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"}, @@ -1778,6 +1825,10 @@ python-magic = [ {file = "python-magic-0.4.24.tar.gz", hash = "sha256:de800df9fb50f8ec5974761054a708af6e4246b03b4bdaee993f948947b0ebcf"}, {file = "python_magic-0.4.24-py2.py3-none-any.whl", hash = "sha256:4fec8ee805fea30c07afccd1592c0f17977089895bdfaae5fec870a84e997626"}, ] +pytz = [ + {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, + {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, +] queuelib = [ {file = "queuelib-1.6.1-py2.py3-none-any.whl", hash = "sha256:90ee30ebb0b57112606358b63c09a681bbb9a7dd1120af09c836b475504cea85"}, {file = "queuelib-1.6.1.tar.gz", hash = "sha256:631d067c9be57e395c382d680d3653ca1452cd29e8da25c5e8d94b5c0c528c31"}, @@ -1893,8 +1944,8 @@ wcwidth = [ {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] werkzeug = [ - {file = "Werkzeug-2.0.1-py3-none-any.whl", hash = "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8"}, - {file = "Werkzeug-2.0.1.tar.gz", hash = "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42"}, + {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, + {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, ] wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, diff --git a/pyproject.toml b/pyproject.toml index d85ea309..8cf6acbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,6 @@ background_indexer = "bin.background_indexer:main" [tool.poetry.dependencies] python = "^3.8" requests = "^2.25.1" -flask = "^2.0.0" gunicorn = "^20.1.0" cchardet = "^2.1.7" redis = "^3.5.3" @@ -58,6 +57,7 @@ pydeep = {version = "^0.4", optional = true} Pillow = "^8.2.0" lief = "^0.11.4" Flask-Login = "^0.5.0" +flask-restx = "^0.4.0" [tool.poetry.extras] misp = ['python-magic', 'pydeep'] diff --git a/website/web/__init__.py b/website/web/__init__.py index d6258952..0a7b81c1 100644 --- a/website/web/__init__.py +++ b/website/web/__init__.py @@ -14,10 +14,12 @@ import logging import hashlib from urllib.parse import quote_plus, unquote_plus import time +import pkg_resources from flask import Flask, render_template, request, send_file, redirect, url_for, Response, flash, jsonify from flask_bootstrap import Bootstrap # type: ignore import flask_login # type: ignore +from flask_restx import Resource, Api, fields, abort # type: ignore from werkzeug.security import generate_password_hash, check_password_hash from pymisp import MISPEvent, MISPServerError @@ -577,7 +579,7 @@ def tree(tree_uuid: str, node_uuid: Optional[str]=None): @flask_login.login_required def mark_as_legitimate(tree_uuid: str): if request.data: - legitimate_entries: Dict = request.get_json(force=True) # type: ignore + legitimate_entries: Dict = request.get_json(force=True) lookyloo.add_to_legitimate(tree_uuid, **legitimate_entries) else: lookyloo.add_to_legitimate(tree_uuid) @@ -696,17 +698,6 @@ def rebuild_cache(): return redirect(url_for('index')) -@app.route('/submit', methods=['POST']) -def submit(): - if flask_login.current_user.is_authenticated: - user = flask_login.current_user.get_id() - else: - user = src_request_ip(request) - to_query: Dict = request.get_json(force=True) # type: ignore - perma_uuid = lookyloo.enqueue_capture(to_query, source='api', user=user, authenticated=flask_login.current_user.is_authenticated) - return Response(perma_uuid, mimetype='text/text') - - @app.route('/search', methods=['GET', 'POST']) def search(): if request.form.get('url'): @@ -1021,122 +1012,255 @@ def web_misp_push_view(tree_uuid: str): default_tags=lookyloo.misp.default_tags) -# Query API - -@app.route('/json/get_token', methods=['POST']) -def json_get_token(): - auth: Dict = request.get_json(force=True) # type: ignore - if 'username' in auth and 'password' in auth: # Expected keys in json - 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.'}) - - -@app.route('/json//status', methods=['GET']) -def get_capture_status(tree_uuid: str): - return jsonify({'status_code': lookyloo.get_capture_status(tree_uuid)}) - - -@app.route('/json//redirects', methods=['GET']) -def json_redirects(tree_uuid: str): - cache = lookyloo.capture_cache(tree_uuid) - if not cache: - return {'error': 'UUID missing in cache, try again later.'} - - to_return: Dict[str, Any] = {'response': {'url': cache.url, 'redirects': []}} - if not cache.redirects: - to_return['response']['info'] = 'No redirects' - return to_return - if cache.incomplete_redirects: - # Trigger tree build, get all redirects - lookyloo.get_crawled_tree(tree_uuid) - cache = lookyloo.capture_cache(tree_uuid) - if cache: - to_return['response']['redirects'] = cache.redirects - else: - to_return['response']['redirects'] = cache.redirects - - return jsonify(to_return) - - -@app.route('/json//misp_export', methods=['GET']) -def misp_export(tree_uuid: str): - with_parents = request.args.get('with_parents') - event = lookyloo.misp_export(tree_uuid, True if with_parents else False) - if isinstance(event, dict): - return jsonify(event) - - to_return = [] - for e in event: - to_return.append(e.to_json(indent=2)) - return jsonify(to_return) - - -@app.route('/json//misp_push', methods=['GET', 'POST']) -@flask_login.login_required -def misp_push(tree_uuid: str): - if request.method == 'POST': - parameters: Dict = request.get_json(force=True) # type: ignore - with_parents = True if 'with_parents' in parameters else False - allow_duplicates = True if 'allow_duplicates' in parameters else False - else: - with_parents = False - allow_duplicates = False - to_return: Dict = {} - if not lookyloo.misp.available: - to_return['error'] = 'MISP module not available.' - elif not lookyloo.misp.enable_push: - to_return['error'] = 'Push not enabled in MISP module.' - else: - event = lookyloo.misp_export(tree_uuid, with_parents) - if isinstance(event, dict): - to_return['error'] = event - else: - new_events = lookyloo.misp.push(event, allow_duplicates) - if isinstance(new_events, dict): - to_return['error'] = new_events - else: - events_to_return = [] - for e in new_events: - events_to_return.append(e.to_json(indent=2)) - jsonify(events_to_return) - - return jsonify(to_return) - - -@app.route('/json/hash_info/', methods=['GET']) -def json_hash_info(h: str): - details, body = lookyloo.get_body_hash_full(h) - if not details: - return {'error': 'Unknown Hash.'} - to_return: Dict[str, Any] = {'response': {'hash': h, 'details': details, - 'body': base64.b64encode(body.getvalue()).decode()}} - return jsonify(to_return) - - -@app.route('/json/url_info', methods=['POST']) -def json_url_info(): - to_query: Dict = request.get_json(force=True) # type: ignore - occurrences = lookyloo.get_url_occurrences(to_query.pop('url'), **to_query) - return jsonify(occurrences) - - -@app.route('/json/hostname_info', methods=['POST']) -def json_hostname_info(): - to_query: Dict = request.get_json(force=True) # type: ignore - occurrences = lookyloo.get_hostname_occurrences(to_query.pop('hostname'), **to_query) - return jsonify(occurrences) - - -@app.route('/json/stats', methods=['GET']) -def json_stats(): - to_return = lookyloo.get_stats() - return Response(json.dumps(to_return), mimetype='application/json') - - @app.route('/whois/', methods=['GET']) def whois(query: str): to_return = lookyloo.uwhois.whois(query) return send_file(BytesIO(to_return.encode()), mimetype='test/plain', as_attachment=True, attachment_filename=f'whois.{query}.txt') + + +# Query API + +authorizations = { + 'apikey': { + 'type': 'apiKey', + 'in': 'header', + 'name': 'Authorization' + } +} + +api = Api(app, title='Lookyloo API', + description='API to submit captures and query a lookyloo instance.', + doc='/doc/', + authorizations=authorizations, + version=pkg_resources.get_distribution('lookyloo').version) + + +def api_auth_check(method): + if flask_login.current_user.is_authenticated or load_user_from_request(request): + return method + abort(403, 'Authentication required.') + + +token_request_fields = api.model('AuthTokenFields', { + 'username': fields.String(description="Your username", required=True), + 'password': fields.String(description="Your password", required=True), +}) + + +@api.route('/json/get_token') +@api.doc(description='Get the API token required for authenticated calls') +class AuthToken(Resource): + + @api.param('username', 'Your username') + @api.param('password', 'Your password') + def get(self): + username = request.args['username'] if request.args.get('username') else False + password = request.args['password'] if request.args.get('password') else False + if username in users_table and check_password_hash(users_table[username]['password'], password): + return {'authkey': users_table[username]['authkey']} + return {'error': 'User/Password invalid.'} + + @api.doc(body=token_request_fields) + def post(self): + auth: Dict = request.get_json(force=True) + if 'username' in auth and 'password' in auth: # Expected keys in json + if (auth['username'] in users_table + and check_password_hash(users_table[auth['username']]['password'], auth['password'])): + return {'authkey': users_table[auth['username']]['authkey']} + return {'error': 'User/Password invalid.'} + + +@api.route('/json//status') +@api.doc(description='Get the status of a capture', + params={'tree_uuid': 'The UUID of the capture'}) +class CaptureStatusQuery(Resource): + def get(self, tree_uuid: str): + return {'status_code': lookyloo.get_capture_status(tree_uuid)} + + +@api.route('/json//redirects') +@api.doc(description='Get all the redirects of a capture', + params={'tree_uuid': 'The UUID of the capture'}) +class CaptureRedirects(Resource): + def get(self, tree_uuid: str): + cache = lookyloo.capture_cache(tree_uuid) + if not cache: + return {'error': 'UUID missing in cache, try again later.'} + + to_return: Dict[str, Any] = {'response': {'url': cache.url, 'redirects': []}} + if not cache.redirects: + to_return['response']['info'] = 'No redirects' + return to_return + if cache.incomplete_redirects: + # Trigger tree build, get all redirects + lookyloo.get_crawled_tree(tree_uuid) + cache = lookyloo.capture_cache(tree_uuid) + if cache: + to_return['response']['redirects'] = cache.redirects + else: + to_return['response']['redirects'] = cache.redirects + + return to_return + + +@api.route('/json//misp_export') +@api.doc(description='Get an export of the capture in MISP format', + params={'tree_uuid': 'The UUID of the capture'}) +class MISPExport(Resource): + def get(self, tree_uuid: str): + with_parents = request.args.get('with_parents') + event = lookyloo.misp_export(tree_uuid, True if with_parents else False) + if isinstance(event, dict): + return event + + to_return = [] + for e in event: + to_return.append(e.to_json(indent=2)) + return to_return + + +misp_push_fields = api.model('MISPPushFields', { + 'allow_duplicates': fields.Integer(description="Push the event even if it is already present on the MISP instance", + example=0, min=0, max=1), + 'with_parents': fields.Integer(description="Also push the parents of the capture (if any)", + example=0, min=0, max=1), +}) + + +@api.route('/json//misp_push') +@api.doc(description='Push an event to a pre-configured MISP instance', + params={'tree_uuid': 'The UUID of the capture'}, + security='apikey') +class MISPPush(Resource): + method_decorators = [api_auth_check] + + @api.param('with_parents', 'Also push the parents of the capture (if any)') + @api.param('allow_duplicates', 'Push the event even if it is already present on the MISP instance') + def get(self, tree_uuid: str): + with_parents = True if request.args.get('with_parents') else False + allow_duplicates = True if request.args.get('allow_duplicates') else False + to_return: Dict = {} + if not lookyloo.misp.available: + to_return['error'] = 'MISP module not available.' + elif not lookyloo.misp.enable_push: + to_return['error'] = 'Push not enabled in MISP module.' + else: + event = lookyloo.misp_export(tree_uuid, with_parents) + if isinstance(event, dict): + to_return['error'] = event + else: + new_events = lookyloo.misp.push(event, allow_duplicates) + if isinstance(new_events, dict): + to_return['error'] = new_events + else: + events_to_return = [] + for e in new_events: + events_to_return.append(e.to_json(indent=2)) + return events_to_return + + return to_return + + @api.doc(body=misp_push_fields) + def post(self, tree_uuid: str): + parameters: Dict = request.get_json(force=True) + with_parents = True if parameters.get('with_parents') else False + allow_duplicates = True if parameters.get('allow_duplicates') else False + + to_return: Dict = {} + if not lookyloo.misp.available: + to_return['error'] = 'MISP module not available.' + elif not lookyloo.misp.enable_push: + to_return['error'] = 'Push not enabled in MISP module.' + else: + event = lookyloo.misp_export(tree_uuid, with_parents) + if isinstance(event, dict): + to_return['error'] = event + else: + new_events = lookyloo.misp.push(event, allow_duplicates) + if isinstance(new_events, dict): + to_return['error'] = new_events + else: + events_to_return = [] + for e in new_events: + events_to_return.append(e.to_json(indent=2)) + return events_to_return + + return to_return + + +@api.route('/json/hash_info/') +@api.doc(description='Search for a ressource with a specific hash (sha512)', + params={'h': 'The hash (sha512)'}) +class HashInfo(Resource): + def get(self, h: str): + details, body = lookyloo.get_body_hash_full(h) + if not details: + return {'error': 'Unknown Hash.'} + to_return: Dict[str, Any] = {'response': {'hash': h, 'details': details, + 'body': base64.b64encode(body.getvalue()).decode()}} + return to_return + + +url_info_fields = api.model('URLInfoFields', { + 'url': fields.String(description="The URL to search", required=True), + 'limit': fields.Integer(description="The maximal amount of captures to return", example=20), +}) + + +@api.route('/json/url_info') +@api.doc(description='Search for a URL') +class URLInfo(Resource): + + @api.doc(body=url_info_fields) + def post(self): + to_query: Dict = request.get_json(force=True) + occurrences = lookyloo.get_url_occurrences(to_query.pop('url'), **to_query) + return occurrences + + +hostname_info_fields = api.model('HostnameInfoFields', { + 'hostname': fields.String(description="The hostname to search", required=True), + 'limit': fields.Integer(description="The maximal amount of captures to return", example=20), +}) + + +@api.route('/json/hostname_info') +@api.doc(description='Search for a hostname') +class HostnameInfo(Resource): + + @api.doc(body=hostname_info_fields) + def post(self): + to_query: Dict = request.get_json(force=True) + occurrences = lookyloo.get_hostname_occurrences(to_query.pop('hostname'), **to_query) + return occurrences + + +@api.route('/json/stats') +@api.doc(description='Get the statistics of the lookyloo instance.') +class InstanceStats(Resource): + def get(self): + return lookyloo.get_stats() + + +submit_fields = api.model('SubmitFields', { + 'url': fields.String(description="The URL to capture", required=True), + 'listing': fields.Integer(description="Display the capture on the index", min=0, max=1, example=1), + 'user_agent': fields.String(description="User agent to use for the capture", example=''), + 'referer': fields.String(description="Referer to pass to the capture", example=''), + 'cookies': fields.String(description="JSON export of a list of cookies as exported from an other capture", example='') +}) + + +@api.route('/submit') +class SubmitCapture(Resource): + + @api.doc(body=submit_fields) + def post(self): + if flask_login.current_user.is_authenticated: + user = flask_login.current_user.get_id() + else: + user = src_request_ip(request) + to_query: Dict = request.get_json(force=True) + perma_uuid = lookyloo.enqueue_capture(to_query, source='api', user=user, authenticated=flask_login.current_user.is_authenticated) + return Response(perma_uuid, mimetype='text/text') From e5b76b3a2f2fdccc6fdb0825a0c83286442834b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Mon, 7 Jun 2021 13:12:23 -0700 Subject: [PATCH 02/18] chg: Move API into a new file, cleanup --- website/web/__init__.py | 463 ++++++++------------------------------ website/web/genericapi.py | 258 +++++++++++++++++++++ website/web/helpers.py | 91 ++++++++ 3 files changed, 438 insertions(+), 374 deletions(-) create mode 100644 website/web/genericapi.py create mode 100644 website/web/helpers.py diff --git a/website/web/__init__.py b/website/web/__init__.py index 0a7b81c1..f9237b51 100644 --- a/website/web/__init__.py +++ b/website/web/__init__.py @@ -1,17 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import base64 from io import BytesIO, StringIO -import os -from pathlib import Path from datetime import datetime, timedelta, timezone import json import http import calendar from typing import Optional, Dict, Any, Union, List import logging -import hashlib from urllib.parse import quote_plus, unquote_plus import time import pkg_resources @@ -19,28 +15,24 @@ import pkg_resources from flask import Flask, render_template, request, send_file, redirect, url_for, Response, flash, jsonify from flask_bootstrap import Bootstrap # type: ignore import flask_login # type: ignore -from flask_restx import Resource, Api, fields, abort # type: ignore -from werkzeug.security import generate_password_hash, check_password_hash +from flask_restx import Api # type: ignore +from .genericapi import api as generic_api +from werkzeug.security import check_password_hash from pymisp import MISPEvent, MISPServerError -from lookyloo.helpers import (get_homedir, update_user_agents, get_user_agents, get_config, +from lookyloo.helpers import (update_user_agents, get_user_agents, get_config, get_taxonomies, load_cookies, CaptureStatus) from lookyloo.lookyloo import Lookyloo, Indexing from lookyloo.exceptions import NoValidHarFile, MissingUUID + from .proxied import ReverseProxied +from .helpers import src_request_ip, User, load_user_from_request, build_users_table, get_secret_key app: Flask = Flask(__name__) app.wsgi_app = ReverseProxied(app.wsgi_app) # type: ignore -secret_file_path: Path = get_homedir() / 'secret_key' - -if not secret_file_path.exists() or secret_file_path.stat().st_size < 64: - with secret_file_path.open('wb') as f: - f.write(os.urandom(64)) - -with secret_file_path.open('rb') as f: - app.config['SECRET_KEY'] = f.read() +app.config['SECRET_KEY'] = get_secret_key() Bootstrap(app) app.config['BOOTSTRAP_SERVE_LOCAL'] = True @@ -51,45 +43,11 @@ app.debug = False # Auth stuff login_manager = flask_login.LoginManager() login_manager.init_app(app) -try: - # Use legacy user mgmt, no need to print a warning, and it will fail on new install. - users = get_config('generic', 'cache_clean_user', quiet=True) -except Exception: - users = get_config('generic', 'users') - -users_table: Dict[str, Dict[str, str]] = {} -for username, authstuff in users.items(): - if isinstance(authstuff, str): - # just a password, make a key - users_table[username] = {} - users_table[username]['password'] = generate_password_hash(authstuff) - users_table[username]['authkey'] = hashlib.pbkdf2_hmac('sha256', - app.config['SECRET_KEY'], - authstuff.encode(), - 100000).hex() - - elif isinstance(authstuff, list) and len(authstuff) == 2: - if isinstance(authstuff[0], str) and isinstance(authstuff[1], str) and len(authstuff[1]) == 64: - users_table[username] = {} - users_table[username]['password'] = generate_password_hash(authstuff[0]) - users_table[username]['authkey'] = authstuff[1] - - if username not in users_table: - raise Exception('User setup invalid. Must be "username": "password" or "username": ["password", "token 64 chars (sha256)"]') - -keys_table = {} -for username, authstuff in users_table.items(): - if 'authkey' in authstuff: - keys_table[authstuff['authkey']] = username - - -class User(flask_login.UserMixin): - pass @login_manager.user_loader def user_loader(username): - if username not in users_table: + if username not in build_users_table(): return None user = User() user.id = username @@ -97,16 +55,8 @@ def user_loader(username): @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 +def _load_user_from_request(request): + return load_user_from_request(request) @app.route('/login', methods=['GET', 'POST']) @@ -121,6 +71,7 @@ def login(): ''' username = request.form['username'] + users_table = build_users_table() if username in users_table and check_password_hash(users_table[username]['password'], request.form['password']): user = User() user.id = username @@ -196,14 +147,6 @@ app.jinja_env.globals.update(month_name=month_name) # ##### Generic/configuration methods ##### -def src_request_ip(request) -> str: - # NOTE: X-Real-IP is the IP passed by the reverse proxy in the headers. - real_ip = request.headers.get('X-Real-IP') - if not real_ip: - real_ip = request.remote_addr - return real_ip - - @app.after_request def after_request(response): # We keep a list user agents in order to build a list to use in the capture @@ -351,6 +294,76 @@ def web_misp_lookup_view(tree_uuid: str): return render_template('misp_lookup.html', uuid=tree_uuid, hits=hits, misp_root_url=misp_root_url) +@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') + return redirect(url_for('tree', tree_uuid=tree_uuid)) + elif not lookyloo.misp.enable_push: + flash('Push not enabled in MISP module.', 'error') + return redirect(url_for('tree', tree_uuid=tree_uuid)) + else: + event = lookyloo.misp_export(tree_uuid) + if isinstance(event, dict): + flash(f'Unable to generate the MISP export: {event}', 'error') + 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') + error = False + events: List[MISPEvent] = [] + with_parents = request.form.get('with_parents') + if with_parents: + exports = lookyloo.misp_export(tree_uuid, True) + if isinstance(exports, dict): + flash(f'Unable to create event: {exports}', 'error') + error = True + else: + events = exports + else: + events = event + + if error: + return redirect(url_for('tree', tree_uuid=tree_uuid)) + + for e in events: + for tag in tags: + e.add_tag(tag) + + # Change the event info field of the last event in the chain + events[-1].info = request.form.get('event_info') + + try: + new_events = lookyloo.misp.push(events, True if request.form.get('force_push') else False, + True if request.form.get('auto_publish') else False) + except MISPServerError: + flash(f'MISP returned an error, the event(s) might still have been created on {lookyloo.misp.client.root_url}', 'error') + else: + if isinstance(new_events, dict): + flash(f'Unable to create event(s): {new_events}', 'error') + else: + for e in new_events: + flash(f'MISP event {e.id} created on {lookyloo.misp.client.root_url}', 'success') + return redirect(url_for('tree', tree_uuid=tree_uuid)) + else: + # the 1st attribute in the event is the link to lookyloo + existing_misp_url = lookyloo.misp.get_existing_event_url(event[-1].attributes[0].value) + + fav_tags = lookyloo.misp.get_fav_tags() + cache = lookyloo.capture_cache(tree_uuid) + + return render_template('misp_push_view.html', tree_uuid=tree_uuid, + event=event[0], fav_tags=fav_tags, + existing_event=existing_misp_url, + auto_publish=lookyloo.misp.auto_publish, + has_parent=True if cache and cache.parent else False, + default_tags=lookyloo.misp.default_tags) + + @app.route('/tree//modules', methods=['GET']) def modules(tree_uuid: str): modules_responses = lookyloo.get_modules_responses(tree_uuid) @@ -791,6 +804,13 @@ def statsfull(): return render_template('stats.html', stats=stats) +@app.route('/whois/', methods=['GET']) +def whois(query: str): + to_return = lookyloo.uwhois.whois(query) + return send_file(BytesIO(to_return.encode()), + mimetype='test/plain', as_attachment=True, attachment_filename=f'whois.{query}.txt') + + # ##### Methods related to a specific URLNode ##### @app.route('/tree//url//request_cookies', methods=['GET']) @@ -942,83 +962,6 @@ def add_context(tree_uuid: str, node_uuid: str): return redirect(url_for('ressources')) -@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') - return redirect(url_for('tree', tree_uuid=tree_uuid)) - elif not lookyloo.misp.enable_push: - flash('Push not enabled in MISP module.', 'error') - return redirect(url_for('tree', tree_uuid=tree_uuid)) - else: - event = lookyloo.misp_export(tree_uuid) - if isinstance(event, dict): - flash(f'Unable to generate the MISP export: {event}', 'error') - 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') - error = False - events: List[MISPEvent] = [] - with_parents = request.form.get('with_parents') - if with_parents: - exports = lookyloo.misp_export(tree_uuid, True) - if isinstance(exports, dict): - flash(f'Unable to create event: {exports}', 'error') - error = True - else: - events = exports - else: - events = event - - if error: - return redirect(url_for('tree', tree_uuid=tree_uuid)) - - for e in events: - for tag in tags: - e.add_tag(tag) - - # Change the event info field of the last event in the chain - events[-1].info = request.form.get('event_info') - - try: - new_events = lookyloo.misp.push(events, True if request.form.get('force_push') else False, - True if request.form.get('auto_publish') else False) - except MISPServerError: - flash(f'MISP returned an error, the event(s) might still have been created on {lookyloo.misp.client.root_url}', 'error') - else: - if isinstance(new_events, dict): - flash(f'Unable to create event(s): {new_events}', 'error') - else: - for e in new_events: - flash(f'MISP event {e.id} created on {lookyloo.misp.client.root_url}', 'success') - return redirect(url_for('tree', tree_uuid=tree_uuid)) - else: - # the 1st attribute in the event is the link to lookyloo - existing_misp_url = lookyloo.misp.get_existing_event_url(event[-1].attributes[0].value) - - fav_tags = lookyloo.misp.get_fav_tags() - cache = lookyloo.capture_cache(tree_uuid) - - return render_template('misp_push_view.html', tree_uuid=tree_uuid, - event=event[0], fav_tags=fav_tags, - existing_event=existing_misp_url, - auto_publish=lookyloo.misp.auto_publish, - has_parent=True if cache and cache.parent else False, - default_tags=lookyloo.misp.default_tags) - - -@app.route('/whois/', methods=['GET']) -def whois(query: str): - to_return = lookyloo.uwhois.whois(query) - return send_file(BytesIO(to_return.encode()), - mimetype='test/plain', as_attachment=True, attachment_filename=f'whois.{query}.txt') - - # Query API authorizations = { @@ -1035,232 +978,4 @@ api = Api(app, title='Lookyloo API', authorizations=authorizations, version=pkg_resources.get_distribution('lookyloo').version) - -def api_auth_check(method): - if flask_login.current_user.is_authenticated or load_user_from_request(request): - return method - abort(403, 'Authentication required.') - - -token_request_fields = api.model('AuthTokenFields', { - 'username': fields.String(description="Your username", required=True), - 'password': fields.String(description="Your password", required=True), -}) - - -@api.route('/json/get_token') -@api.doc(description='Get the API token required for authenticated calls') -class AuthToken(Resource): - - @api.param('username', 'Your username') - @api.param('password', 'Your password') - def get(self): - username = request.args['username'] if request.args.get('username') else False - password = request.args['password'] if request.args.get('password') else False - if username in users_table and check_password_hash(users_table[username]['password'], password): - return {'authkey': users_table[username]['authkey']} - return {'error': 'User/Password invalid.'} - - @api.doc(body=token_request_fields) - def post(self): - auth: Dict = request.get_json(force=True) - if 'username' in auth and 'password' in auth: # Expected keys in json - if (auth['username'] in users_table - and check_password_hash(users_table[auth['username']]['password'], auth['password'])): - return {'authkey': users_table[auth['username']]['authkey']} - return {'error': 'User/Password invalid.'} - - -@api.route('/json//status') -@api.doc(description='Get the status of a capture', - params={'tree_uuid': 'The UUID of the capture'}) -class CaptureStatusQuery(Resource): - def get(self, tree_uuid: str): - return {'status_code': lookyloo.get_capture_status(tree_uuid)} - - -@api.route('/json//redirects') -@api.doc(description='Get all the redirects of a capture', - params={'tree_uuid': 'The UUID of the capture'}) -class CaptureRedirects(Resource): - def get(self, tree_uuid: str): - cache = lookyloo.capture_cache(tree_uuid) - if not cache: - return {'error': 'UUID missing in cache, try again later.'} - - to_return: Dict[str, Any] = {'response': {'url': cache.url, 'redirects': []}} - if not cache.redirects: - to_return['response']['info'] = 'No redirects' - return to_return - if cache.incomplete_redirects: - # Trigger tree build, get all redirects - lookyloo.get_crawled_tree(tree_uuid) - cache = lookyloo.capture_cache(tree_uuid) - if cache: - to_return['response']['redirects'] = cache.redirects - else: - to_return['response']['redirects'] = cache.redirects - - return to_return - - -@api.route('/json//misp_export') -@api.doc(description='Get an export of the capture in MISP format', - params={'tree_uuid': 'The UUID of the capture'}) -class MISPExport(Resource): - def get(self, tree_uuid: str): - with_parents = request.args.get('with_parents') - event = lookyloo.misp_export(tree_uuid, True if with_parents else False) - if isinstance(event, dict): - return event - - to_return = [] - for e in event: - to_return.append(e.to_json(indent=2)) - return to_return - - -misp_push_fields = api.model('MISPPushFields', { - 'allow_duplicates': fields.Integer(description="Push the event even if it is already present on the MISP instance", - example=0, min=0, max=1), - 'with_parents': fields.Integer(description="Also push the parents of the capture (if any)", - example=0, min=0, max=1), -}) - - -@api.route('/json//misp_push') -@api.doc(description='Push an event to a pre-configured MISP instance', - params={'tree_uuid': 'The UUID of the capture'}, - security='apikey') -class MISPPush(Resource): - method_decorators = [api_auth_check] - - @api.param('with_parents', 'Also push the parents of the capture (if any)') - @api.param('allow_duplicates', 'Push the event even if it is already present on the MISP instance') - def get(self, tree_uuid: str): - with_parents = True if request.args.get('with_parents') else False - allow_duplicates = True if request.args.get('allow_duplicates') else False - to_return: Dict = {} - if not lookyloo.misp.available: - to_return['error'] = 'MISP module not available.' - elif not lookyloo.misp.enable_push: - to_return['error'] = 'Push not enabled in MISP module.' - else: - event = lookyloo.misp_export(tree_uuid, with_parents) - if isinstance(event, dict): - to_return['error'] = event - else: - new_events = lookyloo.misp.push(event, allow_duplicates) - if isinstance(new_events, dict): - to_return['error'] = new_events - else: - events_to_return = [] - for e in new_events: - events_to_return.append(e.to_json(indent=2)) - return events_to_return - - return to_return - - @api.doc(body=misp_push_fields) - def post(self, tree_uuid: str): - parameters: Dict = request.get_json(force=True) - with_parents = True if parameters.get('with_parents') else False - allow_duplicates = True if parameters.get('allow_duplicates') else False - - to_return: Dict = {} - if not lookyloo.misp.available: - to_return['error'] = 'MISP module not available.' - elif not lookyloo.misp.enable_push: - to_return['error'] = 'Push not enabled in MISP module.' - else: - event = lookyloo.misp_export(tree_uuid, with_parents) - if isinstance(event, dict): - to_return['error'] = event - else: - new_events = lookyloo.misp.push(event, allow_duplicates) - if isinstance(new_events, dict): - to_return['error'] = new_events - else: - events_to_return = [] - for e in new_events: - events_to_return.append(e.to_json(indent=2)) - return events_to_return - - return to_return - - -@api.route('/json/hash_info/') -@api.doc(description='Search for a ressource with a specific hash (sha512)', - params={'h': 'The hash (sha512)'}) -class HashInfo(Resource): - def get(self, h: str): - details, body = lookyloo.get_body_hash_full(h) - if not details: - return {'error': 'Unknown Hash.'} - to_return: Dict[str, Any] = {'response': {'hash': h, 'details': details, - 'body': base64.b64encode(body.getvalue()).decode()}} - return to_return - - -url_info_fields = api.model('URLInfoFields', { - 'url': fields.String(description="The URL to search", required=True), - 'limit': fields.Integer(description="The maximal amount of captures to return", example=20), -}) - - -@api.route('/json/url_info') -@api.doc(description='Search for a URL') -class URLInfo(Resource): - - @api.doc(body=url_info_fields) - def post(self): - to_query: Dict = request.get_json(force=True) - occurrences = lookyloo.get_url_occurrences(to_query.pop('url'), **to_query) - return occurrences - - -hostname_info_fields = api.model('HostnameInfoFields', { - 'hostname': fields.String(description="The hostname to search", required=True), - 'limit': fields.Integer(description="The maximal amount of captures to return", example=20), -}) - - -@api.route('/json/hostname_info') -@api.doc(description='Search for a hostname') -class HostnameInfo(Resource): - - @api.doc(body=hostname_info_fields) - def post(self): - to_query: Dict = request.get_json(force=True) - occurrences = lookyloo.get_hostname_occurrences(to_query.pop('hostname'), **to_query) - return occurrences - - -@api.route('/json/stats') -@api.doc(description='Get the statistics of the lookyloo instance.') -class InstanceStats(Resource): - def get(self): - return lookyloo.get_stats() - - -submit_fields = api.model('SubmitFields', { - 'url': fields.String(description="The URL to capture", required=True), - 'listing': fields.Integer(description="Display the capture on the index", min=0, max=1, example=1), - 'user_agent': fields.String(description="User agent to use for the capture", example=''), - 'referer': fields.String(description="Referer to pass to the capture", example=''), - 'cookies': fields.String(description="JSON export of a list of cookies as exported from an other capture", example='') -}) - - -@api.route('/submit') -class SubmitCapture(Resource): - - @api.doc(body=submit_fields) - def post(self): - if flask_login.current_user.is_authenticated: - user = flask_login.current_user.get_id() - else: - user = src_request_ip(request) - to_query: Dict = request.get_json(force=True) - perma_uuid = lookyloo.enqueue_capture(to_query, source='api', user=user, authenticated=flask_login.current_user.is_authenticated) - return Response(perma_uuid, mimetype='text/text') +api.add_namespace(generic_api) diff --git a/website/web/genericapi.py b/website/web/genericapi.py new file mode 100644 index 00000000..434069eb --- /dev/null +++ b/website/web/genericapi.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import base64 +from typing import Dict, Any + +from flask import request, Response +import flask_login # type: ignore +from flask_restx import Namespace, Resource, fields, abort # type: ignore +from werkzeug.security import check_password_hash + +from lookyloo.lookyloo import Lookyloo + +from .helpers import src_request_ip, load_user_from_request, build_users_table + +api = Namespace('GenericAPI', description='Generic Lookyloo API', path='/') + + +lookyloo: Lookyloo = Lookyloo() + + +def api_auth_check(method): + if flask_login.current_user.is_authenticated or load_user_from_request(request): + return method + abort(403, 'Authentication required.') + + +token_request_fields = api.model('AuthTokenFields', { + 'username': fields.String(description="Your username", required=True), + 'password': fields.String(description="Your password", required=True), +}) + + +@api.route('/json/get_token') +@api.doc(description='Get the API token required for authenticated calls') +class AuthToken(Resource): + + users_table = build_users_table() + + @api.param('username', 'Your username') + @api.param('password', 'Your password') + def get(self): + username = request.args['username'] if request.args.get('username') else False + password = request.args['password'] if request.args.get('password') else False + if username in self.users_table and check_password_hash(self.users_table[username]['password'], password): + return {'authkey': self.users_table[username]['authkey']} + return {'error': 'User/Password invalid.'} + + @api.doc(body=token_request_fields) + def post(self): + auth: Dict = request.get_json(force=True) + if 'username' in auth and 'password' in auth: # Expected keys in json + if (auth['username'] in self.users_table + and check_password_hash(self.users_table[auth['username']]['password'], auth['password'])): + return {'authkey': self.users_table[auth['username']]['authkey']} + return {'error': 'User/Password invalid.'} + + +@api.route('/json//status') +@api.doc(description='Get the status of a capture', + params={'tree_uuid': 'The UUID of the capture'}) +class CaptureStatusQuery(Resource): + def get(self, tree_uuid: str): + return {'status_code': lookyloo.get_capture_status(tree_uuid)} + + +@api.route('/json//redirects') +@api.doc(description='Get all the redirects of a capture', + params={'tree_uuid': 'The UUID of the capture'}) +class CaptureRedirects(Resource): + def get(self, tree_uuid: str): + cache = lookyloo.capture_cache(tree_uuid) + if not cache: + return {'error': 'UUID missing in cache, try again later.'} + + to_return: Dict[str, Any] = {'response': {'url': cache.url, 'redirects': []}} + if not cache.redirects: + to_return['response']['info'] = 'No redirects' + return to_return + if cache.incomplete_redirects: + # Trigger tree build, get all redirects + lookyloo.get_crawled_tree(tree_uuid) + cache = lookyloo.capture_cache(tree_uuid) + if cache: + to_return['response']['redirects'] = cache.redirects + else: + to_return['response']['redirects'] = cache.redirects + + return to_return + + +@api.route('/json//misp_export') +@api.doc(description='Get an export of the capture in MISP format', + params={'tree_uuid': 'The UUID of the capture'}) +class MISPExport(Resource): + def get(self, tree_uuid: str): + with_parents = request.args.get('with_parents') + event = lookyloo.misp_export(tree_uuid, True if with_parents else False) + if isinstance(event, dict): + return event + + to_return = [] + for e in event: + to_return.append(e.to_json(indent=2)) + return to_return + + +misp_push_fields = api.model('MISPPushFields', { + 'allow_duplicates': fields.Integer(description="Push the event even if it is already present on the MISP instance", + example=0, min=0, max=1), + 'with_parents': fields.Integer(description="Also push the parents of the capture (if any)", + example=0, min=0, max=1), +}) + + +@api.route('/json//misp_push') +@api.doc(description='Push an event to a pre-configured MISP instance', + params={'tree_uuid': 'The UUID of the capture'}, + security='apikey') +class MISPPush(Resource): + method_decorators = [api_auth_check] + + @api.param('with_parents', 'Also push the parents of the capture (if any)') + @api.param('allow_duplicates', 'Push the event even if it is already present on the MISP instance') + def get(self, tree_uuid: str): + with_parents = True if request.args.get('with_parents') else False + allow_duplicates = True if request.args.get('allow_duplicates') else False + to_return: Dict = {} + if not lookyloo.misp.available: + to_return['error'] = 'MISP module not available.' + elif not lookyloo.misp.enable_push: + to_return['error'] = 'Push not enabled in MISP module.' + else: + event = lookyloo.misp_export(tree_uuid, with_parents) + if isinstance(event, dict): + to_return['error'] = event + else: + new_events = lookyloo.misp.push(event, allow_duplicates) + if isinstance(new_events, dict): + to_return['error'] = new_events + else: + events_to_return = [] + for e in new_events: + events_to_return.append(e.to_json(indent=2)) + return events_to_return + + return to_return + + @api.doc(body=misp_push_fields) + def post(self, tree_uuid: str): + parameters: Dict = request.get_json(force=True) + with_parents = True if parameters.get('with_parents') else False + allow_duplicates = True if parameters.get('allow_duplicates') else False + + to_return: Dict = {} + if not lookyloo.misp.available: + to_return['error'] = 'MISP module not available.' + elif not lookyloo.misp.enable_push: + to_return['error'] = 'Push not enabled in MISP module.' + else: + event = lookyloo.misp_export(tree_uuid, with_parents) + if isinstance(event, dict): + to_return['error'] = event + else: + new_events = lookyloo.misp.push(event, allow_duplicates) + if isinstance(new_events, dict): + to_return['error'] = new_events + else: + events_to_return = [] + for e in new_events: + events_to_return.append(e.to_json(indent=2)) + return events_to_return + + return to_return + + +@api.route('/json/hash_info/') +@api.doc(description='Search for a ressource with a specific hash (sha512)', + params={'h': 'The hash (sha512)'}) +class HashInfo(Resource): + def get(self, h: str): + details, body = lookyloo.get_body_hash_full(h) + if not details: + return {'error': 'Unknown Hash.'} + to_return: Dict[str, Any] = {'response': {'hash': h, 'details': details, + 'body': base64.b64encode(body.getvalue()).decode()}} + return to_return + + +url_info_fields = api.model('URLInfoFields', { + 'url': fields.String(description="The URL to search", required=True), + 'limit': fields.Integer(description="The maximal amount of captures to return", example=20), +}) + + +@api.route('/json/url_info') +@api.doc(description='Search for a URL') +class URLInfo(Resource): + + @api.doc(body=url_info_fields) + def post(self): + to_query: Dict = request.get_json(force=True) + occurrences = lookyloo.get_url_occurrences(to_query.pop('url'), **to_query) + return occurrences + + +hostname_info_fields = api.model('HostnameInfoFields', { + 'hostname': fields.String(description="The hostname to search", required=True), + 'limit': fields.Integer(description="The maximal amount of captures to return", example=20), +}) + + +@api.route('/json/hostname_info') +@api.doc(description='Search for a hostname') +class HostnameInfo(Resource): + + @api.doc(body=hostname_info_fields) + def post(self): + to_query: Dict = request.get_json(force=True) + occurrences = lookyloo.get_hostname_occurrences(to_query.pop('hostname'), **to_query) + return occurrences + + +@api.route('/json/stats') +@api.doc(description='Get the statistics of the lookyloo instance.') +class InstanceStats(Resource): + def get(self): + return lookyloo.get_stats() + + +submit_fields = api.model('SubmitFields', { + 'url': fields.String(description="The URL to capture", required=True), + 'listing': fields.Integer(description="Display the capture on the index", min=0, max=1, example=1), + 'user_agent': fields.String(description="User agent to use for the capture", example=''), + 'referer': fields.String(description="Referer to pass to the capture", example=''), + 'cookies': fields.String(description="JSON export of a list of cookies as exported from an other capture", example='') +}) + + +@api.route('/submit') +class SubmitCapture(Resource): + + @api.doc(body=submit_fields) + def post(self): + if flask_login.current_user.is_authenticated: + user = flask_login.current_user.get_id() + else: + user = src_request_ip(request) + to_query: Dict = request.get_json(force=True) + perma_uuid = lookyloo.enqueue_capture(to_query, source='api', user=user, authenticated=flask_login.current_user.is_authenticated) + return Response(perma_uuid, mimetype='text/text') + + +@api.route('/json//stats') +@api.doc(description='Get the statistics of the capture.') +class CaptureStats(Resource): + def get(self, tree_uuid: str): + return lookyloo.get_statistics(tree_uuid) diff --git a/website/web/helpers.py b/website/web/helpers.py new file mode 100644 index 00000000..74a083e4 --- /dev/null +++ b/website/web/helpers.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import hashlib +import os + +from functools import lru_cache +from pathlib import Path +from typing import Dict, List, Union + +import flask_login # type: ignore + +from werkzeug.security import generate_password_hash + +from lookyloo.helpers import get_homedir, get_config + + +def src_request_ip(request) -> str: + # NOTE: X-Real-IP is the IP passed by the reverse proxy in the headers. + real_ip = request.headers.get('X-Real-IP') + if not real_ip: + real_ip = request.remote_addr + return real_ip + + +class User(flask_login.UserMixin): + pass + + +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() + keys_table = build_keys_table() + if api_key in keys_table: + user.id = keys_table[api_key] + return user + return None + + +@lru_cache(64) +def build_keys_table() -> Dict[str, str]: + keys_table = {} + for username, authstuff in build_users_table().items(): + if 'authkey' in authstuff: + keys_table[authstuff['authkey']] = username + return keys_table + + +@lru_cache(64) +def get_users() -> Dict[str, Union[str, List[str]]]: + try: + # Use legacy user mgmt, no need to print a warning, and it will fail on new install. + return get_config('generic', 'cache_clean_user', quiet=True) + except Exception: + return get_config('generic', 'users') + + +@lru_cache(64) +def build_users_table() -> Dict[str, Dict[str, str]]: + users_table: Dict[str, Dict[str, str]] = {} + for username, authstuff in get_users().items(): + if isinstance(authstuff, str): + # just a password, make a key + users_table[username] = {} + users_table[username]['password'] = generate_password_hash(authstuff) + users_table[username]['authkey'] = hashlib.pbkdf2_hmac('sha256', get_secret_key(), + authstuff.encode(), + 100000).hex() + + elif isinstance(authstuff, list) and len(authstuff) == 2: + if isinstance(authstuff[0], str) and isinstance(authstuff[1], str) and len(authstuff[1]) == 64: + users_table[username] = {} + users_table[username]['password'] = generate_password_hash(authstuff[0]) + users_table[username]['authkey'] = authstuff[1] + else: + raise Exception('User setup invalid. Must be "username": "password" or "username": ["password", "token 64 chars (sha256)"]') + return users_table + + +@lru_cache(64) +def get_secret_key() -> bytes: + secret_file_path: Path = get_homedir() / 'secret_key' + if not secret_file_path.exists() or secret_file_path.stat().st_size < 64: + if not secret_file_path.exists() or secret_file_path.stat().st_size < 64: + with secret_file_path.open('wb') as f: + f.write(os.urandom(64)) + with secret_file_path.open('rb') as f: + return f.read() From 810cceb26378158dba4ee492f26b0fa76c3c81f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Mon, 7 Jun 2021 15:37:11 -0700 Subject: [PATCH 03/18] new: Add more calls to the API, cleanup --- website/web/__init__.py | 20 ++--- website/web/genericapi.py | 154 +++++++++++++++++++++++++++++++------- 2 files changed, 137 insertions(+), 37 deletions(-) diff --git a/website/web/__init__.py b/website/web/__init__.py index f9237b51..6638cd13 100644 --- a/website/web/__init__.py +++ b/website/web/__init__.py @@ -223,16 +223,6 @@ def hostnode_popup(tree_uuid: str, node_uuid: str): # ##### Tree level Methods ##### -@app.route('/tree//rebuild') -@flask_login.login_required -def rebuild_tree(tree_uuid: str): - try: - lookyloo.remove_pickle(tree_uuid) - return redirect(url_for('tree', tree_uuid=tree_uuid)) - except Exception: - return redirect(url_for('index')) - - @app.route('/tree//trigger_modules', methods=['GET']) def trigger_modules(tree_uuid: str): force = True if request.args.get('force') else False @@ -495,6 +485,16 @@ def hide_capture(tree_uuid: str): return redirect(url_for('tree', tree_uuid=tree_uuid)) +@app.route('/tree//rebuild') +@flask_login.login_required +def rebuild_tree(tree_uuid: str): + try: + lookyloo.remove_pickle(tree_uuid) + return redirect(url_for('tree', tree_uuid=tree_uuid)) + except Exception: + return redirect(url_for('index')) + + @app.route('/tree//cache', methods=['GET']) def cache_tree(tree_uuid: str): lookyloo.capture_cache(tree_uuid) diff --git a/website/web/genericapi.py b/website/web/genericapi.py index 434069eb..7bb7bc24 100644 --- a/website/web/genericapi.py +++ b/website/web/genericapi.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import json import base64 from typing import Dict, Any -from flask import request, Response +from flask import request, send_file import flask_login # type: ignore from flask_restx import Namespace, Resource, fields, abort # type: ignore from werkzeug.security import check_password_hash @@ -56,20 +57,20 @@ class AuthToken(Resource): return {'error': 'User/Password invalid.'} -@api.route('/json//status') +@api.route('/json//status') @api.doc(description='Get the status of a capture', - params={'tree_uuid': 'The UUID of the capture'}) + params={'capture_uuid': 'The UUID of the capture'}) class CaptureStatusQuery(Resource): - def get(self, tree_uuid: str): - return {'status_code': lookyloo.get_capture_status(tree_uuid)} + def get(self, capture_uuid: str): + return {'status_code': lookyloo.get_capture_status(capture_uuid)} -@api.route('/json//redirects') +@api.route('/json//redirects') @api.doc(description='Get all the redirects of a capture', - params={'tree_uuid': 'The UUID of the capture'}) + params={'capture_uuid': 'The UUID of the capture'}) class CaptureRedirects(Resource): - def get(self, tree_uuid: str): - cache = lookyloo.capture_cache(tree_uuid) + def get(self, capture_uuid: str): + cache = lookyloo.capture_cache(capture_uuid) if not cache: return {'error': 'UUID missing in cache, try again later.'} @@ -79,8 +80,8 @@ class CaptureRedirects(Resource): return to_return if cache.incomplete_redirects: # Trigger tree build, get all redirects - lookyloo.get_crawled_tree(tree_uuid) - cache = lookyloo.capture_cache(tree_uuid) + lookyloo.get_crawled_tree(capture_uuid) + cache = lookyloo.capture_cache(capture_uuid) if cache: to_return['response']['redirects'] = cache.redirects else: @@ -89,13 +90,13 @@ class CaptureRedirects(Resource): return to_return -@api.route('/json//misp_export') +@api.route('/json//misp_export') @api.doc(description='Get an export of the capture in MISP format', - params={'tree_uuid': 'The UUID of the capture'}) + params={'capture_uuid': 'The UUID of the capture'}) class MISPExport(Resource): - def get(self, tree_uuid: str): + def get(self, capture_uuid: str): with_parents = request.args.get('with_parents') - event = lookyloo.misp_export(tree_uuid, True if with_parents else False) + event = lookyloo.misp_export(capture_uuid, True if with_parents else False) if isinstance(event, dict): return event @@ -113,16 +114,16 @@ misp_push_fields = api.model('MISPPushFields', { }) -@api.route('/json//misp_push') +@api.route('/json//misp_push') @api.doc(description='Push an event to a pre-configured MISP instance', - params={'tree_uuid': 'The UUID of the capture'}, + params={'capture_uuid': 'The UUID of the capture'}, security='apikey') class MISPPush(Resource): method_decorators = [api_auth_check] @api.param('with_parents', 'Also push the parents of the capture (if any)') @api.param('allow_duplicates', 'Push the event even if it is already present on the MISP instance') - def get(self, tree_uuid: str): + def get(self, capture_uuid: str): with_parents = True if request.args.get('with_parents') else False allow_duplicates = True if request.args.get('allow_duplicates') else False to_return: Dict = {} @@ -131,7 +132,7 @@ class MISPPush(Resource): elif not lookyloo.misp.enable_push: to_return['error'] = 'Push not enabled in MISP module.' else: - event = lookyloo.misp_export(tree_uuid, with_parents) + event = lookyloo.misp_export(capture_uuid, with_parents) if isinstance(event, dict): to_return['error'] = event else: @@ -147,7 +148,7 @@ class MISPPush(Resource): return to_return @api.doc(body=misp_push_fields) - def post(self, tree_uuid: str): + def post(self, capture_uuid: str): parameters: Dict = request.get_json(force=True) with_parents = True if parameters.get('with_parents') else False allow_duplicates = True if parameters.get('allow_duplicates') else False @@ -158,7 +159,7 @@ class MISPPush(Resource): elif not lookyloo.misp.enable_push: to_return['error'] = 'Push not enabled in MISP module.' else: - event = lookyloo.misp_export(tree_uuid, with_parents) + event = lookyloo.misp_export(capture_uuid, with_parents) if isinstance(event, dict): to_return['error'] = event else: @@ -237,10 +238,29 @@ submit_fields = api.model('SubmitFields', { }) +@api.route('/json//stats') +@api.doc(description='Get the statistics of the capture.', + params={'capture_uuid': 'The UUID of the capture'}) +class CaptureStats(Resource): + def get(self, capture_uuid: str): + return lookyloo.get_statistics(capture_uuid) + + +@api.route('/json//cookies') +@api.doc(description='Get the complete cookie jar created during the capture.', + params={'capture_uuid': 'The UUID of the capture'}) +class CaptureCookies(Resource): + def get(self, capture_uuid: str): + return json.loads(lookyloo.get_cookies(capture_uuid).read()) + + +# Just text + @api.route('/submit') class SubmitCapture(Resource): @api.doc(body=submit_fields) + @api.produces(['text/text']) def post(self): if flask_login.current_user.is_authenticated: user = flask_login.current_user.get_id() @@ -248,11 +268,91 @@ class SubmitCapture(Resource): user = src_request_ip(request) to_query: Dict = request.get_json(force=True) perma_uuid = lookyloo.enqueue_capture(to_query, source='api', user=user, authenticated=flask_login.current_user.is_authenticated) - return Response(perma_uuid, mimetype='text/text') + return perma_uuid -@api.route('/json//stats') -@api.doc(description='Get the statistics of the capture.') -class CaptureStats(Resource): - def get(self, tree_uuid: str): - return lookyloo.get_statistics(tree_uuid) +# Binary stuff + +@api.route('/bin//screenshot') +@api.doc(description='Get the screenshot associated to the capture.', + params={'capture_uuid': 'The UUID of the capture'}) +class CaptureScreenshot(Resource): + + @api.produces(['image/png']) + def get(self, capture_uuid: str): + return send_file(lookyloo.get_screenshot(capture_uuid), mimetype='image/png') + + +@api.route('/bin//export') +@api.doc(description='Get all the files generated by the capture, except the pickle.', + params={'capture_uuid': 'The UUID of the capture'}) +class CaptureExport(Resource): + + @api.produces(['application/zip']) + def get(self, capture_uuid: str): + return send_file(lookyloo.get_capture(capture_uuid), mimetype='application/zip') + + +# Admin stuff + +@api.route('/admin/rebuild_all') +@api.doc(description='Rebuild all the trees. WARNING: IT IS GOING TO TAKE A VERY LONG TIME.', + security='apikey') +class RebuildAll(Resource): + method_decorators = [api_auth_check] + + def post(self): + try: + lookyloo.rebuild_all() + except Exception as e: + return {'error': f'Unable to rebuild all captures: {e}.'} + else: + return {'info': 'Captures successfully rebuilt.'} + + +@api.route('/admin/rebuild_all_cache') +@api.doc(description='Rebuild all the caches. It will take a while, but less that rebuild all.', + security='apikey') +class RebuildAllCache(Resource): + method_decorators = [api_auth_check] + + def post(self): + try: + lookyloo.rebuild_cache() + except Exception as e: + return {'error': f'Unable to rebuild all the caches: {e}.'} + else: + return {'info': 'All caches successfully rebuilt.'} + + +@api.route('/admin//rebuild') +@api.doc(description='Rebuild the tree.', + params={'capture_uuid': 'The UUID of the capture'}, + security='apikey') +class CaptureRebuildTree(Resource): + method_decorators = [api_auth_check] + + def post(self, capture_uuid): + try: + lookyloo.remove_pickle(capture_uuid) + lookyloo.get_crawled_tree(capture_uuid) + except Exception as e: + return {'error': f'Unable to rebuild tree: {e}.'} + else: + return {'info': f'Tree {capture_uuid} successfully rebuilt.'} + + +@api.route('/admin//hide') +@api.doc(description='Hide the capture from the index.', + params={'capture_uuid': 'The UUID of the capture'}, + security='apikey') +class CaptureHide(Resource): + method_decorators = [api_auth_check] + + def post(self, capture_uuid): + try: + lookyloo.hide_capture(capture_uuid) + except Exception as e: + return {'error': f'Unable to hide the tree: {e}.'} + else: + return {'info': f'Capture {capture_uuid} successfully hidden.'} From befcfbbe1dae7e3772d88b3b740a48d4abf74c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 8 Jun 2021 08:46:24 -0700 Subject: [PATCH 04/18] chg: Bump deps --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 773ff23f..bd683642 100644 --- a/poetry.lock +++ b/poetry.lock @@ -793,7 +793,7 @@ docs = ["Sphinx (>=3.5.4,<4.0.0)", "myst-parser (>=0.14.0,<0.15.0)"] [[package]] name = "pymisp" -version = "2.4.143" +version = "2.4.144" description = "Python API for MISP." category = "main" optional = false @@ -1796,8 +1796,8 @@ pylookyloo = [ {file = "pylookyloo-1.6.0.tar.gz", hash = "sha256:3daa751f3400c6365a9c145bebc58f56b2f0605ee8eaf28a6ed6042bcc3800ed"}, ] pymisp = [ - {file = "pymisp-2.4.143-py3-none-any.whl", hash = "sha256:ceb6029045cfd2ab803902dc82e761909ca5337c2f6df484aef4e067e2f08d82"}, - {file = "pymisp-2.4.143.tar.gz", hash = "sha256:a51cddc4f274ec8daf7ccd0dd1fd599ee755ed77d168e72dfad01a41823877e3"}, + {file = "pymisp-2.4.144-py3-none-any.whl", hash = "sha256:81be4569199c117513d35d06f3cd46d5b412c1e8509ec1cfa0e6bfd19751ef4f"}, + {file = "pymisp-2.4.144.tar.gz", hash = "sha256:cf04fe19391fed4251cef1f267d80a19e37ed4b88602fb5f9790a3cd814d9d00"}, ] pyopenssl = [ {file = "pyOpenSSL-20.0.1-py2.py3-none-any.whl", hash = "sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"}, From 3d2c7420a46ba7301e480f24c291ba9f08b6bad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Wed, 9 Jun 2021 12:04:26 -0700 Subject: [PATCH 05/18] chg: Bump mypy --- lookyloo/indexing.py | 18 +++---- lookyloo/lookyloo.py | 2 +- poetry.lock | 114 +++++++++++++++++++++---------------------- pyproject.toml | 4 +- 4 files changed, 68 insertions(+), 70 deletions(-) diff --git a/lookyloo/indexing.py b/lookyloo/indexing.py index fae26a37..c4df4196 100644 --- a/lookyloo/indexing.py +++ b/lookyloo/indexing.py @@ -37,7 +37,7 @@ class Indexing(): return self.redis.zrevrange(f'cn|{cookie_name}', 0, -1, withscores=True) def get_cookies_names_captures(self, cookie_name: str) -> List[Tuple[str, str]]: - return [uuids.split('|') for uuids in self.redis.smembers(f'cn|{cookie_name}|captures')] # type: ignore + return [uuids.split('|') for uuids in self.redis.smembers(f'cn|{cookie_name}|captures')] def index_cookies_capture(self, crawled_tree: CrawledTree) -> None: if self.redis.sismember('indexed_cookies', crawled_tree.uuid): @@ -74,8 +74,8 @@ class Indexing(): pipeline.zincrby('aggregate_domains_cn', cn_freq, f'{main_domain_part}|{cn}') pipeline.zincrby('aggregate_cn_domains', d_freq, f'{cn}|{main_domain_part}') pipeline.execute() - aggregate_domains_cn = self.redis.zrevrange('aggregate_domains_cn', 0, -1, withscores=True) - aggregate_cn_domains = self.redis.zrevrange('aggregate_cn_domains', 0, -1, withscores=True) + aggregate_domains_cn: List[Tuple[str, float]] = self.redis.zrevrange('aggregate_domains_cn', 0, -1, withscores=True) + aggregate_cn_domains: List[Tuple[str, float]] = self.redis.zrevrange('aggregate_cn_domains', 0, -1, withscores=True) self.redis.delete('aggregate_domains_cn') self.redis.delete('aggregate_cn_domains') return {'domains': aggregate_domains_cn, 'cookies': aggregate_cn_domains} @@ -129,7 +129,7 @@ class Indexing(): filter_capture_uuid: Optional[str]=None, limit: int=20) -> Tuple[int, List[Tuple[str, str, str, bool]]]: to_return: List[Tuple[str, str, str, bool]] = [] - all_captures: Set[str] = self.redis.smembers(f'bh|{body_hash}|captures') # type: ignore + all_captures: Set[str] = self.redis.smembers(f'bh|{body_hash}|captures') len_captures = len(all_captures) for capture_uuid in list(all_captures)[:limit]: if capture_uuid == filter_capture_uuid: @@ -149,7 +149,7 @@ class Indexing(): return self.redis.zrevrange(f'bh|{body_hash}', 0, -1, withscores=True) def get_body_hash_urls(self, body_hash: str) -> Dict[str, List[Dict[str, str]]]: - all_captures: Set[str] = self.redis.smembers(f'bh|{body_hash}|captures') # type: ignore + all_captures: Set[str] = self.redis.smembers(f'bh|{body_hash}|captures') urls = defaultdict(list) for capture_uuid in list(all_captures): for entry in self.redis.zrevrange(f'bh|{body_hash}|captures|{capture_uuid}', 0, -1): @@ -189,16 +189,16 @@ class Indexing(): def get_captures_url(self, url: str) -> Set[str]: m = hashlib.md5() m.update(url.encode()) - return self.redis.smembers(f'urls|{m.hexdigest()}|captures') # type: ignore + return self.redis.smembers(f'urls|{m.hexdigest()}|captures') def get_captures_hostname(self, hostname: str) -> Set[str]: - return self.redis.smembers(f'hostnames|{hostname}|captures') # type: ignore + return self.redis.smembers(f'hostnames|{hostname}|captures') # ###### Categories ###### @property def categories(self) -> List[Tuple[str, int]]: - return [(c, int(score)) + return [(c, int(score)) # type: ignore for c, score in self.redis.zrevrange('categories', 0, 200, withscores=True)] def index_categories_capture(self, capture_uuid: str, categories: Iterable[str]): @@ -217,4 +217,4 @@ class Indexing(): pipeline.execute() def get_captures_category(self, category: str) -> Set[str]: - return self.redis.smembers(category) # type: ignore + return self.redis.smembers(category) diff --git a/lookyloo/lookyloo.py b/lookyloo/lookyloo.py index 287fc39d..0f74c57e 100644 --- a/lookyloo/lookyloo.py +++ b/lookyloo/lookyloo.py @@ -657,7 +657,7 @@ class Lookyloo(): if not value or not value[0]: return None uuid, score = value[0] - queue = self.redis.get(f'{uuid}_mgmt') + queue: str = self.redis.get(f'{uuid}_mgmt') # type: ignore self.redis.sadd('ongoing', uuid) lazy_cleanup = self.redis.pipeline() diff --git a/poetry.lock b/poetry.lock index bd683642..6658e81f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -578,7 +578,7 @@ python-versions = ">=3.6" [[package]] name = "mypy" -version = "0.812" +version = "0.901" description = "Optional static typing for Python" category = "dev" optional = false @@ -586,11 +586,12 @@ python-versions = ">=3.5" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" -typed-ast = ">=1.4.0,<1.5.0" +toml = "*" typing-extensions = ">=3.7.4" [package.extras] dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<1.5.0)"] [[package]] name = "mypy-extensions" @@ -1024,6 +1025,14 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "traitlets" version = "5.0.5" @@ -1080,9 +1089,17 @@ optional = false python-versions = "*" [[package]] -name = "typed-ast" -version = "1.4.3" -description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "types-redis" +version = "3.5.0" +description = "Typing stubs for redis" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-requests" +version = "0.1.9" +description = "Typing stubs for requests" category = "dev" optional = false python-versions = "*" @@ -1189,7 +1206,7 @@ misp = ["python-magic", "pydeep"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "27c09f95d480f1517a425ed98847d3096d0ec22b7cccf0727189a31e4206a9ed" +content-hash = "8ab8e928500dbdec94ae776c840a95e5b21b1f385eca361a75601677e9ac855d" [metadata.files] aiohttp = [ @@ -1615,28 +1632,29 @@ multidict = [ {file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"}, ] mypy = [ - {file = "mypy-0.812-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49"}, - {file = "mypy-0.812-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c"}, - {file = "mypy-0.812-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521"}, - {file = "mypy-0.812-cp35-cp35m-win_amd64.whl", hash = "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"}, - {file = "mypy-0.812-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a"}, - {file = "mypy-0.812-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c"}, - {file = "mypy-0.812-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6"}, - {file = "mypy-0.812-cp36-cp36m-win_amd64.whl", hash = "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064"}, - {file = "mypy-0.812-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56"}, - {file = "mypy-0.812-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8"}, - {file = "mypy-0.812-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7"}, - {file = "mypy-0.812-cp37-cp37m-win_amd64.whl", hash = "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564"}, - {file = "mypy-0.812-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506"}, - {file = "mypy-0.812-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5"}, - {file = "mypy-0.812-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66"}, - {file = "mypy-0.812-cp38-cp38-win_amd64.whl", hash = "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e"}, - {file = "mypy-0.812-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a"}, - {file = "mypy-0.812-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a"}, - {file = "mypy-0.812-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97"}, - {file = "mypy-0.812-cp39-cp39-win_amd64.whl", hash = "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df"}, - {file = "mypy-0.812-py3-none-any.whl", hash = "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4"}, - {file = "mypy-0.812.tar.gz", hash = "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119"}, + {file = "mypy-0.901-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:91211acf1485a1db0b1261bc5f9ed450cba3c0dfd8da0a6680e94827591e34d7"}, + {file = "mypy-0.901-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c8bc628961cca4335ac7d1f2ed59b7125d9252fe4c78c3d66d30b50162359c99"}, + {file = "mypy-0.901-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4a622faa3be76114cdce009f8ec173401494cf9e8f22713e7ae75fee9d906ab3"}, + {file = "mypy-0.901-cp35-cp35m-win_amd64.whl", hash = "sha256:8183561bfd950e93eeab8379ae5ec65873c856f5b58498d23aa8691f74c86030"}, + {file = "mypy-0.901-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:da914faaa80c25f463913da6db12adba703822a768f452f29f75b40bb4357139"}, + {file = "mypy-0.901-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:307a6c047596d768c3d689734307e47a91596eb9dbb67cfdf7d1fd9117b27f13"}, + {file = "mypy-0.901-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:a85c6759dcc6a9884131fa06a037bd34352aa3947e7f5d9d5a35652cc3a44bcd"}, + {file = "mypy-0.901-cp36-cp36m-win_amd64.whl", hash = "sha256:9941b685807b60c58020bb67b3217c9df47820dcd00425f55cdf71f31d3c42d9"}, + {file = "mypy-0.901-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:08cf1f31029612e1008a9432337ca4b1fbac989ff7c8200e2c9ec42705cd4c7b"}, + {file = "mypy-0.901-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bc61153eb4df769538bb4a6e1045f59c2e6119339690ec719feeacbfc3809e89"}, + {file = "mypy-0.901-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:1cd241966a35036f936d4739bd71a1c64e15f02bf7d12bb2815cccfb2993a9de"}, + {file = "mypy-0.901-cp37-cp37m-win_amd64.whl", hash = "sha256:97be0e8ed116f7f79472a49cf06dd45dd806771142401f684d4f13ee652a63c0"}, + {file = "mypy-0.901-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79beb6741df15395908ecc706b3a593a98804c1d5b5b6bd0c5b03b67c7ac03a0"}, + {file = "mypy-0.901-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf347c327c48d963bdef5bf365215d3e98b5fddbe5069fc796cec330e8235a20"}, + {file = "mypy-0.901-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:053b92ebae901fc7954677949049f70133f2f63e3e83dc100225c26d6a46fe95"}, + {file = "mypy-0.901-cp38-cp38-win_amd64.whl", hash = "sha256:f208cc967e566698c4e30a1f65843fc88d8da05a8693bac8b975417e0aee9ced"}, + {file = "mypy-0.901-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c86e3f015bfe7958646825d41c0691c6e5a5cd4015e3409b5c29c18a3c712534"}, + {file = "mypy-0.901-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8577d30daf1b7b6582020f539f76e78ee1ed64a0323b28c8e0333c45db9369f"}, + {file = "mypy-0.901-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:5ddd8f4096d5fc2e7d7bb3924ac22758862163ad2c1cdc902c4b85568160e90a"}, + {file = "mypy-0.901-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:4b54518e399c3f4dc53380d4252c83276b2e60623cfc5274076eb8aae57572ac"}, + {file = "mypy-0.901-cp39-cp39-win_amd64.whl", hash = "sha256:7845ad3a31407bfbd64c76d032c16ab546d282930f747023bf07c17b054bebc5"}, + {file = "mypy-0.901-py3-none-any.whl", hash = "sha256:61b10ba18a01d05fc46adbf4f18b0e92178f6b5fd0f45926ffc2a408b5419728"}, + {file = "mypy-0.901.tar.gz", hash = "sha256:18753a8bb9bcf031ff10009852bd48d781798ecbccf45be5449892e6af4e3f9f"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -1865,6 +1883,10 @@ soupsieve = [ {file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"}, {file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"}, ] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] traitlets = [ {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, @@ -1891,37 +1913,11 @@ twisted-iocpsupport = [ {file = "twisted_iocpsupport-1.0.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:ed8cb959d7dce287419e766267a280139fedf14217d85695d7b6384d8ccd3353"}, {file = "twisted_iocpsupport-1.0.1-pp37-pypy37_pp73-win32.whl", hash = "sha256:1535e537b6d60d0cfd2b4785f1c7a021beb62d8df6bac3dc6c45bb866ce648e4"}, ] -typed-ast = [ - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, - {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, - {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, - {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, - {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, - {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, - {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, - {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, - {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, - {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, +types-redis = [ + {file = "types_redis-3.5.0-py2.py3-none-any.whl", hash = "sha256:cf29f55ccc3b44c78259cc22e4c0030f77b7a47ec5656211402be0fcf1a98536"}, +] +types-requests = [ + {file = "types_requests-0.1.9-py2.py3-none-any.whl", hash = "sha256:24a51b692d36101e5b81f08733d74b944ccae3e90589fb712e4dfc6f36965c35"}, ] typing-extensions = [ {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, diff --git a/pyproject.toml b/pyproject.toml index 8cf6acbc..ace74f20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,8 +63,10 @@ flask-restx = "^0.4.0" misp = ['python-magic', 'pydeep'] [tool.poetry.dev-dependencies] -mypy = "^0.812" +mypy = "^0.901" ipython = "^7.23.1" +types-redis = "^3.5.0" +types-requests = "^0.1.8" [build-system] requires = ["poetry_core>=1.0", "setuptools"] From c52509614e11a15f613dfb2bbabdc48277c4ad9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Wed, 9 Jun 2021 17:59:24 -0700 Subject: [PATCH 06/18] new: get capture info API call --- lookyloo/lookyloo.py | 7 +++++++ website/web/__init__.py | 4 ++-- website/web/genericapi.py | 8 ++++++++ website/web/templates/tree.html | 12 ++++++------ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lookyloo/lookyloo.py b/lookyloo/lookyloo.py index 0f74c57e..3efc2200 100644 --- a/lookyloo/lookyloo.py +++ b/lookyloo/lookyloo.py @@ -324,6 +324,13 @@ class Lookyloo(): ct = self.get_crawled_tree(capture_uuid) return ct.root_hartree.stats + def get_info(self, capture_uuid: str, /) -> Dict[str, Any]: + '''Get basic information about the capture.''' + ct = self.get_crawled_tree(capture_uuid) + to_return = {'url': ct.root_url, 'title': ct.root_hartree.har.initial_title, + 'capture_time': ct.start_time.isoformat(), 'user_agent': ct.user_agent} + return to_return + def get_meta(self, capture_uuid: str, /) -> Dict[str, str]: '''Get the meta informations from a capture (mostly, details about the User Agent used.)''' capture_dir = self._get_capture_dir(capture_uuid) diff --git a/website/web/__init__.py b/website/web/__init__.py index 6638cd13..4f6df541 100644 --- a/website/web/__init__.py +++ b/website/web/__init__.py @@ -550,6 +550,7 @@ def tree(tree_uuid: str, node_uuid: Optional[str]=None): ct = lookyloo.get_crawled_tree(tree_uuid) b64_thumbnail = lookyloo.get_screenshot_thumbnail(tree_uuid, for_datauri=True) screenshot_size = lookyloo.get_screenshot(tree_uuid).getbuffer().nbytes + info = lookyloo.get_info(tree_uuid) meta = lookyloo.get_meta(tree_uuid) hostnode_to_highlight = None if node_uuid: @@ -567,8 +568,7 @@ def tree(tree_uuid: str, node_uuid: Optional[str]=None): 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, + info=info, tree_uuid=tree_uuid, public_domain=lookyloo.public_domain, screenshot_thumbnail=b64_thumbnail, page_title=cache.title, screenshot_size=screenshot_size, diff --git a/website/web/genericapi.py b/website/web/genericapi.py index 7bb7bc24..6232020f 100644 --- a/website/web/genericapi.py +++ b/website/web/genericapi.py @@ -246,6 +246,14 @@ class CaptureStats(Resource): return lookyloo.get_statistics(capture_uuid) +@api.route('/json//info') +@api.doc(description='Get basic information about the capture.', + params={'capture_uuid': 'The UUID of the capture'}) +class CaptureInfo(Resource): + def get(self, capture_uuid: str): + return lookyloo.get_info(capture_uuid) + + @api.route('/json//cookies') @api.doc(description='Get the complete cookie jar created during the capture.', params={'capture_uuid': 'The UUID of the capture'}) diff --git a/website/web/templates/tree.html b/website/web/templates/tree.html index 856b8ad0..f4dd7663 100644 --- a/website/web/templates/tree.html +++ b/website/web/templates/tree.html @@ -3,14 +3,14 @@ {% from 'bootstrap/utils.html' import render_messages %} {% from "macros.html" import shorten_string %} -{% block title %}Capture of {{root_url}}{% endblock %} +{% block title %}Capture of {{info['url']}}{% endblock %} {% block card %} { document.getElementById("start_time").innerHTML = `${capture_starttime.getFullYear()}-${("0" + (capture_starttime.getMonth() + 1)).slice(-2)}-${("0" + capture_starttime.getDate()).slice(-2)} ${capture_starttime.toLocaleTimeString()}`; @@ -342,16 +342,16 @@