mirror of
https://gitlab.com/ArkHost/WP-Security-Pack.git
synced 2026-09-19 09:27:30 +02:00
339 lines
8.2 KiB
PHP
339 lines
8.2 KiB
PHP
<?php
|
|
/**
|
|
* File integrity monitoring for Security Pack.
|
|
*
|
|
* @package ArkHost_Security_Pack
|
|
*/
|
|
|
|
// Prevent direct access.
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* File integrity monitoring class.
|
|
*/
|
|
class ARKSP_File_Integrity {
|
|
|
|
/**
|
|
* Option key for file hashes.
|
|
*
|
|
* @var string
|
|
*/
|
|
const HASHES_OPTION = 'arksp_file_hashes';
|
|
|
|
/**
|
|
* Option key for last scan time.
|
|
*
|
|
* @var string
|
|
*/
|
|
const LAST_SCAN_OPTION = 'arksp_file_integrity_last_scan';
|
|
|
|
/**
|
|
* Option key for changed files.
|
|
*
|
|
* @var string
|
|
*/
|
|
const CHANGES_OPTION = 'arksp_file_changes';
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct() {
|
|
if ( ! ARKSP_Plugin::get_setting( 'file_integrity_enabled', true ) ) {
|
|
return;
|
|
}
|
|
|
|
// Schedule daily scan.
|
|
add_action( 'arksp_daily_file_scan', array( $this, 'run_scheduled_scan' ) );
|
|
|
|
if ( ! wp_next_scheduled( 'arksp_daily_file_scan' ) ) {
|
|
wp_schedule_event( time(), 'daily', 'arksp_daily_file_scan' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run scheduled scan.
|
|
*/
|
|
public function run_scheduled_scan() {
|
|
$changes = $this->scan_core_files();
|
|
|
|
if ( ! empty( $changes['modified'] ) || ! empty( $changes['added'] ) || ! empty( $changes['removed'] ) ) {
|
|
// Store changes.
|
|
update_option( self::CHANGES_OPTION, $changes );
|
|
|
|
// Send alert if enabled.
|
|
$this->send_alert( $changes );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scan WordPress core files.
|
|
*
|
|
* @param bool $update_baseline Whether to update the baseline.
|
|
* @return array
|
|
*/
|
|
public function scan_core_files( $update_baseline = false ) {
|
|
global $wp_version;
|
|
|
|
$changes = array(
|
|
'modified' => array(),
|
|
'added' => array(),
|
|
'removed' => array(),
|
|
);
|
|
|
|
// Get stored hashes.
|
|
$stored_hashes = get_option( self::HASHES_OPTION, array() );
|
|
|
|
// Get official checksums from WordPress.org.
|
|
$official_checksums = $this->get_official_checksums( $wp_version );
|
|
|
|
// Current file hashes.
|
|
$current_hashes = array();
|
|
|
|
// Core directories to scan.
|
|
$core_paths = array(
|
|
ABSPATH . 'wp-admin/',
|
|
ABSPATH . 'wp-includes/',
|
|
ABSPATH . 'index.php',
|
|
ABSPATH . 'wp-activate.php',
|
|
ABSPATH . 'wp-blog-header.php',
|
|
ABSPATH . 'wp-comments-post.php',
|
|
ABSPATH . 'wp-cron.php',
|
|
ABSPATH . 'wp-links-opml.php',
|
|
ABSPATH . 'wp-load.php',
|
|
ABSPATH . 'wp-login.php',
|
|
ABSPATH . 'wp-mail.php',
|
|
ABSPATH . 'wp-settings.php',
|
|
ABSPATH . 'wp-signup.php',
|
|
ABSPATH . 'wp-trackback.php',
|
|
ABSPATH . 'xmlrpc.php',
|
|
);
|
|
|
|
// Scan each path.
|
|
foreach ( $core_paths as $path ) {
|
|
if ( is_file( $path ) ) {
|
|
$relative_path = str_replace( ABSPATH, '', $path );
|
|
$hash = md5_file( $path );
|
|
|
|
$current_hashes[ $relative_path ] = $hash;
|
|
} elseif ( is_dir( $path ) ) {
|
|
$files = $this->get_directory_files( $path );
|
|
|
|
foreach ( $files as $file ) {
|
|
$relative_path = str_replace( ABSPATH, '', $file );
|
|
$hash = md5_file( $file );
|
|
|
|
$current_hashes[ $relative_path ] = $hash;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compare with official checksums if available.
|
|
if ( ! empty( $official_checksums ) ) {
|
|
foreach ( $current_hashes as $file => $hash ) {
|
|
if ( isset( $official_checksums[ $file ] ) ) {
|
|
if ( $hash !== $official_checksums[ $file ] ) {
|
|
$changes['modified'][] = array(
|
|
'file' => $file,
|
|
'expected' => $official_checksums[ $file ],
|
|
'actual' => $hash,
|
|
'source' => 'official',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} elseif ( ! empty( $stored_hashes ) ) {
|
|
// Compare with stored baseline.
|
|
foreach ( $current_hashes as $file => $hash ) {
|
|
if ( isset( $stored_hashes[ $file ] ) ) {
|
|
if ( $hash !== $stored_hashes[ $file ] ) {
|
|
$changes['modified'][] = array(
|
|
'file' => $file,
|
|
'previous' => $stored_hashes[ $file ],
|
|
'current' => $hash,
|
|
'source' => 'baseline',
|
|
);
|
|
}
|
|
} else {
|
|
$changes['added'][] = $file;
|
|
}
|
|
}
|
|
|
|
// Check for removed files.
|
|
foreach ( $stored_hashes as $file => $hash ) {
|
|
if ( ! isset( $current_hashes[ $file ] ) ) {
|
|
$changes['removed'][] = $file;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update baseline if requested or first scan.
|
|
if ( $update_baseline || empty( $stored_hashes ) ) {
|
|
update_option( self::HASHES_OPTION, $current_hashes );
|
|
}
|
|
|
|
// Update last scan time.
|
|
update_option( self::LAST_SCAN_OPTION, time() );
|
|
|
|
return $changes;
|
|
}
|
|
|
|
/**
|
|
* Get official checksums from WordPress.org.
|
|
*
|
|
* @param string $version WordPress version.
|
|
* @return array
|
|
*/
|
|
private function get_official_checksums( $version ) {
|
|
$locale = get_locale();
|
|
|
|
// Try to get from cache.
|
|
$cache_key = 'arksp_checksums_' . md5( $version . $locale );
|
|
$cached = get_transient( $cache_key );
|
|
|
|
if ( false !== $cached ) {
|
|
return $cached;
|
|
}
|
|
|
|
// Fetch from WordPress.org.
|
|
$url = sprintf(
|
|
'https://api.wordpress.org/core/checksums/1.0/?version=%s&locale=%s',
|
|
$version,
|
|
$locale
|
|
);
|
|
|
|
$response = wp_remote_get( $url, array( 'timeout' => 30 ) );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return array();
|
|
}
|
|
|
|
$body = wp_remote_retrieve_body( $response );
|
|
$data = json_decode( $body, true );
|
|
|
|
if ( ! isset( $data['checksums'] ) ) {
|
|
return array();
|
|
}
|
|
|
|
$checksums = $data['checksums'];
|
|
|
|
// Cache for 1 day.
|
|
set_transient( $cache_key, $checksums, DAY_IN_SECONDS );
|
|
|
|
return $checksums;
|
|
}
|
|
|
|
/**
|
|
* Get all PHP files in a directory recursively.
|
|
*
|
|
* @param string $dir Directory path.
|
|
* @return array
|
|
*/
|
|
private function get_directory_files( $dir ) {
|
|
$files = array();
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
|
|
foreach ( $iterator as $file ) {
|
|
if ( $file->isFile() ) {
|
|
$ext = strtolower( $file->getExtension() );
|
|
// Only track PHP files and critical files.
|
|
if ( in_array( $ext, array( 'php', 'js', 'css' ), true ) ) {
|
|
$files[] = $file->getPathname();
|
|
}
|
|
}
|
|
}
|
|
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* Send email alert about file changes.
|
|
*
|
|
* @param array $changes File changes.
|
|
*/
|
|
private function send_alert( $changes ) {
|
|
if ( ! ARKSP_Plugin::get_setting( 'email_alerts_enabled', false ) ) {
|
|
return;
|
|
}
|
|
|
|
$email = ARKSP_Plugin::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
|
|
$site_name = get_bloginfo( 'name' );
|
|
$site_url = home_url();
|
|
|
|
$subject = sprintf(
|
|
/* translators: %s: Site name */
|
|
__( '[%s] File Integrity Alert - Core Files Changed', 'arkhost-security-pack' ),
|
|
$site_name
|
|
);
|
|
|
|
$message = sprintf(
|
|
/* translators: %s: Site URL */
|
|
__( "ArkHost Security Pack has detected changes to WordPress core files on %s.\n\n", 'arkhost-security-pack' ),
|
|
$site_url
|
|
);
|
|
|
|
if ( ! empty( $changes['modified'] ) ) {
|
|
$message .= __( "Modified files:\n", 'arkhost-security-pack' );
|
|
foreach ( $changes['modified'] as $file ) {
|
|
$message .= '- ' . ( is_array( $file ) ? $file['file'] : $file ) . "\n";
|
|
}
|
|
$message .= "\n";
|
|
}
|
|
|
|
if ( ! empty( $changes['added'] ) ) {
|
|
$message .= __( "New files detected:\n", 'arkhost-security-pack' );
|
|
foreach ( $changes['added'] as $file ) {
|
|
$message .= '- ' . $file . "\n";
|
|
}
|
|
$message .= "\n";
|
|
}
|
|
|
|
if ( ! empty( $changes['removed'] ) ) {
|
|
$message .= __( "Removed files:\n", 'arkhost-security-pack' );
|
|
foreach ( $changes['removed'] as $file ) {
|
|
$message .= '- ' . $file . "\n";
|
|
}
|
|
$message .= "\n";
|
|
}
|
|
|
|
$message .= __( "This could indicate:\n", 'arkhost-security-pack' );
|
|
$message .= __( "- A recent WordPress update (normal)\n", 'arkhost-security-pack' );
|
|
$message .= __( "- Unauthorized modifications (investigate)\n", 'arkhost-security-pack' );
|
|
$message .= __( "- Plugin/theme conflicts (rare)\n\n", 'arkhost-security-pack' );
|
|
$message .= __( "Review these changes in your WordPress admin panel.", 'arkhost-security-pack' );
|
|
|
|
wp_mail( $email, $subject, $message );
|
|
}
|
|
|
|
/**
|
|
* Get last scan results.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function get_last_scan_results() {
|
|
return array(
|
|
'time' => get_option( self::LAST_SCAN_OPTION, 0 ),
|
|
'changes' => get_option( self::CHANGES_OPTION, array() ),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clear file changes.
|
|
*/
|
|
public function clear_changes() {
|
|
delete_option( self::CHANGES_OPTION );
|
|
}
|
|
|
|
/**
|
|
* Reset baseline.
|
|
*/
|
|
public function reset_baseline() {
|
|
delete_option( self::HASHES_OPTION );
|
|
delete_option( self::CHANGES_OPTION );
|
|
}
|
|
}
|