PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / audit / class-core-audit.php

class-core-audit.php in Defender Security – Malware Scanner, Login Security & Firewall 5.8.1, at src/component/audit/class-core-audit.php

528 lines 18.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Represents an audit system for WordPress core, themes, and plugins.
4 *
5 * @package WP_Defender\Component\Audit
6 */
7
8 namespace WP_Defender\Component\Audit;
9
10 use WP_Upgrader;
11 use WP_Defender\Model\Audit_Log;
12 use WP_Defender\Component\Security_Tweaks\WP_Version;
13
14 /**
15 * Handles the auditing of WordPress core, themes, and plugins such as plugin activation, deactivation, and
16 * installation, and theme activation, deactivation, and installation.
17 */
18 class Core_Audit extends Audit_Event {
19 use \WP_Defender\Traits\Plugin;
20
21 public const ACTION_ACTIVATED = 'activated', ACTION_DEACTIVATED = 'deactivated', ACTION_INSTALLED = 'installed', ACTION_UPGRADED = 'upgraded';
22 public const FILE_ADDED = 'file_added', FILE_MODIFIED = 'file_modified';
23 public const CONTEXT_THEME = 'ct_theme', CONTEXT_PLUGIN = 'ct_plugin', CONTEXT_CORE = 'ct_core';
24
25 /**
26 * Retrieves a list of hooks that the audit system will respond to.
27 *
28 * @return array Associative array of hooks and their corresponding callback information.
29 */
30 public function get_hooks(): array {
31 $data = array(
32 'switch_theme' => array(
33 'args' => array( 'new_name', 'new_theme' ),
34 'callback' => array( self::class, 'process_activate_theme' ),
35 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
36 'action_type' => self::ACTION_ACTIVATED,
37 ),
38 'activated_plugin' => array(
39 'args' => array( 'plugin' ),
40 'text' => sprintf(
41 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin name, 4: Plugin version. */
42 esc_html__( '%1$s %2$s activated plugin: %3$s, version %4$s', 'defender-security' ),
43 '{{blog_name}}',
44 '{{wp_user}}',
45 '{{plugin_name}}',
46 '{{plugin_version}}'
47 ),
48 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
49 'action_type' => self::ACTION_ACTIVATED,
50 'context' => self::CONTEXT_PLUGIN,
51 'program_args' => array(
52 'plugin_abs_path' => array(
53 'callable' => array( self::class, 'get_plugin_abs_path' ),
54 'params' => array(
55 '{{plugin}}',
56 ),
57 ),
58 'plugin_name' => array(
59 'callable' => 'get_plugin_data',
60 'params' => array(
61 '{{plugin_abs_path}}',
62 ),
63 'result_property' => 'Name',
64 ),
65 'plugin_version' => array(
66 'callable' => 'get_plugin_data',
67 'params' => array(
68 '{{plugin_abs_path}}',
69 ),
70 'result_property' => 'Version',
71 ),
72 ),
73 ),
74 'deleted_plugin' => array(
75 'args' => array( 'plugin_file', 'deleted' ),
76 'callback' => array( self::class, 'process_delete_plugin' ),
77 'action_type' => self::ACTION_DEACTIVATED,
78 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
79 ),
80 'deactivated_plugin' => array(
81 'args' => array( 'plugin' ),
82 'text' => sprintf(
83 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin name, 4: Plugin version. */
84 esc_html__( '%1$s %2$s deactivated plugin: %3$s, version %4$s', 'defender-security' ),
85 '{{blog_name}}',
86 '{{wp_user}}',
87 '{{plugin_name}}',
88 '{{plugin_version}}'
89 ),
90 'action_type' => self::ACTION_DEACTIVATED,
91 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
92 'context' => self::CONTEXT_PLUGIN,
93 'program_args' => array(
94 'plugin_abs_path' => array(
95 'callable' => array( self::class, 'get_plugin_abs_path' ),
96 'params' => array(
97 '{{plugin}}',
98 ),
99 ),
100 'plugin_name' => array(
101 'callable' => 'get_plugin_data',
102 'params' => array(
103 '{{plugin_abs_path}}',
104 ),
105 'result_property' => 'Name',
106 ),
107 'plugin_version' => array(
108 'callable' => 'get_plugin_data',
109 'params' => array(
110 '{{plugin_abs_path}}',
111 ),
112 'result_property' => 'Version',
113 ),
114 ),
115 ),
116 'upgrader_process_complete' => array(
117 'args' => array( 'upgrader', 'options' ),
118 'callback' => array( self::class, 'process_installer' ),
119 'action_type' => self::ACTION_UPGRADED,
120 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
121 ),
122 );
123
124 global $wp_version;
125 // @since 2.7.0 Add hook for deleted theme. Use 'deleted_theme' hook that was added since WP v5.8.0.
126 if ( version_compare( $wp_version, '5.8.0', '>=' ) ) {
127 $data['deleted_theme'] = array(
128 'args' => array( 'stylesheet', 'deleted' ),
129 'text' => sprintf(
130 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Stylesheet of the theme. */
131 esc_html__( '%1$s %2$s deleted theme: %3$s', 'defender-security' ),
132 '{{blog_name}}',
133 '{{wp_user}}',
134 '{{stylesheet}}'
135 ),
136 'action_type' => self::ACTION_DEACTIVATED,
137 'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
138 'context' => self::CONTEXT_THEME,
139 );
140 }
141
142 return $data;
143 }
144
145 /**
146 * Prepares the data for recording a core upgrade event.
147 *
148 * @param string $wp_version The new WordPress version.
149 *
150 * @return array Array containing the formatted message and the context.
151 */
152 private function get_upgraded_core_data_for_record( $wp_version ) {
153 return array(
154 sprintf(
155 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Wordpress version. */
156 esc_html__( '%1$s %2$s updated WordPress to %3$s', 'defender-security' ),
157 is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '',
158 $this->get_source_of_action(),
159 $wp_version
160 ),
161 self::CONTEXT_CORE,
162 );
163 }
164
165 /**
166 * Handles the upgrade process for WordPress core and prepares the audit log entry.
167 *
168 * @return array|bool Returns the log entry data or false if no update is needed.
169 */
170 public function upgrade_core() {
171 $update_core = get_site_transient( 'update_core' );
172 if ( is_object( $update_core ) ) {
173 $updates = $update_core->updates;
174 $updates = array_shift( $updates );
175 if ( is_object( $updates ) && property_exists( $updates, 'version' ) ) {
176 return $this->get_upgraded_core_data_for_record( $updates->version );
177 }
178 } elseif ( $this->is_hub_request() ) {
179 // Hub has already updated WP core to the latest version.
180 $wp_version = ( new WP_Version() )->get_latest_version();
181
182 return $wp_version ? $this->get_upgraded_core_data_for_record( $wp_version ) : false;
183 }
184
185 return false;
186 }
187
188 /**
189 * Processes bulk upgrade actions for themes or plugins and generates the audit log entry.
190 *
191 * @param WP_Upgrader $upgrader The upgrade instance.
192 * @param array $options Options containing type and items being upgraded.
193 *
194 * @return array|bool Returns the log entry data or false if unable to generate log data.
195 */
196 public function bulk_upgrade( WP_Upgrader $upgrader, array $options ) {
197 if ( ! is_object( $upgrader->skin ) ) {
198 return false;
199 }
200 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
201 if ( isset( $upgrader->skin->result ) && is_wp_error( $upgrader->skin->result ) ) {
202 // Todo: extend Audit table with a new column for the result of the process and divide it into success and failure.
203 $failed_result = true;
204 } else {
205 $failed_result = false;
206 }
207
208 if ( 'theme' === $options['type'] ) {
209 $texts = array();
210 foreach ( $options['themes'] as $slug ) {
211 $theme = wp_get_theme( $slug );
212 if ( is_object( $theme ) ) {
213 $texts[] = sprintf(
214 /* translators: 1: Theme name, 2: Theme version. */
215 $failed_result ? esc_html( '%1$s version %2$s' ) : esc_html( '%1$s to %2$s' ),
216 $theme->Name, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
217 $theme->get( 'Version' )
218 );
219 }
220 }
221 if ( count( $texts ) ) {
222 return array(
223 sprintf(
224 $failed_result ?
225 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for themes. */
226 esc_html__( '%1$s %2$s was unable to update themes: %3$s', 'defender-security' ) :
227 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for themes. */
228 esc_html__( '%1$s %2$s updated themes: %3$s', 'defender-security' ),
229 $blog_name,
230 $this->get_source_of_action(),
231 implode( ', ', $texts )
232 ),
233 self::CONTEXT_THEME,
234 );
235 } else {
236 return false;
237 }
238 } elseif ( 'plugin' === $options['type'] ) {
239 $texts = array();
240 foreach ( $options['plugins'] as $slug ) {
241 $plugin = get_plugin_data( $this->get_abs_plugin_path_by_slug( $slug ) );
242 if ( is_array( $plugin ) && isset( $plugin['Name'] ) && '' !== $plugin['Name'] ) {
243 $texts[] = sprintf(
244 /* translators: 1: Plugin name, 2: Plugin version. */
245 $failed_result ? esc_html( '%1$s version %2$s' ) : esc_html( '%1$s to %2$s' ),
246 $plugin['Name'],
247 $plugin['Version']
248 );
249 }
250 }
251 if ( count( $texts ) ) {
252 return array(
253 sprintf(
254 $failed_result ?
255 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for plugins. */
256 esc_html__( '%1$s %2$s was unable to update plugins: %3$s', 'defender-security' ) :
257 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for plugins. */
258 esc_html__( '%1$s %2$s updated plugins: %3$s', 'defender-security' ),
259 $blog_name,
260 $this->get_source_of_action(),
261 implode( ', ', $texts )
262 ),
263 self::CONTEXT_PLUGIN,
264 );
265 } else {
266 return false;
267 }
268 }
269 return false;
270 }
271
272 /**
273 * Handles single item upgrade for themes or plugins and prepares the audit log entry.
274 *
275 * @param array $options Options containing type and the item being upgraded.
276 *
277 * @return array|bool Returns the log entry data or false if unable to generate log data.
278 */
279 public function single_upgrade( array $options ) {
280 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
281
282 if ( 'theme' === $options['type'] ) {
283 $theme = wp_get_theme( $options['theme'] );
284 if ( is_object( $theme ) ) {
285 $name = $theme->get( 'Name' );
286 $version = $theme->get( 'Version' );
287
288 return array(
289 sprintf(
290 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Theme name, 4: Theme version. */
291 esc_html__( '%1$s %2$s updated theme: %3$s, version %4$s', 'defender-security' ),
292 $blog_name,
293 $this->get_source_of_action(),
294 $name,
295 $version
296 ),
297 self::CONTEXT_THEME,
298 );
299 } else {
300 return false;
301 }
302 } elseif ( 'plugin' === $options['type'] ) {
303 $slug = $options['plugin'];
304 $data = get_plugin_data( $this->get_abs_plugin_path_by_slug( $slug ) );
305 if ( is_array( $data ) ) {
306 $name = $data['Name'];
307 $version = $data['Version'];
308
309 return array(
310 sprintf(
311 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin name, 4: Plugin version. */
312 esc_html__( '%1$s %2$s updated plugin: %3$s, version %4$s', 'defender-security' ),
313 $blog_name,
314 $this->get_source_of_action(),
315 $name,
316 $version
317 ),
318 self::CONTEXT_PLUGIN,
319 );
320 } else {
321 return false;
322 }
323 }
324 return false;
325 }
326
327 /**
328 * Install process.
329 * Log in the format: {BLOG_NAME} {USERNAME} installed {theme/plugin}: {theme/plugin name}, version {VERSION}.
330 *
331 * @param WP_Upgrader $upgrade The upgrade instance.
332 * @param array $options Options containing type and the item being installed.
333 *
334 * @return bool|array
335 */
336 private function single_install( WP_Upgrader $upgrade, array $options ) {
337 if ( ! is_object( $upgrade->skin ) ) {
338 return false;
339 }
340 // Only for plugins, themes. No for translation, core.
341 if ( ! in_array( $options['type'], array( 'theme', 'plugin' ), true ) ) {
342 return false;
343 }
344
345 $name = '';
346 if ( isset( $upgrade->skin->api->name ) && '' !== $upgrade->skin->api->name ) {
347 $name = $upgrade->skin->api->name;
348 } elseif ( isset( $upgrade->skin->upgrader ) && $upgrade->skin->upgrader instanceof \WP_Upgrader ) {
349 $type_data = ( 'theme' === $options['type'] && isset( $upgrade->skin->upgrader->new_theme_data ) )
350 ? $upgrade->skin->upgrader->new_theme_data
351 : $upgrade->skin->upgrader->new_plugin_data;
352 if ( isset( $type_data['Name'] ) && '' !== $type_data['Name'] && isset( $type_data['Version'] ) && '' !== $type_data['Version'] ) {
353 $name = $type_data['Name'] . ', version ' . $type_data['Version'];
354 }
355 } elseif ( isset( $upgrade->skin->result ) ) {
356 if ( is_array( $upgrade->skin->result ) && isset( $upgrade->skin->result['destination_name'] ) ) {
357 $name = $upgrade->skin->result['destination_name'];
358 } elseif ( is_object( $upgrade->skin->result ) && property_exists(
359 $upgrade->skin->result,
360 'destination_name'
361 ) ) {
362 $name = $upgrade->skin->result->destination_name;
363 }
364 }
365
366 if ( '' === $name ) {
367 return false;
368 }
369
370 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
371 if ( 'theme' === $options['type'] ) {
372 return array(
373 sprintf(
374 /* translators: %s - blog name, %s - username, %s - theme name */
375 esc_html__( '%1$s %2$s installed theme: %3$s', 'defender-security' ),
376 $blog_name,
377 $this->get_source_of_action(),
378 $name
379 ),
380 self::CONTEXT_THEME,
381 self::ACTION_INSTALLED,
382 );
383 } else {
384 return array(
385 sprintf(
386 /* translators: %s - blog name, %s - username, %s - plugin name */
387 esc_html__( '%1$s %2$s installed plugin: %3$s', 'defender-security' ),
388 $blog_name,
389 $this->get_source_of_action(),
390 $name
391 ),
392 self::CONTEXT_PLUGIN,
393 self::ACTION_INSTALLED,
394 );
395 }
396
397 return false;
398 }
399
400 /**
401 * Param 'upgrader' is WP_Upgrader instance. It might be a Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or
402 * Language_Pack_Upgrader instance. Param 'options' is array of bulk item update data. Keys:
403 * 'action' (string) Type of action. Default 'update'.
404 * 'type' (string) Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'.
405 * 'bulk' (bool) Whether the update process is a bulk update. Default true.
406 * 'plugins' (array) Array of the basename paths of the plugins' main files.
407 * 'themes' (array) The theme slugs.
408 * 'translations' (array) Array of translations update data: 'language', 'type', 'slug', 'version'.
409 *
410 * @return array|bool
411 */
412 public function process_installer() {
413 $args = func_get_args();
414 $upgrader = $args[1]['upgrader'];
415 $options = $args[1]['options'];
416 if ( 'core' === $options['type'] ) {
417 return $this->upgrade_core();
418 // If this is core, we just create text and return.
419 } elseif ( isset( $options['bulk'] ) && true === $options['bulk'] ) {
420 // Case: local install/update work with 'bulk' => true (for mass action and no).
421 return $this->bulk_upgrade( $upgrader, $options );
422 } elseif ( 'install' === $options['action'] ) {
423 // Case: actions from Hub.
424 return $this->single_install( $upgrader, $options );
425 } elseif ( 'update' === $options['action'] ) {
426 // Case: actions from Hub.
427 return $this->single_upgrade( $options );
428 }
429 return true;
430 }
431
432 /**
433 * Fires after the theme is switched.
434 *
435 * @return array|bool
436 */
437 public function process_activate_theme() {
438 $args = func_get_args();
439 $new_theme = $args[1]['new_theme'];
440 if ( ! is_object( $new_theme ) ) {
441 return false;
442 }
443 $new_name = $args[1]['new_name'];
444 $version = $new_theme->get( 'Version' );
445 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
446
447 return array(
448 sprintf(
449 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Theme name, 4: Theme version. */
450 esc_html__( '%1$s %2$s activated theme: %3$s, version %4$s', 'defender-security' ),
451 $blog_name,
452 $this->get_source_of_action(),
453 $new_name,
454 $version
455 ),
456 self::CONTEXT_THEME,
457 );
458 }
459
460 /**
461 * Fires immediately after a plugin deletion.
462 * There is no way to get the plugin data (name, version) because it has been removed. Only slug.
463 *
464 * @return array
465 */
466 public function process_delete_plugin(): array {
467 $args = func_get_args();
468
469 // If 'deleted'-arg is false then the plugin deletion wasn't successful.
470 if ( ! isset( $args[1]['deleted'] ) || false === $args[1]['deleted'] ) {
471 // Todo: extend Audit table with a new column for the result of the process and divide it into success and failure.
472 $failed_result = true;
473 } else {
474 $failed_result = false;
475 }
476 $plugin_file = $args[1]['plugin_file'];
477 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
478
479 return array(
480 sprintf(
481 $failed_result ?
482 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin file. */
483 esc_html__( '%1$s %2$s was unable to delete plugin: %3$s', 'defender-security' ) :
484 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin file. */
485 esc_html__( '%1$s %2$s deleted plugin: %3$s', 'defender-security' ),
486 $blog_name,
487 $this->get_source_of_action(),
488 $plugin_file
489 ),
490 self::CONTEXT_PLUGIN,
491 );
492 }
493
494 /**
495 * Provides a dictionary of audit event types and their descriptions.
496 *
497 * @return array Associative array of event keys and their descriptions.
498 */
499 public function dictionary(): array {
500 return array(
501 self::ACTION_DEACTIVATED => esc_html__( 'deactivated', 'defender-security' ),
502 self::ACTION_UPGRADED => esc_html__( 'upgraded', 'defender-security' ),
503 self::ACTION_ACTIVATED => esc_html__( 'activated', 'defender-security' ),
504 self::ACTION_INSTALLED => esc_html__( 'installed', 'defender-security' ),
505 self::CONTEXT_THEME => esc_html__( 'theme', 'defender-security' ),
506 self::CONTEXT_PLUGIN => esc_html__( 'plugin', 'defender-security' ),
507 self::CONTEXT_CORE => esc_html__( 'WordPress', 'defender-security' ),
508 self::FILE_ADDED => esc_html__( 'File Added', 'defender-security' ),
509 self::FILE_MODIFIED => esc_html__( 'File Modified', 'defender-security' ),
510 );
511 }
512
513 /**
514 * Retrieves the absolute path of a plugin.
515 *
516 * @param string $slug The plugin slug or relative path.
517 *
518 * @return string The absolute path of the plugin.
519 */
520 public function get_plugin_abs_path( string $slug ): string {
521 if ( ! is_file( $slug ) ) {
522 $slug = $this->get_abs_plugin_path_by_slug( $slug );
523 }
524
525 return $slug;
526 }
527 }
528