| @@ -1008,8 +1008,28 @@ | ||
| 1008 | 1008 | return count( $pending_users ); |
| 1009 | 1009 | } |
| 1010 | 1010 | |
| 1011 | 1011 | /** |
| 1012 | + * Whether XML-RPC is blocked, whole or its pingback methods | |
| 1013 | + * | |
| 1014 | + * Both levels have to say so: the three-way setting under WP Hardening, read | |
| 1015 | + * through the module's own resolver, and the WP Hardening module that | |
| 1016 | + * enforces it. Until 3.0.2 the Dashboard read login_security[disable_xmlrpc], | |
| 1017 | + * the checkbox that moved to WP Hardening in 2.9.7 and is never written any | |
| 1018 | + * more, so the score never granted its three points and the recommendation | |
| 1019 | + * to disable XML-RPC stayed on for good. | |
| 1020 | + * | |
| 1021 | + * @since 3.0.2 | |
| 1022 | + * | |
| 1023 | + * @param array $options Plugin options. | |
| 1024 | + * @return bool | |
| 1025 | + */ | |
| 1026 | + private function is_xmlrpc_protected( $options ) { | |
| 1027 | + return ! empty( $options['modules']['wp_hardening'] ) | |
| 1028 | + && 'none' !== Vigilante_Comment_Security::resolve_xmlrpc_mode( $this->settings ); | |
| 1029 | + } | |
| 1030 | + | |
| 1031 | + /** | |
| 1012 | 1032 | * Calculate comprehensive security score (0-100) |
| 1013 | 1033 | * |
| 1014 | 1034 | * @param array $options Plugin options. |
| 1015 | 1035 | * @return int Security score. |
| @@ -1067,10 +1087,10 @@ | ||
| 1067 | 1087 | // Max attempts configured |
| 1068 | 1088 | if ( isset( $login['max_attempts'] ) && $login['max_attempts'] <= 5 ) { |
| 1069 | 1089 | $score += 3; |
| 1070 | 1090 | } |
| 1071 | - // XML-RPC disabled | |
| 1072 | - if ( ! empty( $login['disable_xmlrpc'] ) ) { | |
| 1091 | + // XML-RPC blocked, whole or its pingback methods | |
| 1092 | + if ( $this->is_xmlrpc_protected( $options ) ) { | |
| 1073 | 1093 | $score += 3; |
| 1074 | 1094 | } |
| 1075 | 1095 | // 2FA enabled |
| 1076 | 1096 | if ( ! empty( $login['two_factor']['enabled'] ) ) { |
| @@ -1310,14 +1330,14 @@ | ||
| 1310 | 1330 | ); |
| 1311 | 1331 | } |
| 1312 | 1332 | |
| 1313 | 1333 | // Low: XML-RPC enabled (only if login security is active) |
| 1314 | - if ( ! empty( $options['modules']['login_security'] ) && empty( $login['disable_xmlrpc'] ) ) { | |
| 1334 | + if ( ! empty( $options['modules']['login_security'] ) && ! $this->is_xmlrpc_protected( $options ) ) { | |
| 1315 | 1335 | $recommendations[] = array( |
| 1316 | 1336 | 'icon' => 'info', |
| 1317 | 1337 | 'priority' => 'low', |
| 1318 | 1338 | 'message' => __( 'Disable XML-RPC if not needed (reduces attack surface).', 'vigilante' ), |
| 1319 | - 'tab' => 'login', | |
| 1339 | + 'tab' => 'wp-hardening', | |
| 1320 | 1340 | ); |
| 1321 | 1341 | } |
| 1322 | 1342 | |
| 1323 | 1343 | // Low: Strong passwords not enforced (only if user security is active) |
| @@ -1809,9 +1829,8 @@ | ||
| 1809 | 1829 | 'scanComplete' => __( 'Scan complete', 'vigilante' ), |
| 1810 | 1830 | 'loading' => __( 'Loading...', 'vigilante' ), |
| 1811 | 1831 | 'searching' => __( 'Searching...', 'vigilante' ), |
| 1812 | 1832 | 'noUsersFound' => __( 'No users found', 'vigilante' ), |
| 1813 | - 'searchError' => __( 'Error searching users', 'vigilante' ), | |
| 1814 | 1833 | 'sending' => __( 'Sending...', 'vigilante' ), |
| 1815 | 1834 | 'sendNotification' => __( 'Send notification now', 'vigilante' ), |
| 1816 | 1835 | 'notificationsSent' => __( 'notifications sent', 'vigilante' ), |
| 1817 | 1836 | 'skipped' => __( 'skipped', 'vigilante' ), |
| @@ -1830,9 +1849,9 @@ | ||
| 1830 | 1849 | 'modified' => __( 'Modified', 'vigilante' ), |
| 1831 | 1850 | 'suspicious' => __( 'Suspicious', 'vigilante' ), |
| 1832 | 1851 | 'totalScanned' => __( 'Total Scanned', 'vigilante' ), |
| 1833 | 1852 | 'suspiciousFiles' => __( 'Suspicious Files', 'vigilante' ), |
| 1834 | - 'suspiciousWarning' => __( 'These files may contain malicious code or are in unexpected locations. Review immediately!', 'vigilante' ), | |
| 1853 | + 'suspiciousWarning' => __( 'Files that can run code where none is expected, or that contain code written the way malware hides itself. Real infections show up here, and so do harmless files that plugins generate. Check the path and the reason before acting.', 'vigilante' ), | |
| 1835 | 1854 | 'file' => __( 'File', 'vigilante' ), |
| 1836 | 1855 | 'reason' => __( 'Reason', 'vigilante' ), |
| 1837 | 1856 | 'type' => __( 'Type', 'vigilante' ), |
| 1838 | 1857 | 'unknown' => __( 'Unknown', 'vigilante' ), |
| @@ -1841,13 +1860,13 @@ | ||
| 1841 | 1860 | 'logWhatItMeans' => __( 'What it means', 'vigilante' ), |
| 1842 | 1861 | 'logWhatToDo' => __( 'What to do', 'vigilante' ), |
| 1843 | 1862 | 'logSelfSeeDetails' => __( 'Open File Integrity for the full detail', 'vigilante' ), |
| 1844 | 1863 | 'modifiedFiles' => __( 'Modified Files', 'vigilante' ), |
| 1845 | - 'modifiedDescription' => __( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ), | |
| 1864 | + 'modifiedDescription' => __( 'These files do not match the copy WordPress.org publishes for the installed version.', 'vigilante' ), | |
| 1846 | 1865 | 'extraFiles' => __( 'Extra Files', 'vigilante' ), |
| 1847 | 1866 | 'extra' => __( 'Extra', 'vigilante' ), |
| 1848 | 1867 | 'ignored' => __( 'Ignored', 'vigilante' ), |
| 1849 | - 'extraDescription' => __( 'PHP files found in plugins or themes that are not part of the original distribution from WordPress.org.', 'vigilante' ), | |
| 1868 | + 'extraDescription' => __( 'Files that are not part of WordPress, or of the plugin or theme they sit in. Many are legitimate, and an injected file looks the same from here, so confirm where each one comes from.', 'vigilante' ), | |
| 1850 | 1869 | 'actions' => __( 'Actions', 'vigilante' ), |
| 1851 | 1870 | 'ignore' => __( 'Ignore', 'vigilante' ), |
| 1852 | 1871 | 'ignoring' => __( 'Ignoring...', 'vigilante' ), |
| 1853 | 1872 | 'fileIgnored' => __( 'File added to ignored list.', 'vigilante' ), |
| @@ -1855,9 +1874,8 @@ | ||
| 1855 | 1874 | 'confirmClearIgnored' => __( 'Remove all files from the ignored list? They will appear in scan results again.', 'vigilante' ), |
| 1856 | 1875 | 'ignoredCleared' => __( 'Ignored files list cleared. Page will reload...', 'vigilante' ), |
| 1857 | 1876 | 'selectAll' => __( 'Select all', 'vigilante' ), |
| 1858 | 1877 | 'bulkIgnoreSelected' => __( 'Ignore selected', 'vigilante' ), |
| 1859 | - 'bulkUnignoreSelected'=> __( 'Stop ignoring selected', 'vigilante' ), | |
| 1860 | 1878 | 'bulkNoSelection' => __( 'Select at least one file first.', 'vigilante' ), |
| 1861 | 1879 | 'bulkConfirmIgnore' => __( 'Ignore the selected files? They will be hidden from future scan results until you remove them from the ignored list.', 'vigilante' ), |
| 1862 | 1880 | 'bulkConfirmUnignore' => __( 'Remove the selected files from the ignored list? They will appear in scan results again.', 'vigilante' ), |
| 1863 | 1881 | 'bulkProcessing' => __( 'Processing...', 'vigilante' ), |
| @@ -1862,11 +1880,11 @@ | ||
| 1862 | 1880 | 'bulkConfirmUnignore' => __( 'Remove the selected files from the ignored list? They will appear in scan results again.', 'vigilante' ), |
| 1863 | 1881 | 'bulkProcessing' => __( 'Processing...', 'vigilante' ), |
| 1864 | 1882 | /* translators: %d: number of files selected for bulk action. */ |
| 1865 | 1883 | 'bulkSelectedCount' => __( '%d selected', 'vigilante' ), |
| 1866 | - 'allClear' => __( 'All files verified - no issues found!', 'vigilante' ), | |
| 1884 | + 'allClear' => __( 'Good job! Nothing to report in the files that were checked.', 'vigilante' ), | |
| 1867 | 1885 | 'criticalConfigTitle' => __( 'Critical config files modified', 'vigilante' ), |
| 1868 | - 'criticalConfigDesc' => __( 'These files are common targets for code injection. Review the changes and approve if they are legitimate. Vigilant\'s own blocks are excluded from this check.', 'vigilante' ), | |
| 1886 | + 'criticalConfigDesc' => __( 'These files changed since you last approved them. They are a common target for code injection, and they are also rewritten by cache, security and redirection plugins, often on every update. Review the changes and approve them if they belong to something you use: approving records the current content as the reference and does not touch the file. Vigilant\'s own blocks are excluded from this check, and Vigilant never restores these files by itself.', 'vigilante' ), | |
| 1869 | 1887 | 'approve' => __( 'Approve', 'vigilante' ), |
| 1870 | 1888 | 'approving' => __( 'Approving...', 'vigilante' ), |
| 1871 | 1889 | 'approvalLockedNotice' => $this->critical_approval_notice(), |
| 1872 | 1890 | 'criticalApproved' => __( 'Change approved. Next scan will use the current state as baseline.', 'vigilante' ), |
| @@ -1959,13 +1977,10 @@ | ||
| 1959 | 1977 | 'noLogsToExport' => __( 'No logs to export', 'vigilante' ), |
| 1960 | 1978 | 'exportFailed' => __( 'Export failed', 'vigilante' ), |
| 1961 | 1979 | 'exportLogs' => __( 'Export Logs', 'vigilante' ), |
| 1962 | 1980 | // Backup strings |
| 1963 | - 'backupCreated' => __( 'Backup created successfully.', 'vigilante' ), | |
| 1964 | 1981 | 'createBackupNow' => __( 'Download Backup', 'vigilante' ), |
| 1965 | 1982 | 'downloadBackup' => __( 'Download Backup (.zip)', 'vigilante' ), |
| 1966 | - /* translators: %d: number of tables */ | |
| 1967 | - 'tablesCount' => __( '%d tables', 'vigilante' ), | |
| 1968 | 1983 | /* translators: 1: table count, 2: human-readable size */ |
| 1969 | 1984 | 'dbTablesTotal' => __( '%1$d tables total (%2$s)', 'vigilante' ), |
| 1970 | 1985 | /* translators: 1: selected count, 2: human-readable size */ |
| 1971 | 1986 | 'dbTablesSelected' => __( '%1$d tables selected (%2$s)', 'vigilante' ), |
| @@ -1972,9 +1987,8 @@ | ||
| 1972 | 1987 | // Settings search strings |
| 1973 | 1988 | 'searchNoResults' => __( 'No matching settings found.', 'vigilante' ), |
| 1974 | 1989 | /* translators: %d: number of results that did not fit in the list. */ |
| 1975 | 1990 | 'searchMoreResults' => __( '%d more results. Refine the search to see them.', 'vigilante' ), |
| 1976 | - 'searchInTab' => __( 'in', 'vigilante' ), | |
| 1977 | 1991 | // Modules string |
| 1978 | 1992 | /* translators: 1: enabled count, 2: total count */ |
| 1979 | 1993 | 'modulesEnabled' => __( '%1$d / %2$d modules enabled', 'vigilante' ), |
| 1980 | 1994 | // Activity log label maps for JS rendering |
| @@ -1979,8 +1993,9 @@ | ||
| 1979 | 1993 | 'modulesEnabled' => __( '%1$d / %2$d modules enabled', 'vigilante' ), |
| 1980 | 1994 | // Activity log label maps for JS rendering |
| 1981 | 1995 | 'eventTypeLabels' => array( |
| 1982 | 1996 | 'login' => __( 'Login', 'vigilante' ), |
| 1997 | + '2fa' => __( 'Two-factor', 'vigilante' ), | |
| 1983 | 1998 | 'user' => __( 'User', 'vigilante' ), |
| 1984 | 1999 | 'content' => __( 'Content', 'vigilante' ), |
| 1985 | 2000 | 'plugin' => __( 'Plugin', 'vigilante' ), |
| 1986 | 2001 | 'theme' => __( 'Theme', 'vigilante' ), |
| @@ -2046,10 +2061,8 @@ | ||
| 2046 | 2061 | /* translators: 1: first item number, 2: last item number, 3: total items */ |
| 2047 | 2062 | 'paginationOf' => __( '%1$d–%2$d of %3$d', 'vigilante' ), |
| 2048 | 2063 | 'paginationEmpty' => __( '0 items', 'vigilante' ), |
| 2049 | 2064 | // Security Analyzer strings |
| 2050 | - 'analyzerScanNow' => __( 'Scan now', 'vigilante' ), | |
| 2051 | - 'analyzerScanning' => __( 'Scanning…', 'vigilante' ), | |
| 2052 | 2065 | 'analyzerFastPhase' => __( 'Running fast checks…', 'vigilante' ), |
| 2053 | 2066 | 'analyzerSlowPhase' => __( 'Running remote checks…', 'vigilante' ), |
| 2054 | 2067 | 'analyzerScanComplete' => __( 'Security scan complete.', 'vigilante' ), |
| 2055 | 2068 | 'analyzerScanFailed' => __( 'Security scan failed.', 'vigilante' ), |
| @@ -2055,11 +2068,8 @@ | ||
| 2055 | 2068 | 'analyzerScanFailed' => __( 'Security scan failed.', 'vigilante' ), |
| 2056 | 2069 | 'analyzerShowDetails' => __( 'Show detailed breakdown', 'vigilante' ), |
| 2057 | 2070 | 'analyzerHideDetails' => __( 'Hide detailed breakdown', 'vigilante' ), |
| 2058 | 2071 | 'analyzerGoToSetting' => __( 'Go to setting', 'vigilante' ), |
| 2059 | - 'analyzerNoData' => __( 'No data yet — run a scan to populate this category.', 'vigilante' ), | |
| 2060 | - 'analyzerJustNow' => __( 'just now', 'vigilante' ), | |
| 2061 | - 'analyzerAgo' => __( 'ago', 'vigilante' ), | |
| 2062 | 2072 | 'analyzerSettingsSaved' => __( 'Analyzer settings saved.', 'vigilante' ), |
| 2063 | 2073 | 'analyzerLastScanJustNow' => __( 'Last scan just now', 'vigilante' ), |
| 2064 | 2074 | 'analyzerQualityExcellent' => __( 'Excellent', 'vigilante' ), |
| 2065 | 2075 | 'analyzerQualityGood' => __( 'Good', 'vigilante' ), |
| @@ -2065,9 +2075,8 @@ | ||
| 2065 | 2075 | 'analyzerQualityGood' => __( 'Good', 'vigilante' ), |
| 2066 | 2076 | 'analyzerQualityFair' => __( 'Fair', 'vigilante' ), |
| 2067 | 2077 | 'analyzerQualityPoor' => __( 'Poor', 'vigilante' ), |
| 2068 | 2078 | 'analyzerQualityCritical' => __( 'Critical', 'vigilante' ), |
| 2069 | - 'analyzerPts' => __( 'pts', 'vigilante' ), | |
| 2070 | 2079 | 'analyzerLearnMore' => __( 'Learn more', 'vigilante' ), |
| 2071 | 2080 | 'analyzerInfoAllClear' => __( 'All clear', 'vigilante' ), |
| 2072 | 2081 | /* translators: %d: number of findings in an info-only category */ |
| 2073 | 2082 | 'analyzerInfoFindings' => __( '%d findings', 'vigilante' ), |
| @@ -2703,9 +2712,9 @@ | ||
| 2703 | 2712 | <span class="dashicons dashicons-shield-alt" aria-hidden="true"></span> |
| 2704 | 2713 | <?php esc_html_e( 'Security Check', 'vigilante' ); ?> |
| 2705 | 2714 | </h2> |
| 2706 | 2715 | <p class="description"> |
| 2707 | - <?php esc_html_e( 'On-demand audit of what an attacker would see right now, plus 13 internal checks impossible from the outside.', 'vigilante' ); ?> | |
| 2716 | + <?php esc_html_e( 'On-demand audit of what an attacker would see right now, plus internal checks impossible from the outside.', 'vigilante' ); ?> | |
| 2708 | 2717 | </p> |
| 2709 | 2718 | <p class="vigilante-analyzer-last-scan" data-role="ran-at"> |
| 2710 | 2719 | <span class="dashicons dashicons-clock" aria-hidden="true"></span> |
| 2711 | 2720 | <?php echo esc_html( $ran_at_human ); ?> |
| @@ -3721,8 +3730,9 @@ | ||
| 3721 | 3730 | <label> |
| 3722 | 3731 | <input type="checkbox" name="firewall[block_bad_bots]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( ! empty( $options['block_bad_bots'] ) ); ?>> |
| 3723 | 3732 | <?php esc_html_e( 'Block known malicious bots and scanners', 'vigilante' ); ?> |
| 3724 | 3733 | </label> |
| 3734 | + <p class="description"><?php esc_html_e( 'The list also stops SEO crawlers such as Ahrefs, Semrush, Majestic and Screaming Frog, and command line clients such as curl, wget and python-requests. To let one of them through, add its name to the User-Agent Whitelist below. On Apache part of the list is applied in .htaccess, before WordPress loads, so those requests get a 403 and do not appear in the Security Audit.', 'vigilante' ); ?></p> | |
| 3725 | 3735 | </td> |
| 3726 | 3736 | </tr> |
| 3727 | 3737 | </table> |
| 3728 | 3738 | |
| @@ -4035,9 +4045,9 @@ | ||
| 4035 | 4045 | <th scope="row"><?php esc_html_e( 'Limit HTTP Methods', 'vigilante' ); ?></th> |
| 4036 | 4046 | <td> |
| 4037 | 4047 | <label> |
| 4038 | 4048 | <input type="checkbox" name="firewall[limit_http_methods]" value="1" <?php checked( ! empty( $options['limit_http_methods'] ) ); ?>> |
| 4039 | - <?php esc_html_e( 'Allow only GET, POST, HEAD (blocks PUT, DELETE, TRACE, etc.)', 'vigilante' ); ?> | |
| 4049 | + <?php esc_html_e( 'Block the HTTP methods WordPress does not use (TRACE, TRACK, CONNECT and any unknown one). GET, POST, HEAD, OPTIONS, PUT, PATCH and DELETE keep working.', 'vigilante' ); ?> | |
| 4040 | 4050 | </label> |
| 4041 | 4051 | </td> |
| 4042 | 4052 | </tr> |
| 4043 | 4053 | </table> |
| @@ -4773,8 +4783,9 @@ | ||
| 4773 | 4783 | <label> |
| 4774 | 4784 | <input type="checkbox" name="security_headers[csp][enabled]" value="1" <?php checked( ! empty( $options['csp']['enabled'] ) ); ?>> |
| 4775 | 4785 | <?php esc_html_e( 'Enable Content Security Policy', 'vigilante' ); ?> |
| 4776 | 4786 | </label> |
| 4787 | + <p class="description"><?php esc_html_e( 'The default policy includes upgrade-insecure-requests, which asks browsers to request every http:// resource over https. Vigilant writes it only when the site address starts with https://, because on a site served over http it would stop every stylesheet, script and image from loading. If your site is served over https through a proxy or CDN that terminates TLS, set the site address to https:// in Settings > General; the rules follow the next time this tab is saved or Vigilant is updated.', 'vigilante' ); ?></p> | |
| 4777 | 4788 | </td> |
| 4778 | 4789 | </tr> |
| 4779 | 4790 | <tr> |
| 4780 | 4791 | <th scope="row"><?php esc_html_e( 'Report Only Mode', 'vigilante' ); ?></th> |
| @@ -6317,9 +6328,9 @@ | ||
| 6317 | 6328 | <select id="vigilante-f-audit-alerts-immediate-min-severity" name="audit_alerts[immediate][min_severity]"> |
| 6318 | 6329 | <option value="critical" <?php selected( $alert_severity, 'critical' ); ?>><?php esc_html_e( 'Critical only (recommended)', 'vigilante' ); ?></option> |
| 6319 | 6330 | <option value="warning" <?php selected( $alert_severity, 'warning' ); ?>><?php esc_html_e( 'Warning and Critical', 'vigilante' ); ?></option> |
| 6320 | 6331 | </select> |
| 6321 | - <p class="description"><?php esc_html_e( 'A new administrator, a closed plugin or a privilege escalation are all logged as Critical, so "Critical only" already covers them.', 'vigilante' ); ?></p> | |
| 6332 | + <p class="description"><?php esc_html_e( 'A closed plugin is logged as Critical, so "Critical only" covers it. A new administrator and a change of role are logged as Warning: they have their own emails, in Users > Admin monitoring, and reach this alert only with "Warning and Critical".', 'vigilante' ); ?></p> | |
| 6322 | 6333 | </td> |
| 6323 | 6334 | </tr> |
| 6324 | 6335 | <tr id="field-audit-alerts-threshold"> |
| 6325 | 6336 | <th scope="row"><?php esc_html_e( 'Threshold alerts', 'vigilante' ); ?></th> |
| @@ -6412,8 +6423,9 @@ | ||
| 6412 | 6423 | |
| 6413 | 6424 | // Label maps for translated display |
| 6414 | 6425 | $type_labels = array( |
| 6415 | 6426 | 'login' => __( 'Login', 'vigilante' ), |
| 6427 | + '2fa' => __( 'Two-factor', 'vigilante' ), | |
| 6416 | 6428 | 'user' => __( 'User', 'vigilante' ), |
| 6417 | 6429 | 'content' => __( 'Content', 'vigilante' ), |
| 6418 | 6430 | 'plugin' => __( 'Plugin', 'vigilante' ), |
| 6419 | 6431 | 'theme' => __( 'Theme', 'vigilante' ), |
| @@ -6442,8 +6454,9 @@ | ||
| 6442 | 6454 | <input type="text" id="vigilante-log-search" aria-label="<?php esc_attr_e( 'Search the activity log', 'vigilante' ); ?>" size="1" placeholder="<?php esc_attr_e( 'Search logs (min. 3 characters)...', 'vigilante' ); ?>" class="vigilante-log-search-input"> |
| 6443 | 6455 | <select id="vigilante-log-type-filter" aria-label="<?php esc_attr_e( 'Filter the log by event type', 'vigilante' ); ?>"> |
| 6444 | 6456 | <option value=""><?php esc_html_e( 'All Types', 'vigilante' ); ?></option> |
| 6445 | 6457 | <option value="login"><?php esc_html_e( 'Login', 'vigilante' ); ?></option> |
| 6458 | + <option value="2fa"><?php esc_html_e( 'Two-factor', 'vigilante' ); ?></option> | |
| 6446 | 6459 | <option value="user"><?php esc_html_e( 'User', 'vigilante' ); ?></option> |
| 6447 | 6460 | <option value="content"><?php esc_html_e( 'Content', 'vigilante' ); ?></option> |
| 6448 | 6461 | <option value="plugin"><?php esc_html_e( 'Plugin', 'vigilante' ); ?></option> |
| 6449 | 6462 | <option value="theme"><?php esc_html_e( 'Theme', 'vigilante' ); ?></option> |
| @@ -6987,8 +7000,49 @@ | ||
| 6987 | 7000 | </details> |
| 6988 | 7001 | <?php |
| 6989 | 7002 | } |
| 6990 | 7003 | |
| 7004 | + /** | |
| 7005 | + * Print what each kind of finding in a list usually is and what to do | |
| 7006 | + * | |
| 7007 | + * One line per kind actually present in the list, from the single source | |
| 7008 | + * of that text, Vigilante_File_Integrity::finding_guidance(). | |
| 7009 | + * | |
| 7010 | + * @param array $items Findings of one section of the last scan. | |
| 7011 | + */ | |
| 7012 | + private function render_fi_guidance( $items ) { | |
| 7013 | + $seen = array(); | |
| 7014 | + $guides = array(); | |
| 7015 | + | |
| 7016 | + foreach ( (array) $items as $item ) { | |
| 7017 | + $type = is_array( $item ) ? (string) ( $item['type'] ?? '' ) : ''; | |
| 7018 | + | |
| 7019 | + if ( '' === $type || 'vigilante_self' === $type || isset( $seen[ $type ] ) ) { | |
| 7020 | + continue; | |
| 7021 | + } | |
| 7022 | + $seen[ $type ] = true; | |
| 7023 | + | |
| 7024 | + $guide = Vigilante_File_Integrity::finding_guidance( $type ); | |
| 7025 | + | |
| 7026 | + if ( '' !== $guide['advice'] ) { | |
| 7027 | + // A plugin and a theme share their text: one line for both, | |
| 7028 | + // under the two names. | |
| 7029 | + $guides[ $guide['advice'] ][] = $guide['label']; | |
| 7030 | + } | |
| 7031 | + } | |
| 7032 | + | |
| 7033 | + if ( empty( $guides ) ) { | |
| 7034 | + return; | |
| 7035 | + } | |
| 7036 | + ?> | |
| 7037 | + <ul class="vigilante-fi-guidance" style="margin: 8px 0 12px 18px; list-style: disc;"> | |
| 7038 | + <?php foreach ( $guides as $advice => $labels ) : ?> | |
| 7039 | + <li><strong><?php echo esc_html( implode( ', ', $labels ) ); ?>:</strong> <?php echo esc_html( $advice ); ?></li> | |
| 7040 | + <?php endforeach; ?> | |
| 7041 | + </ul> | |
| 7042 | + <?php | |
| 7043 | + } | |
| 7044 | + | |
| 6991 | 7045 | private function render_tab_file_integrity() { |
| 6992 | 7046 | $is_disabled = $this->render_module_disabled_notice( 'file_integrity' ); |
| 6993 | 7047 | $options = $this->settings->get_section( 'file_integrity' ); |
| 6994 | 7048 | // On the main site of a network the critical-file scan is the network's |
| @@ -7060,9 +7114,10 @@ | ||
| 7060 | 7114 | <h2> |
| 7061 | 7115 | <?php esc_html_e( 'File Integrity Monitoring', 'vigilante' ); ?> |
| 7062 | 7116 | <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span> |
| 7063 | 7117 | </h2> |
| 7064 | - <p><?php esc_html_e( 'Detects file modifications using WordPress.org checksums.', 'vigilante' ); ?></p> | |
| 7118 | + <p><?php esc_html_e( 'Compares WordPress, and the plugins and themes that come from WordPress.org, with the official copy of each file, looks for code where there should be none (uploads and the site root) and watches wp-config.php and .htaccess for changes.', 'vigilante' ); ?></p> | |
| 7119 | + <p class="description"><?php esc_html_e( 'What it cannot compare: plugins and themes that are not on WordPress.org, must-use plugins, drop-ins (advanced-cache.php, object-cache.php and the like) and the other folders of wp-content have no official copy. They are searched for hidden code, which finds malware that disguises itself and misses most code written in the open. The results name what could not be compared.', 'vigilante' ); ?></p> | |
| 7065 | 7120 | |
| 7066 | 7121 | <table class="form-table"> |
| 7067 | 7122 | <tr> |
| 7068 | 7123 | <th scope="row"><?php esc_html_e( 'Automatic Scans', 'vigilante' ); ?></th> |
| @@ -7085,13 +7140,13 @@ | ||
| 7085 | 7140 | <tr> |
| 7086 | 7141 | <th scope="row"><label for="vigilante-f-file-integrity-notify-level"><?php esc_html_e( 'Email Notifications', 'vigilante' ); ?></label></th> |
| 7087 | 7142 | <td> |
| 7088 | 7143 | <select id="vigilante-f-file-integrity-notify-level" name="file_integrity[notify_level]"> |
| 7089 | - <option value="all" <?php selected( $notify_level, 'all' ); ?>><?php esc_html_e( 'All issues (modified + suspicious)', 'vigilante' ); ?></option> | |
| 7090 | - <option value="suspicious_only" <?php selected( $notify_level, 'suspicious_only' ); ?>><?php esc_html_e( 'Suspicious files only', 'vigilante' ); ?></option> | |
| 7144 | + <option value="all" <?php selected( $notify_level, 'all' ); ?>><?php esc_html_e( 'Every finding, modified files included', 'vigilante' ); ?></option> | |
| 7145 | + <option value="suspicious_only" <?php selected( $notify_level, 'suspicious_only' ); ?>><?php esc_html_e( 'Serious findings only', 'vigilante' ); ?></option> | |
| 7091 | 7146 | <option value="disabled" <?php selected( $notify_level, 'disabled' ); ?>><?php esc_html_e( 'Disabled', 'vigilante' ); ?></option> |
| 7092 | 7147 | </select> |
| 7093 | - <p class="description"><?php esc_html_e( '"Suspicious files only" reduces noise by skipping modified file notifications. Recommended for most sites.', 'vigilante' ); ?></p> | |
| 7148 | + <p class="description"><?php esc_html_e( 'Serious findings are suspicious and extra files, changes in wp-config.php or .htaccess and closed plugins. That is the recommended choice: a file that is only modified is rarely more than an update that did not finish. The email goes out when a scan finds something new: a finding you were already told about is not sent again, unless the file changes once more.', 'vigilante' ); ?></p> | |
| 7094 | 7149 | </td> |
| 7095 | 7150 | </tr> |
| 7096 | 7151 | <tr> |
| 7097 | 7152 | <th scope="row"><?php esc_html_e( 'Instant Alert', 'vigilante' ); ?></th> |
| @@ -7097,11 +7152,11 @@ | ||
| 7097 | 7152 | <th scope="row"><?php esc_html_e( 'Instant Alert', 'vigilante' ); ?></th> |
| 7098 | 7153 | <td> |
| 7099 | 7154 | <label> |
| 7100 | 7155 | <input type="checkbox" name="file_integrity[instant_alert]" value="1" <?php checked( ! empty( $options['instant_alert'] ) ); ?>> |
| 7101 | - <?php esc_html_e( 'Send immediate alert when modified, suspicious or additional files are detected, or when a closed plugin is found', 'vigilante' ); ?> | |
| 7156 | + <?php esc_html_e( 'Email every finding when a scan ends, modified files included, and email a closed plugin as soon as the daily check finds it', 'vigilante' ); ?> | |
| 7102 | 7157 | </label> |
| 7103 | - <p class="description"><?php esc_html_e( 'Fires even if the Email Notifications setting above is set to Disabled.', 'vigilante' ); ?></p> | |
| 7158 | + <p class="description"><?php esc_html_e( 'This takes precedence over the choice above, Disabled included.', 'vigilante' ); ?></p> | |
| 7104 | 7159 | <p class="description"> |
| 7105 | 7160 | <?php |
| 7106 | 7161 | printf( |
| 7107 | 7162 | /* translators: %s: Link to notification settings */ |
| @@ -7127,29 +7182,29 @@ | ||
| 7127 | 7182 | <td> |
| 7128 | 7183 | <fieldset> |
| 7129 | 7184 | <label> |
| 7130 | 7185 | <input type="checkbox" name="file_integrity[scan_core]" value="1" <?php checked( $options['scan_core'] ?? true ); ?>> |
| 7131 | - <?php esc_html_e( 'Core files (compare against WordPress.org checksums)', 'vigilante' ); ?> | |
| 7186 | + <?php esc_html_e( 'WordPress core (compared with WordPress.org) and the other files in the site root', 'vigilante' ); ?> | |
| 7132 | 7187 | </label> |
| 7133 | 7188 | <br> |
| 7134 | 7189 | <label> |
| 7135 | 7190 | <input type="checkbox" name="file_integrity[scan_plugins]" value="1" <?php checked( $options['scan_plugins'] ?? true ); ?>> |
| 7136 | - <?php esc_html_e( 'Plugins (WordPress.org repository plugins)', 'vigilante' ); ?> | |
| 7191 | + <?php esc_html_e( 'Plugins (compared with WordPress.org; the rest, and the other folders of wp-content, are only searched for hidden code)', 'vigilante' ); ?> | |
| 7137 | 7192 | </label> |
| 7138 | 7193 | <br> |
| 7139 | 7194 | <label> |
| 7140 | 7195 | <input type="checkbox" name="file_integrity[scan_themes]" value="1" <?php checked( $options['scan_themes'] ?? true ); ?>> |
| 7141 | - <?php esc_html_e( 'Themes (WordPress.org repository themes)', 'vigilante' ); ?> | |
| 7196 | + <?php esc_html_e( 'Themes (compared with WordPress.org; the rest are only searched for hidden code)', 'vigilante' ); ?> | |
| 7142 | 7197 | </label> |
| 7143 | 7198 | <br> |
| 7144 | 7199 | <label> |
| 7145 | 7200 | <input type="checkbox" name="file_integrity[scan_uploads]" value="1" <?php checked( $options['scan_uploads'] ?? true ); ?>> |
| 7146 | - <?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?> | |
| 7201 | + <?php esc_html_e( 'Uploads directory (PHP and other scripts, double extensions, .htaccess and PHP configuration files)', 'vigilante' ); ?> | |
| 7147 | 7202 | </label> |
| 7148 | 7203 | <br> |
| 7149 | 7204 | <label> |
| 7150 | 7205 | <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( $options['scan_critical_config'] ?? true ); ?>> |
| 7151 | - <?php esc_html_e( 'Critical config files (wp-config.php, .htaccess baseline monitoring)', 'vigilante' ); ?> | |
| 7206 | + <?php esc_html_e( 'wp-config.php and .htaccess (any change since you last approved them)', 'vigilante' ); ?> | |
| 7152 | 7207 | <?php if ( $vg_main_locked ) : ?> |
| 7153 | 7208 | <span class="description" style="display:block;margin-left:24px;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></span> |
| 7154 | 7209 | <?php endif; ?> |
| 7155 | 7210 | </label> |
| @@ -7163,10 +7218,10 @@ | ||
| 7163 | 7218 | </tr> |
| 7164 | 7219 | <tr> |
| 7165 | 7220 | <th scope="row"><label for="vigilante-f-file-integrity-excluded-paths"><?php esc_html_e( 'Excluded Paths', 'vigilante' ); ?></label></th> |
| 7166 | 7221 | <td> |
| 7167 | - <textarea id="vigilante-f-file-integrity-excluded-paths" name="file_integrity[excluded_paths]" rows="4" class="large-text code" placeholder="wp-content/cache wp-content/languages"><?php echo esc_textarea( implode( "\n", $options['excluded_paths'] ?? array() ) ); ?></textarea> | |
| 7168 | - <p class="description"><?php esc_html_e( 'One path per line, relative to the WordPress root. A path such as wp-content/cache excludes exactly that folder and everything under it. A name on its own, such as cache, excludes any folder called exactly that, wherever it is.', 'vigilante' ); ?></p> | |
| 7222 | + <textarea id="vigilante-f-file-integrity-excluded-paths" name="file_integrity[excluded_paths]" rows="4" class="large-text code" placeholder="wp-content/backups"><?php echo esc_textarea( implode( "\n", $options['excluded_paths'] ?? array() ) ); ?></textarea> | |
| 7223 | + <p class="description"><?php esc_html_e( 'One path per line, relative to the WordPress root. A path such as wp-content/backups excludes exactly that folder and everything under it. A name on its own, such as backups, excludes any folder called exactly that, wherever it is. Nothing in an excluded folder is looked at, so keep the list short.', 'vigilante' ); ?></p> | |
| 7169 | 7224 | </td> |
| 7170 | 7225 | </tr> |
| 7171 | 7226 | <tr> |
| 7172 | 7227 | <th scope="row"><label for="vigilante-f-file-integrity-excluded-extensions"><?php esc_html_e( 'Excluded Extensions', 'vigilante' ); ?></label></th> |
| @@ -7209,11 +7264,49 @@ | ||
| 7209 | 7264 | <div id="vigilante-scan-results" class="vigilante-settings-section" style="display:none;"></div> |
| 7210 | 7265 | |
| 7211 | 7266 | <?php $this->render_self_protection_box( $options ); ?> |
| 7212 | 7267 | |
| 7268 | + <?php | |
| 7269 | + // Whole folders of code left out of every scan. A partial scan tells | |
| 7270 | + // how to check the rest by excluding what it did check, and those | |
| 7271 | + // lines have to come out afterwards: with them in place a scan comes | |
| 7272 | + // out complete and clean without having looked there. Said for as | |
| 7273 | + // long as they are in the list, whoever put them there and why. | |
| 7274 | + $vg_fi_tool = new Vigilante_File_Integrity( $this->settings, $this->database ); | |
| 7275 | + $vg_excluded_code = $vg_fi_tool->excluded_code_folders(); | |
| 7276 | + $vg_folders_to_skip = $vg_fi_tool->folders_to_exclude( $last_results ); | |
| 7277 | + ?> | |
| 7278 | + <?php if ( ! empty( $vg_excluded_code ) ) : ?> | |
| 7279 | + <div id="vigilante-fi-excluded-code" class="notice notice-warning inline"> | |
| 7280 | + <p> | |
| 7281 | + <strong> | |
| 7282 | + <?php | |
| 7283 | + printf( | |
| 7284 | + /* translators: %s: Comma separated list of folders */ | |
| 7285 | + esc_html__( 'Excluded Paths holds whole folders of plugins or themes, or the uploads folder: %s.', 'vigilante' ), | |
| 7286 | + esc_html( implode( ', ', $vg_excluded_code ) ) | |
| 7287 | + ); | |
| 7288 | + ?> | |
| 7289 | + </strong> | |
| 7290 | + <?php esc_html_e( 'No scan looks inside them, the scheduled one included, and a scan can come out complete and clean without having checked them. If you added them to check the rest of a large site, take them out once that scan is done.', 'vigilante' ); ?> | |
| 7291 | + </p> | |
| 7292 | + </div> | |
| 7293 | + <?php endif; ?> | |
| 7294 | + | |
| 7213 | 7295 | <?php if ( $last_scan || $has_closed || $closed_last_check > 0 ) : ?> |
| 7214 | 7296 | <div id="vigilante-section-fi-last-scan" class="vigilante-settings-section"> |
| 7215 | 7297 | <h2><?php esc_html_e( 'Last Scan Results', 'vigilante' ); ?></h2> |
| 7298 | + <?php | |
| 7299 | + // A scan the server stops halfway stores nothing, and neither | |
| 7300 | + // does one WordPress never starts: the tab kept showing the | |
| 7301 | + // last result that did finish, with nothing to say that scans | |
| 7302 | + // had stopped. Its date is the only sign. | |
| 7303 | + if ( $last_scan && $this->settings->is_module_enabled( 'file_integrity' ) && Vigilante_File_Integrity::is_scan_overdue( $last_scan, $options['scan_frequency'] ?? 'daily' ) ) : | |
| 7304 | + ?> | |
| 7305 | + <div class="notice notice-warning inline"> | |
| 7306 | + <p><strong><?php esc_html_e( 'Scans are scheduled, and none has finished since the date below.', 'vigilante' ); ?></strong> <?php esc_html_e( 'Either WordPress is not running its scheduled tasks, or the server stops the scan before it ends, and a scan that is stopped stores no result. What you see here is old. Run a scan now: if it finishes, the scheduled tasks are what is failing. If it does not, the site is too large for the time the server gives a request, and adding large folders that do not need checking to Excluded Paths leaves more time for the rest.', 'vigilante' ); ?></p> | |
| 7307 | + </div> | |
| 7308 | + <?php endif; ?> | |
| 7216 | 7309 | <?php if ( $last_scan ) : ?> |
| 7217 | 7310 | <p> |
| 7218 | 7311 | <?php |
| 7219 | 7312 | // Build the "X files scanned" hint inline with the date so it doesn't |
| @@ -7296,12 +7389,112 @@ | ||
| 7296 | 7389 | </div> |
| 7297 | 7390 | <?php endif; ?> |
| 7298 | 7391 | </div> |
| 7299 | 7392 | |
| 7393 | + <?php | |
| 7394 | + // A scan that could not finish, or could not get the official | |
| 7395 | + // copy to compare with, checked less than it seems. Until now | |
| 7396 | + // its errors were stored and never shown, and the tab could | |
| 7397 | + // say that every file had passed. | |
| 7398 | + $vg_scan_errors = ( $last_results && ! empty( $last_results['errors'] ) && is_array( $last_results['errors'] ) ) ? $last_results['errors'] : array(); | |
| 7399 | + $vg_scan_partial = ! empty( $vg_scan_errors ) || ( $last_results && ! empty( $last_results['incomplete'] ) ); | |
| 7400 | + | |
| 7401 | + // Files of the official copies that are not on the server. Core | |
| 7402 | + // files under wp-content are left out: results stored before | |
| 7403 | + // the bundled themes and plugins got their own turn list | |
| 7404 | + // there the ones people delete on purpose. | |
| 7405 | + $vg_missing_core = array(); | |
| 7406 | + if ( $last_results && ! empty( $last_results['missing'] ) && is_array( $last_results['missing'] ) ) { | |
| 7407 | + foreach ( $last_results['missing'] as $item ) { | |
| 7408 | + if ( ! is_array( $item ) || ! isset( $item['file'] ) ) { | |
| 7409 | + continue; | |
| 7410 | + } | |
| 7411 | + | |
| 7412 | + $vg_missing_type = (string) ( $item['type'] ?? '' ); | |
| 7413 | + | |
| 7414 | + if ( in_array( $vg_missing_type, array( 'plugin', 'theme' ), true ) | |
| 7415 | + || ( 'core' === $vg_missing_type && 0 !== strpos( (string) $item['file'], 'wp-content/' ) ) ) { | |
| 7416 | + $vg_missing_core[] = (string) $item['file']; | |
| 7417 | + } | |
| 7418 | + } | |
| 7419 | + } | |
| 7420 | + ?> | |
| 7421 | + | |
| 7422 | + <?php if ( $last_results && ! empty( $last_results['unverified'] ) && is_array( $last_results['unverified'] ) ) : ?> | |
| 7423 | + <p class="description"> | |
| 7424 | + <?php | |
| 7425 | + printf( | |
| 7426 | + /* translators: %s: Comma separated list of plugin and theme names */ | |
| 7427 | + esc_html__( 'No official copy to compare with, so only searched for hidden code: %s.', 'vigilante' ), | |
| 7428 | + esc_html( implode( ', ', array_unique( array_map( 'strval', $last_results['unverified'] ) ) ) ) | |
| 7429 | + ); | |
| 7430 | + ?> | |
| 7431 | + </p> | |
| 7432 | + <?php endif; ?> | |
| 7433 | + | |
| 7434 | + <?php | |
| 7435 | + // What the scan did not get to, by name. A scan starts from | |
| 7436 | + // the beginning every time, so on a site too large for it | |
| 7437 | + // these are the same on every scan, and whoever reads the | |
| 7438 | + // result has to know that nothing here was looked at. | |
| 7439 | + $vg_scan_notes = array_merge( $vg_scan_errors, Vigilante_File_Integrity::not_checked_lines( $last_results ) ); | |
| 7440 | + ?> | |
| 7441 | + <?php if ( $vg_scan_partial ) : ?> | |
| 7442 | + <div class="notice notice-warning inline"> | |
| 7443 | + <p><strong><?php esc_html_e( 'The last scan could not check everything, so its result is partial.', 'vigilante' ); ?></strong></p> | |
| 7444 | + <?php if ( ! empty( $vg_scan_notes ) ) : ?> | |
| 7445 | + <ul style="margin: 0 0 8px 18px; list-style: disc;"> | |
| 7446 | + <?php foreach ( $vg_scan_notes as $vg_scan_note ) : ?> | |
| 7447 | + <li><?php echo esc_html( (string) $vg_scan_note ); ?></li> | |
| 7448 | + <?php endforeach; ?> | |
| 7449 | + </ul> | |
| 7450 | + <?php endif; ?> | |
| 7451 | + <p> | |
| 7452 | + <?php | |
| 7453 | + printf( | |
| 7454 | + /* translators: %d: Seconds a scan runs for at most */ | |
| 7455 | + esc_html__( 'Run the scan again: a second scan is faster, because what it downloads from WordPress.org is kept for a while. If the result is still partial, either the server cannot reach WordPress.org or the site is larger than a scan can check in its %d seconds, and then every scan leaves the same part unchecked. Adding large folders that do not need checking to Excluded Paths leaves more time for the rest.', 'vigilante' ), | |
| 7456 | + (int) Vigilante_File_Integrity::MAX_SCAN_SECONDS | |
| 7457 | + ); | |
| 7458 | + ?> | |
| 7459 | + </p> | |
| 7460 | + <?php if ( ! empty( $vg_folders_to_skip ) ) : ?> | |
| 7461 | + <p><strong><?php esc_html_e( 'To check what was left out:', 'vigilante' ); ?></strong></p> | |
| 7462 | + <ol style="margin: 0 0 8px 18px;"> | |
| 7463 | + <li><?php esc_html_e( 'Deal first with what this scan found in the plugins and themes it did check: the next result will not list it. Then add the lines below, which are their folders, to Excluded Paths, higher up on this page, and save.', 'vigilante' ); ?></li> | |
| 7464 | + <li><?php esc_html_e( 'Press Run Scan Now. The scan spends its time on the rest. If it comes out partial again, repeat with the lines it gives then.', 'vigilante' ); ?></li> | |
| 7465 | + <li><?php esc_html_e( 'Take those lines out of Excluded Paths. While they are there no scan looks inside those folders, the scheduled one included.', 'vigilante' ); ?></li> | |
| 7466 | + </ol> | |
| 7467 | + <textarea id="vigilante-fi-folders-to-exclude" readonly rows="<?php echo (int) min( 12, max( 3, count( $vg_folders_to_skip ) ) ); ?>" class="large-text code" style="margin-bottom: 8px;"><?php echo esc_textarea( implode( "\n", $vg_folders_to_skip ) ); ?></textarea> | |
| 7468 | + <?php endif; ?> | |
| 7469 | + </div> | |
| 7470 | + <?php endif; ?> | |
| 7471 | + | |
| 7472 | + <?php if ( ! empty( $vg_missing_core ) ) : ?> | |
| 7473 | + <div class="vigilante-file-list"> | |
| 7474 | + <h3><?php esc_html_e( 'Missing Files', 'vigilante' ); ?></h3> | |
| 7475 | + <p class="description"><?php esc_html_e( 'These files are in the official copy and not on the server. Reinstall what they belong to (WordPress from Dashboard > Updates, a plugin or a theme from its own screen) and scan again.', 'vigilante' ); ?></p> | |
| 7476 | + <ul style="margin: 8px 0 12px 18px; list-style: disc;"> | |
| 7477 | + <?php foreach ( array_slice( $vg_missing_core, 0, 20 ) as $vg_missing_file ) : ?> | |
| 7478 | + <li><code><?php echo esc_html( $vg_missing_file ); ?></code></li> | |
| 7479 | + <?php endforeach; ?> | |
| 7480 | + </ul> | |
| 7481 | + <?php if ( count( $vg_missing_core ) > 20 ) : ?> | |
| 7482 | + <p class="description"> | |
| 7483 | + <?php | |
| 7484 | + /* translators: %d: Number of additional files */ | |
| 7485 | + printf( esc_html__( '... and %d more', 'vigilante' ), (int) ( count( $vg_missing_core ) - 20 ) ); | |
| 7486 | + ?> | |
| 7487 | + </p> | |
| 7488 | + <?php endif; ?> | |
| 7489 | + </div> | |
| 7490 | + <?php endif; ?> | |
| 7491 | + | |
| 7300 | 7492 | <?php if ( ! empty( $last_results['suspicious'] ) ) : ?> |
| 7301 | 7493 | <div class="vigilante-file-list vigilante-suspicious-files vigilante-paginated-section" data-bulk-mode="ignore"> |
| 7302 | 7494 | <h3 style="color: #d63638;"><?php esc_html_e( 'Suspicious Files', 'vigilante' ); ?></h3> |
| 7303 | - <p class="description" style="color: #d63638;"><?php esc_html_e( '⚠ Warning: These files may contain malicious code or are in unexpected locations. Review immediately!', 'vigilante' ); ?></p> | |
| 7495 | + <p class="description" style="color: #d63638;"><?php esc_html_e( '⚠ Files that can run code where none is expected, or that contain code written the way malware hides itself. Real infections show up here, and so do harmless files that plugins generate. Check the path and the reason before acting.', 'vigilante' ); ?></p> | |
| 7496 | + <?php $this->render_fi_guidance( $last_results['suspicious'] ); ?> | |
| 7304 | 7497 | <div class="vigilante-fi-bulk-bar"> |
| 7305 | 7498 | <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button> |
| 7306 | 7499 | <span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| 7307 | 7500 | </div> |
| @@ -7337,8 +7530,11 @@ | ||
| 7337 | 7530 | $file_path = (string) $item; |
| 7338 | 7531 | } |
| 7339 | 7532 | if ( 'vigilante_self' === $file_type ) { |
| 7340 | 7533 | $file_type = __( 'Vigilant (self)', 'vigilante' ); |
| 7534 | + } else { | |
| 7535 | + // A name a person can read, instead of the internal slug. | |
| 7536 | + $file_type = Vigilante_File_Integrity::finding_guidance( $file_type )['label']; | |
| 7341 | 7537 | } |
| 7342 | 7538 | ?> |
| 7343 | 7539 | <tr> |
| 7344 | 7540 | <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th> |
| @@ -7357,9 +7553,10 @@ | ||
| 7357 | 7553 | |
| 7358 | 7554 | <?php if ( ! empty( $last_results['extra'] ) ) : ?> |
| 7359 | 7555 | <div class="vigilante-file-list vigilante-extra-files vigilante-paginated-section" data-bulk-mode="ignore"> |
| 7360 | 7556 | <h3 style="color: #b32d2e;"><?php esc_html_e( 'Extra Files', 'vigilante' ); ?></h3> |
| 7361 | - <p class="description"><?php esc_html_e( 'PHP files found in plugins or themes that are not part of the original distribution from WordPress.org. May be legitimate customizations or injected backdoors.', 'vigilante' ); ?></p> | |
| 7557 | + <p class="description"><?php esc_html_e( 'Files that are not part of WordPress, or of the plugin or theme they sit in. Many are legitimate, and an injected file looks the same from here, so confirm where each one comes from.', 'vigilante' ); ?></p> | |
| 7558 | + <?php $this->render_fi_guidance( $last_results['extra'] ); ?> | |
| 7362 | 7559 | <div class="vigilante-fi-bulk-bar"> |
| 7363 | 7560 | <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button> |
| 7364 | 7561 | <span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| 7365 | 7562 | </div> |
| @@ -7381,8 +7578,11 @@ | ||
| 7381 | 7578 | $file_reason = is_array( $item ) ? ( $item['reason'] ?? __( 'Unknown', 'vigilante' ) ) : __( 'Unknown', 'vigilante' ); |
| 7382 | 7579 | $file_type = is_array( $item ) ? ( $item['type'] ?? 'unknown' ) : 'unknown'; |
| 7383 | 7580 | if ( 'vigilante_self' === $file_type ) { |
| 7384 | 7581 | $file_type = __( 'Vigilant (self)', 'vigilante' ); |
| 7582 | + } else { | |
| 7583 | + // A name a person can read, instead of the internal slug. | |
| 7584 | + $file_type = Vigilante_File_Integrity::finding_guidance( $file_type )['label']; | |
| 7385 | 7585 | } |
| 7386 | 7586 | ?> |
| 7387 | 7587 | <tr> |
| 7388 | 7588 | <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th> |
| @@ -7431,9 +7631,9 @@ | ||
| 7431 | 7631 | <?php if ( ! empty( $critical_modified ) ) : ?> |
| 7432 | 7632 | <div class="vigilante-file-list vigilante-critical-config-files"> |
| 7433 | 7633 | <h3 style="color: #e36210;"><?php esc_html_e( 'Critical config files modified', 'vigilante' ); ?></h3> |
| 7434 | 7634 | <p class="description"> |
| 7435 | - <?php esc_html_e( 'These files are common targets for code injection. Review the changes and approve if they are legitimate. Vigilant\'s own blocks are excluded from this check.', 'vigilante' ); ?> | |
| 7635 | + <?php esc_html_e( 'These files changed since you last approved them. They are a common target for code injection, and they are also rewritten by cache, security and redirection plugins, often on every update. Review the changes and approve them if they belong to something you use: approving records the current content as the reference and does not touch the file. Vigilant\'s own blocks are excluded from this check, and Vigilant never restores these files by itself.', 'vigilante' ); ?> | |
| 7436 | 7636 | </p> |
| 7437 | 7637 | <table class="wp-list-table widefat fixed striped"> |
| 7438 | 7638 | <thead> |
| 7439 | 7639 | <tr> |
| @@ -7594,9 +7794,10 @@ | ||
| 7594 | 7794 | |
| 7595 | 7795 | <?php if ( ! empty( $regular_modified ) ) : ?> |
| 7596 | 7796 | <div class="vigilante-file-list vigilante-paginated-section" data-bulk-mode="ignore"> |
| 7597 | 7797 | <h3><?php esc_html_e( 'Modified Files', 'vigilante' ); ?></h3> |
| 7598 | - <p class="description"><?php esc_html_e( 'These files (apparently) differ from the original WordPress or plugin versions.', 'vigilante' ); ?></p> | |
| 7798 | + <p class="description"><?php esc_html_e( 'These files do not match the copy WordPress.org publishes for the installed version.', 'vigilante' ); ?></p> | |
| 7799 | + <?php $this->render_fi_guidance( $regular_modified ); ?> | |
| 7599 | 7800 | <div class="vigilante-fi-bulk-bar"> |
| 7600 | 7801 | <button type="button" class="button vigilante-bulk-ignore" disabled><?php esc_html_e( 'Ignore selected', 'vigilante' ); ?></button> |
| 7601 | 7802 | <span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| 7602 | 7803 | </div> |
| @@ -7627,8 +7828,11 @@ | ||
| 7627 | 7828 | $file_path = (string) $item; |
| 7628 | 7829 | } |
| 7629 | 7830 | if ( 'vigilante_self' === $file_type ) { |
| 7630 | 7831 | $file_type = __( 'Vigilant (self)', 'vigilante' ); |
| 7832 | + } else { | |
| 7833 | + // A name a person can read, instead of the internal slug. | |
| 7834 | + $file_type = Vigilante_File_Integrity::finding_guidance( $file_type )['label']; | |
| 7631 | 7835 | } |
| 7632 | 7836 | ?> |
| 7633 | 7837 | <tr> |
| 7634 | 7838 | <th scope="row" class="check-column"><input type="checkbox" class="vigilante-fi-cb" value="<?php echo esc_attr( $file_path ); ?>"></th> |
| @@ -7650,11 +7854,11 @@ | ||
| 7650 | 7854 | // above, this line contradicted it (its findings no longer |
| 7651 | 7855 | // travel in the tables below). |
| 7652 | 7856 | $vg_self_alarm = in_array( $this->self_integrity_summary( $options )['tone'], array( 'critical', 'off', 'warning' ), true ); |
| 7653 | 7857 | ?> |
| 7654 | - <?php if ( $last_results && ! $vg_self_alarm && empty( $critical_modified ) && empty( $regular_modified ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed ) : ?> | |
| 7858 | + <?php if ( $last_results && ! $vg_self_alarm && empty( $critical_modified ) && empty( $regular_modified ) && empty( $last_results['suspicious'] ) && empty( $last_results['extra'] ) && ! $has_closed && ! $vg_scan_partial && empty( $vg_missing_core ) ) : ?> | |
| 7655 | 7859 | <p class="vigilante-all-clear" style="color: #00a32a; font-weight: bold;"> |
| 7656 | - <?php esc_html_e( 'Good Job! All files passed integrity check. No issues found.', 'vigilante' ); ?> | |
| 7860 | + <?php esc_html_e( 'Good job! Nothing to report in the files that were checked.', 'vigilante' ); ?> | |
| 7657 | 7861 | </p> |
| 7658 | 7862 | <?php endif; ?> |
| 7659 | 7863 | <?php endif; ?> |
| 7660 | 7864 | </div> |
| @@ -7703,9 +7907,9 @@ | ||
| 7703 | 7907 | |
| 7704 | 7908 | <?php if ( ! empty( $ignored_files ) ) : ?> |
| 7705 | 7909 | <div id="vigilante-section-fi-ignored" class="vigilante-settings-section"> |
| 7706 | 7910 | <h2><?php esc_html_e( 'Ignored Files', 'vigilante' ); ?></h2> |
| 7707 | - <p class="description"><?php esc_html_e( 'These files are excluded from scan results and email notifications. They will still be scanned but any findings will be hidden.', 'vigilante' ); ?></p> | |
| 7911 | + <p class="description"><?php esc_html_e( 'These files are excluded from scan results and email notifications. They will still be scanned but any findings will be hidden. To leave a whole folder out, add it to Excluded Paths instead.', 'vigilante' ); ?></p> | |
| 7708 | 7912 | <div class="vigilante-paginated-section" data-bulk-mode="unignore"> |
| 7709 | 7913 | <div class="vigilante-fi-bulk-bar"> |
| 7710 | 7914 | <button type="button" class="button vigilante-bulk-unignore" disabled><?php esc_html_e( 'Stop ignoring selected', 'vigilante' ); ?></button> |
| 7711 | 7915 | <span class="vigilante-fi-bulk-count" aria-live="polite"></span> |
| @@ -7794,9 +7998,9 @@ | ||
| 7794 | 7998 | } elseif ( isset( $_POST['data'] ) ) { |
| 7795 | 7999 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 7796 | 8000 | $raw_data = wp_unslash( $_POST['data'] ); |
| 7797 | 8001 | parse_str( $raw_data, $data ); |
| 7798 | - $data = map_deep( $data, 'sanitize_textarea_field' ); | |
| 8002 | + $data = $this->sanitize_posted_settings( $data ); | |
| 7799 | 8003 | } else { |
| 7800 | 8004 | $data = array(); |
| 7801 | 8005 | } |
| 7802 | 8006 | |
| @@ -8186,8 +8390,48 @@ | ||
| 8186 | 8390 | return array( 'sent' => $sent, 'failed' => $failed ); |
| 8187 | 8391 | } |
| 8188 | 8392 | |
| 8189 | 8393 | /** |
| 8394 | + * Sanitize the settings a tab posted, once the serialized form is parsed | |
| 8395 | + * | |
| 8396 | + * Every value goes through sanitize_textarea_field() except the lists of | |
| 8397 | + * file paths (Vigilante_Settings::path_list_keys()). That one deletes every | |
| 8398 | + * %XX octet and, when it finds one, collapses repeated spaces, so an | |
| 8399 | + * exclusion for a folder called foto%20x was saved as fotox and excluded | |
| 8400 | + * nothing (measured on 1 oct 2026 against 3.0.1). The paths are read as | |
| 8401 | + * paths before the generic sanitizer runs and put back after it. | |
| 8402 | + * | |
| 8403 | + * @since 3.0.2 | |
| 8404 | + * | |
| 8405 | + * @param array $data Parsed form: section => key => value. | |
| 8406 | + * @return array | |
| 8407 | + */ | |
| 8408 | + private function sanitize_posted_settings( $data ) { | |
| 8409 | + $paths = Vigilante_Settings::take_path_lists( $data ); | |
| 8410 | + $data = map_deep( $data, 'sanitize_textarea_field' ); | |
| 8411 | + | |
| 8412 | + return Vigilante_Settings::restore_path_lists( $data, $paths ); | |
| 8413 | + } | |
| 8414 | + | |
| 8415 | + /** | |
| 8416 | + * Sanitize an imported settings file, once it is decoded | |
| 8417 | + * | |
| 8418 | + * Same rule as sanitize_posted_settings(): the lists of paths keep what the | |
| 8419 | + * export wrote, so an exclusion survives an export and an import. | |
| 8420 | + * | |
| 8421 | + * @since 3.0.2 | |
| 8422 | + * | |
| 8423 | + * @param array $imported Decoded export. | |
| 8424 | + * @return array | |
| 8425 | + */ | |
| 8426 | + private function sanitize_imported_settings( $imported ) { | |
| 8427 | + $paths = Vigilante_Settings::take_path_lists( $imported ); | |
| 8428 | + $imported = map_deep( $imported, 'sanitize_text_field' ); | |
| 8429 | + | |
| 8430 | + return Vigilante_Settings::restore_path_lists( $imported, $paths ); | |
| 8431 | + } | |
| 8432 | + | |
| 8433 | + /** | |
| 8190 | 8434 | * Process section data maintaining proper types from defaults |
| 8191 | 8435 | * |
| 8192 | 8436 | * @param array $submitted_data Data submitted from form. |
| 8193 | 8437 | * @param array $defaults Default values for this section. |
| @@ -8514,9 +8758,9 @@ | ||
| 8514 | 8758 | wp_send_json_error( __( 'Invalid JSON format.', 'vigilante' ) ); |
| 8515 | 8759 | } |
| 8516 | 8760 | |
| 8517 | 8761 | // Sanitize imported data recursively |
| 8518 | - $imported = map_deep( $imported, 'sanitize_text_field' ); | |
| 8762 | + $imported = $this->sanitize_imported_settings( $imported ); | |
| 8519 | 8763 | |
| 8520 | 8764 | // Validate structure: only sections and keys of the schema survive, and |
| 8521 | 8765 | // every value takes the type of its default. Until 2.11.0 this was an |
| 8522 | 8766 | // array_replace_recursive() of the file over the defaults, so any key in |
| @@ -8899,8 +9143,12 @@ | ||
| 8899 | 9143 | |
| 8900 | 9144 | delete_option( 'vigilante_last_integrity_results' ); |
| 8901 | 9145 | delete_option( 'vigilante_last_integrity_scan' ); |
| 8902 | 9146 | |
| 9147 | + // Starting over also means being told again: the next scan emails | |
| 9148 | + // whatever it finds, as if nothing had been reported before. | |
| 9149 | + delete_option( Vigilante_File_Integrity::NOTIFIED_OPTION ); | |
| 9150 | + | |
| 8903 | 9151 | /* |
| 8904 | 9152 | * A pending change to wp-config.php or the root .htaccess is closed by |
| 8905 | 9153 | * approving it, which takes the network. Clearing the results was one |
| 8906 | 9154 | * more way to close it without, until the next scan: the ignore list was |
| @@ -8952,9 +9200,29 @@ | ||
| 8952 | 9200 | if ( ! current_user_can( 'manage_options' ) ) { |
| 8953 | 9201 | wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 8954 | 9202 | } |
| 8955 | 9203 | |
| 8956 | - $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : ''; | |
| 9204 | + /* | |
| 9205 | + * The path as the scan reported it, which is what the interface sends | |
| 9206 | + * back. sanitize_text_field() is the wrong reader for it: it deletes | |
| 9207 | + * every %XX octet and collapses repeated spaces, so a file named | |
| 9208 | + * foto%20nueva.php was stored as fotonueva.php, the scan compared the | |
| 9209 | + * list with the paths it finds one by one, never matched, and the | |
| 9210 | + * warning came back (measured on 1 oct 2026 against 3.0.1). The four | |
| 9211 | + * handlers of the ignore list read the path the same way. | |
| 9212 | + * | |
| 9213 | + * Known limits, found by the cross review, that only a file name with a | |
| 9214 | + * stray < in it reaches. wp_strip_all_tags() shortens such a name at the | |
| 9215 | + * < instead of refusing it, so the stored entry can match another file | |
| 9216 | + * (a.php<b is stored as a.php), and the value ignore_file() returns is | |
| 9217 | + * not read, so a refusal of its own is answered as success. The fix, if | |
| 9218 | + * a site ever needs it, is to answer with an error when | |
| 9219 | + * wp_strip_all_tags() changes what was sent and to check that return | |
| 9220 | + * value. An entry the old reader stored for such a name (it wrote the | |
| 9221 | + * < as <) is listed but "Stop ignoring" cannot remove it; "Clear all | |
| 9222 | + * ignored files" does. | |
| 9223 | + */ | |
| 9224 | + $file = ( isset( $_POST['file'] ) && is_string( $_POST['file'] ) ) ? wp_strip_all_tags( wp_unslash( $_POST['file'] ) ) : ''; | |
| 8957 | 9225 | |
| 8958 | 9226 | if ( empty( $file ) ) { |
| 8959 | 9227 | wp_send_json_error( __( 'No file specified.', 'vigilante' ) ); |
| 8960 | 9228 | } |
| @@ -9007,9 +9275,9 @@ | ||
| 9007 | 9275 | if ( ! current_user_can( 'manage_options' ) ) { |
| 9008 | 9276 | wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 9009 | 9277 | } |
| 9010 | 9278 | |
| 9011 | - $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : ''; | |
| 9279 | + $file = ( isset( $_POST['file'] ) && is_string( $_POST['file'] ) ) ? wp_strip_all_tags( wp_unslash( $_POST['file'] ) ) : ''; | |
| 9012 | 9280 | |
| 9013 | 9281 | if ( empty( $file ) ) { |
| 9014 | 9282 | wp_send_json_error( __( 'No file specified.', 'vigilante' ) ); |
| 9015 | 9283 | } |
| @@ -9042,9 +9310,9 @@ | ||
| 9042 | 9310 | $files = array(); |
| 9043 | 9311 | $locked = $this->critical_approval_locked(); |
| 9044 | 9312 | $shared = $locked ? array( 'wp-config.php', '.htaccess' ) : array(); |
| 9045 | 9313 | foreach ( $raw_files as $f ) { |
| 9046 | - $clean = sanitize_text_field( $f ); | |
| 9314 | + $clean = is_string( $f ) ? wp_strip_all_tags( $f ) : ''; | |
| 9047 | 9315 | // Same rule as ajax_ignore_file(): the two shared files when the |
| 9048 | 9316 | // network locks them, and Vigilant's own files always. |
| 9049 | 9317 | if ( '' !== $clean && ! in_array( $clean, $shared, true ) && ! Vigilante_Self_Integrity::is_own_file_path( $clean ) ) { |
| 9050 | 9318 | $files[] = $clean; |
| @@ -9111,9 +9379,9 @@ | ||
| 9111 | 9379 | } |
| 9112 | 9380 | |
| 9113 | 9381 | $files = array(); |
| 9114 | 9382 | foreach ( $raw_files as $f ) { |
| 9115 | - $clean = sanitize_text_field( $f ); | |
| 9383 | + $clean = is_string( $f ) ? wp_strip_all_tags( $f ) : ''; | |
| 9116 | 9384 | if ( '' !== $clean ) { |
| 9117 | 9385 | $files[] = $clean; |
| 9118 | 9386 | } |
| 9119 | 9387 | } |