<?php
/**
 * Analyzer — Token parsing, taint analysis, scoring
 */
if (!defined('ABSPATH')) exit;

class Foxblock_Analyzer
{
    // ── Ngưỡng điểm để coi là mã độc ────────────────────────────────────────
    const THRESHOLD_HIGH   = 8;
    const THRESHOLD_MEDIUM = 5;
    const THRESHOLD_LOW    = 3;

    // ── Điểm cơ bản cho từng hàm nguy hiểm (sink) ───────────────────────────
    private static $sink_scores = array(
        'eval'                  => 5,
        'assert'                => 4,
        'system'                => 4,
        'exec'                  => 4,
        'shell_exec'            => 4,
        'passthru'              => 4,
        'popen'                 => 4,
        'proc_open'             => 4,
        'call_user_func'        => 3,
        'call_user_func_array'  => 3,
        'preg_replace'          => 2, // chỉ nguy hiểm khi có /e flag
        'create_function'       => 4,
        'include'               => 2,
        'include_once'          => 2,
        'require'               => 2,
        'require_once'          => 2,
        'file_put_contents'     => 2,
        'file_get_contents'     => 1,
        'fwrite'                => 2,
        'base64_decode'         => 2,
        'gzinflate'             => 2,
        'gzuncompress'          => 2,
        'str_rot13'             => 1,
    );

    // ── Nguồn taint (user input) ─────────────────────────────────────────────
    private static $taint_sources = array(
        '$_GET', '$_POST', '$_REQUEST', '$_COOKIE', '$_SERVER', '$_FILES',
        '$HTTP_GET_VARS', '$HTTP_POST_VARS',
    );

    // ── Heuristic patterns bổ sung ───────────────────────────────────────────
    private static $heuristic_patterns = array(
        array(
            'pattern' => '/eval\s*\(\s*base64_decode\s*\(/i',
            'score'   => 8,
            'label'   => 'eval(base64_decode(...)) — mẫu backdoor cổ điển',
            'risk'    => 'high',
        ),
        array(
            'pattern' => '/eval\s*\(\s*gzinflate\s*\(/i',
            'score'   => 8,
            'label'   => 'eval(gzinflate(...)) — mã độc nén nhiều lớp',
            'risk'    => 'high',
        ),
        array(
            'pattern' => '/eval\s*\(\s*str_rot13\s*\(/i',
            'score'   => 7,
            'label'   => 'eval(str_rot13(...)) — obfuscation đơn giản',
            'risk'    => 'high',
        ),
        array(
            'pattern' => '/preg_replace\s*\(\s*[\'"].*\/e/i',
            'score'   => 7,
            'label'   => 'preg_replace /e modifier — thực thi code tùy ý',
            'risk'    => 'high',
        ),
        array(
            'pattern' => '/(include|require)(_once)?\s*\(\s*[\'"]https?:\/\//i',
            'score'   => 7,
            'label'   => 'Include file từ URL remote',
            'risk'    => 'high',
        ),
        array(
            'pattern' => '/base64_decode\s*\(\s*[\'"][A-Za-z0-9+\/]{500,}/i',
            'score'   => 5,
            'label'   => 'base64_decode chuỗi rất dài (>500 ký tự)',
            'risk'    => 'high',
        ),
        array(
            'pattern' => '/(\bchr\s*\(\s*\d+\s*\)\s*\.?\s*){8,}/i',
            'score'   => 4,
            'label'   => 'Nối chuỗi chr() liên tiếp — obfuscation',
            'risk'    => 'medium',
        ),
        array(
            'pattern' => '/\\\\x[0-9a-fA-F]{2}(\\\\x[0-9a-fA-F]{2}){15,}/',
            'score'   => 4,
            'label'   => 'Chuỗi hex-encoded dài — obfuscation',
            'risk'    => 'medium',
        ),
        array(
            'pattern' => '/\$[a-zA-Z_]\w*\s*=\s*[\'"][a-zA-Z_]\w*[\'"]\s*;\s*\$\w+\s*\(\s*\$\w+/i',
            'score'   => 5,
            'label'   => 'Gán tên hàm vào biến rồi gọi động — variable function',
            'risk'    => 'high',
        ),
        array(
            'pattern' => '/file_put_contents\s*\(.*[\'"].*\.php[\'"]/i',
            'score'   => 5,
            'label'   => 'Ghi file .php — có thể tạo backdoor',
            'risk'    => 'high',
        ),
    );

    private string $rel_path;
    private string $source;
    private array  $findings = array();

    // Taint tracking: tên biến => true nếu bị taint
    private array $tainted_vars = array();

    public function __construct(string $rel_path, string $source)
    {
        $this->rel_path = $rel_path;
        $this->source   = $source;
    }

    /**
     * Chạy toàn bộ phân tích, trả về mảng findings
     */
    public function analyze(): array
    {
        $this->findings = array();

        // 1. Heuristic regex scan (nhanh, bắt mẫu rõ ràng)
        $this->run_heuristic_scan();

        // 2. Token-based taint analysis
        $this->run_token_analysis();

        // 3. Deobfuscation scan
        $this->run_deobfuscation_scan();

        // Gộp và dedup theo dòng
        return $this->deduplicate($this->findings);
    }

