Building Your Own WP Security Scanner – Complete Guide

Table of Contents

Security is paramount for WordPress websites. While commercial security scanners exist, building your own custom security scanner gives you complete control, deep insights into WordPress vulnerabilities, and the ability to tailor checks specifically to your infrastructure. This comprehensive guide will walk you through creating a professional-grade WordPress security scanner from scratch.

1. Security Scanner Overview

A comprehensive WordPress security scanner should identify vulnerabilities across multiple layers of your installation. Let’s examine the key components and threat categories:

🔍 Core Scanning

Validates WordPress core files against official checksums and detects unauthorized modifications.

🔌 Plugin Analysis

Identifies outdated, vulnerable, or abandoned plugins with known security issues.

🎨 Theme Auditing

Scans theme files for malicious code, backdoors, and security vulnerabilities.

🗄️ Database Security

Checks for SQL injection vectors, weak table prefixes, and unauthorized user accounts.

📁 File Permissions

Audits file and directory permissions to prevent unauthorized access.

🔐 Configuration Review

Analyzes wp-config.php and server configuration for security misconfigurations.

Vulnerability Type Severity Level Detection Method Common Impact
Outdated Core Critical Version comparison Site takeover, data breach
Vulnerable Plugins Critical CVE database check Remote code execution
Weak Passwords High Password strength analysis Unauthorized access
File Modifications High Checksum validation Malware injection
Debug Mode Enabled Medium Config file parsing Information disclosure
Directory Listing Medium HTTP response check File enumeration
Default Admin Username Low Database query Brute force vulnerability

2. Scanner Architecture

Our security scanner will follow a modular architecture with clear separation of concerns. This approach ensures maintainability, extensibility, and adherence to WordPress coding standards.

Architecture Components
  • Scanner Core: Main orchestration class managing scan execution
  • Check Modules: Individual vulnerability detection classes
  • Report Generator: Formats and outputs scan results
  • Database Handler: Manages scan history and results storage
  • API Integration: Communicates with vulnerability databases
  • Notification System: Alerts administrators of critical findings

2.1 Class Structure Diagram

The scanner follows object-oriented principles with the following class hierarchy:

PHP
<?php
/**
 * Main Security Scanner Class
 *
 * @package WP_Security_Scanner
 * @since   1.0.0
 */

namespace WP_Security_Scanner;

/**
 * Class Security_Scanner
 *
 * Core scanner orchestration class following WPCS standards.
 */
class Security_Scanner {

    /**
     * Scanner version.
     *
     * @var string
     */
    const VERSION = '1.0.0';

    /**
     * Check modules registry.
     *
     * @var array
     */
    private $check_modules = array();

    /**
     * Scan results.
     *
     * @var array
     */
    private $results = array();

    /**
     * Scanner configuration.
     *
     * @var array
     */
    private $config = array();

    /**
     * Constructor.
     *
     * @param array $config Scanner configuration options.
     */
    public function __construct( array $config = array() ) {
        $this->config = wp_parse_args(
            $config,
            $this->get_default_config()
        );

        $this->register_check_modules();
    }

    /**
     * Get default scanner configuration.
     *
     * @return array Default configuration values.
     */
    private function get_default_config() {
        return array(
            'enable_core_checks'     => true,
            'enable_plugin_checks'   => true,
            'enable_theme_checks'    => true,
            'enable_db_checks'       => true,
            'enable_file_checks'     => true,
            'scan_timeout'           => 300,
            'max_memory_limit'       => '512M',
            'enable_notifications'   => true,
            'notification_threshold' => 'high',
        );
    }

    /**
     * Register all security check modules.
     *
     * @return void
     */
    private function register_check_modules() {
        $modules = array(
            'core'        => new Checks\Core_Integrity_Check(),
            'plugins'     => new Checks\Plugin_Vulnerability_Check(),
            'themes'      => new Checks\Theme_Security_Check(),
            'database'    => new Checks\Database_Security_Check(),
            'files'       => new Checks\File_Permission_Check(),
            'config'      => new Checks\Configuration_Check(),
            'users'       => new Checks\User_Security_Check(),
            'malware'     => new Checks\Malware_Signature_Check(),
        );

        /**
         * Filter the registered security check modules.
         *
         * @param array $modules Array of check module instances.
         */
        $this->check_modules = apply_filters(
            'wp_security_scanner_modules',
            $modules
        );
    }

    /**
     * Execute security scan.
     *
     * @return array Scan results.
     */
    public function run_scan() {
        // Set execution limits.
        $this->set_execution_limits();

        // Clear previous results.
        $this->results = array(
            'scan_id'      => $this->generate_scan_id(),
            'timestamp'    => current_time( 'mysql' ),
            'version'      => self::VERSION,
            'site_url'     => get_site_url(),
            'checks'       => array(),
            'summary'      => array(),
            'total_issues' => 0,
        );

        // Execute each check module.
        foreach ( $this->check_modules as $module_name => $module ) {
            if ( $this->should_run_module( $module_name ) ) {
                $this->results['checks'][ $module_name ] = $module->execute();
            }
        }

        // Generate summary.
        $this->generate_summary();

        // Store results.
        $this->store_results();

        // Send notifications if needed.
        $this->send_notifications();

        return $this->results;
    }

    /**
     * Set execution limits for scan.
     *
     * @return void
     */
    private function set_execution_limits() {
        // Prevent timeout during scan.
        if ( ! ini_get( 'safe_mode' ) ) {
            set_time_limit( $this->config['scan_timeout'] );
        }

        // Increase memory limit if needed.
        $current_limit = wp_convert_hr_to_bytes( WP_MEMORY_LIMIT );
        $required_limit = wp_convert_hr_to_bytes( $this->config['max_memory_limit'] );

        if ( $current_limit < $required_limit ) {
            ini_set( 'memory_limit', $this->config['max_memory_limit'] );
        }
    }

    /**
     * Check if module should run based on configuration.
     *
     * @param string $module_name Module identifier.
     * @return bool Whether module should execute.
     */
    private function should_run_module( $module_name ) {
        $config_key = 'enable_' . $module_name . '_checks';
        return isset( $this->config[ $config_key ] ) 
            ? (bool) $this->config[ $config_key ] 
            : true;
    }

    /**
     * Generate unique scan identifier.
     *
     * @return string Unique scan ID.
     */
    private function generate_scan_id() {
        return wp_generate_password( 32, false, false );
    }

    /**
     * Generate scan summary from individual check results.
     *
     * @return void
     */
    private function generate_summary() {
        $severity_counts = array(
            'critical' => 0,
            'high'     => 0,
            'medium'   => 0,
            'low'      => 0,
            'info'     => 0,
        );

        foreach ( $this->results['checks'] as $check_results ) {
            if ( isset( $check_results['issues'] ) ) {
                foreach ( $check_results['issues'] as $issue ) {
                    $severity = strtolower( $issue['severity'] );
                    if ( isset( $severity_counts[ $severity ] ) ) {
                        $severity_counts[ $severity ]++;
                        $this->results['total_issues']++;
                    }
                }
            }
        }

        $this->results['summary'] = $severity_counts;
    }

    /**
     * Store scan results in database.
     *
     * @return bool Whether storage was successful.
     */
    private function store_results() {
        global $wpdb;

        $table_name = $wpdb->prefix . 'security_scan_results';

        $inserted = $wpdb->insert(
            $table_name,
            array(
                'scan_id'      => $this->results['scan_id'],
                'timestamp'    => $this->results['timestamp'],
                'results_data' => wp_json_encode( $this->results ),
                'total_issues' => $this->results['total_issues'],
            ),
            array( '%s', '%s', '%s', '%d' )
        );

        return false !== $inserted;
    }

