From ec503b9ec684a2121a8c975b82e99bfeffe8332e Mon Sep 17 00:00:00 2001 From: anti Date: Wed, 8 Apr 2026 21:01:05 -0400 Subject: [PATCH] feat: implement advanced live logs with KQL search, histogram, and live/historical modes --- .hypothesis/constants/0dde2bfda6648a83 | 4 + .hypothesis/constants/0ff817caa72a52a4 | 4 + .hypothesis/constants/15d50d1e53b9b5c3 | 4 + .hypothesis/constants/19d5adc9efd5ec68 | 4 + .hypothesis/constants/1f005a833d034313 | 4 + .hypothesis/constants/1f12b014d4fe2068 | 4 + .hypothesis/constants/2220ccbe8a25f02d | 4 + .hypothesis/constants/2f0b53ebdb35c4e1 | 4 + .hypothesis/constants/3048c6da87ba838d | 4 + .hypothesis/constants/30a7ffe86227f7f1 | 4 + .hypothesis/constants/349ec22a74b50191 | 4 + .hypothesis/constants/37d6bf6c6c0b58e6 | 4 + .hypothesis/constants/3cc47bb868bcb8f4 | 4 + .hypothesis/constants/409656273e7d498b | 4 + .hypothesis/constants/42a1dcb5c22b1ac1 | 4 + .hypothesis/constants/4b12b89e1879f5ab | 4 + .hypothesis/constants/4efedb0b38145ee9 | 4 + .hypothesis/constants/4f57659a52de3fc8 | 4 + .hypothesis/constants/507a3145954fca93 | 4 + .hypothesis/constants/53a42446f9f19b20 | 4 + .hypothesis/constants/574dbe54f9b23d3e | 4 + .hypothesis/constants/62e387790ed5b79f | 4 + .hypothesis/constants/66bd79275cd609e8 | 4 + .hypothesis/constants/76302489300fdc45 | 4 + .hypothesis/constants/77b4b42ea3b9c9bf | 4 + .hypothesis/constants/79661beef79449a5 | 4 + .hypothesis/constants/7f9302a54093ce41 | 4 + .hypothesis/constants/8029f0494746966f | 4 + .hypothesis/constants/8647241ce03e8b57 | 4 + .hypothesis/constants/87dce71ef389d477 | 4 + .hypothesis/constants/8b9368be0f77a253 | 4 + .hypothesis/constants/8c9335cb8231944a | 4 + .hypothesis/constants/8e330e30c399dccc | 4 + .hypothesis/constants/9193e12e937c9da2 | 4 + .hypothesis/constants/933f6b3526e97b62 | 4 + .hypothesis/constants/952b61539a326753 | 4 + .hypothesis/constants/95eb634544ca6000 | 4 + .hypothesis/constants/996aa9c745349122 | 4 + .hypothesis/constants/a115dde40ee13bf8 | 4 + .hypothesis/constants/a36433a7a8a46f4d | 4 + .hypothesis/constants/a36bdeb88e27cda2 | 4 + .hypothesis/constants/a4b0cd024dec37b3 | 4 + .hypothesis/constants/a92a9b5d6ef7fbda | 4 + .hypothesis/constants/b0cdd7ca461ac3a7 | 4 + .hypothesis/constants/b3ae76f264e289ba | 4 + .hypothesis/constants/b4fbfe7d71d1fde1 | 4 + .hypothesis/constants/c1bae63b725863f0 | 4 + .hypothesis/constants/c604d77c59dde05f | 4 + .hypothesis/constants/cc88ec3582943bc7 | 4 + .hypothesis/constants/ceb1d0465029fa83 | 4 + .hypothesis/constants/da39a3ee5e6b4b0d | 4 + .hypothesis/constants/e04c4b026eeb7e26 | 4 + .hypothesis/constants/f61b7d1d118bca37 | 4 + .hypothesis/constants/fb7b3bbd8bd7b0f3 | 4 + .../unicode_data/16.0.0/charmap.json.gz | Bin 0 -> 22308 bytes .../unicode_data/16.0.0/codec-utf-8.json.gz | Bin 0 -> 60 bytes decnet/web/api.py | 10 +- decnet/web/sqlite_repository.py | 140 ++++-- decnet_web/package-lock.json | 413 +++++++++++++++++- decnet_web/package.json | 3 +- decnet_web/src/components/LiveLogs.tsx | 341 ++++++++++++++- 61 files changed, 1083 insertions(+), 40 deletions(-) create mode 100644 .hypothesis/constants/0dde2bfda6648a83 create mode 100644 .hypothesis/constants/0ff817caa72a52a4 create mode 100644 .hypothesis/constants/15d50d1e53b9b5c3 create mode 100644 .hypothesis/constants/19d5adc9efd5ec68 create mode 100644 .hypothesis/constants/1f005a833d034313 create mode 100644 .hypothesis/constants/1f12b014d4fe2068 create mode 100644 .hypothesis/constants/2220ccbe8a25f02d create mode 100644 .hypothesis/constants/2f0b53ebdb35c4e1 create mode 100644 .hypothesis/constants/3048c6da87ba838d create mode 100644 .hypothesis/constants/30a7ffe86227f7f1 create mode 100644 .hypothesis/constants/349ec22a74b50191 create mode 100644 .hypothesis/constants/37d6bf6c6c0b58e6 create mode 100644 .hypothesis/constants/3cc47bb868bcb8f4 create mode 100644 .hypothesis/constants/409656273e7d498b create mode 100644 .hypothesis/constants/42a1dcb5c22b1ac1 create mode 100644 .hypothesis/constants/4b12b89e1879f5ab create mode 100644 .hypothesis/constants/4efedb0b38145ee9 create mode 100644 .hypothesis/constants/4f57659a52de3fc8 create mode 100644 .hypothesis/constants/507a3145954fca93 create mode 100644 .hypothesis/constants/53a42446f9f19b20 create mode 100644 .hypothesis/constants/574dbe54f9b23d3e create mode 100644 .hypothesis/constants/62e387790ed5b79f create mode 100644 .hypothesis/constants/66bd79275cd609e8 create mode 100644 .hypothesis/constants/76302489300fdc45 create mode 100644 .hypothesis/constants/77b4b42ea3b9c9bf create mode 100644 .hypothesis/constants/79661beef79449a5 create mode 100644 .hypothesis/constants/7f9302a54093ce41 create mode 100644 .hypothesis/constants/8029f0494746966f create mode 100644 .hypothesis/constants/8647241ce03e8b57 create mode 100644 .hypothesis/constants/87dce71ef389d477 create mode 100644 .hypothesis/constants/8b9368be0f77a253 create mode 100644 .hypothesis/constants/8c9335cb8231944a create mode 100644 .hypothesis/constants/8e330e30c399dccc create mode 100644 .hypothesis/constants/9193e12e937c9da2 create mode 100644 .hypothesis/constants/933f6b3526e97b62 create mode 100644 .hypothesis/constants/952b61539a326753 create mode 100644 .hypothesis/constants/95eb634544ca6000 create mode 100644 .hypothesis/constants/996aa9c745349122 create mode 100644 .hypothesis/constants/a115dde40ee13bf8 create mode 100644 .hypothesis/constants/a36433a7a8a46f4d create mode 100644 .hypothesis/constants/a36bdeb88e27cda2 create mode 100644 .hypothesis/constants/a4b0cd024dec37b3 create mode 100644 .hypothesis/constants/a92a9b5d6ef7fbda create mode 100644 .hypothesis/constants/b0cdd7ca461ac3a7 create mode 100644 .hypothesis/constants/b3ae76f264e289ba create mode 100644 .hypothesis/constants/b4fbfe7d71d1fde1 create mode 100644 .hypothesis/constants/c1bae63b725863f0 create mode 100644 .hypothesis/constants/c604d77c59dde05f create mode 100644 .hypothesis/constants/cc88ec3582943bc7 create mode 100644 .hypothesis/constants/ceb1d0465029fa83 create mode 100644 .hypothesis/constants/da39a3ee5e6b4b0d create mode 100644 .hypothesis/constants/e04c4b026eeb7e26 create mode 100644 .hypothesis/constants/f61b7d1d118bca37 create mode 100644 .hypothesis/constants/fb7b3bbd8bd7b0f3 create mode 100644 .hypothesis/unicode_data/16.0.0/charmap.json.gz create mode 100644 .hypothesis/unicode_data/16.0.0/codec-utf-8.json.gz diff --git a/.hypothesis/constants/0dde2bfda6648a83 b/.hypothesis/constants/0dde2bfda6648a83 new file mode 100644 index 0000000..70b9640 --- /dev/null +++ b/.hypothesis/constants/0dde2bfda6648a83 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/sqlite_repository.py +# hypothesis_version: 6.151.11 + +[' AND ', ' WHERE ', ':', '[^a-zA-Z0-9_]', 'active_deckies', 'attacker', 'attacker-ip', 'attacker_ip', 'bucket_time', 'count', 'decky', 'decnet.db', 'deployed_deckies', 'event', 'event_type', 'fields', 'id > ?', 'max_id', 'msg', 'must_change_password', 'password_hash', 'raw_line', 'role', 'service', 'time', 'timestamp', 'timestamp <= ?', 'timestamp >= ?', 'total', 'total_logs', 'unique_attackers', 'username', 'uuid'] \ No newline at end of file diff --git a/.hypothesis/constants/0ff817caa72a52a4 b/.hypothesis/constants/0ff817caa72a52a4 new file mode 100644 index 0000000..5254387 --- /dev/null +++ b/.hypothesis/constants/0ff817caa72a52a4 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/logging/file_handler.py +# hypothesis_version: 6.151.11 + +[1024, '%(message)s', 'DECNET_LOG_FILE', 'decnet.syslog', 'utf-8'] \ No newline at end of file diff --git a/.hypothesis/constants/15d50d1e53b9b5c3 b/.hypothesis/constants/15d50d1e53b9b5c3 new file mode 100644 index 0000000..ea03552 --- /dev/null +++ b/.hypothesis/constants/15d50d1e53b9b5c3 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/tftp.py +# hypothesis_version: 6.151.11 + +['LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'restart', 'templates', 'tftp', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/19d5adc9efd5ec68 b/.hypothesis/constants/19d5adc9efd5ec68 new file mode 100644 index 0000000..372bfa2 --- /dev/null +++ b/.hypothesis/constants/19d5adc9efd5ec68 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/ingester.py +# hypothesis_version: 6.151.11 + +['.json', 'decnet.web.ingester', 'r', 'replace', 'utf-8'] \ No newline at end of file diff --git a/.hypothesis/constants/1f005a833d034313 b/.hypothesis/constants/1f005a833d034313 new file mode 100644 index 0000000..4df4a4b --- /dev/null +++ b/.hypothesis/constants/1f005a833d034313 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/logging/forwarder.py +# hypothesis_version: 6.151.11 + +[2.0, ':'] \ No newline at end of file diff --git a/.hypothesis/constants/1f12b014d4fe2068 b/.hypothesis/constants/1f12b014d4fe2068 new file mode 100644 index 0000000..4ae687e --- /dev/null +++ b/.hypothesis/constants/1f12b014d4fe2068 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/mongodb.py +# hypothesis_version: 6.151.11 + +[27017, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'mongodb', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/2220ccbe8a25f02d b/.hypothesis/constants/2220ccbe8a25f02d new file mode 100644 index 0000000..aa9c846 --- /dev/null +++ b/.hypothesis/constants/2220ccbe8a25f02d @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/snmp.py +# hypothesis_version: 6.151.11 + +[161, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'restart', 'snmp', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/2f0b53ebdb35c4e1 b/.hypothesis/constants/2f0b53ebdb35c4e1 new file mode 100644 index 0000000..6fc4732 --- /dev/null +++ b/.hypothesis/constants/2f0b53ebdb35c4e1 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/sip.py +# hypothesis_version: 6.151.11 + +[5060, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'restart', 'sip', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/3048c6da87ba838d b/.hypothesis/constants/3048c6da87ba838d new file mode 100644 index 0000000..18ba47e --- /dev/null +++ b/.hypothesis/constants/3048c6da87ba838d @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/auth.py +# hypothesis_version: 6.151.11 + +[1440, 'HS256', 'exp', 'iat', 'utf-8'] \ No newline at end of file diff --git a/.hypothesis/constants/30a7ffe86227f7f1 b/.hypothesis/constants/30a7ffe86227f7f1 new file mode 100644 index 0000000..fa23399 --- /dev/null +++ b/.hypothesis/constants/30a7ffe86227f7f1 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/mssql.py +# hypothesis_version: 6.151.11 + +[1433, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'mssql', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/349ec22a74b50191 b/.hypothesis/constants/349ec22a74b50191 new file mode 100644 index 0000000..8399e32 --- /dev/null +++ b/.hypothesis/constants/349ec22a74b50191 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/composer.py +# hypothesis_version: 6.151.11 + +['/var/log/decnet', '3.8', 'BASE_IMAGE', 'DECNET_LOG_FILE', 'HOSTNAME', 'NET_ADMIN', 'args', 'bridge', 'build', 'cap_add', 'command', 'container_name', 'decnet_logs', 'depends_on', 'driver', 'environment', 'external', 'hostname', 'image', 'infinity', 'internal', 'ipv4_address', 'network_mode', 'networks', 'restart', 'services', 'sleep', 'sysctls', 'unless-stopped', 'version', 'volumes'] \ No newline at end of file diff --git a/.hypothesis/constants/37d6bf6c6c0b58e6 b/.hypothesis/constants/37d6bf6c6c0b58e6 new file mode 100644 index 0000000..f20a315 --- /dev/null +++ b/.hypothesis/constants/37d6bf6c6c0b58e6 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/elasticsearch.py +# hypothesis_version: 6.151.11 + +[9200, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'elasticsearch', 'environment', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/3cc47bb868bcb8f4 b/.hypothesis/constants/3cc47bb868bcb8f4 new file mode 100644 index 0000000..7cdc792 --- /dev/null +++ b/.hypothesis/constants/3cc47bb868bcb8f4 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/telnet.py +# hypothesis_version: 6.151.11 + +[':', 'COWRIE_SSH_ENABLED', 'NET_BIND_SERVICE', 'cap_add', 'container_name', 'cowrie/cowrie', 'environment', 'false', 'image', 'restart', 'telnet', 'true', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/409656273e7d498b b/.hypothesis/constants/409656273e7d498b new file mode 100644 index 0000000..70b9640 --- /dev/null +++ b/.hypothesis/constants/409656273e7d498b @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/sqlite_repository.py +# hypothesis_version: 6.151.11 + +[' AND ', ' WHERE ', ':', '[^a-zA-Z0-9_]', 'active_deckies', 'attacker', 'attacker-ip', 'attacker_ip', 'bucket_time', 'count', 'decky', 'decnet.db', 'deployed_deckies', 'event', 'event_type', 'fields', 'id > ?', 'max_id', 'msg', 'must_change_password', 'password_hash', 'raw_line', 'role', 'service', 'time', 'timestamp', 'timestamp <= ?', 'timestamp >= ?', 'total', 'total_logs', 'unique_attackers', 'username', 'uuid'] \ No newline at end of file diff --git a/.hypothesis/constants/42a1dcb5c22b1ac1 b/.hypothesis/constants/42a1dcb5c22b1ac1 new file mode 100644 index 0000000..3aba509 --- /dev/null +++ b/.hypothesis/constants/42a1dcb5c22b1ac1 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/ini_loader.py +# hypothesis_version: 6.151.11 + +[100, 512, 1024, ',', '.', '1', '[', ']', 'amount', 'archetype', 'binary', 'custom-', 'exceeds maximum', 'exec', 'general', 'gw', 'interface', 'ip', 'log-target', 'log_target', 'mutate-interval', 'mutate_interval', 'net', 'nmap-os', 'nmap_os', 'ports', 'services'] \ No newline at end of file diff --git a/.hypothesis/constants/4b12b89e1879f5ab b/.hypothesis/constants/4b12b89e1879f5ab new file mode 100644 index 0000000..16932ef --- /dev/null +++ b/.hypothesis/constants/4b12b89e1879f5ab @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/archetypes.py +# hypothesis_version: 6.151.11 + +[', ', 'Database Server', 'DevOps Host', 'Domain Controller', 'File Server', 'IoT Device', 'Linux Server', 'Mail Server', 'Monitoring Node', 'Network Printer', 'VoIP Server', 'Web Server', 'Windows Server', 'Windows Workstation', 'alpine', 'conpot', 'database-server', 'deaddeck', 'debian', 'devops-host', 'docker_api', 'domain-controller', 'embedded', 'fedora', 'file-server', 'ftp', 'http', 'imap', 'industrial-control', 'iot-device', 'k8s', 'ldap', 'linux', 'linux-server', 'llmnr', 'mail-server', 'monitoring-node', 'mqtt', 'mysql', 'pop3', 'postgres', 'printer', 'rdp', 'real_ssh', 'redis', 'rocky9', 'sip', 'smb', 'smtp', 'snmp', 'ssh', 'telnet', 'ubuntu20', 'ubuntu22', 'voip-server', 'web-server', 'windows', 'windows-server', 'windows-workstation'] \ No newline at end of file diff --git a/.hypothesis/constants/4efedb0b38145ee9 b/.hypothesis/constants/4efedb0b38145ee9 new file mode 100644 index 0000000..b1c7a57 --- /dev/null +++ b/.hypothesis/constants/4efedb0b38145ee9 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/env.py +# hypothesis_version: 6.151.11 + +['.env', '.env.local', '0.0.0.0', '8000', '8080', 'DECNET_ADMIN_USER', 'DECNET_API_HOST', 'DECNET_API_PORT', 'DECNET_JWT_SECRET', 'DECNET_WEB_HOST', 'DECNET_WEB_PORT', 'admin'] \ No newline at end of file diff --git a/.hypothesis/constants/4f57659a52de3fc8 b/.hypothesis/constants/4f57659a52de3fc8 new file mode 100644 index 0000000..9583d59 --- /dev/null +++ b/.hypothesis/constants/4f57659a52de3fc8 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/cli.py +# hypothesis_version: 6.151.11 + +[8000, ',', ', ', '--all', '--api', '--api-port', '--archetype', '--config', '--deckies', '--decky', '--distro', '--dry-run', '--emit-syslog', '--host', '--id', '--interface', '--ip-start', '--ipvlan', '--log-file', '--log-target', '--min-deckies', '--mode', '--mutate-interval', '--no-cache', '--output', '--port', '--randomize-distros', '--randomize-services', '--services', '--subnet', '--watch', '--web-port', '-a', '-c', '-d', '-f', '-i', '-m', '-n', '-o', '-w', '/index.html', '0.0.0.0', 'Available Services', 'Default Services', 'Description', 'Display Name', 'Docker Image', 'Image', 'Machine Archetypes', 'Name', 'Ports', 'Slug', 'archetypes', 'bold cyan', 'correlate', 'decnet', 'decnet.cli', 'decnet.log', 'decnet.web.api:app', 'decnet_web', 'dim', 'dist', 'distros', 'green', 'json', 'linux', 'mutate', 'services', 'swarm', 'syslog', 'table', 'unihost', 'uvicorn', 'web'] \ No newline at end of file diff --git a/.hypothesis/constants/507a3145954fca93 b/.hypothesis/constants/507a3145954fca93 new file mode 100644 index 0000000..59e91a6 --- /dev/null +++ b/.hypothesis/constants/507a3145954fca93 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/http.py +# hypothesis_version: 6.151.11 + +[443, '/opt/html_files', 'CUSTOM_BODY', 'EXTRA_HEADERS', 'FAKE_APP', 'FILES_DIR', 'LOG_TARGET', 'NODE_NAME', 'RESPONSE_CODE', 'SERVER_HEADER', 'build', 'container_name', 'context', 'custom_body', 'environment', 'extra_headers', 'fake_app', 'files', 'http', 'response_code', 'restart', 'server_header', 'templates', 'unless-stopped', 'volumes'] \ No newline at end of file diff --git a/.hypothesis/constants/53a42446f9f19b20 b/.hypothesis/constants/53a42446f9f19b20 new file mode 100644 index 0000000..d8c1242 --- /dev/null +++ b/.hypothesis/constants/53a42446f9f19b20 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/ftp.py +# hypothesis_version: 6.151.11 + +['LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'ftp', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/574dbe54f9b23d3e b/.hypothesis/constants/574dbe54f9b23d3e new file mode 100644 index 0000000..0d41063 --- /dev/null +++ b/.hypothesis/constants/574dbe54f9b23d3e @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/correlation/engine.py +# hypothesis_version: 6.151.11 + +[3600, ',', 'Attacker IP', 'Deckies', 'Duration', 'Events', 'First Seen', 'Traversal Path', 'bold red', 'correlator', 'cyan', 'decnet-correlator', 'dim', 'events_indexed', 'lines_parsed', 'right', 'stats', 'traversal_detected', 'traversals', 'unique_ips', 'yellow'] \ No newline at end of file diff --git a/.hypothesis/constants/62e387790ed5b79f b/.hypothesis/constants/62e387790ed5b79f new file mode 100644 index 0000000..09cb0cb --- /dev/null +++ b/.hypothesis/constants/62e387790ed5b79f @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/vnc.py +# hypothesis_version: 6.151.11 + +[5900, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'restart', 'templates', 'unless-stopped', 'vnc'] \ No newline at end of file diff --git a/.hypothesis/constants/66bd79275cd609e8 b/.hypothesis/constants/66bd79275cd609e8 new file mode 100644 index 0000000..21e2f78 --- /dev/null +++ b/.hypothesis/constants/66bd79275cd609e8 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/correlation/parser.py +# hypothesis_version: 6.151.11 + +['"', '-', '\\', '\\"', '\\\\', '\\]', ']', 'client_ip', 'ip', 'remote_ip', 'src', 'src_ip'] \ No newline at end of file diff --git a/.hypothesis/constants/76302489300fdc45 b/.hypothesis/constants/76302489300fdc45 new file mode 100644 index 0000000..68be2a1 --- /dev/null +++ b/.hypothesis/constants/76302489300fdc45 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/config.py +# hypothesis_version: 6.151.11 + +[0.0, ':', 'compose_path', 'config', 'debian', 'debian:bookworm-slim', 'decnet-state.json', 'linux', 'log_target', 'services', 'swarm', 'unihost'] \ No newline at end of file diff --git a/.hypothesis/constants/77b4b42ea3b9c9bf b/.hypothesis/constants/77b4b42ea3b9c9bf new file mode 100644 index 0000000..dbaf534 --- /dev/null +++ b/.hypothesis/constants/77b4b42ea3b9c9bf @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/llmnr.py +# hypothesis_version: 6.151.11 + +[5353, 5355, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'llmnr', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/79661beef79449a5 b/.hypothesis/constants/79661beef79449a5 new file mode 100644 index 0000000..427d19f --- /dev/null +++ b/.hypothesis/constants/79661beef79449a5 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/registry.py +# hypothesis_version: 6.151.11 + +['base', 'decnet.services.', 'registry'] \ No newline at end of file diff --git a/.hypothesis/constants/7f9302a54093ce41 b/.hypothesis/constants/7f9302a54093ce41 new file mode 100644 index 0000000..18fa665 --- /dev/null +++ b/.hypothesis/constants/7f9302a54093ce41 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/correlation/__init__.py +# hypothesis_version: 6.151.11 + +['AttackerTraversal', 'CorrelationEngine', 'LogEvent', 'TraversalHop', 'parse_line'] \ No newline at end of file diff --git a/.hypothesis/constants/8029f0494746966f b/.hypothesis/constants/8029f0494746966f new file mode 100644 index 0000000..f4746df --- /dev/null +++ b/.hypothesis/constants/8029f0494746966f @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/ldap.py +# hypothesis_version: 6.151.11 + +[389, 636, 'LOG_TARGET', 'NET_BIND_SERVICE', 'NODE_NAME', 'build', 'cap_add', 'container_name', 'context', 'environment', 'ldap', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/8647241ce03e8b57 b/.hypothesis/constants/8647241ce03e8b57 new file mode 100644 index 0000000..0add520 --- /dev/null +++ b/.hypothesis/constants/8647241ce03e8b57 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/api.py +# hypothesis_version: 6.151.11 + +[400, 404, 500, 512, 1000, 1024, '*', '/api/v1/auth/login', '/api/v1/deckies', '/api/v1/logs', '/api/v1/stats', '/api/v1/stream', '1.0.0', 'Authorization', 'Bearer', 'Bearer ', 'Decky not found', 'No active deployment', 'WWW-Authenticate', 'access_token', 'admin', 'bearer', 'data', 'decnet.web.api', 'histogram', 'id', 'lastEventId', 'limit', 'logs', 'message', 'must_change_password', 'offset', 'password_hash', 'role', 'stats', 'text/event-stream', 'token', 'token_type', 'total', 'type', 'unihost', 'username', 'uuid'] \ No newline at end of file diff --git a/.hypothesis/constants/87dce71ef389d477 b/.hypothesis/constants/87dce71ef389d477 new file mode 100644 index 0000000..795c72e --- /dev/null +++ b/.hypothesis/constants/87dce71ef389d477 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/mqtt.py +# hypothesis_version: 6.151.11 + +[1883, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'mqtt', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/8b9368be0f77a253 b/.hypothesis/constants/8b9368be0f77a253 new file mode 100644 index 0000000..c01b6c8 --- /dev/null +++ b/.hypothesis/constants/8b9368be0f77a253 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/custom_service.py +# hypothesis_version: 6.151.11 + +['-', 'LOG_TARGET', 'NODE_NAME', '_', 'command', 'container_name', 'environment', 'image', 'restart', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/8c9335cb8231944a b/.hypothesis/constants/8c9335cb8231944a new file mode 100644 index 0000000..40e2302 --- /dev/null +++ b/.hypothesis/constants/8c9335cb8231944a @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/docker_api.py +# hypothesis_version: 6.151.11 + +[2375, 2376, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'docker_api', 'environment', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/8e330e30c399dccc b/.hypothesis/constants/8e330e30c399dccc new file mode 100644 index 0000000..7b9b106 --- /dev/null +++ b/.hypothesis/constants/8e330e30c399dccc @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/real_ssh.py +# hypothesis_version: 6.151.11 + +['NET_BIND_SERVICE', 'SSH_HOSTNAME', 'SSH_ROOT_PASSWORD', 'admin', 'build', 'cap_add', 'container_name', 'context', 'environment', 'hostname', 'password', 'real_ssh', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/9193e12e937c9da2 b/.hypothesis/constants/9193e12e937c9da2 new file mode 100644 index 0000000..3481ac4 --- /dev/null +++ b/.hypothesis/constants/9193e12e937c9da2 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/logging/syslog_formatter.py +# hypothesis_version: 6.151.11 + +[255, '"', '-', '1', '\\', '\\"', '\\\\', '\\]', ']', 'decnet@55555'] \ No newline at end of file diff --git a/.hypothesis/constants/933f6b3526e97b62 b/.hypothesis/constants/933f6b3526e97b62 new file mode 100644 index 0000000..275bac0 --- /dev/null +++ b/.hypothesis/constants/933f6b3526e97b62 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/web/repository.py +# hypothesis_version: 6.151.11 + +[] \ No newline at end of file diff --git a/.hypothesis/constants/952b61539a326753 b/.hypothesis/constants/952b61539a326753 new file mode 100644 index 0000000..8eeb44a --- /dev/null +++ b/.hypothesis/constants/952b61539a326753 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/ssh.py +# hypothesis_version: 6.151.11 + +[2222, ':', 'COWRIE_HOSTNAME', 'COWRIE_SSH_VERSION', 'NET_BIND_SERVICE', 'NODE_NAME', 'build', 'cap_add', 'container_name', 'context', 'cowrie', 'environment', 'hardware_platform', 'kernel_build_string', 'kernel_version', 'restart', 'ssh', 'ssh_banner', 'templates', 'true', 'unless-stopped', 'users'] \ No newline at end of file diff --git a/.hypothesis/constants/95eb634544ca6000 b/.hypothesis/constants/95eb634544ca6000 new file mode 100644 index 0000000..7b49590 --- /dev/null +++ b/.hypothesis/constants/95eb634544ca6000 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/smb.py +# hypothesis_version: 6.151.11 + +[139, 445, 'LOG_TARGET', 'NET_BIND_SERVICE', 'NODE_NAME', 'build', 'cap_add', 'container_name', 'context', 'environment', 'restart', 'smb', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/996aa9c745349122 b/.hypothesis/constants/996aa9c745349122 new file mode 100644 index 0000000..5f2dc31 --- /dev/null +++ b/.hypothesis/constants/996aa9c745349122 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/base.py +# hypothesis_version: 6.151.11 + +[] \ No newline at end of file diff --git a/.hypothesis/constants/a115dde40ee13bf8 b/.hypothesis/constants/a115dde40ee13bf8 new file mode 100644 index 0000000..6029563 --- /dev/null +++ b/.hypothesis/constants/a115dde40ee13bf8 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/mysql.py +# hypothesis_version: 6.151.11 + +[3306, 'LOG_TARGET', 'MYSQL_VERSION', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'mysql', 'restart', 'templates', 'unless-stopped', 'version'] \ No newline at end of file diff --git a/.hypothesis/constants/a36433a7a8a46f4d b/.hypothesis/constants/a36433a7a8a46f4d new file mode 100644 index 0000000..18ab98e --- /dev/null +++ b/.hypothesis/constants/a36433a7a8a46f4d @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/smtp.py +# hypothesis_version: 6.151.11 + +[587, 'LOG_TARGET', 'NET_BIND_SERVICE', 'NODE_NAME', 'SMTP_BANNER', 'SMTP_MTA', 'banner', 'build', 'cap_add', 'container_name', 'context', 'environment', 'mta', 'restart', 'smtp', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/a36bdeb88e27cda2 b/.hypothesis/constants/a36bdeb88e27cda2 new file mode 100644 index 0000000..632b00a --- /dev/null +++ b/.hypothesis/constants/a36bdeb88e27cda2 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/postgres.py +# hypothesis_version: 6.151.11 + +[5432, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'postgres', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/a4b0cd024dec37b3 b/.hypothesis/constants/a4b0cd024dec37b3 new file mode 100644 index 0000000..33a906a --- /dev/null +++ b/.hypothesis/constants/a4b0cd024dec37b3 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/pop3.py +# hypothesis_version: 6.151.11 + +[110, 995, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'pop3', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/a92a9b5d6ef7fbda b/.hypothesis/constants/a92a9b5d6ef7fbda new file mode 100644 index 0000000..11589ce --- /dev/null +++ b/.hypothesis/constants/a92a9b5d6ef7fbda @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/os_fingerprint.py +# hypothesis_version: 6.151.11 + +['128', '2', '255', '3', '6', '64', 'bsd', 'cisco', 'embedded', 'linux', 'windows'] \ No newline at end of file diff --git a/.hypothesis/constants/b0cdd7ca461ac3a7 b/.hypothesis/constants/b0cdd7ca461ac3a7 new file mode 100644 index 0000000..a12233e --- /dev/null +++ b/.hypothesis/constants/b0cdd7ca461ac3a7 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/k8s.py +# hypothesis_version: 6.151.11 + +[6443, 8080, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'k8s', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/b3ae76f264e289ba b/.hypothesis/constants/b3ae76f264e289ba new file mode 100644 index 0000000..be51ca8 --- /dev/null +++ b/.hypothesis/constants/b3ae76f264e289ba @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/redis.py +# hypothesis_version: 6.151.11 + +[6379, 'LOG_TARGET', 'NODE_NAME', 'REDIS_OS', 'REDIS_VERSION', 'build', 'container_name', 'context', 'environment', 'os_string', 'redis', 'restart', 'templates', 'unless-stopped', 'version'] \ No newline at end of file diff --git a/.hypothesis/constants/b4fbfe7d71d1fde1 b/.hypothesis/constants/b4fbfe7d71d1fde1 new file mode 100644 index 0000000..2250f94 --- /dev/null +++ b/.hypothesis/constants/b4fbfe7d71d1fde1 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/correlation/graph.py +# hypothesis_version: 6.151.11 + +[' → ', 'attacker_ip', 'deckies', 'decky', 'decky_count', 'duration_seconds', 'event_type', 'first_seen', 'hop_count', 'hops', 'last_seen', 'path', 'service', 'timestamp'] \ No newline at end of file diff --git a/.hypothesis/constants/c1bae63b725863f0 b/.hypothesis/constants/c1bae63b725863f0 new file mode 100644 index 0000000..2a9531f --- /dev/null +++ b/.hypothesis/constants/c1bae63b725863f0 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/imap.py +# hypothesis_version: 6.151.11 + +[143, 993, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'imap', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/c604d77c59dde05f b/.hypothesis/constants/c604d77c59dde05f new file mode 100644 index 0000000..5f7d4fe --- /dev/null +++ b/.hypothesis/constants/c604d77c59dde05f @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/network.py +# hypothesis_version: 6.151.11 + +['/', 'add', 'addr', 'bridge', 'decnet_ipvlan0', 'decnet_lan', 'decnet_macvlan0', 'default', 'del', 'dev', 'inet ', 'inet6', 'ip', 'ipvlan', 'ipvlan_mode', 'l2', 'link', 'macvlan', 'mode', 'parent', 'route', 'set', 'show', 'type', 'up', 'via'] \ No newline at end of file diff --git a/.hypothesis/constants/cc88ec3582943bc7 b/.hypothesis/constants/cc88ec3582943bc7 new file mode 100644 index 0000000..a0c1c88 --- /dev/null +++ b/.hypothesis/constants/cc88ec3582943bc7 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/distros.py +# hypothesis_version: 6.151.11 + +['Alpine Linux 3.19', 'Arch Linux', 'CentOS 7', 'Debian 12 (Bookworm)', 'Fedora 39', 'Kali Linux (Rolling)', 'Rocky Linux 9', 'alpha', 'alpine', 'alpine:3.19', 'arch', 'archlinux:latest', 'backup', 'bravo', 'centos7', 'centos:7', 'charlie', 'db', 'debian', 'debian:bookworm-slim', 'delta', 'dev', 'echo', 'fedora', 'fedora:39', 'files', 'foxtrot', 'generic', 'golf', 'hotel', 'india', 'juliet', 'kali', 'kilo', 'lima', 'mail', 'mike', 'minimal', 'monitor', 'nova', 'oscar', 'prod', 'proxy', 'rhel', 'rocky9', 'rockylinux:9-minimal', 'rolling', 'stage', 'ubuntu20', 'ubuntu22', 'ubuntu:20.04', 'ubuntu:22.04', 'web'] \ No newline at end of file diff --git a/.hypothesis/constants/ceb1d0465029fa83 b/.hypothesis/constants/ceb1d0465029fa83 new file mode 100644 index 0000000..962a59c --- /dev/null +++ b/.hypothesis/constants/ceb1d0465029fa83 @@ -0,0 +1,4 @@ +# file: /home/anti/.local/bin/pytest +# hypothesis_version: 6.151.11 + +['__main__'] \ No newline at end of file diff --git a/.hypothesis/constants/da39a3ee5e6b4b0d b/.hypothesis/constants/da39a3ee5e6b4b0d new file mode 100644 index 0000000..62b7279 --- /dev/null +++ b/.hypothesis/constants/da39a3ee5e6b4b0d @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/__init__.py +# hypothesis_version: 6.151.11 + +[] \ No newline at end of file diff --git a/.hypothesis/constants/e04c4b026eeb7e26 b/.hypothesis/constants/e04c4b026eeb7e26 new file mode 100644 index 0000000..dfad0cb --- /dev/null +++ b/.hypothesis/constants/e04c4b026eeb7e26 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/rdp.py +# hypothesis_version: 6.151.11 + +[3389, 'LOG_TARGET', 'NODE_NAME', 'build', 'container_name', 'context', 'environment', 'rdp', 'restart', 'templates', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/constants/f61b7d1d118bca37 b/.hypothesis/constants/f61b7d1d118bca37 new file mode 100644 index 0000000..caadac3 --- /dev/null +++ b/.hypothesis/constants/f61b7d1d118bca37 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/deployer.py +# hypothesis_version: 6.151.11 + +[5.0, ', ', '--build', '--no-cache', '--watch', '-d', '-f', 'DECNET Deckies', 'Decky', 'Deployed Deckies', 'Hostname', 'IP', 'IPvlan', 'IPvlan L2', 'MACVLAN', 'Services', 'Status', '[green]up[/]', '[red]degraded[/]', 'absent', 'bold', 'build', 'cmdline', 'compose', 'decnet-compose.yml', 'decnet.cli', 'decnet.web.api:app', 'docker', 'down', 'green', 'manifest for', 'manifest unknown', 'mutate', 'name', 'not found', 'pid', 'pull access denied', 'red', 'rm', 'running', 'stop', 'up', 'uvicorn'] \ No newline at end of file diff --git a/.hypothesis/constants/fb7b3bbd8bd7b0f3 b/.hypothesis/constants/fb7b3bbd8bd7b0f3 new file mode 100644 index 0000000..3749001 --- /dev/null +++ b/.hypothesis/constants/fb7b3bbd8bd7b0f3 @@ -0,0 +1,4 @@ +# file: /home/anti/Tools/DECNET/decnet/services/conpot.py +# hypothesis_version: 6.151.11 + +[161, 502, 'CONPOT_TEMPLATE', 'conpot', 'container_name', 'default', 'environment', 'honeynet/conpot', 'image', 'restart', 'unless-stopped'] \ No newline at end of file diff --git a/.hypothesis/unicode_data/16.0.0/charmap.json.gz b/.hypothesis/unicode_data/16.0.0/charmap.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..1bfee5ca433cb10697a50f0ce585ab95980b61f8 GIT binary patch literal 22308 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 StreamingResponse: import json @@ -245,7 +247,7 @@ async def stream_events( break # Poll for new logs - new_logs = await repo.get_logs_after_id(last_id, limit=50, search=search) + new_logs = await repo.get_logs_after_id(last_id, limit=50, search=search, start_time=start_time, end_time=end_time) if new_logs: # Update last_id to the max id in the fetched batch last_id = max(log["id"] for log in new_logs) @@ -260,6 +262,12 @@ async def stream_events( stats = await repo.get_stats_summary() payload = json.dumps({"type": "stats", "data": stats}) yield f"event: message\ndata: {payload}\n\n" + + # Also yield histogram + histogram = await repo.get_log_histogram(search=search, start_time=start_time, end_time=end_time, interval_minutes=15) + hist_payload = json.dumps({"type": "histogram", "data": histogram}) + yield f"event: message\ndata: {hist_payload}\n\n" + loops_since_stats = 0 loops_since_stats += 1 diff --git a/decnet/web/sqlite_repository.py b/decnet/web/sqlite_repository.py index 45dc3c7..8cfc65c 100644 --- a/decnet/web/sqlite_repository.py +++ b/decnet/web/sqlite_repository.py @@ -12,6 +12,7 @@ class SQLiteRepository(BaseRepository): async def initialize(self) -> None: async with aiosqlite.connect(self.db_path) as _db: + await _db.execute("PRAGMA journal_mode=WAL") # Logs table await _db.execute(""" CREATE TABLE IF NOT EXISTS logs ( @@ -82,20 +83,76 @@ class SQLiteRepository(BaseRepository): ) await _db.commit() + def _build_where_clause( + self, + search: Optional[str] = None, + start_time: Optional[str] = None, + end_time: Optional[str] = None, + base_where: Optional[str] = None, + base_params: Optional[list[Any]] = None + ) -> tuple[str, list[Any]]: + import shlex + import re + + where_clauses = [] + params = [] + + if base_where: + where_clauses.append(base_where) + if base_params: + params.extend(base_params) + + if start_time: + where_clauses.append("timestamp >= ?") + params.append(start_time) + if end_time: + where_clauses.append("timestamp <= ?") + params.append(end_time) + + if search: + try: + tokens = shlex.split(search) + except ValueError: + tokens = search.split(" ") + + core_fields = { + "decky": "decky", + "service": "service", + "event": "event_type", + "attacker": "attacker_ip", + "attacker-ip": "attacker_ip", + "attacker_ip": "attacker_ip" + } + + for token in tokens: + if ":" in token: + key, val = token.split(":", 1) + if key in core_fields: + where_clauses.append(f"{core_fields[key]} = ?") + params.append(val) + else: + key_safe = re.sub(r'[^a-zA-Z0-9_]', '', key) + where_clauses.append(f"json_extract(fields, '$.{key_safe}') = ?") + params.append(val) + else: + where_clauses.append("(raw_line LIKE ? OR decky LIKE ? OR service LIKE ? OR attacker_ip LIKE ?)") + like_val = f"%{token}%" + params.extend([like_val, like_val, like_val, like_val]) + + if where_clauses: + return " WHERE " + " AND ".join(where_clauses), params + return "", [] + async def get_logs( self, limit: int = 50, offset: int = 0, - search: Optional[str] = None + search: Optional[str] = None, + start_time: Optional[str] = None, + end_time: Optional[str] = None ) -> list[dict[str, Any]]: - _query: str = "SELECT * FROM logs" - _params: list[Any] = [] - if search: - _query += " WHERE raw_line LIKE ? OR decky LIKE ? OR service LIKE ? OR attacker_ip LIKE ?" - _like_val: str = f"%{search}%" - _params.extend([_like_val, _like_val, _like_val, _like_val]) - - _query += " ORDER BY timestamp DESC LIMIT ? OFFSET ?" + _where, _params = self._build_where_clause(search, start_time, end_time) + _query = f"SELECT * FROM logs{_where} ORDER BY timestamp DESC LIMIT ? OFFSET ?" _params.extend([limit, offset]) async with aiosqlite.connect(self.db_path) as _db: @@ -112,16 +169,16 @@ class SQLiteRepository(BaseRepository): _row: aiosqlite.Row | None = await _cursor.fetchone() return _row["max_id"] if _row and _row["max_id"] is not None else 0 - async def get_logs_after_id(self, last_id: int, limit: int = 50, search: Optional[str] = None) -> list[dict[str, Any]]: - _query: str = "SELECT * FROM logs WHERE id > ?" - _params: list[Any] = [last_id] - - if search: - _query += " AND (raw_line LIKE ? OR decky LIKE ? OR service LIKE ? OR attacker_ip LIKE ?)" - _like_val: str = f"%{search}%" - _params.extend([_like_val, _like_val, _like_val, _like_val]) - - _query += " ORDER BY id ASC LIMIT ?" + async def get_logs_after_id( + self, + last_id: int, + limit: int = 50, + search: Optional[str] = None, + start_time: Optional[str] = None, + end_time: Optional[str] = None + ) -> list[dict[str, Any]]: + _where, _params = self._build_where_clause(search, start_time, end_time, base_where="id > ?", base_params=[last_id]) + _query = f"SELECT * FROM logs{_where} ORDER BY id ASC LIMIT ?" _params.append(limit) async with aiosqlite.connect(self.db_path) as _db: @@ -130,13 +187,14 @@ class SQLiteRepository(BaseRepository): _rows: list[aiosqlite.Row] = await _cursor.fetchall() return [dict(_row) for _row in _rows] - async def get_total_logs(self, search: Optional[str] = None) -> int: - _query: str = "SELECT COUNT(*) as total FROM logs" - _params: list[Any] = [] - if search: - _query += " WHERE raw_line LIKE ? OR decky LIKE ? OR service LIKE ? OR attacker_ip LIKE ?" - _like_val: str = f"%{search}%" - _params.extend([_like_val, _like_val, _like_val, _like_val]) + async def get_total_logs( + self, + search: Optional[str] = None, + start_time: Optional[str] = None, + end_time: Optional[str] = None + ) -> int: + _where, _params = self._build_where_clause(search, start_time, end_time) + _query = f"SELECT COUNT(*) as total FROM logs{_where}" async with aiosqlite.connect(self.db_path) as _db: _db.row_factory = aiosqlite.Row @@ -144,6 +202,36 @@ class SQLiteRepository(BaseRepository): _row: Optional[aiosqlite.Row] = await _cursor.fetchone() return _row["total"] if _row else 0 + async def get_log_histogram( + self, + search: Optional[str] = None, + start_time: Optional[str] = None, + end_time: Optional[str] = None, + interval_minutes: int = 15 + ) -> list[dict[str, Any]]: + # Map interval to sqlite strftime modifiers + # Since SQLite doesn't have an easy "bucket by X minutes" natively, + # we can do it by grouping by (strftime('%s', timestamp) / (interval_minutes * 60)) + # and then multiplying back to get the bucket start time. + + _where, _params = self._build_where_clause(search, start_time, end_time) + + _query = f""" + SELECT + datetime((strftime('%s', timestamp) / {interval_minutes * 60}) * {interval_minutes * 60}, 'unixepoch') as bucket_time, + COUNT(*) as count + FROM logs + {_where} + GROUP BY bucket_time + ORDER BY bucket_time ASC + """ + + async with aiosqlite.connect(self.db_path) as _db: + _db.row_factory = aiosqlite.Row + async with _db.execute(_query, _params) as _cursor: + _rows: list[aiosqlite.Row] = await _cursor.fetchall() + return [{"time": _row["bucket_time"], "count": _row["count"]} for _row in _rows] + async def get_stats_summary(self) -> dict[str, Any]: async with aiosqlite.connect(self.db_path) as _db: _db.row_factory = aiosqlite.Row diff --git a/decnet_web/package-lock.json b/decnet_web/package-lock.json index 1f29ab1..913bd2a 100644 --- a/decnet_web/package-lock.json +++ b/decnet_web/package-lock.json @@ -12,7 +12,8 @@ "lucide-react": "^1.7.0", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-router-dom": "^7.14.0" + "react-router-dom": "^7.14.0", + "recharts": "^3.8.1" }, "devDependencies": { "@eslint/js": "^9.39.4", @@ -591,6 +592,42 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.13", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.13.tgz", @@ -855,6 +892,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -866,6 +915,69 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -894,7 +1006,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -910,6 +1022,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.58.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", @@ -1437,6 +1555,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1515,9 +1642,130 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1536,6 +1784,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1628,6 +1882,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1835,6 +2099,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2146,6 +2416,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2173,6 +2453,15 @@ "node": ">=0.8.19" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2862,6 +3151,36 @@ "react": "^19.2.4" } }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", @@ -2900,6 +3219,57 @@ "react-dom": ">=18" } }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3032,6 +3402,12 @@ "node": ">=8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3169,6 +3545,37 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "8.0.7", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.7.tgz", diff --git a/decnet_web/package.json b/decnet_web/package.json index 5fdd23f..4e82f89 100644 --- a/decnet_web/package.json +++ b/decnet_web/package.json @@ -14,7 +14,8 @@ "lucide-react": "^1.7.0", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-router-dom": "^7.14.0" + "react-router-dom": "^7.14.0", + "recharts": "^3.8.1" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/decnet_web/src/components/LiveLogs.tsx b/decnet_web/src/components/LiveLogs.tsx index 0df96f5..7dd44e1 100644 --- a/decnet_web/src/components/LiveLogs.tsx +++ b/decnet_web/src/components/LiveLogs.tsx @@ -1,17 +1,340 @@ -import React from 'react'; -import { Terminal } from 'lucide-react'; +import React, { useEffect, useState, useRef } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { + Terminal, Search, Activity, + ChevronLeft, ChevronRight, Play, Pause +} from 'lucide-react'; +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell +} from 'recharts'; +import api from '../utils/api'; import './Dashboard.css'; +interface LogEntry { + id: number; + timestamp: string; + decky: string; + service: string; + event_type: string; + attacker_ip: string; + raw_line: string; + fields: string; + msg: string; +} + +interface HistogramData { + time: string; + count: number; +} + const LiveLogs: React.FC = () => { + const [searchParams, setSearchParams] = useSearchParams(); + + // URL-synced state + const query = searchParams.get('q') || ''; + const timeRange = searchParams.get('time') || '1h'; + const isLive = searchParams.get('live') !== 'false'; + const page = parseInt(searchParams.get('page') || '1'); + + // Local state + const [logs, setLogs] = useState([]); + const [histogram, setHistogram] = useState([]); + const [totalLogs, setTotalLogs] = useState(0); + const [loading, setLoading] = useState(true); + const [streaming, setStreaming] = useState(isLive); + const [searchInput, setSearchInput] = useState(query); + + const eventSourceRef = useRef(null); + const limit = 50; + + // Sync search input if URL changes (e.g. back button) + useEffect(() => { + setSearchInput(query); + }, [query]); + + const fetchData = async () => { + if (streaming) return; // Don't fetch historical if streaming + + setLoading(true); + try { + const offset = (page - 1) * limit; + let url = `/logs?limit=${limit}&offset=${offset}&search=${encodeURIComponent(query)}`; + + // Calculate time bounds for historical fetch + const now = new Date(); + let startTime: string | null = null; + if (timeRange !== 'all') { + const minutes = timeRange === '15m' ? 15 : timeRange === '1h' ? 60 : timeRange === '24h' ? 1440 : 0; + if (minutes > 0) { + startTime = new Date(now.getTime() - minutes * 60000).toISOString(); + url += `&start_time=${startTime}`; + } + } + + const res = await api.get(url); + setLogs(res.data.data); + setTotalLogs(res.data.total); + + // Fetch histogram for historical view + const histUrl = `/logs/histogram?search=${encodeURIComponent(query)}` + (startTime ? `&start_time=${startTime}` : ''); + const histRes = await api.get(histUrl); + setHistogram(histRes.data); + + } catch (err) { + console.error('Failed to fetch historical logs', err); + } finally { + setLoading(false); + } + }; + + const setupSSE = () => { + if (eventSourceRef.current) { + eventSourceRef.current.close(); + } + + const token = localStorage.getItem('token'); + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1'; + let url = `${baseUrl}/stream?token=${token}&search=${encodeURIComponent(query)}`; + + if (timeRange !== 'all') { + const minutes = timeRange === '15m' ? 15 : timeRange === '1h' ? 60 : timeRange === '24h' ? 1440 : 0; + if (minutes > 0) { + const startTime = new Date(Date.now() - minutes * 60000).toISOString(); + url += `&start_time=${startTime}`; + } + } + + const es = new EventSource(url); + eventSourceRef.current = es; + + es.onmessage = (event) => { + try { + const payload = JSON.parse(event.data); + if (payload.type === 'logs') { + setLogs(prev => [...payload.data, ...prev].slice(0, 500)); + } else if (payload.type === 'histogram') { + setHistogram(payload.data); + } else if (payload.type === 'stats') { + setTotalLogs(payload.data.total_logs); + } + } catch (err) { + console.error('Failed to parse SSE payload', err); + } + }; + + es.onerror = () => { + console.error('SSE connection lost, reconnecting...'); + }; + }; + + useEffect(() => { + if (streaming) { + setupSSE(); + setLoading(false); + } else { + if (eventSourceRef.current) { + eventSourceRef.current.close(); + eventSourceRef.current = null; + } + fetchData(); + } + + return () => { + if (eventSourceRef.current) { + eventSourceRef.current.close(); + } + }; + }, [query, timeRange, streaming, page]); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + setSearchParams({ q: searchInput, time: timeRange, live: streaming.toString(), page: '1' }); + }; + + const handleToggleLive = () => { + const newStreaming = !streaming; + setStreaming(newStreaming); + setSearchParams({ q: query, time: timeRange, live: newStreaming.toString(), page: '1' }); + }; + + const handleTimeChange = (newTime: string) => { + setSearchParams({ q: query, time: newTime, live: streaming.toString(), page: '1' }); + }; + + const changePage = (newPage: number) => { + setSearchParams({ q: query, time: timeRange, live: 'false', page: newPage.toString() }); + }; + return ( -
-
- -