    // ── 1. Heuristic Scan ────────────────────────────────────────────────────

    private function run_heuristic_scan(): void
    {
        $lines = explode("\n", $this->source);
        foreach ($lines as $line_no => $line) {
            foreach (self::$heuristic_patterns as $rule) {
                if (preg_match($rule['pattern'], $line)) {
                    $this->add_finding(
                        $line_no + 1,
                        $rule['score'],
                        $rule['label'],
                        $rule['risk'],
                        trim($line),
                        'heuristic'
                    );
                }
            }
        }
    }

    // ── 2. Token Analysis + Taint Tracking ──────────────────────────────────

    private function run_token_analysis(): void
    {
        // Bỏ qua file quá lớn (>512KB) để tránh OOM
        if (strlen($this->source) > 524288) return;

        $tokens = @token_get_all($this->source);
        if (!is_array($tokens)) return;

        $count = count($tokens);
        $score = 0;
        $score_reasons = array();
        $current_line  = 1;

        for ($i = 0; $i < $count; $i++) {
            $token = $tokens[$i];

            // Cập nhật dòng hiện tại
            if (is_array($token)) {
                $current_line = $token[2];
            }

            // ── Phát hiện taint source ──────────────────────────────────────
            if (is_array($token) && $token[0] === T_VARIABLE) {
                $var_name = $token[1];

                // Kiểm tra gán từ superglobal: $x = $_POST[...]
                if (in_array($var_name, self::$taint_sources, true)) {
                    // Tìm biến nhận gán ở phía trái
                    $assigned_var = $this->find_assignment_target($tokens, $i);
                    if ($assigned_var) {
                        $this->tainted_vars[$assigned_var] = true;
                    }
                    $score += 3;
                    $score_reasons[] = "Sử dụng {$var_name} (user input)";
                }

                // Biến đã bị taint được gán tiếp cho biến khác
                if (isset($this->tainted_vars[$var_name])) {
                    $assigned_var = $this->find_assignment_target($tokens, $i);
                    if ($assigned_var) {
                        $this->tainted_vars[$assigned_var] = true;
                    }
                }
            }

            // ── Phát hiện sink calls ─────────────────────────────────────────
            if (is_array($token) && $token[0] === T_STRING) {
                $func_name = strtolower($token[1]);

                if (!isset(self::$sink_scores[$func_name])) continue;
                if (Foxblock_Whitelist::is_safe_context($func_name, $this->rel_path)) continue;

                // Lấy argument đầu tiên của hàm
                $first_arg = $this->get_first_argument($tokens, $i);
                $is_tainted = $this->is_tainted_arg($first_arg);
                $is_dynamic = $this->is_dynamic_arg($first_arg);

                $base_score = self::$sink_scores[$func_name];

                // Chỉ tính điểm cao nếu argument là dynamic hoặc tainted
                if ($is_tainted) {
                    $final_score = $base_score + 3;
                    $reason = "{$func_name}() với input người dùng (tainted)";
                    $risk = $base_score >= 4 ? 'high' : 'medium';
                } elseif ($is_dynamic) {
                    $final_score = $base_score + 1;
                    $reason = "{$func_name}() với giá trị động";
                    $risk = $base_score >= 4 ? 'high' : 'medium';
                } else {
                    // Hardcoded value — điểm thấp, ít nguy hiểm
                    $final_score = max(1, $base_score - 2);
                    $reason = "{$func_name}() với giá trị tĩnh";
                    $risk = 'low';
                }

                // eval/assert luôn đáng ngờ dù hardcoded
                if (in_array($func_name, array('eval', 'assert', 'create_function'), true)) {
                    $final_score = max($final_score, $base_score);
                    $risk = 'high';
                }

                if ($final_score >= self::THRESHOLD_LOW) {
                    $line_content = $this->get_line($current_line);
                    $this->add_finding($current_line, $final_score, $reason, $risk, $line_content, 'token');
                }
            }
        }
    }

    /**
     * Tìm biến đang được gán (phía trái của =)
     * Ví dụ: $x = $_POST['cmd'] → trả về '$x'
     */
    private function find_assignment_target(array $tokens, int $pos): ?string
    {
        // Tìm ngược lại từ vị trí hiện tại
        for ($j = $pos - 1; $j >= max(0, $pos - 5); $j--) {
            if (!is_array($tokens[$j])) {
                if ($tokens[$j] === '=') {
                    // Tìm biến trước dấu =
                    for ($k = $j - 1; $k >= max(0, $j - 3); $k--) {
                        if (is_array($tokens[$k]) && $tokens[$k][0] === T_VARIABLE) {
                            return $tokens[$k][1];
                        }
                        if (!is_array($tokens[$k]) || $tokens[$k][0] !== T_WHITESPACE) break;
                    }
                }
                break;
            }
        }
        return null;
    }

