new: Show current user and config on web

pull/920/head
Raphaël Vinot 2024-06-17 19:04:08 +02:00
parent 59503d6378
commit eee8e32671
9 changed files with 107 additions and 43 deletions

View File

@ -2,7 +2,8 @@ import logging
from .context import Context # noqa from .context import Context # noqa
from .indexing import Indexing # noqa from .indexing import Indexing # noqa
from .lookyloo import Lookyloo, CaptureSettings # noqa from .helpers import CaptureSettings # noqa
from .lookyloo import Lookyloo # noqa
logging.getLogger(__name__).addHandler(logging.NullHandler()) logging.getLogger(__name__).addHandler(logging.NullHandler())

View File

@ -291,7 +291,7 @@ class CapturesIndex(Mapping): # type: ignore[type-arg]
if hasattr(cc, 'timestamp'): if hasattr(cc, 'timestamp'):
recent_captures[uuid] = cc.timestamp.timestamp() recent_captures[uuid] = cc.timestamp.timestamp()
if recent_captures: if recent_captures:
self.redis.zadd('recent_captures', recent_captures) self.redis.zadd('recent_captures', recent_captures, nx=True)
def _get_capture_dir(self, uuid: str) -> str: def _get_capture_dir(self, uuid: str) -> str:
# Try to get from the recent captures cache in redis # Try to get from the recent captures cache in redis

View File

@ -20,6 +20,7 @@ from urllib.parse import urlparse
from har2tree import CrawledTree, HostNode, URLNode from har2tree import CrawledTree, HostNode, URLNode
from lacuscore import CaptureSettings as LacuscoreCaptureSettings
from playwrightcapture import get_devices from playwrightcapture import get_devices
from publicsuffixlist import PublicSuffixList # type: ignore[import-untyped] from publicsuffixlist import PublicSuffixList # type: ignore[import-untyped]
from pytaxonomies import Taxonomies # type: ignore[attr-defined] from pytaxonomies import Taxonomies # type: ignore[attr-defined]
@ -392,3 +393,30 @@ class ParsedUserAgent(UserAgent):
def __str__(self) -> str: def __str__(self) -> str:
return f'OS: {self.platform} - Browser: {self.browser} {self.version} - UA: {self.string}' return f'OS: {self.platform} - Browser: {self.browser} {self.version} - UA: {self.string}'
class CaptureSettings(LacuscoreCaptureSettings, total=False):
'''The capture settings that can be passed to Lookyloo'''
listing: int | None
not_queued: int | None
auto_report: bool | str | dict[str, str] | None # {'email': , 'comment': , 'recipient_mail':}
dnt: str | None
browser_name: str | None
os: str | None
parent: str | None
# overwrite set to True means the settings in the config file overwrite the settings
# provided by the user. False will simply append the settings from the config file if they
# don't exist.
class UserCaptureSettings(CaptureSettings, total=False):
overwrite: bool
@lru_cache(64)
def load_user_config(username: str) -> UserCaptureSettings | None:
user_config_path = get_homedir() / 'config' / 'users' / f'{username}.json'
if not user_config_path.exists():
return None
with user_config_path.open() as _c:
return json.load(_c)

View File