    /**
     * Send notifications if critical issues detected.
     *
     * @return void
     */
    private function send_notifications() {
        if ( ! $this->config['enable_notifications'] ) {
            return;
        }

        $threshold = $this->config['notification_threshold'];
        $should_notify = false;

        switch ( $threshold ) {
            case 'critical':
                $should_notify = $this->results['summary']['critical'] > 0;
                break;
            case 'high':
                $should_notify = $this->results['summary']['critical'] > 0 
                    || $this->results['summary']['high'] > 0;
                break;
            case 'medium':
                $should_notify = $this->results['total_issues'] > 0;
                break;
        }

        if ( $should_notify ) {
            do_action(
                'wp_security_scanner_send_notification',
                $this->results
            );
        }
    }

    /**
     * Get scan results.
     *
     * @return array Current scan results.
     */
    public function get_results() {
        return $this->results;
    }
}

3. Building the Core Structure

The base check class provides a common interface for all security modules. Each specific check extends this abstract class:

PHP
<?php
/**
 * Abstract Security Check Class
 *
 * @package WP_Security_Scanner\Checks
 * @since   1.0.0
 */

namespace WP_Security_Scanner\Checks;

/**
 * Class Abstract_Security_Check
 *
 * Base class for all security check modules.
 */
abstract class Abstract_Security_Check {

    /**
     * Check identifier.
     *
     * @var string
     */
    protected $check_id;

    /**
     * Check name.
     *
     * @var string
     */
    protected $check_name;

    /**
     * Check description.
     *
     * @var string
     */
    protected $check_description;

    /**
     * Check results.
     *
     * @var array
     */
    protected $results = array();

    /**
     * Execute the security check.
     *
     * Must be implemented by child classes.
     *
     * @return array Check results.
     */
    abstract public function execute();

    /**
     * Add an issue to results.
     *
     * @param string $severity Issue severity (critical|high|medium|low|info).
     * @param string $title    Issue title.
     * @param string $description Issue description.
     * @param array  $metadata Additional metadata.
     * @return void
     */
    protected function add_issue( $severity, $title, $description, array $metadata = array() ) {
        $this->results['issues'][] = array(
            'severity'    => $severity,
            'title'       => sanitize_text_field( $title ),
            'description' => wp_kses_post( $description ),
            'metadata'    => $metadata,
            'timestamp'   => current_time( 'mysql' ),
        );
    }

    /**
     * Get check results.
     *
     * @return array Results array.
     */
    public function get_results() {
        return $this->results;
    }

    /**
     * Initialize results array.
     *
     * @return void
     */
    protected function init_results() {
        $this->results = array(
            'check_id'          => $this->check_id,
            'check_name'        => $this->check_name,
            'check_description' => $this->check_description,
            'status'            => 'pending',
            'issues'            => array(),
            'passed'            => 0,
            'failed'            => 0,
        );
    }

    /**
     * Mark check as complete.
     *
     * @param string $status Status (passed|failed|error).
     * @return void
     */
    protected function complete_check( $status = 'passed' ) {
        $this->results['status'] = $status;
        $this->results['completed_at'] = current_time( 'mysql' );
        $this->results['failed'] = count( $this->results['issues'] );
    }

    /**
     * Sanitize file path for output.
     *
     * @param string $path File path.
     * @return string Sanitized path.
     */
    protected function sanitize_file_path( $path ) {
        $path = str_replace( ABSPATH, '', $path );
        return sanitize_text_field( $path );
    }
}

4. Implementing Vulnerability Checks

4.1 Core Integrity Verification

Checking WordPress core file integrity ensures no unauthorized modifications have been made. This is crucial for detecting backdoors and malware:

PHP
<?php
/**
 * WordPress Core Integrity Check
 *
 * @package WP_Security_Scanner\Checks
 * @since   1.0.0
 */

namespace WP_Security_Scanner\Checks;

/**
 * Class Core_Integrity_Check
 *
 * Validates WordPress core files against official checksums.
 */
class Core_Integrity_Check extends Abstract_Security_Check {

    /**
     * Constructor.
     */
    public function __construct() {
        $this->check_id = 'core_integrity';
        $this->check_name = __( 'WordPress Core Integrity', 'wp-security-scanner' );
        $this->check_description = __( 
            'Verifies WordPress core files against official checksums', 
            'wp-security-scanner' 
        );
    }

    /**
     * Execute core integrity check.
     *
     * @return array Check results.
     */
    public function execute() {
        $this->init_results();

        // Get current WordPress version.
        global $wp_version;

        // Fetch official checksums from WordPress.org API.
        $checksums = $this->fetch_core_checksums( $wp_version );

        if ( is_wp_error( $checksums ) ) {
            $this->add_issue(
                'high',
                __( 'Unable to Verify Core Files', 'wp-security-scanner' ),
                sprintf(
                    /* translators: %s: Error message */
                    __( 'Could not fetch checksums from WordPress.org: %s', 'wp-security-scanner' ),
                    $checksums->get_error_message()
                ),
                array( 'error_code' => $checksums->get_error_code() )
            );
            $this->complete_check( 'error' );
            return $this->results;
        }

        // Verify each core file.
        $modified_files = array();
        $missing_files = array();

        foreach ( $checksums as $file => $expected_checksum ) {
            $file_path = ABSPATH . $file;

            if ( ! file_exists( $file_path ) ) {
                $missing_files[] = $file;
                continue;
            }

            $actual_checksum = md5_file( $file_path );

            if ( $actual_checksum !== $expected_checksum ) {
                $modified_files[] = array(
                    'file'              => $file,
                    'expected_checksum' => $expected_checksum,
                    'actual_checksum'   => $actual_checksum,
                );
            }
        }

        // Report modified files.
        if ( ! empty( $modified_files ) ) {
            foreach ( $modified_files as $file_data ) {
                $this->add_issue(
                    'critical',
                    __( 'Core File Modified', 'wp-security-scanner' ),
                    sprintf(
                        /* translators: %s: File path */
                        __( 'WordPress core file has been modified: %s', 'wp-security-scanner' ),
                        '<code>' . esc_html( $file_data['file'] ) . '</code>'
                    ),
                    $file_data
                );
            }
        }

        // Report missing files.
        if ( ! empty( $missing_files ) ) {
            foreach ( $missing_files as $file ) {
                $this->add_issue(
                    'high',
                    __( 'Core File Missing', 'wp-security-scanner' ),
                    sprintf(
                        /* translators: %s: File path */
                        __( 'WordPress core file is missing: %s', 'wp-security-scanner' ),
                        '<code>' . esc_html( $file ) . '</code>'
                    ),
                    array( 'file' => $file )
                );
            }
        }

        $this->complete_check( 
            empty( $modified_files ) && empty( $missing_files ) ? 'passed' : 'failed' 
        );

        return $this->results;
    }

    /**
     * Fetch core checksums from WordPress.org API.
     *
     * @param string $version WordPress version.
     * @param string $locale  WordPress locale.
     * @return array|WP_Error Checksums array or error object.
     */
    private function fetch_core_checksums( $version, $locale = 'en_US' ) {
        $api_url = sprintf(
            'https://api.wordpress.org/core/checksums/1.0/?version=%s&locale=%s',
            $version,
            $locale
        );

        $response = wp_remote_get(
            $api_url,
            array(
                'timeout' => 30,
                'sslverify' => true,
            )
        );

        if ( is_wp_error( $response ) ) {
            return $response;
        }

        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );

        if ( ! isset( $data['checksums'] ) ) {
            return new \WP_Error(
                'invalid_response',
                __( 'Invalid response from WordPress.org API', 'wp-security-scanner' )
            );
        }

