wfScanner.php
1382 lines
| 1 | <?php |
| 2 | |
| 3 | class wfScanner { |
| 4 | const SCAN_TYPE_QUICK = 'quick'; |
| 5 | const SCAN_TYPE_LIMITED = 'limited'; |
| 6 | const SCAN_TYPE_STANDARD = 'standard'; |
| 7 | const SCAN_TYPE_HIGH_SENSITIVITY = 'highsensitivity'; |
| 8 | const SCAN_TYPE_CUSTOM = 'custom'; |
| 9 | |
| 10 | const SCAN_SCHEDULING_MODE_AUTOMATIC = 'auto'; |
| 11 | const SCAN_SCHEDULING_MODE_MANUAL = 'manual'; |
| 12 | |
| 13 | const MANUAL_SCHEDULING_ONCE_DAILY = 'onceDaily'; |
| 14 | const MANUAL_SCHEDULING_TWICE_DAILY = 'twiceDaily'; |
| 15 | const MANUAL_SCHEDULING_EVERY_OTHER_DAY = 'everyOtherDay'; |
| 16 | const MANUAL_SCHEDULING_WEEKDAYS = 'weekdays'; |
| 17 | const MANUAL_SCHEDULING_WEEKENDS = 'weekends'; |
| 18 | const MANUAL_SCHEDULING_ODD_DAYS_WEEKENDS = 'oddDaysWE'; |
| 19 | const MANUAL_SCHEDULING_CUSTOM = 'custom'; |
| 20 | |
| 21 | const SIGNATURE_MODE_PREMIUM = 'premium'; |
| 22 | const SIGNATURE_MODE_COMMUNITY = 'community'; |
| 23 | |
| 24 | const STATUS_PENDING = 'pending'; |
| 25 | const STATUS_RUNNING = 'running'; |
| 26 | const STATUS_RUNNING_WARNING = 'running-warning'; |
| 27 | const STATUS_COMPLETE_SUCCESS = 'complete-success'; |
| 28 | const STATUS_COMPLETE_WARNING = 'complete-warning'; |
| 29 | const STATUS_PREMIUM = 'premium'; |
| 30 | const STATUS_DISABLED = 'disabled'; |
| 31 | |
| 32 | const STAGE_SPAMVERTISING_CHECKS = 'spamvertising'; |
| 33 | const STAGE_SPAM_CHECK = 'spam'; |
| 34 | const STAGE_BLACKLIST_CHECK = 'blacklist'; |
| 35 | const STAGE_SERVER_STATE = 'server'; |
| 36 | const STAGE_FILE_CHANGES = 'changes'; |
| 37 | const STAGE_PUBLIC_FILES = 'public'; |
| 38 | const STAGE_MALWARE_SCAN = 'malware'; |
| 39 | const STAGE_CONTENT_SAFETY = 'content'; |
| 40 | const STAGE_PASSWORD_STRENGTH = 'password'; |
| 41 | const STAGE_VULNERABILITY_SCAN = 'vulnerability'; |
| 42 | const STAGE_OPTIONS_AUDIT = 'options'; |
| 43 | |
| 44 | const SUMMARY_TOTAL_USERS = 'totalUsers'; |
| 45 | const SUMMARY_TOTAL_PAGES = 'totalPages'; |
| 46 | const SUMMARY_TOTAL_POSTS = 'totalPosts'; |
| 47 | const SUMMARY_TOTAL_COMMENTS = 'totalComments'; |
| 48 | const SUMMARY_TOTAL_CATEGORIES = 'totalCategories'; |
| 49 | const SUMMARY_TOTAL_TABLES = 'totalTables'; |
| 50 | const SUMMARY_TOTAL_ROWS = 'totalRows'; |
| 51 | const SUMMARY_SCANNED_POSTS = 'scannedPosts'; |
| 52 | const SUMMARY_SCANNED_COMMENTS = 'scannedComments'; |
| 53 | const SUMMARY_SCANNED_FILES = 'scannedFiles'; |
| 54 | const SUMMARY_SCANNED_PLUGINS = 'scannedPlugins'; |
| 55 | const SUMMARY_SCANNED_THEMES = 'scannedThemes'; |
| 56 | const SUMMARY_SCANNED_USERS = 'scannedUsers'; |
| 57 | const SUMMARY_SCANNED_URLS = 'scannedURLs'; |
| 58 | |
| 59 | const CENTRAL_STAGE_UPDATE_THRESHOLD = 5; |
| 60 | |
| 61 | private $_scanType = false; |
| 62 | |
| 63 | private $_summary = false; |
| 64 | private $_destructRegistered = false; |
| 65 | private $_dirty = false; |
| 66 | |
| 67 | /** |
| 68 | * Returns the singleton wfScanner with the user-configured scan type set. |
| 69 | * |
| 70 | * @return wfScanner |
| 71 | */ |
| 72 | public static function shared() { |
| 73 | static $_scanner = null; |
| 74 | if ($_scanner === null) { |
| 75 | $_scanner = new wfScanner(); |
| 76 | } |
| 77 | return $_scanner; |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Schedules a cron rescheduling to happen at the end of the current process's execution. |
| 82 | */ |
| 83 | public static function setNeedsRescheduling() { |
| 84 | static $willReschedule = false; |
| 85 | if (!$willReschedule) { |
| 86 | $willReschedule = true; |
| 87 | register_shutdown_function(array(self::shared(), 'scheduleScans')); |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Returns whether or not the scan type passed is valid. |
| 93 | * |
| 94 | * @param $type |
| 95 | * @return bool |
| 96 | */ |
| 97 | public static function isValidScanType($type) { |
| 98 | switch ($type) { |
| 99 | case self::SCAN_TYPE_QUICK: |
| 100 | case self::SCAN_TYPE_LIMITED: |
| 101 | case self::SCAN_TYPE_HIGH_SENSITIVITY: |
| 102 | case self::SCAN_TYPE_CUSTOM: |
| 103 | case self::SCAN_TYPE_STANDARD: |
| 104 | return true; |
| 105 | } |
| 106 | return false; |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * Returns the display string for the given type. |
| 111 | * |
| 112 | * @param string $type |
| 113 | * @return string |
| 114 | */ |
| 115 | public static function displayScanType($type) { |
| 116 | switch ($type) { |
| 117 | case self::SCAN_TYPE_QUICK: |
| 118 | return __('Quick', 'wordfence'); |
| 119 | case self::SCAN_TYPE_LIMITED: |
| 120 | return __('Limited', 'wordfence'); |
| 121 | case self::SCAN_TYPE_HIGH_SENSITIVITY: |
| 122 | return __('High Sensitivity', 'wordfence'); |
| 123 | case self::SCAN_TYPE_CUSTOM: |
| 124 | return __('Custom', 'wordfence'); |
| 125 | case self::SCAN_TYPE_STANDARD: |
| 126 | default: |
| 127 | return __('Standard', 'wordfence'); |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Returns the display detail string for the given type. |
| 133 | * |
| 134 | * @param string $type |
| 135 | * @return string |
| 136 | */ |
| 137 | public static function displayScanTypeDetail($type) { |
| 138 | switch ($type) { |
| 139 | case self::SCAN_TYPE_QUICK: |
| 140 | case self::SCAN_TYPE_LIMITED: |
| 141 | return __('Low resource utilization, limited detection capability', 'wordfence'); |
| 142 | case self::SCAN_TYPE_HIGH_SENSITIVITY: |
| 143 | return __('Standard detection capability, chance of false positives', 'wordfence'); |
| 144 | case self::SCAN_TYPE_CUSTOM: |
| 145 | return __('Custom scan options selected', 'wordfence'); |
| 146 | case self::SCAN_TYPE_STANDARD: |
| 147 | default: |
| 148 | return __('Standard detection capability', 'wordfence'); |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | /** |
| 153 | * Returns an array of the scan options (as keys) and the corresponding value for the quick scan type. All omitted |
| 154 | * scan stages are considered disabled. |
| 155 | * |
| 156 | * @return array |
| 157 | */ |
| 158 | public static function quickScanTypeOptions() { |
| 159 | $oldVersions = true; |
| 160 | $wafStatus = true; |
| 161 | if (wfConfig::get('scanType') == self::SCAN_TYPE_CUSTOM) { //Obey the setting in custom if that's the true scan type |
| 162 | $oldVersions = wfConfig::get('scansEnabled_oldVersions'); |
| 163 | $wafStatus = wfConfig::get('scansEnabled_wafStatus'); |
| 164 | } |
| 165 | |
| 166 | return array_merge(self::_inactiveScanOptions(), array( |
| 167 | 'scansEnabled_oldVersions' => $oldVersions, |
| 168 | 'scansEnabled_wafStatus' => $wafStatus, |
| 169 | )); |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Returns an array of the scan options (as keys) and the corresponding value for the limited scan type. |
| 174 | * |
| 175 | * @return array |
| 176 | */ |
| 177 | public static function limitedScanTypeOptions() { |
| 178 | return array_merge(self::_inactiveScanOptions(), array( |
| 179 | 'scansEnabled_checkHowGetIPs' => true, |
| 180 | 'scansEnabled_malware' => true, |
| 181 | 'scansEnabled_fileContents' => true, |
| 182 | 'scansEnabled_fileContentsGSB' => true, |
| 183 | 'scansEnabled_suspiciousOptions' => true, |
| 184 | 'scansEnabled_oldVersions' => true, |
| 185 | 'scansEnabled_wafStatus' => true, |
| 186 | 'lowResourceScansEnabled' => true, |
| 187 | 'scan_exclude' => wfConfig::get('scan_exclude', ''), |
| 188 | 'scan_include_extra' => wfConfig::get('scan_include_extra', ''), |
| 189 | 'scansEnabled_geoipSupport' => true, |
| 190 | )); |
| 191 | } |
| 192 | |
| 193 | /** |
| 194 | * Returns an array of the scan options (as keys) and the corresponding value for the standard scan type. |
| 195 | * |
| 196 | * @return array |
| 197 | */ |
| 198 | public static function standardScanTypeOptions() { |
| 199 | return array_merge(self::_inactiveScanOptions(), array( |
| 200 | 'spamvertizeCheck' => true, |
| 201 | 'checkSpamIP' => true, |
| 202 | 'scansEnabled_checkGSB' => true, |
| 203 | 'scansEnabled_checkHowGetIPs' => true, |
| 204 | 'scansEnabled_checkReadableConfig' => true, |
| 205 | 'scansEnabled_suspectedFiles' => true, |
| 206 | 'scansEnabled_core' => true, |
| 207 | 'scansEnabled_coreUnknown' => true, |
| 208 | 'scansEnabled_malware' => true, |
| 209 | 'scansEnabled_fileContents' => true, |
| 210 | 'scansEnabled_fileContentsGSB' => true, |
| 211 | 'scansEnabled_posts' => true, |
| 212 | 'scansEnabled_comments' => true, |
| 213 | 'scansEnabled_suspiciousOptions' => true, |
| 214 | 'scansEnabled_oldVersions' => true, |
| 215 | 'scansEnabled_suspiciousAdminUsers' => true, |
| 216 | 'scansEnabled_passwds' => true, |
| 217 | 'scansEnabled_diskSpace' => true, |
| 218 | 'scansEnabled_wafStatus' => true, |
| 219 | 'scan_exclude' => wfConfig::get('scan_exclude', ''), |
| 220 | 'scan_include_extra' => wfConfig::get('scan_include_extra', ''), |
| 221 | 'scansEnabled_geoipSupport' => true, |
| 222 | )); |
| 223 | } |
| 224 | |
| 225 | /** |
| 226 | * Returns an array of the scan options (as keys) and the corresponding value for the high sensitivity scan type. |
| 227 | * |
| 228 | * @return array |
| 229 | */ |
| 230 | public static function highSensitivityScanTypeOptions() { |
| 231 | return array_merge(self::_inactiveScanOptions(), array( |
| 232 | 'spamvertizeCheck' => true, |
| 233 | 'checkSpamIP' => true, |
| 234 | 'scansEnabled_checkGSB' => true, |
| 235 | 'scansEnabled_checkHowGetIPs' => true, |
| 236 | 'scansEnabled_checkReadableConfig' => true, |
| 237 | 'scansEnabled_suspectedFiles' => true, |
| 238 | 'scansEnabled_core' => true, |
| 239 | 'scansEnabled_themes' => true, |
| 240 | 'scansEnabled_plugins' => true, |
| 241 | 'scansEnabled_coreUnknown' => true, |
| 242 | 'scansEnabled_malware' => true, |
| 243 | 'scansEnabled_fileContents' => true, |
| 244 | 'scansEnabled_fileContentsGSB' => true, |
| 245 | 'scansEnabled_posts' => true, |
| 246 | 'scansEnabled_comments' => true, |
| 247 | 'scansEnabled_suspiciousOptions' => true, |
| 248 | 'scansEnabled_oldVersions' => true, |
| 249 | 'scansEnabled_suspiciousAdminUsers' => true, |
| 250 | 'scansEnabled_passwds' => true, |
| 251 | 'scansEnabled_diskSpace' => true, |
| 252 | 'scansEnabled_wafStatus' => true, |
| 253 | 'other_scanOutside' => true, |
| 254 | 'scansEnabled_scanImages' => true, |
| 255 | 'scan_exclude' => wfConfig::get('scan_exclude', ''), |
| 256 | 'scan_include_extra' => wfConfig::get('scan_include_extra', ''), |
| 257 | 'scansEnabled_geoipSupport' => true, |
| 258 | )); |
| 259 | } |
| 260 | |
| 261 | /** |
| 262 | * Returns an array of the scan options (as keys) and the corresponding value for the custom scan type. |
| 263 | * |
| 264 | * @return array |
| 265 | */ |
| 266 | public static function customScanTypeOptions() { |
| 267 | $allOptions = self::_inactiveScanOptions(); |
| 268 | foreach ($allOptions as $key => &$value) { |
| 269 | $value = wfConfig::get($key); |
| 270 | } |
| 271 | |
| 272 | $allOptions['scansEnabled_geoipSupport'] = true; |
| 273 | $allOptions['scansEnabled_highSense'] = false; //deprecated |
| 274 | |
| 275 | return $allOptions; |
| 276 | } |
| 277 | |
| 278 | /** |
| 279 | * Returns an array of scan options and their inactive values for convenience in merging with the various scan type |
| 280 | * option arrays. |
| 281 | * |
| 282 | * @return array |
| 283 | */ |
| 284 | protected static function _inactiveScanOptions() { |
| 285 | return array( |
| 286 | 'spamvertizeCheck' => false, |
| 287 | 'checkSpamIP' => false, |
| 288 | 'scansEnabled_checkGSB' => false, |
| 289 | 'scansEnabled_checkHowGetIPs' => false, |
| 290 | 'scansEnabled_checkReadableConfig' => false, |
| 291 | 'scansEnabled_suspectedFiles' => false, |
| 292 | 'scansEnabled_core' => false, |
| 293 | 'scansEnabled_themes' => false, |
| 294 | 'scansEnabled_plugins' => false, |
| 295 | 'scansEnabled_coreUnknown' => false, |
| 296 | 'scansEnabled_malware' => false, |
| 297 | 'scansEnabled_fileContents' => false, |
| 298 | 'scan_include_extra' => '', |
| 299 | 'scansEnabled_fileContentsGSB' => false, |
| 300 | 'scansEnabled_posts' => false, |
| 301 | 'scansEnabled_comments' => false, |
| 302 | 'scansEnabled_suspiciousOptions' => false, |
| 303 | 'scansEnabled_oldVersions' => false, |
| 304 | 'scansEnabled_suspiciousAdminUsers' => false, |
| 305 | 'scansEnabled_passwds' => false, |
| 306 | 'scansEnabled_diskSpace' => false, |
| 307 | 'scansEnabled_wafStatus' => false, |
| 308 | 'other_scanOutside' => false, |
| 309 | 'scansEnabled_scanImages' => false, |
| 310 | 'scansEnabled_highSense' => false, |
| 311 | 'lowResourceScansEnabled' => false, |
| 312 | 'scan_exclude' => '', |
| 313 | 'scansEnabled_geoipSupport' => false, |
| 314 | ); |
| 315 | } |
| 316 | |
| 317 | /** |
| 318 | * Returns the scan options only available to premium users. |
| 319 | * |
| 320 | * @return array |
| 321 | */ |
| 322 | protected static function _premiumScanOptions() { |
| 323 | return array('spamvertizeCheck', 'checkSpamIP', 'scansEnabled_checkGSB'); |
| 324 | } |
| 325 | |
| 326 | /** |
| 327 | * Returns an array of weights for calculating the scan option status score. |
| 328 | * |
| 329 | * @return array |
| 330 | */ |
| 331 | protected static function _scanOptionWeights() { |
| 332 | return array( |
| 333 | 'spamvertizeCheck' => 0.05, |
| 334 | 'checkSpamIP' => 0.05, |
| 335 | 'scansEnabled_checkGSB' => 0.05, |
| 336 | 'scansEnabled_checkHowGetIPs' => 0.05, |
| 337 | 'scansEnabled_checkReadableConfig' => 0.05, |
| 338 | 'scansEnabled_suspectedFiles' => 0.05, |
| 339 | 'scansEnabled_core' => 0.05, |
| 340 | 'scansEnabled_themes' => 0, |
| 341 | 'scansEnabled_plugins' => 0, |
| 342 | 'scansEnabled_coreUnknown' => 0.05, |
| 343 | 'scansEnabled_malware' => 0.05, |
| 344 | 'scansEnabled_fileContents' => 0.1, |
| 345 | 'scan_include_extra' => 0, |
| 346 | 'scansEnabled_fileContentsGSB' => 0.05, |
| 347 | 'scansEnabled_posts' => 0.05, |
| 348 | 'scansEnabled_comments' => 0.05, |
| 349 | 'scansEnabled_suspiciousOptions' => 0.05, |
| 350 | 'scansEnabled_oldVersions' => 0.1, |
| 351 | 'scansEnabled_suspiciousAdminUsers' => 0.05, |
| 352 | 'scansEnabled_passwds' => 0.05, |
| 353 | 'scansEnabled_diskSpace' => 0.05, |
| 354 | 'other_scanOutside' => 0, |
| 355 | 'scansEnabled_scanImages' => 0, |
| 356 | 'scansEnabled_highSense' => 0, |
| 357 | 'lowResourceScansEnabled' => 0, |
| 358 | 'scan_exclude' => 0, |
| 359 | 'scansEnabled_geoipSupport' => 0, |
| 360 | 'scansEnabled_wafStatus' => 0, |
| 361 | ); |
| 362 | } |
| 363 | |
| 364 | /** |
| 365 | * wfScanner constructor. |
| 366 | * @param int|bool $scanType If false, defaults to the config option `scanType`. |
| 367 | */ |
| 368 | public function __construct($scanType = false) { |
| 369 | if ($scanType === false || !self::isValidScanType($scanType)) { |
| 370 | $this->_scanType = wfConfig::get('scanType'); |
| 371 | } |
| 372 | else { |
| 373 | $this->_scanType = $scanType; |
| 374 | } |
| 375 | } |
| 376 | |
| 377 | /** |
| 378 | * Returns whether or not the scanner will run as premium. |
| 379 | * |
| 380 | * @return bool |
| 381 | */ |
| 382 | public function isPremiumScan() { |
| 383 | return !!wfConfig::get('isPaid'); |
| 384 | } |
| 385 | |
| 386 | /** |
| 387 | * Returns whether or not automatic scans will run. |
| 388 | * |
| 389 | * @return bool |
| 390 | */ |
| 391 | public function isEnabled() { |
| 392 | return !!wfConfig::get('scheduledScansEnabled'); |
| 393 | } |
| 394 | |
| 395 | /** |
| 396 | * Returns whether or not a scan is running. A scan is considered running if the timestamp |
| 397 | * under wf_scanRunning is within WORDFENCE_MAX_SCAN_LOCK_TIME seconds of now. |
| 398 | * |
| 399 | * @return bool |
| 400 | */ |
| 401 | public function isRunning() { |
| 402 | $scanRunning = wfConfig::get('wf_scanRunning'); |
| 403 | return ($scanRunning && time() - $scanRunning < WORDFENCE_MAX_SCAN_LOCK_TIME); |
| 404 | } |
| 405 | |
| 406 | /** |
| 407 | * Returns the current scan scheduling mode. |
| 408 | * |
| 409 | * @return string One of the SCAN_SCHEDULING_MODE_ constants |
| 410 | */ |
| 411 | public function schedulingMode() { |
| 412 | if (wfConfig::get('isPaid') && wfConfig::get('schedMode') == 'manual') { |
| 413 | return self::SCAN_SCHEDULING_MODE_MANUAL; |
| 414 | } |
| 415 | return self::SCAN_SCHEDULING_MODE_AUTOMATIC; |
| 416 | } |
| 417 | |
| 418 | /** |
| 419 | * Returns the manual scheduling type. This is only applicable when the scheduling mode is |
| 420 | * SCAN_SCHEDULING_MODE_MANUAL. |
| 421 | * |
| 422 | * @return string One of the MANUAL_SCHEDULING_ constants. |
| 423 | */ |
| 424 | public function manualSchedulingType() { |
| 425 | return wfConfig::get('manualScanType', self::MANUAL_SCHEDULING_ONCE_DAILY); |
| 426 | } |
| 427 | |
| 428 | /** |
| 429 | * Returns the start hour used for non-custom manual schedules. This is initially random but may be modified |
| 430 | * by the user later. |
| 431 | * |
| 432 | * @return int An hour number. |
| 433 | */ |
| 434 | public function manualSchedulingStartHour() { |
| 435 | return wfConfig::get('schedStartHour'); |
| 436 | } |
| 437 | |
| 438 | /** |
| 439 | * Returns the currently defined custom schedule. This is only applicable when the scheduling mode is |
| 440 | * SCAN_SCHEDULING_MODE_MANUAL and the manual type is set to MANUAL_SCHEDULING_CUSTOM. |
| 441 | * |
| 442 | * @return array The array will be of the format array(0 => array(0 => false, 1 => false ... 23 => false), ... 6 => array(...)) |
| 443 | */ |
| 444 | public function customSchedule() { |
| 445 | $normalizedSchedule = array_fill(0, 7, array_fill(0, 24, false)); |
| 446 | $storedSchedule = wfConfig::get_ser('scanSched', array()); |
| 447 | if (is_array($storedSchedule) && !empty($storedSchedule) && is_array($storedSchedule[0])) { |
| 448 | foreach ($storedSchedule as $dayNumber => $day) { |
| 449 | foreach ($day as $hourNumber => $enabled) { |
| 450 | $normalizedSchedule[$dayNumber][$hourNumber] = wfUtils::truthyToBoolean($enabled); |
| 451 | } |
| 452 | } |
| 453 | } |
| 454 | return $normalizedSchedule; |
| 455 | } |
| 456 | |
| 457 | public function shouldRunQuickScan() { |
| 458 | if (!$this->isEnabled()) { |
| 459 | return false; |
| 460 | } |
| 461 | |
| 462 | if (time() - $this->lastQuickScanTime() < 79200) { //Do not run within 22 hours of a completed quick scan |
| 463 | return false; |
| 464 | } |
| 465 | |
| 466 | $lastFullScanCompletion = (int) $this->lastScanTime(); |
| 467 | if (time() - $lastFullScanCompletion < 43200) { //Do not run within 12 hours of a completed full scan |
| 468 | return false; |
| 469 | } |
| 470 | |
| 471 | $nextFullScan = $this->nextScheduledScanTime(); |
| 472 | if ($nextFullScan === false || $nextFullScan - time() < 3600) { //Scans are not running (e.g., custom schedule selected with no times configured) or if scheduled, then avoid running within 1 hour of a pending full scan |
| 473 | return false; |
| 474 | } |
| 475 | |
| 476 | $now = time(); |
| 477 | $tzOffset = wfUtils::formatLocalTime('Z', $now); |
| 478 | $currentDayOfWeekUTC = date('w', $now); |
| 479 | $currentHourUTC = date('G', $now); |
| 480 | $preferredHourUTC = false; |
| 481 | |
| 482 | if ($this->schedulingMode() == wfScanner::SCAN_SCHEDULING_MODE_MANUAL) { |
| 483 | $manualType = $this->manualSchedulingType(); |
| 484 | $preferredHourUTC = fmod(round(($this->manualSchedulingStartHour() * 3600 - $tzOffset) / 3600, 2), 24); //round() rather than floor() to account for fractional time zones |
| 485 | switch ($manualType) { |
| 486 | case self::MANUAL_SCHEDULING_ONCE_DAILY: |
| 487 | case self::MANUAL_SCHEDULING_EVERY_OTHER_DAY: |
| 488 | case self::MANUAL_SCHEDULING_WEEKDAYS: |
| 489 | case self::MANUAL_SCHEDULING_WEEKENDS: |
| 490 | case self::MANUAL_SCHEDULING_ODD_DAYS_WEEKENDS: |
| 491 | $preferredHourUTC = fmod($preferredHourUTC + 12, 24); |
| 492 | break; |
| 493 | case self::MANUAL_SCHEDULING_TWICE_DAILY: |
| 494 | $preferredHourUTC = fmod($preferredHourUTC + 6, 24); //When automatic scans run twice daily, possibly run a quick scan 6 hours offset (will only run if either scheduled one fails for some reason) |
| 495 | break; |
| 496 | case self::MANUAL_SCHEDULING_CUSTOM: //Iterate from the current day backwards and base it on the first time found, may or may not actually run depending on the spacing of the custom schedule |
| 497 | $preferredHourUTC = false; |
| 498 | $oneWeekSchedule = $this->customSchedule(); |
| 499 | for ($i = 7; $i > 0; $i--) { //Sample sequence for `$currentDayOfWeekUTC == 2` => 2, 1, 0, 6, 5, 4, 3 |
| 500 | $checkingDayNumber = ($currentDayOfWeekUTC + $i) % 7; |
| 501 | $day = $oneWeekSchedule[$checkingDayNumber]; |
| 502 | $dayHour = array_search(true, $day); |
| 503 | if ($dayHour !== false) { |
| 504 | $preferredHourUTC = fmod(round(($dayHour * 3600 - $tzOffset) / 3600, 2) + 12, 24); |
| 505 | break; |
| 506 | } |
| 507 | } |
| 508 | break; |
| 509 | } |
| 510 | |
| 511 | if ($preferredHourUTC !== false) { //The preferred hour of a manual scan schedule has been determined, run the quick scan at the desired offset if we're in that hour |
| 512 | return ($currentHourUTC >= $preferredHourUTC); |
| 513 | } |
| 514 | } |
| 515 | |
| 516 | $noc1ScanSchedule = wfConfig::get_ser('noc1ScanSchedule', array()); |
| 517 | if (count($noc1ScanSchedule)) { |
| 518 | $preferredHourUTC = (int) fmod( (fmod($noc1ScanSchedule[0], 86400) / 3600) + 12, 24); |
| 519 | return ($currentHourUTC >= $preferredHourUTC); |
| 520 | } |
| 521 | |
| 522 | return false; //If we've reached this point, the scan config is in a weird state so just skip the quick scan |
| 523 | } |
| 524 | |
| 525 | /** |
| 526 | * Returns an associative array containing the current state each scan stage and its corresponding status. |
| 527 | * |
| 528 | * @return array |
| 529 | */ |
| 530 | public function stageStatus() { |
| 531 | $status = $this->_defaultStageStatuses(); |
| 532 | $runningStatus = wfConfig::get_ser('scanStageStatuses', array(), false); |
| 533 | $status = array_merge($status, $runningStatus); |
| 534 | |
| 535 | foreach ($status as $stage => &$value) { //Convert value array into status only |
| 536 | $value = $value['status']; |
| 537 | if (!$this->isRunning() && $value == self::STATUS_RUNNING) { |
| 538 | $value = self::STATUS_PENDING; |
| 539 | } |
| 540 | } |
| 541 | |
| 542 | return $status; |
| 543 | } |
| 544 | |
| 545 | /** |
| 546 | * Returns an array of all scan options for the given stage that are enabled. |
| 547 | * |
| 548 | * @param string $stage One of the STAGE_ constants |
| 549 | * @return array |
| 550 | */ |
| 551 | private function _scanJobsForStage($stage) { |
| 552 | $always = array(); |
| 553 | $options = array(); |
| 554 | switch ($stage) { |
| 555 | case self::STAGE_SPAMVERTISING_CHECKS: |
| 556 | $options = array( |
| 557 | 'spamvertizeCheck', |
| 558 | ); |
| 559 | break; |
| 560 | case self::STAGE_SPAM_CHECK: |
| 561 | $options = array( |
| 562 | 'checkSpamIP', |
| 563 | ); |
| 564 | break; |
| 565 | case self::STAGE_BLACKLIST_CHECK: |
| 566 | $options = array( |
| 567 | 'scansEnabled_checkGSB', |
| 568 | ); |
| 569 | break; |
| 570 | case self::STAGE_SERVER_STATE: |
| 571 | if ($this->scanType() != self::SCAN_TYPE_QUICK) { |
| 572 | $always = array( |
| 573 | 'checkSkippedFiles', |
| 574 | ); |
| 575 | } |
| 576 | $options = array( |
| 577 | 'scansEnabled_checkHowGetIPs', |
| 578 | 'scansEnabled_diskSpace', |
| 579 | 'scansEnabled_wafStatus', |
| 580 | 'scansEnabled_geoipSupport', |
| 581 | ); |
| 582 | break; |
| 583 | case self::STAGE_FILE_CHANGES: |
| 584 | $options = array( |
| 585 | 'scansEnabled_core', |
| 586 | 'scansEnabled_themes', |
| 587 | 'scansEnabled_plugins', |
| 588 | 'scansEnabled_coreUnknown', |
| 589 | ); |
| 590 | break; |
| 591 | case self::STAGE_PUBLIC_FILES: |
| 592 | $options = array( |
| 593 | 'scansEnabled_checkReadableConfig', |
| 594 | 'scansEnabled_suspectedFiles', |
| 595 | ); |
| 596 | break; |
| 597 | case self::STAGE_MALWARE_SCAN: |
| 598 | $options = array( |
| 599 | 'scansEnabled_malware', |
| 600 | 'scansEnabled_fileContents', |
| 601 | ); |
| 602 | break; |
| 603 | case self::STAGE_CONTENT_SAFETY: |
| 604 | $options = array( |
| 605 | 'scansEnabled_posts', |
| 606 | 'scansEnabled_comments', |
| 607 | 'scansEnabled_fileContentsGSB', |
| 608 | ); |
| 609 | break; |
| 610 | case self::STAGE_PASSWORD_STRENGTH: |
| 611 | $options = array( |
| 612 | 'scansEnabled_passwds', |
| 613 | ); |
| 614 | break; |
| 615 | case self::STAGE_VULNERABILITY_SCAN: |
| 616 | $options = array( |
| 617 | 'scansEnabled_oldVersions', |
| 618 | ); |
| 619 | break; |
| 620 | case self::STAGE_OPTIONS_AUDIT: |
| 621 | $options = array( |
| 622 | 'scansEnabled_suspiciousOptions', |
| 623 | 'scansEnabled_suspiciousAdminUsers', |
| 624 | ); |
| 625 | break; |
| 626 | } |
| 627 | |
| 628 | $enabledOptions = $this->scanOptions(); |
| 629 | $filteredOptions = array(); |
| 630 | foreach ($options as $o) { |
| 631 | if (isset($enabledOptions[$o]) && $enabledOptions[$o]) { |
| 632 | $filteredOptions[] = $o; |
| 633 | } |
| 634 | } |
| 635 | |
| 636 | return array_merge($filteredOptions, $always); |
| 637 | } |
| 638 | |
| 639 | /** |
| 640 | * Returns an associative array containing each scan stage's default state. The keys are the stage identifiers and the value |
| 641 | * is an array in the format |
| 642 | * array( |
| 643 | * 'started' => the number of tasks for this stage that have started (initially 0), |
| 644 | * 'finished' => the number of tasks that have started and finished (initially 0), |
| 645 | * 'expected' => the expected number of tasks to run for this stage (based on the scan type and options enabled) |
| 646 | * ) |
| 647 | * |
| 648 | * @return array |
| 649 | */ |
| 650 | private function _defaultStageStatuses() { |
| 651 | $status = array( |
| 652 | self::STAGE_SPAMVERTISING_CHECKS => array('status' => ($this->isPremiumScan() ? self::STATUS_PENDING : self::STATUS_PREMIUM), 'started' => 0, 'finished' => 0, 'expected' => 0), |
| 653 | self::STAGE_SPAM_CHECK => array('status' => ($this->isPremiumScan() ? self::STATUS_PENDING : self::STATUS_PREMIUM), 'started' => 0, 'finished' => 0, 'expected' => 0), |
| 654 | self::STAGE_BLACKLIST_CHECK => array('status' => ($this->isPremiumScan() ? self::STATUS_PENDING : self::STATUS_PREMIUM), 'started' => 0, 'finished' => 0, 'expected' => 0), |
| 655 | self::STAGE_SERVER_STATE => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0), |
| 656 | self::STAGE_FILE_CHANGES => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0), |
| 657 | self::STAGE_PUBLIC_FILES => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0), |
| 658 | self::STAGE_MALWARE_SCAN => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0), |
| 659 | self::STAGE_CONTENT_SAFETY => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0), |
| 660 | self::STAGE_PASSWORD_STRENGTH => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0), |
| 661 | self::STAGE_VULNERABILITY_SCAN => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0), |
| 662 | self::STAGE_OPTIONS_AUDIT => array('status' => self::STATUS_PENDING, 'started' => 0, 'finished' => 0, 'expected' => 0), |
| 663 | ); |
| 664 | |
| 665 | foreach ($status as $stage => &$parameters) { |
| 666 | if ($parameters['status'] == self::STATUS_PREMIUM) { |
| 667 | continue; |
| 668 | } |
| 669 | |
| 670 | $jobs = $this->_scanJobsForStage($stage); |
| 671 | if (count($jobs)) { |
| 672 | $parameters['expected'] = count($jobs); |
| 673 | } |
| 674 | else { |
| 675 | $parameters['status'] = self::STATUS_DISABLED; |
| 676 | } |
| 677 | } |
| 678 | |
| 679 | return $status; |
| 680 | } |
| 681 | |
| 682 | /** |
| 683 | * Resets the state of the scan stage status record. |
| 684 | */ |
| 685 | public function resetStages() { |
| 686 | if ($this->scanType() == self::SCAN_TYPE_QUICK) { //Suppress for quick scans |
| 687 | return; |
| 688 | } |
| 689 | wfConfig::set_ser('scanStageStatuses', $this->_defaultStageStatuses(), false, wfConfig::DONT_AUTOLOAD); |
| 690 | } |
| 691 | |
| 692 | private function _shouldForceUpdate($stageID) { |
| 693 | if ($stageID == wfScanner::STAGE_MALWARE_SCAN) { |
| 694 | return true; |
| 695 | } |
| 696 | return false; |
| 697 | } |
| 698 | |
| 699 | /** |
| 700 | * Increments the stage started counter and marks it as running if not already in that state. |
| 701 | * |
| 702 | * @param string $stageID One of the STAGE_ constants |
| 703 | */ |
| 704 | public function startStage($stageID) { |
| 705 | if ($this->scanType() == self::SCAN_TYPE_QUICK) { //Suppress for quick scans |
| 706 | return; |
| 707 | } |
| 708 | |
| 709 | $runningStatus = wfConfig::get_ser('scanStageStatuses', array(), false); |
| 710 | if ($runningStatus[$stageID]['status'] != self::STATUS_RUNNING_WARNING) { |
| 711 | $runningStatus[$stageID]['status'] = self::STATUS_RUNNING; |
| 712 | } |
| 713 | |
| 714 | $runningStatus[$stageID]['started'] += 1; |
| 715 | wfConfig::set_ser('scanStageStatuses', $runningStatus, false, wfConfig::DONT_AUTOLOAD); |
| 716 | if (wfCentral::isConnected() && ($this->_shouldForceUpdate($stageID) || (time() - wfConfig::getInt('lastScanStageStatusUpdate', 0)) > self::CENTRAL_STAGE_UPDATE_THRESHOLD)) { |
| 717 | wfCentral::updateScanStatus($runningStatus); |
| 718 | } |
| 719 | } |
| 720 | |
| 721 | /** |
| 722 | * Increments the stage finished counter and updates the stage status according to whether it's fully finished or encountered a negative status. |
| 723 | * |
| 724 | * @param string $stageID One of the STAGE_ constants. |
| 725 | * @param string $status One of the wfIssues::STATUS_ constants |
| 726 | */ |
| 727 | public function completeStage($stageID, $status) { |
| 728 | if ($this->scanType() == self::SCAN_TYPE_QUICK) { //Suppress for quick scans |
| 729 | return; |
| 730 | } |
| 731 | |
| 732 | $runningStatus = wfConfig::get_ser('scanStageStatuses', array(), false); |
| 733 | |
| 734 | if ($runningStatus[$stageID]['status'] == self::STATUS_RUNNING && ($status == wfIssues::STATE_PROBLEM)) { |
| 735 | $runningStatus[$stageID]['status'] = self::STATUS_RUNNING_WARNING; |
| 736 | } |
| 737 | |
| 738 | $runningStatus[$stageID]['finished'] += 1; |
| 739 | if ($runningStatus[$stageID]['finished'] >= $runningStatus[$stageID]['expected']) { |
| 740 | if ($runningStatus[$stageID]['status'] == self::STATUS_RUNNING) { |
| 741 | $runningStatus[$stageID]['status'] = self::STATUS_COMPLETE_SUCCESS; |
| 742 | } |
| 743 | else { |
| 744 | $runningStatus[$stageID]['status'] = self::STATUS_COMPLETE_WARNING; |
| 745 | } |
| 746 | } |
| 747 | |
| 748 | wfConfig::set_ser('scanStageStatuses', $runningStatus, false, wfConfig::DONT_AUTOLOAD); |
| 749 | if (wfCentral::isConnected()) { |
| 750 | $forceSend = true; //Force sending the last stage completion update even if the timing would otherwise prevent it |
| 751 | foreach ($runningStatus as $stageID => $stage) { |
| 752 | if ($runningStatus[$stageID]['finished'] < $runningStatus[$stageID]['expected']) { |
| 753 | $forceSend = false; |
| 754 | break; |
| 755 | } |
| 756 | } |
| 757 | |
| 758 | if ($forceSend || (time() - wfConfig::getInt('lastScanStageStatusUpdate', 0)) > self::CENTRAL_STAGE_UPDATE_THRESHOLD) { |
| 759 | wfCentral::updateScanStatus($runningStatus); |
| 760 | } |
| 761 | } |
| 762 | |
| 763 | } |
| 764 | |
| 765 | /** |
| 766 | * Returns the selected type of the scan. |
| 767 | * |
| 768 | * @return string |
| 769 | */ |
| 770 | public function scanType() { |
| 771 | switch ($this->_scanType) { |
| 772 | case self::SCAN_TYPE_QUICK://SCAN_TYPE_QUICK is not user-selectable |
| 773 | case self::SCAN_TYPE_LIMITED: |
| 774 | case self::SCAN_TYPE_STANDARD: |
| 775 | case self::SCAN_TYPE_HIGH_SENSITIVITY: |
| 776 | case self::SCAN_TYPE_CUSTOM: |
| 777 | return $this->_scanType; |
| 778 | } |
| 779 | return self::SCAN_TYPE_STANDARD; |
| 780 | } |
| 781 | |
| 782 | /** |
| 783 | * Returns the display name for the selected type of the scan. |
| 784 | * |
| 785 | * @return string |
| 786 | */ |
| 787 | public function scanTypeName() { |
| 788 | switch ($this->_scanType) { |
| 789 | case self::SCAN_TYPE_QUICK: |
| 790 | return __('Quick Scan', 'wordfence'); |
| 791 | case self::SCAN_TYPE_LIMITED: |
| 792 | return __('Limited Scan', 'wordfence'); |
| 793 | case self::SCAN_TYPE_HIGH_SENSITIVITY: |
| 794 | return __('High Sensitivity', 'wordfence'); |
| 795 | case self::SCAN_TYPE_CUSTOM: |
| 796 | return __('Custom Scan', 'wordfence'); |
| 797 | case self::SCAN_TYPE_STANDARD: |
| 798 | default: |
| 799 | return __('Standard Scan', 'wordfence'); |
| 800 | } |
| 801 | } |
| 802 | |
| 803 | /** |
| 804 | * Returns a normalized percentage (i.e., in the range [0, 1]) to the corresponding display percentage |
| 805 | * based on license type. |
| 806 | * |
| 807 | * @param float $percentage |
| 808 | * @return float |
| 809 | */ |
| 810 | protected function _normalizedPercentageToDisplay($percentage) { |
| 811 | if ($this->isPremiumScan()) { |
| 812 | return round($percentage, 2); |
| 813 | } |
| 814 | |
| 815 | return round($percentage * 0.70, 2); |
| 816 | } |
| 817 | |
| 818 | /** |
| 819 | * Returns a normalized percentage (i.e., in the range [0, 1]) for the scan type status indicator. |
| 820 | * |
| 821 | * @return float |
| 822 | */ |
| 823 | public function scanTypeStatus() { |
| 824 | $isFree = !wfConfig::get('isPaid'); |
| 825 | $weights = self::_scanOptionWeights(); |
| 826 | $options = $this->scanOptions(); |
| 827 | $score = 0.0; |
| 828 | $premiumOptions = self::_premiumScanOptions(); |
| 829 | foreach ($options as $key => $value) { |
| 830 | if ($isFree && array_search($key, $premiumOptions) !== false) { |
| 831 | continue; |
| 832 | } |
| 833 | |
| 834 | if ($value) { |
| 835 | $score += $weights[$key]; |
| 836 | } |
| 837 | } |
| 838 | return $this->_normalizedPercentageToDisplay($score); |
| 839 | } |
| 840 | |
| 841 | public function scanTypeStatusList() { |
| 842 | $isFree = !wfConfig::get('isPaid'); |
| 843 | $weights = self::_scanOptionWeights(); |
| 844 | $options = $this->scanOptions(); |
| 845 | $disabledOptionCount = 0; |
| 846 | $premiumDisabledOptionCount = 0; |
| 847 | $percentage = 0.0; |
| 848 | $premiumPercentage = 0.0; |
| 849 | $premiumOptions = self::_premiumScanOptions(); |
| 850 | $statusList = array(); |
| 851 | foreach ($options as $key => $value) { |
| 852 | if ($isFree && array_search($key, $premiumOptions) !== false) { |
| 853 | $premiumPercentage += $weights[$key]; |
| 854 | $premiumDisabledOptionCount++; |
| 855 | continue; |
| 856 | } |
| 857 | |
| 858 | if (!$value && $weights[$key] > 0) { |
| 859 | $percentage += $weights[$key]; |
| 860 | $disabledOptionCount++; |
| 861 | } |
| 862 | } |
| 863 | |
| 864 | $remainingPercentage = 1 - $this->scanTypeStatus(); |
| 865 | if ($isFree) { |
| 866 | $remainingPercentage -= 0.30; |
| 867 | $statusList[] = array( |
| 868 | 'percentage' => 0.30, |
| 869 | 'title' => __('Enable Premium Scan Signatures.', 'wordfence'), |
| 870 | ); |
| 871 | } |
| 872 | |
| 873 | if ($premiumPercentage > 0) { |
| 874 | $subtraction = min($this->_normalizedPercentageToDisplay($premiumPercentage), $remainingPercentage); |
| 875 | $remainingPercentage -= $subtraction; |
| 876 | $statusList[] = array( |
| 877 | 'percentage' => $subtraction, |
| 878 | 'title' => __('Enable Premium Reputation Checks.', 'wordfence'), |
| 879 | ); |
| 880 | } |
| 881 | |
| 882 | if ($percentage > 0) { |
| 883 | $subtraction = min($this->_normalizedPercentageToDisplay($percentage), $remainingPercentage); |
| 884 | $statusList[] = array( |
| 885 | 'percentage' => $subtraction, |
| 886 | 'title' => sprintf(/* translators: option count */ _n('Enable %d scan option.', 'Enable %d scan options.', $disabledOptionCount,'wordfence'), number_format_i18n($disabledOptionCount)), |
| 887 | ); |
| 888 | } |
| 889 | |
| 890 | return $statusList; |
| 891 | } |
| 892 | /** |
| 893 | * Returns the malware signature feed that is in use. |
| 894 | * |
| 895 | * @return string |
| 896 | */ |
| 897 | public function signatureMode() { |
| 898 | if ($this->isPremiumScan()) { |
| 899 | return self::SIGNATURE_MODE_PREMIUM; |
| 900 | } |
| 901 | return self::SIGNATURE_MODE_COMMUNITY; |
| 902 | } |
| 903 | |
| 904 | /** |
| 905 | * Returns a normalized percentage (i.e., in the range [0, 1]) for the reputation status indicator. |
| 906 | * |
| 907 | * @return float |
| 908 | */ |
| 909 | public function reputationStatus() { |
| 910 | $score = 0.0; |
| 911 | if ($this->isPremiumScan()) { |
| 912 | $options = $this->scanOptions(); |
| 913 | if ($options['spamvertizeCheck']) { $score += 0.333; } |
| 914 | if ($options['checkSpamIP']) { $score += 0.333; } |
| 915 | if ($options['scansEnabled_checkGSB']) { $score += 0.333; } |
| 916 | } |
| 917 | return round($score, 2); |
| 918 | } |
| 919 | |
| 920 | /** |
| 921 | * @return array |
| 922 | */ |
| 923 | public function reputationStatusList() { |
| 924 | $statusList = array(); |
| 925 | $options = $this->scanOptions(); |
| 926 | |
| 927 | $reputationChecks = array( |
| 928 | 'spamvertizeCheck' => __('Enable scan option to check if this website is being "Spamvertised".', 'wordfence'), |
| 929 | 'checkSpamIP' => __('Enable scan option to check if your website IP is generating spam.', 'wordfence'), |
| 930 | 'scansEnabled_checkGSB' => __('Enable scan option to check if your website is on a domain blocklist.', 'wordfence'), |
| 931 | ); |
| 932 | |
| 933 | foreach ($reputationChecks as $option => $optionLabel) { |
| 934 | if (!$this->isPremiumScan() || !$options[$option]) { |
| 935 | $statusList[] = array( |
| 936 | 'percentage' => round(1 / count($reputationChecks), 2), |
| 937 | 'title' => $optionLabel, |
| 938 | ); |
| 939 | } |
| 940 | } |
| 941 | return $statusList; |
| 942 | } |
| 943 | |
| 944 | /** |
| 945 | * Returns the options for the configured scan type. |
| 946 | * |
| 947 | * @return array |
| 948 | */ |
| 949 | public function scanOptions() { |
| 950 | switch ($this->scanType()) { |
| 951 | case self::SCAN_TYPE_QUICK: |
| 952 | return self::quickScanTypeOptions(); |
| 953 | case self::SCAN_TYPE_LIMITED: |
| 954 | return self::limitedScanTypeOptions(); |
| 955 | case self::SCAN_TYPE_STANDARD: |
| 956 | return self::standardScanTypeOptions(); |
| 957 | case self::SCAN_TYPE_HIGH_SENSITIVITY: |
| 958 | return self::highSensitivityScanTypeOptions(); |
| 959 | case self::SCAN_TYPE_CUSTOM: |
| 960 | return self::customScanTypeOptions(); |
| 961 | } |
| 962 | } |
| 963 | |
| 964 | /** |
| 965 | * Returns the array of jobs for the scan type. |
| 966 | * |
| 967 | * @return array |
| 968 | */ |
| 969 | public function jobs() { |
| 970 | $options = $this->scanOptions(); |
| 971 | $preferredOrder = array( |
| 972 | 'checkSpamvertized' => array('spamvertizeCheck'), |
| 973 | 'checkSpamIP' => array('checkSpamIP'), |
| 974 | 'checkGSB' => array('scansEnabled_checkGSB'), |
| 975 | 'checkHowGetIPs' => array('scansEnabled_checkHowGetIPs'), |
| 976 | 'diskSpace' => array('scansEnabled_diskSpace'), |
| 977 | 'wafStatus' => array('scansEnabled_wafStatus'), |
| 978 | 'geoipSupport' => array('scansEnabled_geoipSupport'), |
| 979 | 'checkSkippedFiles' => ($this->scanType() != self::SCAN_TYPE_QUICK), //Always runs except for quick |
| 980 | 'knownFiles' => ($this->scanType() != self::SCAN_TYPE_QUICK), //Always runs except for quick, options are scansEnabled_core, scansEnabled_themes, scansEnabled_plugins, scansEnabled_coreUnknown, scansEnabled_malware |
| 981 | 'checkReadableConfig' => array('scansEnabled_checkReadableConfig'), |
| 982 | 'fileContents' => ($this->scanType() != self::SCAN_TYPE_QUICK), //Always runs except for quick, options are scansEnabled_fileContents and scansEnabled_fileContentsGSB |
| 983 | 'suspectedFiles' => array('scansEnabled_suspectedFiles'), |
| 984 | 'posts' => array('scansEnabled_posts'), |
| 985 | 'comments' => array('scansEnabled_comments'), |
| 986 | 'passwds' => array('scansEnabled_passwds'), |
| 987 | 'oldVersions' => array('scansEnabled_oldVersions'), |
| 988 | 'suspiciousAdminUsers' => array('scansEnabled_suspiciousAdminUsers'), |
| 989 | 'suspiciousOptions' => array('scansEnabled_suspiciousOptions'), |
| 990 | ); |
| 991 | |
| 992 | $jobs = array(); |
| 993 | foreach ($preferredOrder as $job => $enabler) { |
| 994 | if ($enabler === true) { |
| 995 | $jobs[] = $job; |
| 996 | } |
| 997 | else if (is_array($enabler)) { |
| 998 | foreach ($enabler as $o) { |
| 999 | if ($options[$o]) { |
| 1000 | $jobs[] = $job; |
| 1001 | break; |
| 1002 | } |
| 1003 | } |
| 1004 | } |
| 1005 | } |
| 1006 | return $jobs; |
| 1007 | } |
| 1008 | |
| 1009 | /** |
| 1010 | * Returns whether or not the scanner should use its low resource mode. |
| 1011 | * |
| 1012 | * @return bool |
| 1013 | */ |
| 1014 | public function useLowResourceScanning() { |
| 1015 | $options = $this->scanOptions(); |
| 1016 | return $options['lowResourceScansEnabled']; |
| 1017 | } |
| 1018 | |
| 1019 | /** |
| 1020 | * Returns the array of user-defined malware signatures for use by the scanner. |
| 1021 | * |
| 1022 | * @return array |
| 1023 | */ |
| 1024 | public function userScanSignatures() { |
| 1025 | $options = $this->scanOptions(); |
| 1026 | $value = $options['scan_include_extra']; |
| 1027 | $signatures = array(); |
| 1028 | if (!empty($value)) { |
| 1029 | $regexs = explode("\n", $value); |
| 1030 | $id = 1000001; |
| 1031 | foreach ($regexs as $r) { |
| 1032 | $r = rtrim($r, "\r"); |
| 1033 | if (preg_match('/' . $r . '/i', "") !== false) { |
| 1034 | $signatures[] = array($id++, time(), $r, __('User defined scan pattern', 'wordfence')); |
| 1035 | } |
| 1036 | } |
| 1037 | } |
| 1038 | return $signatures; |
| 1039 | } |
| 1040 | |
| 1041 | /** |
| 1042 | * Returns whether or not the scanner should check files outside of the WordPress installation. |
| 1043 | * |
| 1044 | * @return bool |
| 1045 | */ |
| 1046 | public function scanOutsideWordPress() { |
| 1047 | $options = $this->scanOptions(); |
| 1048 | return $options['other_scanOutside']; |
| 1049 | } |
| 1050 | |
| 1051 | /** |
| 1052 | * Returns the cleaned up array of user-excluded scan paths and patterns. |
| 1053 | * |
| 1054 | * @return array |
| 1055 | */ |
| 1056 | public function userExclusions() { |
| 1057 | $options = $this->scanOptions(); |
| 1058 | $value = $options['scan_exclude']; |
| 1059 | return explode("\n", wfUtils::cleanupOneEntryPerLine($value)); |
| 1060 | } |
| 1061 | |
| 1062 | /** |
| 1063 | * Fetches the scan summary items into the internal cache. |
| 1064 | */ |
| 1065 | private function _fetchSummaryItems() { |
| 1066 | if ($this->_summary !== false) { |
| 1067 | return; |
| 1068 | } |
| 1069 | |
| 1070 | $this->_summary = wfConfig::get_ser('wf_summaryItems', array()); |
| 1071 | } |
| 1072 | |
| 1073 | /** |
| 1074 | * Writes the scan summary cache to permanent storage. |
| 1075 | */ |
| 1076 | private function _saveSummaryItems() { |
| 1077 | if ($this->_summary !== false && $this->_dirty) { |
| 1078 | $this->_summary['lastUpdate'] = time(); |
| 1079 | wfConfig::set_ser('wf_summaryItems', $this->_summary); |
| 1080 | } |
| 1081 | $this->_dirty = false; |
| 1082 | } |
| 1083 | |
| 1084 | /** |
| 1085 | * Saves the scan summary cache if it has been more than two seconds since the last update. |
| 1086 | * |
| 1087 | * @return bool Whether or not it saved. |
| 1088 | */ |
| 1089 | private function _maybeSaveSummaryItems() { |
| 1090 | if ($this->_summary !== false && $this->_summary['lastUpdate'] < (time() - 2)) { |
| 1091 | $this->_saveSummaryItems(); |
| 1092 | return true; |
| 1093 | } |
| 1094 | return false; |
| 1095 | } |
| 1096 | |
| 1097 | /** |
| 1098 | * Populates the scan summary with the default counters. |
| 1099 | */ |
| 1100 | public function resetSummaryItems() { |
| 1101 | global $wpdb; |
| 1102 | |
| 1103 | $this->_summary = array(); |
| 1104 | $this->_summary[self::SUMMARY_SCANNED_POSTS] = 0; |
| 1105 | $this->_summary[self::SUMMARY_SCANNED_COMMENTS] = 0; |
| 1106 | $this->_summary[self::SUMMARY_SCANNED_FILES] = 0; |
| 1107 | $this->_summary[self::SUMMARY_SCANNED_PLUGINS] = 0; |
| 1108 | $this->_summary[self::SUMMARY_SCANNED_THEMES] = 0; |
| 1109 | $this->_summary[self::SUMMARY_SCANNED_USERS] = 0; |
| 1110 | $this->_summary[self::SUMMARY_SCANNED_URLS] = 0; |
| 1111 | |
| 1112 | $this->_dirty = true; |
| 1113 | $this->_saveSummaryItems(); |
| 1114 | } |
| 1115 | |
| 1116 | /** |
| 1117 | * Forces a save of the scan summary cache. |
| 1118 | */ |
| 1119 | public function flushSummaryItems() { |
| 1120 | $this->_saveSummaryItems(); |
| 1121 | } |
| 1122 | |
| 1123 | /** |
| 1124 | * Returns the corresponding summary value for $key or $default if not found. |
| 1125 | * |
| 1126 | * @param $key |
| 1127 | * @param mixed $default The value returned if there is no value for $key. |
| 1128 | * @return mixed |
| 1129 | */ |
| 1130 | public function getSummaryItem($key, $default = false) { |
| 1131 | $this->_fetchSummaryItems(); |
| 1132 | if (isset($this->_summary[$key])) { |
| 1133 | return $this->_summary[$key]; |
| 1134 | } |
| 1135 | return $default; |
| 1136 | } |
| 1137 | |
| 1138 | /** |
| 1139 | * Returns an array of all summary items. |
| 1140 | * |
| 1141 | * @return array |
| 1142 | */ |
| 1143 | public function getAllSummaryItems() { |
| 1144 | $this->_fetchSummaryItems(); |
| 1145 | if (is_array($this->_summary)) { |
| 1146 | return $this->_summary; |
| 1147 | } |
| 1148 | return array(); |
| 1149 | } |
| 1150 | |
| 1151 | /** |
| 1152 | * Sets the summary item $key as $value. |
| 1153 | * |
| 1154 | * @param $key |
| 1155 | * @param $value |
| 1156 | */ |
| 1157 | public function setSummaryItem($key, $value) { |
| 1158 | $this->_fetchSummaryItems(); |
| 1159 | $this->_summary[$key] = $value; |
| 1160 | $this->_dirty = true; |
| 1161 | |
| 1162 | if (!$this->_maybeSaveSummaryItems() && !$this->_destructRegistered) { |
| 1163 | register_shutdown_function(array($this, 'flushSummaryItems')); |
| 1164 | $this->_destructRegistered = true; |
| 1165 | } |
| 1166 | } |
| 1167 | |
| 1168 | /** |
| 1169 | * Atomically increments the summary item under $key by $value. |
| 1170 | * |
| 1171 | * @param $key |
| 1172 | * @param int $value |
| 1173 | */ |
| 1174 | public function incrementSummaryItem($key, $value = 1) { |
| 1175 | if ($value == 0) { return; } |
| 1176 | $this->_fetchSummaryItems(); |
| 1177 | if (isset($this->_summary[$key])) { |
| 1178 | $this->_summary[$key] += $value; |
| 1179 | $this->_dirty = true; |
| 1180 | |
| 1181 | if (!$this->_maybeSaveSummaryItems() && !$this->_destructRegistered) { |
| 1182 | register_shutdown_function(array($this, 'flushSummaryItems')); |
| 1183 | $this->_destructRegistered = true; |
| 1184 | } |
| 1185 | } |
| 1186 | } |
| 1187 | |
| 1188 | /** |
| 1189 | * Schedules a single scan for the given time. If $originalTime is provided, it will be associated with the cron. |
| 1190 | * |
| 1191 | * @param $futureTime |
| 1192 | * @param bool|int $originalTime |
| 1193 | */ |
| 1194 | public function scheduleSingleScan($futureTime, $originalTime = false) { |
| 1195 | if (is_main_site()) { // Removed ability to activate on network site in v5.3.12 |
| 1196 | if ($originalTime === false) { |
| 1197 | $originalTime = $futureTime; |
| 1198 | } |
| 1199 | wp_schedule_single_event($futureTime, 'wordfence_start_scheduled_scan', array((int) $originalTime)); |
| 1200 | |
| 1201 | //Saving our own copy of the schedule because the wp-cron functions all require the args list to act |
| 1202 | $allScansScheduled = wfConfig::get_ser('allScansScheduled', array()); |
| 1203 | $allScansScheduled[] = array('timestamp' => $futureTime, 'args' => array((int) $originalTime)); |
| 1204 | wfConfig::set_ser('allScansScheduled', $allScansScheduled); |
| 1205 | } |
| 1206 | } |
| 1207 | |
| 1208 | /** |
| 1209 | * Clears all scheduled scan cron jobs and re-creates them. |
| 1210 | */ |
| 1211 | public function scheduleScans() { |
| 1212 | $this->unscheduleAllScans(); |
| 1213 | if (!$this->isEnabled()) { |
| 1214 | return; |
| 1215 | } |
| 1216 | |
| 1217 | if ($this->schedulingMode() == wfScanner::SCAN_SCHEDULING_MODE_MANUAL) { |
| 1218 | //Generate a two-week schedule |
| 1219 | $manualType = $this->manualSchedulingType(); |
| 1220 | $preferredHour = $this->manualSchedulingStartHour(); |
| 1221 | switch ($manualType) { |
| 1222 | case self::MANUAL_SCHEDULING_ONCE_DAILY: |
| 1223 | $schedule = array_fill(0, 14, array_fill(0, 24, 0)); |
| 1224 | foreach ($schedule as $dayNumber => &$day) { |
| 1225 | $day[$preferredHour] = 1; |
| 1226 | } |
| 1227 | break; |
| 1228 | case self::MANUAL_SCHEDULING_TWICE_DAILY: |
| 1229 | $schedule = array_fill(0, 14, array_fill(0, 24, 0)); |
| 1230 | foreach ($schedule as $dayNumber => &$day) { |
| 1231 | $day[$preferredHour] = 1; |
| 1232 | $day[fmod($preferredHour + 12, 24)] = 1; |
| 1233 | } |
| 1234 | break; |
| 1235 | case self::MANUAL_SCHEDULING_EVERY_OTHER_DAY: |
| 1236 | $baseDay = floor(time() / 86400); |
| 1237 | $schedule = array_fill(0, 14, array_fill(0, 24, 0)); |
| 1238 | foreach ($schedule as $dayNumber => &$day) { |
| 1239 | if (($baseDay + $dayNumber) % 2) { |
| 1240 | $day[$preferredHour] = 1; |
| 1241 | } |
| 1242 | } |
| 1243 | break; |
| 1244 | case self::MANUAL_SCHEDULING_WEEKDAYS: |
| 1245 | $schedule = array_fill(0, 14, array_fill(0, 24, 0)); |
| 1246 | foreach ($schedule as $dayNumber => &$day) { |
| 1247 | if ($dayNumber > 0 && $dayNumber < 6) { |
| 1248 | $day[$preferredHour] = 1; |
| 1249 | } |
| 1250 | } |
| 1251 | break; |
| 1252 | case self::MANUAL_SCHEDULING_WEEKENDS: |
| 1253 | $schedule = array_fill(0, 14, array_fill(0, 24, 0)); |
| 1254 | foreach ($schedule as $dayNumber => &$day) { |
| 1255 | if ($dayNumber == 0 || $dayNumber == 6) { |
| 1256 | $day[$preferredHour] = 1; |
| 1257 | } |
| 1258 | } |
| 1259 | break; |
| 1260 | case self::MANUAL_SCHEDULING_ODD_DAYS_WEEKENDS: |
| 1261 | $schedule = array_fill(0, 14, array_fill(0, 24, 0)); |
| 1262 | foreach ($schedule as $dayNumber => &$day) { |
| 1263 | if ($dayNumber == 0 || $dayNumber == 6 || ($dayNumber % 2)) { |
| 1264 | $day[$preferredHour] = 1; |
| 1265 | } |
| 1266 | } |
| 1267 | break; |
| 1268 | case self::MANUAL_SCHEDULING_CUSTOM: |
| 1269 | $oneWeekSchedule = $this->customSchedule(); |
| 1270 | $schedule = array(); |
| 1271 | foreach ($oneWeekSchedule as $day) { $schedule[] = $day; } |
| 1272 | foreach ($oneWeekSchedule as $day) { $schedule[] = $day; } |
| 1273 | break; |
| 1274 | } |
| 1275 | |
| 1276 | $now = time(); |
| 1277 | $tzOffset = wfUtils::formatLocalTime('Z', $now); |
| 1278 | |
| 1279 | //Apply the time zone shift so the start times align to the server's time zone |
| 1280 | $shiftedSchedule = array_fill(0, 14, array()); |
| 1281 | foreach ($schedule as $dayNumber => $day) { |
| 1282 | foreach ($day as $hourNumber => $enabled) { |
| 1283 | if ($enabled) { |
| 1284 | $effectiveHour = round(($hourNumber * 3600 - $tzOffset) / 3600); //round() rather than floor() to account for fractional time zones |
| 1285 | $wrappedHour = (int) fmod($effectiveHour + 24, 24); |
| 1286 | if ($effectiveHour < 0) { |
| 1287 | if ($dayNumber > 0) { |
| 1288 | $shiftedSchedule[$dayNumber - 1][$wrappedHour] = true; |
| 1289 | } |
| 1290 | } |
| 1291 | else if ($effectiveHour > 23) { |
| 1292 | if ($dayNumber < count($schedule) - 1) { |
| 1293 | $shiftedSchedule[$dayNumber + 1][$wrappedHour] = true; |
| 1294 | } |
| 1295 | } |
| 1296 | else { |
| 1297 | $shiftedSchedule[$dayNumber][$effectiveHour] = true; |
| 1298 | } |
| 1299 | } |
| 1300 | } |
| 1301 | } |
| 1302 | $schedule = $shiftedSchedule; |
| 1303 | |
| 1304 | //Trim out all but an 8-day period |
| 1305 | $currentDayOfWeekUTC = date('w', $now); |
| 1306 | $currentHourUTC = date('G', $now); |
| 1307 | $periodStart = floor($now / 86400) * 86400 - $currentDayOfWeekUTC * 86400; |
| 1308 | $schedule = array_slice($schedule, $currentDayOfWeekUTC, null, true); |
| 1309 | $schedule = array_slice($schedule, 0, 8, true); |
| 1310 | |
| 1311 | //Schedule them |
| 1312 | foreach ($schedule as $dayNumber => $day) { |
| 1313 | foreach ($day as $hourNumber => $enabled) { |
| 1314 | if ($enabled) { |
| 1315 | if ($dayNumber == $currentDayOfWeekUTC && $currentHourUTC > $hourNumber) { //It's today and we've already passed its hour, skip it |
| 1316 | continue; |
| 1317 | } |
| 1318 | else if ($dayNumber > 6 && ($dayNumber % 7) == $currentDayOfWeekUTC && $currentHourUTC <= $hourNumber) { //It's one week from today but beyond the current hour, skip it this cycle |
| 1319 | continue; |
| 1320 | } |
| 1321 | |
| 1322 | $scanTime = $periodStart + $dayNumber * 86400 + $hourNumber * 3600 + wfWAFUtils::random_int(0, 3600); |
| 1323 | wordfence::status(4, 'info', sprintf( |
| 1324 | /* translators: 1. Day of week. 2. Hour of day. 3. Localized date. */ |
| 1325 | __('Scheduled time for day %1$s hour %2$s is: %3$s', 'wordfence'), |
| 1326 | $dayNumber, |
| 1327 | $hourNumber, |
| 1328 | wfUtils::formatLocalTime('l jS \of F Y h:i:s A P', $scanTime) |
| 1329 | )); |
| 1330 | $this->scheduleSingleScan($scanTime); |
| 1331 | } |
| 1332 | } |
| 1333 | } |
| 1334 | } |
| 1335 | else { |
| 1336 | $noc1ScanSchedule = wfConfig::get_ser('noc1ScanSchedule', array()); |
| 1337 | foreach ($noc1ScanSchedule as $timestamp) { |
| 1338 | $timestamp = wfUtils::denormalizedTime($timestamp); |
| 1339 | if ($timestamp > time()) { |
| 1340 | $this->scheduleSingleScan($timestamp); |
| 1341 | } |
| 1342 | } |
| 1343 | } |
| 1344 | } |
| 1345 | |
| 1346 | public function unscheduleAllScans() { |
| 1347 | $allScansScheduled = wfConfig::get_ser('allScansScheduled', array()); |
| 1348 | foreach ($allScansScheduled as $entry) { |
| 1349 | wp_unschedule_event($entry['timestamp'], 'wordfence_start_scheduled_scan', $entry['args']); |
| 1350 | } |
| 1351 | wp_clear_scheduled_hook('wordfence_start_scheduled_scan'); |
| 1352 | wfConfig::set_ser('allScansScheduled', array()); |
| 1353 | } |
| 1354 | |
| 1355 | public function nextScheduledScanTime() { |
| 1356 | $nextTime = false; |
| 1357 | $cron = _get_cron_array(); |
| 1358 | foreach($cron as $key => $val){ |
| 1359 | if(isset($val['wordfence_start_scheduled_scan'])){ |
| 1360 | $nextTime = $key; |
| 1361 | break; |
| 1362 | } |
| 1363 | } |
| 1364 | return $nextTime; |
| 1365 | } |
| 1366 | |
| 1367 | public function lastScanTime() { |
| 1368 | return wfConfig::get('scanTime'); |
| 1369 | } |
| 1370 | |
| 1371 | public function recordLastScanTime() { |
| 1372 | wfConfig::set('scanTime', microtime(true)); |
| 1373 | } |
| 1374 | |
| 1375 | public function lastQuickScanTime() { |
| 1376 | return wfConfig::get('lastQuickScan', 0); |
| 1377 | } |
| 1378 | |
| 1379 | public function recordLastQuickScanTime() { |
| 1380 | wfConfig::set('lastQuickScan', microtime(true)); |
| 1381 | } |
| 1382 | } |