<?php
/**
 * Malware Scanner
 *
 * Scans files for:
 *  - Suspicious PHP function calls (eval, base64_decode, shell_exec …)
 *  - Obfuscated / encoded payloads
 *  - Webshell patterns
 *  - Suspicious PHP-in-uploads
 *
 * Files that exceed the threat threshold are quarantined:
 *  - Moved to GS_QUARANTINE with a manifest file.
 *  - A stub file is left in place (so the application doesn't crash).
 *
 * @package GuardianShield
 */

namespace GuardianShield;

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

class Scanner {

	/** Score at which a file is flagged as malware. */
	private const THREAT_THRESHOLD = 8;

	/** Max file size to scan (bytes). Larger files skipped to avoid memory issues. */
	private const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB

	/** Patterns scored by severity. Each entry: [regex, score, label] */
	private const MALWARE_PATTERNS = [
		// Remote code execution primitives — very high risk.
		[ '/\beval\s*\(\s*base64_decode\s*\(/i',           10, 'eval_base64'       ],
		[ '/\beval\s*\(\s*(str_rot13|gzinflate|gzuncompress|str_replace)/i', 9, 'eval_obfuscated' ],
		[ '/\beval\s*\(/i',                                 5,  'eval_call'         ],
		[ '/\bbase64_decode\s*\(\s*[\'"][A-Za-z0-9+\/]{100,}[\'"]\s*\)/i', 8, 'base64_payload'  ],
		[ '/\bpreg_replace\s*\(\s*[\'"].*\/e[\'"\s]*,/i',  9,  'preg_replace_eval' ],

		// Shell access.
		[ '/\bshell_exec\s*\(/i',                          8,  'shell_exec'        ],
		[ '/\bpassthru\s*\(/i',                            8,  'passthru'          ],
		[ '/\bsystem\s*\(\s*[\$_]/i',                      9,  'system_call'       ],
		[ '/\bpopen\s*\(/i',                               7,  'popen'             ],
		[ '/\bproc_open\s*\(/i',                           7,  'proc_open'         ],
		[ '/\bexec\s*\(/i',                                6,  'exec_call'         ],

		// Backdoor / webshell patterns.
		[ '/\$_(?:GET|POST|REQUEST|COOKIE|SERVER)\s*\[\s*[\'"][^\'"]+[\'"]\s*\]\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE)/i', 10, 'webshell_callback' ],
		[ '/assert\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)/i', 10, 'assert_input'      ],
		[ '/\bcreate_function\s*\(\s*[\'"].*[\'"]\s*,\s*\$_(GET|POST|REQUEST|COOKIE)/i', 9, 'create_function_input' ],
		[ '/FilesMan|r57shell|c99shell|WSO\s+Shell/i',     10, 'known_webshell'    ],

		// Suspicious network functions.
		[ '/\bfsockopen\s*\(\s*[\$"\']/i',                 5,  'fsockopen'         ],
		[ '/\bcurl_exec\s*\(/i',                           4,  'curl_exec'         ],
		[ '/\bfile_get_contents\s*\(\s*["\']https?:\/\//i', 5, 'remote_include'    ],
		[ '/\binclude\s*\(\s*["\']https?:\/\//i',          9,  'remote_include_direct' ],
		[ '/\brequire\s*\(\s*["\']https?:\/\//i',          9,  'remote_require'    ],

		// Common obfuscation techniques.
		[ '/\$[a-z0-9_]+\s*=\s*str_rot13\s*\(/i',         6,  'str_rot13'         ],
		[ '/\$[a-z0-9_]+\s*=\s*gzinflate\s*\(/i',         7,  'gzinflate'         ],
		[ '/\\\\x[0-9a-f]{2}\\\\x[0-9a-f]{2}\\\\x[0-9a-f]{2}/i', 5, 'hex_encoded' ],
		[ '/chr\s*\(\s*\d+\s*\)\s*\.\s*chr\s*\(\s*\d+\s*\)/i', 4, 'chr_concat'   ],

		// WordPress-specific injections.
		[ '/add_action\s*\(\s*["\']wp_head["\']\s*,\s*create_function/i', 9, 'wp_head_injection' ],
		[ '/\$wp_filter\s*\[.*\]\s*=\s*array\s*\(\s*\d+\s*=>/i',         6, 'filter_injection'  ],
	];

	private Logger $logger;
	private bool   $quarantine_enabled;

	public function __construct( Logger $logger ) {
		$this->logger             = $logger;
		$this->quarantine_enabled = (bool) gs_get_option( 'quarantine_enabled', true );
	}

	// ── Scheduled Entry Point ────────────────────
	public function run_scheduled_scan(): void {
		$start = microtime( true );

		$results = $this->scan_uploads();

		// Log completion.
		$elapsed = round( microtime( true ) - $start, 2 );
		$count   = count( $results );

		$this->logger->log_attack( [
			'ip_address'  => '0.0.0.0',
			'attack_type' => 'scan_complete',
			'severity'    => 1,
			'description' => sprintf( 'Malware scan completed: %d threats found in %.2fs', $count, $elapsed ),
			'blocked'     => 0,
		] );

		if ( $count > 0 ) {
			do_action( 'gs_malware_detected', $results );
		}
	}