FULL LIVE LOG STREAM

+
+ {/* Control Bar */} +
+
+
+ + setSearchInput(e.target.value)} + /> +
+ + +
-
-

STREAM ESTABLISHED. WAITING FOR INCOMING DATA...

-

(Dedicated Live Logs view placeholder)

+ + {/* Histogram Chart */} +
+
+
+ ATTACK VOLUME OVER TIME +
+
+ MATCHES: {totalLogs.toLocaleString()} +
+
+ + + + + Math.floor(val).toString()} + /> + + + {histogram.map((entry, index) => ( + h.count)) || 1)) * 0.4} /> + ))} + + + +
+ + {/* Logs Table */} +
+
+
+ +

LOG EXPLORER

+
+ {!streaming && ( +
+ + Page {page} of {Math.ceil(totalLogs / limit)} + +
+ + +
+
+ )} +
+ +
+ + + + + + + + + + + + {logs.length > 0 ? logs.map(log => { + let parsedFields: Record = {}; + if (log.fields) { + try { + parsedFields = JSON.parse(log.fields); + } catch (e) {} + } + + return ( + + + + + + + + ); + }) : ( + + + + )} + +
TIMESTAMPDECKYSERVICEATTACKEREVENT
{new Date(log.timestamp).toLocaleString()}{log.decky}{log.service}{log.attacker_ip} +
+
+ {log.event_type} {log.msg && log.msg !== '-' && — {log.msg}} +
+ {Object.keys(parsedFields).length > 0 && ( +
+ {Object.entries(parsedFields).map(([k, v]) => ( + + {k}: {typeof v === 'object' ? JSON.stringify(v) : v} + + ))} +
+ )} +
+
+ {loading ? 'RETRIEVING DATA...' : 'NO LOGS MATCHING CRITERIA'} +
+
);