PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.0
Defender Security – Malware Scanner, Login Security & Firewall v5.7.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-malware-scan.php

class-malware-scan.php in Defender Security – Malware Scanner, Login Security & Firewall 5.7.0, at src/behavior/scan/class-malware-scan.php

304 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles malware scan.
4 *
5 * @package WP_Defender\Behavior\Scan
6 */
7
8 namespace WP_Defender\Behavior\Scan;
9
10 use ArrayIterator;
11 use WP_Defender\Traits\IO;
12 use Calotes\Base\Component;
13 use WP_Defender\Traits\Plugin;
14 use WP_Defender\Component\Timer;
15 use WP_Defender\Model\Scan_Item;
16 use WP_Defender\Behavior\WPMUDEV;
17 use WP_Defender\Model\Scan as Model_Scan;
18 use WP_Defender\Model\Setting\Scan as Scan_Settings;
19 use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
20 use WP_Defender\Controller\Scan as Scan_Controller;
21 use WP_Defender\Component\Scan as Scan_Component;
22
23 /**
24 * It is responsible for performing suspicious checks on files using a quick scan and a deep scan.
25 */
26 class Malware_Scan extends Component {
27
28 use IO;
29 use Plugin;
30
31 public const YARA_RULES = 'defender_yara_rules';
32 public const MALWARE_LOG = 'malware_scan.log';
33
34 /**
35 * Backup memory.
36 *
37 * @var string
38 */
39 private $memory;
40
41 /**
42 * Holds the WPMUDEV object or null if not set.
43 *
44 * @var WPMUDEV
45 */
46 private $wpmudev;
47
48 /**
49 * Holds the Model_Scan model or null if not set.
50 *
51 * @var Model_Scan
52 */
53 private $scan;
54
55 /**
56 * Constructor for the Malware_Scan class.
57 *
58 * @param WPMUDEV $wpmudev The WPMUDEV object.
59 * @param Model_Scan $scan The Model_Scan model.
60 */
61 public function __construct( WPMUDEV $wpmudev, Model_Scan $scan ) {
62 $this->wpmudev = $wpmudev;
63 $this->scan = $scan;
64 }
65
66 /**
67 * Retrieves additional rules based on the provided scan settings.
68 *
69 * @param Scan_Settings $scan_settings The scan settings object.
70 *
71 * @return array Returns an array containing plugin-related rules.
72 */
73 protected function get_additional_rules( Scan_Settings $scan_settings ): array {
74 $plugin_cache = false;
75 $plugin_slugs_changes = array();
76 $plugin_pro_slugs = array();
77 $plugin_all_slugs = array();
78 // Checked Plugin option.
79 if ( $scan_settings->integrity_check && $scan_settings->check_plugins ) {
80 $plugin_cache = true;
81 $arr = get_site_option( Plugin_Integrity::PLUGIN_SLUGS, false );
82 if ( is_array( $arr ) && ! empty( $arr ) ) {
83 $plugin_slugs_changes = $arr;
84 }
85 $plugin_slugs = get_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS, false );
86 if ( is_array( $plugin_slugs ) && ! empty( $plugin_slugs ) ) {
87 $plugin_pro_slugs = $plugin_slugs;
88 }
89 $actioned_plugins = get_site_option( Scan_Component::PLUGINS_ACTIONED );
90 $plugin_all_slugs = Scan_Component::are_actioned_plugins( $actioned_plugins )
91 ? array_keys( $actioned_plugins )
92 : $this->get_plugin_slugs();
93 }
94
95 return array(
96 'plugin_change' => $plugin_cache,
97 'plugin_all_slugs' => $plugin_all_slugs,
98 // List of plugins with modifications.
99 'plugin_slugs_changes' => $plugin_slugs_changes,
100 // List of pro plugins.
101 'plugin_pro_slugs' => $plugin_pro_slugs,
102 );
103 }
104
105 /**
106 * Check if a file has been modified based on the given rules.
107 *
108 * @param string $file_path The path of the file to check.
109 * @param array $rules The rules to determine if a file has been modified.
110 *
111 * @return bool Returns true if the file has been modified, false otherwise.
112 */
113 protected function was_modificated_file( $file_path, $rules ): bool {
114 // Unchecked 'Plugin change file' option, so green light to display Suspicious checks.
115 if ( ! $rules['plugin_change'] ) {
116 return true;
117 }
118
119 $search_on_plugin = WP_PLUGIN_DIR . '/';
120 if ( false !== stripos( $file_path, $search_on_plugin ) ) {
121 /**
122 * Suspicious code in /plugins.
123 * Empty list of plugin slugs because there are premium plugins,
124 * not modifications on Free plugins,
125 * or not plugins on site.
126 * Should check separate custom files/dirs in the root too.
127 */
128 $rev_file = str_replace( $search_on_plugin, '', $file_path );
129 $matches = explode( '/', $rev_file );
130 $base_slug = array_shift( $matches );
131 // Custom files/dirs.
132 if ( ! in_array( $base_slug, $rules['plugin_all_slugs'], true ) ) {
133 return true;
134 }
135 if ( empty( $rules['plugin_slugs_changes'] ) && empty( $rules['plugin_pro_slugs'] ) ) {
136 // No modifications.
137 return false;
138 }
139 // Is it on premium plugins?
140 if ( in_array( $base_slug, $rules['plugin_pro_slugs'], true ) ) {
141 return true;
142 }
143 if ( in_array( $base_slug, (array) $rules['plugin_slugs_changes'], true ) ) {
144 // Modifications in this plugin.
145 return true;
146 }
147
148 // Modifications are not here.
149 return false;
150 }
151
152 // Other WP places.
153 return true;
154 }
155
156 /**
157 * Perform a suspicious check on files using a quick scan and a deep scan.
158 *
159 * @param Malware_Quick_Scan $quick_scan The quick scan object.
160 * @param Malware_Deep_Scan $deep_scan The deep scan object.
161 *
162 * @return bool Returns true if the check is successful, false otherwise.
163 */
164 public function suspicious_check( Malware_Quick_Scan $quick_scan, Malware_Deep_Scan $deep_scan ): bool {
165 $files = get_site_option( Gather_Fact::CACHE_CONTENT, array() );
166 if ( empty( $files ) ) {
167 return true;
168 }
169
170 set_time_limit( 0 );
171 $this->prepare_emergency_shutdown();
172 $timer = new Timer();
173 $rules = $this->fetch_yara_rules();
174
175 $model = $this->scan;
176 $pos = (int) $model->task_checkpoint;
177
178 $combinations = $this->get_additional_rules( new Scan_Settings() );
179 $files = new ArrayIterator( $files );
180 $files->seek( $pos );
181 while ( $files->valid() ) {
182 if ( ! $timer->check() ) {
183 $reason = 'Rage quit';
184
185 /**
186 * Retrieves the Scan_Analytics class.
187 *
188 * @var Scan_Analytics $scan_analytics
189 */
190 $scan_analytics = wd_di()->get( Scan_Analytics::class );
191
192 $scan_analytics->track_feature(
193 $scan_analytics::EVENT_SCAN_FAILED,
194 array(
195 $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_ERROR,
196 'Error_Reason' => $reason,
197 )
198 );
199
200 $this->log( $reason, self::MALWARE_LOG );
201 $model->save();
202 break;
203 }
204 if ( $model->is_issue_ignored( $files->current() ) ) {
205 $this->log( sprintf( 'skip %s because of file is ignored', $files->current() ), self::MALWARE_LOG );
206 $files->next();
207 continue;
208 }
209
210 [ $result, $qs_detail ] = $quick_scan->do_quick_scan( $files->current(), $rules );
211 if ( $result ) {
212 $this->log( sprintf( 'file %s suspicious', $files->current() ), self::MALWARE_LOG );
213 $result = $deep_scan->do_deep_scan( $files->current(), $qs_detail );
214 /**
215 * Add new item if Suspicious code is found and:
216 * plugins are premium,
217 * plugins are on wp.org but the code doesn't match from the WP repo (there are differences in checksums),
218 * deactivated options of File change detection > Scan plugin file changes.
219 */
220 if ( is_array( $result ) && $this->was_modificated_file( $files->current(), $combinations ) ) {
221 $result['file'] = $files->current();
222 $model->add_item( Scan_Item::TYPE_SUSPICIOUS, $result );
223 }
224 }
225 $files->next();
226 $files_key = $files->key();
227 $model->task_checkpoint = ! is_null( $files_key ) ? $files_key : '';
228 $model->calculate_percent( $files_key * 100 / $files->count(), 6 );
229 if ( 0 === $files_key % 100 ) {
230 // We should update the model percent each 100 files so we have some progress on the screen.
231 $model->save();
232 }
233 }
234
235 if ( ! $files->valid() ) {
236 $last = Model_Scan::get_last();
237 if ( is_object( $last ) ) {
238 $ignored_issues = $last->get_issues( Scan_Item::TYPE_SUSPICIOUS, Scan_Item::STATUS_IGNORE );
239 foreach ( $ignored_issues as $issue ) {
240 $this->scan->add_item(
241 Scan_Item::TYPE_SUSPICIOUS,
242 $issue->raw_data,
243 Scan_Item::STATUS_IGNORE
244 );
245 }
246 }
247 $model->task_checkpoint = '';
248 }
249
250 $model->save();
251
252 return ! $files->valid();
253 }
254
255 /**
256 * We will use this for a safe switch when memory out happen.
257 */
258 public function prepare_emergency_shutdown() {
259 $this->memory = str_repeat( '*', 1024 * 1024 );
260
261 register_shutdown_function(
262 function () {
263 if ( Model_Scan::STATUS_FINISH === $this->scan->status ) {
264 return;
265 }
266
267 $this->memory = null;
268 $err = error_get_last();
269 if (
270 ( ! is_null( $err ) )
271 && ( ! in_array( $err['type'], array( E_NOTICE, E_WARNING, E_DEPRECATED ), true ) )
272 ) {
273 $this->log( $err, Scan_Controller::SCAN_LOG );
274 $this->log( 'Something wrong happen, saving and quit.', Scan_Controller::SCAN_LOG );
275 $this->scan->status = Model_Scan::STATUS_ERROR;
276 ++$this->scan->task_checkpoint;
277 $this->scan->save();
278 }
279 }
280 );
281 }
282
283 /**
284 * Fetch yara rules from API.
285 *
286 * @return array
287 */
288 private function fetch_yara_rules(): array {
289 $rules = get_site_option( self::YARA_RULES, false );
290 if ( is_array( $rules ) ) {
291 return $rules;
292 }
293
294 $rules = $this->wpmudev->make_wpmu_request( WPMUDEV::API_SCAN_SIGNATURE );
295 if ( is_array( $rules ) ) {
296 update_site_option( self::YARA_RULES, $rules );
297
298 return $rules;
299 }
300
301 return array();
302 }
303 }
304