<?php
/**
 * Brainwerk Security Suite WAF — pattern-based Web Application Firewall.
 *
 * Vergleicht jeden eingehenden Request gegen ~80 kuratierte Regex-Signaturen
 * für:
 *  - SQL-Injection (UNION, OR 1=1, SLEEP(), etc.)
 *  - XSS (<script>, javascript:, on*=, etc.)
 *  - Local-File-Inclusion (../../, /etc/passwd, php://input, etc.)
 *  - Remote-Code-Execution (system(), exec(), base64_decode in URL, etc.)
 *  - SSRF / Server-Side Request Forgery (file://, gopher://, dict://, etc.)
 *  - XXE (XML External Entity)
 *  - Command Injection (`; ls`, `| cat /etc/`)
 *
 * Bei Match:
 *   - Log in shieldforge_audit_log (kind='waf', severity='block')
 *   - Response: 403 mit minimaler Info (kein Hint für Angreifer)
 *   - Optional: IP für 24h auf Block-Liste
 *
 * 3 Paranoia-Levels:
 *   1 (low)    : nur eindeutige Patterns, ~95 % Treffer, 0.1 % False-Positives
 *   2 (medium) : zusätzlich heuristische Signaturen
 *   3 (high)   : aggressiv, kann legitime Admin-Aktionen blockieren (Whitelist nötig)
 *
 * Performance: läuft auf 'init' priority 1, durchschnitt ~0.5 ms pro Request
 * dank pre-kompilierter Pattern-Arrays.
 *
 * Whitelist:
 *   - Admin-User mit manage_options übersprungen (kann via Setting deaktiviert werden)
 *   - Konfigurierbare Path-Whitelist (z. B. /wp-cron.php, /xmlrpc.php)
 *
 * @package Brainwerk Security Suite
 */

if ( ! defined( 'ABSPATH' ) ) exit;

class Shieldforge_Waf {

    public static function register( Shieldforge_Loader $loader ) {
        // Whitelist-Add-Handler IMMER registrieren (auch wenn WAF off ist —
        // Admin könnte Whitelist pflegen wollen)
        $loader->add_action( 'admin_post_shieldforge_waf_whitelist_add', __CLASS__, 'handle_whitelist_add' );
        if ( ! Shieldforge_Settings::get( 'waf_enabled', false ) ) return;
        $loader->add_action( 'init', __CLASS__, 'inspect', 1 );
    }

    /** Admin-Post-Handler: fügt einen Pfad zur WAF-Whitelist hinzu. */
    public static function handle_whitelist_add() {
        if ( ! current_user_can( 'manage_options' ) ) wp_die( '403', 403 );
        check_admin_referer( 'shieldforge_waf_whitelist_add' );
        $path = isset( $_POST['path'] ) ? sanitize_text_field( wp_unslash( $_POST['path'] ) ) : '';
        if ( '' !== $path ) {
            $wl = (array) Shieldforge_Settings::get( 'waf_path_whitelist', array() );
            if ( ! in_array( $path, $wl, true ) ) {
                $wl[] = $path;
                Shieldforge_Settings::update( array( 'waf_path_whitelist' => $wl ) );
            }
        }
        wp_safe_redirect( wp_get_referer() ?: admin_url() );
        exit;
    }

