first commit

This commit is contained in:
Yuri Karamian
2026-01-25 20:21:17 +01:00
commit 9f06a4c2a2
24 changed files with 9192 additions and 0 deletions
@@ -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 );
}
}