PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.0
Defender Security – Malware Scanner, Login Security & Firewall v6.2.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 / class-known-vulnerability.php

class-known-vulnerability.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.0, at src/behavior/scan/class-known-vulnerability.php

330 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles core integrity scan.
4 *
5 * @package WP_Defender\Behavior\Scan
6 */
7
8 namespace WP_Defender\Behavior\Scan;
9
10 use WP_Error;
11 use Countable;
12 use WP_Defender\Traits\IO;
13 use Calotes\Base\Component;
14 use WP_Defender\Model\Scan;
15 use WP_Defender\Traits\Theme;
16 use WP_Defender\Traits\Plugin;
17 use WP_Defender\Model\Scan_Item;
18 use WP_Defender\Behavior\WPMUDEV;
19 use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
20 use WP_Defender\Component\Scan as Scan_Component;
21
22 /**
23 * Represents a component responsible for checking known vulnerabilities.
24 */
25 class Known_Vulnerability extends Component {
26
27 use IO;
28 use Plugin;
29 use Theme;
30
31 /**
32 * Holds the Scan model or null if not set.
33 *
34 * @var Scan|null
35 */
36 private ?Scan $scan;
37
38 /**
39 * Holds the WPMUDEV object or null if not set.
40 *
41 * @var WPMUDEV|null
42 */
43 private ?WPMUDEV $wpmudev;
44
45 /**
46 * Initializes the Known_Vulnerability class with the WPMUDEV and Scan instances.
47 *
48 * @param WPMUDEV $wpmudev The WPMUDEV object.
49 * @param Scan $scan The Scan model.
50 */
51 public function __construct( WPMUDEV $wpmudev, Scan $scan ) {
52 $this->wpmudev = $wpmudev;
53 $this->scan = $scan;
54 }
55
56 /**
57 * Performs the vulnerability check.
58 *
59 * @return bool
60 */
61 public function vuln_check(): bool {
62 global $wp_version;
63 $info_cached = array();
64 $data = array(
65 'plugins' => wp_json_encode( $this->gather_fact( 'plugin', $info_cached ) ),
66 'themes' => wp_json_encode( $this->gather_fact( 'theme', $info_cached ) ),
67 'wordpress' => $wp_version,
68 );
69
70 $ret = $this->make_check( $data );
71 if ( is_wp_error( $ret ) ) {
72 $this->handle_error( $ret );
73
74 return true;
75 }
76
77 $ignored_issues = $this->get_ignored_issues();
78 $this->process_results( $ret, $info_cached, $ignored_issues );
79 $this->scan->calculate_percent( 100, 5 );
80 $this->add_ignored_issues( $ignored_issues );
81
82 return true;
83 }
84
85 /**
86 * Gathers information about installed plugins or themes.
87 *
88 * @param string $type The type of information to gather (plugin or theme).
89 * @param array $info_cached The cached information.
90 *
91 * @return array
92 */
93 private function gather_fact( string $type, &$info_cached ): array {
94 $items = array();
95
96 if ( 'plugin' === $type ) {
97 $actioned_plugins = get_site_option( Scan_Component::PLUGINS_ACTIONED );
98 if ( Scan_Component::are_actioned_plugins( $actioned_plugins ) ) {
99 foreach ( $actioned_plugins as $slug => $plugin_data ) {
100 $items[ $slug ] = $plugin_data['Version'];
101 $info_cached[ $slug ] = array( $plugin_data['Name'], $plugin_data['Version'], $plugin_data['Slug'] );
102 }
103 } else {
104 // An emergency option if for some reason the plugin's quick data is not saved in the first step.
105 foreach ( $this->get_plugins() as $slug => $plugin ) {
106 $base_slug = $this->get_plugin_slug_by( $slug );
107 $items[ $base_slug ] = $plugin['Version'];
108 $info_cached[ $base_slug ] = array( $plugin['Name'], $plugin['Version'], $slug );
109 }
110 }
111 } elseif ( 'theme' === $type ) {
112 $model = Scan::get_last();
113 foreach ( $this->get_themes() as $slug => $item ) {
114 // It's actual because we don't have ignored theme slugs.
115 if ( is_object( $model ) && $model->is_issue_ignored( $slug ) ) {
116 continue;
117 }
118
119 $base_slug = explode( '/', $slug );
120 $base_slug = array_shift( $base_slug );
121 $items[ $base_slug ] = $item['Version'];
122 $info_cached[ $base_slug ] = array( $item['Name'], $item['Version'], $slug );
123 }
124 } else {
125 return $items;
126 }
127
128 return $items;
129 }
130
131 /**
132 * Makes a request to the WPMUDEV API to check for known vulnerabilities.
133 *
134 * @param mixed $data The data to send in the request.
135 *
136 * @return array|WP_Error
137 */
138 protected function make_check( $data ) {
139 return $this->wpmudev->make_wpmu_request( WPMUDEV::API_SCAN_KNOWN_VULN, $data, array( 'method' => 'POST' ) );
140 }
141
142 /**
143 * Handles the error by tracking the failure event, logging the error message, and updating the scan status.
144 *
145 * @param WP_Error $error The error object containing the error message.
146 */
147 private function handle_error( WP_Error $error ) {
148 $error_message = $error->get_error_message();
149 $scan_analytics = wd_di()->get( Scan_Analytics::class );
150
151 $scan_analytics->track_feature(
152 Scan_Analytics::EVENT_SCAN_FAILED,
153 array(
154 Scan_Analytics::EVENT_SCAN_FAILED_PROP => Scan_Analytics::EVENT_SCAN_FAILED_ERROR,
155 'Error_Reason' => $error_message,
156 )
157 );
158
159 $this->log( $error_message, \WP_Defender\Controller\Scan::SCAN_LOG );
160 $this->scan->status = Scan::STATUS_ERROR;
161 $this->scan->save();
162 }
163
164 /**
165 * Retrieves the list of ignored issues.
166 *
167 * @return array The list of ignored issues.
168 */
169 private function get_ignored_issues() {
170 $last = Scan::get_last();
171
172 return is_object( $last ) ? $last->get_issues(
173 Scan_Item::TYPE_VULNERABILITY,
174 Scan_Item::STATUS_IGNORE
175 ) : array();
176 }
177
178 /**
179 * Processes the results of the vulnerability check and saves them in the Scan model.
180 *
181 * @param array $results The results of the vulnerability check.
182 * @param array $info_cached The cached information.
183 * @param array $ignored_issues The ignored issues.
184 */
185 private function process_results( $results, $info_cached, $ignored_issues ) {
186 $this->process_result( $results['plugins'], $info_cached, 'plugin', $ignored_issues );
187 $this->process_result( $results['themes'], $info_cached, 'theme', $ignored_issues );
188 $this->process_result( $results['wordpress'], array(), 'wp_core', $ignored_issues );
189 }
190
191 /**
192 * Processes the result of the vulnerability check and saves them in the Scan model.
193 *
194 * @param mixed $result The result of the vulnerability check.
195 * @param array $info The cached information.
196 * @param string $type The type of the vulnerability check.
197 * @param array $ignored_issues The ignored issues.
198 */
199 private function process_result( $result, $info, $type, $ignored_issues ) {
200 if ( ! is_array( $result ) || array() === $result ) {
201 return;
202 }
203
204 $this->log( sprintf( 'Checking %s:', $type ) );
205 $model = $this->scan;
206
207 foreach ( $result as $base_slug => $bugs ) {
208 if ( 'wp_core' === $type ) {
209 global $wp_version;
210 $is_exist = false;
211 if ( is_array( $ignored_issues ) && array() !== $ignored_issues ) {
212 foreach ( $ignored_issues as $issue ) {
213 if ( $wp_version === $issue->raw_data['version'] ) {
214 $is_exist = true;
215 }
216 }
217 }
218
219 if ( ! $is_exist ) {
220 $raw_data = array(
221 'type' => $type,
222 'slug' => '',
223 'base_slug' => '',
224 'version' => $wp_version,
225 'name' => 'WordPress Core',
226 'bugs' => array(
227 array(
228 'vuln_type' => $bugs['vuln_type'],
229 'title' => $bugs['title'],
230 'ref' => $bugs['references'],
231 'fixed_in' => $bugs['fixed_in'],
232 'cvss_score' => $bugs['cvss']['score'] ?? '',
233 ),
234 ),
235 'new_structure' => '3.4.0',
236 );
237
238 $model->add_item( Scan_Item::TYPE_VULNERABILITY, $raw_data );
239 }
240 } elseif ( 'plugin' === $type || 'theme' === $type ) {
241 if ( ! isset( $info[ $base_slug ] ) || ! is_array( $info[ $base_slug ] ) ) {
242 continue;
243 }
244
245 if ( isset( $bugs['confirmed'] ) && ( is_array( $bugs['confirmed'] ) || $bugs['confirmed'] instanceof Countable ? count( $bugs['confirmed'] ) : 0 ) ) {
246 $wp_org_plugin_details = array(
247 'is_existed' => false,
248 'last_version' => '0',
249 );
250 if ( 'plugin' === $type ) {
251 if ( ! $this->is_likely_wporg_slug( $base_slug ) ) {
252 continue;
253 }
254 // Additional conditions for wp.org plugin.
255 $results = $this->handle_wp_org_response_by( $base_slug );
256 if ( $results['success'] && isset( $results['body']['version'] ) ) {
257 $wp_org_plugin_details = array(
258 'is_existed' => true,
259 'last_version' => $results['body']['version'],
260 );
261 }
262 // If there is not a plugin version, likely the plugin is closed. We'd not display Update button.
263 }
264
265 [ $name, $current_version, $slug ] = $info[ $base_slug ];
266 $raw_data = array(
267 'type' => $type,
268 'slug' => $slug,
269 'base_slug' => $base_slug,
270 'version' => $current_version,
271 'name' => $name,
272 'bugs' => array(),
273 );
274
275 $confirmed_bugs = (array) $bugs['confirmed'];
276 // @since 3.4.0 Restructure the view in relation to the new CVSS Score field.
277 $raw_data['new_structure'] = '3.4.0';
278 $this->log( sprintf( '%s has %d known bugs', $slug, count( $confirmed_bugs ) ) );
279
280 foreach ( $confirmed_bugs as $bug ) {
281 if ( 'plugin' === $type && $wp_org_plugin_details['is_existed']
282 && version_compare( $wp_org_plugin_details['last_version'], $bug['fixed_in'], '<=' )
283 ) {
284 // This is another plugin but with the same slug.
285 continue;
286 }
287 $raw_data['bugs'][] = array(
288 'vuln_type' => $bug['vuln_type'],
289 'title' => $bug['title'],
290 'ref' => $bug['references'],
291 'fixed_in' => $bug['fixed_in'],
292 'cvss_score' => $bug['cvss']['score'] ?? '',
293 );
294 }
295
296 $model->add_item( Scan_Item::TYPE_VULNERABILITY, $raw_data );
297 }
298 }
299 }
300 }
301
302 /**
303 * Adds ignored vulnerability issues to the scan.
304 *
305 * @param array $ignored_issues The array of ignored vulnerability issues to add.
306 */
307 private function add_ignored_issues( $ignored_issues ) {
308 if ( ! is_array( $ignored_issues ) || array() === $ignored_issues ) {
309 return;
310 }
311
312 // wp_core issues have no slug, so carry them forward as-is.
313 $wp_core_issues = array();
314 $other_issues = array();
315 foreach ( $ignored_issues as $issue ) {
316 if ( 'wp_core' === ( $issue->raw_data['type'] ?? '' ) ) {
317 $wp_core_issues[] = $issue;
318 } else {
319 $other_issues[] = $issue;
320 }
321 }
322
323 foreach ( $wp_core_issues as $issue ) {
324 $this->scan->add_item( Scan_Item::TYPE_VULNERABILITY, $issue->raw_data, Scan_Item::STATUS_IGNORE );
325 }
326
327 $this->scan->carry_forward_ignored_issues( Scan_Item::TYPE_VULNERABILITY, $other_issues, 'slug' );
328 }
329 }
330