plugin_dir = trailingslashit( str_replace( '\\', '/', dirname( WPSCAN_PLUGIN_FILE ) ) );
$this->plugin_url = site_url( str_replace( str_replace( '\\', '/', ABSPATH ), '', $this->plugin_dir ) );
// Languages.
load_plugin_textdomain( 'wpscan', false, $this->plugin_dir . 'languages' );
// Cache values in memory.
$this->report = get_option( $this->OPT_REPORT, array() );
$this->ignored_vulnerabilities = get_option( $this->OPT_IGNORED, array() );
// Actions.
add_action( 'admin_menu', array( $this, 'menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue' ) );
add_action( 'admin_bar_menu', array( $this, 'admin_bar' ), 65 );
add_action( $this->WPSCAN_SCHEDULE, array( $this, 'check_now' ) );
add_action( $this->WPSCAN_RUN_ALL, array( $this, 'check_now' ) );
add_action( 'in_admin_header', array( $this, 'deactivate_screen' ) );
if ( defined( 'WPSCAN_API_TOKEN' ) ) {
add_action( 'admin_init', array( $this, 'api_token_from_constant' ) );
}
// Filters.
add_filter( 'plugin_action_links_' . plugin_basename( WPSCAN_PLUGIN_FILE ), array( $this, 'add_action_links' ) );
// Micro apps (modules).
$this->classes['report'] = new Report( $this );
$this->classes['settings'] = new Settings( $this );
$this->classes['account'] = new Account( $this );
$this->classes['notification'] = new Notification( $this );
$this->classes['summary'] = new Summary( $this );
$this->classes['ignoreVulnerabilities'] = new ignoreVulnerabilities( $this );
$this->classes['dashboard'] = new Dashboard( $this );
$this->classes['sitehealth'] = new SiteHealth( $this );
$this->classes['checks/system'] = new Checks\System( $this );
}
/**
* Plugin Loaded
*
* @return void
* @since 1.0.0
* @access public
*/
public function loaded() {
load_plugin_textdomain( 'wpscan', false, $this->plugin_dir . 'languages' );
}
/**
* Activate actions. Runs when the plugin is activated.
*
* @return void
* @since 1.0.0
* @access public
*/
public function activate() {
$this->delete_doing_cron_transient();
}
/**
* Deactivate actions
*
* @return void
* @since 1.0.0
* @access public
*/
public function deactivate() {
delete_option( $this->OPT_SCANNING_INTERVAL );
delete_option( $this->OPT_SCANNING_TIME );
as_unschedule_all_actions( $this->WPSCAN_SCHEDULE );
}
/**
* Deactivate screen
*
* @return void
* @since 1.14.0
* @access public
*/
public function deactivate_screen() {
global $pagenow;
if ( 'plugins.php' === $pagenow ) {
include_once plugin_dir_path( WPSCAN_PLUGIN_FILE ) . 'views/deactivate.php';
}
}
/**
* Use the global constant WPSCAN_API_TOKEN if defined.
*
* @return void
* @since 1.0.0
* @access public
* @example define('WPSCAN_API_TOKEN', 'xxx');
*
*/
public function api_token_from_constant() {
if ( get_option( $this->OPT_API_TOKEN ) !== WPSCAN_API_TOKEN ) {
$sanitize = $this->classes['settings']->sanitize_api_token( WPSCAN_API_TOKEN );
if ( $sanitize ) {
update_option( $this->OPT_API_TOKEN, WPSCAN_API_TOKEN );
} else {
delete_option( $this->OPT_API_TOKEN );
}
}
}
/**
* Register Admin Scripts
*
* @return void
* @since 1.0.0
* @access public
*/
public function admin_enqueue( $hook ) {
global $pagenow;
$screen = get_current_screen();
if ( $hook === $this->page_hook || 'dashboard' === $screen->id ) {
wp_enqueue_style(
'wpscan',
plugins_url( 'assets/css/style.css', WPSCAN_PLUGIN_FILE ),
array(),
$this->wpscan_plugin_version()
);
}
if ( $hook === $this->page_hook ) {
wp_enqueue_script( 'common' );
wp_enqueue_script( 'wp-lists' );
wp_enqueue_script( 'postbox' );
wp_enqueue_script(
'wpscan',
plugins_url( 'assets/js/scripts.js', WPSCAN_PLUGIN_FILE ),
array( 'jquery' ),
$this->wpscan_plugin_version()
);
wp_enqueue_script(
'wpscan-download-report',
plugins_url( 'assets/js/download-report.js', WPSCAN_PLUGIN_FILE ),
array( 'pdfmake' ),
$this->wpscan_plugin_version()
);
wp_enqueue_script(
'pdfmake',
plugins_url( 'assets/vendor/pdfmake/pdfmake.min.js', WPSCAN_PLUGIN_FILE ),
array( 'wpscan' ),
$this->wpscan_plugin_version()
);
wp_enqueue_script(
'wpscan-download-report-fonts',
plugins_url( 'assets/vendor/pdfmake/vfs_fonts.js', WPSCAN_PLUGIN_FILE ),
array( 'wpscan' ),
$this->wpscan_plugin_version()
);
$localized = array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'action_check' => 'wpscan_check_now',
'action_security_check' => 'wpscan_security_check_now',
'action_cron' => $this->WPSCAN_TRANSIENT_CRON,
'ajax_nonce' => wp_create_nonce( 'wpscan' ),
'doing_cron' => false !== as_next_scheduled_action( $this->WPSCAN_RUN_ALL ) ? 'YES' : 'NO',
'doing_security_cron' => get_option( $this->WPSCAN_RUN_SECURITY ),
'running' => esc_html__( 'Running', 'wpscan' ),
'not_running' => esc_html__( 'Run', 'wpscan' ),
);
wp_localize_script( 'wpscan', 'wpscan', $localized );
}
if ( 'plugins.php' === $pagenow ) {
wp_enqueue_style(
'wpscan-deactivate',
plugins_url( 'assets/css/deactivate.css', WPSCAN_PLUGIN_FILE ),
array(),
$this->wpscan_plugin_version()
);
wp_enqueue_script(
'wpscan-deactivate',
plugins_url( 'assets/js/deactivate.js', WPSCAN_PLUGIN_FILE ),
array( 'jquery' ),
$this->wpscan_plugin_version()
);
}
}
/**
* Get the latest report
*
* @return array|bool
* @since 1.0.0
* @access public
*/
public function get_report() {
if ( ! empty( $this->report ) ) {
return ( $this->report );
}
return get_option( $this->OPT_REPORT, array() );
}
/**
* Get the latest report
*
* @return array|bool
* @since 1.0.0
* @access public
*/
public function get_ignored_vulnerabilities() {
if ( ! empty( $this->ignored_vulnerabilities ) ) {
return ( $this->ignored_vulnerabilities );
}
return get_option( $this->OPT_IGNORED, array() );
}
/**
* Return the total of vulnerabilities found or -1 if errors
*
* @return int
* @since 1.0.0
* @access public
*/
public function get_total() {
$report = get_option( $this->OPT_REPORT );
if ( empty( $report ) ) {
return 0;
}
$total = 0;
foreach ( array( 'wordpress', 'themes', 'plugins', 'security-checks' ) as $type ) {
if ( isset( $report[ $type ] ) ) {
if ( isset( $report[ $type ]['total'] ) ) {
unset( $report[ $type ]['total'] );
}
foreach ( $report[ $type ] as $slug => $item ) {
$p = $report[ $type ][ $slug ];
if ( isset( $p['vulnerabilities'] ) && is_array( $p['vulnerabilities'] ) ) {
$total += count( $p['vulnerabilities'] );
}
}
}
}
return $total;
}
/**
* Return the total of vulnerabilities found but not ignored
*
* @return int
* @since 1.0.0
* @access public
*/
public function get_total_not_ignored() {
$report = $this->get_report();
$ignored = get_option( $this->OPT_IGNORED, array() );
$total = $this->get_total();
$types = array( 'wordpress', 'plugins', 'themes', 'security-checks' );
foreach ( $types as $type ) {
if ( isset( $report[ $type ] ) ) {
foreach ( $report[ $type ] as $item ) {
if ( empty( $item['vulnerabilities'] ) ) {
continue;
}
foreach ( $item['vulnerabilities'] as $vuln ) {
$id = 'security-checks' === $type ? $vuln['id'] : $vuln->id;
if ( in_array( $id, $ignored, true ) ) {
-- $total;
}
}
}
}
}
return $total;
}
/**
* Create a menu on Tools section
*
* @return void
* @since 1.0.0
* @access public
*/
public function menu() {
$total = $this->get_total_not_ignored();
$count = $total > 0 ? ' ' . $total . '' : null;
add_menu_page(
'WPScan',
'WPScan' . $count,
$this->WPSCAN_ROLE,
'wpscan',
array( $this->classes['report'], 'page' ),
$this->plugin_url . 'assets/svg/menu-icon.svg',
null
);
}
/**
* Include a shortcut on Plugins Page
*
* @param array $links - Array of links provided by the filter
*
* @access public
* @return array
* @since 1.0.0
*/
public function add_action_links( $links ) {
$links[] = '' . __( 'View' ) . '';
return $links;
}
/**
* Get the WPScan plugin version.
*
* @return string
* @since 1.0.0
* @access public
*/
public function wpscan_plugin_version() {
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return get_plugin_data( $this->plugin_dir . 'wpscan.php' )['Version'];
}
/**
* Get information from the API
*
* @return object|int the JSON object or the response code.
* @since 1.0.0
* @access public
*/
public function api_get( $endpoint, $api_token = null ) {
if ( empty( $api_token ) ) {
$api_token = get_option( $this->OPT_API_TOKEN );
}
// Make sure endpoint starts with a slash.
if ( substr( $endpoint, 0, 1 ) !== '/' ) {
$endpoint = '/' . $endpoint;
}
$args = array(
'headers' => array(
'Authorization' => 'Token token=' . $api_token,
'user-agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . home_url() . ' WPScan/' . $this->wpscan_plugin_version(),
),
);
// Hook before the request.
//do_action( 'wpscan/api/get/before', $endpoint );
// Start the request.
$response = wp_remote_get( WPSCAN_API_URL . $endpoint, $args );
$code = wp_remote_retrieve_response_code( $response );
// Hook after the request.
//do_action( 'wpscan/api/get/after', $endpoint, $response );
if ( 200 === $code ) {
return json_decode( wp_remote_retrieve_body( $response ) );
} else {
$errors = get_option( $this->OPT_ERRORS, array() );
switch ( $code ) {
case 401:
array_push( $errors, __( 'Your API Token expired', 'wpscan' ) );
break;
case 403:
array_push( $errors, __( 'You have entered an invalid API Token', 'wpscan' ) );
break;
case 404:
// We don't have the plugin/theme, do nothing.
break;
case 429:
array_push( $errors, sprintf( '%s %s.', __( 'You hit our free API usage limit. To increase your daily API limit please upgrade to paid usage from your', 'wpscan' ), WPSCAN_PROFILE_URL, __( 'WPScan profile page', 'wpscan' ) ) );
break;
case 500:
array_push( $errors, sprintf( '%s %s', __( 'There seems to be a problem with the WPScan API. Status: 500. Check the ', 'wpscan' ), WPSCAN_STATUS_URL, __( 'API Status', 'wpscan' ) ) );
break;
case 502:
array_push( $errors, sprintf( '%s %s', __( 'There seems to be a problem with the WPScan API. Status: 502. Check the ', 'wpscan' ), WPSCAN_STATUS_URL, __( 'API Status', 'wpscan' ) ) );
break;
case '':
array_push( $errors, sprintf( '%s %s', __( 'We were unable to connect to the WPScan API. Check the ', 'wpscan' ), WPSCAN_STATUS_URL, __( 'API Status', 'wpscan' ) ) );
break;
default:
array_push( $errors, sprintf( '%s %s.', __( 'We received an unknown response from the API. Status: ' . esc_html( $code ), 'wpscan' ), WPSCAN_STATUS_URL, __( 'Check API Status', 'wpscan' ) ) );
break;
}
// Save the errors.
update_option( $this->OPT_ERRORS, array_unique( $errors ) );
}
return $code;
}
/**
* Function to start checking right now
*
* @return void
* @since 1.0.0
* @access public
*/
public function check_now() {
if ( get_transient( $this->WPSCAN_TRANSIENT_CRON ) || empty( get_option( $this->OPT_API_TOKEN ) ) ) {
return;
}
// Start cron job and set timeout.
set_transient( $this->WPSCAN_TRANSIENT_CRON, time(), 60 );
$this->verify();
delete_transient( $this->WPSCAN_TRANSIENT_CRON );
// Notify by mail when solicited.
$this->classes['notification']->notify();
}
/**
* Function to verify on WpScan Database for vulnerabilities
*
* @return void
* @since 1.0.0
* @access public
*/
public function verify() {
$ignored = get_option( $this->OPT_IGNORE_ITEMS );
$ignored = wp_parse_args(
$ignored,
array(
'plugins' => array(),
'themes' => array(),
)
);
// Reset errors.
update_option( $this->OPT_ERRORS, array() );
update_option( $this->classes['checks/system']->OPT_FATAL_ERRORS, array() );
// Plugins.
$this->report['plugins'] = $this->verify_plugins( $ignored['plugins'] );
// Themes.
$this->report['themes'] = $this->verify_themes( $ignored['themes'] );
// WordPress.
if ( ! isset( $ignored['wordpress'] ) ) {
$this->report['wordpress'] = $this->verify_wordpress();
} else {
$this->report['wordpress'] = array();
}
// Security checks.
$this->report['security-checks'] = array();
foreach ( $this->classes['checks/system']->checks as $id => $data ) {
$data['instance']->perform();
$this->report['security-checks'][ $id ]['vulnerabilities'] = array();
if ( $data['instance']->vulnerabilities ) {
$this->report['security-checks'][ $id ]['vulnerabilities'] = $data['instance']->get_vulnerabilities();
$this->maybe_fire_issue_found_action( 'security-check', $id, $this->report['security-checks'][ $id ] );
}
}
// Caching.
$this->report['cache'] = strtotime( current_time( 'mysql' ) );
// Saving.
update_option( $this->OPT_REPORT, $this->report, true );
// Updates account status (API calls etc).
$this->classes['account']->update_account_status();
}
/**
* Fires the wpscan_issue_found action if needed
*
* @param string $type - The affected component type: plugin, theme, WordPress or security-check
* @param string $slug - The affected component slug.
* For WordPress, it will be the version (ie 5.5.3)
* For security-checks, it will be the id of the check, ie xmlrpc-enabled
* @param array $details - An array containing some keys, such as vulnerabilities
* @param array additional_details - An array with the plugin details, such as Version etc
**@since 1.14.0
*
*/
public function maybe_fire_issue_found_action( $type, $slug, $details, $additional_details = array() ) {
if ( ! count( $details['vulnerabilities'] ) > 0 ) {
return;
}
do_action( $this->WPSCAN_ISSUE_FOUND, $type, $slug, $details, $additional_details );
}
/**
* Check plugins for any known vulnerabilities
*
* @param array $ignored - An array of plugins slug to ignore
*
* @access public
* @return array
* @since 1.0.0
*
*/
public function verify_plugins( $ignored ) {
$plugins = array();
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
foreach ( get_plugins() as $name => $details ) {
$slug = $this->get_plugin_slug( $name, $details );
if ( isset( $ignored[ $slug ] ) ) {
continue;
}
$result = $this->api_get( '/plugins/' . $slug );
if ( is_object( $result ) ) {
$plugins[ $slug ]['vulnerabilities'] = $this->get_vulnerabilities( $result, $details['Version'] );
if ( isset( $result->$slug->closed ) ) {
$plugins[ $slug ]['closed'] = is_object( $result->$slug->closed ) ? true : false;
} else {
$plugins[ $slug ]['closed'] = false;
}
$this->maybe_fire_issue_found_action( 'plugin', $slug, $plugins[ $slug ], $details );
} else {
if ( 404 === $result ) {
$plugins[ $slug ]['not_found'] = true;
}
}
}
return $plugins;
}
/**
* Check themes for any known vulnerabilities
*
* @param array $ignored - An array of themes slug to ignore.
*
* @access public
* @return array
* @since 1.0.0
*
*/
public function verify_themes( $ignored ) {
$themes = array();
if ( ! function_exists( 'wp_get_themes' ) ) {
require_once ABSPATH . 'wp-admin/includes/theme.php';
}
$filter = array(
'errors' => null,
'allowed' => null,
'blog_id' => 0,
);
foreach ( wp_get_themes( $filter ) as $name => $details ) {
$slug = $this->get_theme_slug( $name, $details );
if ( isset( $ignored[ $slug ] ) ) {
continue;
}
$result = $this->api_get( '/themes/' . $slug );
if ( is_object( $result ) ) {
$themes[ $slug ]['vulnerabilities'] = $this->get_vulnerabilities( $result, $details['Version'] );
if ( isset( $result->$slug->closed ) ) {
$themes[ $slug ]['closed'] = is_object( $result->$slug->closed ) ? true : false;
} else {
$themes[ $slug ]['closed'] = false;
}
$this->maybe_fire_issue_found_action( 'theme', $slug, $themes[ $slug ], $details );
} else {
if ( 404 === $result ) {
$themes[ $slug ]['not_found'] = true;
}
}
}
return $themes;
}
/**
* Check WordPress for any known vulnerabilities.
*
* @return array
* @since 1.0.0
* @access public
*/
public function verify_wordpress() {
$wordpress = array();
$version = get_bloginfo( 'version' );
$result = $this->api_get( '/wordpresses/' . str_replace( '.', '', $version ) );
if ( is_object( $result ) ) {
$wordpress[ $version ]['vulnerabilities'] = $this->get_vulnerabilities( $result, $version );
$this->maybe_fire_issue_found_action( 'WordPress', $version, $wordpress[ $version ] );
}
return $wordpress;
}
/**
* Filter vulnerability list from WPScan
*
* @param array $data - Report data for the element to check.
* @param string $version - Installed version.
*
* @access public
* @return string
* @since 1.0.0
*
*/
public function get_vulnerabilities( $data, $version ) {
$list = array();
$key = key( $data );
if ( empty( $data->$key->vulnerabilities ) ) {
return $list;
}
// Trim and remove potential leading 'v'.
$version = ltrim( trim( $version ), 'v' );
foreach ( $data->$key->vulnerabilities as $item ) {
if ( $item->fixed_in ) {
if ( version_compare( $version, $item->fixed_in, '<' ) ) {
$list[] = $item;
}
} else {
$list[] = $item;
}
}
return $list;
}
/**
* Get vulnerability title.
*
* @param string $vulnerability - element array.
*
* @access public
* @return string
* @since 1.0.0
*
*/
public function get_sanitized_vulnerability_title( $vulnerability ) {
$title = esc_html( $vulnerability->title ) . ' - ';
$title .= empty( $vulnerability->fixed_in )
? __( 'Not fixed', 'wpscan' )
: sprintf( __( 'Fixed in version %s', 'wpscan' ), esc_html( $vulnerability->fixed_in ) );
return $title;
}
/**
* Get the plugin slug for the given name
*
* @param string $name - plugin name "folder/file.php" or "hello.php".
* @param string $details details.
*
* @access public
* @return string
* @since 1.0.0
*
*/
public function get_plugin_slug( $name, $details ) {
$name = $this->get_name( $name );
$name = $this->get_real_slug( $name, $details['PluginURI'] );
return sanitize_title( $name );
}
/**
* Get the theme slug for the given name.
*
* @param string $name - plugin name "folder/file.php" or "hello.php".
* @param string $details details.
*
* @access public
* @return string
* @since 1.0.0
*
*/
public function get_theme_slug( $name, $details ) {
$name = $this->get_name( $name );
$name = $this->get_real_slug( $name, $details['ThemeURI'] );
return sanitize_title( $name );
}
/**
* Get the plugin/theme name
*
* @param string $name - plugin name "folder/file.php" or "hello.php".
*
* @access public
* @return string
* @since 1.0.0
*
*/
private function get_name( $name ) {
return strstr( $name, '/' ) ? dirname( $name ) : $name;
}
/**
* Get plugin/theme slug.
*
* The name returned by get_plugins or get_themes is not always the real slug.
* If the pluginURI is a WordPress url, we take the slug from there.
* this also fixes folder renames on plugins if the readme is correct.
*
* @param string $name - asset name from get_plugins or wp_get_themes.
* @param string $url - either the value or ThemeURI or PluginURI.
*
* @access public
* @return string
* @since 1.0.0
*
*/
private function get_real_slug( $name, $url ) {
$slug = $name;
$match = preg_match( '/https?:\/\/wordpress\.org\/(?:extend\/)?(?:plugins|themes)\/([^\/]+)\/?/', $url, $matches );
if ( 1 === $match ) {
$slug = $matches[1];
}
return sanitize_title( $slug );
}
/**
* Create a shortcut on Admin Bar to show the total of vulnerabilities found.
*
* @return void
* @since 1.0.0
* @access public
*/
public function admin_bar( $wp_admin_bar ) {
if ( ! current_user_can( $this->WPSCAN_ROLE ) ) {
return;
}
$total = $this->get_total_not_ignored();
if ( $total > 0 ) {
$args = array(
'id' => 'wpscan',
'title' => '' . $total . '',
'href' => admin_url( 'admin.php?page=wpscan' ),
'meta' => array(
'title' => sprintf( _n( '%d vulnerability found', '%d vulnerabilities found', $total, 'wpscan' ), $total ),
),
);
$wp_admin_bar->add_node( $args );
}
}
/**
* Check if interval scanning is disabled
*
* @return bool
* @since 1.0.0
* @access public
* @example define('WPSCAN_DISABLE_SCANNING_INTERVAL', true);
*
*/
public function is_interval_scanning_disabled() {
if ( defined( 'WPSCAN_DISABLE_SCANNING_INTERVAL' ) ) {
return WPSCAN_DISABLE_SCANNING_INTERVAL;
} else {
return false;
}
}
/**
* Delete doing_cron transient, as they could hang in older versions
*
* @return void
* @since 1.12.2
* @access public
*/
public function delete_doing_cron_transient() {
delete_transient( $this->WPSCAN_TRANSIENT_CRON );
}
}