jtl-wafi-agent.py aktualisiert

This commit is contained in:
2025-12-22 13:48:39 +01:00
parent 7a3f7de19a
commit fd9dc5d79c

View File

@@ -37,7 +37,7 @@ from logging.handlers import RotatingFileHandler
# ============================================================================= # =============================================================================
# VERSION # VERSION
# ============================================================================= # =============================================================================
VERSION = "2.3.0" VERSION = "2.4.0"
# ============================================================================= # =============================================================================
# PFADE - AGENT # PFADE - AGENT
@@ -1078,6 +1078,121 @@ if (rand(1, $cleanup_probability) === 1) {{
return; return;
''' '''
# =============================================================================
# PHP TEMPLATES - BOT MONITOR-ONLY (No Blocking)
# =============================================================================
BOT_MONITOR_TEMPLATE = '''<?php
/**
* Bot Monitor-Only Script - Logging without Blocking
* Valid until: {expiry_date}
* Detects and logs bots/crawlers WITHOUT rate-limiting or blocking
* All requests are ALLOWED through - monitoring only!
*/
$expiry_date = strtotime('{expiry_timestamp}');
if (time() > $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 # CACHE VALIDATION FUNCTIONS
@@ -1166,7 +1281,8 @@ else {{ echo "OK:" . count($data); }}
# SHOP REGISTRY FUNCTIONS # SHOP REGISTRY FUNCTIONS
# ============================================================================= # =============================================================================
def add_shop_to_active(shop: str, mode: str = "geoip", geo_region: str = "dach", 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.""" """Registriert einen Shop als aktiv."""
os.makedirs(os.path.dirname(ACTIVE_SHOPS_FILE), exist_ok=True) os.makedirs(os.path.dirname(ACTIVE_SHOPS_FILE), exist_ok=True)
shops = {} 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 shop_data["rate_limit"] = rate_limit
if ban_duration is not None: if ban_duration is not None:
shop_data["ban_duration"] = ban_duration shop_data["ban_duration"] = ban_duration
if bot_monitor_only:
shop_data["bot_monitor_only"] = True
shops[shop] = shop_data shops[shop] = shop_data
with open(ACTIVE_SHOPS_FILE, 'w') as f: with open(ACTIVE_SHOPS_FILE, 'w') as f:
@@ -1242,6 +1360,17 @@ def get_shop_rate_limit_config(shop: str):
return None, None 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]: def get_shop_activation_time(shop: str) -> Optional[datetime]:
"""Gibt die Aktivierungszeit eines Shops zurück.""" """Gibt die Aktivierungszeit eines Shops zurück."""
if not os.path.isfile(ACTIVE_SHOPS_FILE): 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", def activate_blocking(shop: str, silent: bool = True, mode: str = "geoip",
geo_region: str = "dach", rate_limit: int = None, 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. 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" geo_region: "dach", "eurozone" oder "none"
rate_limit: Requests pro Minute (nur bei bot-mode) rate_limit: Requests pro Minute (nur bei bot-mode)
ban_duration: Ban-Dauer in Sekunden (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: Returns:
True wenn erfolgreich, False sonst 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 # Step 2: Blocking-Script erstellen
if bot_mode: if bot_mode:
# Rate-Limit Verzeichnisse erstellen # Verzeichnisse erstellen (für beide Modi)
os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True) os.makedirs(ratelimit_path, mode=0o777, exist_ok=True)
os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True)
os.chmod(ratelimit_path, 0o777) 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) 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: if rate_limit is None:
rate_limit = DEFAULT_RATE_LIMIT rate_limit = DEFAULT_RATE_LIMIT
if ban_duration is None: if ban_duration is None:
ban_duration = DEFAULT_BAN_DURATION * 60 ban_duration = DEFAULT_BAN_DURATION * 60
geoip_content = BOT_SCRIPT_TEMPLATE.format( geoip_content = BOT_SCRIPT_TEMPLATE.format(
expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'), expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'),
expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'), expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'),
log_file=SHOP_LOG_FILE, log_file=SHOP_LOG_FILE,
ratelimit_dir=RATELIMIT_DIR, ratelimit_dir=RATELIMIT_DIR,
bot_patterns=generate_php_bot_patterns(), bot_patterns=generate_php_bot_patterns(),
generic_patterns=generate_php_generic_patterns(), generic_patterns=generate_php_generic_patterns(),
bot_ip_ranges=generate_php_bot_ip_ranges(), bot_ip_ranges=generate_php_bot_ip_ranges(),
rate_limit=rate_limit, rate_limit=rate_limit,
ban_duration=ban_duration, ban_duration=ban_duration,
ban_duration_min=ban_duration // 60 ban_duration_min=ban_duration // 60
) )
else: else:
countries_array = generate_php_countries_array(geo_region) countries_array = generate_php_countries_array(geo_region)
geoip_content = GEOIP_SCRIPT_TEMPLATE.format( geoip_content = GEOIP_SCRIPT_TEMPLATE.format(
@@ -1415,7 +1563,7 @@ def activate_blocking(shop: str, silent: bool = True, mode: str = "geoip",
# Step 4: Registrieren # Step 4: Registrieren
if bot_mode: 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: else:
add_shop_to_active(shop, mode, geo_region) 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() detected_bot = line.split('BOT: ')[1].split(' |')[0].strip()
except: except:
pass 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: elif 'BLOCKED (banned): ' in line:
try: try:
detected_bot = line.split('BLOCKED (banned): ')[1].split(' |')[0].strip() detected_bot = line.split('BLOCKED (banned): ')[1].split(' |')[0].strip()
@@ -1873,11 +2027,13 @@ class JTLWAFiAgent:
shop_geo = get_shop_geo_region(shop) shop_geo = get_shop_geo_region(shop)
rate_limit, ban_duration = get_shop_rate_limit_config(shop) rate_limit, ban_duration = get_shop_rate_limit_config(shop)
activation_time = get_shop_activation_time(shop) activation_time = get_shop_activation_time(shop)
monitor_only = get_shop_monitor_mode(shop)
shop_data["mode"] = shop_mode shop_data["mode"] = shop_mode
shop_data["geo_region"] = shop_geo shop_data["geo_region"] = shop_geo
shop_data["rate_limit"] = rate_limit shop_data["rate_limit"] = rate_limit
shop_data["ban_duration"] = ban_duration shop_data["ban_duration"] = ban_duration
shop_data["bot_monitor_only"] = monitor_only
if activation_time: if activation_time:
shop_data["activated"] = activation_time.isoformat() shop_data["activated"] = activation_time.isoformat()
@@ -2028,10 +2184,14 @@ class JTLWAFiAgent:
geo_region = data.get('geo_region', 'dach') geo_region = data.get('geo_region', 'dach')
rate_limit = data.get('rate_limit') rate_limit = data.get('rate_limit')
ban_duration = data.get('ban_duration') ban_duration = data.get('ban_duration')
bot_monitor_only = data.get('bot_monitor_only', False)
# Korrektes Logging je nach Modus # Korrektes Logging je nach Modus
if mode == 'bot': 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: else:
logger.info(f"Aktiviere {shop} (mode=geoip, region={geo_region})") logger.info(f"Aktiviere {shop} (mode=geoip, region={geo_region})")
@@ -2042,14 +2202,16 @@ class JTLWAFiAgent:
mode=mode, mode=mode,
geo_region=geo_region, geo_region=geo_region,
rate_limit=rate_limit, rate_limit=rate_limit,
ban_duration=ban_duration ban_duration=ban_duration,
bot_monitor_only=bot_monitor_only
) )
if success: if success:
mode_desc = 'monitor' if bot_monitor_only else mode
await self._send_event('command.result', { await self._send_event('command.result', {
'command_id': command_id, 'command_id': command_id,
'status': 'success', 'status': 'success',
'message': f'Shop {shop} aktiviert ({mode})', 'message': f'Shop {shop} aktiviert ({mode_desc})',
'shop': shop 'shop': shop
}) })
# Full Update senden # Full Update senden