new: find related captures by hostname and URL

pull/919/head
Raphaël Vinot 2024-05-14 18:52:26 +02:00
parent 228432efbb
commit 3a99f1a63f
11 changed files with 342 additions and 196 deletions

29
.gitignore vendored
View File

@ -112,11 +112,14 @@ FileSaver.js
d3.v5.min.js d3.v5.min.js
d3.v5.js d3.v5.js
cache.pid *.pid
dump.rdb *.rdb
*log*
full_index/db
# Local config files # Local config files
config/*.json config/*.json
config/users/*.json
config/*.json.bkp config/*.json.bkp
config/takedown_filters.ini config/takedown_filters.ini
@ -126,3 +129,25 @@ known_content_user/
user_agents/ user_agents/
.DS_Store .DS_Store
.idea
archived_captures
discarded_captures
removed_captures
website/web/static/d3.min.js
website/web/static/datatables.min.css
website/web/static/datatables.min.js
website/web/static/jquery.min.js
# Modules
circl_pypdns
eupi
own_user_agents
phishtank
riskiq
sanejs
urlhaus
urlscan
vt_url

View File

@ -39,8 +39,7 @@ from pymisp import MISPEvent, MISPServerError # type: ignore[attr-defined]
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from werkzeug.wrappers.response import Response as WerkzeugResponse from werkzeug.wrappers.response import Response as WerkzeugResponse
from lookyloo import Lookyloo, CaptureSettings, Indexing from lookyloo import Lookyloo, CaptureSettings
from lookyloo.capturecache import CaptureCache
from lookyloo.default import get_config from lookyloo.default import get_config
from lookyloo.exceptions import MissingUUID, NoValidHarFile from lookyloo.exceptions import MissingUUID, NoValidHarFile
from lookyloo.helpers import get_taxonomies, UserAgents, load_cookies from lookyloo.helpers import get_taxonomies, UserAgents, load_cookies
@ -54,7 +53,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_lookyloo_instance, get_indexing)
from .proxied import ReverseProxied from .proxied import ReverseProxied
logging.config.dictConfig(get_config('logging')) logging.config.dictConfig(get_config('logging'))
@ -270,23 +269,6 @@ def file_response(func): # type: ignore[no-untyped-def]
# ##### Methods querying the indexes ##### # ##### Methods querying the indexes #####
@functools.cache
def get_indexing(user: User | None) -> Indexing:
'''Depending if we're logged in or not, we (can) get different indexes:
if index_everything is enabled, we have an index in kvrocks that contains all
the indexes for all the captures.
It is only accessible to the admin user.
'''
if not get_config('generic', 'index_everything'):
return Indexing()
if not user or not user.is_authenticated:
# No user or anonymous
return Indexing()
# Logged in user
return Indexing(full_index=True)
def _get_body_hash_investigator(body_hash: str, /) -> tuple[list[tuple[str, str, datetime, str, str]], list[tuple[str, float]]]: def _get_body_hash_investigator(body_hash: str, /) -> tuple[list[tuple[str, str, datetime, str, str]], list[tuple[str, float]]]:
'''Returns all the captures related to a hash (sha512), used in the web interface.''' '''Returns all the captures related to a hash (sha512), used in the web interface.'''
total_captures, details = get_indexing(flask_login.current_user).get_body_hash_captures(body_hash, limit=-1) total_captures, details = get_indexing(flask_login.current_user).get_body_hash_captures(body_hash, limit=-1)
@ -365,70 +347,33 @@ def get_all_hostnames(capture_uuid: str, /) -> dict[str, dict[str, int | list[UR
return to_return return to_return
def get_latest_url_capture(url: str, /) -> CaptureCache | None: def get_all_urls(capture_uuid: str, /) -> dict[str, dict[str, int | list[URLNode] | str]]:
'''Get the most recent capture with this URL''' ct = lookyloo.get_crawled_tree(capture_uuid)
captures = lookyloo.sorted_capture_cache(get_indexing(flask_login.current_user).get_captures_url(url)) to_return: dict[str, dict[str, list[URLNode] | int | str]] = defaultdict()
if captures: for node in ct.root_hartree.url_tree.traverse():
return captures[0] if not node.name:
return None continue
captures = get_indexing(flask_login.current_user).get_captures_url(node.name)
# Note for future: mayeb get url, capture title, something better than just the hash to show to the user
def get_url_occurrences(url: str, /, limit: int=20, cached_captures_only: bool=True) -> list[dict[str, Any]]: if node.hostname not in to_return:
'''Get the most recent captures and URL nodes where the URL has been seen.''' to_return[node.name] = {'total_captures': len(captures), 'nodes': [],
captures = lookyloo.sorted_capture_cache(get_indexing(flask_login.current_user).get_captures_url(url), cached_captures_only=cached_captures_only) 'quoted_url': quote_plus(node.name)}
to_return[node.name]['nodes'].append(node) # type: ignore[union-attr]
to_return: list[dict[str, Any]] = []
for capture in captures[:limit]:
ct = lookyloo.get_crawled_tree(capture.uuid)
to_append: dict[str, str | dict[str, Any]] = {'capture_uuid': capture.uuid,
'start_timestamp': capture.timestamp.isoformat(),
'title': capture.title}
urlnodes: dict[str, dict[str, str]] = {}
for urlnode in ct.root_hartree.url_tree.search_nodes(name=url):
urlnodes[urlnode.uuid] = {'start_time': urlnode.start_time.isoformat(),
'hostnode_uuid': urlnode.hostnode_uuid}
if hasattr(urlnode, 'body_hash'):
urlnodes[urlnode.uuid]['hash'] = urlnode.body_hash
to_append['urlnodes'] = urlnodes
to_return.append(to_append)
return to_return
def get_hostname_occurrences(hostname: str, /, with_urls_occurrences: bool=False, limit: int=20, cached_captures_only: bool=True) -> list[dict[str, Any]]:
'''Get the most recent captures and URL nodes where the hostname has been seen.'''
captures = lookyloo.sorted_capture_cache(get_indexing(flask_login.current_user).get_captures_hostname(hostname), cached_captures_only=cached_captures_only)
to_return: list[dict[str, Any]] = []
for capture in captures[:limit]:
ct = lookyloo.get_crawled_tree(capture.uuid)
to_append: dict[str, str | list[Any] | dict[str, Any]] = {
'capture_uuid': capture.uuid,
'start_timestamp': capture.timestamp.isoformat(),
'title': capture.title}
hostnodes: list[str] = []
if with_urls_occurrences:
urlnodes: dict[str, dict[str, str]] = {}
for hostnode in ct.root_hartree.hostname_tree.search_nodes(name=hostname):
hostnodes.append(hostnode.uuid)
if with_urls_occurrences:
for urlnode in hostnode.urls:
urlnodes[urlnode.uuid] = {'start_time': urlnode.start_time.isoformat(),
'url': urlnode.name,
'hostnode_uuid': urlnode.hostnode_uuid}
if hasattr(urlnode, 'body_hash'):
urlnodes[urlnode.uuid]['hash'] = urlnode.body_hash
to_append['hostnodes'] = hostnodes
if with_urls_occurrences:
to_append['urlnodes'] = urlnodes
to_return.append(to_append)
return to_return return to_return
def get_hostname_investigator(hostname: str) -> list[tuple[str, str, str, datetime]]: def get_hostname_investigator(hostname: str) -> list[tuple[str, str, str, datetime]]:
'''Returns all the captures loading content from that hostname, used in the web interface.'''
cached_captures = lookyloo.sorted_capture_cache([uuid for uuid in get_indexing(flask_login.current_user).get_captures_hostname(hostname=hostname)]) cached_captures = lookyloo.sorted_capture_cache([uuid for uuid in get_indexing(flask_login.current_user).get_captures_hostname(hostname=hostname)])
return [(cache.uuid, cache.title, cache.redirects[-1], cache.timestamp) for cache in cached_captures] return [(cache.uuid, cache.title, cache.redirects[-1], cache.timestamp) for cache in cached_captures]
def get_url_investigator(url: str) -> list[tuple[str, str, str, datetime]]:
'''Returns all the captures loading content from that url, used in the web interface.'''
cached_captures = lookyloo.sorted_capture_cache([uuid for uuid in get_indexing(flask_login.current_user).get_captures_url(url=url)])
return [(cache.uuid, cache.title, cache.redirects[-1], cache.timestamp) for cache in cached_captures]
def get_cookie_name_investigator(cookie_name: str, /) -> tuple[list[tuple[str, str]], list[tuple[str, float, list[tuple[str, float]]]]]: def get_cookie_name_investigator(cookie_name: str, /) -> tuple[list[tuple[str, str]], list[tuple[str, float, list[tuple[str, float]]]]]:
'''Returns all the captures related to a cookie name entry, used in the web interface.''' '''Returns all the captures related to a cookie name entry, used in the web interface.'''
cached_captures = lookyloo.sorted_capture_cache([entry[0] for entry in get_indexing(flask_login.current_user).get_cookies_names_captures(cookie_name)]) cached_captures = lookyloo.sorted_capture_cache([entry[0] for entry in get_indexing(flask_login.current_user).get_cookies_names_captures(cookie_name)])
@ -1282,6 +1227,12 @@ def tree_hostnames(tree_uuid: str) -> str:
return render_template('tree_hostnames.html', tree_uuid=tree_uuid, hostnames=hostnames) return render_template('tree_hostnames.html', tree_uuid=tree_uuid, hostnames=hostnames)
@app.route('/tree/<string:tree_uuid>/urls', methods=['GET'])
def tree_urls(tree_uuid: str) -> str:
urls = get_all_urls(tree_uuid)
return render_template('tree_urls.html', tree_uuid=tree_uuid, urls=urls)
@app.route('/tree/<string:tree_uuid>/pandora', methods=['GET', 'POST']) @app.route('/tree/<string:tree_uuid>/pandora', methods=['GET', 'POST'])
def pandora_submit(tree_uuid: str) -> dict[str, Any] | Response: def pandora_submit(tree_uuid: str) -> dict[str, Any] | Response:
node_uuid = None node_uuid = None
@ -1743,8 +1694,8 @@ def body_hash_details(body_hash: str) -> str:
@app.route('/urls/<string:url>', methods=['GET']) @app.route('/urls/<string:url>', methods=['GET'])
def url_details(url: str) -> str: def url_details(url: str) -> str:
url = unquote_plus(url).strip() url = unquote_plus(url).strip()
hits = get_url_occurrences(url, limit=50) captures = get_url_investigator(url)
return render_template('url.html', url=url, hits=hits) return render_template('url.html', url=url, captures=captures)
@app.route('/hostnames/<string:hostname>', methods=['GET']) @app.route('/hostnames/<string:hostname>', methods=['GET'])

View File

@ -21,7 +21,7 @@ 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 .helpers import build_users_table, load_user_from_request, src_request_ip, get_lookyloo_instance 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='/')
@ -305,6 +305,27 @@ class HashInfo(Resource): # type: ignore[misc]
return to_return return to_return
def get_url_occurrences(url: str, /, limit: int=20, cached_captures_only: bool=True) -> list[dict[str, Any]]:
'''Get the most recent captures and URL nodes where the URL has been seen.'''
captures = lookyloo.sorted_capture_cache(get_indexing(flask_login.current_user).get_captures_url(url), cached_captures_only=cached_captures_only)
to_return: list[dict[str, Any]] = []
for capture in captures[:limit]:
ct = lookyloo.get_crawled_tree(capture.uuid)
to_append: dict[str, str | dict[str, Any]] = {'capture_uuid': capture.uuid,
'start_timestamp': capture.timestamp.isoformat(),
'title': capture.title}
urlnodes: dict[str, dict[str, str]] = {}
for urlnode in ct.root_hartree.url_tree.search_nodes(name=url):
urlnodes[urlnode.uuid] = {'start_time': urlnode.start_time.isoformat(),
'hostnode_uuid': urlnode.hostnode_uuid}
if hasattr(urlnode, 'body_hash'):
urlnodes[urlnode.uuid]['hash'] = urlnode.body_hash
to_append['urlnodes'] = urlnodes
to_return.append(to_append)
return to_return
url_info_fields = api.model('URLInfoFields', { url_info_fields = api.model('URLInfoFields', {
'url': fields.String(description="The URL to search", required=True), 'url': fields.String(description="The URL to search", required=True),
'limit': fields.Integer(description="The maximal amount of captures to return", example=20), 'limit': fields.Integer(description="The maximal amount of captures to return", example=20),
@ -318,12 +339,41 @@ class URLInfo(Resource): # type: ignore[misc]
@api.doc(body=url_info_fields) # type: ignore[misc] @api.doc(body=url_info_fields) # type: ignore[misc]
def post(self) -> list[dict[str, Any]]: def post(self) -> list[dict[str, Any]]:
from . import get_url_occurrences
to_query: dict[str, Any] = request.get_json(force=True) to_query: dict[str, Any] = request.get_json(force=True)
occurrences = get_url_occurrences(to_query.pop('url'), **to_query) occurrences = get_url_occurrences(to_query.pop('url'), **to_query)
return occurrences return occurrences
def get_hostname_occurrences(hostname: str, /, with_urls_occurrences: bool=False, limit: int=20, cached_captures_only: bool=True) -> list[dict[str, Any]]:
'''Get the most recent captures and URL nodes where the hostname has been seen.'''
captures = lookyloo.sorted_capture_cache(get_indexing(flask_login.current_user).get_captures_hostname(hostname), cached_captures_only=cached_captures_only)
to_return: list[dict[str, Any]] = []
for capture in captures[:limit]:
ct = lookyloo.get_crawled_tree(capture.uuid)
to_append: dict[str, str | list[Any] | dict[str, Any]] = {
'capture_uuid': capture.uuid,
'start_timestamp': capture.timestamp.isoformat(),
'title': capture.title}
hostnodes: list[str] = []
if with_urls_occurrences:
urlnodes: dict[str, dict[str, str]] = {}
for hostnode in ct.root_hartree.hostname_tree.search_nodes(name=hostname):
hostnodes.append(hostnode.uuid)
if with_urls_occurrences:
for urlnode in hostnode.urls:
urlnodes[urlnode.uuid] = {'start_time': urlnode.start_time.isoformat(),
'url': urlnode.name,
'hostnode_uuid': urlnode.hostnode_uuid}
if hasattr(urlnode, 'body_hash'):
urlnodes[urlnode.uuid]['hash'] = urlnode.body_hash
to_append['hostnodes'] = hostnodes
if with_urls_occurrences:
to_append['urlnodes'] = urlnodes
to_return.append(to_append)
return to_return
hostname_info_fields = api.model('HostnameInfoFields', { hostname_info_fields = api.model('HostnameInfoFields', {
'hostname': fields.String(description="The hostname to search", required=True), 'hostname': fields.String(description="The hostname to search", required=True),
'limit': fields.Integer(description="The maximal amount of captures to return", example=20), 'limit': fields.Integer(description="The maximal amount of captures to return", example=20),
@ -337,7 +387,6 @@ class HostnameInfo(Resource): # type: ignore[misc]
@api.doc(body=hostname_info_fields) # type: ignore[misc] @api.doc(body=hostname_info_fields) # type: ignore[misc]
def post(self) -> list[dict[str, Any]]: def post(self) -> list[dict[str, Any]]:
from . import get_hostname_occurrences
to_query: dict[str, Any] = request.get_json(force=True) to_query: dict[str, Any] = request.get_json(force=True)
return get_hostname_occurrences(to_query.pop('hostname'), **to_query) return get_hostname_occurrences(to_query.pop('hostname'), **to_query)

View File

@ -6,14 +6,14 @@ import hashlib
import json import json
import os import os
import re import re
from functools import lru_cache from functools import lru_cache, cache
from pathlib import Path from pathlib import Path
import flask_login # type: ignore[import-untyped] import flask_login # type: ignore[import-untyped]
from flask import Request from flask import Request
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from lookyloo import Lookyloo from lookyloo import Lookyloo, Indexing
from lookyloo.default import get_config, get_homedir from lookyloo.default import get_config, get_homedir
__global_lookyloo_instance = None __global_lookyloo_instance = None
@ -113,3 +113,20 @@ def get_secret_key() -> bytes:
def sri_load() -> dict[str, dict[str, str]]: def sri_load() -> dict[str, dict[str, str]]:
with (get_homedir() / 'website' / 'web' / 'sri.txt').open() as f: with (get_homedir() / 'website' / 'web' / 'sri.txt').open() as f:
return json.load(f) return json.load(f)
@cache
def get_indexing(user: User | None) -> Indexing:
'''Depending if we're logged in or not, we (can) get different indexes:
if index_everything is enabled, we have an index in kvrocks that contains all
the indexes for all the captures.
It is only accessible to the admin user.
'''
if not get_config('generic', 'index_everything'):
return Indexing()
if not user or not user.is_authenticated:
# No user or anonymous
return Indexing()
# Logged in user
return Indexing(full_index=True)

View File

@ -77,8 +77,8 @@
<table id="bodyHashDetailsTable" class="table table-striped" style="width:100%"> <table id="bodyHashDetailsTable" class="table table-striped" style="width:100%">
<thead> <thead>
<tr> <tr>
<th>Timestamp</th> <th>Capture Time</th>
<th>Title</th> <th>Capture Title</th>
<th>URL</th> <th>URL</th>
</tr> </tr>
</thead> </thead>

View File

@ -1,42 +1,7 @@
{% from "macros.html" import shorten_string %} {% from "macros.html" import shorten_string %}
{% if from_popup %}
{% extends "main.html" %}
{% from 'bootstrap5/utils.html' import render_messages %}
{% block title %}{{ url }}{% endblock %}
{% block scripts %}
{{ super() }}
<script type="text/javascript">
$('#table').DataTable( {
"order": [[ 0, "desc" ]],
"pageLength": 50,
"columnDefs": [{
"targets": 0,
"render": function ( data, type, row, meta ) {
let date = new Date(data);
return date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, "0") + '-' + date.getDate().toString().padStart(2, "0") + ' ' + date.toTimeString();
}
}]
});
</script>
<script type="text/javascript">
function openTreeInNewTab(treeUUID) {
window.opener.openTreeInNewTab(treeUUID);
};
</script>
{% endblock %}
{%endif%}
{% block content %} {% block content %}
{% if from_popup %}
<button onclick="window.history.back();" class="btn btn-primary" type="button">Go Back</button>
{%endif%}
<center> <center>
<h4>{{ hostname }}</h4> <h4>{{ hostname }}</h4>
</center> </center>
@ -50,7 +15,8 @@
return date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, "0") + '-' + date.getDate().toString().padStart(2, "0") + ' ' + date.toTimeString(); return date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, "0") + '-' + date.getDate().toString().padStart(2, "0") + ' ' + date.toTimeString();
} }
}, },
{ width: '80%', targets: 1 }], { width: '40%', targets: 1 },
{ width: '40%', targets: 2 }],
}); });
</script> </script>

View File

@ -161,6 +161,20 @@
}); });
</script> </script>
<script> <script>
$('#urlsModal').on('show.bs.modal', function(e) {
var button = $(e.relatedTarget);
var modal = $(this);
modal.find('.modal-body').load(button.data("remote"));
});
</script>
<script>
$('#urlDetailsModal').on('show.bs.modal', function(e) {
var button = $(e.relatedTarget);
var modal = $(this);
modal.find('.modal-body').load(button.data("remote"));
});
</script>
<script>
$('#mispPushModal').on('show.bs.modal', function(e) { $('#mispPushModal').on('show.bs.modal', function(e) {
var button = $(e.relatedTarget); var button = $(e.relatedTarget);
var modal = $(this); var modal = $(this);
@ -343,23 +357,27 @@
<a href="#bodyHashesModal" data-remote="{{ url_for('tree_body_hashes', tree_uuid=tree_uuid) }}" <a href="#bodyHashesModal" data-remote="{{ url_for('tree_body_hashes', tree_uuid=tree_uuid) }}"
data-bs-toggle="modal" data-bs-target="#bodyHashesModal" role="button" data-bs-toggle="modal" data-bs-target="#bodyHashesModal" role="button"
title="All ressources contained in the tree">Ressources Capture</a> title="All ressources contained in the tree">Ressources</a>
<a href="#hostnamesModal" data-remote="{{ url_for('tree_hostnames', tree_uuid=tree_uuid) }}" <a href="#hostnamesModal" data-remote="{{ url_for('tree_hostnames', tree_uuid=tree_uuid) }}"
data-bs-toggle="modal" data-bs-target="#hostnamesModal" role="button" data-bs-toggle="modal" data-bs-target="#hostnamesModal" role="button"
title="All hostnames contained in the tree">Hostnames Capture</a> title="All hostnames contained in the tree">Hostnames</a>
<a href="#urlsModal" data-remote="{{ url_for('tree_urls', tree_uuid=tree_uuid) }}"
data-bs-toggle="modal" data-bs-target="#urlsModal" role="button"
title="All URLs contained in the tree">URLs</a>
<a href="#faviconsModal" data-remote="{{ url_for('tree_favicons', tree_uuid=tree_uuid) }}" <a href="#faviconsModal" data-remote="{{ url_for('tree_favicons', tree_uuid=tree_uuid) }}"
data-bs-toggle="modal" data-bs-target="#faviconsModal" role="button" data-bs-toggle="modal" data-bs-target="#faviconsModal" role="button"
title="Favicons found on the rendered page">Favicons Capture</a> title="Favicons found on the rendered page">Favicons</a>
<a href="#captureHashesTypesModal" data-remote="{{ url_for('tree_capture_hashes_types', tree_uuid=tree_uuid) }}" <a href="#captureHashesTypesModal" data-remote="{{ url_for('tree_capture_hashes_types', tree_uuid=tree_uuid) }}"
data-bs-toggle="modal" data-bs-target="#captureHashesTypesModal" role="button" data-bs-toggle="modal" data-bs-target="#captureHashesTypesModal" role="button"
title="Compare hashes of the rendered page">Capture hashes types</a> title="Compare hashes of the rendered page">(Fuzzy)Hashes types</a>
<a href="#identifiersModal" data-remote="{{ url_for('tree_identifiers', tree_uuid=tree_uuid) }}" <a href="#identifiersModal" data-remote="{{ url_for('tree_identifiers', tree_uuid=tree_uuid) }}"
data-bs-toggle="modal" data-bs-target="#identifiersModal" role="button" data-bs-toggle="modal" data-bs-target="#identifiersModal" role="button"
title="Identifiers found on the rendered page">Identifiers Capture</a> title="Identifiers found on the rendered page">Other Identifiers</a>
</div> </div>
</div> </div>
@ -798,6 +816,43 @@
</div> </div>
</div> </div>
<div class="modal fade" id="urlsModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="urlsModalLabel">URLs in tree</h5>
<button type="button" class="btn btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
... loading urls ...
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="urlDetailsModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="hostnameDetailsModalLabel">Other occurrences of the URL</h5>
<button type="button" class="btn btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
... loading url details ...
</div>
<div class="modal-footer">
<a class="btn btn-primary" href="#HostnamesModal"
data-remote="{{ url_for('tree_urls', tree_uuid=tree_uuid) }}"
data-bs-toggle="modal" data-bs-target="#urlsModal" role="button">Back to capture's URLs</a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="mispPushModal" tabindex="-1" role="dialog"> <div class="modal fade" id="mispPushModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document"> <div class="modal-dialog modal-xl" role="document">
<div class="modal-content"> <div class="modal-content">

View File

@ -18,7 +18,7 @@
<table id="bodyHashesTable" class="table table-striped" style="width:100%"> <table id="bodyHashesTable" class="table table-striped" style="width:100%">
<thead> <thead>
<tr> <tr>
<th>Captures total</th> <th>Number of captures</th>
<th>File type</th> <th>File type</th>
<th>Ressource URL in capture</th> <th>Ressource URL in capture</th>
<th>Hash (sha512)</th> <th>Hash (sha512)</th>

View File

@ -0,0 +1,64 @@
{% from "macros.html" import popup_icons_response %}
<script type="text/javascript">
var table = new DataTable('#hostnamesTable', {
order: [[ 0, "desc" ]],
columnDefs: [{ width: '10%', targets: 0 },
{ width: '40%', targets: 1 },
{ width: '50%', targets: 2 }],
initComplete: function (settings, json) {
$('[data-bs-toggle="tooltip"]').tooltip({html: true});
}
}).on('draw', function() {
$('[data-bs-toggle="tooltip"]').tooltip({html: true});
});
</script>
<table id="hostnamesTable" class="table table-striped" style="width:100%">
<thead>
<tr>
<th>Number of captures</th>
<th>Hostname</th>
<th>URLs</th>
</tr>
</thead>
<tbody>
{% for hostname, info in hostnames.items() %}
<tr>
<td>{{ info['total_captures'] }}</td>
<td>
<span class="d-inline-block text-break">
<a href="#hostnameDetailsModal" data-remote="{{ url_for('hostname_details', hostname=hostname) }}"
data-bs-toggle="modal" data-bs-target="#hostnameDetailsModal" role="button">
{{hostname}}
</a>
</span>
</td>
<td>
<p class="d-inline-flex gap-1">
<button class="btn btn-primary" type="button"
data-bs-toggle="collapse" data-bs-target="#collapseAllNodes_{{loop.index}}"
aria-expanded="false" aria-controls="collapseAllNodes_{{loop.index}}">
Show
</button>
</p>
<div class="collapse" id="collapseAllNodes_{{loop.index}}">
<div class="card card-body">
<span class="d-inline-block text-break">
<ul>
{% for node in info['nodes'] %}
<li>
<p class="text-break">{{ node.name }}</p>
<a href="#/" onclick="openTreeInNewTab('{{ tree_uuid }}', '{{ node.uuid }}')">Show on tree</a>
</li>
{% endfor %}
<ul>
</span>
</div>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -0,0 +1,38 @@
{% from "macros.html" import popup_icons_response %}
<script type="text/javascript">
var table = new DataTable('#urlsTable', {
order: [[ 0, "desc" ]],
columnDefs: [{ width: '10%', targets: 0 },
{ width: '90%', targets: 1 }],
initComplete: function (settings, json) {
$('[data-bs-toggle="tooltip"]').tooltip({html: true});
}
}).on('draw', function() {
$('[data-bs-toggle="tooltip"]').tooltip({html: true});
});
</script>
<table id="urlsTable" class="table table-striped" style="width:100%">
<thead>
<tr>
<th>Number of captures</th>
<th>URL</th>
</tr>
</thead>
<tbody>
{% for url, info in urls.items() %}
<tr>
<td>{{ info['total_captures'] }}</td>
<td>
<span class="d-inline-block text-break">
<a href="#urlDetailsModal" data-remote="{{ url_for('url_details', url=info['quoted_url']) }}"
data-bs-toggle="modal" data-bs-target="#urlDetailsModal" role="button">
{{url}}
</a>
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -1,70 +1,51 @@
{% extends "main.html" %} {% from "macros.html" import shorten_string %}
{% from 'bootstrap5/utils.html' import render_messages %}
{% block title %}{{ url }}{% endblock %}
{% block scripts %}
{{ super() }}
<script type="text/javascript">
$('#table').DataTable( {
"order": [[ 0, "desc" ]],
"pageLength": 50,
"columnDefs": [{
"targets": 0,
"render": function ( data, type, row, meta ) {
let date = new Date(data);
return date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, "0") + '-' + date.toTimeString();
}
}]
});
</script>
<script type="text/javascript">
function openTreeInNewTab(treeUUID) {
window.opener.openTreeInNewTab(treeUUID);
};
</script>
{% endblock %}
{% block content %} {% block content %}
<center>
<h4>{{ url }}</h4> <center>
<button onclick="window.history.back();" class="btn btn-primary" type="button">Go Back</button> <h4>{{ url }}</h4>
</center> </center>
<div class="table-responsive">
<table id="table" class="table" style="width:96%"> <script type="text/javascript">
<thead> new DataTable('#urlTable', {
<tr> order: [[ 0, "desc" ]],
<th>Start timestamp</th> columnDefs: [{ width: '20%', targets: 0,
<th>Captures</th> render: (data) => {
</tr> const date = new Date(data);
</thead> return date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, "0") + '-' + date.getDate().toString().padStart(2, "0") + ' ' + date.toTimeString();
<tbody> }
{% for hit in hits %} },
<tr> { width: '40%', targets: 1 },
<td> { width: '40%', targets: 2 }],
{{ hit['start_timestamp'] }} });
</td> </script>
<td><a href="{{ url_for('tree', tree_uuid=hit['capture_uuid']) }}">{{ hit['title'] }}</a>
</br> <table id="urlTable" class="table table-striped" style="width:100%">
Nodes: <thead>
<ul> <tr>
{% for urlnode_uuid, data in hit['urlnodes'].items() %} <th>Capture Time</th>
<li><a href="{{ url_for('tree', tree_uuid=hit['capture_uuid'], node_uuid=data['hostnode_uuid']) }}">{{ data['start_time'] }}</a></li> <th>Capture Title</th>
{% endfor %} <th>Landing page</th>
</ul> </tr>
</td> </thead>
</tr> <tbody>
{% endfor %} {% for capture_uuid, title, landing_page, capture_time in captures %}
</tbody> <tr>
</table> <td>
</div> {{capture_time}}
<p>The same file was seen in these captures:</p> </td>
<ul> <td>
{% for capture_uuid, title in captures %} <a href="{{ url_for('tree', tree_uuid=capture_uuid) }}">
<li><a href="#/" onclick="openTreeInNewTab('{{ capture_uuid }}')">{{ title }}</a></li> {{ title }}
{% endfor %} </a>
</ul> </td>
<td>
<span class="d-inline-block text-break" style="max-width: 400px;">
{{ landing_page }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %} {% endblock %}