netreplay 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. netreplay-0.1.0/LICENSE +21 -0
  2. netreplay-0.1.0/PKG-INFO +167 -0
  3. netreplay-0.1.0/README.md +143 -0
  4. netreplay-0.1.0/gui/__init__.py +10 -0
  5. netreplay-0.1.0/gui/api.py +81 -0
  6. netreplay-0.1.0/gui/components/__init__.py +1 -0
  7. netreplay-0.1.0/gui/components/details.py +71 -0
  8. netreplay-0.1.0/gui/components/flow_list.py +44 -0
  9. netreplay-0.1.0/gui/components/header.py +81 -0
  10. netreplay-0.1.0/gui/components/timeline.py +154 -0
  11. netreplay-0.1.0/gui/main.py +219 -0
  12. netreplay-0.1.0/netreplay/__init__.py +6 -0
  13. netreplay-0.1.0/netreplay/api/__init__.py +4 -0
  14. netreplay-0.1.0/netreplay/api/app.py +62 -0
  15. netreplay-0.1.0/netreplay/api/routes/__init__.py +14 -0
  16. netreplay-0.1.0/netreplay/api/routes/capture.py +90 -0
  17. netreplay-0.1.0/netreplay/api/routes/flows.py +76 -0
  18. netreplay-0.1.0/netreplay/api/routes/packets.py +41 -0
  19. netreplay-0.1.0/netreplay/api/routes/sessions.py +60 -0
  20. netreplay-0.1.0/netreplay/api/schemas.py +89 -0
  21. netreplay-0.1.0/netreplay/api/websocket.py +51 -0
  22. netreplay-0.1.0/netreplay/cli/__init__.py +4 -0
  23. netreplay-0.1.0/netreplay/cli/main.py +317 -0
  24. netreplay-0.1.0/netreplay/core/__init__.py +10 -0
  25. netreplay-0.1.0/netreplay/core/capture/__init__.py +10 -0
  26. netreplay-0.1.0/netreplay/core/capture/base.py +41 -0
  27. netreplay-0.1.0/netreplay/core/capture/pcap_backend.py +50 -0
  28. netreplay-0.1.0/netreplay/core/capture/scapy_backend.py +119 -0
  29. netreplay-0.1.0/netreplay/core/flows/models.py +71 -0
  30. netreplay-0.1.0/netreplay/core/flows/tracker.py +97 -0
  31. netreplay-0.1.0/netreplay/core/packets/models.py +51 -0
  32. netreplay-0.1.0/netreplay/core/packets/parser.py +130 -0
  33. netreplay-0.1.0/netreplay/core/protocols/__init__.py +1 -0
  34. netreplay-0.1.0/netreplay/core/protocols/decrypt.py +291 -0
  35. netreplay-0.1.0/netreplay/core/protocols/decrypt_service.py +115 -0
  36. netreplay-0.1.0/netreplay/core/protocols/dns.py +101 -0
  37. netreplay-0.1.0/netreplay/core/protocols/tls.py +145 -0
  38. netreplay-0.1.0/netreplay/core/replay/__init__.py +4 -0
  39. netreplay-0.1.0/netreplay/core/service.py +277 -0
  40. netreplay-0.1.0/netreplay/core/storage/__init__.py +16 -0
  41. netreplay-0.1.0/netreplay/core/storage/database.py +499 -0
  42. netreplay-0.1.0/netreplay/core/storage/nrp.py +49 -0
  43. netreplay-0.1.0/netreplay/core/timeline/__init__.py +14 -0
  44. netreplay-0.1.0/netreplay/core/timeline/service.py +132 -0
  45. netreplay-0.1.0/netreplay.egg-info/PKG-INFO +167 -0
  46. netreplay-0.1.0/netreplay.egg-info/SOURCES.txt +60 -0
  47. netreplay-0.1.0/netreplay.egg-info/dependency_links.txt +1 -0
  48. netreplay-0.1.0/netreplay.egg-info/entry_points.txt +2 -0
  49. netreplay-0.1.0/netreplay.egg-info/requires.txt +12 -0
  50. netreplay-0.1.0/netreplay.egg-info/top_level.txt +3 -0
  51. netreplay-0.1.0/pyproject.toml +42 -0
  52. netreplay-0.1.0/setup.cfg +4 -0
  53. netreplay-0.1.0/tests/test_api.py +106 -0
  54. netreplay-0.1.0/tests/test_flows.py +100 -0
  55. netreplay-0.1.0/tests/test_nrp.py +58 -0
  56. netreplay-0.1.0/tests/test_parser.py +171 -0
  57. netreplay-0.1.0/tests/test_pcap_import.py +57 -0
  58. netreplay-0.1.0/tests/test_pcap_import_decrypt.py +85 -0
  59. netreplay-0.1.0/tests/test_pipeline.py +129 -0
  60. netreplay-0.1.0/tests/test_storage.py +101 -0
  61. netreplay-0.1.0/tests/test_timeline.py +123 -0
  62. netreplay-0.1.0/tests/test_tls_decrypt.py +118 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NetReplay contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: netreplay
