mirror of
https://gitlab.com/ArkHost/WP-Security-Pack.git
synced 2026-09-19 17:37:30 +02:00
first commit
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
/**
|
||||
* File integrity monitoring for WP Security Pack.
|
||||
*
|
||||
* @package WP_Security_Pack
|
||||
*/
|
||||
|
||||
// Prevent direct access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* File integrity monitoring class.
|
||||
*/
|
||||
class WPSP_File_Integrity {
|
||||
|
||||
/**
|
||||
* Option key for file hashes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const HASHES_OPTION = 'wpsp_file_hashes';
|
||||
|
||||
/**
|
||||
* Option key for last scan time.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const LAST_SCAN_OPTION = 'wpsp_file_integrity_last_scan';
|
||||
|
||||
/**
|
||||
* Option key for changed files.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CHANGES_OPTION = 'wpsp_file_changes';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( ! WP_Security_Pack::get_setting( 'file_integrity_enabled', true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule daily scan.
|
||||
add_action( 'wpsp_daily_file_scan', array( $this, 'run_scheduled_scan' ) );
|
||||
|
||||
if ( ! wp_next_scheduled( 'wpsp_daily_file_scan' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'wpsp_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 = 'wpsp_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 ( ! WP_Security_Pack::get_setting( 'email_alerts_enabled', false ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = WP_Security_Pack::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', 'wp-security-pack' ),
|
||||
$site_name
|
||||
);
|
||||
|
||||
$message = sprintf(
|
||||
/* translators: %s: Site URL */
|
||||
__( "WP Security Pack has detected changes to WordPress core files on %s.\n\n", 'wp-security-pack' ),
|
||||
$site_url
|
||||
);
|
||||
|
||||
if ( ! empty( $changes['modified'] ) ) {
|
||||
$message .= __( "Modified files:\n", 'wp-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", 'wp-security-pack' );
|
||||
foreach ( $changes['added'] as $file ) {
|
||||
$message .= '- ' . $file . "\n";
|
||||
}
|
||||
$message .= "\n";
|
||||
}
|
||||
|
||||
if ( ! empty( $changes['removed'] ) ) {
|
||||
$message .= __( "Removed files:\n", 'wp-security-pack' );
|
||||
foreach ( $changes['removed'] as $file ) {
|
||||
$message .= '- ' . $file . "\n";
|
||||
}
|
||||
$message .= "\n";
|
||||
}
|
||||
|
||||
$message .= __( "This could indicate:\n", 'wp-security-pack' );
|
||||
$message .= __( "- A recent WordPress update (normal)\n", 'wp-security-pack' );
|
||||
$message .= __( "- Unauthorized modifications (investigate)\n", 'wp-security-pack' );
|
||||
$message .= __( "- Plugin/theme conflicts (rare)\n\n", 'wp-security-pack' );
|
||||
$message .= __( "Review these changes in your WordPress admin panel.", 'wp-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 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user