PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.4.0
Defender Security – Malware Scanner, Login Security & Firewall v5.4.0
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 / behavior / scan-item / class-vuln-result.php

class-vuln-result.php in Defender Security – Malware Scanner, Login Security & Firewall 5.4.0, at src/behavior/scan-item/class-vuln-result.php

411 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Represents a vulnerability result.
4 *
5 * @package WP_Defender\Behavior\Scan_Item
6 */
7
8 namespace WP_Defender\Behavior\Scan_Item;
9
10 use WP_Error;
11 use Theme_Upgrader;
12 use Plugin_Upgrader;
13 use WP_Defender\Traits\IO;
14 use WP_Defender\Model\Scan;
15 use WP_Defender\Traits\Theme;
16 use WP_Defender\Traits\Plugin;
17 use Calotes\Component\Behavior;
18 use WP_Defender\Traits\Formats;
19 use WP_Defender\Model\Scan_Item;
20 use WP_Defender\Component\Error_Code;
21 use WP_Defender\Component\Security_Tweaks\WP_Version;
22 use WP_Defender\Controller\Scan as Scan_Controller;
23
24 /**
25 * Represents a vulnerability result.
26 */
27 class Vuln_Result extends Behavior {
28
29 use Formats;
30 use IO;
31 use Plugin;
32 use Theme;
33
34 /**
35 * Checks if upgrade is possible based on the presence of fixed version in vulnerabilities.
36 *
37 * @param array $bugs The array of vulnerabilities to check.
38 *
39 * @return string The upgrade status ('disabled' or 'enabled').
40 */
41 protected function upgrade_possible( array $bugs ): string {
42 $upgrade = 'disabled';
43 foreach ( $bugs as $bug ) {
44 if ( ! empty( $bug['fixed_in'] ) ) {
45 $upgrade = 'enabled';
46 break;
47 }
48 }
49
50 return $upgrade;
51 }
52
53 /**
54 * Converts the scan item information into an array for output.
55 *
56 * @return array
57 */
58 public function to_array(): array {
59 $data = $this->owner->raw_data;
60 if ( isset( $data['name'], $data['version'], $data['bugs'] ) ) {
61 if ( 'wp_core' === $data['type'] ) {
62 // Check if the current WP version is the latest.
63 $upgrade = ( new WP_Version() )->check() ? 'disabled' : 'enabled';
64 } elseif ( in_array( $data['type'], array( 'plugin', 'theme' ), true ) ) {
65 $upgrade = $this->upgrade_possible( $data['bugs'] );
66 } else {
67 $upgrade = 'disabled';
68 }
69
70 return array(
71 'id' => $this->owner->id,
72 'type' => Scan_Item::TYPE_VULNERABILITY,
73 'file_name' => $data['name'],
74 'short_desc' => sprintf(
75 /* translators: %s: Version of WP core, plugin or theme. */
76 esc_html__( 'Vulnerability found in %s.', 'defender-security' ),
77 $data['version']
78 ),
79 'details' => isset( $data['new_structure'] )
80 ? $this->get_details_as_array( $data )
81 : $this->get_detail_as_string( $data ),
82 // Need for all scan items for WP-CLI command. Full path = base slug for this item.
83 'full_path' => $data['slug'],
84 'new_structure' => isset( $data['new_structure'] ) ? 'yes' : 'no',
85 'upgrade' => $upgrade,
86 );
87 }
88
89 return array();
90 }
91
92 /**
93 * Ignore the suspicious file for the current scan.
94 *
95 * @return array An array with a message indicating successful ignore.
96 */
97 public function ignore(): array {
98 $scan = Scan::get_last();
99 $scan->ignore_issue( $this->owner->id );
100
101 return array(
102 'message' => esc_html__( 'The suspicious file has been successfully ignored.', 'defender-security' ),
103 );
104 }
105
106 /**
107 * Allow the suspicious file for the current scan.
108 *
109 * @return array An array with a message indicating successful restoration.
110 */
111 public function unignore(): array {
112 $scan = Scan::get_last();
113 $scan->unignore_issue( $this->owner->id );
114
115 return array(
116 'message' => esc_html__( 'The suspicious file has been successfully restored.', 'defender-security' ),
117 );
118 }
119
120 /**
121 * Resolves the vulnerability issue based on the type of vulnerability.
122 *
123 * @return array|bool|WP_Error The resolved issue or an error if the issue type is not found.
124 */
125 public function resolve() {
126 $data = $this->owner->raw_data;
127 // Redirect for WordPress-type.
128 if ( 'wp_core' === $data['type'] ) {
129 return array( 'url' => network_admin_url( 'update-core.php' ) );
130 } elseif ( 'plugin' === $data['type'] ) {
131 return $this->upgrade_plugin( $data['slug'] );
132 } elseif ( 'theme' === $data['type'] ) {
133 return $this->upgrade_theme( $data['base_slug'] );
134 }
135
136 // If type does not match.
137 return new WP_Error(
138 Error_Code::INVALID,
139 esc_html__( 'Please try again! We could not find the issue type.', 'defender-security' )
140 );
141 }
142
143 /**
144 * Upgrades the theme based on the provided slug.
145 *
146 * @param mixed $slug The slug of the theme to upgrade.
147 *
148 * @return array|WP_Error An array with a message if the upgrade is successful or a WP_Error on failure.
149 */
150 private function upgrade_theme( $slug ) {
151 $skin = new Silent_Skin();
152 $upgrader = new Theme_Upgrader( $skin );
153 $ret = $upgrader->upgrade( $slug );
154
155 if ( true === $ret ) {
156 $model = Scan::get_last();
157 $model->remove_issue( $this->owner->id );
158
159 do_action( 'wpdef_fixed_scan_issue', 'vulnerability', 'resolve' );
160
161 return array( 'message' => esc_html__( 'This item has been resolved.', 'defender-security' ) );
162 }
163
164 // This is WP error.
165 if ( is_wp_error( $ret ) ) {
166 return $ret;
167 }
168
169 // Sometimes it returns false because of it could not complete the update process.
170 return new WP_Error(
171 Error_Code::INVALID,
172 esc_html__( "We couldn't update your theme. Please try updating with another method.", 'defender-security' )
173 );
174 }
175
176 /**
177 * Upgrade a plugin based on the provided slug.
178 *
179 * @param mixed $slug The slug of the plugin to upgrade.
180 *
181 * @return array An array with information about the upgrade status.
182 * @since 2.8.1
183 */
184 private function upgrade_plugin( $slug ): array {
185 $skin = new Plugin_Skin();
186 $upgrader = new Plugin_Upgrader( $skin );
187 $result = $upgrader->bulk_upgrade( array( $slug ) );
188
189 if ( is_wp_error( $skin->result ) ) {
190 return array(
191 'type_notice' => 'error',
192 'message' => $skin->result->get_error_message(),
193 );
194 } elseif ( $skin->get_errors()->has_errors() ) {
195 return array(
196 'type_notice' => 'error',
197 'message' => $skin->get_error_messages(),
198 );
199 } elseif ( is_array( $result ) && ! empty( $result[ $slug ] ) ) {
200 $model = Scan::get_last();
201 $model->remove_issue( $this->owner->id );
202
203 do_action( 'wpdef_fixed_scan_issue', 'vulnerability', 'resolve' );
204
205 return array( 'message' => esc_html__( 'This item has been resolved.', 'defender-security' ) );
206 } elseif ( false === $result ) {
207 return array(
208 'type_notice' => 'error',
209 'message' => esc_html__(
210 'Unable to connect to the filesystem. Please confirm your credentials.',
211 'defender-security'
212 ),
213 );
214 }
215
216 return array(
217 'type_notice' => 'info',
218 'message' => esc_html__( 'There is no update available for this plugin.', 'defender-security' ),
219 );
220 }
221
222 /**
223 * Removes a vulnerability at the specified path.
224 *
225 * @param string $path The path of the vulnerability to remove.
226 *
227 * @return bool Returns true on successful removal, false otherwise.
228 */
229 private function remove_vulnerability( string $path ): bool {
230 if ( is_dir( $path ) ) {
231 return $this->delete_dir( $path );
232 } else {
233 // Sometimes a plugin consists of one file.
234 wp_delete_file( $path );
235 return true;
236 }
237 }
238
239 /**
240 * Removes a plugin based on the provided data, checking if it is active and has the necessary permissions.
241 *
242 * @param array $data An array containing information about the plugin to be removed.
243 *
244 * @return array An array with the status of the removal process and any related messages.
245 */
246 private function remove_plugin( array $data ): array {
247 $active = $this->is_active_plugin( $data['slug'] );
248 if ( $active ) {
249 return array(
250 'type_notice' => 'error',
251 'message' => esc_html__( 'This plugin cannot be removed because it is active.', 'defender-security' ),
252 );
253 }
254
255 $abs_path = wp_normalize_path( WP_PLUGIN_DIR ) . DIRECTORY_SEPARATOR . $data['base_slug'];
256 if ( file_exists( $abs_path ) && ! $this->remove_vulnerability( $abs_path ) ) {
257 return array(
258 'type_notice' => 'error',
259 'message' => esc_html__(
260 'Defender does not have enough permission to remove this plugin.',
261 'defender-security'
262 ),
263 );
264 }
265
266 $message = sprintf(
267 /* translators: %s: Plugin name. */
268 esc_html__( '%s plugin', 'defender-security' ),
269 '<b>' . $data['name'] . '</b>'
270 );
271 $this->log( $message . 'is deleted', Scan_Controller::SCAN_LOG );
272 $model = Scan::get_last();
273 $model->remove_issue( $this->owner->id );
274
275 do_action( 'wpdef_fixed_scan_issue', 'vulnerability', 'delete' );
276
277 return array(
278 'collect_type' => true,
279 'message' => $message,
280 );
281 }
282
283 /**
284 * Removes a theme from the system.
285 *
286 * @param array $data The data of the theme to be removed.
287 *
288 * @return array The result of the removal operation.
289 */
290 private function remove_theme( array $data ): array {
291 $active = $this->is_active_theme( $data['base_slug'] );
292 if ( $active ) {
293 return array(
294 'type_notice' => 'error',
295 'message' => esc_html__( 'This theme cannot be removed because it is active.', 'defender-security' ),
296 );
297 }
298
299 $abs_path = $this->get_path_of_themes_dir() . $data['base_slug'];
300 if ( file_exists( $abs_path ) && ! $this->remove_vulnerability( $abs_path ) ) {
301 return array(
302 'type_notice' => 'error',
303 'message' => esc_html__(
304 'Defender does not have enough permission to remove this theme.',
305 'defender-security'
306 ),
307 );
308 }
309
310 $message = sprintf(
311 /* translators: %s: Plugin theme. */
312 esc_html__( '%s theme', 'defender-security' ),
313 '<b>' . $data['name'] . '</b>'
314 );
315 $this->log( $message . 'is deleted', Scan_Controller::SCAN_LOG );
316 $model = Scan::get_last();
317 $model->remove_issue( $this->owner->id );
318
319 do_action( 'wpdef_fixed_scan_issue', 'vulnerability', 'delete' );
320
321 return array(
322 'collect_type' => true,
323 'message' => $message,
324 );
325 }
326
327 /**
328 * Retrieves the vulnerability body text based on the provided bug data.
329 *
330 * @param array $bug An array containing information about the vulnerability.
331 *
332 * @return string The vulnerability body text.
333 */
334 protected function get_vulnerability_body( array $bug ): string {
335 $text = '#' . $bug['title'] . PHP_EOL;
336 $text .= '-' . esc_html__( 'Vulnerability type:', 'defender-security' ) . ' ' . $bug['vuln_type'] . PHP_EOL;
337 if ( empty( $bug['fixed_in'] ) ) {
338 $text .= '-' . esc_html__( 'No Update Available', 'defender-security' ) . PHP_EOL;
339 } else {
340 $text .= '-' . esc_html__(
341 'This bug has been fixed in version:',
342 'defender-security'
343 ) . ' ' . $bug['fixed_in'] . PHP_EOL;
344 }
345
346 return $text;
347 }
348
349 /**
350 * Returns the vulnerability details as a string.
351 *
352 * @param array $data The data containing the vulnerability details.
353 *
354 * @return string The vulnerability details as a string.
355 */
356 public function get_detail_as_string( array $data ): string {
357 $strings = array();
358 foreach ( $data['bugs'] as $bug ) {
359 $strings[] = $this->get_vulnerability_body( $bug );
360 }
361
362 return implode( PHP_EOL, $strings );
363 }
364
365 /**
366 * Returns the details of vulnerabilities as an array.
367 *
368 * @param array $data The data containing information about vulnerabilities.
369 *
370 * @return array An array with vulnerability details including score and detailed description.
371 */
372 public function get_details_as_array( array $data ): array {
373 $arr = array();
374 foreach ( $data['bugs'] as $bug ) {
375 $text = $this->get_vulnerability_body( $bug );
376 $arr[] = array(
377 'score' => $bug['cvss_score'],
378 'detail' => str_replace( PHP_EOL, '<br/>', $text ),
379 );
380 }
381
382 return $arr;
383 }
384
385 /**
386 * Delete inactive plugin or theme.
387 *
388 * @return array|WP_Error
389 */
390 public function delete() {
391 $data = $this->owner->raw_data;
392 // WP core, plugin or theme.
393 if ( 'wp_core' === $data['type'] ) {
394 return array(
395 'type_notice' => 'error',
396 'message' => esc_html__( 'WordPress core cannot be removed.', 'defender-security' ),
397 );
398 } elseif ( 'plugin' === $data['type'] ) {
399 return $this->remove_plugin( $data );
400 } elseif ( 'theme' === $data['type'] ) {
401 return $this->remove_theme( $data );
402 }
403
404 // Sometimes it returns false because of it could not complete the remove process.
405 return new WP_Error(
406 Error_Code::INVALID,
407 esc_html__( "We couldn't remove this item.", 'defender-security' )
408 );
409 }
410 }
411