diff --git a/jtl-wafi-agent.py b/jtl-wafi-agent.py
index 65faa43..d57a668 100644
--- a/jtl-wafi-agent.py
+++ b/jtl-wafi-agent.py
@@ -1331,6 +1331,10 @@ class LiveStatsTracker:
self.blocked_paths: Dict[str, List[float]] = {} # Path -> [blocked timestamps]
self.ip_404s: Dict[str, List[float]] = {} # IP -> [404 timestamps]
+ # IP Request Details (für IP-Detail Popup)
+ self.ip_request_details: Dict[str, List[Dict]] = {} # IP -> [{ts, path, ua}, ...]
+ self.max_request_details = 100 # Max Anzahl Details pro IP
+
# Human/Bot Counters
self.human_requests: List[float] = []
self.bot_requests: List[float] = []
@@ -1353,7 +1357,13 @@ class LiveStatsTracker:
self.human_requests = [t for t in self.human_requests if t > cutoff]
self.bot_requests = [t for t in self.bot_requests if t > cutoff]
- def record_request(self, ip: str, path: str, is_bot: bool, is_blocked: bool = False, is_404: bool = False):
+ # Request Details cleanup
+ for ip in list(self.ip_request_details.keys()):
+ self.ip_request_details[ip] = [r for r in self.ip_request_details[ip] if r['ts'] > cutoff]
+ if not self.ip_request_details[ip]:
+ del self.ip_request_details[ip]
+
+ def record_request(self, ip: str, path: str, is_bot: bool, is_blocked: bool = False, is_404: bool = False, user_agent: str = ''):
"""Zeichnet einen Request auf."""
now = time.time()
@@ -1389,6 +1399,30 @@ class LiveStatsTracker:
if ip not in self.ip_info:
self.ip_info[ip] = get_ip_info(ip)
+ # Request Details speichern (für IP-Detail Popup)
+ if ip not in self.ip_request_details:
+ self.ip_request_details[ip] = []
+ self.ip_request_details[ip].append({
+ 'ts': now,
+ 'path': path,
+ 'ua': user_agent,
+ 'blocked': is_blocked
+ })
+ # Limitieren auf max_request_details
+ if len(self.ip_request_details[ip]) > self.max_request_details:
+ self.ip_request_details[ip] = self.ip_request_details[ip][-self.max_request_details:]
+
+ def get_ip_requests(self, ip: str, limit: int = 0) -> List[Dict]:
+ """Gibt die Requests einer IP zurück. limit=0 für alle."""
+ self.cleanup_old_data()
+ if ip not in self.ip_request_details:
+ return []
+ # Neueste zuerst
+ all_requests = list(reversed(self.ip_request_details[ip]))
+ if limit > 0:
+ return all_requests[:limit]
+ return all_requests
+
def get_top_ips(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Gibt Top IPs mit Zusatzinfos zurück."""
self.cleanup_old_data()
@@ -3858,6 +3892,13 @@ class JTLWAFiAgent:
if 'Path: ' in line:
path = line.split('Path: ')[1].split(' |')[0].strip()
+ # User-Agent extrahieren
+ user_agent = ''
+ if 'UA: ' in line:
+ user_agent = line.split('UA: ')[1].split(' |')[0].strip()
+ elif 'User-Agent: ' in line:
+ user_agent = line.split('User-Agent: ')[1].split(' |')[0].strip()
+
# Bot/Human erkennen
is_bot = any(x in line for x in ['BOT: ', 'BOT:', 'BLOCKED_BOT:', 'MONITOR_BOT:', 'BANNED_BOT:'])
@@ -3868,7 +3909,7 @@ class JTLWAFiAgent:
is_404 = '404' in line
if ip:
- tracker.record_request(ip, path, is_bot, is_blocked, is_404)
+ tracker.record_request(ip, path, is_bot, is_blocked, is_404, user_agent)
except Exception as e:
logger.debug(f"LiveStats record error: {e}")
@@ -4490,9 +4531,10 @@ class JTLWAFiAgent:
})
async def _handle_whois_command(self, data: Dict[str, Any]):
- """Führt WHOIS-Lookup für eine IP durch und sendet Ergebnis."""
+ """Führt WHOIS-Lookup für eine IP durch und sendet Ergebnis mit Request-Historie."""
command_id = data.get('command_id', 'unknown')
ip = data.get('ip')
+ shop = data.get('shop') # Optional: für Request-Historie
if not ip:
await self._send_event('whois_result', {
@@ -4506,7 +4548,23 @@ class JTLWAFiAgent:
# WHOIS-Lookup durchführen (wird gecacht)
result = whois_lookup(ip)
- # Ergebnis senden (ohne raw für kleinere Payload)
+ # Request-Historie für diese IP sammeln (aus allen Shops wenn kein Shop angegeben)
+ requests = []
+ if shop:
+ tracker = get_shop_stats_tracker(shop)
+ requests = tracker.get_ip_requests(ip) # Alle Requests
+ else:
+ # Aus allen aktiven Shops sammeln
+ for s in list(_shop_stats_trackers.keys()):
+ tracker = get_shop_stats_tracker(s)
+ shop_requests = tracker.get_ip_requests(ip) # Alle Requests
+ for r in shop_requests:
+ r['shop'] = s
+ requests.extend(shop_requests)
+ # Nach Zeit sortieren (neueste zuerst)
+ requests.sort(key=lambda x: x['ts'], reverse=True)
+
+ # Ergebnis senden
await self._send_event('whois_result', {
'command_id': command_id,
'success': True,
@@ -4516,10 +4574,11 @@ class JTLWAFiAgent:
'asn': result.get('asn', ''),
'country': result.get('country', ''),
'abuse': result.get('abuse', ''),
- 'range': result.get('range', '')
+ 'range': result.get('range', ''),
+ 'requests': requests
})
- logger.info(f"WHOIS für {ip}: {result.get('org', 'Unknown')} ({result.get('asn', 'N/A')})")
+ logger.info(f"WHOIS für {ip}: {result.get('org', 'Unknown')} ({result.get('asn', 'N/A')}) - {len(requests)} Requests")
except Exception as e:
logger.error(f"WHOIS Fehler für {ip}: {e}")
diff --git a/jtl-wafi-dashboard.py b/jtl-wafi-dashboard.py
index f578941..a55bb20 100644
--- a/jtl-wafi-dashboard.py
+++ b/jtl-wafi-dashboard.py
@@ -1778,11 +1778,12 @@ def get_dashboard_html() -> str:
function renderDetailTopIps(ips){if(!ips||!ips.length)return '
Keine Daten
';return ips.map(ip=>''+ip.ip+' '+getCountryName(ip.country)+' | '+(ip.org||ip.asn||'-')+'
'+ip.count+'x 🚫 ✓
').join('');}
function detailQuickBan(ip){if(!currentDetailShop)return;const duration=prompt('Ban-Dauer auswählen:\\n\\n900 = 15 Minuten\\n3600 = 1 Stunde\\n21600 = 6 Stunden\\n86400 = 24 Stunden\\n604800 = 7 Tage\\n-1 = Permanent','3600');if(duration===null)return;ws.send(JSON.stringify({type:'command.ban',data:{shop:currentDetailShop,ip:ip,duration:parseInt(duration),reason:'Manual ban from dashboard'}}));toast('IP '+ip+' wird gebannt...','success');}
function detailQuickWhitelist(ip){if(!currentDetailShop)return;const desc=prompt('Beschreibung (optional):','');if(desc===null)return;ws.send(JSON.stringify({type:'command.whitelist',data:{shop:currentDetailShop,ip:ip,description:desc}}));toast('IP '+ip+' wird gewhitelisted...','success');}
- let currentWhoisIp=null;
- function showIpDetail(ip){currentWhoisIp=ip;document.getElementById('ipDetailContent').innerHTML='Lade WHOIS-Daten für '+ip+'...
';document.getElementById('ipDetailModal').classList.add('open');ws.send(JSON.stringify({type:'command.whois',data:{ip:ip,command_id:'whois_'+Date.now()}}));}
- function renderWhoisResult(data){if(!data.success){document.getElementById('ipDetailContent').innerHTML='Fehler: '+(data.error||'Unbekannter Fehler')+'
';return;}const ip=data.ip||currentWhoisIp;const html='IP-Adresse '+ip+' Organisation '+(data.org||'-')+' Netname '+(data.netname||'-')+' ASN '+(data.asn||'-')+' Land '+(data.country?getCountryName(data.country)+' ('+data.country+')':'-')+' IP-Range '+(data.range||'-')+' Abuse-Kontakt '+(data.abuse?''+data.abuse+' ':'-')+'
'+(currentDetailShop?'
🚫 Bannen ✓ Whitelist ':'')+'
🔗 WHOIS.com ';document.getElementById('ipDetailContent').innerHTML=html;}
- function ipDetailBan(ip){if(!currentDetailShop){toast('Kein Shop ausgewählt','warning');return;}const duration=prompt('Ban-Dauer auswählen:\\n\\n900 = 15 Minuten\\n3600 = 1 Stunde\\n21600 = 6 Stunden\\n86400 = 24 Stunden\\n604800 = 7 Tage\\n-1 = Permanent','3600');if(duration===null)return;ws.send(JSON.stringify({type:'command.ban',data:{shop:currentDetailShop,ip:ip,duration:parseInt(duration),reason:'Manual ban from IP detail'}}));toast('IP '+ip+' wird gebannt...','success');closeModal('ipDetailModal');}
- function ipDetailWhitelist(ip){if(!currentDetailShop){toast('Kein Shop ausgewählt','warning');return;}const desc=prompt('Beschreibung (optional):','');if(desc===null)return;ws.send(JSON.stringify({type:'command.whitelist',data:{shop:currentDetailShop,ip:ip,description:desc}}));toast('IP '+ip+' wird gewhitelisted...','success');closeModal('ipDetailModal');}
+ let currentWhoisIp=null,currentWhoisData=null,showAllRequests=false;
+ function showIpDetail(ip){currentWhoisIp=ip;currentWhoisData=null;showAllRequests=false;document.getElementById('ipDetailContent').innerHTML='Lade WHOIS-Daten für '+ip+'...
';document.getElementById('ipDetailModal').classList.add('open');const shop=currentDetailShop||currentLogsShop||'';ws.send(JSON.stringify({type:'command.whois',data:{ip:ip,shop:shop,command_id:'whois_'+Date.now()}}));}
+ function renderWhoisResult(data){if(!data.success){document.getElementById('ipDetailContent').innerHTML='Fehler: '+(data.error||'Unbekannter Fehler')+'
';return;}currentWhoisData=data;renderWhoisContent();}
+ function renderWhoisContent(){const data=currentWhoisData;if(!data)return;const ip=data.ip||currentWhoisIp;const allReqs=data.requests||[];const initialLimit=50;const showLimit=showAllRequests?allReqs.length:Math.min(initialLimit,allReqs.length);const reqs=allReqs.slice(0,showLimit);let reqsHtml='';if(allReqs.length>0){reqsHtml='📋 Requests ('+showLimit+'/'+allReqs.length+') '+(allReqs.length>initialLimit&&!showAllRequests?'Alle '+allReqs.length+' anzeigen ':'')+'
';reqs.forEach(r=>{const t=new Date(r.ts*1000).toLocaleTimeString('de-DE');const blocked=r.blocked?'
[BLOCKED] ':'';reqsHtml+='
'+t+' '+blocked+'
'+r.path+'
'+(r.ua?'
'+r.ua+'
':'')+'
';});reqsHtml+='
';}else{reqsHtml='📋 Keine aktuellen Requests gefunden
';}const html='IP-Adresse '+ip+' Organisation '+(data.org||'-')+' Netname '+(data.netname||'-')+' ASN '+(data.asn||'-')+' Land '+(data.country?getCountryName(data.country)+' ('+data.country+')':'-')+' IP-Range '+(data.range||'-')+' Abuse-Kontakt '+(data.abuse?''+data.abuse+' ':'-')+'
'+(currentDetailShop||currentLogsShop?'
🚫 Bannen ✓ Whitelist ':'')+'
🔗 WHOIS.com '+reqsHtml;document.getElementById('ipDetailContent').innerHTML=html;}
+ function ipDetailBan(ip){const shop=currentDetailShop||currentLogsShop;if(!shop){toast('Kein Shop ausgewählt','warning');return;}const duration=prompt('Ban-Dauer auswählen:\\n\\n900 = 15 Minuten\\n3600 = 1 Stunde\\n21600 = 6 Stunden\\n86400 = 24 Stunden\\n604800 = 7 Tage\\n-1 = Permanent','3600');if(duration===null)return;ws.send(JSON.stringify({type:'command.ban',data:{shop:shop,ip:ip,duration:parseInt(duration),reason:'Manual ban from IP detail'}}));toast('IP '+ip+' wird gebannt...','success');closeModal('ipDetailModal');}
+ function ipDetailWhitelist(ip){const shop=currentDetailShop||currentLogsShop;if(!shop){toast('Kein Shop ausgewählt','warning');return;}const desc=prompt('Beschreibung (optional):','');if(desc===null)return;ws.send(JSON.stringify({type:'command.whitelist',data:{shop:shop,ip:ip,description:desc}}));toast('IP '+ip+' wird gewhitelisted...','success');closeModal('ipDetailModal');}
function quickBan(ip){if(!currentLogsShop)return;const duration=prompt('Ban-Dauer auswählen:\\n\\n900 = 15 Minuten\\n3600 = 1 Stunde\\n21600 = 6 Stunden\\n86400 = 24 Stunden\\n604800 = 7 Tage\\n-1 = Permanent','3600');if(duration===null)return;ws.send(JSON.stringify({type:'command.ban',data:{shop:currentLogsShop,ip:ip,duration:parseInt(duration),reason:'Manual ban from dashboard'}}));toast('IP '+ip+' wird gebannt...','success');}
function quickWhitelist(ip){if(!currentLogsShop)return;const desc=prompt('Beschreibung (optional):','');if(desc===null)return;ws.send(JSON.stringify({type:'command.whitelist',data:{shop:currentLogsShop,ip:ip,description:desc}}));toast('IP '+ip+' wird gewhitelisted...','success');}
function addLogEntry(line){const c=document.getElementById('logsContent');if(c.querySelector('div[style*="color:#666"]'))c.innerHTML='';const e=document.createElement('div');e.className='log-entry'+(line.includes('BANNED')?' banned':'');const ipRegex=/\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\b/g;e.innerHTML=line.replace(//g,'>').replace(ipRegex,'$1 ');c.insertBefore(e,c.firstChild);while(c.children.length>100)c.removeChild(c.lastChild);}