time() ) ); } if ( Malsift_Firewall::settings()['uploads_php'] && Malsift_Firewall::server_supports_htaccess() ) { Malsift_Firewall::set_uploads_block( true ); } } /** * Deactivation: remove scheduled events. */ public static function deactivate() { wp_clear_scheduled_hook( 'malsift_daily_scan' ); wp_clear_scheduled_hook( 'malsift_continue_scan' ); delete_option( Malsift_Storage::OPT_STATE ); Malsift_Firewall::set_uploads_block( false ); } /** * Schedule the daily scan for 03:00 site time. */ public static function schedule() { if ( ! wp_next_scheduled( 'malsift_daily_scan' ) ) { $when = new DateTime( 'tomorrow 03:00', wp_timezone() ); wp_schedule_event( $when->getTimestamp(), 'daily', 'malsift_daily_scan' ); } } /** * Cron: start the daily scan. */ public static function cron_start() { if ( Malsift_Scanner::running() ) { return; } $start = Malsift_Scanner::start( 'scheduled' ); if ( ! is_wp_error( $start ) ) { self::cron_continue(); } } /** * Cron: continue a scan in 20-second slices. */ public static function cron_continue() { $progress = Malsift_Scanner::run( 20 ); if ( ! is_wp_error( $progress ) && empty( $progress['done'] ) ) { wp_schedule_single_event( time() + 60, 'malsift_continue_scan' ); } } /** * Block the theme/plugin code editor when locked. * * @param array $caps Primitive caps. * @param string $cap Meta cap. * @return array */ public static function lock_editor( $caps, $cap ) { if ( in_array( $cap, array( 'edit_themes', 'edit_plugins', 'edit_files' ), true ) && Malsift_Storage::settings()['lock_editor'] ) { $caps[] = 'do_not_allow'; } return $caps; } /** * Admin menu. */ public static function menu() { $cap = self::cap(); add_menu_page( __( 'Malsift', 'malsift' ), __( 'Malsift', 'malsift' ), $cap, 'malsift', array( __CLASS__, 'render' ), 'dashicons-shield', 80 ); add_submenu_page( 'malsift', __( 'Malsift Dashboard', 'malsift' ), __( 'Dashboard', 'malsift' ), $cap, 'malsift', array( __CLASS__, 'render' ) ); add_submenu_page( 'malsift', __( 'Malsift Findings', 'malsift' ), __( 'Findings', 'malsift' ), $cap, 'malsift-findings', array( __CLASS__, 'render' ) ); add_submenu_page( 'malsift', __( 'Malsift Firewall', 'malsift' ), __( 'Firewall', 'malsift' ), $cap, 'malsift-firewall', array( __CLASS__, 'render' ) ); add_submenu_page( 'malsift', __( 'Malsift Reinstall', 'malsift' ), __( 'Reinstall', 'malsift' ), $cap, 'malsift-reinstall', array( __CLASS__, 'render' ) ); add_submenu_page( 'malsift', __( 'Malsift Quarantine', 'malsift' ), __( 'Quarantine', 'malsift' ), $cap, 'malsift-quarantine', array( __CLASS__, 'render' ) ); add_submenu_page( 'malsift', __( 'Malsift Scan History', 'malsift' ), __( 'Scan history', 'malsift' ), $cap, 'malsift-history', array( __CLASS__, 'render' ) ); add_submenu_page( 'malsift', __( 'Malsift Settings', 'malsift' ), __( 'Settings', 'malsift' ), $cap, 'malsift-settings', array( __CLASS__, 'render' ) ); /** * Fires after Malsift's admin menu is registered, so add-ons can add screens. * * @param string $cap Capability required for Malsift screens. */ do_action( 'malsift_admin_menu', $cap ); } /** * Scripts and styles on Malsift screens only. * * @param string $hook Screen hook. */ public static function assets( $hook ) { if ( false === strpos( $hook, 'malsift' ) ) { return; } // The file time is part of the version, so browsers and caches pick up changed files. $ver = function ( $file ) { $time = @filemtime( MALSIFT_DIR . $file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- missing file falls back to the plugin version. return MALSIFT_VERSION . ( $time ? '.' . $time : '' ); }; wp_enqueue_style( 'malsift-admin', MALSIFT_URL . 'assets/admin.css', array(), $ver( 'assets/admin.css' ) ); wp_enqueue_script( 'malsift-admin', MALSIFT_URL . 'assets/admin.js', array(), $ver( 'assets/admin.js' ), true ); wp_localize_script( 'malsift-admin', 'malsiftAdmin', array( 'ajax' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'malsift' ), 'i18n' => array( 'starting' => __( 'Starting scan…', 'malsift' ), 'error' => __( 'Something went wrong:', 'malsift' ), 'fixAll' => __( 'Apply the safe automatic fix to every open threat? Replaced and removed files are kept in quarantine.', 'malsift' ), 'working' => __( 'Working…', 'malsift' ), 'purge' => __( 'Delete these files permanently? This cannot be undone.', 'malsift' ), 'reinstall' => __( 'Reinstall the selected items from WordPress.org? Changed files are backed up to quarantine first. Keep this page open until it finishes.', 'malsift' ), 'reinstalling' => __( 'Reinstalling…', 'malsift' ), 'done' => __( 'Done', 'malsift' ), 'failed' => __( 'Failed', 'malsift' ), 'rescan' => __( 'Reinstall finished. Run a new scan to confirm the site is clean.', 'malsift' ), ), ) ); } /** * Verify AJAX request. */ private static function check_ajax() { check_ajax_referer( 'malsift', 'nonce' ); if ( ! current_user_can( self::cap() ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'malsift' ) ), 403 ); } } /** * AJAX: start/step a scan. */ public static function ajax_scan() { self::check_ajax(); $op = isset( $_POST['op'] ) ? sanitize_key( wp_unslash( $_POST['op'] ) ) : 'step'; $progress = 'start' === $op ? Malsift_Scanner::start( 'manual' ) : Malsift_Scanner::run( 8 ); if ( is_wp_error( $progress ) ) { wp_send_json_error( array( 'message' => $progress->get_error_message() ) ); } wp_send_json_success( $progress ); } /** * AJAX: run an action on a finding. */ public static function ajax_action() { self::check_ajax(); $id = isset( $_POST['id'] ) ? sanitize_key( wp_unslash( $_POST['id'] ) ) : ''; $action = isset( $_POST['act'] ) ? sanitize_key( wp_unslash( $_POST['act'] ) ) : ''; $r = Malsift_Actions::run( $id, $action ); if ( is_wp_error( $r ) ) { wp_send_json_error( array( 'message' => $r->get_error_message() ) ); } wp_send_json_success( $r ); } /** * AJAX: fix all threats. */ public static function ajax_fix_all() { self::check_ajax(); $r = Malsift_Actions::fix_all(); /* translators: 1: fixed, 2: failed, 3: needing manual review. */ $msg = sprintf( __( 'Fixed %1$d, failed %2$d, %3$d need manual review. Run a new scan to confirm the site is clean.', 'malsift' ), $r['fixed'], $r['failed'], $r['skipped'] ); wp_send_json_success( array( 'message' => trim( $msg . ' ' . implode( ' ', $r['messages'] ) ) ) ); } /** * AJAX: restore or purge quarantine items. */ public static function ajax_quarantine() { self::check_ajax(); $group = isset( $_POST['group'] ) ? sanitize_key( wp_unslash( $_POST['group'] ) ) : ''; $op = isset( $_POST['op'] ) ? sanitize_key( wp_unslash( $_POST['op'] ) ) : ''; $r = 'restore' === $op ? Malsift_Quarantine::restore( $group ) : Malsift_Quarantine::purge( $group ); if ( is_wp_error( $r ) ) { wp_send_json_error( array( 'message' => $r->get_error_message() ) ); } wp_send_json_success( array( 'message' => 'restore' === $op ? __( 'Restored to the original location.', 'malsift' ) : __( 'Deleted permanently.', 'malsift' ) ) ); } /** * AJAX: reinstall one item. */ public static function ajax_reinstall() { self::check_ajax(); $type = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( $_POST['type'] ) ) : ''; $key = isset( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : ''; $r = Malsift_Reinstall::run( $type, $key ); if ( is_wp_error( $r ) ) { wp_send_json_error( array( 'message' => $r->get_error_message() ) ); } wp_send_json_success( array( 'message' => $r ) ); } /** * Save settings. */ public static function save_settings() { if ( ! current_user_can( self::cap() ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'malsift' ) ); } check_admin_referer( 'malsift_settings' ); $email = isset( $_POST['alert_email'] ) ? sanitize_email( wp_unslash( $_POST['alert_email'] ) ) : ''; $max = isset( $_POST['max_file_mb'] ) ? absint( $_POST['max_file_mb'] ) : 8; Malsift_Storage::update_settings( array( 'daily_scan' => empty( $_POST['daily_scan'] ) ? 0 : 1, 'email_alerts' => empty( $_POST['email_alerts'] ) ? 0 : 1, 'alert_email' => $email ? $email : get_option( 'admin_email' ), 'lock_editor' => empty( $_POST['lock_editor'] ) ? 0 : 1, 'max_file_mb' => min( 64, max( 1, $max ) ), 'delete_on_uninstall' => empty( $_POST['delete_on_uninstall'] ) ? 0 : 1, 'spam_delete_permanently' => empty( $_POST['spam_delete_permanently'] ) ? 0 : 1, ) ); if ( Malsift_Storage::settings()['daily_scan'] ) { self::schedule(); } else { wp_clear_scheduled_hook( 'malsift_daily_scan' ); } wp_safe_redirect( add_query_arg( 'saved', '1', self::page_url( 'malsift-settings' ) ) ); exit; } /* ------------------------------------------------------------------ */ /* Rendering */ /* ------------------------------------------------------------------ */ /** * Render the current screen. */ public static function render() { if ( ! current_user_can( self::cap() ) ) { return; } $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : 'malsift'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $result = get_option( Malsift_Storage::OPT_RESULT ); $result = is_array( $result ) ? $result : null; echo '
'; self::rescue_banner( $result ); self::header( $result, $page ); echo ''; switch ( $page ) { case 'malsift-findings': self::page_findings( $result ); break; case 'malsift-reinstall': self::page_reinstall( $result ); break; case 'malsift-quarantine': self::page_quarantine(); break; case 'malsift-history': self::page_history(); break; case 'malsift-settings': self::page_settings(); break; case 'malsift-firewall': self::page_firewall(); break; case 'malsift': self::page_dashboard( $result ); break; default: /** * Render an add-on screen. Return true when the page was rendered. * * @param bool $done Rendered. * @param string $page Page slug. * @param array $result Last scan result or null. */ if ( ! apply_filters( 'malsift_render_page', false, $page, $result ) ) { self::page_dashboard( $result ); } } echo '
'; } /** * Tabs shown at the top of every Malsift screen. Add-ons add theirs with the malsift_tabs filter. * * @param array|null $result Last result. * @return array slug => [label, order, count, count_class, url] */ public static function tabs( $result = null ) { $open = 0; $threats = 0; if ( is_array( $result ) ) { $open = (int) $result['counts']['threat'] + (int) $result['counts']['warning']; $threats = (int) $result['counts']['threat']; } $tabs = array( 'malsift' => array( __( 'Overview', 'malsift' ), 10 ), 'malsift-findings' => array( __( 'Findings', 'malsift' ), 20, $open, $threats ? 'is-bad' : '' ), 'malsift-firewall' => array( __( 'Firewall', 'malsift' ), 30 ), 'malsift-reinstall' => array( __( 'Reinstall', 'malsift' ), 80 ), 'malsift-quarantine' => array( __( 'Quarantine', 'malsift' ), 85 ), 'malsift-history' => array( __( 'History', 'malsift' ), 90 ), 'malsift-settings' => array( __( 'Settings', 'malsift' ), 95 ), ); /** * Filter the Malsift tabs. Each tab is [label, order, count, count class, url]. * * @param array $tabs Tabs keyed by page slug. * @param array|null $result Last scan result. */ $tabs = apply_filters( 'malsift_tabs', $tabs, $result ); uasort( $tabs, function ( $a, $b ) { return ( isset( $a[1] ) ? (int) $a[1] : 50 ) - ( isset( $b[1] ) ? (int) $b[1] : 50 ); } ); return $tabs; } /** * Hide Malsift's sub-menu items in the WordPress menu: the tabs replace them. */ public static function menu_style() { if ( current_user_can( self::cap() ) ) { echo ''; } } /** * Header with title, buttons and tabs. * * @param array|null $result Last result. * @param string $page Current page. */ private static function header( $result, $page = 'malsift' ) { $running = Malsift_Scanner::running(); /** * Short badge next to the Malsift name (for example "PRO"). * * @param string $badge Badge text. */ $badge = (string) apply_filters( 'malsift_header_badge', '' ); ?>

'ok', 'warn' => 'warn', 'warning' => 'warn', 'bad' => 'bad', 'threat' => 'bad', 'info' => 'info', ); printf( '%s', esc_attr( isset( $map[ $status ] ) ? $map[ $status ] : 'info' ), esc_html( $label ) ); } /** * Action buttons for a finding. * * @param array $f Finding. */ private static function actions( $f ) { if ( ! empty( $f['resolved'] ) ) { self::chip( 'ok', __( 'Fixed', 'malsift' ) ); return; } if ( ! empty( $f['ignored'] ) ) { printf( '', esc_attr( $f['id'] ), esc_html__( 'Stop ignoring', 'malsift' ) ); return; } $first = true; foreach ( $f['actions'] as $action ) { if ( 'link' === $action ) { if ( ! empty( $f['meta']['link'] ) ) { printf( '%s', esc_url( $f['meta']['link'] ), esc_html( Malsift_Actions::label( $action, $f ) ) ); } continue; } $class = 'ignore' === $action ? 'malsift-a malsift-a-g' : ( $first ? 'malsift-a malsift-a-p' : 'malsift-a' ); printf( '', esc_attr( $class ), esc_attr( $f['id'] ), esc_attr( $action ), esc_attr( Malsift_Actions::confirm_text( $action, $f ) ), esc_html( Malsift_Actions::label( $action, $f ) ) ); if ( 'ignore' !== $action ) { $first = false; } } } /** * Target line for a finding. * * @param array $f Finding. * @return string */ private static function target_text( $f ) { if ( in_array( $f['type'], array( 'file', 'dir' ), true ) ) { return $f['target'] . ( 'dir' === $f['type'] ? '/' : '' ); } return ''; } /** * A finding: one line that opens to show the explanation and the fixes. * * @param array $f Finding. * @param bool $open Open by default. */ private static function finding_card( $f, $open = false ) { $target = self::target_text( $f ); $sub = $target ? $target : strtok( (string) $f['detail'], "\n" ); $labels = array( 'threat' => array( 'bad', __( 'Threat', 'malsift' ) ), 'warning' => array( 'warn', __( 'Review', 'malsift' ) ), 'info' => array( 'info', __( 'Info', 'malsift' ) ), ); $sev = isset( $labels[ $f['severity'] ] ) ? $labels[ $f['severity'] ] : $labels['info']; if ( ! empty( $f['resolved'] ) ) { $sev = array( 'ok', __( 'Fixed', 'malsift' ) ); } elseif ( ! empty( $f['ignored'] ) ) { $sev = array( 'info', __( 'Known', 'malsift' ) ); } ?>
>

BLACKSPACE LABS · SITE RESCUE

getOffset( new DateTime() ) ) / DAY_IN_SECONDS ); $off = (int) wp_timezone()->getOffset( new DateTime() ); foreach ( Malsift_Firewall::log() as $e ) { if ( ! in_array( $e['action'], array( 'blocked', 'locked' ), true ) ) { continue; } $i = $days - 1 - ( $today - (int) floor( ( $e['time'] + $off ) / DAY_IN_SECONDS ) ); if ( $i >= 0 && $i < $days ) { $out[ $i ] += max( 1, (int) $e['count'] ); } } return $out; } /** * Small bar chart. * * @param int[] $values Values. * @param string $label Accessible label. * @return string SVG. */ private static function sparkline( $values, $label ) { $max = max( 1, max( $values ) ); $n = count( $values ); $w = 13; $gap = 4; $h = 40; $svg = ''; foreach ( $values as $i => $v ) { $bh = max( 3, (int) round( $h * $v / $max ) ); $svg .= ''; } return $svg . ''; } /** * Protection items for the dashboard. Add-ons add theirs with the malsift_protection_items filter. * * @return array[] */ public static function protection_items() { $fw = Malsift_Firewall::settings(); $s = Malsift_Storage::settings(); $stats = Malsift_Firewall::stats( 7 ); $modes = array( 'off' => array( 'bad', __( 'Off', 'malsift' ) ), 'learning' => array( 'warn', __( 'Learning', 'malsift' ) ), 'protect' => array( 'ok', __( 'Protecting', 'malsift' ) ), ); $ht = Malsift_Firewall::server_supports_htaccess(); $items = array( 'firewall' => array( 'title' => __( 'Firewall', 'malsift' ), /* translators: %s: number of blocked attacks. */ 'detail' => sprintf( __( '%s blocked this week', 'malsift' ), number_format_i18n( $stats['blocked'] ) ), 'chip' => $modes[ $fw['mode'] ], 'link' => self::page_url( 'malsift-firewall' ), ), 'login_limit' => array( 'title' => __( 'Login limits', 'malsift' ), /* translators: 1: attempts, 2: minutes. */ 'detail' => sprintf( __( '%1$d tries, then a %2$d-minute lockout', 'malsift' ), $fw['login_attempts'], $fw['lockout_minutes'] ), 'toggle' => (bool) $fw['login_limit'], ), 'block_enum' => array( 'title' => __( 'Hide usernames', 'malsift' ), 'detail' => __( 'Stops visitors from listing user accounts', 'malsift' ), 'toggle' => (bool) $fw['block_enum'], ), 'uploads_php' => array( 'title' => __( 'Block PHP in uploads', 'malsift' ), 'detail' => $ht ? __( 'Uploaded webshells cannot run', 'malsift' ) : __( 'Ask your host (server does not read .htaccess)', 'malsift' ), 'toggle' => $ht && $fw['uploads_php'], 'disabled' => ! $ht, ), 'lock_editor' => array( 'title' => __( 'Lock the code editor', 'malsift' ), 'detail' => __( 'No PHP edits from the dashboard', 'malsift' ), 'toggle' => (bool) $s['lock_editor'], ), 'daily_scan' => array( 'title' => __( 'Daily scan', 'malsift' ), 'detail' => __( 'Every day at 03:00 site time', 'malsift' ), 'toggle' => (bool) $s['daily_scan'], ), ); /** * Filter the protection items on the dashboard. An item with "toggle" shows a switch * that posts to admin-post.php with "post_action" (default malsift_toggle) and "key". * * @param array $items Items keyed by name. */ return apply_filters( 'malsift_protection_items', $items ); } /** * Render one protection item. * * @param string $key Key. * @param array $item Item. */ private static function protection_row( $key, $item ) { echo '
' . esc_html( $item['title'] ) . '' . esc_html( $item['detail'] ) . '
'; if ( isset( $item['toggle'] ) ) { $action = isset( $item['post_action'] ) ? $item['post_action'] : 'malsift_toggle'; echo '
'; wp_nonce_field( $action . '_' . $key ); echo ''; printf( '', $item['toggle'] ? 'true' : 'false', esc_attr( $item['title'] ), ! empty( $item['disabled'] ) ? ' disabled' : '' ); echo '
'; } elseif ( ! empty( $item['chip'] ) ) { echo ! empty( $item['link'] ) ? '' : ''; self::chip( $item['chip'][0], $item['chip'][1] ); echo ! empty( $item['link'] ) ? '' : ''; } elseif ( ! empty( $item['link'] ) ) { echo '' . esc_html( isset( $item['link_label'] ) ? $item['link_label'] : __( 'Set up', 'malsift' ) ) . ''; } echo '
'; } /** * Recent activity: scans and firewall events. * * @return array[] [time, chip status, chip label, text] */ private static function activity() { $labels = Malsift_Firewall::labels(); $items = array(); foreach ( array_slice( Malsift_Firewall::log(), 0, 6 ) as $e ) { $names = array( 'blocked' => array( 'bad', __( 'Blocked', 'malsift' ) ), 'logged' => array( 'warn', __( 'Logged', 'malsift' ) ), 'locked' => array( 'warn', __( 'Locked out', 'malsift' ) ), ); $n = isset( $names[ $e['action'] ] ) ? $names[ $e['action'] ] : $names['logged']; $text = sprintf( /* translators: 1: attack type, 2: IP address. */ __( '%1$s from %2$s', 'malsift' ), isset( $labels[ $e['rule'] ] ) ? $labels[ $e['rule'] ] : $e['rule'], $e['ip'] ); $items[] = array( (int) $e['time'], $n[0], $n[1], $text . ( $e['count'] > 1 ? ' · ' . sprintf( /* translators: %d: count. */ _n( '%d time', '%d times', $e['count'], 'malsift' ), $e['count'] ) : '' ) ); } $history = get_option( Malsift_Storage::OPT_HISTORY, array() ); foreach ( array_slice( (array) $history, 0, 4 ) as $h ) { if ( $h['threats'] ) { /* translators: %d: count. */ $text = sprintf( _n( 'Scan finished — %d threat found', 'Scan finished — %d threats found', $h['threats'], 'malsift' ), $h['threats'] ); } elseif ( $h['warnings'] ) { /* translators: %d: count. */ $text = sprintf( _n( 'Scan finished — no malware, %d item to review', 'Scan finished — no malware, %d items to review', $h['warnings'], 'malsift' ), $h['warnings'] ); } else { $text = __( 'Scan finished — clean', 'malsift' ); } $items[] = array( (int) $h['time'], $h['threats'] ? 'bad' : 'ok', __( 'Scan', 'malsift' ), $text ); } /** * Filter the dashboard activity list. Each item is [time, chip status, chip label, text]. * * @param array $items Items. */ $items = apply_filters( 'malsift_activity', $items ); usort( $items, function ( $a, $b ) { return $b[0] - $a[0]; } ); return array_slice( $items, 0, 6 ); } /** * Dashboard. * * @param array|null $result Last result. */ private static function page_dashboard( $result ) { if ( ! $result ) { ?>

+

6 ) : ?>