@ -16,7 +16,6 @@ import time
from collections import defaultdict from collections import defaultdict
from datetime import date, datetime, timedelta, timezone from datetime import date, datetime, timedelta, timezone
from functools import lru_cache
from email.message import EmailMessage from email.message import EmailMessage
from functools import cached_property from functools import cached_property
from io import BytesIO from io import BytesIO
@ -34,7 +33,8 @@ from lacuscore import (LacusCore,
CaptureStatus as CaptureStatusCore, CaptureStatus as CaptureStatusCore,
# CaptureResponse as CaptureResponseCore) # CaptureResponse as CaptureResponseCore)
# CaptureResponseJson as CaptureResponseJsonCore, # CaptureResponseJson as CaptureResponseJsonCore,
CaptureSettings as CaptureSettingsCore) # CaptureSettings as CaptureSettingsCore
)
from PIL import Image, UnidentifiedImageError from PIL import Image, UnidentifiedImageError
from playwrightcapture import get_devices from playwrightcapture import get_devices
from puremagic import from_string, PureError # type: ignore[import-untyped] from puremagic import from_string, PureError # type: ignore[import-untyped]
@ -58,7 +58,8 @@ from .exceptions import (MissingCaptureDirectory,
from .helpers import (get_captures_dir, get_email_template, from .helpers import (get_captures_dir, get_email_template,
get_resources_hashes, get_taxonomies, get_resources_hashes, get_taxonomies,
uniq_domains, ParsedUserAgent, load_cookies, UserAgents, uniq_domains, ParsedUserAgent, load_cookies, UserAgents,
get_useragent_for_requests, load_takedown_filters get_useragent_for_requests, load_takedown_filters,
CaptureSettings, UserCaptureSettings, load_user_config
) )
from .modules import (MISPs, PhishingInitiative, UniversalWhois, from .modules import (MISPs, PhishingInitiative, UniversalWhois,
UrlScan, VirusTotal, Phishtank, Hashlookup, UrlScan, VirusTotal, Phishtank, Hashlookup,
@ -68,33 +69,6 @@ if TYPE_CHECKING:
from playwright.async_api import Cookie from playwright.async_api import Cookie
class CaptureSettings(CaptureSettingsCore, total=False):
'''The capture settings that can be passed to Lookyloo'''
listing: int | None
not_queued: int | None
auto_report: bool | str | dict[str, str] | None # {'email': , 'comment': , 'recipient_mail':}
dnt: str | None
browser_name: str | None
os: str | None
parent: str | None
# overwrite set to True means the settings in the config file overwrite the settings
# provided by the user. False will simply append the settings from the config file if they
# don't exist.
class UserCaptureSettings(CaptureSettings, total=False):
overwrite: bool
@lru_cache(64)
def load_user_config(username: str) -> UserCaptureSettings | None:
user_config_path = get_homedir() / 'config' / 'users' / f'{username}.json'
if not user_config_path.exists():
return None
with user_config_path.open() as _c:
return json.load(_c)
class Lookyloo(): class Lookyloo():
def __init__(self, cache_max_size: int | None=None) -> None: def __init__(self, cache_max_size: int | None=None) -> None:

View File

@ -41,7 +41,7 @@ from werkzeug.wrappers.response import Response as WerkzeugResponse
from lookyloo import Lookyloo, CaptureSettings from lookyloo import Lookyloo, CaptureSettings
from lookyloo.default import get_config from lookyloo.default import get_config
from lookyloo.exceptions import MissingUUID, NoValidHarFile, LacusUnreachable from lookyloo.exceptions import MissingUUID, NoValidHarFile, LacusUnreachable
from lookyloo.helpers import get_taxonomies, UserAgents, load_cookies from lookyloo.helpers import get_taxonomies, UserAgents, load_cookies, UserCaptureSettings, load_user_config
if sys.version_info < (3, 9): if sys.version_info < (3, 9):
from pytz import all_timezones_set from pytz import all_timezones_set
@ -52,7 +52,7 @@ else:
from .genericapi import api as generic_api from .genericapi import api as generic_api
from .helpers import (User, build_users_table, get_secret_key, from .helpers import (User, build_users_table, get_secret_key,
load_user_from_request, src_request_ip, sri_load, load_user_from_request, src_request_ip, sri_load,
get_lookyloo_instance, get_indexing) get_lookyloo_instance, get_indexing, build_keys_table)
from .proxied import ReverseProxied from .proxied import ReverseProxied
logging.config.dictConfig(get_config('logging')) logging.config.dictConfig(get_config('logging'))
@ -73,6 +73,7 @@ pkg_version = version('lookyloo')
# Auth stuff # Auth stuff
login_manager = flask_login.LoginManager() login_manager = flask_login.LoginManager()
login_manager.init_app(app) login_manager.init_app(app)
build_keys_table()
# User agents manager # User agents manager
user_agents = UserAgents() user_agents = UserAgents()
@ -1314,6 +1315,7 @@ def index_generic(show_hidden: bool=False, show_error: bool=True, category: str
cached.redirects)) cached.redirects))
titles = sorted(titles, key=lambda x: (x[2], x[3]), reverse=True) titles = sorted(titles, key=lambda x: (x[2], x[3]), reverse=True)
return render_template('index.html', titles=titles, public_domain=lookyloo.public_domain, return render_template('index.html', titles=titles, public_domain=lookyloo.public_domain,
show_hidden=show_hidden,
show_project_page=get_config('generic', 'show_project_page'), show_project_page=get_config('generic', 'show_project_page'),
version=pkg_version) version=pkg_version)
@ -1422,13 +1424,14 @@ def search() -> str | Response | WerkzeugResponse:
return render_template('search.html') return render_template('search.html')
def _prepare_capture_template(user_ua: str | None, predefined_url: str | None=None) -> str: def _prepare_capture_template(user_ua: str | None, predefined_url: str | None=None, *, user_config: UserCaptureSettings | None=None) -> str:
return render_template('capture.html', user_agents=user_agents.user_agents, return render_template('capture.html', user_agents=user_agents.user_agents,
default=user_agents.default, default=user_agents.default,
personal_ua=user_ua, personal_ua=user_ua,
default_public=get_config('generic', 'default_public'), default_public=get_config('generic', 'default_public'),
devices=lookyloo.get_playwright_devices(), devices=lookyloo.get_playwright_devices(),
predefined_url_to_capture=predefined_url if predefined_url else '', predefined_url_to_capture=predefined_url if predefined_url else '',
user_config=user_config,
has_global_proxy=True if lookyloo.global_proxy else False) has_global_proxy=True if lookyloo.global_proxy else False)
@ -1496,8 +1499,10 @@ def submit_capture() -> str | Response | WerkzeugResponse:
@app.route('/capture', methods=['GET', 'POST']) @app.route('/capture', methods=['GET', 'POST'])
def capture_web() -> str | Response | WerkzeugResponse: def capture_web() -> str | Response | WerkzeugResponse:
user_config: UserCaptureSettings | None = None
if flask_login.current_user.is_authenticated: if flask_login.current_user.is_authenticated:
user = flask_login.current_user.get_id() user = flask_login.current_user.get_id()
user_config = load_user_config(user)
else: else:
user = src_request_ip(request) user = src_request_ip(request)
@ -1609,7 +1614,7 @@ def capture_web() -> str | Response | WerkzeugResponse:
return redirect(url_for('tree', tree_uuid=perma_uuid)) return redirect(url_for('tree', tree_uuid=perma_uuid))
# render template # render template
return _prepare_capture_template(user_ua=request.headers.get('User-Agent')) return _prepare_capture_template(user_ua=request.headers.get('User-Agent'), user_config=user_config)
@app.route('/simple_capture', methods=['GET', 'POST']) @app.route('/simple_capture', methods=['GET', 'POST'])

