'GET', 'permission_callback' => '__return_true', 'callback' => function () { $token = get_option( 'shieldome_verify_token', '' ); if ( ! is_string( $token ) || ! preg_match( '/^[0-9a-f]{16,64}$/', $token ) ) { return new WP_Error( 'shieldome_no_token', 'No verification pending', [ 'status' => 404 ] ); } return rest_ensure_response( 'shieldome-verify=' . $token ); }, ] ); } /** Settings > "Verify this site": request a token, publish it, ask Shieldome to check it. */ public function ajax_verify_site() { check_ajax_referer( 'shieldome_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) wp_die( esc_html__( 'Unauthorized', 'shieldome-scanner' ) ); $api_key = get_option( 'shieldome_api_key', '' ); if ( empty( $api_key ) ) { wp_send_json_error( [ 'message' => __( 'API key not configured. Save your API key first.', 'shieldome-scanner' ) ] ); } $call = function ( $step ) use ( $api_key ) { $resp = wp_remote_post( SHIELDOME_API_BASE . '/wp-plugin/verify-site', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-Shieldome-Key' => $api_key ], 'body' => wp_json_encode( [ 'site_url' => home_url(), 'step' => $step ] ), 'timeout' => 30, ] ); if ( is_wp_error( $resp ) ) { return [ 'error' => $resp->get_error_message() ]; } $body = json_decode( wp_remote_retrieve_body( $resp ), true ); return is_array( $body ) ? $body : [ 'error' => __( 'Unexpected response from Shieldome.', 'shieldome-scanner' ) ]; }; $init = $call( 'init' ); if ( empty( $init['verified'] ) ) { if ( empty( $init['token'] ) || ! preg_match( '/^[0-9a-f]{16,64}$/', (string) $init['token'] ) ) { $this->finish_verify( false, $init['error'] ?? __( 'Could not start verification.', 'shieldome-scanner' ) ); } update_option( 'shieldome_verify_token', $init['token'], false ); $init = $call( 'check' ); } if ( ! empty( $init['verified'] ) ) { $this->finish_verify( true, sprintf( /* translators: %s: the verified domain */ __( '%s is verified. Scans now include the active checks.', 'shieldome-scanner' ), $init['domain'] ?? wp_parse_url( home_url(), PHP_URL_HOST ) ) ); } $this->finish_verify( false, $init['message'] ?? $init['error'] ?? __( 'Verification failed.', 'shieldome-scanner' ) ); } private function finish_verify( $ok, $message ) { if ( $ok ) { delete_option( 'shieldome_verify_token' ); } update_option( 'shieldome_verify_status', ( $ok ? 'verified' : 'failed' ) . '|' . current_time( 'mysql' ) . '|' . $message, false ); $ok ? wp_send_json_success( [ 'message' => $message ] ) : wp_send_json_error( [ 'message' => $message ] ); } /** Once a day, so newly published vulnerabilities in installed plugins are caught between scans. */ public function ensure_schedule() { if ( ! wp_next_scheduled( 'shieldome_daily_inventory' ) ) { wp_schedule_event( time() + 300, 'daily', 'shieldome_daily_inventory' ); } } /** * Daily: the installed plugins/themes/core to Shieldome, for this site * only, when an API key is set and inventory sending is on. The result is * kept as an option for the settings page. */ public function send_inventory() { $api_key = get_option( 'shieldome_api_key', '' ); if ( empty( $api_key ) || get_option( 'shieldome_send_inventory', '1' ) !== '1' ) { return; } $resp = wp_remote_post( SHIELDOME_API_BASE . '/wp-inventory', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-Shieldome-Key' => $api_key ], 'body' => wp_json_encode( [ 'site_url' => get_site_url(), 'wp_inventory' => $this->collect_inventory() ] ), 'timeout' => 30, ] ); if ( is_wp_error( $resp ) ) { $status = 'error: ' . $resp->get_error_message(); } else { $code = wp_remote_retrieve_response_code( $resp ); $body = json_decode( wp_remote_retrieve_body( $resp ), true ); $status = $code === 200 ? 'sent' : ( 'refused (' . $code . '): ' . ( $body['error'] ?? 'unknown error' ) ); } update_option( 'shieldome_inventory_status', current_time( 'mysql' ) . ' - ' . $status ); } public function add_menu() { add_menu_page( __( 'Shieldome Scanner', 'shieldome-scanner' ), __( 'Shieldome', 'shieldome-scanner' ), 'manage_options', 'shieldome-scanner', [ $this, 'render_admin' ], 'dashicons-shield', 80 ); add_submenu_page( 'shieldome-scanner', __( 'Settings', 'shieldome-scanner' ), __( 'Settings', 'shieldome-scanner' ), 'manage_options', 'shieldome-settings', [ $this, 'render_settings' ] ); } public function register_settings() { register_setting( 'shieldome_options', 'shieldome_api_key', [ 'sanitize_callback' => 'sanitize_text_field' ] ); register_setting( 'shieldome_options', 'shieldome_target_url', [ 'sanitize_callback' => 'esc_url_raw' ] ); register_setting( 'shieldome_options', 'shieldome_send_inventory', [ 'sanitize_callback' => [ $this, 'sanitize_flag' ], 'default' => '1' ] ); } public function sanitize_flag( $value ) { return $value ? '1' : '0'; } /** * Installed plugins (active and inactive), themes and the core version, * as WordPress lists them. Sent with a scan of this site so Shieldome can * check every installed component against known vulnerabilities, not * only the ones visible from outside. Names and versions only. */ private function collect_inventory() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = []; foreach ( get_plugins() as $file => $data ) { $slug = dirname( $file ); if ( $slug === '.' ) { $slug = basename( $file, '.php' ); } $plugins[] = [ 'slug' => strtolower( $slug ), 'version' => isset( $data['Version'] ) ? (string) $data['Version'] : '', 'active' => is_plugin_active( $file ), ]; } $themes = []; foreach ( wp_get_themes() as $stylesheet => $theme ) { $themes[] = [ 'slug' => strtolower( $stylesheet ), 'version' => (string) $theme->get( 'Version' ), 'active' => ( $stylesheet === get_stylesheet() || $stylesheet === get_template() ), ]; } return [ 'core' => get_bloginfo( 'version' ), 'plugins' => array_slice( $plugins, 0, 1000 ), 'themes' => array_slice( $themes, 0, 200 ), 'source' => 'shieldome-wp-plugin/' . SHIELDOME_VERSION, ]; } /** The inventory describes this site only: never sent for another target. */ private function target_is_this_site( $target ) { $t = wp_parse_url( $target, PHP_URL_HOST ); $s = wp_parse_url( get_site_url(), PHP_URL_HOST ); return $t && $s && strtolower( $t ) === strtolower( $s ); } public function enqueue_assets( $hook ) { if ( strpos( $hook, 'shieldome' ) === false ) return; wp_enqueue_style( 'shieldome', plugins_url( 'assets/admin.css', __FILE__ ), [], SHIELDOME_VERSION ); wp_enqueue_script( 'shieldome', plugins_url( 'assets/admin.js', __FILE__ ), ['jquery'], SHIELDOME_VERSION, true ); wp_localize_script( 'shieldome', 'shieldomeData', [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'shieldome_nonce' ), 'site_url' => get_site_url(), 'i18n' => [ 'starting' => __( 'Starting...', 'shieldome-scanner' ), 'sending' => __( 'Sending scan request...', 'shieldome-scanner' ), 'started' => __( 'Scan started. Checking for results...', 'shieldome-scanner' ), 'error' => __( 'Error:', 'shieldome-scanner' ), 'unknown' => __( 'Unknown error', 'shieldome-scanner' ), 'failed' => __( 'Request failed.', 'shieldome-scanner' ), 'start' => __( 'Start Security Scan', 'shieldome-scanner' ), 'done' => __( 'Done! Grade:', 'shieldome-scanner' ), 'report' => __( 'View report', 'shieldome-scanner' ), 'scanfail' => __( 'Scan failed.', 'shieldome-scanner' ), 'scanning' => __( 'Scanning...', 'shieldome-scanner' ), 'slow' => __( 'Taking longer than expected. Check app.shieldome.com for results.', 'shieldome-scanner' ), 'verifying' => __( 'Verifying...', 'shieldome-scanner' ), 'verify' => __( 'Verify this site', 'shieldome-scanner' ), ], ] ); } public function ajax_start_scan() { check_ajax_referer( 'shieldome_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) wp_die( esc_html__( 'Unauthorized', 'shieldome-scanner' ) ); $api_key = get_option( 'shieldome_api_key', '' ); $target = get_option( 'shieldome_target_url', get_site_url() ); if ( empty( $api_key ) ) { wp_send_json_error( [ 'message' => __( 'API key not configured. Go to Shieldome > Settings.', 'shieldome-scanner' ) ] ); } // Authenticate with the Shieldome API key and request a combined // vulnerability + performance scan of the admin's own site, which the // plugin is authorized to scan. $payload = [ 'target_url' => $target, 'scan_type' => 'both', 'scan_authorized' => true ]; if ( get_option( 'shieldome_send_inventory', '1' ) === '1' && $this->target_is_this_site( $target ) ) { $payload['wp_inventory'] = $this->collect_inventory(); } $resp = wp_remote_post( SHIELDOME_API_BASE . '/scan', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-Shieldome-Key' => $api_key ], 'body' => wp_json_encode( $payload ), 'timeout' => 30, ] ); if ( is_wp_error( $resp ) ) wp_send_json_error( [ 'message' => $resp->get_error_message() ] ); $body = json_decode( wp_remote_retrieve_body( $resp ), true ); if ( ! empty( $body['scan_id'] ) ) { update_option( 'shieldome_last_scan_id', $body['scan_id'] ); wp_send_json_success( [ 'scan_id' => $body['scan_id'] ] ); } else { wp_send_json_error( [ 'message' => $body['error'] ?? __( 'Failed to start scan. Check your API key.', 'shieldome-scanner' ) ] ); } } public function ajax_poll_scan() { check_ajax_referer( 'shieldome_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) wp_die( esc_html__( 'Unauthorized', 'shieldome-scanner' ) ); $scan_id = isset( $_POST['scan_id'] ) ? sanitize_text_field( wp_unslash( $_POST['scan_id'] ) ) : ''; $api_key = get_option( 'shieldome_api_key', '' ); if ( empty( $scan_id ) || ! preg_match( '/^[0-9a-f-]{36}$/', $scan_id ) || empty( $api_key ) ) { wp_send_json_error( [ 'message' => __( 'Missing params', 'shieldome-scanner' ) ] ); } $resp = wp_remote_get( SHIELDOME_API_BASE . '/scan/' . rawurlencode( $scan_id ), [ 'headers' => [ 'X-Shieldome-Key' => $api_key ], 'timeout' => 15, ] ); if ( is_wp_error( $resp ) ) wp_send_json_error( [ 'message' => $resp->get_error_message() ] ); $body = json_decode( wp_remote_retrieve_body( $resp ), true ); if ( ! empty( $body['status'] ) && $body['status'] === 'completed' ) { // The grade and risk score are returned under "summary". $summary = $body['summary'] ?? []; update_option( 'shieldome_last_grade', $summary['risk_grade'] ?? 'F' ); update_option( 'shieldome_last_risk_score', $summary['risk_score'] ?? 100 ); update_option( 'shieldome_last_scan_date', current_time( 'mysql' ) ); } wp_send_json_success( $body ); } public function render_admin() { $api_key = get_option( 'shieldome_api_key', '' ); $last_grade = get_option( 'shieldome_last_grade', '' ); $last_score = get_option( 'shieldome_last_risk_score', '' ); $last_date = get_option( 'shieldome_last_scan_date', '' ); $last_id = get_option( 'shieldome_last_scan_id', '' ); $target_url = get_option( 'shieldome_target_url', get_site_url() ); include plugin_dir_path( __FILE__ ) . 'templates/admin-page.php'; } public function render_settings() { include plugin_dir_path( __FILE__ ) . 'templates/settings-page.php'; } public function glance_items() { $grade = get_option( 'shieldome_last_grade', '' ); if ( ! $grade ) return; $colors = [ 'A' => '#22c55e', 'B' => '#60a5fa', 'C' => '#fbbf24', 'D' => '#f97316', 'F' => '#ef4444' ]; $color = $colors[ $grade ] ?? '#94a3b8'; printf( '