3
+ Version: 0.1.0
4
+ Summary: NetReplay - network traffic time machine. Capture, analyze and replay network traffic as a story.
5
+ Author: NetReplay
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/arbuztratil-design/NetReplay
8
+ Project-URL: Repository, https://github.com/arbuztratil-design/NetReplay
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: scapy>=2.5
13
+ Requires-Dist: cryptography>=42
14
+ Requires-Dist: fastapi>=0.110
15
+ Requires-Dist: uvicorn>=0.29
16
+ Requires-Dist: websockets>=12.0
17
+ Requires-Dist: typer>=0.12
18
+ Requires-Dist: flet>=0.86
19
+ Requires-Dist: httpx>=0.27
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=8.0; extra == "dev"
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # NetReplay
26
+
27
+ Машина времени для сетевого трафика. Захват → разбор → анализ потоков →
28
+ временная линия → «перемотка» событий обратно.
29
+
30
+ MVP: `netreplay capture` собирает трафик в файл `.nrp`, `netreplay timeline`
31
+ показывает его как читаемую историю, `netreplay replay` воспроизводит события
32
+ с реалистичными паузами, а `netreplay serve` + `netreplay gui` дают REST/WebSocket
33
+ API и оконный клиент.
34
+
35
+ ## Возможности
36
+
37
+ - Захват пакетов через Scapy (на Windows требуется Npcap).
38
+ - Разбор Ethernet / IPv4 / IPv6 / TCP / UDP / ARP / DNS / TLS: SNI, версии TLS,
39
+ DNS query/response.
40
+ - Офлайн-расшифровка TLS 1.2 (AES-128/256-GCM, PRF SHA-256/SHA-384) из
41
+ внешнего SSLKEYLOGFILE: расшифрованные прикладные данные попадают в таймлайн
42
+ как события `DECRYPT`.
43
+ - Потоки: нормализованный 5-tuple, TCP state machine
44
+ (SYN → SYN/ACK → ESTABLISHED → FIN → CLOSED, RST).
45
+ - Timeline-события: старт потока, переходы TCP, DNS, TLS, DECRYPT — с фильтрами
46
+ по времени, типам и потоку.
47
+ - Собственный формат `.nrp` — versioned SQLite (magic `NREP`, v1), WAL,
48
+ payload отдельными 64 КБ-чанками в `raw_blocks`.
49
+ - API: REST + WebSocket (live-события захвата), GUI как чистый клиент API.
50
+
51
+ ## Установка
52
+
53
+ ```powershell
54
+ pip install -e .
55
+ ```
56
+
57
+ На Windows для живого захвата установите [Npcap](https://npcap.com)
58
+ (с опцией «WinPcap API–compatible Mode»). Без него `netreplay capture`
59
+ завершится с понятной ошибкой.
60
+
61
+ ## Использование
62
+
63
+ ```powershell
64
+ # Список интерфейсов
65
+ netreplay interfaces
66
+
67
+ # Живой захват 10 секунд
68
+ netreplay capture -i "Ethernet" -o capture.nrp -d 10
69
+
70
+ # Метаданные, потоки и временная линия
71
+ netreplay inspect capture.nrp
72
+ netreplay flows capture.nrp
73
+ netreplay timeline capture.nrp
74
+ netreplay timeline capture.nrp --types TCP,TLS --limit 50
75
+
76
+ # Исторический реплей с ускорением x50
77
+ netreplay replay capture.nrp --speed 50
78
+
79
+ # Список сессий в workspace (переменная окружения NETREPLAY_WORKSPACE,
80
+ # по умолчанию ./netreplay_data)
81
+ netreplay sessions
82
+
83
+ # Импорт и анализ существующего PCAP/PCAPNG без захвата
84
+ netreplay import-pcap dump.pcap -o dump.nrp
85
+
86
+ # REST + WebSocket API и GUI
87
+ netreplay serve # http://127.0.0.1:8000, документация /docs
88
+ netreplay gui # в отдельном терминале; подключится к API
89
+ ```
90
+
91
+ Быстрая проверка без сети: в папке `examples/` лежит готовый `demo.nrp`
92
+ (11 пакетов: DNS, TCP handshake, TLS, завершение соединения).
93
+
94
+ ```powershell
95
+ netreplay timeline examples/demo.nrp
96
+ ```
97
+
98
+ Офлайн-импорт: `netreplay import-pcap` прогоняет `.pcap`/`.pcapng` через
99
+ тот же пайплайн (разбор → потоки → события → `.nrp`), что и живой захват.
100
+ Результат открывается любыми обычными командами: `flows`, `timeline`,
101
+ `replay`, `serve`/`gui`.
102
+
103
+ Расшифровка TLS в офлайн-режиме: если передан файл ключей SSLKEYLOGFILE
104
+ (строки `CLIENT_RANDOM`), TLS 1.2 соединения (AES-128/256-GCM, SHA-256/SHA-384)
105
+ расшифровываются и публикуются в таймлайн как события `DECRYPT` с
106
+ превью прикладных данных (например, начала HTTP-запроса/ответа). Такой
107
+ файл умеют отдавать curl, OpenSSL и браузеры через переменную окружения
108
+ `SSLKEYLOGFILE`.
109
+
110
+ ```powershell
111
+ netreplay import-pcap dump.pcap -o dump.nrp --keylog keys.log
112
+ ```
113
+
114
+ ## Архитектура
115
+
116
+ ```
117
+ netreplay/
118
+ core/ # вся логика, без наружных зависимостей
119
+ packets/parser.py # Scapy -> ParsedPacket
120
+ protocols/dns.py, tls.py
121
+ protocols/decrypt.py # TLS 1.2 AES-GCM расшифровка (keylog) + key-block PRF
122
+ protocols/decrypt_service.py # пост-проход по .nrp -> события DECRYPT
123
+ flows/tracker.py # нормализация 5-tuple, TCP state machine
124
+ storage/database.py # SQLite-хранилище сессий
125
+ storage/nrp.py # формат .nrp: magic, версия, чанки
126
+ timeline/service.py # события + timeline + replay
127
+ capture/scapy_backend.py
128
+ capture/pcap_backend.py # офлайн-источник: PCAP/PCAPNG -> CapturedPacket
129
+ service.py # CaptureController, NetReplayService, import_pcap
130
+ api/ # FastAPI: REST + WebSocket, схемы
131
+ cli/main.py # Typer-команды (используют Core)
132
+ gui/ # Flet-клиент, данные только через API
133
+ tests/ # pytest (Windows: + реальный TLS 1.2 handshake
134
+ # через OpenSSL DLL и проверка расшифровки)
135
+ ```
136
+
137
+ Правила разделения:
138
+ - GUI не трогает захват, хранилище и внутренние структуры — только API.
139
+ - CLI не дублирует логику Core (разбор, потоки, timeline) — только вызывает её.
140
+ - Core не зависит от FastAPI/Typer/Flet.
141
+ - `PcapBackend` реализует тот же `CaptureBackend`, что и `ScapyBackend`,
142
+ поэтому офлайн-импорт переиспользует пайплайн без изменений.
143
+
144
+ ## API
145
+
146
+ - `GET /sessions`, `GET /sessions/{id}`, `GET /sessions/{id}/timeline`,
147
+ `GET /sessions/{id}/flows`
148
+ - `GET /flows/{id}`, `GET /packets/{id}` (с `?raw=true` — hex payload)
149
+ - `POST /capture/start`, `POST /capture/stop`, `GET /capture/status`,
150
+ `GET /interfaces`
151
+ - `WS /ws` — пульс и live-события захвата:
152
+ `{"type":"event","timestamp":...,"flow_id":...,"protocol":"TCP","summary":"..."}`
153
+
154
+ ## Тестирование
155
+
156
+ ```powershell
157
+ python -m pytest tests -q
158
+ ```
159
+
160
+ ## Roadmap (после MVP)
161
+
162
+ - ~~Офлайн-анализ существующих PCAP без захвата~~ — `netreplay import-pcap`.
163
+ - ~~Отложенная расшифровка TLS (внешний кейлог-файл)~~ — `--keylog`.
164
+ - TLS 1.3, CBC/ChaCha20-сьюты, проверка Finished-сообщений.
165
+ - Перехват и обратная инъекция пакетов (replay-out).
166
+ - Векторы похожести/поиск по домену и IP в `inspect`.
167
+ - Модульный CLI-бэкенд (Mock/PCAP-файл) без изменения ядра.
@@ -0,0 +1,143 @@
1
+ # NetReplay
2
+
3
+ Машина времени для сетевого трафика. Захват → разбор → анализ потоков →
4
+ временная линия → «перемотка» событий обратно.
5
+
6
+ MVP: `netreplay capture` собирает трафик в файл `.nrp`, `netreplay timeline`
7
+ показывает его как читаемую историю, `netreplay replay` воспроизводит события
8
+ с реалистичными паузами, а `netreplay serve` + `netreplay gui` дают REST/WebSocket
9
+ API и оконный клиент.
10
+
11
+ ## Возможности
12
+
13
+ - Захват пакетов через Scapy (на Windows требуется Npcap).
14
+ - Разбор Ethernet / IPv4 / IPv6 / TCP / UDP / ARP / DNS / TLS: SNI, версии TLS,
15
+ DNS query/response.
16
+ - Офлайн-расшифровка TLS 1.2 (AES-128/256-GCM, PRF SHA-256/SHA-384) из
17
+ внешнего SSLKEYLOGFILE: расшифрованные прикладные данные попадают в таймлайн
18
+ как события `DECRYPT`.
19
+ - Потоки: нормализованный 5-tuple, TCP state machine
20
+ (SYN → SYN/ACK → ESTABLISHED → FIN → CLOSED, RST).
21
+ - Timeline-события: старт потока, переходы TCP, DNS, TLS, DECRYPT — с фильтрами
22
+ по времени, типам и потоку.
23
+ - Собственный формат `.nrp` — versioned SQLite (magic `NREP`, v1), WAL,
24
+ payload отдельными 64 КБ-чанками в `raw_blocks`.
25
+ - API: REST + WebSocket (live-события захвата), GUI как чистый клиент API.
26
+
27
+ ## Установка
28
+
29
+ ```powershell
30
+ pip install -e .
31
+ ```
32
+
33
+ На Windows для живого захвата установите [Npcap](https://npcap.com)
34
+ (с опцией «WinPcap API–compatible Mode»). Без него `netreplay capture`
35
+ завершится с понятной ошибкой.
36
+
37
+ ## Использование
38
+
39
+ ```powershell
40
+ # Список интерфейсов
41
+ netreplay interfaces
42
+
43
+ # Живой захват 10 секунд
44
+ netreplay capture -i "Ethernet" -o capture.nrp -d 10
45
+
46
+ # Метаданные, потоки и временная линия
47
+ netreplay inspect capture.nrp
48
+ netreplay flows capture.nrp
49
+ netreplay timeline capture.nrp
50
+ netreplay timeline capture.nrp --types TCP,TLS --limit 50
51
+
52
+ # Исторический реплей с ускорением x50
53
+ netreplay replay capture.nrp --speed 50
54
+
55
+ # Список сессий в workspace (переменная окружения NETREPLAY_WORKSPACE,
56
+ # по умолчанию ./netreplay_data)
57
+ netreplay sessions
58
+
59
+ # Импорт и анализ существующего PCAP/PCAPNG без захвата
60
+ netreplay import-pcap dump.pcap -o dump.nrp
61
+
62
+ # REST + WebSocket API и GUI
63
+ netreplay serve # http://127.0.0.1:8000, документация /docs
64
+ netreplay gui # в отдельном терминале; подключится к API
65
+ ```
66
+
67
+ Быстрая проверка без сети: в папке `examples/` лежит готовый `demo.nrp`
68
+ (11 пакетов: DNS, TCP handshake, TLS, завершение соединения).
69
+
70
+ ```powershell
71
+ netreplay timeline examples/demo.nrp
72
+ ```
73
+
74
+ Офлайн-импорт: `netreplay import-pcap` прогоняет `.pcap`/`.pcapng` через
75
+ тот же пайплайн (разбор → потоки → события → `.nrp`), что и живой захват.
76
+ Результат открывается любыми обычными командами: `flows`, `timeline`,
77
+ `replay`, `serve`/`gui`.
78
+
79
+ Расшифровка TLS в офлайн-режиме: если передан файл ключей SSLKEYLOGFILE
80
+ (строки `CLIENT_RANDOM`), TLS 1.2 соединения (AES-128/256-GCM, SHA-256/SHA-384)
81
+ расшифровываются и публикуются в таймлайн как события `DECRYPT` с
82
+ превью прикладных данных (например, начала HTTP-запроса/ответа). Такой
83
+ файл умеют отдавать curl, OpenSSL и браузеры через переменную окружения
84
+ `SSLKEYLOGFILE`.
85
+
86
+ ```powershell
87
+ netreplay import-pcap dump.pcap -o dump.nrp --keylog keys.log
88
+ ```
89
+
90
+ ## Архитектура
91
+
92
+ ```
93
+ netreplay/
94
+ core/ # вся логика, без наружных зависимостей
95
+ packets/parser.py # Scapy -> ParsedPacket
96
+ protocols/dns.py, tls.py
97
+ protocols/decrypt.py # TLS 1.2 AES-GCM расшифровка (keylog) + key-block PRF
98
+ protocols/decrypt_service.py # пост-проход по .nrp -> события DECRYPT
99
+ flows/tracker.py # нормализация 5-tuple, TCP state machine
100
+ storage/database.py # SQLite-хранилище сессий
101
+ storage/nrp.py # формат .nrp: magic, версия, чанки
102
+ timeline/service.py # события + timeline + replay
103
+ capture/scapy_backend.py
104
+ capture/pcap_backend.py # офлайн-источник: PCAP/PCAPNG -> CapturedPacket
105
+ service.py # CaptureController, NetReplayService, import_pcap
106
+ api/ # FastAPI: REST + WebSocket, схемы
107
+ cli/main.py # Typer-команды (используют Core)
108
+ gui/ # Flet-клиент, данные только через API
109
+ tests/ # pytest (Windows: + реальный TLS 1.2 handshake
110
+ # через OpenSSL DLL и проверка расшифровки)
111
+ ```
112
+
113
+ Правила разделения:
114
+ - GUI не трогает захват, хранилище и внутренние структуры — только API.
115
+ - CLI не дублирует логику Core (разбор, потоки, timeline) — только вызывает её.
116
+ - Core не зависит от FastAPI/Typer/Flet.
117
+ - `PcapBackend` реализует тот же `CaptureBackend`, что и `ScapyBackend`,
118
+ поэтому офлайн-импорт переиспользует пайплайн без изменений.
119
+
120
+ ## API
121
+
122
+ - `GET /sessions`, `GET /sessions/{id}`, `GET /sessions/{id}/timeline`,
123
+ `GET /sessions/{id}/flows`
124
+ - `GET /flows/{id}`, `GET /packets/{id}` (с `?raw=true` — hex payload)
125
+ - `POST /capture/start`, `POST /capture/stop`, `GET /capture/status`,
126
+ `GET /interfaces`
127
+ - `WS /ws` — пульс и live-события захвата:
128
+ `{"type":"event","timestamp":...,"flow_id":...,"protocol":"TCP","summary":"..."}`
129
+
130
+ ## Тестирование
131
+
132
+ ```powershell
133
+ python -m pytest tests -q
134
+ ```
135
+
136
+ ## Roadmap (после MVP)
137
+
138
+ - ~~Офлайн-анализ существующих PCAP без захвата~~ — `netreplay import-pcap`.
139
+ - ~~Отложенная расшифровка TLS (внешний кейлог-файл)~~ — `--keylog`.
140
+ - TLS 1.3, CBC/ChaCha20-сьюты, проверка Finished-сообщений.
141
+ - Перехват и обратная инъекция пакетов (replay-out).
142
+ - Векторы похожести/поиск по домену и IP в `inspect`.
143
+ - Модульный CLI-бэкенд (Mock/PCAP-файл) без изменения ядра.
@@ -0,0 +1,10 @@
1
+ """Flet GUI - the NetReplay desktop client.
2
+
3
+ The GUI is a pure API client: it does not parse packets, capture traffic or
4
+ touch the store. All data comes from the NetReplay REST API + WebSocket.
5
+ """
6
+ from gui.main import main
7
+
8
+ import netreplay # noqa: F401 (ensure package is on path)
9
+
10
+ __all__ = ["main"]
@@ -0,0 +1,81 @@
1
+ """Thin HTTP client for the NetReplay API."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+
9
+ class ApiError(Exception):
10
+ pass
11
+
12
+
13
+ class NetReplayClient:
14
+ def __init__(self, base_url: str):
15
+ self.base_url = base_url.rstrip("/")
16
+ self._client = httpx.Client(
17
+ timeout=httpx.Timeout(10.0, connect=3.0), trust_env=False
18
+ )
19
+
20
+ def _get(self, path: str, **params: Any) -> Any:
21
+ try:
22
+ resp = self._client.get(f"{self.base_url}{path}", params=params)
23
+ resp.raise_for_status()
24
+ return resp.json()
25
+ except httpx.HTTPError as exc:
26
+ raise ApiError(f"GET {path}: {exc}") from exc
27
+
28
+ def _post(self, path: str, body: Any | None = None) -> Any:
29
+ try:
30
+ resp = self._client.post(f"{self.base_url}{path}", json=body or {})
31
+ resp.raise_for_status()
32
+ return resp.json()
33
+ except httpx.HTTPError as exc:
34
+ raise ApiError(f"POST {path}: {exc}") from exc
35
+
36
+ def interfaces(self) -> list[dict]:
37
+ return self._get("/api/interfaces")
38
+
39
+ def sessions(self) -> list[dict]:
40
+ return self._get("/api/sessions")
41
+
42
+ def session(self, session_id: str) -> dict:
43
+ return self._get(f"/api/sessions/{session_id}")
44
+
45
+ def timeline(
46
+ self,
47
+ session_id: str,
48
+ start: float | None = None,
49
+ end: float | None = None,
50
+ flow_id: int | None = None,
51
+ limit: int = 2000,
52
+ ) -> list[dict]:
53
+ params = {}
54
+ if start is not None:
55
+ params["start"] = start
56
+ if end is not None:
57
+ params["end"] = end
58
+ if flow_id is not None:
59
+ params["flow_id"] = flow_id
60
+ params["limit"] = limit
61
+ return self._get(f"/api/sessions/{session_id}/timeline", **params)
62
+
63
+ def flows(self, session_id: str) -> list[dict]:
64
+ return self._get(f"/api/sessions/{session_id}/flows")
65
+
66
+ def flow(self, flow_id: int, include_packets: bool = True) -> dict:
67
+ return self._get(
68
+ "/api/flows/{}".format(flow_id), include_packets="true" if include_packets else "false"
69
+ )
70
+
71
+ def packet(self, packet_id: int, raw: bool = False) -> dict:
72
+ return self._get("/api/packets/{}".format(packet_id), raw="true" if raw else "false")
73
+
74
+ def capture_start(self, interface: str) -> dict:
75
+ return self._post("/api/capture/start", {"interface": interface})
76
+
77
+ def capture_stop(self) -> dict:
78
+ return self._post("/api/capture/stop")
79
+
80
+ def capture_status(self) -> dict:
81
+ return self._get("/api/capture/status")
@@ -0,0 +1 @@
1
+ """Reusable GUI components."""
@@ -0,0 +1,71 @@
1
+ """Event / flow details widget."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+
6
+ import flet as ft
7
+
8
+
9
+ def _fmt(ts: float) -> str:
10
+ local = time.localtime(ts)
11
+ return time.strftime("%H:%M:%S", local) + f".{int((ts % 1) * 1000):03d}"
12
+
13
+
14
+ class DetailsPanel:
15
+ def __init__(self):
16
+ self._header = ft.Text("Details", size=14, weight=ft.FontWeight.BOLD)
17
+ self._event = ft.Text("", size=12, color=ft.Colors.BLUE_200, selectable=True)
18
+ self._flow = ft.Text("", size=12, color=ft.Colors.TEAL_200, selectable=True)
19
+ self._body = ft.ListView(expand=True, spacing=2, padding=4)
20
+
21
+ def controls(self) -> list[ft.Control]:
22
+ return [self._header, self._event, self._flow, self._body]
23
+
24
+ def show_event(self, event: dict) -> None:
25
+ self._event.value = (
26
+ f"{_fmt(event['timestamp'])} {event['type']} {event['summary']}"
27
+ )
28
+ hint = "select a flow to see its packets" if not event.get("flow_id") else (
29
+ f"flow #{event['flow_id']} - select it in the list for packets"
30
+ )
31
+ self._flow.value = hint
32
+
33
+ def show_flow(self, flow: dict) -> None:
34
+ src = f"{flow['source']}:{flow['src_port']}" if flow["src_port"] else flow["source"]
35
+ dst = (
36
+ f"{flow['destination']}:{flow['dst_port']}"
37
+ if flow["dst_port"]
38
+ else flow["destination"]
39
+ )
40
+ lines = [
41
+ f"Flow #{flow['id']}",
42
+ f"{flow['protocol']} {src} -> {dst}",
43
+ f"state {flow['state']} start {_fmt(flow['start_ts'])} "
44
+ f"end {_fmt(flow['end_ts'])}",
45
+ f"packets {flow['packet_count']} bytes {flow['bytes']}",
46
+ "",
47
+ "packets:",
48
+ ]
49
+ self._flow.value = "\n".join(lines)
50
+ tiles: list[ft.Control] = []
51
+ for pkt in flow.get("packets", []):
52
+ src = f"{pkt['source']}:{pkt['src_port']}" if pkt["src_port"] else pkt["source"]
53
+ dst = (
54
+ f"{pkt['destination']}:{pkt['dst_port']}"
55
+ if pkt["dst_port"]
56
+ else pkt["destination"]
57
+ )
58
+ tiles.append(
59
+ ft.Text(
60
+ f"{_fmt(pkt['ts'])} #{pkt['id']} {pkt['protocol']} "
61
+ f"{src} -> {dst} len={pkt['length']}",
62
+ size=11,
63
+ font_family="monospace",
64
+ )
65
+ )
66
+ self._body.controls = tiles
67
+
68
+ def show_message(self, message: str) -> None:
69
+ self._event.value = message
70
+ self._flow.value = ""
71
+ self._body.controls = []
@@ -0,0 +1,44 @@
1
+ """Flow list widget."""
2
+ from __future__ import annotations
3
+
4
+ import flet as ft
5
+
6
+
7
+ class FlowList:
8
+ def __init__(self, on_select):
9
+ self._on_select = on_select
10
+ self._count = ft.Text("0 flows", size=11, color=ft.Colors.GREY_400)
11
+ self._list = ft.ListView(expand=True, spacing=2, padding=4, auto_scroll=False)
12
+
13
+ def controls(self) -> list[ft.Control]:
14
+ return [self._count, self._list]
15
+
16
+ def render(self, flows: list[dict]) -> None:
17
+ self._count.value = f"{len(flows)} flows"
18
+ tiles: list[ft.Control] = []
19
+ for flow in flows:
20
+ src = f"{flow['source']}:{flow['src_port']}" if flow["src_port"] else flow["source"]
21
+ dst = (
22
+ f"{flow['destination']}:{flow['dst_port']}"
23
+ if flow["dst_port"]
24
+ else flow["destination"]
25
+ )
26
+ pkts = flow["packet_count"]
27
+ size = flow["bytes"]
28
+ title = ft.Text(f"{src} -> {dst}", size=12, font_family="monospace")
29
+ subtitle = ft.Text(
30
+ f"{flow['protocol']} state={flow['state']} pkts={pkts} bytes={size}",
31
+ size=11,
32
+ color=ft.Colors.GREY_400,
33
+ )
34
+ flow_id = flow["id"]
35
+ tiles.append(
36
+ ft.ListTile(
37
+ title=title,
38
+ subtitle=subtitle,
39
+ dense=True,
40
+ on_click=lambda _e, fid=flow_id: self._on_select(fid),
41
+ )
42
+ )
43
+ tiles.append(ft.Divider(height=1, color=ft.Colors.GREY_800))
44
+ self._list.controls = tiles
@@ -0,0 +1,81 @@
1
+ """Header widget: capture controls, interface picker, session picker."""
2
+ from __future__ import annotations
3
+
4
+ import flet as ft
5
+
6
+
7
+ class Header:
8
+ def __init__(self, on_start, on_stop, on_open, on_refresh):
9
+ self._on_start = on_start
10
+ self._on_stop = on_stop
11
+ self._on_open = on_open
12
+ self._on_refresh = on_refresh
13
+
14
+ self.interface = ft.Dropdown(
15
+ width=420,
16
+ label="Interface",
17
+ options=[],
18
+ )
19
+ self.start_btn = ft.FilledButton("Start Capture", on_click=lambda _e: self._on_start())
20
+ self.stop_btn = ft.OutlinedButton("Stop", disabled=True, on_click=lambda _e: self._on_stop())
21
+ self.session = ft.Dropdown(width=220, label="Capture", options=[])
22
+ self.open_btn = ft.OutlinedButton("Open", on_click=lambda _e: self._on_open())
23
+ self.refresh_btn = ft.IconButton(ft.Icons.REFRESH, on_click=lambda _e: self._on_refresh())
24
+ self.status = ft.Text("", size=11, color=ft.Colors.GREY_400)
25
+ self.info = ft.Text("", size=11, color=ft.Colors.TEAL_200)
26
+ self.diag = ft.Text("", size=11, color=ft.Colors.GREY_400)
27
+
28
+ def controls(self) -> list[ft.Control]:
29
+ return [
30
+ ft.Row(
31
+ [
32
+ self.interface,
33
+ self.start_btn,
34
+ self.stop_btn,
35
+ ft.VerticalDivider(),
36
+ self.session,
37
+ self.open_btn,
38
+ self.refresh_btn,
39
+ ],
40
+ wrap=True,
41
+ ),
42
+ ft.Row([self.info, self.status, self.diag], wrap=True),
43
+ ]
44
+
45
+ def set_interfaces(self, interfaces: list[dict], selected: str | None = None) -> None:
46
+ opts = [
47
+ ft.DropdownOption(
48
+ key=i["name"],
49
+ text=f"{i['name']} [{i['description']}]" if i.get("description") else i["name"],
50
+ tooltip=i.get("description") or i["name"],
51
+ )
52
+ for i in interfaces
53
+ ]
54
+ current = self.interface.value
55
+ self.interface.options = opts
56
+ keep = selected or current
57
+ if keep and any(o.key == keep for o in opts):
58
+ self.interface.value = keep
59
+
60
+ def set_sessions(self, sessions: list[dict]) -> None:
61
+ opts = [
62
+ ft.DropdownOption(key=s["session_id"], text=f"{s['name']} ({s['packet_count']} pkts)")
63
+ for s in sessions
64
+ ]
65
+ current = self.session.value
66
+ self.session.options = opts
67
+ if current and any(o.key == current for o in opts):
68
+ self.session.value = current
69
+
70
+ def set_capture(self, running: bool, packets: int, flows: int, error: str | None) -> None:
71
+ self.start_btn.disabled = running
72
+ self.stop_btn.disabled = not running
73
+ if error:
74
+ self.status.value = f"capture error: {error}"
75
+ self.status.color = ft.Colors.RED_300
76
+ elif running:
77
+ self.status.value = f"capturing... packets={packets} flows={flows}"
78
+ self.status.color = ft.Colors.AMBER_200
79
+ else:
80
+ self.status.value = f"idle (packets={packets} flows={flows})"
81
+ self.status.color = ft.Colors.GREY_400