info( __( 'No plugins are installed to check.', 'aardwolf-security-scanner' ) ); } $closed_plugins = array(); // Removed from the directory. $network_failed = false; foreach ( $plugins as $file => $data ) { $slug = $this->slug_from_file( $file ); if ( '' === $slug ) { continue; } $name = isset( $data['Name'] ) ? $data['Name'] : $slug; $org = $this->query_wporg( $slug ); if ( is_wp_error( $org ) ) { $network_failed = true; } elseif ( is_array( $org ) && ! empty( $org['closed'] ) ) { $closed_plugins[] = $name; } } if ( ! empty( $closed_plugins ) ) { return $this->fail( Aardwolf_Check_Result::SEVERITY_HIGH, sprintf( /* translators: %s: comma separated plugin names. */ __( 'These installed plugins have been removed from the WordPress.org directory, which often means they were pulled for an unresolved security issue: %s', 'aardwolf-security-scanner' ), implode( ', ', $closed_plugins ) ), __( 'Review each affected plugin. Find a maintained replacement and remove the abandoned one — a plugin that is no longer in the directory will not receive security fixes. If a plugin is business-critical and no alternative exists, consider putting the site behind a web application firewall until it is resolved.', 'aardwolf-security-scanner' ), 'https://wordpress.org/documentation/article/manage-plugins/' ); } if ( $network_failed ) { return $this->info( __( 'Could not reach the WordPress.org Plugin API (the server may block outbound requests), so directory status could not be checked.', 'aardwolf-security-scanner' ), __( 'Ensure your server can make outbound HTTPS requests. Keeping every plugin updated remains the most important safeguard.', 'aardwolf-security-scanner' ) ); } return $this->pass( __( 'No installed plugins have been removed from the WordPress.org directory.', 'aardwolf-security-scanner' ) ); } /** * Derive the plugin directory slug from its main file path. * * @param string $file e.g. "akismet/akismet.php". * @return string */ protected function slug_from_file( $file ) { if ( false !== strpos( $file, '/' ) ) { return sanitize_key( dirname( $file ) ); } // Single-file plugin: use the basename without extension. return sanitize_key( basename( $file, '.php' ) ); } /** * Query the wordpress.org Plugin Information API. Results are cached in a * transient for a day to avoid hammering the API on every scan. * * @param string $slug Plugin slug. * @return array|WP_Error Decoded response, or WP_Error on failure. */ protected function query_wporg( $slug ) { $cache_key = 'aardwolf_sc_wporg_' . $slug; $cached = get_transient( $cache_key ); if ( false !== $cached ) { return is_array( $cached ) ? $cached : new WP_Error( 'cached_error' ); } $url = sprintf( 'https://api.wordpress.org/plugins/info/1.0/%s.json', rawurlencode( $slug ) ); $response = wp_remote_get( $url, array( 'timeout' => 8, 'headers' => array( 'User-Agent' => 'Aardwolf-Security-Scanner/' . AARDWOLF_SC_VERSION ), ) ); if ( is_wp_error( $response ) ) { return $response; } $code = (int) wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); // A 404 means the plugin is not (or no longer) in the directory. That is // not necessarily "closed" (it may be a premium or custom plugin), so we // record it as unknown rather than flagging it. if ( 404 === $code || null === $body ) { $result = array( 'closed' => false, 'unknown' => true, ); } else { $result = array( 'closed' => ! empty( $body['closed'] ), 'version' => isset( $body['version'] ) ? $body['version'] : '', ); } set_transient( $cache_key, $result, DAY_IN_SECONDS ); return $result; } }