    /**
     * Lấy argument đầu tiên của function call
     */
    private function get_first_argument(array $tokens, int $func_pos): array
    {
        $count = count($tokens);
        $args  = array();

        // Tìm dấu (
        $start = -1;
        for ($i = $func_pos + 1; $i < min($count, $func_pos + 4); $i++) {
            if (!is_array($tokens[$i]) && $tokens[$i] === '(') {
                $start = $i;
                break;
            }
            if (is_array($tokens[$i]) && $tokens[$i][0] !== T_WHITESPACE) break;
        }
        if ($start === -1) return $args;

        // Thu thập tokens đến dấu ) hoặc ,
        $depth = 0;
        for ($i = $start + 1; $i < min($count, $start + 30); $i++) {
            $t = $tokens[$i];
            if (!is_array($t)) {
                if ($t === '(') { $depth++; }
                elseif ($t === ')') {
                    if ($depth === 0) break;
                    $depth--;
                }
                elseif ($t === ',' && $depth === 0) break;
            }
            $args[] = $t;
        }
        return $args;
    }

    /**
     * Kiểm tra argument có chứa tainted variable không
     */
    private function is_tainted_arg(array $arg_tokens): bool
    {
        foreach ($arg_tokens as $t) {
            if (is_array($t) && $t[0] === T_VARIABLE) {
                if (in_array($t[1], self::$taint_sources, true)) return true;
                if (isset($this->tainted_vars[$t[1]])) return true;
            }
        }
        return false;
    }

    /**
     * Kiểm tra argument có phải dynamic (biến, function call) không
     */
    private function is_dynamic_arg(array $arg_tokens): bool
    {
        foreach ($arg_tokens as $t) {
            if (is_array($t) && in_array($t[0], array(T_VARIABLE, T_STRING), true)) {
                return true;
            }
        }
        return false;
    }

    // ── 3. Deobfuscation Scan ────────────────────────────────────────────────

    private function run_deobfuscation_scan(): void
    {
        // Tìm tất cả chuỗi base64 dài trong source
        preg_match_all('/[\'"]([A-Za-z0-9+\/]{100,}={0,2})[\'"]/', $this->source, $matches, PREG_OFFSET_CAPTURE);

        foreach ($matches[1] as $idx => $match) {
            $b64_string = $match[0]; // [0] = string, [1] = offset
            $decoded = Foxblock_Deobfuscator::decode($b64_string);
            if ($decoded === null) continue;

            // Quét nội dung đã giải mã
            $sub_analyzer = new self('[decoded:' . $this->rel_path . ']', $decoded);
            $sub_findings = $sub_analyzer->analyze();

            if (!empty($sub_findings)) {
                // Tính dòng từ offset
                $offset = $matches[0][$idx][1];
                $line_no = substr_count(substr($this->source, 0, $offset), "\n") + 1;

                $this->add_finding(
                    $line_no,
                    7,
                    'Mã độc ẩn trong chuỗi base64 đã giải mã: ' . $sub_findings[0]['label'],
                    'high',
                    substr($b64_string, 0, 80) . '...',
                    'deobfuscated'
                );
            }
        }

        // Tìm chr() concat
        preg_match_all('/((?:chr\s*\(\s*\d+\s*\)\s*\.?\s*){6,})/i', $this->source, $chr_matches, PREG_OFFSET_CAPTURE);
        foreach ($chr_matches[1] as $idx => $chr_match) {
            $chr_expr = $chr_match[0]; // string
            $offset   = $chr_match[1]; // offset
            $decoded  = Foxblock_Deobfuscator::decode_chr_concat($chr_expr);
            if ($decoded && preg_match('/eval|exec|shell|base64|system/i', $decoded)) {
                $line_no = substr_count(substr($this->source, 0, $offset), "\n") + 1;
                $this->add_finding($line_no, 6, 'chr() concat giải mã thành: ' . substr($decoded, 0, 60), 'high', substr($chr_expr, 0, 80), 'deobfuscated');
            }
        }
    }

    // ── Helpers ──────────────────────────────────────────────────────────────

    private function add_finding(int $line, int $score, string $label, string $risk, string $code, string $method): void
    {
        $this->findings[] = array(
            'line'   => $line,
            'score'  => $score,
            'label'  => $label,
            'risk'   => $risk,
            'code'   => substr($code, 0, 200),
            'method' => $method,
        );
    }

    private function get_line(int $line_no): string
    {
        $lines = explode("\n", $this->source);
        return isset($lines[$line_no - 1]) ? trim($lines[$line_no - 1]) : '';
    }

    /**
     * Gộp các finding trùng dòng, giữ cái điểm cao nhất
     */
    private function deduplicate(array $findings): array
    {
        $seen = array();
        $result = array();
        foreach ($findings as $f) {
            $key = $f['line'] . '|' . $f['label'];
            if (!isset($seen[$key])) {
                $seen[$key] = true;
                $result[] = $f;
            }
        }
        // Sắp xếp theo score giảm dần
        usort($result, fn($a, $b) => $b['score'] - $a['score']);
        return $result;
    }
}
