Skip to content

Commit 2f1ff4c

Browse files
committed
Prepare 3.0.0 for testing
1 parent 6021d01 commit 2f1ff4c

File tree

305 files changed

+16569
-5997
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

305 files changed

+16569
-5997
lines changed

.github/copilot-instructions.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# T-Pot Attack Map - AI Coding Instructions
2+
3+
## Project Overview
4+
Real-time cyber attack visualization tool for the [T-Pot honeypot system](https://github.com/telekom-security/tpotce). Python backend processes honeypot events from Elasticsearch and streams to a Leaflet/D3.js frontend via WebSocket.
5+
6+
## Architecture & Data Flow
7+
**Producer-Consumer model decoupled by Redis Pub/Sub:**
8+
9+
1. **Data Source**: Elasticsearch with `logstash-*` indices (T-Pot honeypot events)
10+
2. **Producer** ([DataServer.py](../DataServer.py)):
11+
- Polls Elasticsearch every 0.5s for last 100 events
12+
- Queries stats every 10s (1m/1h/24h aggregations)
13+
- Maps ports → protocols via `port_to_type()` function
14+
- Publishes JSON to Redis channel `attack-map-production`
15+
- **Synchronous** (`redis.StrictRedis`, blocking operations)
16+
3. **Consumer & Web Server** ([AttackMapServer.py](../AttackMapServer.py)):
17+
- aiohttp server on port 1234 (configurable via `web_port`)
18+
- Subscribes to Redis channel, forwards to WebSocket clients
19+
- Serves static files from `static/` directory
20+
- **Fully async** (`redis.asyncio`, aiohttp, asyncio)
21+
4. **Frontend** ([static/map.js](../static/map.js), [dashboard.js](../static/dashboard.js)):
22+
- WebSocket client connects to `/websocket`
23+
- Leaflet.js map (CartoDB basemaps, dark/light themes)
24+
- D3.js v7 for animated attack lines and circles
25+
- IndexedDB cache with LocalStorage fallback (24h retention)
26+
27+
## Critical Developer Patterns
28+
29+
### Dual Configuration Toggle
30+
Connection strings are **hardcoded** with production/local variants:
31+
```python
32+
# Production (inside T-Pot Docker):
33+
# redis_url = 'redis://map_redis:6379'
34+
# Local development:
35+
redis_url = 'redis://127.0.0.1:6379'
36+
```
37+
**Rule**: Always preserve both configurations. Active config is uncommented, production config is commented out.
38+
39+
### Data: service_rgb Dictionary
40+
**Note**: Protocol-to-color mapping exists only in **DataServer.py**:
41+
- [DataServer.py](../DataServer.py#L30-L85) lines 30-85
42+
43+
**Adding a protocol**:
44+
1. Update `port_to_type()` in DataServer.py
45+
2. Add color to `service_rgb` in DataServer.py
46+
47+
### Async/Sync Boundary
48+
- **AttackMapServer.py**: 100% async (use `await`, `asyncio.create_task`)
49+
- **DataServer.py**: 100% sync (no async/await, uses `time.sleep()`)
50+
- Redis clients are different: `redis.asyncio` vs `redis.StrictRedis`
51+
52+
### Frontend Animation Management
53+
[map.js](../static/map.js) handles D3 animations with visibility checks:
54+
- Global `isPageVisible` flag prevents animation backlog when tab hidden
55+
- `isWakingUp` grace period (1s) suppresses burst on tab resume
56+
- D3 elements cleared on zoom to prevent coordinate desync
57+
- Use `d3.easeCircleIn` for consistent easing
58+
59+
## Data Schema
60+
61+
**WebSocket message types**:
62+
```javascript
63+
{type: "Traffic", ...} // Individual attack event
64+
{type: "Stats", ...} // Aggregate statistics (1m/1h/24h)
65+
```
66+
67+
**Attack event fields** (DataServer.py, lines 200-233):
68+
- `src_ip`, `src_lat`, `src_long`, `src_port`, `iso_code`
69+
- `dst_ip`, `dst_lat`, `dst_long`, `dst_port`, `dst_iso_code`
70+
- `protocol`, `color`, `honeypot`, `event_time`, `ip_rep`
71+
- `country`, `continent_code`, `tpot_hostname`
72+
73+
## Environment & Dependencies
74+
75+
**Python setup** (use virtual environment):
76+
```bash
77+
source bin/activate # Virtual env already exists
78+
pip install -r requirements.txt
79+
```
80+
81+
**Dependencies** ([requirements.txt](../requirements.txt)):
82+
- `aiohttp` (async web server)
83+
- `elasticsearch==8.18.1` (specific version!)
84+
- `redis` (includes asyncio support)
85+
- `pytz`, `tzlocal` (timezone handling)
86+
87+
**Frontend assets** (all local in `static/`):
88+
- Leaflet.js, D3.js v7, Bootstrap 5
89+
- Font Awesome, custom fonts (Inter, JetBrains Mono)
90+
- Flagpack icons in `static/flags/`
91+
92+
## Common Workflows
93+
94+
### Local Development Setup
95+
1. **Start Redis**: `redis-server` (port 6379)
96+
2. **Elasticsearch**: SSH tunnel or local instance on port 64298
97+
```bash
98+
ssh -L 64298:localhost:9200 tpot-server
99+
```
100+
3. **Data Server**: `python3 DataServer.py` (terminal 1)
101+
4. **Web Server**: `python3 AttackMapServer.py` (terminal 2)
102+
5. **Access**: http://localhost:1234
103+
104+
### Debugging Data Flow
105+
- **No attacks showing**: Check DataServer.py console for Elasticsearch errors
106+
- **WebSocket disconnects**: Check Redis connection in AttackMapServer.py
107+
- **Protocol showing as OTHER**: Add port mapping in `port_to_type()`
108+
109+
### Theme Development
110+
- HTML `data-theme` attribute toggles dark/light
111+
- Map tiles auto-switch via `mapLayers` object ([map.js](../static/map.js#L37-L51))
112+
- CSS custom properties defined in [index.css](../static/index.css)
113+
114+
## Performance Gotchas
115+
- **Elasticsearch query size**: Limited to 100 events per poll (DataServer.py line 189)
116+
- **Cache limits**: Max 10,000 events in IndexedDB ([dashboard.js](../static/dashboard.js#L14))
117+
- **Animation throttling**: Skip D3 animations when tab hidden (prevents memory bloat)
118+
- **Redis pubsub**: Single channel `attack-map-production`, all clients receive all events

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Created by venv; see https://docs.python.org/3/library/venv.html
2+
bin/
3+
include/
4+
lib/
5+
pyvenv.cfg
6+
__pycache__/
7+

AttackMapServer.py

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,16 @@
1313

1414
# Configuration
1515
# Within T-Pot: redis_url = 'redis://map_redis:6379'
16-
# redis_url = 'redis://127.0.0.1:6379'
17-
# web_port = 1234
16+
#redis_url = 'redis://127.0.0.1:6379'
17+
#web_port = 1234
1818
redis_url = 'redis://map_redis:6379'
1919
web_port = 64299
20-
version = 'Attack Map Server 2.2.6'
20+
version = 'Attack Map Server 3.0.0'
21+
2122

22-
# Color Codes for Attack Map
23-
service_rgb = {
24-
'FTP': '#ff0000',
25-
'SSH': '#ff8000',
26-
'TELNET': '#ffff00',
27-
'EMAIL': '#80ff00',
28-
'SQL': '#00ff00',
29-
'DNS': '#00ff80',
30-
'HTTP': '#00ffff',
31-
'HTTPS': '#0080ff',
32-
'VNC': '#0000ff',
33-
'SNMP': '#8000ff',
34-
'SMB': '#bf00ff',
35-
'MEDICAL': '#ff00ff',
36-
'RDP': '#ff0060',
37-
'SIP': '#ffccff',
38-
'ADB': '#ffcccc',
39-
'OTHER': '#ffffff'
40-
}
4123

4224
async def redis_subscriber(websockets):
25+
was_disconnected = False
4326
while True:
4427
try:
4528
# Create a Redis connection
@@ -49,22 +32,29 @@ async def redis_subscriber(websockets):
4932
# Subscribe to a Redis channel
5033
channel = "attack-map-production"
5134
await pubsub.subscribe(channel)
52-
print("[*] Redis connection established.")
35+
36+
# Print reconnection message if we were previously disconnected
37+
if was_disconnected:
38+
print("[*] Redis connection re-established")
39+
was_disconnected = False
40+
5341
# Start a loop to listen for messages on the channel
5442
while True:
5543
message = await pubsub.get_message(ignore_subscribe_messages=True)
5644
if message:
5745
try:
5846
# Only take the data and forward as JSON to the connected websocket clients
59-
json_data = json.dumps(json.loads(message['data']))
47+
# Decode bytes directly instead of load/dump cycle
48+
json_data = message['data'].decode('utf-8')
6049
# Process all connected websockets in parallel
61-
await asyncio.gather(*[ws.send_str(json_data) for ws in websockets])
50+
await asyncio.gather(*[ws.send_str(json_data) for ws in websockets], return_exceptions=True)
6251
except:
6352
print("Something went wrong while sending JSON data.")
6453
else:
6554
await asyncio.sleep(0.1)
6655
except redis.RedisError as e:
67-
print("[ ] Waiting for Redis ...")
56+
print(f"[ ] Connection lost to Redis ({type(e).__name__}), retrying...")
57+
was_disconnected = True
6858
await asyncio.sleep(5)
6959

7060
async def my_websocket_handler(request):
@@ -78,11 +68,11 @@ async def my_websocket_handler(request):
7868
elif msg.type == web.WSMsgType.ERROR:
7969
print(f'WebSocket connection closed with exception {ws.exception()}')
8070
request.app['websockets'].remove(ws)
81-
print(f"[ ] WebSocket connection closed. Clients active: {len(request.app['websockets'])}")
71+
print(f"[-] WebSocket connection closed. Clients active: {len(request.app['websockets'])}")
8272
return ws
8373

8474
async def my_index_handler(request):
85-
return web.FileResponse('index.html')
75+
return web.FileResponse('static/index.html')
8676

8777
async def start_background_tasks(app):
8878
app['websockets'] = []
@@ -92,6 +82,24 @@ async def cleanup_background_tasks(app):
9282
app['redis_subscriber'].cancel()
9383
await app['redis_subscriber']
9484

85+
async def check_redis_connection():
86+
"""Check Redis connection on startup and wait until available."""
87+
print("[*] Checking Redis connection...")
88+
waiting_printed = False
89+
90+
while True:
91+
try:
92+
r = redis.Redis.from_url(redis_url)
93+
await r.ping() # Simple connection test
94+
await r.aclose() # Clean up test connection
95+
print("[*] Redis connection established")
96+
return True
97+
except Exception as e:
98+
if not waiting_printed:
99+
print(f"[...] Waiting for Redis... (Error: {type(e).__name__})")
100+
waiting_printed = True
101+
await asyncio.sleep(5)
102+
95103
async def make_webapp():
96104
app = web.Application()
97105
app.add_routes([
@@ -107,4 +115,8 @@ async def make_webapp():
107115

108116
if __name__ == '__main__':
109117
print(version)
118+
# Check Redis connection on startup
119+
asyncio.run(check_redis_connection())
120+
print("[*] Starting web server...\n")
110121
web.run_app(make_webapp(), port=web_port)
122+

0 commit comments

Comments
 (0)