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?'':'')+'🔗 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?'':'')+'
';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?'':'')+'🔗 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);}