' . esc_html__( 'Nothing needs your attention.', 'malsift' ) . '

'; } foreach ( array_slice( $open, 0, 6 ) as $f ) { self::finding_card( $f ); } ?>

$item ) { self::protection_row( $key, $item ); } ?>

__( 'Files', 'malsift' ), 'access' => __( 'Access', 'malsift' ), 'database' => __( 'Database', 'malsift' ), 'hardening' => __( 'Hardening', 'malsift' ), ); $current = ''; foreach ( Malsift_Scanner::checks() as $key => $def ) { if ( $def[0] !== $current ) { $current = $def[0]; echo '
' . esc_html( $groups[ $current ] ) . '
'; } $check = isset( $result['checks'][ $key ] ) ? $result['checks'][ $key ] : array( 'status' => 'info', 'label' => __( 'Not checked yet', 'malsift' ), ); echo '
' . esc_html( $def[1] ) . '' . esc_html( $def[2] ) . '
'; self::chip( $check['status'], $check['label'] ); echo '
'; } ?>
' . esc_html__( 'No scans yet.', 'malsift' ) . '

'; return; } echo ''; foreach ( array_slice( $history, 0, $limit ) as $h ) { echo ''; } echo '
' . esc_html( wp_date( 'j M, H:i', $h['time'] ) ) . '' . esc_html( 'scheduled' === $h['trigger'] ? __( 'Daily scan', 'malsift' ) : __( 'Full scan', 'malsift' ) ) . ''; if ( $h['threats'] ) { /* translators: %d: count. */ self::chip( 'bad', sprintf( _n( '%d threat', '%d threats', $h['threats'], 'malsift' ), $h['threats'] ) ); } elseif ( $h['warnings'] ) { /* translators: %d: count. */ self::chip( 'warn', sprintf( _n( '%d warning', '%d warnings', $h['warnings'], 'malsift' ), $h['warnings'] ) ); } else { self::chip( 'ok', __( 'Clean', 'malsift' ) ); } echo '
'; } /** * All findings. * * @param array|null $result Last result. */ private static function page_findings( $result ) { if ( ! $result ) { echo '

' . esc_html__( 'No findings yet', 'malsift' ) . '

' . esc_html__( 'Run a scan to see what Malsift finds.', 'malsift' ) . '

'; return; } $filter = isset( $_GET['show'] ) ? sanitize_key( wp_unslash( $_GET['show'] ) ) : 'open'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $match = array( 'open' => function ( $f ) { return empty( $f['resolved'] ) && empty( $f['ignored'] ) && 'info' !== $f['severity']; }, 'threat' => function ( $f ) { return empty( $f['resolved'] ) && empty( $f['ignored'] ) && 'threat' === $f['severity']; }, 'warning' => function ( $f ) { return empty( $f['resolved'] ) && empty( $f['ignored'] ) && 'warning' === $f['severity']; }, 'resolved' => function ( $f ) { return ! empty( $f['resolved'] ); }, 'ignored' => function ( $f ) { return ! empty( $f['ignored'] ) && empty( $f['resolved'] ); }, 'info' => function ( $f ) { return 'info' === $f['severity'] && empty( $f['resolved'] ); }, ); $names = array( 'open' => __( 'Open', 'malsift' ), 'threat' => __( 'Threats', 'malsift' ), 'warning' => __( 'To review', 'malsift' ), 'resolved' => __( 'Fixed', 'malsift' ), 'ignored' => __( 'Known', 'malsift' ), 'info' => __( 'Info', 'malsift' ), ); if ( ! isset( $match[ $filter ] ) ) { $filter = 'open'; } ?>
__( 'Files', 'malsift' ), 'access' => __( 'Access', 'malsift' ), 'database' => __( 'Database', 'malsift' ), 'hardening' => __( 'Settings and updates', 'malsift' ), ); $checks = Malsift_Scanner::checks(); $by = array(); foreach ( array_filter( $result['findings'], $match[ $filter ] ) as $f ) { $g = isset( $checks[ $f['check'] ] ) ? $checks[ $f['check'] ][0] : 'files'; $by[ $g ][] = $f; } if ( ! $by ) { echo '

' . esc_html__( 'Nothing here.', 'malsift' ) . '

'; } $first = true; foreach ( $groups as $g => $title ) { if ( empty( $by[ $g ] ) ) { continue; } echo '

' . esc_html( $title ) . ' ' . esc_html( sprintf( /* translators: %d: items. */ _n( '%d item', '%d items', count( $by[ $g ] ), 'malsift' ), count( $by[ $g ] ) ) ) . '

'; foreach ( $by[ $g ] as $f ) { self::finding_card( $f, $first && 'open' === $filter ); $first = false; } echo '
'; } if ( ! empty( $result['notes'] ) && 'info' === $filter ) { echo '

' . esc_html__( 'Scan notes', 'malsift' ) . '

'; } } /** * Reinstall screen. * * @param array|null $result Last result. */ private static function page_reinstall( $result ) { $blocked = Malsift_Reinstall::blocked(); $items = Malsift_Reinstall::items(); $issues = array(); if ( $result ) { foreach ( $result['findings'] as $f ) { if ( in_array( $f['type'], array( 'file', 'dir' ), true ) && empty( $f['resolved'] ) && empty( $f['ignored'] ) && 'info' !== $f['severity'] ) { foreach ( $items as $id => $item ) { $in = 'core' === $item['type'] ? ( 0 === strpos( $f['target'], 'wp-admin/' ) || 0 === strpos( $f['target'], 'wp-includes/' ) || false === strpos( $f['target'], '/' ) ) : Malsift_Storage::in( $f['target'], $item['rel'] ); if ( $in ) { $issues[ $id ] = isset( $issues[ $id ] ) ? $issues[ $id ] + 1 : 1; break; } } } } } if ( $blocked ) { echo '

' . esc_html__( 'File changes are disabled on this site (DISALLOW_FILE_MODS in wp-config.php), so reinstalling is not possible. Remove the setting temporarily, reinstall, then add it back.', 'malsift' ) . '

'; } $groups = array( 'core' => __( 'WordPress core', 'malsift' ), 'plugin' => __( 'Plugins', 'malsift' ), 'theme' => __( 'Themes', 'malsift' ), ); ?>

$item ) { if ( $item['type'] !== $current ) { $current = $item['type']; printf( '', esc_html( $groups[ $current ] ) ); } $can = $item['available'] && ! $blocked; echo ''; } ?>
>
%s
'; if ( $can ) { printf( '', esc_attr( $item['type'] ), esc_attr( $item['key'] ), esc_attr( $item['name'] ) ); } echo '' . esc_html( $item['name'] ); if ( $item['note'] ) { echo '
' . esc_html( $item['note'] ) . '
'; } echo '
' . esc_html( $item['version'] ) . ''; if ( ! $result ) { echo '—'; } elseif ( isset( $issues[ $id ] ) ) { /* translators: %d: number of files. */ self::chip( 'bad', sprintf( _n( '%d file flagged', '%d files flagged', $issues[ $id ], 'malsift' ), $issues[ $id ] ) ); } else { self::chip( 'ok', __( 'Clean', 'malsift' ) ); } echo '' . ( $can ? '' : esc_html__( 'Not available', 'malsift' ) ) . '

' . esc_html__( 'Quarantine', 'malsift' ) . ' ' . esc_html__( 'Files are stored compressed in a protected folder and cannot run.', 'malsift' ) . '

'; if ( ! $groups ) { echo '

' . esc_html__( 'Quarantine is empty.', 'malsift' ) . '

'; return; } echo ''; foreach ( $groups as $id => $g ) { printf( '', esc_html( $g['label'] ), $g['count'] > 1 ? ' ' . esc_html( sprintf( /* translators: %d: files. */ _n( '%d file', '%d files', $g['count'], 'malsift' ), $g['count'] ) ) . '' : '', esc_html( $g['reason'] ), esc_html( wp_date( 'j M Y, H:i', $g['time'] ) ), esc_html( size_format( $g['size'] ) ), esc_attr( $id ), esc_html__( 'Restore', 'malsift' ), esc_attr( $id ), esc_html__( 'Delete permanently', 'malsift' ) ); } echo '
' . esc_html__( 'File or folder', 'malsift' ) . '' . esc_html__( 'Reason', 'malsift' ) . '' . esc_html__( 'Date', 'malsift' ) . '' . esc_html__( 'Size', 'malsift' ) . '
%s%s%s%s%s
'; } /** * History screen. */ private static function page_history() { echo '

' . esc_html__( 'Scan history', 'malsift' ) . ' ' . esc_html__( 'Last 30 scans', 'malsift' ) . '

'; self::history_table( 30 ); echo '
'; } /** * Settings screen. */ private static function page_settings() { $s = Malsift_Storage::settings(); if ( isset( $_GET['saved'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended echo '

' . esc_html__( 'Settings saved.', 'malsift' ) . '

'; } $next = wp_next_scheduled( 'malsift_daily_scan' ); ?>

$on ) ); break; case 'uploads_php': $on = $on && Malsift_Firewall::server_supports_htaccess() ? 1 : 0; Malsift_Firewall::update( array( 'uploads_php' => $on ) ); Malsift_Firewall::set_uploads_block( (bool) $on ); break; case 'lock_editor': Malsift_Storage::update_settings( array( 'lock_editor' => $on ) ); break; case 'daily_scan': Malsift_Storage::update_settings( array( 'daily_scan' => $on ) ); if ( $on ) { self::schedule(); } else { wp_clear_scheduled_hook( 'malsift_daily_scan' ); } break; } wp_safe_redirect( self::page_url() ); exit; } /** * Firewall mode buttons. */ public static function firewall_mode() { if ( ! current_user_can( self::cap() ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'malsift' ) ); } check_admin_referer( 'malsift_fw_mode' ); $mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : ''; if ( in_array( $mode, array( 'off', 'learning', 'protect' ), true ) ) { Malsift_Firewall::update( array( 'mode' => $mode ) ); } wp_safe_redirect( add_query_arg( 'saved', '1', self::page_url( 'malsift-firewall' ) ) ); exit; } /** * Save firewall settings. */ public static function save_firewall() { if ( ! current_user_can( self::cap() ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'malsift' ) ); } check_admin_referer( 'malsift_firewall' ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitised by the helpers below. $mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : 'learning'; $source = isset( $_POST['ip_source'] ) ? sanitize_key( wp_unslash( $_POST['ip_source'] ) ) : 'auto'; $xmlrpc = isset( $_POST['xmlrpc'] ) ? sanitize_key( wp_unslash( $_POST['xmlrpc'] ) ) : 'no_multicall'; $enabled = isset( $_POST['rules'] ) ? array_map( 'sanitize_key', (array) wp_unslash( $_POST['rules'] ) ) : array(); $allow = isset( $_POST['allow_ips'] ) ? Malsift_Firewall::parse_ip_list( wp_unslash( $_POST['allow_ips'] ) ) : array(); $block = isset( $_POST['block_ips'] ) ? Malsift_Firewall::parse_ip_list( wp_unslash( $_POST['block_ips'] ) ) : array(); // phpcs:enable $me = Malsift_Firewall::client_ip(); $block = array_values( array_filter( $block, function ( $entry ) use ( $me ) { return ! Malsift_Firewall::ip_listed( $me, array( $entry ) ); } ) ); $disabled = array_values( array_diff( array_keys( Malsift_Firewall::rules() ), $enabled ) ); $uploads = ! empty( $_POST['uploads_php'] ) && Malsift_Firewall::server_supports_htaccess(); Malsift_Firewall::update( array( 'mode' => in_array( $mode, array( 'off', 'learning', 'protect' ), true ) ? $mode : 'learning', 'ip_source' => in_array( $source, array( 'auto', 'remote_addr', 'cloudflare', 'x_forwarded_for', 'x_real_ip' ), true ) ? $source : 'auto', 'xmlrpc' => in_array( $xmlrpc, array( 'allow', 'no_multicall', 'off' ), true ) ? $xmlrpc : 'no_multicall', 'disabled_rules' => $disabled, 'allow_ips' => $allow, 'block_ips' => $block, 'login_limit' => empty( $_POST['login_limit'] ) ? 0 : 1, 'login_attempts' => isset( $_POST['login_attempts'] ) ? min( 50, max( 2, absint( $_POST['login_attempts'] ) ) ) : 5, 'login_window' => isset( $_POST['login_window'] ) ? min( 1440, max( 1, absint( $_POST['login_window'] ) ) ) : 15, 'lockout_minutes' => isset( $_POST['lockout_minutes'] ) ? min( 10080, max( 1, absint( $_POST['lockout_minutes'] ) ) ) : 30, 'generic_errors' => empty( $_POST['generic_errors'] ) ? 0 : 1, 'block_enum' => empty( $_POST['block_enum'] ) ? 0 : 1, 'uploads_php' => $uploads ? 1 : 0, ) ); Malsift_Firewall::set_uploads_block( $uploads ); wp_safe_redirect( add_query_arg( 'saved', '1', self::page_url( 'malsift-firewall' ) ) ); exit; } /** * Quick actions from the firewall log. */ public static function firewall_event() { if ( ! current_user_can( self::cap() ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'malsift' ) ); } check_admin_referer( 'malsift_fw_event' ); $op = isset( $_GET['op'] ) ? sanitize_key( wp_unslash( $_GET['op'] ) ) : ''; $val = isset( $_GET['val'] ) ? sanitize_text_field( wp_unslash( $_GET['val'] ) ) : ''; $s = Malsift_Firewall::settings(); $msg = ''; if ( 'allow_rule' === $op && isset( Malsift_Firewall::rules()[ $val ] ) ) { Malsift_Firewall::update( array( 'disabled_rules' => array_values( array_unique( array_merge( $s['disabled_rules'], array( $val ) ) ) ) ) ); $msg = 'rule'; } elseif ( 'block_ip' === $op && filter_var( $val, FILTER_VALIDATE_IP ) && $val !== Malsift_Firewall::client_ip() ) { Malsift_Firewall::update( array( 'block_ips' => array_values( array_unique( array_merge( $s['block_ips'], array( $val ) ) ) ) ) ); $msg = 'blocked'; } elseif ( 'allow_ip' === $op && filter_var( $val, FILTER_VALIDATE_IP ) ) { Malsift_Firewall::update( array( 'allow_ips' => array_values( array_unique( array_merge( $s['allow_ips'], array( $val ) ) ) ) ) ); Malsift_Firewall::unlock( $val ); $msg = 'allowed'; } elseif ( 'clear' === $op ) { Malsift_Firewall::clear_log(); $msg = 'cleared'; } wp_safe_redirect( add_query_arg( 'done', $msg, self::page_url( 'malsift-firewall' ) ) ); exit; } /** * Link for a firewall log action. * * @param string $op Operation. * @param string $val Value. * @return string */ private static function fw_link( $op, $val = '' ) { return wp_nonce_url( add_query_arg( array( 'action' => 'malsift_fw_event', 'op' => $op, 'val' => rawurlencode( $val ), ), admin_url( 'admin-post.php' ) ), 'malsift_fw_event' ); } /** * Firewall screen. */ private static function page_firewall() { $s = Malsift_Firewall::settings(); $labels = Malsift_Firewall::labels(); $stats = Malsift_Firewall::stats( 7 ); $log = Malsift_Firewall::log(); $me = Malsift_Firewall::client_ip(); $headers = Malsift_Firewall::proxy_headers(); $htaccess = Malsift_Firewall::server_supports_htaccess(); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- display only. if ( isset( $_GET['saved'] ) ) { echo '

' . esc_html__( 'Firewall settings saved.', 'malsift' ) . '

'; } $done = isset( $_GET['done'] ) ? sanitize_key( wp_unslash( $_GET['done'] ) ) : ''; // phpcs:enable $messages = array( 'rule' => __( 'That rule is now switched off. You can switch it back on below.', 'malsift' ), 'blocked' => __( 'IP address added to the block list.', 'malsift' ), 'allowed' => __( 'IP address added to the allow list and unlocked.', 'malsift' ), 'cleared' => __( 'Firewall log cleared.', 'malsift' ), ); if ( isset( $messages[ $done ] ) ) { echo '

' . esc_html( $messages[ $done ] ) . '

'; } $mode_text = array( 'off' => array( 'warn', __( 'Off', 'malsift' ), __( 'The firewall is off. Login protection and the other options below still work.', 'malsift' ) ), 'learning' => array( 'warn', __( 'Learning mode', 'malsift' ), __( 'Attacks are logged but not blocked. Check the log below for a few days; if nothing legitimate shows up, switch to Protect.', 'malsift' ) ), 'protect' => array( 'ok', __( 'Protecting', 'malsift' ), __( 'Requests that look like attacks are blocked. Logged-in editors and administrators are never blocked.', 'malsift' ) ), ); $m = $mode_text[ $s['mode'] ]; $colors = array( 'off' => '#9ca3af', 'learning' => '#d97706', 'protect' => '#059669', ); $blocks = self::daily_blocks( 7 ); $counts = array(); foreach ( $log as $e ) { if ( $e['time'] >= time() - WEEK_IN_SECONDS && 'login_lockout' !== $e['rule'] ) { $counts[ $e['rule'] ] = ( isset( $counts[ $e['rule'] ] ) ? $counts[ $e['rule'] ] : 0 ) + (int) $e['count']; } } arsort( $counts ); $top = key( $counts ); ?>

__( 'Protect', 'malsift' ), 'learning' => __( 'Learning', 'malsift' ), 'off' => __( 'Off', 'malsift' ), ) as $k => $label ) { printf( '', esc_attr( $k ), $k === $s['mode'] ? 'true' : 'false', esc_html( $label ) ); } ?>

__( 'Blocked', 'malsift' ), 'logged' => __( 'Logged', 'malsift' ), 'locked' => __( 'Locked out', 'malsift' ), ); $all = isset( $_GET['all'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only. foreach ( array_slice( $log, 0, $all ? 200 : 15 ) as $e ) : $label = isset( $labels[ $e['rule'] ] ) ? $labels[ $e['rule'] ] : $e['rule']; ?>
1 ) : ?>
15 ) : ?>

<?php echo esc_html( sprintf( /* translators: %s: site host. */ __( 'Security scan report – %s', 'malsift' ), $site ) ); ?>

/100

$def ) : ?> 'info', 'label' => __( 'Not checked yet', 'malsift' ), ); ?>

', esc_attr( 'threat' === $f['severity'] ? 'bad' : 'warn' ), esc_html( 'threat' === $f['severity'] ? __( 'Threat', 'malsift' ) : __( 'Warning', 'malsift' ) ), esc_html( $f['title'] ), $target ? '
' . esc_html( $target ) . '' : '', esc_html( $status ) ); } if ( ! $any ) { echo ''; } ?>
%s%s%s%s
' . esc_html__( 'No issues found.', 'malsift' ) . '