diff --git a/jtl-wafi-agent.py b/jtl-wafi-agent.py index 63a019c..ffd2fe6 100644 --- a/jtl-wafi-agent.py +++ b/jtl-wafi-agent.py @@ -37,7 +37,7 @@ from logging.handlers import RotatingFileHandler # ============================================================================= # VERSION # ============================================================================= -VERSION = "2.3.0" +VERSION = "2.4.0" # ============================================================================= # PFADE - AGENT @@ -1078,6 +1078,121 @@ if (rand(1, $cleanup_probability) === 1) {{ return; ''' +# ============================================================================= +# PHP TEMPLATES - BOT MONITOR-ONLY (No Blocking) +# ============================================================================= +BOT_MONITOR_TEMPLATE = ''' $expiry_date) return; + +$log_file = __DIR__ . '/{log_file}'; +$stats_file = __DIR__ . '/{ratelimit_dir}/monitor_stats.json'; +$stats_dir = __DIR__ . '/{ratelimit_dir}'; + +$visitor_ip = $_SERVER['REMOTE_ADDR'] ?? ''; +$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; + +// Ensure stats directory exists +if (!is_dir($stats_dir)) @mkdir($stats_dir, 0777, true); + +// === IP-in-CIDR Check Function === +function ip_in_cidr($ip, $cidr) {{ + if (strpos($cidr, '/') === false) return false; + list($subnet, $mask) = explode('/', $cidr); + $ip_long = ip2long($ip); + $subnet_long = ip2long($subnet); + if ($ip_long === false || $subnet_long === false) return false; + $mask_long = -1 << (32 - (int)$mask); + return ($ip_long & $mask_long) === ($subnet_long & $mask_long); +}} + +// === Bot IP Ranges (für getarnte Bots) === +$bot_ip_ranges = [ + {bot_ip_ranges} +]; + +// === Bot Detection Patterns (User-Agent) === +$bot_patterns = [ + {bot_patterns} +]; + +$generic_patterns = [{generic_patterns}]; + +// === STEP 0: IP-basierte Bot-Erkennung (höchste Priorität) === +$detected_bot = null; + +if (!empty($visitor_ip)) {{ + foreach ($bot_ip_ranges as $bot_name => $ip_ranges) {{ + foreach ($ip_ranges as $cidr) {{ + if (ip_in_cidr($visitor_ip, $cidr)) {{ + $detected_bot = $bot_name; + break 2; + }} + }} + }} +}} + +// === STEP 1: User-Agent-basierte Erkennung (falls IP nicht erkannt) === +if ($detected_bot === null && !empty($user_agent)) {{ + foreach ($bot_patterns as $bot_name => $pattern) {{ + if (preg_match($pattern, $user_agent)) {{ + $detected_bot = $bot_name; + break; + }} + }} + + if ($detected_bot === null) {{ + $ua_lower = strtolower($user_agent); + foreach ($generic_patterns as $pattern) {{ + if (strpos($ua_lower, $pattern) !== false) {{ + $detected_bot = "Bot ($pattern)"; + break; + }} + }} + }} +}} + +// Not a bot - allow through without logging +if ($detected_bot === null) return; + +// === STEP 2: Log the bot request (MONITOR ONLY - no blocking!) === +$timestamp = date('Y-m-d H:i:s'); +$uri = $_SERVER['REQUEST_URI'] ?? '/'; +@file_put_contents($log_file, "[$timestamp] MONITOR: $detected_bot | IP: $visitor_ip | URI: $uri\\n", FILE_APPEND | LOCK_EX); + +// === STEP 3: Update simple stats for dashboard === +$stats = []; +if (file_exists($stats_file)) {{ + $stats = @json_decode(@file_get_contents($stats_file), true) ?: []; +}} + +$minute_key = date('Y-m-d H:i'); +if (!isset($stats['by_minute'])) $stats['by_minute'] = []; +if (!isset($stats['by_minute'][$minute_key])) $stats['by_minute'][$minute_key] = []; +if (!isset($stats['by_minute'][$minute_key][$detected_bot])) $stats['by_minute'][$minute_key][$detected_bot] = 0; +$stats['by_minute'][$minute_key][$detected_bot]++; + +// Cleanup old minutes (keep last 60) +$minutes = array_keys($stats['by_minute']); +if (count($minutes) > 60) {{ + sort($minutes); + $to_remove = array_slice($minutes, 0, count($minutes) - 60); + foreach ($to_remove as $m) unset($stats['by_minute'][$m]); +}} + +@file_put_contents($stats_file, json_encode($stats), LOCK_EX); + +// === ALLOW REQUEST THROUGH - NO BLOCKING === +return; +''' + # ============================================================================= # CACHE VALIDATION FUNCTIONS @@ -1166,7 +1281,8 @@ else {{ echo "OK:" . count($data); }} # SHOP REGISTRY FUNCTIONS # ============================================================================= def add_shop_to_active(shop: str, mode: str = "geoip", geo_region: str = "dach", - rate_limit: int = None, ban_duration: int = None): + rate_limit: int = None, ban_duration: int = None, + bot_monitor_only: bool = False): """Registriert einen Shop als aktiv.""" os.makedirs(os.path.dirname(ACTIVE_SHOPS_FILE), exist_ok=True) shops = {} @@ -1187,6 +1303,8 @@ def add_shop_to_active(shop: str, mode: str = "geoip", geo_region: str = "dach", shop_data["rate_limit"] = rate_limit if ban_duration is not None: shop_data["ban_duration"] = ban_duration + if bot_monitor_only: + shop_data["bot_monitor_only"] = True shops[shop] = shop_data with open(ACTIVE_SHOPS_FILE, 'w') as f: @@ -1242,6 +1360,17 @@ def get_shop_rate_limit_config(shop: str): return None, None +def get_shop_monitor_mode(shop: str) -> bool: + """Gibt zurück ob ein Shop im Monitor-Only Modus ist.""" + if not os.path.isfile(ACTIVE_SHOPS_FILE): + return False + try: + with open(ACTIVE_SHOPS_FILE, 'r') as f: + return json.load(f).get(shop, {}).get("bot_monitor_only", False) + except: + return False + + def get_shop_activation_time(shop: str) -> Optional[datetime]: """Gibt die Aktivierungszeit eines Shops zurück.""" if not os.path.isfile(ACTIVE_SHOPS_FILE): @@ -1289,7 +1418,7 @@ def get_active_shops() -> List[str]: # ============================================================================= def activate_blocking(shop: str, silent: bool = True, mode: str = "geoip", geo_region: str = "dach", rate_limit: int = None, - ban_duration: int = None) -> bool: + ban_duration: int = None, bot_monitor_only: bool = False) -> bool: """ Aktiviert Blocking für einen Shop. @@ -1300,6 +1429,7 @@ def activate_blocking(shop: str, silent: bool = True, mode: str = "geoip", geo_region: "dach", "eurozone" oder "none" rate_limit: Requests pro Minute (nur bei bot-mode) ban_duration: Ban-Dauer in Sekunden (nur bei bot-mode) + bot_monitor_only: Nur Monitoring, kein Blocking (nur bei bot-mode) Returns: True wenn erfolgreich, False sonst @@ -1365,31 +1495,49 @@ def activate_blocking(shop: str, silent: bool = True, mode: str = "geoip", # Step 2: Blocking-Script erstellen if bot_mode: - # Rate-Limit Verzeichnisse erstellen - os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True) - os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True) + # Verzeichnisse erstellen (für beide Modi) + os.makedirs(ratelimit_path, mode=0o777, exist_ok=True) os.chmod(ratelimit_path, 0o777) - os.chmod(os.path.join(ratelimit_path, 'bans'), 0o777) - os.chmod(os.path.join(ratelimit_path, 'counts'), 0o777) set_owner(ratelimit_path, uid, gid, recursive=True) + + if bot_monitor_only: + # Monitor-Only Modus: Nur Logging, kein Blocking + geoip_content = BOT_MONITOR_TEMPLATE.format( + expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'), + expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'), + log_file=SHOP_LOG_FILE, + ratelimit_dir=RATELIMIT_DIR, + bot_patterns=generate_php_bot_patterns(), + generic_patterns=generate_php_generic_patterns(), + bot_ip_ranges=generate_php_bot_ip_ranges() + ) + if not silent: + logger.info(f"🔍 Monitor-Only Modus für {shop}") + else: + # Rate-Limit Modus: Bans/Counts Verzeichnisse erstellen + os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True) + os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True) + os.chmod(os.path.join(ratelimit_path, 'bans'), 0o777) + os.chmod(os.path.join(ratelimit_path, 'counts'), 0o777) + set_owner(ratelimit_path, uid, gid, recursive=True) - if rate_limit is None: - rate_limit = DEFAULT_RATE_LIMIT - if ban_duration is None: - ban_duration = DEFAULT_BAN_DURATION * 60 + if rate_limit is None: + rate_limit = DEFAULT_RATE_LIMIT + if ban_duration is None: + ban_duration = DEFAULT_BAN_DURATION * 60 - geoip_content = BOT_SCRIPT_TEMPLATE.format( - expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'), - expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'), - log_file=SHOP_LOG_FILE, - ratelimit_dir=RATELIMIT_DIR, - bot_patterns=generate_php_bot_patterns(), - generic_patterns=generate_php_generic_patterns(), - bot_ip_ranges=generate_php_bot_ip_ranges(), - rate_limit=rate_limit, - ban_duration=ban_duration, - ban_duration_min=ban_duration // 60 - ) + geoip_content = BOT_SCRIPT_TEMPLATE.format( + expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'), + expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'), + log_file=SHOP_LOG_FILE, + ratelimit_dir=RATELIMIT_DIR, + bot_patterns=generate_php_bot_patterns(), + generic_patterns=generate_php_generic_patterns(), + bot_ip_ranges=generate_php_bot_ip_ranges(), + rate_limit=rate_limit, + ban_duration=ban_duration, + ban_duration_min=ban_duration // 60 + ) else: countries_array = generate_php_countries_array(geo_region) geoip_content = GEOIP_SCRIPT_TEMPLATE.format( @@ -1415,7 +1563,7 @@ def activate_blocking(shop: str, silent: bool = True, mode: str = "geoip", # Step 4: Registrieren if bot_mode: - add_shop_to_active(shop, mode, geo_region, rate_limit, ban_duration) + add_shop_to_active(shop, mode, geo_region, rate_limit, ban_duration, bot_monitor_only) else: add_shop_to_active(shop, mode, geo_region) @@ -1538,6 +1686,12 @@ def get_shop_log_stats(shop: str) -> Dict[str, Any]: detected_bot = line.split('BOT: ')[1].split(' |')[0].strip() except: pass + elif 'MONITOR: ' in line: + # Monitor-Only Mode - gleiche Statistik wie BOT: + try: + detected_bot = line.split('MONITOR: ')[1].split(' |')[0].strip() + except: + pass elif 'BLOCKED (banned): ' in line: try: detected_bot = line.split('BLOCKED (banned): ')[1].split(' |')[0].strip() @@ -1873,11 +2027,13 @@ class JTLWAFiAgent: shop_geo = get_shop_geo_region(shop) rate_limit, ban_duration = get_shop_rate_limit_config(shop) activation_time = get_shop_activation_time(shop) + monitor_only = get_shop_monitor_mode(shop) shop_data["mode"] = shop_mode shop_data["geo_region"] = shop_geo shop_data["rate_limit"] = rate_limit shop_data["ban_duration"] = ban_duration + shop_data["bot_monitor_only"] = monitor_only if activation_time: shop_data["activated"] = activation_time.isoformat() @@ -2028,10 +2184,14 @@ class JTLWAFiAgent: geo_region = data.get('geo_region', 'dach') rate_limit = data.get('rate_limit') ban_duration = data.get('ban_duration') + bot_monitor_only = data.get('bot_monitor_only', False) # Korrektes Logging je nach Modus if mode == 'bot': - logger.info(f"Aktiviere {shop} (mode=bot, rate_limit={rate_limit}/min, ban={ban_duration}s)") + if bot_monitor_only: + logger.info(f"Aktiviere {shop} (mode=bot, MONITOR-ONLY)") + else: + logger.info(f"Aktiviere {shop} (mode=bot, rate_limit={rate_limit}/min, ban={ban_duration}s)") else: logger.info(f"Aktiviere {shop} (mode=geoip, region={geo_region})") @@ -2042,14 +2202,16 @@ class JTLWAFiAgent: mode=mode, geo_region=geo_region, rate_limit=rate_limit, - ban_duration=ban_duration + ban_duration=ban_duration, + bot_monitor_only=bot_monitor_only ) if success: + mode_desc = 'monitor' if bot_monitor_only else mode await self._send_event('command.result', { 'command_id': command_id, 'status': 'success', - 'message': f'Shop {shop} aktiviert ({mode})', + 'message': f'Shop {shop} aktiviert ({mode_desc})', 'shop': shop }) # Full Update senden