        return $data['checksums'];
    }
}

4.2 Plugin Vulnerability Detection

Detecting vulnerable plugins is essential for WordPress security. This module integrates with the WPScan vulnerability database:

⚠️ API Key Requirement

To use the WPScan Vulnerability Database API, you’ll need to register for a free API key at wpscan.com. The free tier allows 25 requests per day.

PHP
<?php
/**
 * Plugin Vulnerability Check
 *
 * @package WP_Security_Scanner\Checks
 * @since   1.0.0
 */

namespace WP_Security_Scanner\Checks;

/**
 * Class Plugin_Vulnerability_Check
 *
 * Scans installed plugins for known vulnerabilities.
 */
class Plugin_Vulnerability_Check extends Abstract_Security_Check {

    /**
     * WPScan API endpoint.
     *
     * @var string
     */
    const WPSCAN_API_URL = 'https://wpscan.com/api/v3/plugins/';

    /**
     * API token for WPScan.
     *
     * @var string
     */
    private $api_token;

    /**
     * Constructor.
     */
    public function __construct() {
        $this->check_id = 'plugin_vulnerabilities';
        $this->check_name = __( 'Plugin Vulnerability Check', 'wp-security-scanner' );
        $this->check_description = __( 
            'Scans installed plugins for known security vulnerabilities', 
            'wp-security-scanner' 
        );

        // Get API token from options.
        $this->api_token = get_option( 'wp_security_scanner_wpscan_token', '' );
    }

    /**
     * Execute plugin vulnerability check.
     *
     * @return array Check results.
     */
    public function execute() {
        $this->init_results();

        if ( empty( $this->api_token ) ) {
            $this->add_issue(
                'info',
                __( 'WPScan API Token Not Configured', 'wp-security-scanner' ),
                __( 
                    'Configure a WPScan API token to enable vulnerability scanning.', 
                    'wp-security-scanner' 
                )
            );
            $this->complete_check( 'error' );
            return $this->results;
        }

        // Get all installed plugins.
        if ( ! function_exists( 'get_plugins' ) ) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }

        $all_plugins = get_plugins();

        foreach ( $all_plugins as $plugin_file => $plugin_data ) {
            $plugin_slug = dirname( $plugin_file );

            // Skip if plugin slug is empty (single-file plugins).
            if ( empty( $plugin_slug ) || '.' === $plugin_slug ) {
                $plugin_slug = basename( $plugin_file, '.php' );
            }

            $this->check_plugin_vulnerabilities(
                $plugin_slug,
                $plugin_data['Name'],
                $plugin_data['Version']
            );
        }

        $this->complete_check( 
            empty( $this->results['issues'] ) ? 'passed' : 'failed' 
        );