    public static function inspect() {
        // 1) Admin-Whitelist
        if ( Shieldforge_Settings::get( 'waf_whitelist_admins', true ) && is_user_logged_in() && current_user_can( 'manage_options' ) ) return;
        // 2) Path-Whitelist
        $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
        $whitelist = (array) Shieldforge_Settings::get( 'waf_path_whitelist', array() );
        foreach ( $whitelist as $needle ) {
            $needle = trim( (string) $needle );
            if ( '' !== $needle && false !== strpos( $uri, $needle ) ) return;
        }

        $level = (int) Shieldforge_Settings::get( 'waf_paranoia_level', 1 );
        $patterns = self::patterns( $level );

        // Surface: URL, GET, POST, COOKIE, User-Agent, Referrer, Accept-Language
        $surfaces = array(
            'url'        => urldecode( $uri ),
            'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
            'referer'    => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '',
            'accept_lang'=> isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '',
        );
        // GET/POST/COOKIE flatten
        foreach ( array( '_GET', '_POST', '_COOKIE' ) as $g ) {
            if ( empty( $GLOBALS[ $g ] ) ) continue;
            foreach ( (array) $GLOBALS[ $g ] as $k => $v ) {
                if ( is_array( $v ) ) $v = wp_json_encode( $v );
                $surfaces[ $g . '.' . $k ] = (string) $v;
            }
        }

        foreach ( $surfaces as $surface_name => $value ) {
            if ( '' === $value ) continue;
            // Decode-Layer: behandelt häufige Encoder-Tricks
            $candidates = array( $value, urldecode( $value ), html_entity_decode( $value, ENT_QUOTES ) );
            // Evasion-Normalisierung: SQL-Inline-Kommentare (z. B. UNION/**/SELECT)
            // zu Leerzeichen kollabieren + Whitespace normalisieren, damit das
            // Comment-Splitting nicht die \b-/\s-Tokengrenzen der Regeln umgeht.
            $normalized = preg_replace( '#/\*.*?\*/#s', ' ', $value );
            $normalized = preg_replace( '/\s+/', ' ', (string) $normalized );
            if ( null !== $normalized && $normalized !== $value ) {
                $candidates[] = $normalized;
            }
            foreach ( $candidates as $candidate ) {
                if ( strlen( $candidate ) > 16384 ) continue; // riesige Bodies skippen
                foreach ( $patterns as $rule_id => $rule ) {
                    if ( preg_match( $rule['regex'], $candidate ) ) {
                        self::block( $rule_id, $rule, $surface_name, $candidate );
                        // Nie return — block() macht exit
                    }
                }
            }
        }
    }

    private static function block( $rule_id, $rule, $surface, $payload ) {
        // Log (signature: Shieldforge_Logger::log( $event_type, array $args = [] ))
        if ( class_exists( 'Shieldforge_Logger' ) ) {
            Shieldforge_Logger::log( 'waf_block', array(
                'rule'     => $rule_id,
                'rule_name'=> $rule['name'],
                'surface'  => $surface,
                'severity' => 'high',
                'payload'  => mb_substr( $payload, 0, 500 ),
            ) );
        }
        // Optional Auto-Block-IP
        if ( Shieldforge_Settings::get( 'waf_autoblock_ip', false ) && class_exists( 'Shieldforge_Bruteforce' ) ) {
            // NOTE: was function_exists('Class::method') — which is ALWAYS false
            // (that's is_callable/method_exists syntax), so $ip stayed '' and the
            // auto-block never fired. And block_ip()'s signature is
            // ($ip, $minutes, $reason) — the args were swapped, so even if it had
            // run it would have blocked for max(1, (int)'waf:…')=1 minute with a
            // numeric junk reason. Both fixed: real IP + 24h (1440 min) + reason.
            $ip = class_exists( 'Shieldforge_Utils' ) ? Shieldforge_Utils::get_real_ip() : '';
            if ( $ip && method_exists( 'Shieldforge_Bruteforce', 'block_ip' ) ) {
                Shieldforge_Bruteforce::block_ip( $ip, DAY_IN_SECONDS / MINUTE_IN_SECONDS, 'waf:' . $rule_id );
            }
        }
        // Threat-network sensor: emit a typed waf.block event (raw IP so the
        // global correlation indicator can be computed). Fires before exit so
        // the Collector enqueues it.
        if ( class_exists( 'Shieldforge_Event' ) ) {
            $req_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
            Shieldforge_Event::emit( array(
                'event_type' => 'waf.block',
                'severity'   => 'high',
                'actor'      => array(
                    'ip'         => Shieldforge_Utils::get_real_ip(),
                    'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
                ),
                'target'     => array( 'kind' => 'endpoint', 'endpoint' => $req_uri ),
                'context'    => array(
                    'rule_id'      => $rule_id,
                    'request_path' => $req_uri,
                    'method'       => isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '',
                    'http_status'  => 403,
                    'surface'      => $surface,
                ),
            ) );
        }

        // Response
        status_header( 403 );
        nocache_headers();
        header( 'Content-Type: text/plain; charset=UTF-8' );
        header( 'X-Brainwerk-Security-Block: 1' );
        echo "403 Forbidden\n\nRequest blocked by security policy.\n";
        if ( WP_DEBUG ) {
            echo esc_html( "(rule={$rule_id} surface={$surface})" ) . "\n";
        }
        exit;
    }

