static::run_security(), 'speed' => static::run_speed(), 'resources' => static::run_migration(), ); // Free shared HTTP responses after the scan completes (R-02). static::clear_response_cache(); return $results; } /** * Run only the security checks. * * @return array List of security check results. * Each check result is an associative array with keys: * - id: string (check identifier) * - title: string (check title) * - description: string (check description) * - status: string ('pass', 'warning', 'fail', 'info') * - severity: string ('low', 'medium', 'high') * - details: array (optional, additional info about the check result) * * @since 4.9.0 */ public static function run_security(): array { return array( Admin_Username_Check::run(), Database_Prefix_Check::run(), Debug_Mode_Check::run(), File_Editor_Check::run(), File_Permissions_Check::run(), Login_URL_Check::run(), Pending_Updates_Check::run(), PHP_Version_Check::run(), Security_Headers_Check::run(), SSL_Certificate_Check::run(), User_Enumeration_Check::run(), Version_Disclosure_Check::run(), Directory_Listing_Check::run(), WordPress_Version_Check::run(), XMLRPC_Check::run(), ); } /** * Run only the speed checks. * * @return array List of performance check results. * Each check result is an associative array with keys: * - id: string (check identifier) * - title: string (check title) * - description: string (check description) * - status: string ('pass', 'warning', 'fail', 'info') * - severity: string ('low', 'medium', 'high') * - details: array (optional, additional info about the check result) * * @since 4.9.0 */ public static function run_speed(): array { return array( Autoloaded_Options_Check::run(), Cron_Jobs_Check::run(), Database_Overhead_Check::run(), External_Requests_Check::run(), GZIP_Compression_Check::run(), Heartbeat_API_Check::run(), Image_Optimization_Check::run(), Lazy_Loading_Check::run(), Object_Caching_Check::run(), Page_Caching_Check::run(), PHP_Memory_Limit_Check::run(), Post_Revisions_Check::run(), ); } /** * Run only the migration checks. * * @return array List of migration check results. * Each check result is an associative array with keys: * - id: string (check identifier) * - title: string (check title) * - description: string (check description) * - status: string ('pass', 'warning', 'fail', 'info') * - severity: string ('low', 'medium', 'high') * - details: array (optional, additional info about the check result) * * @since 4.9.0 */ public static function run_migration(): array { return array( Active_Plugins_Check::run(), Custom_Tables_Check::run(), Database_Size_Check::run(), // Hardcoded_URLs_Check::run(), Migration_Complexity_Check::run(), MU_Plugins_Check::run(), // Serialized_Data_Check::run(), Tables_Overhead_Check::run(), // Uploads_Size_Check::run(), ); } // ------------------------------------------------------------------ // Loopback request helpers // ------------------------------------------------------------------ /** * HTTP header name sent on every internal loopback request. * * @since 4.9.0 */ public const LOOPBACK_HEADER = 'X-ADVAN-Internal-Check'; /** * Corresponding $_SERVER key for the loopback header. * * @since 4.9.0 */ private const LOOPBACK_SERVER_KEY = 'HTTP_X_ADVAN_INTERNAL_CHECK'; /** * Transient key that holds the per-run random loopback auth token. * * @since 4.9.0 */ private const LOOPBACK_TOKEN_TRANSIENT = 'advan_loopback_token'; /** * In-memory cache of HTTP responses keyed by URL (R-02). * * Shared across security checks within a single scan run so that * checks targeting the same URL reuse the response instead of * issuing redundant HTTP requests. * * @var array * * @since 5.1.1 */ private static $response_cache = array(); /** * Perform a GET request to the given URL, reusing a cached response * when available. Checks hitting the same URL within a scan run will * share a single HTTP round-trip (R-02). * * @param string $url Target URL. * @param array $args Optional wp_remote_get arguments. The loopback * header is added automatically. * * @return array|\WP_Error * * @since 5.1.1 */ public static function cached_get( string $url, array $args = array() ) { if ( isset( self::$response_cache[ $url ] ) ) { return self::$response_cache[ $url ]; } $defaults = array( 'timeout' => 10, 'sslverify' => false, 'redirection' => 2, 'headers' => self::get_loopback_headers(), ); $args = \wp_parse_args( $args, $defaults ); // Ensure loopback header is always present. if ( ! isset( $args['headers'][ self::LOOPBACK_HEADER ] ) ) { $args['headers'][ self::LOOPBACK_HEADER ] = self::get_loopback_token(); } $response = \wp_remote_get( $url, $args ); self::$response_cache[ $url ] = $response; return $response; } /** * Clear the per-run HTTP response cache. * * Called automatically after a full scan completes. * * @return void * * @since 5.1.1 */ public static function clear_response_cache(): void { self::$response_cache = array(); } /** * Returns (and lazily creates) a short-lived random token that authenticates * loopback HTTP requests for the current scanner run. * * The token is stored in a transient for 2 minutes. Every check within the * same run shares it. After expiry, an externally captured header value * becomes worthless. * * @return string * * @since 4.9.0 */ public static function get_loopback_token(): string { $token = \get_transient( self::LOOPBACK_TOKEN_TRANSIENT ); if ( false === $token ) { $token = \wp_generate_password( 32, false, false ); \set_transient( self::LOOPBACK_TOKEN_TRANSIENT, $token, 2 * MINUTE_IN_SECONDS ); } return (string) $token; } /** * Returns the headers array to include in every loopback wp_remote_* call. * * @return array * * @since 4.9.0 */ public static function get_loopback_headers(): array { return array( self::LOOPBACK_HEADER => self::get_loopback_token() ); } /** * Registers the determine_current_user filter that prevents WordPress * rotating the admin session token when scanner checks issue loopback * HTTP requests to the same site. * * Hardening: the filter validates a per-run random token stored in a * transient. An external request that sends the header with no token * (or the wrong token) is left completely unaffected; normal WordPress * authentication proceeds as usual. * * Must be called before wp_validate_auth_cookie fires (priority 10). * * @return void * * @since 4.9.0 */ public static function register_auth_filter(): void { $server_key = self::LOOPBACK_SERVER_KEY; $transient_key = self::LOOPBACK_TOKEN_TRANSIENT; \add_filter( 'determine_current_user', static function ( $user_id ) use ( $server_key, $transient_key ) { if ( empty( $_SERVER[ $server_key ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput return $user_id; } // Validate token to prevent the header being abused by external actors. $provided = \sanitize_text_field( \wp_unslash( $_SERVER[ $server_key ] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $expected = \get_transient( $transient_key ); if ( false === $expected || ! \hash_equals( (string) $expected, $provided ) ) { // Bad or missing token: leave auth intact and return unchanged. return $user_id; } // Valid internal loopback: skip cookie auth to prevent session rotation. \remove_filter( 'determine_current_user', 'wp_validate_auth_cookie' ); \remove_filter( 'determine_current_user', 'wp_validate_application_password', 20 ); \remove_filter( 'determine_current_user', 'wp_validate_logged_in_cookie', 20 ); return 0; }, 1 // must run before wp_validate_auth_cookie (priority 10). ); } } }