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 / class-malware-scan.php

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

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