        return $this->results;
    }

    /**
     * Check specific plugin for vulnerabilities.
     *
     * @param string $plugin_slug    Plugin slug.
     * @param string $plugin_name    Plugin name.
     * @param string $plugin_version Plugin version.
     * @return void
     */
    private function check_plugin_vulnerabilities( $plugin_slug, $plugin_name, $plugin_version ) {
        $vulnerabilities = $this->fetch_plugin_vulnerabilities( $plugin_slug );

        if ( is_wp_error( $vulnerabilities ) ) {
            return;
        }

        if ( empty( $vulnerabilities ) ) {
            return;
        }

        // Check if current version is affected.
        foreach ( $vulnerabilities as $vuln ) {
            if ( $this->is_version_vulnerable( $plugin_version, $vuln ) ) {
                $this->add_issue(
                    $this->map_vulnerability_severity( $vuln ),
                    sprintf(
                        /* translators: %s: Plugin name */
                        __( 'Vulnerable Plugin: %s', 'wp-security-scanner' ),
                        $plugin_name
                    ),
                    $this->format_vulnerability_description( $vuln, $plugin_version ),
                    array(
                        'plugin_slug'    => $plugin_slug,
                        'plugin_version' => $plugin_version,
                        'vuln_id'        => $vuln['id'],
                        'cvss_score'     => $vuln['cvss']['score'] ?? null,
                    )
                );
            }
        }
    }

    /**
     * Fetch plugin vulnerabilities from WPScan API.
     *
     * @param string $plugin_slug Plugin slug.
     * @return array|WP_Error Vulnerabilities array or error.
     */
    private function fetch_plugin_vulnerabilities( $plugin_slug ) {
        $api_url = self::WPSCAN_API_URL . $plugin_slug;

        $response = wp_remote_get(
            $api_url,
            array(
                'timeout' => 15,
                'headers' => array(
                    'Authorization' => 'Token token=' . $this->api_token,
                ),
            )
        );

        if ( is_wp_error( $response ) ) {
            return $response;
        }

        $response_code = wp_remote_retrieve_response_code( $response );

        // 404 means no vulnerabilities found.
        if ( 404 === $response_code ) {
            return array();
        }

        if ( 200 !== $response_code ) {
            return new \WP_Error(
                'api_error',
                sprintf(
                    /* translators: %d: HTTP response code */
                    __( 'WPScan API returned error code: %d', 'wp-security-scanner' ),
                    $response_code
                )
            );
        }

        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );

        return $data[ $plugin_slug ]['vulnerabilities'] ?? array();
    }

    /**
     * Check if version is vulnerable based on vulnerability data.
     *
     * @param string $version Version to check.
     * @param array  $vuln    Vulnerability data.
     * @return bool Whether version is vulnerable.
     */
    private function is_version_vulnerable( $version, $vuln ) {
        $fixed_in = $vuln['fixed_in'] ?? null;

        if ( null === $fixed_in ) {
            // No fix available, all versions vulnerable.
            return true;
        }

        // Version is vulnerable if it's lower than the fixed version.
        return version_compare( $version, $fixed_in, '<' );
    }

    /**
     * Map WPScan CVSS score to severity level.
     *
     * @param array $vuln Vulnerability data.
     * @return string Severity level.
     */
    private function map_vulnerability_severity( $vuln ) {
        $cvss_score = $vuln['cvss']['score'] ?? 0;

        if ( $cvss_score >= 9.0 ) {
            return 'critical';
        } elseif ( $cvss_score >= 7.0 ) {
            return 'high';
        } elseif ( $cvss_score >= 4.0 ) {
            return 'medium';
        } else {
            return 'low';
        }
    }

    /**
     * Format vulnerability description.
     *
     * @param array  $vuln    Vulnerability data.
     * @param string $version Current plugin version.
     * @return string Formatted description.
     */
    private function format_vulnerability_description( $vuln, $version ) {
        $description = sprintf(
            /* translators: 1: Vulnerability title, 2: Current version */
            __( '<strong>%1$s</strong>
Current version: %2$s', 'wp-security-scanner' ),
            esc_html( $vuln['title'] ),
            esc_html( $version )
        );

        if ( isset( $vuln['fixed_in'] ) ) {
            $description .= sprintf(
                /* translators: %s: Fixed version */
                __( '
Fixed in version: %s', 'wp-security-scanner' ),
                esc_html( $vuln['fixed_in'] )
            );
        } else {
            $description .= '
' . __( 
                '<strong>No fix available yet</strong>', 
                'wp-security-scanner' 
            );
        }

        if ( isset( $vuln['cvss']['score'] ) ) {
            $description .= sprintf(
                /* translators: %s: CVSS score */
                __( '
CVSS Score: %s', 'wp-security-scanner' ),
                esc_html( $vuln['cvss']['score'] )
            );
        }

        return $description;
    }
}

5. File Integrity Monitoring

File integrity monitoring detects unauthorized changes to critical files and identifies potential malware. This implementation uses file hashing and pattern matching:

PHP
<?php
/**
 * File Permission and Integrity Check
 *
 * @package WP_Security_Scanner\Checks
 * @since   1.0.0
 */

namespace WP_Security_Scanner\Checks;

/**
 * Class File_Permission_Check
 *
 * Audits file and directory permissions.
 */
class File_Permission_Check extends Abstract_Security_Check {

    /**
     * Recommended file permissions.
     *
     * @var array
     */
    private $recommended_permissions = array(
        'files'       => 0644,
        'directories' => 0755,
    );

    /**
     * Critical files to check.
     *
     * @var array
     */
    private $critical_files = array(
        'wp-config.php',
        '.htaccess',
        'index.php',
    );

    /**
     * Constructor.
     */
    public function __construct() {
        $this->check_id = 'file_permissions';
        $this->check_name = __( 'File Permission Check', 'wp-security-scanner' );
        $this->check_description = __( 
            'Audits file and directory permissions for security issues', 
            'wp-security-scanner' 
        );
    }

    /**
     * Execute file permission check.
     *
     * @return array Check results.
     */
    public function execute() {
        $this->init_results();

        // Check wp-config.php permissions.
        $this->check_wp_config_permissions();

        // Check .htaccess permissions.
        $this->check_htaccess_permissions();

        // Check uploads directory permissions.
        $this->check_uploads_permissions();

        // Check plugin/theme directories.
        $this->check_content_directories();

        // Scan for suspicious files.
        $this->scan_suspicious_files();

        $this->complete_check( 
            empty( $this->results['issues'] ) ? 'passed' : 'failed' 
        );

        return $this->results;
    }

    /**
     * Check wp-config.php permissions.
     *
     * @return void
     */
    private function check_wp_config_permissions() {
        $config_file = ABSPATH . 'wp-config.php';

        if ( ! file_exists( $config_file ) ) {
            $this->add_issue(
                'critical',
                __( 'wp-config.php Not Found', 'wp-security-scanner' ),
                __( 
                    'The wp-config.php file could not be located.', 
                    'wp-security-scanner' 
                )
            );
            return;
        }

        $perms = fileperms( $config_file ) & 0777;

        // wp-config.php should be 0600 or 0640 (owner read/write only).
        if ( $perms > 0640 ) {
            $this->add_issue(
                'high',
                __( 'wp-config.php Has Insecure Permissions', 'wp-security-scanner' ),
                sprintf(
                    /* translators: 1: Current permissions, 2: Recommended permissions */
                    __( 
                        'Current permissions: %1$s. Recommended: %2$s or more restrictive.', 
                        'wp-security-scanner' 
                    ),
                    $this->format_permissions( $perms ),
                    $this->format_permissions( 0640 )
                ),
                array(
                    'file'                    => 'wp-config.php',
                    'current_permissions'     => $perms,
                    'recommended_permissions' => 0640,
                )
            );
        }

        // Check if wp-config.php is publicly accessible.
        if ( $this->is_file_publicly_accessible( 'wp-config.php' ) ) {
            $this->add_issue(
                'critical',
                __( 'wp-config.php Publicly Accessible', 'wp-security-scanner' ),
                __( 
                    'The wp-config.php file can be accessed directly via HTTP. This is a critical security risk.', 
                    'wp-security-scanner' 
                ),
                array( 'file' => 'wp-config.php' )
            );
        }
    }

    /**
     * Check .htaccess permissions.
     *
     * @return void
     */
    private function check_htaccess_permissions() {
        $htaccess_file = ABSPATH . '.htaccess';

        if ( ! file_exists( $htaccess_file ) ) {
            return; // .htaccess is optional.
        }

        $perms = fileperms( $htaccess_file ) & 0777;

        // .htaccess should be 0644 (owner write, all read).
        if ( $perms > 0644 ) {
            $this->add_issue(
                'medium',
                __( '.htaccess Has Insecure Permissions', 'wp-security-scanner' ),
                sprintf(
                    /* translators: 1: Current permissions, 2: Recommended permissions */
                    __( 
                        'Current permissions: %1$s. Recommended: %2$s.', 
                        'wp-security-scanner' 
                    ),
                    $this->format_permissions( $perms ),
                    $this->format_permissions( 0644 )
                ),
                array(
                    'file'                    => '.htaccess',
                    'current_permissions'     => $perms,
                    'recommended_permissions' => 0644,
                )
            );
        }
    }

    /**
     * Check uploads directory permissions.
     *
     * @return void
     */
    private function check_uploads_permissions() {
        $upload_dir = wp_upload_dir();
        $uploads_path = $upload_dir['basedir'];

        if ( ! is_dir( $uploads_path ) ) {
            return;
        }

        // Check for PHP files in uploads directory.
        $php_files = $this->find_php_files_in_uploads( $uploads_path );

        if ( ! empty( $php_files ) ) {
            foreach ( $php_files as $php_file ) {
                $this->add_issue(
                    'critical',
                    __( 'PHP File in Uploads Directory', 'wp-security-scanner' ),
                    sprintf(
                        /* translators: %s: File path */
                        __( 
                            'Potentially malicious PHP file found: %s', 
                            'wp-security-scanner' 
                        ),
                        '<code>' . esc_html( $this->sanitize_file_path( $php_file ) ) . '</code>'
                    ),
                    array( 'file' => $php_file )
                );
            }
        }
    }

    /**
     * Find PHP files in uploads directory.
     *
     * @param string $directory Directory to scan.
     * @param int    $depth     Current recursion depth.
     * @return array Found PHP files.
     */
    private function find_php_files_in_uploads( $directory, $depth = 0 ) {
        $php_files = array();
        $max_depth = 5; // Limit recursion depth.

        if ( $depth > $max_depth ) {
            return $php_files;
        }

        $iterator = new \DirectoryIterator( $directory );

        foreach ( $iterator as $file ) {
            if ( $file->isDot() ) {
                continue;
            }

            if ( $file->isDir() ) {
                $php_files = array_merge(
                    $php_files,
                    $this->find_php_files_in_uploads( 
                        $file->getPathname(), 
                        $depth + 1 
                    )
                );
            } elseif ( 'php' === $file->getExtension() ) {
                $php_files[] = $file->getPathname();
            }
        }

        return $php_files;
    }

    /**
     * Check content directories permissions.
     *
     * @return void
     */
    private function check_content_directories() {
        $content_dirs = array(
            WP_CONTENT_DIR . '/plugins',
            WP_CONTENT_DIR . '/themes',
        );

        foreach ( $content_dirs as $dir ) {
            if ( ! is_dir( $dir ) ) {
                continue;
            }

            $perms = fileperms( $dir ) & 0777;

            // Directories should be 0755 maximum.
            if ( $perms > 0755 ) {
                $this->add_issue(
                    'medium',
                    __( 'Directory Has Insecure Permissions', 'wp-security-scanner' ),
                    sprintf(
                        /* translators: 1: Directory path, 2: Current permissions, 3: Recommended permissions */
                        __( 
                            'Directory %1$s has permissions %2$s. Recommended: %3$s.', 
                            'wp-security-scanner' 
                        ),
                        '<code>' . esc_html( $this->sanitize_file_path( $dir ) ) . '</code>',
                        $this->format_permissions( $perms ),
                        $this->format_permissions( 0755 )
                    ),
                    array(
                        'directory'               => $dir,
                        'current_permissions'     => $perms,
                        'recommended_permissions' => 0755,
                    )
                );
            }
        }
    }

    /**
     * Scan for suspicious files.
     *
     * @return void
     */
    private function scan_suspicious_files() {
        $suspicious_patterns = array(
            '*.suspected',
            '*.bak.php',
            '*~',
            '*.swp',
        );

        $root_dir = ABSPATH;
        $suspicious_files = array();

        foreach ( $suspicious_patterns as $pattern ) {
            $found_files = glob( $root_dir . $pattern );
            if ( ! empty( $found_files ) ) {
                $suspicious_files = array_merge( $suspicious_files, $found_files );
            }
        }

        foreach ( $suspicious_files as $file ) {
            $this->add_issue(
                'medium',
                __( 'Suspicious File Detected', 'wp-security-scanner' ),
                sprintf(
                    /* translators: %s: File path */
                    __( 
                        'Potentially suspicious file found: %s', 
                        'wp-security-scanner' 
                    ),
                    '<code>' . esc_html( $this->sanitize_file_path( $file ) ) . '</code>'
                ),
                array( 'file' => $file )
            );
        }
    }

    /**
     * Check if file is publicly accessible.
     *
     * @param string $file Relative file path.
     * @return bool Whether file is accessible.
     */
    private function is_file_publicly_accessible( $file ) {
        $url = site_url( $file );

        $response = wp_remote_head(
            $url,
            array(
                'timeout'     => 5,
                'redirection' => 0,
                'sslverify'   => false,
            )
        );

        if ( is_wp_error( $response ) ) {
            return false;
        }

        $response_code = wp_remote_retrieve_response_code( $response );

        // 200 means file is accessible.
        return 200 === $response_code;
    }

    /**
     * Format permissions as octal string.
     *
     * @param int $perms Permissions value.
     * @return string Formatted permissions.
     */
    private function format_permissions( $perms ) {
        return sprintf( '0%o', $perms );
    }
}

6. Database Security Scanning

Database security checks identify SQL injection vulnerabilities, weak configurations, and suspicious user accounts:

💡 Pro Tip: Always use prepared statements and the $wpdb class methods when interacting with the WordPress database. Never concatenate user input directly into SQL queries.
PHP
<?php
/**
 * Database Security Check
 *
 * @package WP_Security_Scanner\Checks
 * @since   1.0.0
 */

namespace WP_Security_Scanner\Checks;

/**
 * Class Database_Security_Check
 *
 * Audits database security configuration and content.
 */
class Database_Security_Check extends Abstract_Security_Check {

    /**
     * Constructor.
     */
    public function __construct() {
        $this->check_id = 'database_security';
        $this->check_name = __( 'Database Security Check', 'wp-security-scanner' );
        $this->check_description = __( 
            'Analyzes database configuration and identifies security issues', 
            'wp-security-scanner' 
        );
    }

    /**
     * Execute database security check.
     *
     * @return array Check results.
     */
    public function execute() {
        global $wpdb;

        $this->init_results();

        // Check table prefix.
        $this->check_table_prefix();

        // Check for default admin username.
        $this->check_default_admin_username();

        // Check for users with weak passwords.
        $this->check_weak_passwords();

        // Check for suspicious user accounts.
        $this->check_suspicious_users();

        // Check for admin users without email.
        $this->check_users_without_email();

        // Check database version.
        $this->check_database_version();

        $this->complete_check( 
            empty( $this->results['issues'] ) ? 'passed' : 'failed' 
        );

        return $this->results;
    }

    /**
     * Check database table prefix.
     *
     * @return void
     */
    private function check_table_prefix() {
        global $wpdb;

        // Default WordPress prefix is 'wp_'.
        if ( 'wp_' === $wpdb->prefix ) {
            $this->add_issue(
                'low',
                __( 'Default Database Table Prefix', 'wp-security-scanner' ),
                __( 
                    'Using the default table prefix "wp_" makes your database more vulnerable to SQL injection attacks. Consider changing it to a unique prefix.', 
                    'wp-security-scanner' 
                ),
                array( 'current_prefix' => $wpdb->prefix )
            );
        }
    }

    /**
     * Check for default admin username.
     *
     * @return void
     */
    private function check_default_admin_username() {
        global $wpdb;

        // Check if user with username 'admin' exists.
        $admin_user = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT ID FROM {$wpdb->users} WHERE user_login = %s",
                'admin'
            )
        );

        if ( $admin_user ) {
            $this->add_issue(
                'medium',
                __( 'Default Admin Username Detected', 'wp-security-scanner' ),
                __( 
                    'A user account with the username "admin" exists. This is a common target for brute force attacks. Consider renaming this account.', 
                    'wp-security-scanner' 
                ),
                array( 'user_id' => $admin_user )
            );
        }
    }

    /**
     * Check for users with weak passwords.
     *
     * Note: WordPress stores hashed passwords, so we can only check
     * for common weak password patterns in user metadata.
     *
     * @return void
     */
    private function check_weak_passwords() {
        global $wpdb;

        // Get all administrator users.
        $admin_users = get_users(
            array(
                'role'   => 'administrator',
                'fields' => array( 'ID', 'user_login', 'user_email' ),
            )
        );

        foreach ( $admin_users as $user ) {
            // Check if user has set up 2FA (example meta key).
            $has_2fa = get_user_meta( $user->ID, 'two_factor_enabled', true );

            if ( ! $has_2fa ) {
                $this->add_issue(
                    'medium',
                    __( 'Administrator Without Two-Factor Authentication', 'wp-security-scanner' ),
                    sprintf(
                        /* translators: %s: Username */
                        __( 
                            'Administrator account "%s" does not have two-factor authentication enabled.', 
                            'wp-security-scanner' 
                        ),
                        esc_html( $user->user_login )
                    ),
                    array(
                        'user_id'    => $user->ID,
                        'user_login' => $user->user_login,
                    )
                );
            }
        }
    }

    /**
     * Check for suspicious user accounts.
     *
     * @return void
     */
    private function check_suspicious_users() {
        global $wpdb;

        // Check for users created in the last 7 days with admin role.
        $recent_admins = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT u.ID, u.user_login, u.user_email, u.user_registered
                FROM {$wpdb->users} u
                INNER JOIN {$wpdb->usermeta} um ON u.ID = um.user_id
                WHERE um.meta_key = %s
                AND um.meta_value LIKE %s
                AND u.user_registered > DATE_SUB(NOW(), INTERVAL 7 DAY)
                ORDER BY u.user_registered DESC",
                $wpdb->prefix . 'capabilities',
                '%administrator%'
            )
        );

        if ( ! empty( $recent_admins ) ) {
            foreach ( $recent_admins as $user ) {
                $this->add_issue(
                    'high',
                    __( 'Recently Created Administrator Account', 'wp-security-scanner' ),
                    sprintf(
                        /* translators: 1: Username, 2: Registration date */
                        __( 
                            'Administrator account "%1$s" was created recently on %2$s. Verify this is a legitimate account.', 
                            'wp-security-scanner' 
                        ),
                        esc_html( $user->user_login ),
                        esc_html( $user->user_registered )
                    ),
                    array(
                        'user_id'         => $user->ID,
                        'user_login'      => $user->user_login,
                        'user_registered' => $user->user_registered,
                    )
                );
            }
        }

        // Check for users with suspicious login patterns.
        $suspicious_logins = array( 'admin', 'test', 'demo', 'guest', 'root' );

        foreach ( $suspicious_logins as $login ) {
            $user = get_user_by( 'login', $login );

            if ( $user && user_can( $user, 'administrator' ) ) {
                $this->add_issue(
                    'medium',
                    __( 'Suspicious Administrator Username', 'wp-security-scanner' ),
                    sprintf(
                        /* translators: %s: Username */
                        __( 
                            'Administrator account with suspicious username detected: "%s"', 
                            'wp-security-scanner' 
                        ),
                        esc_html( $login )
                    ),
                    array(
                        'user_id'    => $user->ID,
                        'user_login' => $login,
                    )
                );
            }
        }
    }

    /**
     * Check for admin users without email addresses.
     *
     * @return void
     */
    private function check_users_without_email() {
        global $wpdb;

        $users_without_email = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT u.ID, u.user_login
                FROM {$wpdb->users} u
                INNER JOIN {$wpdb->usermeta} um ON u.ID = um.user_id
                WHERE um.meta_key = %s
                AND um.meta_value LIKE %s
                AND (u.user_email = '' OR u.user_email IS NULL)",
                $wpdb->prefix . 'capabilities',
                '%administrator%'
            )
        );

        if ( ! empty( $users_without_email ) ) {
            foreach ( $users_without_email as $user ) {
                $this->add_issue(
                    'medium',
                    __( 'Administrator Without Email Address', 'wp-security-scanner' ),
                    sprintf(
                        /* translators: %s: Username */
                        __( 
                            'Administrator account "%s" does not have an email address configured.', 
                            'wp-security-scanner' 
                        ),
                        esc_html( $user->user_login )
                    ),
                    array(
                        'user_id'    => $user->ID,
                        'user_login' => $user->user_login,
                    )
                );
            }
        }
    }

    /**
     * Check database version.
     *
     * @return void
     */
    private function check_database_version() {
        global $wpdb;

        $db_version = $wpdb->db_version();

        // MySQL versions below 5.6 or MariaDB below 10.0 are EOL.
        if ( version_compare( $db_version, '5.6', '<' ) ) {
            $this->add_issue(
                'high',
                __( 'Outdated Database Version', 'wp-security-scanner' ),
                sprintf(
                    /* translators: %s: Database version */
                    __( 
                        'Your database version (%s) is outdated and no longer receives security updates. Upgrade to a supported version.', 
                        'wp-security-scanner' 
                    ),
                    esc_html( $db_version )
                ),
                array( 'db_version' => $db_version )
            );
        }
    }
}