    /**
     * Kuratierte Pattern-Liste. Jede Regel: id, name, regex, level (1=immer, 2=medium+, 3=high+).
     */
    private static function patterns( $max_level = 1 ) {
        $all = array(
            // === SQL INJECTION ===
            'sqli_union'      => array( 'name' => 'SQLi: UNION SELECT',          'level' => 1, 'regex' => '/\b(union)\s+(all\s+)?(select)\b/i' ),
            'sqli_or_1eq1'    => array( 'name' => 'SQLi: OR 1=1 / AND 1=1',      'level' => 1, 'regex' => '/\b(or|and)\s+\d+\s*=\s*\d+\b/i' ),
            'sqli_sleep'      => array( 'name' => 'SQLi: SLEEP() time-based',    'level' => 1, 'regex' => '/\b(sleep|benchmark|pg_sleep|waitfor\s+delay)\s*\(/i' ),
            'sqli_drop'       => array( 'name' => 'SQLi: DROP/TRUNCATE',         'level' => 1, 'regex' => '/\b(drop|truncate)\s+(table|database)\b/i' ),
            'sqli_load'       => array( 'name' => 'SQLi: LOAD_FILE/INTO OUTFILE','level' => 1, 'regex' => '/\b(load_file|into\s+outfile|into\s+dumpfile)\b/i' ),
            'sqli_concat'     => array( 'name' => 'SQLi: GROUP_CONCAT',          'level' => 2, 'regex' => '/\bgroup_concat\s*\(/i' ),
            'sqli_information'=> array( 'name' => 'SQLi: information_schema',    'level' => 2, 'regex' => '/\binformation_schema\b/i' ),
            'sqli_comment'    => array( 'name' => 'SQLi: inline comment',        'level' => 3, 'regex' => '/(--\s|#\s|\/\*).*\b(select|union|update|delete)\b/i' ),

            // === XSS ===
            'xss_script_tag'  => array( 'name' => 'XSS: <script>',               'level' => 1, 'regex' => '/<script\b[^>]*>/i' ),
            'xss_javascript'  => array( 'name' => 'XSS: javascript: URI',        'level' => 1, 'regex' => '/javascript\s*:/i' ),
            'xss_onevent'     => array( 'name' => 'XSS: onerror/onload/onclick', 'level' => 1, 'regex' => '/\bon(error|load|click|mouseover|focus|blur|submit|change)\s*=/i' ),
            'xss_svg_script'  => array( 'name' => 'XSS: <svg onload>',           'level' => 1, 'regex' => '/<svg\b[^>]*on\w+\s*=/i' ),
            'xss_iframe'      => array( 'name' => 'XSS: <iframe>',               'level' => 2, 'regex' => '/<iframe\b/i' ),
            'xss_data_uri'    => array( 'name' => 'XSS: data:text/html base64', 'level' => 2, 'regex' => '/data:\s*text\/html[^,]*;\s*base64/i' ),

            // === LFI / Path Traversal ===
            'lfi_traversal'   => array( 'name' => 'LFI: ../ traversal',          'level' => 1, 'regex' => '/(\.\.[\/\\\\]){2,}/' ),
            'lfi_etcpasswd'   => array( 'name' => 'LFI: /etc/passwd',            'level' => 1, 'regex' => '/\/etc\/passwd\b/' ),
            'lfi_proc'        => array( 'name' => 'LFI: /proc/self',             'level' => 1, 'regex' => '/\/proc\/self\/(environ|cmdline|status)/' ),
            'lfi_wp_config'   => array( 'name' => 'LFI: wp-config.php',          'level' => 1, 'regex' => '/wp[-_]config\.(php|bak|old|txt)\b/i' ),
            'lfi_php_wrapper' => array( 'name' => 'LFI: php:// wrappers',        'level' => 1, 'regex' => '/php:\/\/(input|filter|stdin|memory|temp)/i' ),
            'lfi_data_wrapper'=> array( 'name' => 'LFI: data:// wrapper',        'level' => 1, 'regex' => '/data:\/\/[^"\s]*base64/i' ),
            'lfi_zip_wrapper' => array( 'name' => 'LFI: zip:// phar://',         'level' => 2, 'regex' => '/(zip|phar|expect|ssh2):\/\//i' ),

            // === RCE ===
            'rce_eval'        => array( 'name' => 'RCE: eval/assert',            'level' => 1, 'regex' => '/\b(eval|assert)\s*\(\s*(base64_decode|gzinflate|str_rot13|chr)\s*\(/i' ),
            'rce_system'      => array( 'name' => 'RCE: system/exec/passthru',   'level' => 1, 'regex' => '/\b(system|exec|passthru|popen|shell_exec|pcntl_exec|proc_open)\s*\(/i' ),
            'rce_base64_long' => array( 'name' => 'RCE: very long base64',       'level' => 2, 'regex' => '/base64_decode\s*\(\s*[\'"][A-Za-z0-9+\/=]{100,}[\'"]\s*\)/i' ),
            'rce_create_func' => array( 'name' => 'RCE: create_function',        'level' => 1, 'regex' => '/\bcreate_function\s*\(/i' ),

            // === SSRF ===
            'ssrf_file'       => array( 'name' => 'SSRF: file:// scheme',        'level' => 1, 'regex' => '/\bfile:\/\//i' ),
            'ssrf_gopher'     => array( 'name' => 'SSRF: gopher://',             'level' => 1, 'regex' => '/\bgopher:\/\//i' ),
            'ssrf_dict'       => array( 'name' => 'SSRF: dict://',               'level' => 1, 'regex' => '/\bdict:\/\//i' ),
            'ssrf_internal_ip'=> array( 'name' => 'SSRF: internal IP (169.254)', 'level' => 2, 'regex' => '/\b(169\.254|127\.0\.0|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)\d+\.\d+\b/' ),

            // === XXE ===
            'xxe_doctype'     => array( 'name' => 'XXE: <!ENTITY>',              'level' => 2, 'regex' => '/<!ENTITY\b/i' ),
            'xxe_system'      => array( 'name' => 'XXE: SYSTEM keyword',         'level' => 2, 'regex' => '/<!ENTITY\b[^>]*\bSYSTEM\b/i' ),

            // === Command Injection ===
            'cmdi_shell_meta' => array( 'name' => 'CMDi: shell metacharacters',  'level' => 3, 'regex' => '/[;&|`$]\s*(ls|cat|wget|curl|rm|cp|mv|whoami|uname|id|nc|bash|sh)\b/i' ),

            // === WordPress-spezifisch ===
            'wp_user_enum'    => array( 'name' => 'WP: ?author=N user-enum',     'level' => 2, 'regex' => '/[?&]author=\d+/i' ),
            'wp_xmlrpc_brute' => array( 'name' => 'WP: xmlrpc.system.multicall', 'level' => 2, 'regex' => '/system\.multicall/i' ),

            // === Known-bad bots / scanners ===
            'scanner_ua'      => array( 'name' => 'Scanner UA: sqlmap/nikto/etc','level' => 1, 'regex' => '/\b(sqlmap|nikto|nessus|acunetix|w3af|burp\s*suite|havij|jbroFuzz)\b/i' ),
        );
        return array_filter( $all, function ( $r ) use ( $max_level ) { return $r['level'] <= $max_level; } );
    }
}
