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 = "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 = '''<?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
@@ -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