Changeset 3483739
- Timestamp:
- 03/16/2026 11:00:53 AM (3 weeks ago)
- Location:
- vulntitan/trunk
- Files:
-
- 9 edited
-
CHANGELOG.md (modified) (1 diff)
-
assets/js/firewall.js (modified) (11 diffs)
-
assets/js/firewall.min.js (modified) (11 diffs)
-
includes/Admin/Admin.php (modified) (1 diff)
-
includes/Admin/Ajax.php (modified) (3 diffs)
-
includes/Admin/Pages/Firewall.php (modified) (1 diff)
-
includes/Services/FirewallService.php (modified) (5 diffs)
-
readme.txt (modified) (2 diffs)
-
vulntitan.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
vulntitan/trunk/CHANGELOG.md
r3483644 r3483739 5 5 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 6 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 8 ## [2.1.10] - 2026-03-16 9 ### Added 10 - Added Learning Mode suggestions for WAF whitelisting with configurable thresholds and review-only approvals. 11 - Added a Learning Suggestions panel with approve/dismiss actions. 12 13 ### Fixed 14 - Fixed PHP 8.4 deprecation warning by making trusted proxy settings explicitly nullable. 7 15 8 16 ## [2.1.9] - 2026-03-16 -
vulntitan/trunk/assets/js/firewall.js
r3483644 r3483739 41 41 const $firewallWafCommandEnabled = $('#vulntitan-firewall-waf-command-enabled'); 42 42 const $firewallWafWhitelistPaths = $('#vulntitan-firewall-waf-whitelist-paths'); 43 const $firewallLearningEnabled = $('#vulntitan-firewall-learning-enabled'); 44 const $firewallLearningThreshold = $('#vulntitan-firewall-learning-threshold'); 45 const $firewallLearningWindow = $('#vulntitan-firewall-learning-window'); 46 const $firewallLearningRefresh = $('#vulntitan-firewall-learning-refresh'); 47 const $firewallLearningList = $('#vulntitan-firewall-learning-list'); 48 const $firewallLearningEmpty = $('#vulntitan-firewall-learning-empty'); 43 49 const $firewallTrustCloudflare = $('#vulntitan-firewall-trust-cloudflare'); 44 50 const $firewallTrustedProxies = $('#vulntitan-firewall-trusted-proxies'); … … 74 80 allLogs: [], 75 81 approvals: [], 82 learningSuggestions: [], 83 learningLoaded: false, 84 learningLoading: false, 76 85 selectedLogId: '', 77 86 visibleLogLimit: LOG_PAGE_SIZE, … … 432 441 .prop('hidden', !isActive); 433 442 }); 443 444 if (normalizedTab === 'waf') { 445 maybeLoadLearningSuggestions(false); 446 } 434 447 } 435 448 … … 494 507 Array.isArray(data.waf_whitelist_paths) ? data.waf_whitelist_paths.join('\n') : '' 495 508 ); 509 $firewallLearningEnabled.prop('checked', !!Number(data.learning_mode_enabled || 0)); 510 $firewallLearningThreshold.val(Number(data.learning_suggestion_threshold || 3)); 511 $firewallLearningWindow.val(Number(data.learning_suggestion_window_days || 7)); 496 512 $firewallTrustCloudflare.prop('checked', !!Number(data.trust_cloudflare || 0)); 497 513 $firewallTrustedProxies.val( … … 830 846 applySettings(data.settings || {}); 831 847 renderLoginAccess(data.login_access || {}); 848 renderLearningSuggestions(); 832 849 } 833 850 … … 862 879 $firewallWafCommandEnabled.prop('disabled', !!isBusy); 863 880 $firewallWafWhitelistPaths.prop('disabled', !!isBusy); 881 $firewallLearningEnabled.prop('disabled', !!isBusy); 882 $firewallLearningThreshold.prop('disabled', !!isBusy); 883 $firewallLearningWindow.prop('disabled', !!isBusy); 884 $firewallLearningRefresh.prop('disabled', !!isBusy); 864 885 $firewallTrustCloudflare.prop('disabled', !!isBusy); 865 886 $firewallTrustedProxies.prop('disabled', !!isBusy); … … 1016 1037 } 1017 1038 1039 function renderLearningSuggestions() { 1040 if (!$firewallLearningList.length) { 1041 return; 1042 } 1043 1044 const enabled = $firewallLearningEnabled.is(':checked'); 1045 const suggestions = Array.isArray(state.learningSuggestions) ? state.learningSuggestions : []; 1046 1047 if (!enabled) { 1048 $firewallLearningList.empty(); 1049 $firewallLearningEmpty 1050 .text(i18n.firewall_learning_disabled || 'Learning suggestions are disabled.') 1051 .show(); 1052 return; 1053 } 1054 1055 if (!suggestions.length) { 1056 $firewallLearningList.empty(); 1057 $firewallLearningEmpty 1058 .text(i18n.firewall_learning_empty || 'No learning suggestions yet.') 1059 .show(); 1060 return; 1061 } 1062 1063 $firewallLearningEmpty.hide(); 1064 $firewallLearningList.html(suggestions.map(renderLearningItem).join('')); 1065 } 1066 1067 function renderLearningItem(item) { 1068 const pattern = String(item.pattern || ''); 1069 const lastSeen = item.last_seen_local || item.last_seen || ''; 1070 const count = Number(item.count || 0); 1071 const ruleGroup = item.rule_group || ''; 1072 const ruleId = item.rule_id || ''; 1073 const action = item.approval_action || ''; 1074 const restRoute = item.approval_rest_route || ''; 1075 const actionLabel = action 1076 ? `${i18n.firewall_approval_action || 'Action'}: ${action}` 1077 : (restRoute ? `${i18n.firewall_approval_route || 'Route'}: ${restRoute}` : ''); 1078 1079 return ` 1080 <div class="vulntitan-firewall-log-item vulntitan-firewall-approval-card"> 1081 <div class="vulntitan-firewall-log-head"> 1082 <span class="vulntitan-firewall-log-badge is-warning"> 1083 ${escapeHtml(i18n.firewall_learning_suggestion || 'Learning suggestion')} 1084 </span> 1085 <time class="vulntitan-firewall-log-time">${escapeHtml(lastSeen)}</time> 1086 </div> 1087 <div class="vulntitan-firewall-log-request">${escapeHtml(pattern || '/')}</div> 1088 <div class="vulntitan-firewall-log-meta"> 1089 <span class="vulntitan-firewall-log-meta-item">${escapeHtml(i18n.firewall_learning_hits || 'Hits')}: ${count}</span> 1090 ${ruleGroup ? `<span class="vulntitan-firewall-log-meta-item">Group: ${escapeHtml(ruleGroup)}</span>` : ''} 1091 ${ruleId ? `<span class="vulntitan-firewall-log-meta-item">Rule: ${escapeHtml(ruleId)}</span>` : ''} 1092 ${actionLabel ? `<span class="vulntitan-firewall-log-meta-item">${escapeHtml(actionLabel)}</span>` : ''} 1093 </div> 1094 <div class="vulntitan-firewall-approval-pattern"> 1095 <span>${escapeHtml(i18n.firewall_approval_pattern || 'Whitelist pattern')}:</span> 1096 <code>${escapeHtml(pattern)}</code> 1097 </div> 1098 <div class="vulntitan-firewall-approval-actions"> 1099 <button type="button" class="vulntitan-fw-btn vulntitan-fw-btn-primary" data-firewall-learning-allow="${escapeHtml(pattern)}"> 1100 ${escapeHtml(i18n.firewall_learning_allow || 'Allow')} 1101 </button> 1102 <button type="button" class="vulntitan-fw-btn vulntitan-fw-btn-secondary" data-firewall-learning-dismiss="${escapeHtml(pattern)}"> 1103 ${escapeHtml(i18n.firewall_learning_dismiss || 'Dismiss')} 1104 </button> 1105 </div> 1106 </div> 1107 `; 1108 } 1109 1110 function loadLearningSuggestions(options) { 1111 if (!$firewallLearningList.length) { 1112 return; 1113 } 1114 1115 const config = $.extend({ 1116 silent: false, 1117 force: false 1118 }, options || {}); 1119 1120 if (!$firewallLearningEnabled.is(':checked')) { 1121 state.learningSuggestions = []; 1122 renderLearningSuggestions(); 1123 return; 1124 } 1125 1126 if (state.learningLoading) { 1127 return; 1128 } 1129 1130 if (state.learningLoaded && !config.force) { 1131 renderLearningSuggestions(); 1132 return; 1133 } 1134 1135 state.learningLoading = true; 1136 1137 if (!config.silent) { 1138 setFeedback('info', i18n.firewall_learning_loading || 'Loading learning suggestions...'); 1139 } 1140 1141 $.post(VulnTitan.ajaxUrl, { 1142 action: 'vulntitan_firewall_get_learning', 1143 nonce: VulnTitan.nonce 1144 }, function (response) { 1145 if (!response || !response.success) { 1146 const message = (response && response.data && response.data.message) 1147 ? response.data.message 1148 : (i18n.firewall_learning_failed || 'Failed to load learning suggestions.'); 1149 if (!config.silent) { 1150 setFeedback('error', message); 1151 } 1152 return; 1153 } 1154 1155 const payload = response.data || {}; 1156 state.learningSuggestions = Array.isArray(payload.suggestions) ? payload.suggestions : []; 1157 state.learningLoaded = true; 1158 renderLearningSuggestions(); 1159 1160 if (!config.silent) { 1161 setFeedback('success', i18n.firewall_learning_updated || 'Learning suggestions updated.'); 1162 } 1163 }).fail(function () { 1164 if (!config.silent) { 1165 setFeedback('error', i18n.firewall_learning_failed || 'Failed to load learning suggestions.'); 1166 } 1167 }).always(function () { 1168 state.learningLoading = false; 1169 }); 1170 } 1171 1172 function maybeLoadLearningSuggestions(force) { 1173 if (force) { 1174 loadLearningSuggestions({ force: true, silent: true }); 1175 return; 1176 } 1177 1178 if (!state.learningLoaded) { 1179 loadLearningSuggestions({ silent: true }); 1180 } else { 1181 renderLearningSuggestions(); 1182 } 1183 } 1184 1018 1185 function saveSettings() { 1019 1186 if (state.requestInFlight) { … … 1051 1218 waf_command_injection_enabled: $firewallWafCommandEnabled.is(':checked') ? 1 : 0, 1052 1219 waf_whitelist_paths: String($firewallWafWhitelistPaths.val() || ''), 1220 learning_mode_enabled: $firewallLearningEnabled.is(':checked') ? 1 : 0, 1221 learning_suggestion_threshold: Number($firewallLearningThreshold.val() || 3), 1222 learning_suggestion_window_days: Number($firewallLearningWindow.val() || 7), 1053 1223 trust_cloudflare: $firewallTrustCloudflare.is(':checked') ? 1 : 0, 1054 1224 trusted_proxies: String($firewallTrustedProxies.val() || ''), … … 1079 1249 }); 1080 1250 1251 const activeTab = String($firewallTabs.filter('.is-active').data('firewallTab') || ''); 1252 if (activeTab === 'waf' && $firewallLearningEnabled.is(':checked')) { 1253 state.learningLoaded = false; 1254 loadLearningSuggestions({ 1255 silent: true, 1256 force: true 1257 }); 1258 } 1259 1081 1260 const muInstall = payload.mu_loader_install || {}; 1082 1261 if (payload.notice) { … … 1238 1417 } 1239 1418 1419 function sendLearningAction(action, pattern, $button, confirmMessage, successMessage) { 1420 const cleanPattern = String(pattern || '').trim(); 1421 if (!cleanPattern) { 1422 setFeedback('error', i18n.firewall_approval_invalid || 'Invalid learning suggestion.'); 1423 return; 1424 } 1425 1426 if (confirmMessage && !window.confirm(confirmMessage)) { 1427 return; 1428 } 1429 1430 if ($button && $button.length) { 1431 $button.prop('disabled', true); 1432 } 1433 1434 setFeedback('info', i18n.firewall_action_in_progress || 'Working...'); 1435 1436 $.post(VulnTitan.ajaxUrl, { 1437 action: action, 1438 nonce: VulnTitan.nonce, 1439 pattern: cleanPattern 1440 }, function (response) { 1441 if (!response || !response.success) { 1442 const message = (response && response.data && response.data.message) 1443 ? response.data.message 1444 : (i18n.firewall_action_failed || 'Action failed.'); 1445 setFeedback('error', message); 1446 return; 1447 } 1448 1449 const payload = response.data || {}; 1450 if (payload.settings) { 1451 applySettings(payload.settings); 1452 } 1453 1454 if (Array.isArray(payload.suggestions)) { 1455 state.learningSuggestions = payload.suggestions; 1456 state.learningLoaded = true; 1457 renderLearningSuggestions(); 1458 } 1459 1460 setFeedback('success', successMessage); 1461 }).fail(function () { 1462 setFeedback('error', i18n.firewall_action_failed || 'Action failed.'); 1463 }).always(function () { 1464 if ($button && $button.length) { 1465 $button.prop('disabled', false); 1466 } 1467 }); 1468 } 1469 1240 1470 function handleFeedToggle() { 1241 1471 state.feedPaused = !state.feedPaused; … … 1358 1588 }); 1359 1589 1590 $firewallLearningRefresh.off('click').on('click', function () { 1591 loadLearningSuggestions({ 1592 silent: false, 1593 force: true 1594 }); 1595 }); 1596 1597 $firewallLearningEnabled.off('change').on('change', function () { 1598 if ($firewallLearningEnabled.is(':checked')) { 1599 state.learningLoaded = false; 1600 loadLearningSuggestions({ 1601 silent: true, 1602 force: true 1603 }); 1604 return; 1605 } 1606 1607 state.learningLoaded = false; 1608 state.learningSuggestions = []; 1609 renderLearningSuggestions(); 1610 }); 1611 1612 $firewallLearningList.off('click', '[data-firewall-learning-allow]').on('click', '[data-firewall-learning-allow]', function () { 1613 const $button = $(this); 1614 const pattern = String($button.data('firewallLearningAllow') || ''); 1615 sendLearningAction( 1616 'vulntitan_firewall_apply_learning', 1617 pattern, 1618 $button, 1619 i18n.firewall_learning_confirm_allow || 'Allow this suggested pattern and add it to the WAF whitelist?', 1620 i18n.firewall_approval_success || 'Whitelist updated.' 1621 ); 1622 }); 1623 1624 $firewallLearningList.off('click', '[data-firewall-learning-dismiss]').on('click', '[data-firewall-learning-dismiss]', function () { 1625 const $button = $(this); 1626 const pattern = String($button.data('firewallLearningDismiss') || ''); 1627 sendLearningAction( 1628 'vulntitan_firewall_dismiss_learning', 1629 pattern, 1630 $button, 1631 i18n.firewall_learning_confirm_dismiss || 'Dismiss this learning suggestion?', 1632 i18n.firewall_learning_dismissed || 'Learning suggestion dismissed.' 1633 ); 1634 }); 1635 1360 1636 $firewallLoadMore.off('click', '[data-firewall-load-more]').on('click', '[data-firewall-load-more]', function () { 1361 1637 state.visibleLogLimit += LOG_PAGE_SIZE; -
vulntitan/trunk/assets/js/firewall.min.js
r3483644 r3483739 41 41 const $firewallWafCommandEnabled = $('#vulntitan-firewall-waf-command-enabled'); 42 42 const $firewallWafWhitelistPaths = $('#vulntitan-firewall-waf-whitelist-paths'); 43 const $firewallLearningEnabled = $('#vulntitan-firewall-learning-enabled'); 44 const $firewallLearningThreshold = $('#vulntitan-firewall-learning-threshold'); 45 const $firewallLearningWindow = $('#vulntitan-firewall-learning-window'); 46 const $firewallLearningRefresh = $('#vulntitan-firewall-learning-refresh'); 47 const $firewallLearningList = $('#vulntitan-firewall-learning-list'); 48 const $firewallLearningEmpty = $('#vulntitan-firewall-learning-empty'); 43 49 const $firewallTrustCloudflare = $('#vulntitan-firewall-trust-cloudflare'); 44 50 const $firewallTrustedProxies = $('#vulntitan-firewall-trusted-proxies'); … … 74 80 allLogs: [], 75 81 approvals: [], 82 learningSuggestions: [], 83 learningLoaded: false, 84 learningLoading: false, 76 85 selectedLogId: '', 77 86 visibleLogLimit: LOG_PAGE_SIZE, … … 432 441 .prop('hidden', !isActive); 433 442 }); 443 444 if (normalizedTab === 'waf') { 445 maybeLoadLearningSuggestions(false); 446 } 434 447 } 435 448 … … 494 507 Array.isArray(data.waf_whitelist_paths) ? data.waf_whitelist_paths.join('\n') : '' 495 508 ); 509 $firewallLearningEnabled.prop('checked', !!Number(data.learning_mode_enabled || 0)); 510 $firewallLearningThreshold.val(Number(data.learning_suggestion_threshold || 3)); 511 $firewallLearningWindow.val(Number(data.learning_suggestion_window_days || 7)); 496 512 $firewallTrustCloudflare.prop('checked', !!Number(data.trust_cloudflare || 0)); 497 513 $firewallTrustedProxies.val( … … 814 830 } 815 831 832 function applyPayload(payload, options) { 833 const data = payload || {}; 834 const config = $.extend({ 835 syncSettings: true 836 }, options || {}); 837 838 latestMuLoaderStatus = data.mu_loader || latestMuLoaderStatus; 839 state.allLogs = Array.isArray(data.logs) ? data.logs : []; 840 state.approvals = Array.isArray(data.approvals) ? data.approvals : []; 841 state.proxyStatus = data.proxy_status || state.proxyStatus || {}; 842 state.lastUpdatedAt = new Date(); 843 state.lastRefreshFailed = false; 844 845 if (config.syncSettings) { 846 applySettings(data.settings || {}); 847 renderLoginAccess(data.login_access || {}); 848 renderLearningSuggestions(); 849 } 850 851 renderOverview(data.summary || {}, latestMuLoaderStatus || {}); 852 renderFeed(); 853 renderApprovals(); 854 updateFeedChrome(); 855 updateProxyWarnings(); 856 } 857 858 function setBusyState(isBusy) { 859 state.hardBusy = !!isBusy; 860 861 $firewallRoot.toggleClass('is-busy', !!isBusy); 862 $firewallSaveSettings.prop('disabled', !!isBusy); 863 $firewallRefresh.prop('disabled', !!isBusy); 864 $firewallClearLogs.prop('disabled', !!isBusy); 865 $firewallFeedToggle.prop('disabled', !!isBusy); 866 $firewallFeedSearch.prop('disabled', !!isBusy); 867 $firewallFeedFilters.prop('disabled', !!isBusy); 868 $firewallLoadMore.find('[data-firewall-load-more]').prop('disabled', !!isBusy); 869 $firewallTabs.prop('disabled', !!isBusy); 870 $firewallEnabled.prop('disabled', !!isBusy); 871 $firewallLoginProtection.prop('disabled', !!isBusy); 872 $firewallCustomLoginSlug.prop('disabled', !!isBusy); 873 $firewallTwoFactorEnabled.prop('disabled', !!isBusy); 874 $firewallCaptchaProvider.prop('disabled', !!isBusy); 875 $firewallXmlrpcPolicy.prop('disabled', !!isBusy); 876 $firewallXmlrpcAllowlist.prop('disabled', !!isBusy); 877 $firewallWeakPasswordBlockingEnabled.prop('disabled', !!isBusy); 878 $firewallWafSqliEnabled.prop('disabled', !!isBusy); 879 $firewallWafCommandEnabled.prop('disabled', !!isBusy); 880 $firewallWafWhitelistPaths.prop('disabled', !!isBusy); 881 $firewallLearningEnabled.prop('disabled', !!isBusy); 882 $firewallLearningThreshold.prop('disabled', !!isBusy); 883 $firewallLearningWindow.prop('disabled', !!isBusy); 884 $firewallLearningRefresh.prop('disabled', !!isBusy); 885 $firewallTrustCloudflare.prop('disabled', !!isBusy); 886 $firewallTrustedProxies.prop('disabled', !!isBusy); 887 $firewallMaxAttempts.prop('disabled', !!isBusy); 888 $firewallLockoutMinutes.prop('disabled', !!isBusy); 889 $firewallCommentShieldEnabled.prop('disabled', !!isBusy); 890 $firewallCommentSuspiciousAction.prop('disabled', !!isBusy); 891 $firewallCommentMinSubmitSeconds.prop('disabled', !!isBusy); 892 $firewallCommentMaxLinks.prop('disabled', !!isBusy); 893 $firewallCommentRateLimitAttempts.prop('disabled', !!isBusy); 894 $firewallCommentRateLimitWindowMinutes.prop('disabled', !!isBusy); 895 $firewallLogRetention.prop('disabled', !!isBusy); 896 $firewallLockoutAllowlist.prop('disabled', !!isBusy); 897 $firewallWeeklySummaryEmailEnabled.prop('disabled', !!isBusy); 898 $firewallWeeklySummaryEmailRecipient.prop('disabled', !!isBusy); 899 refreshIdentityControlState(); 900 } 901 902 function loadFirewallData(options) { 903 const config = $.extend({ 904 showLoadingNotice: false, 905 hardBusy: false, 906 silentError: false, 907 clearFeedbackOnSuccess: false, 908 syncSettings: false 909 }, options || {}); 910 911 if (state.requestInFlight) { 912 return; 913 } 914 915 if (config.hardBusy) { 916 setBusyState(true); 917 } 918 919 state.requestInFlight = true; 920 updateFeedChrome(); 921 922 if (config.showLoadingNotice) { 923 setFeedback('info', i18n.firewall_refreshing || 'Refreshing firewall data...'); 924 } 925 926 $.post(VulnTitan.ajaxUrl, { 927 action: 'vulntitan_firewall_get_data', 928 nonce: VulnTitan.nonce 929 }, function (response) { 930 if (!response || !response.success) { 931 state.lastRefreshFailed = true; 932 updateFeedChrome(); 933 934 if (!config.silentError) { 935 const message = (response && response.data && response.data.message) 936 ? response.data.message 937 : (i18n.firewall_load_failed || 'Failed to load firewall data.'); 938 setFeedback('error', message); 939 } 940 return; 941 } 942 943 applyPayload(response.data || {}, { 944 syncSettings: config.syncSettings 945 }); 946 947 if (config.clearFeedbackOnSuccess) { 948 setFeedback('', ''); 949 } 950 }).fail(function () { 951 state.lastRefreshFailed = true; 952 updateFeedChrome(); 953 954 if (!config.silentError) { 955 setFeedback('error', i18n.firewall_load_failed || 'Failed to load firewall data.'); 956 } 957 }).always(function () { 958 state.requestInFlight = false; 959 updateFeedChrome(); 960 961 if (config.hardBusy) { 962 setBusyState(false); 963 } 964 }); 965 } 966 816 967 function renderApprovals() { 817 968 if (!$firewallApprovalsList.length) { … … 886 1037 } 887 1038 888 function applyPayload(payload, options) { 889 const data = payload || {}; 1039 function renderLearningSuggestions() { 1040 if (!$firewallLearningList.length) { 1041 return; 1042 } 1043 1044 const enabled = $firewallLearningEnabled.is(':checked'); 1045 const suggestions = Array.isArray(state.learningSuggestions) ? state.learningSuggestions : []; 1046 1047 if (!enabled) { 1048 $firewallLearningList.empty(); 1049 $firewallLearningEmpty 1050 .text(i18n.firewall_learning_disabled || 'Learning suggestions are disabled.') 1051 .show(); 1052 return; 1053 } 1054 1055 if (!suggestions.length) { 1056 $firewallLearningList.empty(); 1057 $firewallLearningEmpty 1058 .text(i18n.firewall_learning_empty || 'No learning suggestions yet.') 1059 .show(); 1060 return; 1061 } 1062 1063 $firewallLearningEmpty.hide(); 1064 $firewallLearningList.html(suggestions.map(renderLearningItem).join('')); 1065 } 1066 1067 function renderLearningItem(item) { 1068 const pattern = String(item.pattern || ''); 1069 const lastSeen = item.last_seen_local || item.last_seen || ''; 1070 const count = Number(item.count || 0); 1071 const ruleGroup = item.rule_group || ''; 1072 const ruleId = item.rule_id || ''; 1073 const action = item.approval_action || ''; 1074 const restRoute = item.approval_rest_route || ''; 1075 const actionLabel = action 1076 ? `${i18n.firewall_approval_action || 'Action'}: ${action}` 1077 : (restRoute ? `${i18n.firewall_approval_route || 'Route'}: ${restRoute}` : ''); 1078 1079 return ` 1080 <div class="vulntitan-firewall-log-item vulntitan-firewall-approval-card"> 1081 <div class="vulntitan-firewall-log-head"> 1082 <span class="vulntitan-firewall-log-badge is-warning"> 1083 ${escapeHtml(i18n.firewall_learning_suggestion || 'Learning suggestion')} 1084 </span> 1085 <time class="vulntitan-firewall-log-time">${escapeHtml(lastSeen)}</time> 1086 </div> 1087 <div class="vulntitan-firewall-log-request">${escapeHtml(pattern || '/')}</div> 1088 <div class="vulntitan-firewall-log-meta"> 1089 <span class="vulntitan-firewall-log-meta-item">${escapeHtml(i18n.firewall_learning_hits || 'Hits')}: ${count}</span> 1090 ${ruleGroup ? `<span class="vulntitan-firewall-log-meta-item">Group: ${escapeHtml(ruleGroup)}</span>` : ''} 1091 ${ruleId ? `<span class="vulntitan-firewall-log-meta-item">Rule: ${escapeHtml(ruleId)}</span>` : ''} 1092 ${actionLabel ? `<span class="vulntitan-firewall-log-meta-item">${escapeHtml(actionLabel)}</span>` : ''} 1093 </div> 1094 <div class="vulntitan-firewall-approval-pattern"> 1095 <span>${escapeHtml(i18n.firewall_approval_pattern || 'Whitelist pattern')}:</span> 1096 <code>${escapeHtml(pattern)}</code> 1097 </div> 1098 <div class="vulntitan-firewall-approval-actions"> 1099 <button type="button" class="vulntitan-fw-btn vulntitan-fw-btn-primary" data-firewall-learning-allow="${escapeHtml(pattern)}"> 1100 ${escapeHtml(i18n.firewall_learning_allow || 'Allow')} 1101 </button> 1102 <button type="button" class="vulntitan-fw-btn vulntitan-fw-btn-secondary" data-firewall-learning-dismiss="${escapeHtml(pattern)}"> 1103 ${escapeHtml(i18n.firewall_learning_dismiss || 'Dismiss')} 1104 </button> 1105 </div> 1106 </div> 1107 `; 1108 } 1109 1110 function loadLearningSuggestions(options) { 1111 if (!$firewallLearningList.length) { 1112 return; 1113 } 1114 890 1115 const config = $.extend({ 891 syncSettings: true 1116 silent: false, 1117 force: false 892 1118 }, options || {}); 893 1119 894 latestMuLoaderStatus = data.mu_loader || latestMuLoaderStatus; 895 state.allLogs = Array.isArray(data.logs) ? data.logs : []; 896 state.approvals = Array.isArray(data.approvals) ? data.approvals : []; 897 state.proxyStatus = data.proxy_status || state.proxyStatus || {}; 898 state.lastUpdatedAt = new Date(); 899 state.lastRefreshFailed = false; 900 901 if (config.syncSettings) { 902 applySettings(data.settings || {}); 903 renderLoginAccess(data.login_access || {}); 904 } 905 906 renderOverview(data.summary || {}, latestMuLoaderStatus || {}); 907 renderFeed(); 908 renderApprovals(); 909 updateFeedChrome(); 910 updateProxyWarnings(); 911 } 912 913 function setBusyState(isBusy) { 914 state.hardBusy = !!isBusy; 915 916 $firewallRoot.toggleClass('is-busy', !!isBusy); 917 $firewallSaveSettings.prop('disabled', !!isBusy); 918 $firewallRefresh.prop('disabled', !!isBusy); 919 $firewallClearLogs.prop('disabled', !!isBusy); 920 $firewallFeedToggle.prop('disabled', !!isBusy); 921 $firewallFeedSearch.prop('disabled', !!isBusy); 922 $firewallFeedFilters.prop('disabled', !!isBusy); 923 $firewallLoadMore.find('[data-firewall-load-more]').prop('disabled', !!isBusy); 924 $firewallTabs.prop('disabled', !!isBusy); 925 $firewallEnabled.prop('disabled', !!isBusy); 926 $firewallLoginProtection.prop('disabled', !!isBusy); 927 $firewallCustomLoginSlug.prop('disabled', !!isBusy); 928 $firewallTwoFactorEnabled.prop('disabled', !!isBusy); 929 $firewallCaptchaProvider.prop('disabled', !!isBusy); 930 $firewallXmlrpcPolicy.prop('disabled', !!isBusy); 931 $firewallXmlrpcAllowlist.prop('disabled', !!isBusy); 932 $firewallWeakPasswordBlockingEnabled.prop('disabled', !!isBusy); 933 $firewallWafSqliEnabled.prop('disabled', !!isBusy); 934 $firewallWafCommandEnabled.prop('disabled', !!isBusy); 935 $firewallWafWhitelistPaths.prop('disabled', !!isBusy); 936 $firewallTrustCloudflare.prop('disabled', !!isBusy); 937 $firewallTrustedProxies.prop('disabled', !!isBusy); 938 $firewallMaxAttempts.prop('disabled', !!isBusy); 939 $firewallLockoutMinutes.prop('disabled', !!isBusy); 940 $firewallCommentShieldEnabled.prop('disabled', !!isBusy); 941 $firewallCommentSuspiciousAction.prop('disabled', !!isBusy); 942 $firewallCommentMinSubmitSeconds.prop('disabled', !!isBusy); 943 $firewallCommentMaxLinks.prop('disabled', !!isBusy); 944 $firewallCommentRateLimitAttempts.prop('disabled', !!isBusy); 945 $firewallCommentRateLimitWindowMinutes.prop('disabled', !!isBusy); 946 $firewallLogRetention.prop('disabled', !!isBusy); 947 $firewallLockoutAllowlist.prop('disabled', !!isBusy); 948 $firewallWeeklySummaryEmailEnabled.prop('disabled', !!isBusy); 949 $firewallWeeklySummaryEmailRecipient.prop('disabled', !!isBusy); 950 refreshIdentityControlState(); 951 } 952 953 function loadFirewallData(options) { 954 const config = $.extend({ 955 showLoadingNotice: false, 956 hardBusy: false, 957 silentError: false, 958 clearFeedbackOnSuccess: false, 959 syncSettings: false 960 }, options || {}); 961 962 if (state.requestInFlight) { 963 return; 964 } 965 966 if (config.hardBusy) { 967 setBusyState(true); 968 } 969 970 state.requestInFlight = true; 971 updateFeedChrome(); 972 973 if (config.showLoadingNotice) { 974 setFeedback('info', i18n.firewall_refreshing || 'Refreshing firewall data...'); 1120 if (!$firewallLearningEnabled.is(':checked')) { 1121 state.learningSuggestions = []; 1122 renderLearningSuggestions(); 1123 return; 1124 } 1125 1126 if (state.learningLoading) { 1127 return; 1128 } 1129 1130 if (state.learningLoaded && !config.force) { 1131 renderLearningSuggestions(); 1132 return; 1133 } 1134 1135 state.learningLoading = true; 1136 1137 if (!config.silent) { 1138 setFeedback('info', i18n.firewall_learning_loading || 'Loading learning suggestions...'); 975 1139 } 976 1140 977 1141 $.post(VulnTitan.ajaxUrl, { 978 action: 'vulntitan_firewall_get_ data',1142 action: 'vulntitan_firewall_get_learning', 979 1143 nonce: VulnTitan.nonce 980 1144 }, function (response) { 981 1145 if (!response || !response.success) { 982 state.lastRefreshFailed = true; 983 updateFeedChrome(); 984 985 if (!config.silentError) { 986 const message = (response && response.data && response.data.message) 987 ? response.data.message 988 : (i18n.firewall_load_failed || 'Failed to load firewall data.'); 1146 const message = (response && response.data && response.data.message) 1147 ? response.data.message 1148 : (i18n.firewall_learning_failed || 'Failed to load learning suggestions.'); 1149 if (!config.silent) { 989 1150 setFeedback('error', message); 990 1151 } … … 992 1153 } 993 1154 994 applyPayload(response.data || {}, { 995 syncSettings: config.syncSettings 996 }); 997 998 if (config.clearFeedbackOnSuccess) { 999 setFeedback('', ''); 1155 const payload = response.data || {}; 1156 state.learningSuggestions = Array.isArray(payload.suggestions) ? payload.suggestions : []; 1157 state.learningLoaded = true; 1158 renderLearningSuggestions(); 1159 1160 if (!config.silent) { 1161 setFeedback('success', i18n.firewall_learning_updated || 'Learning suggestions updated.'); 1000 1162 } 1001 1163 }).fail(function () { 1002 state.lastRefreshFailed = true; 1003 updateFeedChrome(); 1004 1005 if (!config.silentError) { 1006 setFeedback('error', i18n.firewall_load_failed || 'Failed to load firewall data.'); 1164 if (!config.silent) { 1165 setFeedback('error', i18n.firewall_learning_failed || 'Failed to load learning suggestions.'); 1007 1166 } 1008 1167 }).always(function () { 1009 state.requestInFlight = false; 1010 updateFeedChrome(); 1011 1012 if (config.hardBusy) { 1013 setBusyState(false); 1014 } 1168 state.learningLoading = false; 1015 1169 }); 1170 } 1171 1172 function maybeLoadLearningSuggestions(force) { 1173 if (force) { 1174 loadLearningSuggestions({ force: true, silent: true }); 1175 return; 1176 } 1177 1178 if (!state.learningLoaded) { 1179 loadLearningSuggestions({ silent: true }); 1180 } else { 1181 renderLearningSuggestions(); 1182 } 1016 1183 } 1017 1184 … … 1051 1218 waf_command_injection_enabled: $firewallWafCommandEnabled.is(':checked') ? 1 : 0, 1052 1219 waf_whitelist_paths: String($firewallWafWhitelistPaths.val() || ''), 1220 learning_mode_enabled: $firewallLearningEnabled.is(':checked') ? 1 : 0, 1221 learning_suggestion_threshold: Number($firewallLearningThreshold.val() || 3), 1222 learning_suggestion_window_days: Number($firewallLearningWindow.val() || 7), 1053 1223 trust_cloudflare: $firewallTrustCloudflare.is(':checked') ? 1 : 0, 1054 1224 trusted_proxies: String($firewallTrustedProxies.val() || ''), … … 1079 1249 }); 1080 1250 1251 const activeTab = String($firewallTabs.filter('.is-active').data('firewallTab') || ''); 1252 if (activeTab === 'waf' && $firewallLearningEnabled.is(':checked')) { 1253 state.learningLoaded = false; 1254 loadLearningSuggestions({ 1255 silent: true, 1256 force: true 1257 }); 1258 } 1259 1081 1260 const muInstall = payload.mu_loader_install || {}; 1082 1261 if (payload.notice) { … … 1238 1417 } 1239 1418 1419 function sendLearningAction(action, pattern, $button, confirmMessage, successMessage) { 1420 const cleanPattern = String(pattern || '').trim(); 1421 if (!cleanPattern) { 1422 setFeedback('error', i18n.firewall_approval_invalid || 'Invalid learning suggestion.'); 1423 return; 1424 } 1425 1426 if (confirmMessage && !window.confirm(confirmMessage)) { 1427 return; 1428 } 1429 1430 if ($button && $button.length) { 1431 $button.prop('disabled', true); 1432 } 1433 1434 setFeedback('info', i18n.firewall_action_in_progress || 'Working...'); 1435 1436 $.post(VulnTitan.ajaxUrl, { 1437 action: action, 1438 nonce: VulnTitan.nonce, 1439 pattern: cleanPattern 1440 }, function (response) { 1441 if (!response || !response.success) { 1442 const message = (response && response.data && response.data.message) 1443 ? response.data.message 1444 : (i18n.firewall_action_failed || 'Action failed.'); 1445 setFeedback('error', message); 1446 return; 1447 } 1448 1449 const payload = response.data || {}; 1450 if (payload.settings) { 1451 applySettings(payload.settings); 1452 } 1453 1454 if (Array.isArray(payload.suggestions)) { 1455 state.learningSuggestions = payload.suggestions; 1456 state.learningLoaded = true; 1457 renderLearningSuggestions(); 1458 } 1459 1460 setFeedback('success', successMessage); 1461 }).fail(function () { 1462 setFeedback('error', i18n.firewall_action_failed || 'Action failed.'); 1463 }).always(function () { 1464 if ($button && $button.length) { 1465 $button.prop('disabled', false); 1466 } 1467 }); 1468 } 1469 1240 1470 function handleFeedToggle() { 1241 1471 state.feedPaused = !state.feedPaused; … … 1358 1588 }); 1359 1589 1590 $firewallLearningRefresh.off('click').on('click', function () { 1591 loadLearningSuggestions({ 1592 silent: false, 1593 force: true 1594 }); 1595 }); 1596 1597 $firewallLearningEnabled.off('change').on('change', function () { 1598 if ($firewallLearningEnabled.is(':checked')) { 1599 state.learningLoaded = false; 1600 loadLearningSuggestions({ 1601 silent: true, 1602 force: true 1603 }); 1604 return; 1605 } 1606 1607 state.learningLoaded = false; 1608 state.learningSuggestions = []; 1609 renderLearningSuggestions(); 1610 }); 1611 1612 $firewallLearningList.off('click', '[data-firewall-learning-allow]').on('click', '[data-firewall-learning-allow]', function () { 1613 const $button = $(this); 1614 const pattern = String($button.data('firewallLearningAllow') || ''); 1615 sendLearningAction( 1616 'vulntitan_firewall_apply_learning', 1617 pattern, 1618 $button, 1619 i18n.firewall_learning_confirm_allow || 'Allow this suggested pattern and add it to the WAF whitelist?', 1620 i18n.firewall_approval_success || 'Whitelist updated.' 1621 ); 1622 }); 1623 1624 $firewallLearningList.off('click', '[data-firewall-learning-dismiss]').on('click', '[data-firewall-learning-dismiss]', function () { 1625 const $button = $(this); 1626 const pattern = String($button.data('firewallLearningDismiss') || ''); 1627 sendLearningAction( 1628 'vulntitan_firewall_dismiss_learning', 1629 pattern, 1630 $button, 1631 i18n.firewall_learning_confirm_dismiss || 'Dismiss this learning suggestion?', 1632 i18n.firewall_learning_dismissed || 'Learning suggestion dismissed.' 1633 ); 1634 }); 1635 1360 1636 $firewallLoadMore.off('click', '[data-firewall-load-more]').on('click', '[data-firewall-load-more]', function () { 1361 1637 state.visibleLogLimit += LOG_PAGE_SIZE; -
vulntitan/trunk/includes/Admin/Admin.php
r3483333 r3483739 179 179 'firewall_approval_no_pattern' => esc_html__('No safe auto-approval pattern available.', 'vulntitan'), 180 180 'firewall_approval_invalid' => esc_html__('Invalid approval request.', 'vulntitan'), 181 'firewall_learning_loading' => esc_html__('Loading learning suggestions...', 'vulntitan'), 182 'firewall_learning_updated' => esc_html__('Learning suggestions updated.', 'vulntitan'), 183 'firewall_learning_failed' => esc_html__('Failed to load learning suggestions.', 'vulntitan'), 184 'firewall_learning_empty' => esc_html__('No learning suggestions yet.', 'vulntitan'), 185 'firewall_learning_disabled' => esc_html__('Learning suggestions are disabled.', 'vulntitan'), 186 'firewall_learning_suggestion' => esc_html__('Learning suggestion', 'vulntitan'), 187 'firewall_learning_hits' => esc_html__('Hits', 'vulntitan'), 188 'firewall_learning_last_seen' => esc_html__('Last seen', 'vulntitan'), 189 'firewall_learning_allow' => esc_html__('Allow', 'vulntitan'), 190 'firewall_learning_dismiss' => esc_html__('Dismiss', 'vulntitan'), 191 'firewall_learning_dismissed' => esc_html__('Learning suggestion dismissed.', 'vulntitan'), 192 'firewall_learning_confirm_allow' => esc_html__('Allow this suggested pattern and add it to the WAF whitelist?', 'vulntitan'), 193 'firewall_learning_confirm_dismiss' => esc_html__('Dismiss this learning suggestion?', 'vulntitan'), 194 'firewall_learning_refresh' => esc_html__('Refresh suggestions', 'vulntitan'), 181 195 ], 182 196 ]); -
vulntitan/trunk/includes/Admin/Ajax.php
r3483644 r3483739 29 29 add_action('wp_ajax_vulntitan_firewall_unblock_ip', [$this, 'firewallUnblockIp']); 30 30 add_action('wp_ajax_vulntitan_firewall_allowlist_ip', [$this, 'firewallAllowlistIp']); 31 add_action('wp_ajax_vulntitan_firewall_get_learning', [$this, 'firewallGetLearning']); 32 add_action('wp_ajax_vulntitan_firewall_apply_learning', [$this, 'firewallApplyLearning']); 33 add_action('wp_ajax_vulntitan_firewall_dismiss_learning', [$this, 'firewallDismissLearning']); 31 34 } 32 35 … … 92 95 'waf_command_injection_enabled' => isset($_POST['waf_command_injection_enabled']) ? (int) wp_unslash($_POST['waf_command_injection_enabled']) : 1, 93 96 'waf_whitelist_paths' => isset($_POST['waf_whitelist_paths']) ? (string) wp_unslash($_POST['waf_whitelist_paths']) : '', 97 'learning_mode_enabled' => isset($_POST['learning_mode_enabled']) ? (int) wp_unslash($_POST['learning_mode_enabled']) : 0, 98 'learning_suggestion_threshold' => isset($_POST['learning_suggestion_threshold']) ? (int) wp_unslash($_POST['learning_suggestion_threshold']) : 3, 99 'learning_suggestion_window_days' => isset($_POST['learning_suggestion_window_days']) ? (int) wp_unslash($_POST['learning_suggestion_window_days']) : 7, 94 100 'trust_cloudflare' => isset($_POST['trust_cloudflare']) ? (int) wp_unslash($_POST['trust_cloudflare']) : 0, 95 101 'trusted_proxies' => isset($_POST['trusted_proxies']) ? (string) wp_unslash($_POST['trusted_proxies']) : '', … … 271 277 'allowlisted' => !$alreadyAllowlisted, 272 278 'settings' => $settings, 279 ]); 280 } 281 282 public function firewallGetLearning(): void 283 { 284 check_ajax_referer('vulntitan_ajax_scan', 'nonce'); 285 286 if (!current_user_can('manage_options')) { 287 wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403); 288 } 289 290 $settings = FirewallService::getSettings(); 291 $enabled = !empty($settings['learning_mode_enabled']); 292 $suggestions = $enabled ? FirewallService::getLearningSuggestions(30) : []; 293 294 wp_send_json_success([ 295 'enabled' => $enabled ? 1 : 0, 296 'suggestions' => $suggestions, 297 ]); 298 } 299 300 public function firewallApplyLearning(): void 301 { 302 check_ajax_referer('vulntitan_ajax_scan', 'nonce'); 303 304 if (!current_user_can('manage_options')) { 305 wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403); 306 } 307 308 $pattern = isset($_POST['pattern']) ? trim((string) wp_unslash($_POST['pattern'])) : ''; 309 if ($pattern === '') { 310 wp_send_json_error(['message' => esc_html__('Invalid learning suggestion.', 'vulntitan')], 400); 311 } 312 313 $result = FirewallService::applyLearningSuggestion($pattern); 314 if (empty($result['success'])) { 315 wp_send_json_error(['message' => esc_html__('Invalid learning suggestion.', 'vulntitan')], 400); 316 } 317 318 wp_send_json_success([ 319 'settings' => $result['settings'] ?? FirewallService::getSettings(), 320 'suggestions' => FirewallService::getLearningSuggestions(30), 321 ]); 322 } 323 324 public function firewallDismissLearning(): void 325 { 326 check_ajax_referer('vulntitan_ajax_scan', 'nonce'); 327 328 if (!current_user_can('manage_options')) { 329 wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403); 330 } 331 332 $pattern = isset($_POST['pattern']) ? trim((string) wp_unslash($_POST['pattern'])) : ''; 333 if ($pattern === '') { 334 wp_send_json_error(['message' => esc_html__('Invalid learning suggestion.', 'vulntitan')], 400); 335 } 336 337 if (!FirewallService::dismissLearningSuggestion($pattern)) { 338 wp_send_json_error(['message' => esc_html__('Invalid learning suggestion.', 'vulntitan')], 400); 339 } 340 341 wp_send_json_success([ 342 'suggestions' => FirewallService::getLearningSuggestions(30), 273 343 ]); 274 344 } -
vulntitan/trunk/includes/Admin/Pages/Firewall.php
r3483644 r3483739 423 423 </label> 424 424 </div> 425 426 <div class="vulntitan-firewall-section"> 427 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Learning Mode', 'vulntitan'); ?></div> 428 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Generate smart allowlist suggestions from repeated admin-triggered WAF blocks. This never auto-whitelists—everything requires approval.', 'vulntitan'); ?></div> 429 430 <label class="vulntitan-firewall-toggle"> 431 <input type="checkbox" id="vulntitan-firewall-learning-enabled" class="vulntitan-firewall-checkbox"> 432 <span><?php esc_html_e('Enable learning suggestions', 'vulntitan'); ?></span> 433 </label> 434 435 <div class="vulntitan-firewall-field-grid"> 436 <label class="vulntitan-firewall-field"> 437 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Minimum hits to suggest', 'vulntitan'); ?></span> 438 <input type="number" id="vulntitan-firewall-learning-threshold" class="vulntitan-firewall-input" min="2" max="20" value="3"> 439 </label> 440 441 <label class="vulntitan-firewall-field"> 442 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Lookback window (days)', 'vulntitan'); ?></span> 443 <input type="number" id="vulntitan-firewall-learning-window" class="vulntitan-firewall-input" min="1" max="30" value="7"> 444 </label> 445 446 <div class="vulntitan-firewall-field"> 447 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Suggestions', 'vulntitan'); ?></span> 448 <button type="button" class="vulntitan-fw-btn vulntitan-fw-btn-secondary" id="vulntitan-firewall-learning-refresh"><?php esc_html_e('Refresh suggestions', 'vulntitan'); ?></button> 449 <small class="vulntitan-firewall-field-help"><?php esc_html_e('Learning only analyzes WAF blocks triggered by logged-in administrators.', 'vulntitan'); ?></small> 450 </div> 451 </div> 452 </div> 453 454 <div class="vulntitan-firewall-section"> 455 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Learning Suggestions', 'vulntitan'); ?></div> 456 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Review suggested whitelist patterns derived from repeated WAF blocks. Approving adds a pattern to the WAF whitelist.', 'vulntitan'); ?></div> 457 <div id="vulntitan-firewall-learning-empty" class="vulntitan-firewall-approvals-empty"> 458 <?php esc_html_e('Learning suggestions are disabled.', 'vulntitan'); ?> 459 </div> 460 <div id="vulntitan-firewall-learning-list" class="vulntitan-firewall-log-list"></div> 461 </div> 425 462 </section> 426 463 -
vulntitan/trunk/includes/Services/FirewallService.php
r3483644 r3483739 8 8 protected const OPTION_MU_STATUS = 'vulntitan_firewall_mu_status'; 9 9 protected const OPTION_SCHEMA_VERSION = 'vulntitan_firewall_schema_version'; 10 protected const OPTION_LEARNING_DISMISSED = 'vulntitan_firewall_learning_dismissed'; 10 11 protected const MU_LOADER_FILENAME = 'vulntitan-firewall.php'; 11 12 protected const DEFAULT_LOG_RETENTION_DAYS = 30; … … 41 42 'waf_command_injection_enabled' => 1, 42 43 'waf_whitelist_paths' => [], 44 'learning_mode_enabled' => 0, 45 'learning_suggestion_threshold' => 3, 46 'learning_suggestion_window_days' => 7, 43 47 'trust_cloudflare' => 0, 44 48 'trusted_proxies' => [], … … 728 732 } 729 733 734 public static function getLearningSuggestions(int $limit = 20): array 735 { 736 $settings = self::getSettings(); 737 if (empty($settings['learning_mode_enabled'])) { 738 return []; 739 } 740 741 if (!self::ensureTable()) { 742 return []; 743 } 744 745 global $wpdb; 746 747 $safeLimit = max(1, min(50, $limit)); 748 $minHits = max(2, min(20, (int)($settings['learning_suggestion_threshold'] ?? 3))); 749 $windowDays = max(1, min(30, (int)($settings['learning_suggestion_window_days'] ?? 7))); 750 $threshold = gmdate('Y-m-d H:i:s', time() - ($windowDays * DAY_IN_SECONDS)); 751 $likePattern = '%"approval_status":"pending"%'; 752 $queryLimit = max(200, min(2000, $safeLimit * 80)); 753 $tableName = self::getTableName(); 754 755 $rows = $wpdb->get_results( 756 $wpdb->prepare( 757 "SELECT id, rule_group, rule_id, request_method, request_host, request_uri, request_path, details, created_at 758 FROM {$tableName} 759 WHERE event_type = 'request_blocked' 760 AND rule_group IN ('waf_sqli', 'waf_command_injection', 'waf') 761 AND details LIKE %s 762 AND created_at >= %s 763 ORDER BY id DESC 764 LIMIT {$queryLimit}", 765 $likePattern, 766 $threshold 767 ), 768 ARRAY_A 769 ); 770 771 if (!is_array($rows) || !$rows) { 772 return []; 773 } 774 775 $dismissed = self::getLearningDismissedPatterns(); 776 $whitelist = $settings['waf_whitelist_paths'] ?? []; 777 if (!is_array($whitelist)) { 778 $whitelist = []; 779 } 780 781 $grouped = []; 782 783 foreach ($rows as $row) { 784 $details = self::decodeDetails((string)($row['details'] ?? '')); 785 if (($details['approval_status'] ?? '') !== 'pending') { 786 continue; 787 } 788 789 $row['details'] = $details; 790 $pattern = self::buildApprovalPattern($row); 791 if ($pattern === '') { 792 continue; 793 } 794 795 if (in_array($pattern, $whitelist, true) || in_array($pattern, $dismissed, true)) { 796 continue; 797 } 798 799 $key = strtolower($pattern); 800 if (!isset($grouped[$key])) { 801 $grouped[$key] = [ 802 'pattern' => $pattern, 803 'count' => 0, 804 'rule_group' => (string)($row['rule_group'] ?? ''), 805 'rule_id' => (string)($row['rule_id'] ?? ''), 806 'approval_action' => isset($details['approval_action']) ? self::sanitizeShortText((string) $details['approval_action'], 80) : '', 807 'approval_rest_route' => isset($details['approval_rest_route']) ? self::sanitizeShortText((string) $details['approval_rest_route'], 160) : '', 808 'request_path' => (string)($row['request_path'] ?? ''), 809 'request_uri' => (string)($row['request_uri'] ?? ''), 810 'last_seen' => (string)($row['created_at'] ?? ''), 811 ]; 812 } 813 814 $grouped[$key]['count']++; 815 816 if ((string)($row['created_at'] ?? '') > (string)$grouped[$key]['last_seen']) { 817 $grouped[$key]['last_seen'] = (string)($row['created_at'] ?? ''); 818 } 819 } 820 821 $filtered = array_filter($grouped, function ($item) use ($minHits) { 822 return (int)($item['count'] ?? 0) >= $minHits; 823 }); 824 825 if (!$filtered) { 826 return []; 827 } 828 829 $sorted = array_values($filtered); 830 usort($sorted, function ($a, $b) { 831 $countDiff = (int)($b['count'] ?? 0) - (int)($a['count'] ?? 0); 832 if ($countDiff !== 0) { 833 return $countDiff; 834 } 835 return strcmp((string)($b['last_seen'] ?? ''), (string)($a['last_seen'] ?? '')); 836 }); 837 838 $sorted = array_slice($sorted, 0, $safeLimit); 839 840 foreach ($sorted as &$item) { 841 $item['last_seen_local'] = self::toLocalDate((string)($item['last_seen'] ?? '')); 842 } 843 unset($item); 844 845 return $sorted; 846 } 847 848 public static function applyLearningSuggestion(string $pattern): array 849 { 850 $pattern = self::normalizeLearningPattern($pattern); 851 if ($pattern === '') { 852 return [ 853 'success' => false, 854 'message' => 'invalid_pattern', 855 ]; 856 } 857 858 $settings = self::getSettings(); 859 $whitelist = $settings['waf_whitelist_paths'] ?? []; 860 if (!is_array($whitelist)) { 861 $whitelist = []; 862 } 863 864 if (!in_array($pattern, $whitelist, true)) { 865 $whitelist[] = $pattern; 866 $settings = self::saveSettings([ 867 'waf_whitelist_paths' => $whitelist, 868 ]); 869 } 870 871 self::removeLearningDismissedPattern($pattern); 872 873 return [ 874 'success' => true, 875 'settings' => $settings, 876 ]; 877 } 878 879 public static function dismissLearningSuggestion(string $pattern): bool 880 { 881 $pattern = self::normalizeLearningPattern($pattern); 882 if ($pattern === '') { 883 return false; 884 } 885 886 $dismissed = self::getLearningDismissedPatterns(); 887 if (!in_array($pattern, $dismissed, true)) { 888 $dismissed[] = $pattern; 889 } 890 891 self::storeLearningDismissedPatterns($dismissed); 892 893 return true; 894 } 895 896 protected static function normalizeLearningPattern(string $pattern): string 897 { 898 $pattern = trim($pattern); 899 if ($pattern === '') { 900 return ''; 901 } 902 903 $normalized = self::sanitizeWhitelistPaths([$pattern]); 904 return $normalized[0] ?? ''; 905 } 906 907 protected static function getLearningDismissedPatterns(): array 908 { 909 $stored = get_option(self::OPTION_LEARNING_DISMISSED, []); 910 if (!is_array($stored)) { 911 $stored = []; 912 } 913 914 return self::sanitizeWhitelistPaths($stored); 915 } 916 917 protected static function storeLearningDismissedPatterns(array $patterns): void 918 { 919 $patterns = self::sanitizeWhitelistPaths($patterns); 920 if (count($patterns) > 200) { 921 $patterns = array_slice($patterns, -200); 922 } 923 924 update_option(self::OPTION_LEARNING_DISMISSED, $patterns, false); 925 } 926 927 protected static function removeLearningDismissedPattern(string $pattern): void 928 { 929 $pattern = self::normalizeLearningPattern($pattern); 930 if ($pattern === '') { 931 return; 932 } 933 934 $dismissed = self::getLearningDismissedPatterns(); 935 $filtered = array_values(array_filter($dismissed, function ($item) use ($pattern) { 936 return $item !== $pattern; 937 })); 938 939 if ($filtered !== $dismissed) { 940 self::storeLearningDismissedPatterns($filtered); 941 } 942 } 943 730 944 protected static function buildApprovalPattern(array $row): string 731 945 { … … 949 1163 } 950 1164 951 protected static function getTrustedProxyIps( array $settings = null): array1165 protected static function getTrustedProxyIps(?array $settings = null): array 952 1166 { 953 1167 $trusted = []; … … 1301 1515 'waf_command_injection_enabled' => !empty($settings['waf_command_injection_enabled']) ? 1 : 0, 1302 1516 'waf_whitelist_paths' => $wafWhitelistPaths, 1517 'learning_mode_enabled' => !empty($settings['learning_mode_enabled']) ? 1 : 0, 1518 'learning_suggestion_threshold' => max(2, min(20, (int)($settings['learning_suggestion_threshold'] ?? $defaults['learning_suggestion_threshold']))), 1519 'learning_suggestion_window_days' => max(1, min(30, (int)($settings['learning_suggestion_window_days'] ?? $defaults['learning_suggestion_window_days']))), 1303 1520 'trust_cloudflare' => !empty($settings['trust_cloudflare']) ? 1 : 0, 1304 1521 'trusted_proxies' => $trustedProxies, -
vulntitan/trunk/readme.txt
r3483644 r3483739 4 4 Tested up to: 6.9 5 5 Requires PHP: 7.4 6 Stable tag: 2.1. 96 Stable tag: 2.1.10 7 7 License: GPLv2 8 8 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 169 169 == Changelog == 170 170 171 = v2.1.10 - 16 Mar, 2026 = 172 * Added Learning Mode suggestions for WAF whitelisting, with configurable thresholds and review-only approvals. 173 * Added a Learning Suggestions panel and actions to approve or dismiss suggested patterns. 174 * Fixed a PHP 8.4 deprecation warning by making trusted proxy settings nullable explicitly. 175 171 176 = v2.1.9 - 16 Mar, 2026 = 172 177 * Added Proxy/CDN configuration in Firewall settings, including Trust Cloudflare and trusted proxy IPs. -
vulntitan/trunk/vulntitan.php
r3483644 r3483739 4 4 * Plugin URI: https://vulntitan.com/vulntitan/ 5 5 * Description: VulnTitan is a WordPress security plugin with vulnerability scanning, malware detection, file integrity monitoring, comment anti-spam protection, and a built-in firewall with WAF payload rules and login protection. 6 * Version: 2.1. 96 * Version: 2.1.10 7 7 * Author: Jaroslav Svetlik 8 8 * Author URI: https://vulntitan.com … … 30 30 31 31 // Define plugin constants 32 define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.1. 9');32 define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.1.10'); 33 33 define('VULNTITAN_PLUGIN_BASENAME', plugin_basename(__FILE__)); 34 34 define('VULNTITAN_PLUGIN_DIR', untrailingslashit(plugin_dir_path(__FILE__)));
Note: See TracChangeset
for help on using the changeset viewer.