" so the * site address is not included. */ class IntegrityScan extends Feature { const OPTION = 'ajsec_last_scan'; const ACTION = 'ajsec_run_scan'; const ENDPOINT = 'https://api.wordpress.org/core/checksums/1.0/'; /** * {@inheritDoc} */ public function id() { return 'integrity_scan'; } /** * {@inheritDoc} */ public function label() { return __( 'Integrity scan', '007aj-security-scanner-firewall' ); } /** * {@inheritDoc} */ public function description() { return __( 'Manual check of WordPress core files against the official WordPress.org checksums. Reports modified, missing and unexpected files.', '007aj-security-scanner-firewall' ); } /** * Always available: it only runs when an administrator clicks the button. * * @return bool */ public function is_enabled() { return true; } /** * {@inheritDoc} */ public function register() { add_action( 'admin_post_' . self::ACTION, array( $this, 'handle' ) ); } /** * Button handler. */ public function handle() { if ( ! current_user_can( Plugin::capability() ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to do that.', '007aj-security-scanner-firewall' ), 403 ); } check_admin_referer( self::ACTION ); $result = self::run(); wp_safe_redirect( add_query_arg( array( 'page' => 'ajsec-scan', 'ajsec_msg' => empty( $result['error'] ) ? 'scan_done' : 'scan_error', ), admin_url( 'admin.php' ) ) ); exit; } /** * Run a scan and store the result. * * @return array Stored result. */ public static function run() { global $wp_version; $version = (string) $wp_version; $locale = get_locale(); $started = microtime( true ); $fetched = self::fetch_checksums( $version, $locale ); $result = array( 'time' => time(), 'version' => $version, 'locale' => $fetched['locale'], 'error' => $fetched['error'], 'checked' => 0, 'counts' => array( 'modified' => 0, 'missing' => 0, 'unknown' => 0, ), 'modified' => array(), 'missing' => array(), 'unknown' => array(), ); if ( '' === $fetched['error'] ) { $result = array_merge( $result, Scanner::compare( ABSPATH, $fetched['checksums'] ) ); } $result['seconds'] = round( microtime( true ) - $started, 2 ); update_option( self::OPTION, $result, false ); if ( '' === $result['error'] ) { /* translators: 1: files checked, 2: modified, 3: missing, 4: unknown. */ $detail = sprintf( __( '%1$d files checked: %2$d modified, %3$d missing, %4$d unknown', '007aj-security-scanner-firewall' ), $result['checked'], $result['counts']['modified'], $result['counts']['missing'], $result['counts']['unknown'] ); } else { $detail = $result['error']; } Log::add( 'scan_completed', $detail, get_current_user_id() ); return $result; } /** * Get the official checksums for a version. Falls back to en_US when the locale has no list. * * @param string $version WordPress version. * @param string $locale Site locale. * @return array array( 'checksums' => array, 'locale' => string, 'error' => string ). */ public static function fetch_checksums( $version, $locale ) { $locales = array_unique( array( $locale, 'en_US' ) ); $error = ''; foreach ( $locales as $try ) { $url = add_query_arg( array( 'version' => rawurlencode( $version ), 'locale' => rawurlencode( $try ), ), self::ENDPOINT ); $response = wp_safe_remote_get( $url, array( 'timeout' => 20, 'user-agent' => 'WordPress/' . $version, ) ); if ( is_wp_error( $response ) ) { /* translators: %s: error message. */ $error = sprintf( __( 'Could not reach WordPress.org: %s', '007aj-security-scanner-firewall' ), $response->get_error_message() ); break; } if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { /* translators: %d: HTTP status code. */ $error = sprintf( __( 'WordPress.org returned HTTP %d.', '007aj-security-scanner-firewall' ), (int) wp_remote_retrieve_response_code( $response ) ); break; } $body = json_decode( (string) wp_remote_retrieve_body( $response ), true ); if ( is_array( $body ) && ! empty( $body['checksums'] ) && is_array( $body['checksums'] ) ) { $list = $body['checksums']; // Responses for several versions are keyed by version. if ( isset( $list[ $version ] ) && is_array( $list[ $version ] ) ) { $list = $list[ $version ]; } return array( 'checksums' => $list, 'locale' => $try, 'error' => '', ); } /* translators: %s: WordPress version. */ $error = sprintf( __( 'WordPress.org has no checksums for version %s (development and modified builds are not listed).', '007aj-security-scanner-firewall' ), $version ); } return array( 'checksums' => array(), 'locale' => $locale, 'error' => $error, ); } /** * Last stored result, or null. * * @return array|null */ public static function last() { $last = get_option( self::OPTION ); return is_array( $last ) ? $last : null; } }