PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.2
Defender Security – Malware Scanner, Login Security & Firewall v5.7.2
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.7.2, at src/component/audit/class-core-audit.php

526 lines 18.0 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 ( empty( $update_core ) && $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'] ) && ! empty( $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 }
270
271 /**
272 * Handles single item upgrade for themes or plugins and prepares the audit log entry.
273 *
274 * @param WP_Upgrader $upgrader The upgrade instance.
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( WP_Upgrader $upgrader, 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 }
325
326 /**
327 * Install process.
328 * Log in the format: {BLOG_NAME} {USERNAME} installed {theme/plugin}: {theme/plugin name}, version {VERSION}.
329 *
330 * @param WP_Upgrader $upgrade The upgrade instance.
331 * @param array $options Options containing type and the item being installed.
332 *
333 * @return bool|array
334 */
335 private function single_install( WP_Upgrader $upgrade, array $options ) {
336 if ( ! is_object( $upgrade->skin ) ) {
337 return false;
338 }
339 // Only for plugins, themes. No for translation, core.
340 if ( ! in_array( $options['type'], array( 'theme', 'plugin' ), true ) ) {
341 return false;
342 }
343
344 $name = '';
345 if ( ! empty( $upgrade->skin->api ) ) {
346 $name = $upgrade->skin->api->name;
347 } elseif ( ! empty( $upgrade->skin->upgrader ) ) {
348 $type_data = ( 'theme' === $options['type'] && isset( $upgrade->skin->upgrader->new_theme_data ) )
349 ? $upgrade->skin->upgrader->new_theme_data
350 : $upgrade->skin->upgrader->new_plugin_data;
351 if ( ! empty( $type_data['Name'] ) && ! empty( $type_data['Version'] ) ) {
352 $name = $type_data['Name'] . ', version ' . $type_data['Version'];
353 }
354 } elseif ( ! empty( $upgrade->skin->result ) ) {
355 if ( is_array( $upgrade->skin->result ) && isset( $upgrade->skin->result['destination_name'] ) ) {
356 $name = $upgrade->skin->result['destination_name'];
357 } elseif ( is_object( $upgrade->skin->result ) && property_exists(
358 $upgrade->skin->result,
359 'destination_name'
360 ) ) {
361 $name = $upgrade->skin->result->destination_name;
362 }
363 }
364
365 if ( empty( $name ) ) {
366 return false;
367 }
368
369 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
370 if ( 'theme' === $options['type'] ) {
371 return array(
372 sprintf(
373 /* translators: %s - blog name, %s - username, %s - theme name */
374 esc_html__( '%1$s %2$s installed theme: %3$s', 'defender-security' ),
375 $blog_name,
376 $this->get_source_of_action(),
377 $name
378 ),
379 self::CONTEXT_THEME,
380 self::ACTION_INSTALLED,
381 );
382 } else {
383 return array(
384 sprintf(
385 /* translators: %s - blog name, %s - username, %s - plugin name */
386 esc_html__( '%1$s %2$s installed plugin: %3$s', 'defender-security' ),
387 $blog_name,
388 $this->get_source_of_action(),
389 $name
390 ),
391 self::CONTEXT_PLUGIN,
392 self::ACTION_INSTALLED,
393 );
394 }
395
396 return false;
397 }
398
399 /**
400 * Param 'upgrader' is WP_Upgrader instance. It might be a Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or
401 * Language_Pack_Upgrader instance. Param 'options' is array of bulk item update data. Keys:
402 * 'action' (string) Type of action. Default 'update'.
403 * 'type' (string) Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'.
404 * 'bulk' (bool) Whether the update process is a bulk update. Default true.
405 * 'plugins' (array) Array of the basename paths of the plugins' main files.
406 * 'themes' (array) The theme slugs.
407 * 'translations' (array) Array of translations update data: 'language', 'type', 'slug', 'version'.
408 *
409 * @return mixed
410 */
411 public function process_installer() {
412 $args = func_get_args();
413 $upgrader = $args[1]['upgrader'];
414 $options = $args[1]['options'];
415 if ( 'core' === $options['type'] ) {
416 return $this->upgrade_core();
417 // If this is core, we just create text and return.
418 } elseif ( isset( $options['bulk'] ) && true === $options['bulk'] ) {
419 // Case: local install/update work with 'bulk' => true (for mass action and no).
420 return $this->bulk_upgrade( $upgrader, $options );
421 } elseif ( 'install' === $options['action'] ) {
422 // Case: actions from Hub.
423 return $this->single_install( $upgrader, $options );
424 } elseif ( 'update' === $options['action'] ) {
425 // Case: actions from Hub.
426 return $this->single_upgrade( $upgrader, $options );
427 }
428 }
429
430 /**
431 * Fires after the theme is switched.
432 *
433 * @return array|bool
434 */
435 public function process_activate_theme() {
436 $args = func_get_args();
437 $new_theme = $args[1]['new_theme'];
438 if ( ! is_object( $new_theme ) ) {
439 return false;
440 }
441 $new_name = $args[1]['new_name'];
442 $version = $new_theme->get( 'Version' );
443 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
444
445 return array(
446 sprintf(
447 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Theme name, 4: Theme version. */
448 esc_html__( '%1$s %2$s activated theme: %3$s, version %4$s', 'defender-security' ),
449 $blog_name,
450 $this->get_source_of_action(),
451 $new_name,
452 $version
453 ),
454 self::CONTEXT_THEME,
455 );
456 }
457
458 /**
459 * Fires immediately after a plugin deletion.
460 * There is no way to get the plugin data (name, version) because it has been removed. Only slug.
461 *
462 * @return array
463 */
464 public function process_delete_plugin(): array {
465 $args = func_get_args();
466
467 // If 'deleted'-arg is false then the plugin deletion wasn't successful.
468 if ( empty( $args[1]['deleted'] ) ) {
469 // Todo: extend Audit table with a new column for the result of the process and divide it into success and failure.
470 $failed_result = true;
471 } else {
472 $failed_result = false;
473 }
474 $plugin_file = $args[1]['plugin_file'];
475 $blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
476
477 return array(
478 sprintf(
479 $failed_result ?
480 /* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin file. */
481 esc_html__( '%1$s %2$s was unable to delete plugin: %3$s', 'defender-security' ) :
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 deleted plugin: %3$s', 'defender-security' ),
484 $blog_name,
485 $this->get_source_of_action(),
486 $plugin_file
487 ),
488 self::CONTEXT_PLUGIN,
489 );
490 }
491
492 /**
493 * Provides a dictionary of audit event types and their descriptions.
494 *
495 * @return array Associative array of event keys and their descriptions.
496 */
497 public function dictionary(): array {
498 return array(
499 self::ACTION_DEACTIVATED => esc_html__( 'deactivated', 'defender-security' ),
500 self::ACTION_UPGRADED => esc_html__( 'upgraded', 'defender-security' ),
501 self::ACTION_ACTIVATED => esc_html__( 'activated', 'defender-security' ),
502 self::ACTION_INSTALLED => esc_html__( 'installed', 'defender-security' ),
503 self::CONTEXT_THEME => esc_html__( 'theme', 'defender-security' ),
504 self::CONTEXT_PLUGIN => esc_html__( 'plugin', 'defender-security' ),
505 self::CONTEXT_CORE => esc_html__( 'WordPress', 'defender-security' ),
506 self::FILE_ADDED => esc_html__( 'File Added', 'defender-security' ),
507 self::FILE_MODIFIED => esc_html__( 'File Modified', 'defender-security' ),
508 );
509 }
510
511 /**
512 * Retrieves the absolute path of a plugin.
513 *
514 * @param string $slug The plugin slug or relative path.
515 *
516 * @return string The absolute path of the plugin.
517 */
518 public function get_plugin_abs_path( string $slug ): string {
519 if ( ! is_file( $slug ) ) {
520 $slug = $this->get_abs_plugin_path_by_slug( $slug );
521 }
522
523 return $slug;
524 }
525 }
526