Plugin Directory

Changeset 3334658


Ignore:
Timestamp:
07/26/2025 10:47:22 PM (7 months ago)
Author:
cloudaware
Message:

Adding user management and folder hashing

Location:
cloudaware-security-audit/trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • cloudaware-security-audit/trunk/cloudaware-security-audit.php

    r3331590 r3334658  
    44Plugin URI:   https://www.cloudaware.eu
    55Description:  Plugin to monitor and audit security aspects of your Wordpress installation
    6 Version:      1.0.8
     6Version:      1.0.9
    77Author:       Jeroen Hermans
    88License:      GPLv2
     
    1111
    1212defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
     13define("REQUESTHEADERS", array(
     14    'timeout' => 10,
     15    'User-Agent' => 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0'
     16));
    1317
    1418function cloudseca_make_data() {
     
    1721  //This include is needed to get all updates for plugins in the same way as:
    1822  //https://wordpress.org/plugins/wpvulnerability/
    19     if ( ! function_exists( 'get_plugin_updates' ) ) {
    20         require_once ABSPATH . 'wp-admin/includes/update.php';
    21     }
     23  if ( ! function_exists( 'get_plugin_updates' ) ) {
     24    require_once ABSPATH . 'wp-admin/includes/update.php';
     25  }
    2226  $plugin_updates = get_plugin_updates();
    2327  $theme_updates  = get_theme_updates();
     
    2630  //This include is needed to get all plugins installed on the system in the same way as:
    2731  //https://wordpress.org/plugins/wpvulnerability/
    28     if ( ! function_exists( 'get_plugins' ) ) {
    29         require_once ABSPATH . 'wp-admin/includes/plugin.php';
    30     }
    31     $plugins        = get_plugins();
    32     $themes         = wp_get_themes();
     32  if ( ! function_exists( 'get_plugins' ) ) {
     33    require_once ABSPATH . 'wp-admin/includes/plugin.php';
     34  }
     35  $plugins        = get_plugins();
     36  $themes         = wp_get_themes();
    3337
    3438  //Global
     
    4751  //Optionally include wpvulnerability in order to get data about vulnerabilities
    4852  $wpvulnerabilities = array();
    49     if ( ! function_exists( 'wpvulnerability_plugin_get_vulnerabilities' ) ) {
     53  if ( ! function_exists( 'wpvulnerability_plugin_get_vulnerabilities' ) ) {
    5054    if (defined('WPVULNERABILITY_PLUGIN_PATH')) {
    51         $file_path = WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-plugins.php';
    52         if ( file_exists($file_path) ) {
    53             require_once $file_path;
     55      $file_path = WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-plugins.php';
     56      if ( file_exists($file_path) ) {
     57        require_once $file_path;
    5458      }
    5559    }
    5660    $wpvulnerabilities = wpvulnerability_plugin_get_vulnerabilities();
    57     }
     61  }
    5862
    5963  $data = array('global_autoupdates' => array('themes' => $global_theme_autoupdate, 'plugins' => $global_plugin_autoupdate),
     
    6367                'url'     => get_option( 'siteurl' ),
    6468                'time'    => time(),
    65                 'config'  => array()
     69                'config'  => cloudseca_get_config($plugins),
     70                'themehashes'  => hashFoldersInDirectory(ABSPATH, 'wp-content/themes'),
     71                'pluginhashes' => hashFoldersInDirectory(ABSPATH, 'wp-content/plugins')
    6672          );
    6773
     
    9399    //This include is needed to get information about installed plugins on the system in the same way as:
    94100    //https://wordpress.org/plugins/wpvulnerability/
    95       if ( ! function_exists( 'plugins_api' ) ) {
    96           require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
    97       }
     101    if ( ! function_exists( 'plugins_api' ) ) {
     102      require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
     103    }
    98104    $call_api = plugins_api( 'plugin_information', $args );
    99105    $plugindata['Active_installs'] = $call_api->active_installs;
     
    105111    if($name == 'revslider/revslider.php') {
    106112      $url = 'https://www.sliderrevolution.com/documentation/changelog/';
    107       $res = wp_remote_get($url);
     113      $res = wp_remote_get($url, REQUESTHEADERS);
    108114      $html = wp_remote_retrieve_body($res);
    109115      $dom = new DOMDocument;
     
    114120      preg_match_all($re, $nodevalue, $matches, PREG_SET_ORDER, 0);
    115121
    116       $plugindata['Version_latest']      = $matches[0][1];
    117       $plugindata['Version_latest_date'] = date_parse($matches[0][2]);
    118       $plugindata['Version_latest_date'] = $plugindata['Version_latest_date']['year'].'-'.$plugindata['Version_latest_date']['month'].'-'.$plugindata['Version_latest_date']['day'];
     122      $plugindata['version_latest']      = $matches[0][1];
     123      $plugindata['version_latest_date'] = date_parse($matches[0][2]);
     124      $plugindata['version_latest_date'] = $plugindata['Version_latest_date']['year'].'-'.$plugindata['Version_latest_date']['month'].'-'.$plugindata['Version_latest_date']['day'];
    119125    } else {
    120126      if( array_key_exists($name, $plugin_updates) ) {
    121         $plugindata['Version_latest'] = $plugin_updates[$name]->update->new_version;
     127        $plugindata['version_latest'] = $plugin_updates[$name]->update->new_version;
    122128      } else {
    123         $plugindata['Version_latest'] = $plugindata['Version'];
     129        $plugindata['version_latest'] = $plugindata['Version'];
    124130      }
    125131    }
     
    128134  $active_theme = wp_get_theme()->get_stylesheet();
    129135  foreach($themes as $name => &$themedata) {
    130     if( in_array($name, $auto_update_themes) ) {
    131       $data['themes'][$name]['Autoupdate'] = true;
    132     } else {
    133       $data['themes'][$name]['Autoupdate'] = false;
    134     }
    135    
    136     if($active_theme == $name) {
    137       $data['themes'][$name]['Active'] = true;
    138     } else {
    139       $data['themes'][$name]['Active'] = false;
    140     }
     136    $data['themes'][$name]['autoupdate'] = in_array($name, $auto_update_themes);
     137    $data['themes'][$name]['active'] = ($active_theme == $name);
    141138
    142139    $themedetails                     = wp_get_theme($name);
    143140    $data['themes'][$name]['Update']  = $themedata->update;
    144141    $data['themes'][$name]['Name']    = $themedetails->get('Name');
    145     $data['themes'][$name]['Version'] = $themedetails->get('Version');
     142    $data['themes'][$name]['version'] = $themedetails->get('version');
    146143
    147144    if( array_key_exists($name, $theme_updates) ) {
    148       $data['themes'][$name]['Version_latest'] = $theme_updates[$name]->update['new_version'];
     145      $data['themes'][$name]['version_latest'] = $theme_updates[$name]->update['new_version'];
    149146    } else {
    150       $data['themes'][$name]['Version_latest'] = $data['themes'][$name]['Version'];
     147      $data['themes'][$name]['version_latest'] = $data['themes'][$name]['version'];
    151148    }
    152149  }
     
    162159
    163160    $url = 'https://api.github.com/repos/ImageMagick/ImageMagick/releases';
    164     $res = wp_remote_get($url, array(
    165       'timeout' => 10,
    166       'User-Agent' => 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0'
    167     ));
     161    $res = wp_remote_get($url, REQUESTHEADERS);
    168162    $json = wp_remote_retrieve_body($res);
    169163
     
    171165    $latest_version = $releases[0]['name'];
    172166
    173     $data['imagemagick']['Version']        = $current_version;
    174     $data['imagemagick']['Version_latest'] = $latest_version;
     167    $data['imagemagick']['version']        = $current_version;
     168    $data['imagemagick']['version_latest'] = $latest_version;
    175169  } catch(Exception $e) {}
    176170
     
    178172    $data['curl'] = array();
    179173  }
    180   $data['curl']['Version'] = curl_version()['version'];
     174  $data['curl']['version'] = curl_version()['version'];
    181175
    182176  $url  = 'https://api.github.com/repos/curl/curl/releases';
    183   $res = wp_remote_get($url, array(
    184     'timeout' => 10,
    185     'User-Agent' => 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0'
    186   ));
     177  $res = wp_remote_get($url, REQUESTHEADERS);
    187178  $json = wp_remote_retrieve_body($res);
    188179
    189180  $obj  = json_decode($json, true);
    190   $data['curl']['Version_latest'] = $obj[0]['name'];
    191 
    192 
    193   #2FA
    194   $data['config']['2fa_enabled'] = false;
    195   if (
    196     array_key_exists('wordfence/wordfence.php', $data['plugins']) &&
    197     function_exists('is_plugin_active') &&
    198     is_plugin_active('wordfence/wordfence.php')
    199   ) { #Wordfence is installed
    200     // Define the roles to check
    201     $target_roles = ['administrator', 'contributor', 'editor'];
    202 
    203     // Get all defined roles
    204     if (!function_exists('get_editable_roles')) {
    205       // needed for get_editable_roles()
    206       require_once ABSPATH . 'wp-admin/includes/user.php';
    207     }
    208     $all_roles = get_editable_roles();
    209 
    210     // Filter roles that exist
    211     $existing_roles = array_filter($target_roles, function($role) use ($all_roles) {
    212       return array_key_exists($role, $all_roles);
    213     });
    214 
    215     if (!empty($existing_roles)) {
    216       // Prepare setting keys for those roles
    217       $setting_keys = array_map(function($role) {
    218         return "required-2fa-role.$role";
    219       }, $existing_roles);
    220 
    221       // Try to get cached results
    222       $cache_key = 'wordfence_2fa_roles_settings';
    223       $settings = wp_cache_get($cache_key, 'wordfence');
    224       if ($settings === false) {
    225         $placeholders = implode(', ', array_fill(0, count($setting_keys), '%s'));
    226         $table_name = $wpdb->prefix . 'wfls_settings';
    227 
    228         // Fetch settings in a single query
    229         $query = "SELECT name, value FROM $table_name WHERE name IN ($placeholders)";
    230         // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
    231         $results = $wpdb->get_results($wpdb->prepare($query, ...$setting_keys), OBJECT_K);
    232         // Cache the results
    233         $settings = [];
    234         foreach ($results as $row) {
    235           $settings[$row->name] = $row->value;
    236         }
    237 
    238         wp_cache_set($cache_key, $settings, 'wordfence', 300); // Cache for 5 minutes
    239       }
    240 
    241       $all_roles_have_2fa = true;
    242       foreach ($existing_roles as $role) {
    243         $key = "required-2fa-role.$role";
    244         if (!isset($settings[$key]) || intval($settings[$key]) <= 0) {
    245           $all_roles_have_2fa = false;
    246           break;
    247         }
    248       }
    249 
    250       if ($all_roles_have_2fa) {
    251         $data['config']['2fa_enabled'] = true;
    252       }
    253     }
    254   }
    255 
    256   #Configuration
    257   if ( username_exists( 'admin' ) ) {
    258       $data['config']['admin_user_found'] = true;
    259   } else {
    260       $data['config']['admin_user_found'] = false;
    261   }
    262   if (defined('DISALLOW_FILE_EDIT')) {
    263       $data['config']['disallow_file_edit'] = true;
    264   } else {
    265       $data['config']['disallow_file_edit'] = false;
    266   }
    267   if (defined('WP_DEBUG') && WP_DEBUG) {
    268       $data['config']['debug'] = true;
    269   } else {
    270       $data['config']['debug'] = false;
    271   }
    272   if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
    273       $data['config']['debug_log'] = true;
    274   } else {
    275       $data['config']['debug_log'] = false;
    276   }
    277   if (defined('WP_DEBUG_DISPLAY') && WP_DEBUG_DISPLAY) {
    278       $data['config']['debug_display'] = true;
    279   } else {
    280       $data['config']['debug_display'] = false;
    281   }
    282   if (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) {
    283       $data['config']['script_debug'] = true;
    284   } else {
    285       $data['config']['script_debug'] = false;
    286   }
    287 
    288 
    289   if (defined('WP_HOME') && strpos(WP_HOME, 'https://') === 0) {
    290       $data['config']['home_https'] = true;
    291   } else {
    292       $data['config']['home_https'] = false;
    293   }
    294   if (defined('WP_SITEURL') && strpos(WP_SITEURL, 'https://') === 0) {
    295       $data['config']['siteurl_https'] = true;
    296   } else {
    297       $data['config']['siteurl_https'] = false;
    298   }
    299   if (defined('FORCE_SSL_ADMIN') && strpos(FORCE_SSL_ADMIN, 'https://') === 0) {
    300       $data['config']['force_ssl_admin'] = true;
    301   } else {
    302       $data['config']['force_ssl_admin'] = false;
    303   }
    304 
    305   if (defined('AUTOSAVE_INTERVAL')) {
    306       $data['config']['autosave_interval'] = AUTOSAVE_INTERVAL;
    307   } else {
    308       $data['config']['autosave_interval'] = null;
    309   }
    310   if (defined('WP_POST_REVISIONS')) {
    311       $data['config']['post_revisions'] = WP_POST_REVISIONS;
    312   } else {
    313       $data['config']['post_revisions'] = null;
    314   }
    315   if (defined('EMPTY_TRASH_DAYS')) {
    316       $data['config']['empty_trash_days'] = EMPTY_TRASH_DAYS;
    317   } else {
    318       $data['config']['empty_trash_days'] = null;
    319   }
    320   if (defined('WP_MEMORY_LIMIT')) {
    321       $data['config']['memory_limit'] = WP_MEMORY_LIMIT;
    322   } else {
    323       $data['config']['memory_limit'] = null;
    324   }
    325 
    326 
    327   $url = rtrim(get_option( 'siteurl' ), "/");
    328   $url .= '/xmlrpc.php';
    329   $res = wp_remote_get($url, array(
    330     'timeout' => 10,
    331     'User-Agent' => 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0'
    332   ));
    333   if (wp_remote_retrieve_response_code($res) == 200 ) {
    334       $data['config']['xmlrpc_enabled'] = true;
    335   } else {
    336       $data['config']['xmlrpc_enabled'] = false;
    337   }
    338   $data['config']['table_prefix'] = $wpdb->prefix;
     181  $data['curl']['version_latest'] = $obj[0]['name'];
    339182
    340183  return $data;
    341184}
    342185
    343 //see wp-admin/update-core.php
     186//rest api endpoint function
    344187function cloudseca_security_status (WP_REST_Request $request) {
    345     #if ( ! function_exists( 'core_auto_updates_settings' ) ) {
    346     #   require_once ABSPATH . 'wp-admin/update-core.php';
    347     #}
    348 
    349   #if(function_exists('needed_function')){
    350   #  needed_function();
    351   #}
    352 
    353   #$wp_version     = wp_get_wp_version();
    354   #$cur_wp_version = preg_replace( '/-.*$/', '', $wp_version );
    355   //$data     = array('name'=>$request->get_param( 'naam' ));
    356188  $data = cloudseca_make_data();
    357189  return new WP_REST_Response( $data );
     
    378210
    379211function cloudseca_menu() {
    380     add_options_page( 'CloudAware', 'CloudAware Security', 'manage_options', 'cloudseca-admin-menu', 'cloudseca_options' );
     212  add_options_page( 'CloudAware', 'CloudAware Security', 'manage_options', 'cloudseca-admin-menu', 'cloudseca_options' );
    381213}
    382214
    383215function cloudseca_options() {
    384     if ( !current_user_can( 'manage_options' ) )  {
    385         wp_die( 'You do not have sufficient permissions to access this page.' );
    386     }
     216  if ( !current_user_can( 'manage_options' ) )  {
     217    wp_die( 'You do not have sufficient permissions to access this page.' );
     218  }
    387219  echo "<h2>".esc_html("Cloudaware Security Settings")."</h2>\n";
    388220  echo "<form action=\"".esc_url("options.php")."\" method=\"post\">\n";
    389221  settings_fields( 'cloudseca_plugin_options' );
    390222  do_settings_sections( 'cloudseca_plugin' );
    391   echo "<input name=\"submit\" class=\"button button-primary\" type=\"submit\" value=\"". esc_attr( 'Save' ). "\" />\n";
     223  #echo "<input name=\"submit\" class=\"button button-primary\" type=\"submit\" value=\"". esc_attr( 'Save' ). "\" />\n";
     224  submit_button('Save Settings');
    392225  echo "</form>\n";
     226
     227  echo "<button id=\"cloudseca_activate_btn\" class=\"button button-secondary\">Create role and user</button>\n";
     228  echo "<div id=\"cloudseca_modal\" style=\"display:none;\">\n";
     229  echo "  <p><span class=\"dashicons dashicons-warning\" style=\"color: #d63638; font-size: 18px; vertical-align: middle; margin-right: 6px;\"></span>\n";
     230  echo "  A new user <strong>cloudaware</strong> will be created with minimal access (role <code>cloudseca_api</code>).<br>\n";
     231  echo "  If a cloudaware.eu callback url has been defined, a secure application password will be generated and sent to CloudAware’s secure callback URL for monitoring. If the callback url is not in the cloudaware.eu domain, it will be shown to you once and not send anywhere else.</p>\n";
     232  echo "  <button id=\"cloudseca_confirm_btn\" class=\"button button-primary\" style=\"background-color: #28a745; border-color: #28a745;\">Confirm</button>\n";
     233  echo "  <button id=\"cloudseca_cancel_btn\" class=\"button\" style=\"background-color: #dc3545; border-color: #dc3545; color: white;\">Cancel</button>\n";
     234  echo "</div>\n";
     235  echo "<div id=\"cloudseca_response\"></div>\n";
     236
     237  echo "<script>\n";
     238  echo "document.addEventListener('DOMContentLoaded', function () {\n";
     239  echo "    const activateBtn = document.getElementById('cloudseca_activate_btn');\n";
     240  echo "    const modal = document.getElementById('cloudseca_modal');\n";
     241  echo "    const confirmBtn = document.getElementById('cloudseca_confirm_btn');\n";
     242  echo "    const cancelBtn = document.getElementById('cloudseca_cancel_btn');\n";
     243  echo "    const response = document.getElementById('cloudseca_response');\n\n";
     244
     245  echo "    activateBtn.addEventListener('click', function(e) {\n";
     246  echo "        e.preventDefault();\n";
     247  echo "        modal.style.display = 'block';\n";
     248  echo "    });\n\n";
     249
     250  echo "    cancelBtn.addEventListener('click', function() {\n";
     251  echo "        modal.style.display = 'none';\n";
     252  echo "    });\n\n";
     253
     254  echo "    confirmBtn.addEventListener('click', function() {\n";
     255  echo "        modal.style.display = 'none';\n";
     256  echo "        response.innerHTML = 'Activating...';\n";
     257  echo "        fetch(ajaxurl, {\n";
     258  echo "            method: 'POST',\n";
     259  echo "            headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n";
     260  echo "            body: 'action=cloudseca_activate_api_user&_wpnonce=".esc_js(wp_create_nonce("cloudseca_nonce")) ."'\n";
     261  echo "        })\n";
     262  echo "        .then(res => res.json())\n";
     263  echo "        .then(data => {\n";
     264  echo "            response.innerHTML = data.success ? '<strong>Success:</strong> ' + data.data.message : '<strong>Error:</strong> ' + data.data.message;\n";
     265  echo "        })\n";
     266  echo "        .catch(err => {\n";
     267  echo "            response.innerHTML = '<strong>Error:</strong> Could not connect.';\n";
     268  echo "        });\n";
     269  echo "    });\n";
     270  echo "});\n";
     271  echo "</script>\n";
    393272}
    394273
    395274function cloudseca_register_settings() {
     275    // phpcs:ignore PluginCheck.CodeAnalysis.SettingSanitization.register_settingDynamic
    396276    register_setting( 'cloudseca_plugin_options', //settings group name
    397277                      'cloudseca_plugin_options', //name of option
     
    414294}
    415295add_action( 'admin_init', 'cloudseca_register_settings' );
    416 
     296add_action('wp_ajax_cloudseca_activate_api_user', 'cloudseca_handle_api_user_creation');
    417297
    418298function cloudseca_plugin_options_validate( $input ) {
    419     #var_dump($input);
    420299    $newinput['callback_url'] = trim( $input['callback_url'] );
    421300    #if ( ! preg_match( '/^[a-z0-9]{32}$/i', $newinput['api_key'] ) ) {
     
    438317//get_option('dbi_example_plugin_options')[api_key]
    439318
    440 
    441 
     319function cloudseca_handle_api_user_creation() {
     320    check_ajax_referer('cloudseca_nonce');
     321
     322    $role_name  = 'cloudseca_api';
     323    $role_label = 'Cloudseca API';
     324    $username   = 'cloudaware';
     325    $email      = 'wordpresssecurity@cloudaware.eu';
     326    // The exact permissions this role should have
     327    $desired_perms = [
     328        'activate_plugins'        => true,
     329        'list_users'              => true,
     330        'read'                    => true,
     331        'switch_themes'           => true,
     332        'view_site_health_checks' => true,
     333    ];
     334
     335    // 1. Create role if needed
     336    $roles = wp_roles();
     337    if (!$roles->is_role($role_name)) {
     338        // Role does not exist — create it
     339        add_role($role_name, $role_label, $desired_perms);
     340    } else {
     341        // Role exists — ensure it has only the desired capabilities
     342        $role = get_role($role_name);
     343        if ($role) {
     344            // First, remove all existing caps
     345            foreach ($role->capabilities as $cap => $value) {
     346                $role->remove_cap($cap);
     347            }
     348
     349            // Then, add the desired capabilities
     350            foreach ($desired_perms as $perm => $value) {
     351                $role->add_cap($perm, $value);
     352            }
     353        }
     354    }
     355
     356    // 2. Create user if needed
     357    $user_id = username_exists($username);
     358    if (!$user_id && !email_exists($email)) {
     359        $password = wp_generate_password(24, true);
     360        $user_id = wp_create_user($username, $password, $email);
     361        if (is_wp_error($user_id)) {
     362            wp_send_json_error(['message' => 'Failed to create user.']);
     363        }
     364        $user = get_user_by('id', $user_id);
     365        $user->set_role($role_name);
     366    } else {
     367        // User exists — check and update role if necessary
     368        $user = get_user_by('id', $user_id);
     369        if ($user && $user->role !== $role_name) {
     370            $user->set_role($role_name);
     371        }
     372    }
     373
     374    if (!$user_id) {
     375        wp_send_json_error(['message' => 'User exists but could not retrieve ID.']);
     376    }
     377
     378    // 3. Create application password
     379    if (!class_exists('WP_Application_Passwords')) {
     380        require_once ABSPATH . 'wp-includes/class-wp-application-passwords.php';
     381    }
     382
     383    $app_exists = WP_Application_Passwords::application_name_exists_for_user($user_id, 'cloudaware');
     384    if (!$app_exists) {
     385        $app_pass = WP_Application_Passwords::create_new_application_password($user_id, ['name' => 'cloudaware']);
     386        if (is_wp_error($app_pass)) {
     387            wp_send_json_error(['message' => 'Failed to create application password.']);
     388        }
     389
     390        // 4. Send app pass to callback URL
     391        $options = get_option('cloudseca_plugin_options');
     392        #$callback = get_option('cloudseca_plugin_options')['callback_url'];
     393        $callback = isset($options['callback_url']) ? trim($options['callback_url']) : '';
     394        #if (!$callback || !filter_var($callback, FILTER_VALIDATE_URL)) {
     395        #    wp_send_json_error(['message' => 'Invalid or missing callback URL.']);
     396        #}
     397
     398        if ($callback && stripos($callback, 'cloudaware.eu') !== false && filter_var($callback, FILTER_VALIDATE_URL)) {
     399          $send = wp_remote_post($callback, [
     400              'headers' => ['Content-Type' => 'application/json; charset=utf-8'],
     401              'body' => json_encode([
     402                  'app_pass' => $app_pass[0],
     403                  'url'      => get_option('siteurl'),
     404              ]),
     405              'method'      => 'POST',
     406              'data_format' => 'body',
     407              'timeout'     => 10,
     408          ]);
     409
     410          $code = wp_remote_retrieve_response_code($send);
     411          if ($code >= 200 && $code < 300) {
     412              wp_send_json_success(['message' => 'API user created and app password sent to CloudAware.']);
     413          } else {
     414              wp_send_json_error(['message' => 'App password created but failed to notify CloudAware.']);
     415          }
     416        } else {
     417            // Show password to user
     418            wp_send_json_success([
     419                'message' => 'API user created. Please copy the application password now — it will not be shown again: <code>'.$app_pass[0].'</code>'
     420            ]);        }
     421    } else {
     422        wp_send_json_success(['message' => 'Application password already exists.']);
     423    }
     424}
     425
     426function cloudseca_get_config($plugins){
     427  global $wpdb;
     428  $config = array();
     429
     430  #2FA
     431  $config['2fa_enabled'] = false;
     432  if (
     433    array_key_exists('wordfence/wordfence.php', $plugins) &&
     434    function_exists('is_plugin_active') &&
     435    is_plugin_active('wordfence/wordfence.php')
     436  ) { #Wordfence is installed
     437    // Define the roles to check
     438    $target_roles = ['administrator', 'contributor', 'editor'];
     439
     440    // Get all defined roles
     441    if (!function_exists('get_editable_roles')) {
     442      // needed for get_editable_roles()
     443      require_once ABSPATH . 'wp-admin/includes/user.php';
     444    }
     445    $all_roles = get_editable_roles();
     446
     447    // Filter roles that exist
     448    $existing_roles = array_filter($target_roles, function($role) use ($all_roles) {
     449      return array_key_exists($role, $all_roles);
     450    });
     451
     452    if (!empty($existing_roles)) {
     453      // Prepare setting keys for those roles
     454      $setting_keys = array_map(function($role) {
     455        return "required-2fa-role.$role";
     456      }, $existing_roles);
     457
     458      // Try to get cached results
     459      $cache_key = 'wordfence_2fa_roles_settings';
     460      $settings = wp_cache_get($cache_key, 'wordfence');
     461      if ($settings === false) {
     462        $placeholders = implode(', ', array_fill(0, count($setting_keys), '%s'));
     463        $table_name = $wpdb->prefix . 'wfls_settings';
     464
     465        // Fetch settings in a single query
     466        $query = "SELECT name, value FROM esc_sql($table_name) WHERE name IN ($placeholders)";
     467        // This IS a prepared statement using splat notation
     468        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
     469        $results = $wpdb->get_results($wpdb->prepare($query, ...$setting_keys), OBJECT_K);
     470        // Cache the results
     471        $settings = [];
     472        foreach ($results as $row) {
     473          $settings[$row->name] = $row->value;
     474        }
     475
     476        wp_cache_set($cache_key, $settings, 'wordfence', 300); // Cache for 5 minutes
     477      }
     478
     479      $all_roles_have_2fa = true;
     480      foreach ($existing_roles as $role) {
     481        $key = "required-2fa-role.$role";
     482        if (!isset($settings[$key]) || intval($settings[$key]) <= 0) {
     483          $all_roles_have_2fa = false;
     484          break;
     485        }
     486      }
     487
     488      if ($all_roles_have_2fa) {
     489        $config['2fa_enabled'] = true;
     490      }
     491    }
     492  }
     493
     494  #Configuration
     495  $config['admin_user_found'] = username_exists( 'admin' );
     496  $config['disallow_file_edit'] = defined('DISALLOW_FILE_EDIT');
     497  $config['debug'] = (defined('WP_DEBUG') && WP_DEBUG);
     498  $config['debug_log'] = (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG);
     499  $config['debug_display'] = defined('WP_DEBUG_DISPLAY') && WP_DEBUG_DISPLAY;
     500  $config['script_debug'] = defined('SCRIPT_DEBUG') && SCRIPT_DEBUG;
     501  $config['home_https'] = (defined('WP_HOME') && strpos(WP_HOME, 'https://') === 0) ;
     502  $config['siteurl_https'] = (defined('WP_SITEURL') && strpos(WP_SITEURL, 'https://') === 0);
     503  $config['force_ssl_admin'] = (defined('FORCE_SSL_ADMIN') && strpos(FORCE_SSL_ADMIN, 'https://') === 0);
     504  $config['autosave_interval'] = defined('AUTOSAVE_INTERVAL')?AUTOSAVE_INTERVAL:null;
     505  $config['post_revisions'] = defined('WP_POST_REVISIONS')?WP_POST_REVISIONS:null;
     506  $config['empty_trash_days'] = defined('EMPTY_TRASH_DAYS')?EMPTY_TRASH_DAYS:null;
     507  $config['memory_limit'] = defined('WP_MEMORY_LIMIT')?WP_MEMORY_LIMIT:null;
     508
     509  $url = rtrim(get_option( 'siteurl' ), "/");
     510  $url .= '/xmlrpc.php';
     511  $res = wp_remote_get($url, REQUESTHEADERS);
     512  $config['xmlrpc_enabled'] = (wp_remote_retrieve_response_code($res) == 200);
     513  $config['table_prefix'] = $wpdb->prefix;
     514
     515  return $config;
     516}
     517
     518function getFolderHash($folderPath) {
     519    $fileHashes = [];
     520
     521    $iterator = new RecursiveIteratorIterator(
     522        new RecursiveDirectoryIterator($folderPath, FilesystemIterator::SKIP_DOTS)
     523    );
     524
     525    foreach ($iterator as $file) {
     526        if ($file->isFile()) {
     527            $relativePath = str_replace('\\', '/', substr($file->getPathname(), strlen($folderPath)));
     528            $contentHash = md5_file($file->getPathname());
     529            $fileHashes[$relativePath] = $contentHash;
     530        }
     531    }
     532
     533    // Sort by path to ensure consistent order
     534    ksort($fileHashes);
     535
     536    // Combine all file hashes into a single string
     537    $combined = '';
     538    foreach ($fileHashes as $path => $hash) {
     539        $combined .= $path . ':' . $hash . "\n";
     540    }
     541
     542    // Final folder-level hash
     543    return md5($combined);
     544}
     545
     546function hashFoldersInDirectory($baseDir, $subPath) {
     547    $result = [];
     548    $fullPath = rtrim($baseDir, '/') . '/' . trim($subPath, '/');
     549
     550    if (!is_dir($fullPath)) {
     551        return $result;
     552    }
     553
     554    foreach (scandir($fullPath) as $item) {
     555        if ($item === '.' || $item === '..') continue;
     556        $itemPath = $fullPath . '/' . $item;
     557        if (is_dir($itemPath)) {
     558            $relative = $subPath . '/' . $item;
     559            $result[$relative] = getFolderHash($itemPath);
     560        }
     561    }
     562
     563    return $result;
     564}
    442565
    443566
     
    446569####### https://blazzdev.com/scheduled-tasks-cron-wordpress-plugin-boilerplate/
    447570############################################################################
    448 
    449 #####Initialise
    450 register_activation_hook( __FILE__, 'cloudseca_activate_plugin' );
    451 function cloudseca_activate_plugin() { // runs on plugin activation
    452     #require_once( ABSPATH . 'wp-admin/includes/user.php' );  //wp_create_user()
    453     #require_once( ABSPATH . 'wp-includes/pluggable.php' ); //get_user_by()
    454     #require_once( ABSPATH . 'wp-includes/capabilities.php' ); //add_role()
    455     #require_once( ABSPATH . 'wp-includes/class-wp-application-passwords.php' );
    456 
    457   if ( get_option( 'cloudseca_plugin_options' )                 === false ||
    458        get_option( 'cloudseca_plugin_options' )['callback_url'] === false ||
    459        get_option( 'cloudseca_plugin_options' )['callback_url'] ==  ''
    460   ) {
    461     #add_option( 'cloudseca_plugin_options', array('callback_url' => 'https://app.cloudaware.eu/callbacks') );
    462     add_option( 'cloudseca_plugin_options', array('callback_url' => '') );
    463   }
    464   if ( ! wp_next_scheduled( 'cloudseca_cron_security_check' ) ) {
    465     wp_schedule_event( time(), 'daily', 'cloudseca_cron_security_check' ); // cloudseca_cron_security_check is a hook
    466   }
    467 
    468   #if ( ! wp_roles()->is_role( 'cloudseca_api' ) ) {
    469   #  add_role('cloudseca_api', 'cloudseca_api', array(
    470   #    'activate_plugins' => true,
    471   #    'list_users' => true,
    472   #    'read' => true,
    473   #    'switch_themes' => true,
    474   #    'view_site_health_checks' => true,
    475   #  ));
    476   #}
    477   #$username = 'cloudaware';
    478   #$email    = 'wordpresssecurity@cloudaware.eu';
    479     #$password = wp_generate_password(32, true);
    480 
    481   #$user_id = username_exists( $username );
    482   #if ( !$user_id && email_exists($email) == false ) {
    483   #  $user_id = wp_create_user( $username, $password, $email );
    484   #  if( !is_wp_error($user_id) ) {
    485   #      $user = get_user_by( 'id', $user_id );
    486   #      $user->set_role( 'cloudseca_api' );
    487   #  }
    488   #}
    489   #Future use from configuration page to send an app password to cloudaware as a sort of activation
    490   #This will always be with consent of the admin
    491   #$app_exists = WP_Application_Passwords::application_name_exists_for_user( $user_id, 'cloudaware' );
    492   #if ( ! $app_exists ) {
    493     #  $app_pass = WP_Application_Passwords::create_new_application_password( $user_id, array( 'name' => 'cloudaware' ) );
    494   #  $res = wp_remote_post(get_option('cloudseca_plugin_options')['callback_url'], array(
    495   #      'headers'     => array('Content-Type' => 'application/json; charset=utf-8'),
    496   #      'body'        => json_encode(array('app_pass' => $app_pass, 'url' => get_option( 'siteurl' ))),
    497   #      'method'      => 'POST',
    498   #      'data_format' => 'body',
    499   #  ));
    500   #}
    501 };
    502 
    503571add_action( 'cloudseca_cron_security_check', 'cloudseca_plugin_cron_daily' );
    504572function cloudseca_plugin_cron_daily() {
     
    514582}
    515583
     584#####Initialise
     585register_activation_hook( __FILE__, 'cloudseca_activate_plugin' );
     586function cloudseca_activate_plugin() { // runs on plugin activation
     587  if ( get_option( 'cloudseca_plugin_options' )                 === false ||
     588       get_option( 'cloudseca_plugin_options' )['callback_url'] === false ||
     589       get_option( 'cloudseca_plugin_options' )['callback_url'] ==  ''
     590  ) {
     591    add_option( 'cloudseca_plugin_options', array('callback_url' => '') );
     592  }
     593};
     594
     595add_action('init', 'cloudseca_init_plugin');
     596function cloudseca_init_plugin() {
     597  if ( ! wp_next_scheduled( 'cloudseca_cron_security_check' ) ) {
     598    wp_schedule_event( time(), 'daily', 'cloudseca_cron_security_check' ); // cloudseca_cron_security_check is a hook
     599  }
     600}
    516601
    517602#Deinitialise
    518603register_deactivation_hook( __FILE__, 'cloudseca_deactivate_plugin' );
    519604function cloudseca_deactivate_plugin() {
    520     #require_once( ABSPATH . 'wp-admin/includes/user.php' );  //wp_delete_user()
    521     #require_once( ABSPATH . 'wp-includes/pluggable.php' ); //get_user_by()
    522     #require_once( ABSPATH . 'wp-includes/capabilities.php' ); //remove_role()
    523 
    524605  $timestamp = wp_next_scheduled( 'cloudseca_cron_security_check' );
    525606  wp_unschedule_event( $timestamp, 'cloudseca_cron_security_check' );
    526 
    527   delete_option('cloudseca_plugin_options');
    528 
    529   #$user = get_user_by( 'login', 'cloudaware' );
    530     #wp_delete_user( $user->ID );
    531   #remove_role( 'cloudseca_api' );
    532 }
     607}
     608
     609//Deinstall
     610register_uninstall_hook(__FILE__, 'cloudseca_plugin_uninstall');
     611function cloudseca_plugin_uninstall() {
     612    // Delete user
     613    $user = get_user_by('login', 'cloudaware');
     614    if ($user) {
     615        wp_delete_user($user->ID);
     616    }
     617
     618    // Remove custom role
     619    remove_role('cloudseca_api');
     620
     621    // Delete plugin options
     622    delete_option('cloudseca_plugin_options');
     623}
  • cloudaware-security-audit/trunk/readme.txt

    r3331590 r3334658  
    11=== CloudAware Security Audit ===
    22
    3 Contributors: cloudaware
     3Contributors: cloudaware, underdarknl
    44Tags: security, audit
    55Requires at least: 6.0
    66Tested up to: 6.8
    7 Stable tag: 1.0.8
     7Stable tag: 1.0.9
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    2424- see what themes are installed
    2525- check if 2FA is enabled
     26- see MD5 hashes of all theme and plugin folders
     27
    2628For installations where the RESTAPI is disabled, the plugin can also push this information to an endpoint.
    2729This will work for installations that are behind a geoblock or have no RESTAPI. To disable this, remove the
     
    3234Install the plugin via the Wordpress "Plugins" menu in Wordpress and then
    3335activate using the blue "Activate" button.
     36You can add a new user with restrictive role to your Wordpress installation from within the plugin settings page
     37by clicking on a button.
    3438
    3539== Frequently Asked Questions ==
     
    6670Specifically no version information is transmitted to external services. 
    6771
     72If you fill out an external url in the callback URL field in the settings, a Wordpress cronjob will send a POST request
     73with the audit data to this URL daily.
     74
    6875== Changelog ==
     76
     77= v1.0.9 =
     78* Code cleanup
     79* Add hashing of theme and plugin folders
     80* Add button to setting to add new user and role to system
     81* Cleaner initialisation, deinitialisation
    6982
    7083= v1.0.8 =
Note: See TracChangeset for help on using the changeset viewer.