PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.3
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.3
3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 All 93 releases
← All changes | admin/class-admin.php +318 -50 3.0.1 → 3.0.3 View file →
@@ -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&#10;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( '&#9888; 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( '&#9888; 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 &lt;) 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 }