7. Reporting and Alerting

A comprehensive reporting system presents scan results in an actionable format. This includes HTML reports, email notifications, and dashboard widgets:

PHP
<?php
/**
 * Security Scanner Report Generator
 *
 * @package WP_Security_Scanner\Reports
 * @since   1.0.0
 */

namespace WP_Security_Scanner\Reports;

/**
 * Class Report_Generator
 *
 * Generates security scan reports in various formats.
 */
class Report_Generator {

    /**
     * Scan results data.
     *
     * @var array
     */
    private $results;

    /**
     * Constructor.
     *
     * @param array $results Scan results.
     */
    public function __construct( array $results ) {
        $this->results = $results;
    }

    /**
     * Generate HTML report.
     *
     * @return string HTML report.
     */
    public function generate_html_report() {
        ob_start();
        ?>
        <div class="wp-security-report">
            <div class="report-header">
                <h2><?php esc_html_e( 'Security Scan Report', 'wp-security-scanner' ); ?></h2>
                
                    <?php
                    printf(
                        /* translators: 1: Scan date, 2: Site URL */
                        esc_html__( 'Scan completed: %1$s | Site: %2$s', 'wp-security-scanner' ),
                        esc_html( $this->results['timestamp'] ),
                        esc_html( $this->results['site_url'] )
                    );
                    ?>
                

            </div>

            <div class="report-summary">
                <h3><?php esc_html_e( 'Summary', 'wp-security-scanner' ); ?></h3>
                <div class="summary-grid">
                    <?php $this->render_summary_card( 'critical', __( 'Critical', 'wp-security-scanner' ) ); ?>
                    <?php $this->render_summary_card( 'high', __( 'High', 'wp-security-scanner' ) ); ?>
                    <?php $this->render_summary_card( 'medium', __( 'Medium', 'wp-security-scanner' ) ); ?>
                    <?php $this->render_summary_card( 'low', __( 'Low', 'wp-security-scanner' ) ); ?>
                </div>
            </div>

            <div class="report-details">
                <h3><?php esc_html_e( 'Detailed Findings', 'wp-security-scanner' ); ?></h3>
                <?php $this->render_detailed_findings(); ?>
            </div>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * Render summary card.
     *
     * @param string $severity Severity level.
     * @param string $label    Display label.
     * @return void
     */
    private function render_summary_card( $severity, $label ) {
        $count = $this->results['summary'][ $severity ] ?? 0;
        $class = 'summary-card severity-' . $severity;
        ?>
        <div class="<?php echo esc_attr( $class ); ?>">
            <div class="card-count"><?php echo esc_html( $count ); ?></div>
            <div class="card-label"><?php echo esc_html( $label ); ?></div>
        </div>
        <?php
    }

    /**
     * Render detailed findings.
     *
     * @return void
     */
    private function render_detailed_findings() {
        foreach ( $this->results['checks'] as $check_name => $check_data ) {
            if ( empty( $check_data['issues'] ) ) {
                continue;
            }

            ?>
            <div class="check-section">
                <h4><?php echo esc_html( $check_data['check_name'] ); ?></h4>
                
                    <?php echo esc_html( $check_data['check_description'] ); ?>
                


                <table class="issues-table">
                    <thead>
                        <tr>
                            <th><?php esc_html_e( 'Severity', 'wp-security-scanner' ); ?></th>
                            <th><?php esc_html_e( 'Issue', 'wp-security-scanner' ); ?></th>
                            <th><?php esc_html_e( 'Description', 'wp-security-scanner' ); ?></th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ( $check_data['issues'] as $issue ) : ?>
                            <tr class="severity-<?php echo esc_attr( strtolower( $issue['severity'] ) ); ?>">
                                <td>
                                    <span class="severity-badge">
                                        <?php echo esc_html( ucfirst( $issue['severity'] ) ); ?>
                                    </span>
                                </td>
                                <td><?php echo esc_html( $issue['title'] ); ?></td>
                                <td><?php echo wp_kses_post( $issue['description'] ); ?></td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
            <?php
        }
    }

    /**
     * Generate email notification.
     *
     * @return bool Whether email was sent successfully.
     */
    public function send_email_notification() {
        $admin_email = get_option( 'admin_email' );
        $site_name = get_bloginfo( 'name' );

        $subject = sprintf(
            /* translators: 1: Site name, 2: Number of issues */
            __( '[%1$s] Security Scan Alert - %2$d Issues Found', 'wp-security-scanner' ),
            $site_name,
            $this->results['total_issues']
        );

        $message = $this->generate_email_body();

        $headers = array(
            'Content-Type: text/html; charset=UTF-8',
        );

        return wp_mail( $admin_email, $subject, $message, $headers );
    }

    /**
     * Generate email body.
     *
     * @return string Email HTML body.
     */
    private function generate_email_body() {
        ob_start();
        ?>
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="UTF-8">
            <style>
                body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
                .container { max-width: 600px; margin: 0 auto; padding: 20px; }
                .header { background: #0073aa; color: white; padding: 20px; text-align: center; }
                .summary { background: #f5f5f5; padding: 15px; margin: 20px 0; }
                .severity-critical { color: #c62828; font-weight: bold; }
                .severity-high { color: #e65100; font-weight: bold; }
                .severity-medium { color: #f57f17; }
                .severity-low { color: #2e7d32; }
                table { width: 100%; border-collapse: collapse; margin: 20px 0; }
                th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
                th { background: #f5f5f5; font-weight: bold; }
                .footer { margin-top: 30px; padding-top: 20px; border-top: 2px solid #ddd; font-size: 0.9em; color: #666; }
            </style>
        </head>
        <body>
            <div class="container">
                <div class="header">
                    <h1><?php esc_html_e( 'Security Scan Alert', 'wp-security-scanner' ); ?></h1>
                </div>

                <div class="summary">
                    <h2><?php esc_html_e( 'Summary', 'wp-security-scanner' ); ?></h2>
                    
                        <?php
                        printf(
                            /* translators: 1: Total issues, 2: Site URL */
                            esc_html__( '%1$d security issues found on %2$s', 'wp-security-scanner' ),
                            esc_html( $this->results['total_issues'] ),
                            esc_html( $this->results['site_url'] )
                        );
                        ?>
                    

                    <ul>
                        <li class="severity-critical">
                            <?php
                            printf(
                                /* translators: %d: Number of critical issues */
                                esc_html__( 'Critical: %d', 'wp-security-scanner' ),
                                esc_html( $this->results['summary']['critical'] )
                            );
                            ?>
                        </li>
                        <li class="severity-high">
                            <?php
                            printf(
                                /* translators: %d: Number of high issues */
                                esc_html__( 'High: %d', 'wp-security-scanner' ),
                                esc_html( $this->results['summary']['high'] )
                            );
                            ?>
                        </li>
                        <li class="severity-medium">
                            <?php
                            printf(
                                /* translators: %d: Number of medium issues */
                                esc_html__( 'Medium: %d', 'wp-security-scanner' ),
                                esc_html( $this->results['summary']['medium'] )
                            );
                            ?>
                        </li>
                        <li class="severity-low">
                            <?php
                            printf(
                                /* translators: %d: Number of low issues */
                                esc_html__( 'Low: %d', 'wp-security-scanner' ),
                                esc_html( $this->results['summary']['low'] )
                            );
                            ?>
                        </li>
                    </ul>
                </div>

                <h2><?php esc_html_e( 'Action Required', 'wp-security-scanner' ); ?></h2>
                <?php esc_html_e( 'Please log in to your WordPress admin panel to review the full security report and take appropriate action.', 'wp-security-scanner' ); ?>


                <div class="footer">
                    
                        <?php
                        printf(
                            /* translators: %s: Current date and time */
                            esc_html__( 'Scan completed: %s', 'wp-security-scanner' ),
                            esc_html( current_time( 'mysql' ) )
                        );
                        ?>
                    

                    <?php esc_html_e( 'This is an automated security notification from WP Security Scanner.', 'wp-security-scanner' ); ?>

                </div>
            </div>
        </body>
        </html>
        <?php
        return ob_get_clean();
    }

    /**
     * Generate JSON export.
     *
     * @return string JSON-encoded results.
     */
    public function generate_json_export() {
        return wp_json_encode( $this->results, JSON_PRETTY_PRINT );
    }

    /**
     * Generate CSV export.
     *
     * @return string CSV data.
     */
    public function generate_csv_export() {
        $csv_data = array();

        // CSV header.
        $csv_data[] = array(
            'Check Name',
            'Severity',
            'Issue Title',
            'Description',
            'Timestamp',
        );

        // Add issues.
        foreach ( $this->results['checks'] as $check_data ) {
            if ( empty( $check_data['issues'] ) ) {
                continue;
            }

            foreach ( $check_data['issues'] as $issue ) {
                $csv_data[] = array(
                    $check_data['check_name'],
                    $issue['severity'],
                    $issue['title'],
                    wp_strip_all_tags( $issue['description'] ),
                    $issue['timestamp'],
                );
            }
        }

        // Convert to CSV string.
        ob_start();
        $output = fopen( 'php://output', 'w' );

        foreach ( $csv_data as $row ) {
            fputcsv( $output, $row );
        }

        fclose( $output );
        return ob_get_clean();
    }
}

8. Automation and Scheduling

Automate security scans using WordPress cron to ensure continuous monitoring:

PHP
<?php
/**
 * Security Scanner Automation
 *
 * @package WP_Security_Scanner\Automation
 * @since   1.0.0
 */

namespace WP_Security_Scanner\Automation;

use WP_Security_Scanner\Security_Scanner;
use WP_Security_Scanner\Reports\Report_Generator;

/**
 * Class Scanner_Scheduler
 *
 * Manages automated security scan scheduling.
 */
class Scanner_Scheduler {

    /**
     * Cron hook name.
     *
     * @var string
     */
    const CRON_HOOK = 'wp_security_scanner_scheduled_scan';

    /**
     * Initialize scheduler.
     *
     * @return void
     */
    public function init() {
        add_action( self::CRON_HOOK, array( $this, 'run_scheduled_scan' ) );
        add_action( 'wp_security_scanner_send_notification', array( $this, 'handle_notification' ) );
    }

    /**
     * Schedule recurring scans.
     *
     * @param string $frequency Cron frequency (hourly|twicedaily|daily|weekly).
     * @return bool Whether scheduling was successful.
     */
    public function schedule_scans( $frequency = 'daily' ) {
        // Unschedule existing scans.
        $this->unschedule_scans();

        // Schedule new scans.
        $timestamp = wp_next_scheduled( self::CRON_HOOK );

        if ( false === $timestamp ) {
            return wp_schedule_event(
                time(),
                $frequency,
                self::CRON_HOOK
            );
        }

        return true;
    }

    /**
     * Unschedule all scans.
     *
     * @return bool Whether unscheduling was successful.
     */
    public function unschedule_scans() {
        $timestamp = wp_next_scheduled( self::CRON_HOOK );

        if ( false !== $timestamp ) {
            return wp_unschedule_event( $timestamp, self::CRON_HOOK );
        }

        return true;
    }

    /**
     * Run scheduled security scan.
     *
     * @return void
     */
    public function run_scheduled_scan() {
        // Prevent concurrent scans.
        if ( get_transient( 'wp_security_scanner_running' ) ) {
            return;
        }

        set_transient( 'wp_security_scanner_running', true, 600 );

        try {
            // Initialize scanner.
            $scanner = new Security_Scanner(
                array(
                    'enable_core_checks'     => true,
                    'enable_plugin_checks'   => true,
                    'enable_theme_checks'    => true,
                    'enable_db_checks'       => true,
                    'enable_file_checks'     => true,
                    'enable_notifications'   => true,
                    'notification_threshold' => 'high',
                )
            );

            // Execute scan.
            $results = $scanner->run_scan();

            // Log scan completion.
            error_log(
                sprintf(
                    'WP Security Scanner: Scheduled scan completed. Found %d issues.',
                    $results['total_issues']
                )
            );

        } catch ( \Exception $e ) {
            error_log(
                sprintf(
                    'WP Security Scanner: Scheduled scan failed - %s',
                    $e->getMessage()
                )
            );
        } finally {
            delete_transient( 'wp_security_scanner_running' );
        }
    }

    /**
     * Handle security notification.
     *
     * @param array $results Scan results.
     * @return void
     */
    public function handle_notification( $results ) {
        $report_generator = new Report_Generator( $results );
        $report_generator->send_email_notification();
    }

    /**
     * Get next scheduled scan time.
     *
     * @return int|false Timestamp or false if not scheduled.
     */
    public function get_next_scan_time() {
        return wp_next_scheduled( self::CRON_HOOK );
    }

    /**
     * Register custom cron schedules.
     *
     * @param array $schedules Existing schedules.
     * @return array Modified schedules.
     */
    public function add_cron_schedules( $schedules ) {
        $schedules['weekly'] = array(
            'interval' => 604800,
            'display'  => __( 'Once Weekly', 'wp-security-scanner' ),
        );

        return $schedules;
    }
}

// Initialize scheduler.
add_filter( 'cron_schedules', array( new Scanner_Scheduler(), 'add_cron_schedules' ) );

9. Best Practices and Security Considerations

✅ Essential Security Best Practices
  • Sanitize All Inputs: Use WordPress sanitization functions for all user inputs
  • Escape All Outputs: Use esc_html(), esc_attr(), and esc_url() appropriately
  • Use Nonces: Verify intent with WordPress nonces for all actions
  • Check Capabilities: Always verify user permissions before executing sensitive operations
  • Prepare SQL Queries: Use $wpdb->prepare() for all database queries
  • Limit API Calls: Implement rate limiting and caching for external API requests
  • Validate File Operations: Use wp_check_filetype() and validate paths
  • Log Security Events: Maintain audit trails of security-related actions

9.1 Implementation Checklist

Category Implementation Priority Complexity
Core Integrity Checksum validation against official WordPress API Critical Medium
Plugin Scanning WPScan API integration for vulnerability detection Critical Medium
File Permissions Recursive directory scanning with permission validation High Low
Database Security User enumeration, table prefix, and configuration checks High Low
Malware Detection Signature-based scanning with pattern matching High High
Automated Scanning WordPress Cron integration with configurable frequency Medium Low
Report Generation HTML, JSON, and CSV export capabilities Medium Medium
Email Notifications Automated alerts with configurable thresholds Low Low

9.2 Performance Optimization

⚡ Performance Tips:

  • Implement scan result caching to avoid redundant checks
  • Use transients to store API responses temporarily
  • Batch database queries where possible
  • Limit file system recursion depth to prevent memory exhaustion
  • Run intensive scans during off-peak hours
  • Consider background processing for large sites

9.3 Extending the Scanner

The modular architecture makes it easy to add custom security checks. Here’s an example of creating a custom check module:

PHP
<?php
/**
 * Custom Security Check Example
 *
 * @package WP_Security_Scanner\Checks
 * @since   1.0.0
 */

namespace WP_Security_Scanner\Checks;

/**
 * Class Configuration_Check
 *
 * Checks WordPress configuration for security issues.
 */
class Configuration_Check extends Abstract_Security_Check {

    /**
     * Constructor.
     */
    public function __construct() {
        $this->check_id = 'configuration';
        $this->check_name = __( 'Configuration Security Check', 'wp-security-scanner' );
        $this->check_description = __( 
            'Analyzes wp-config.php and WordPress configuration', 
            'wp-security-scanner' 
        );
    }

    /**
     * Execute configuration check.
     *
     * @return array Check results.
     */
    public function execute() {
        $this->init_results();

        // Check debug mode.
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            $this->add_issue(
                'medium',
                __( 'Debug Mode Enabled', 'wp-security-scanner' ),
                __( 
                    'WP_DEBUG is enabled. This should be disabled in production environments to prevent information disclosure.', 
                    'wp-security-scanner' 
                )
            );
        }

        // Check file editing.
        if ( ! defined( 'DISALLOW_FILE_EDIT' ) || ! DISALLOW_FILE_EDIT ) {
            $this->add_issue(
                'high',
                __( 'File Editing Not Disabled', 'wp-security-scanner' ),
                __( 
                    'WordPress file editor is enabled. Set DISALLOW_FILE_EDIT to true in wp-config.php to prevent unauthorized code changes.', 
                    'wp-security-scanner' 
                )
            );
        }

        // Check SSL.
        if ( ! is_ssl() ) {
            $this->add_issue(
                'high',
                __( 'SSL Not Configured', 'wp-security-scanner' ),
                __( 
                    'Your site is not using HTTPS. Enable SSL to encrypt data transmission and protect user credentials.', 
                    'wp-security-scanner' 
                )
            );
        }

        // Check security keys and salts.
        $this->check_security_keys();

        // Check file upload settings.
        $this->check_file_upload_settings();

        $this->complete_check( 
            empty( $this->results['issues'] ) ? 'passed' : 'failed' 
        );

        return $this->results;
    }

    /**
     * Check WordPress security keys and salts.
     *
     * @return void
     */
    private function check_security_keys() {
        $keys = array(
            'AUTH_KEY',
            'SECURE_AUTH_KEY',
            'LOGGED_IN_KEY',
            'NONCE_KEY',
            'AUTH_SALT',
            'SECURE_AUTH_SALT',
            'LOGGED_IN_SALT',
            'NONCE_SALT',
        );

        $default_key = 'put your unique phrase here';

        foreach ( $keys as $key ) {
            if ( ! defined( $key ) ) {
                $this->add_issue(
                    'critical',
                    sprintf(
                        /* translators: %s: Key name */
                        __( 'Security Key Not Defined: %s', 'wp-security-scanner' ),
                        $key
                    ),
                    __( 
                        'Security keys and salts should be defined in wp-config.php. Generate new keys at https://api.wordpress.org/secret-key/1.1/salt/', 
                        'wp-security-scanner' 
                    ),
                    array( 'key' => $key )
                );
                continue;
            }

            $key_value = constant( $key );

            // Check if using default value.
            if ( false !== strpos( $key_value, $default_key ) ) {
                $this->add_issue(
                    'critical',
                    sprintf(
                        /* translators: %s: Key name */
                        __( 'Default Security Key: %s', 'wp-security-scanner' ),
                        $key
                    ),
                    __( 
                        'Security key is using default value. Generate unique keys at https://api.wordpress.org/secret-key/1.1/salt/', 
                        'wp-security-scanner' 
                    ),
                    array( 'key' => $key )
                );
            }

            // Check key length (should be reasonably long).
            if ( strlen( $key_value ) < 64 ) {
                $this->add_issue(
                    'high',
                    sprintf(
                        /* translators: %s: Key name */
                        __( 'Weak Security Key: %s', 'wp-security-scanner' ),
                        $key
                    ),
                    __( 
                        'Security key is too short. Use a longer, more complex key for better security.', 
                        'wp-security-scanner' 
                    ),
                    array( 'key' => $key )
                );
            }
        }
    }

    /**
     * Check file upload settings.
     *
     * @return void
     */
    private function check_file_upload_settings() {
        $allowed_file_types = get_allowed_mime_types();

        // Check if dangerous file types are allowed.
        $dangerous_types = array(
            'exe'  => 'application/x-msdownload',
            'php'  => 'application/x-php',
            'phtml' => 'application/x-php',
            'sh'   => 'application/x-sh',
        );

        foreach ( $dangerous_types as $ext => $mime ) {
            if ( isset( $allowed_file_types[ $ext ] ) ) {
                $this->add_issue(
                    'high',
                    __( 'Dangerous File Type Allowed', 'wp-security-scanner' ),
                    sprintf(
                        /* translators: %s: File extension */
                        __( 
                            'File type "%s" is allowed for upload. This could allow execution of malicious code.', 
                            'wp-security-scanner' 
                        ),
                        esc_html( $ext )
                    ),
                    array(
                        'extension' => $ext,
                        'mime_type' => $mime,
                    )
                );
            }
        }
    }
}

Conclusion

Building your own WordPress security scanner provides deep insights into your site’s security posture and allows you to customize checks for your specific needs. This implementation follows WordPress coding standards, uses best security practices, and provides a solid foundation that can be extended with additional checks.

Remember to keep your scanner updated, regularly review new vulnerability patterns, and contribute back to the WordPress security community by responsibly disclosing any vulnerabilities you discover.

Comments (0)

← WordPress Log Analysis Deep Dive - Complete Guide to Debug, Error & Access Logs Building Your Own Object Cache Drop-In with PHPFastCache Plugin →
Share this page
Back to top