<?php
/**
 * Semantics of "default" state for Coraza rules.
 *
 * Rule state in the WHP UI can be: enforce / detect / off / (default).
 * "(default)" means: no DB override is set, the rule behaves per the
 * baked-in image config + the global WAF mode. THIS file resolves what
 * that actually means for a given rule, so the UI can show
 * "(default → enforce)" / "(default → score)" / etc.
 *
 * Source of truth for the baked enforce list:
 *   haproxy-manager-base/coraza-spoa/overrides.conf
 *   (also docker-cp'd at deploy time into the running coraza-spoa)
 * Kept in sync via the day-one-enforce list documented in
 *   /root/whp/scripts/apply-coraza-waf-mode.sh
 */

if (!function_exists('coraza_global_mode')) {
    function coraza_global_mode(): string {
        $path = '/docker/whp/settings.json';
        if (!file_exists($path)) {
            return 'off';
        }
        $data = json_decode((string)file_get_contents($path), true);
        $mode = $data['coraza_waf']['mode'] ?? 'off';
        return in_array($mode, ['off', 'detect_only', 'enforce'], true) ? $mode : 'off';
    }
}

if (!function_exists('coraza_default_state')) {
    /**
     * Resolve what "(default)" means for a given rule under the current
     * global mode.
     *
     * @return array {label, class, reason}
     *   label  = 'off' | 'detect' | 'score' | 'enforce'
     *   class  = Bootstrap badge class
     *   reason = one-line tooltip text
     */
    function coraza_default_state(int $rule_id, ?string $mode = null): array {
        $mode = $mode ?? coraza_global_mode();

        if ($mode === 'off') {
            return [
                'label'  => 'off',
                'class'  => 'bg-secondary',
                'reason' => 'WAF is globally disabled (settings → Security → coraza_waf.mode=off).',
            ];
        }
        if ($mode === 'detect_only') {
            return [
                'label'  => 'detect',
                'class'  => 'bg-info text-dark',
                'reason' => 'Global mode is detect-only — all rules log to audit.log, no requests block.',
            ];
        }

        // Coraza engine-built-in rules (id range 200000-209999) block per
        // their own disruptive action; they do NOT go through the anomaly
        // scorer. Example: 200002 "Failed to parse request body" denies.
        if ($rule_id >= 200000 && $rule_id < 210000) {
            return [
                'label'  => 'enforce',
                'class'  => 'bg-danger',
                'reason' => 'Coraza engine-built-in rule — blocks on match independently of anomaly score.',
            ];
        }

        // Anomaly-score aggregators. 949110 is the inbound gatekeeper;
        // 980130 is the outbound. These are the rules that actually issue
        // the deny when accumulated score crosses the threshold.
        if ($rule_id === 949110 || $rule_id === 980130) {
            return [
                'label'  => 'enforce',
                'class'  => 'bg-danger',
                'reason' => 'Anomaly-score aggregator — blocks when score crosses threshold (default 5 inbound).',
            ];
        }

        // Day-one promoted list (mirrored from baked overrides.conf inside
        // the coraza-spoa image). These get an explicit individual block
        // even when they would otherwise just contribute to score.
        $promo_ranges = [
            [920440, 920440],   // backup-file disclosure
            [930120, 930120],   // LFI OS files
            [930130, 930130],   // .env disclosure
            [932100, 932160],   // RCE Unix command injection
            [933170, 933200],   // PHP webshell/eval
            [944100, 944300],   // Log4Shell
        ];
        foreach ($promo_ranges as [$lo, $hi]) {
            if ($rule_id >= $lo && $rule_id <= $hi) {
                return [
                    'label'  => 'enforce',
                    'class'  => 'bg-danger',
                    'reason' => 'Day-one enforce promotion (baked overrides.conf) — blocks on match.',
                ];
            }
        }

        // Anything else under enforce mode: typical CRS detection rule
        // that adds to the anomaly score. Request blocks only if 949110
        // sees a cumulative score over threshold (so 1 rule alone usually
        // doesn't block; multiple matches do).
        return [
            'label'  => 'score',
            'class'  => 'bg-warning text-dark',
            'reason' => 'Contributes to anomaly score; request blocks only when 949110 sees the total score cross threshold.',
        ];
    }
}

if (!function_exists('coraza_rule_origin')) {
    /**
     * Classify the rule's origin from its ID range. Used in the rule-detail
     * modal to label whether a rule is CRS, engine-built-in, generated
     * exception, etc.
     */
    function coraza_rule_origin(int $rule_id): string {
        if ($rule_id >= 200000 && $rule_id < 210000) return 'Coraza engine built-in';
        if ($rule_id >= 990000000) return 'WHP generated host-exception';
        if ($rule_id >= 900000 && $rule_id < 1000000) return 'OWASP CRS';
        return 'Custom override or unknown';
    }
}