	// ── Scan Uploads Directory ───────────────────
	public function scan_uploads(): array {
		$upload_dir = wp_upload_dir();
		$base_dir   = $upload_dir['basedir'];

		if ( ! is_dir( $base_dir ) ) {
			return [];
		}

		$threats = [];
		$iterator = new \RecursiveIteratorIterator(
			new \RecursiveDirectoryIterator( $base_dir, \RecursiveDirectoryIterator::SKIP_DOTS ),
			\RecursiveIteratorIterator::LEAVES_ONLY
		);

		foreach ( $iterator as $file ) {
			if ( ! $file->isFile() ) {
				continue;
			}

			$path = $file->getRealPath();

			// Only scan PHP and potentially dangerous file types.
			if ( ! $this->is_scannable_file( $path ) ) {
				continue;
			}

			$result = $this->scan_file( $path );
			if ( $result ) {
				$threats[] = $result;
			}
		}

		return $threats;
	}

	// ── Single File Scan ─────────────────────────
	public function scan_file( string $path ): ?array {
		if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
			return null;
		}

		if ( filesize( $path ) > self::MAX_FILE_SIZE ) {
			return null; // Skip huge files.
		}

		$content  = file_get_contents( $path );
		if ( false === $content ) {
			return null;
		}

		$score   = 0;
		$matched = [];

		foreach ( self::MALWARE_PATTERNS as [ $pattern, $pattern_score, $label ] ) {
			if ( preg_match( $pattern, $content ) ) {
				$score   += $pattern_score;
				$matched[] = $label;
			}
		}

		if ( $score === 0 ) {
			return null; // Clean file.
		}

		$severity = match ( true ) {
			$score >= 20 => 4, // Critical
			$score >= 12 => 3, // High
			$score >= 8  => 2, // Medium
			default      => 1, // Low
		};

		$status = 'detected';

		// Quarantine high-severity threats automatically.
		if ( $score >= self::THREAT_THRESHOLD && $this->quarantine_enabled ) {
			if ( $this->quarantine_file( $path ) ) {
				$status = 'quarantined';
			}
		}

		$threat = [
			'file_path'     => $path,
			'threat_type'   => implode( ', ', array_unique( $matched ) ),
			'threat_detail' => sprintf( 'Threat score: %d, Rules: %s', $score, implode( ', ', $matched ) ),
			'severity'      => $severity,
			'status'        => $status,
			'score'         => $score,
		];

		// Persist to database.
		$this->logger->log_scan_result( array_merge( $threat, [ 'scan_type' => 'malware' ] ) );

		// Log as attack too.
		$this->logger->log_attack( [
			'ip_address'   => '0.0.0.0',
			'attack_type'  => 'malware_detected',
			'severity'     => $severity,
			'description'  => sprintf( 'Malware in: %s — %s', $path, $threat['threat_detail'] ),
			'request_uri'  => $path,
			'user_agent'   => 'Scanner',
			'blocked'      => $status === 'quarantined' ? 1 : 0,
			'threat_score' => min( 100, $score * 3 ),
		] );

		return $threat;
	}

	// ── Quarantine ───────────────────────────────
	/**
	 * Moves file to GS_QUARANTINE and leaves a harmless stub in its place.
	 * Records a JSON manifest alongside the quarantined file.
	 */
	public function quarantine_file( string $path ): bool {
		if ( ! is_writable( dirname( $path ) ) ) {
			return false;
		}

		$safe_name = md5( $path ) . '_' . basename( $path ) . '.quarantine';
		$dest      = GS_QUARANTINE . $safe_name;

		// Move file.
		if ( ! rename( $path, $dest ) ) {
			return false;
		}

		// Write manifest.
		$manifest = [
			'original_path' => $path,
			'quarantine'    => $dest,
			'quarantined_at'=> current_time( 'mysql', true ),
			'sha256'        => hash_file( 'sha256', $dest ),
		];
		file_put_contents( $dest . '.json', wp_json_encode( $manifest, JSON_PRETTY_PRINT ) );

		// Leave a stub so CMS doesn't crash on missing file.
		file_put_contents( $path, '<?php /* Guardian Shield: This file was quarantined for security reasons. */ ?>' );

		return true;
	}

	// ── File Type Filter ─────────────────────────
	private function is_scannable_file( string $path ): bool {
		$ext = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );
		return in_array( $ext, [ 'php', 'php3', 'php4', 'php5', 'php7', 'phtml', 'phar', 'js' ], true );
	}

	// ── On-demand scan for Admin UI ──────────────
	public function scan_plugin( string $plugin_dir ): array {
		$threats  = [];
		$iterator = new \RecursiveIteratorIterator(
			new \RecursiveDirectoryIterator( $plugin_dir, \RecursiveDirectoryIterator::SKIP_DOTS )
		);

		foreach ( $iterator as $file ) {
			if ( ! $file->isFile() || ! $this->is_scannable_file( $file->getRealPath() ) ) {
				continue;
			}
			$result = $this->scan_file( $file->getRealPath() );
			if ( $result ) {
				$threats[] = $result;
			}
		}

		return $threats;
	}
}