View File

@ -14,7 +14,7 @@ from zipfile import ZipFile
import flask_login # type: ignore[import-untyped] import flask_login # type: ignore[import-untyped]
from flask import request, send_file, Response from flask import request, send_file, Response
from flask_restx import Namespace, Resource, fields # type: ignore[import-untyped] from flask_restx import Namespace, Resource, fields, abort # type: ignore[import-untyped]
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from lacuscore import CaptureStatus as CaptureStatusCore from lacuscore import CaptureStatus as CaptureStatusCore
@ -22,8 +22,10 @@ from pylacus import CaptureStatus as CaptureStatusPy
from lookyloo import CaptureSettings, Lookyloo from lookyloo import CaptureSettings, Lookyloo
from lookyloo.comparator import Comparator from lookyloo.comparator import Comparator
from lookyloo.exceptions import MissingUUID, NoValidHarFile from lookyloo.exceptions import MissingUUID, NoValidHarFile
from lookyloo.helpers import load_user_config, UserCaptureSettings
from .helpers import build_users_table, load_user_from_request, src_request_ip, get_lookyloo_instance, get_indexing from .helpers import (build_users_table, load_user_from_request, src_request_ip,
get_lookyloo_instance, get_indexing)
api = Namespace('GenericAPI', description='Generic Lookyloo API', path='/') api = Namespace('GenericAPI', description='Generic Lookyloo API', path='/')
@ -34,7 +36,7 @@ comparator: Comparator = Comparator()
def api_auth_check(method): # type: ignore[no-untyped-def] def api_auth_check(method): # type: ignore[no-untyped-def]
if flask_login.current_user.is_authenticated or load_user_from_request(request): if flask_login.current_user.is_authenticated or load_user_from_request(request):
return method return method
return 'Authentication required.', 403 abort(403, 'Authentication required.')
token_request_fields = api.model('AuthTokenFields', { token_request_fields = api.model('AuthTokenFields', {
@ -49,6 +51,17 @@ def handle_no_HAR_file_exception(error: Any) -> tuple[dict[str, str], int]:
return {'message': str(error)}, 400 return {'message': str(error)}, 400
@api.route('/json/get_user_config')
@api.doc(description='Get the configuration of the user (if any)', security='apikey')
class UserConfig(Resource): # type: ignore[misc]
method_decorators = [api_auth_check]
def get(self) -> UserCaptureSettings | None | tuple[dict[str, str], int]:
if not flask_login.current_user.is_authenticated:
return {'error': 'User not authenticated.'}, 401
return load_user_config(flask_login.current_user.get_id())
@api.route('/json/get_token') @api.route('/json/get_token')
@api.doc(description='Get the API token required for authenticated calls') @api.doc(description='Get the API token required for authenticated calls')
class AuthToken(Resource): # type: ignore[misc] class AuthToken(Resource): # type: ignore[misc]

View File

@ -14,7 +14,7 @@ from flask import Request
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from lookyloo import Lookyloo, Indexing from lookyloo import Lookyloo, Indexing
from lookyloo.default import get_config, get_homedir from lookyloo.default import get_config, get_homedir, LookylooException
__global_lookyloo_instance = None __global_lookyloo_instance = None
@ -57,9 +57,12 @@ def is_valid_username(username: str) -> bool:
@lru_cache(64) @lru_cache(64)
def build_keys_table() -> dict[str, str]: def build_keys_table() -> dict[str, str]:
keys_table = {} keys_table: dict[str, str] = {}
for username, authstuff in build_users_table().items(): for username, authstuff in build_users_table().items():
if 'authkey' in authstuff: if 'authkey' in authstuff:
if authstuff['authkey'] in keys_table:
existing_user = keys_table[authstuff['authkey']]
raise LookylooException(f'Duplicate authkey found for {existing_user} and {username}.')
keys_table[authstuff['authkey']] = username keys_table[authstuff['authkey']] = username
return keys_table return keys_table
@ -85,7 +88,7 @@ def build_users_table() -> dict[str, dict[str, str]]:
users_table[username] = {} users_table[username] = {}
users_table[username]['password'] = generate_password_hash(authstuff) users_table[username]['password'] = generate_password_hash(authstuff)
users_table[username]['authkey'] = hashlib.pbkdf2_hmac('sha256', get_secret_key(), users_table[username]['authkey'] = hashlib.pbkdf2_hmac('sha256', get_secret_key(),
authstuff.encode(), f'{username}{authstuff}'.encode(),
100000).hex() 100000).hex()
elif isinstance(authstuff, list) and len(authstuff) == 2: elif isinstance(authstuff, list) and len(authstuff) == 2:

View File

@ -29,6 +29,37 @@
</a> </a>
</center> </center>
{{ render_messages(container=True, dismissible=True) }} {{ render_messages(container=True, dismissible=True) }}
{% if current_user.is_authenticated %}
<p class="lead">You are logged-in as <strong>{{ current_user.id }}</strong>
</br>
{% if user_config %}
{% if user_config['overwrite'] == true %}
The settings in your users configuration file will overwrite the settings you configure in the form below.
{% else %}
The settings in your users configuration file will only be used if you don't overwrite them in the form below.
{% endif %}
<p>
<dl class="row">
{% for key, value in user_config.items() %}
{% if key != 'overwrite' %}
<dt class="col-sm-3">{{ key }}</dt>
<dd class="col-sm-9">
{% if value is mapping %}
<dl class="row">
{% for sub_key, sub_value in value.items() %}
<dt class="col-sm-4">{{ sub_key}}</dt>
<dd class="col-sm-8">{{ sub_value }}</dd>
{% endfor %}
</dl>
{% else %}
{{ value }}
{% endif %}
<dd>
{% endif %}
{% endfor %}
<dl>
{% endif %}
{% endif %}
<form role="form" action="{{ url_for('capture_web') }}" method=post enctype=multipart/form-data> <form role="form" action="{{ url_for('capture_web') }}" method=post enctype=multipart/form-data>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -87,8 +87,17 @@ $(document).ready(function () {
<a href="{{ url_for('simple_capture') }}"> <a href="{{ url_for('simple_capture') }}">
<button class="new-capture-button btn btn-primary">Takedown process</button> <button class="new-capture-button btn btn-primary">Takedown process</button>
</a> </a>
<br>
<p class="lead">
You are logged-in as <strong>{{ current_user.id }}</strong>,
{% if show_hidden == false %}
and you can check the <a href="{{ url_for('index_hidden') }}">hidden</a> captures.
{% else %}
and you're looking at the hidden captures. Go back to the <a href="{{ url_for('index') }}">public</a> captures.
{% endif %}
<p>
{% endif %} {% endif %}
<br><br> <br>
{{ render_messages(container=True, dismissible=True) }} {{ render_messages(container=True, dismissible=True) }}
</center> </center>