Changeset 3481425
- Timestamp:
- 03/12/2026 07:08:56 PM (3 weeks ago)
- Location:
- vulntitan
- Files:
-
- 3 added
- 21 edited
- 4 copied
-
assets/screenshot-7.png (added)
-
tags/2.0.6 (copied) (copied from vulntitan/trunk)
-
tags/2.0.6/CHANGELOG.md (copied) (copied from vulntitan/trunk/CHANGELOG.md) (1 diff)
-
tags/2.0.6/assets/css/admin.css (modified) (7 diffs)
-
tags/2.0.6/assets/css/admin.min.css (modified) (7 diffs)
-
tags/2.0.6/assets/js/firewall.js (modified) (13 diffs)
-
tags/2.0.6/assets/js/firewall.min.js (modified) (13 diffs)
-
tags/2.0.6/includes/Admin/Admin.php (modified) (1 diff)
-
tags/2.0.6/includes/Admin/Ajax.php (modified) (4 diffs)
-
tags/2.0.6/includes/Admin/Pages/Firewall.php (modified) (1 diff)
-
tags/2.0.6/includes/Plugin.php (modified) (2 diffs)
-
tags/2.0.6/includes/Services/FirewallService.php (modified) (5 diffs)
-
tags/2.0.6/includes/Services/LoginAccessService.php (added)
-
tags/2.0.6/readme.txt (copied) (copied from vulntitan/trunk/readme.txt) (6 diffs)
-
tags/2.0.6/vulntitan.php (copied) (copied from vulntitan/trunk/vulntitan.php) (2 diffs)
-
trunk/CHANGELOG.md (modified) (1 diff)
-
trunk/assets/css/admin.css (modified) (7 diffs)
-
trunk/assets/css/admin.min.css (modified) (7 diffs)
-
trunk/assets/js/firewall.js (modified) (13 diffs)
-
trunk/assets/js/firewall.min.js (modified) (13 diffs)
-
trunk/includes/Admin/Admin.php (modified) (1 diff)
-
trunk/includes/Admin/Ajax.php (modified) (4 diffs)
-
trunk/includes/Admin/Pages/Firewall.php (modified) (1 diff)
-
trunk/includes/Plugin.php (modified) (2 diffs)
-
trunk/includes/Services/FirewallService.php (modified) (5 diffs)
-
trunk/includes/Services/LoginAccessService.php (added)
-
trunk/readme.txt (modified) (6 diffs)
-
trunk/vulntitan.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
vulntitan/tags/2.0.6/CHANGELOG.md
r3480442 r3481425 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.0.6] - 2026-03-12 9 ### Added 10 - Added configurable custom login slug support so administrators can expose a private login URL instead of the default `wp-login.php` path. 11 12 ### Changed 13 - Reworked the Firewall admin page into a tabbed settings workspace with more breathing room between controls and a dedicated full-width recent events section. 14 - Replaced inline Firewall action notices with floating toast feedback for save, refresh, clear-log, warning, and error states. 15 16 ### Security 17 - Hidden login access now blocks direct guest access to `wp-login.php` and default `wp-admin` entry points with `404` responses while preserving the custom login route. 7 18 8 19 ## [2.0.5] - 2026-03-11 -
vulntitan/tags/2.0.6/assets/css/admin.css
r3479599 r3481425 578 578 579 579 .vulntitan-firewall-feedback { 580 margin-top: 10px; 581 } 582 583 .vulntitan-firewall-notice { 584 padding: 10px 12px; 585 border-radius: 8px; 580 position: fixed; 581 top: 48px; 582 right: 20px; 583 z-index: 100000; 584 width: min(380px, calc(100vw - 32px)); 585 margin: 0; 586 pointer-events: none; 587 } 588 589 .vulntitan-firewall-toast { 590 pointer-events: auto; 591 display: grid; 592 grid-template-columns: auto minmax(0, 1fr) auto; 593 align-items: start; 594 gap: 12px; 595 padding: 14px 14px 14px 12px; 596 border-radius: 14px; 586 597 border: 1px solid #1f3346; 587 background: #101a24;598 background: rgba(10, 18, 28, 0.96); 588 599 color: #bfdbfe; 589 600 font-size: 12px; 590 line-height: 1.45; 591 } 592 593 .vulntitan-firewall-notice.is-success { 601 line-height: 1.5; 602 box-shadow: 0 18px 34px rgba(4, 9, 16, 0.38); 603 backdrop-filter: blur(14px); 604 opacity: 0; 605 transform: translateY(-8px); 606 transition: opacity 0.18s ease, transform 0.18s ease; 607 } 608 609 .vulntitan-firewall-feedback.is-visible .vulntitan-firewall-toast { 610 opacity: 1; 611 transform: translateY(0); 612 } 613 614 .vulntitan-firewall-toast-badge { 615 display: inline-flex; 616 align-items: center; 617 justify-content: center; 618 min-width: 58px; 619 min-height: 28px; 620 padding: 0 10px; 621 border-radius: 999px; 622 border: 1px solid rgba(90, 176, 255, 0.24); 623 background: rgba(90, 176, 255, 0.12); 624 color: #cfe4ff; 625 font-size: 10px; 626 font-weight: 700; 627 letter-spacing: 0.08em; 628 text-transform: uppercase; 629 } 630 631 .vulntitan-firewall-toast-message { 632 min-width: 0; 633 padding-top: 3px; 634 color: inherit; 635 } 636 637 .vulntitan-firewall-toast-close { 638 appearance: none; 639 width: 28px; 640 height: 28px; 641 border: 0; 642 border-radius: 999px; 643 background: rgba(255, 255, 255, 0.04); 644 color: #8fa7bf; 645 font-size: 12px; 646 font-weight: 700; 647 line-height: 1; 648 cursor: pointer; 649 transition: background-color 0.18s ease, color 0.18s ease; 650 } 651 652 .vulntitan-firewall-toast-close:hover, 653 .vulntitan-firewall-toast-close:focus { 654 outline: none; 655 background: rgba(255, 255, 255, 0.08); 656 color: #e2e8f0; 657 } 658 659 .vulntitan-firewall-toast.is-success { 594 660 border-color: #1f5131; 595 background: #0f1d16;661 background: rgba(11, 29, 21, 0.96); 596 662 color: #86efac; 597 663 } 598 664 599 .vulntitan-firewall-notice.is-error { 665 .vulntitan-firewall-toast.is-success .vulntitan-firewall-toast-badge { 666 border-color: rgba(91, 206, 122, 0.28); 667 background: rgba(31, 81, 49, 0.32); 668 color: #bbf7d0; 669 } 670 671 .vulntitan-firewall-toast.is-error { 600 672 border-color: #5f1f1f; 601 background: #221416;673 background: rgba(34, 20, 22, 0.97); 602 674 color: #fecaca; 603 675 } 604 676 605 .vulntitan-firewall-notice.is-warning { 677 .vulntitan-firewall-toast.is-error .vulntitan-firewall-toast-badge { 678 border-color: rgba(248, 113, 113, 0.28); 679 background: rgba(127, 29, 29, 0.28); 680 color: #fecaca; 681 } 682 683 .vulntitan-firewall-toast.is-warning { 606 684 border-color: #5f4a1f; 607 background: #221d13; 685 background: rgba(34, 29, 19, 0.97); 686 color: #fde68a; 687 } 688 689 .vulntitan-firewall-toast.is-warning .vulntitan-firewall-toast-badge { 690 border-color: rgba(245, 158, 11, 0.28); 691 background: rgba(95, 74, 31, 0.3); 608 692 color: #fde68a; 609 693 } … … 1755 1839 1756 1840 .vulntitan-wrapper--firewall .vulntitan-firewall-feedback { 1757 margin-top: 18px;1841 margin-top: 0; 1758 1842 } 1759 1843 1760 1844 .vulntitan-wrapper--firewall .vulntitan-firewall-grid { 1761 1845 margin-top: 22px; 1762 gap: 18px; 1846 grid-template-columns: minmax(0, 1fr); 1847 gap: 22px; 1763 1848 } 1764 1849 … … 1768 1853 border-radius: 16px; 1769 1854 box-shadow: 0 16px 30px rgba(6, 10, 16, 0.35); 1855 padding: 18px; 1856 } 1857 1858 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings, 1859 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 1860 min-width: 0; 1861 } 1862 1863 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings { 1864 padding: 20px; 1865 } 1866 1867 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 1868 padding: 20px; 1869 background: linear-gradient(180deg, rgba(11, 18, 28, 0.96), rgba(8, 14, 22, 0.98)); 1770 1870 } 1771 1871 1772 1872 .vulntitan-firewall-panel-head { 1773 1873 display: flex; 1774 align-items: center;1874 align-items: flex-start; 1775 1875 justify-content: space-between; 1776 1876 gap: 12px; 1777 1877 margin-bottom: 16px; 1878 flex-wrap: wrap; 1778 1879 } 1779 1880 … … 1785 1886 } 1786 1887 1888 .vulntitan-wrapper--firewall .vulntitan-firewall-workspace { 1889 display: grid; 1890 grid-template-columns: minmax(0, 1fr); 1891 gap: 18px; 1892 align-items: start; 1893 } 1894 1895 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs { 1896 display: grid; 1897 grid-template-columns: repeat(3, minmax(0, 1fr)); 1898 gap: 12px; 1899 align-content: start; 1900 } 1901 1902 .vulntitan-wrapper--firewall .vulntitan-firewall-tab { 1903 appearance: none; 1904 width: 100%; 1905 border: 1px solid rgba(90, 176, 255, 0.16); 1906 border-radius: 14px; 1907 background: linear-gradient(180deg, rgba(11, 17, 24, 0.94), rgba(7, 12, 18, 0.98)); 1908 min-height: 112px; 1909 padding: 16px 18px; 1910 text-align: left; 1911 display: grid; 1912 gap: 6px; 1913 color: var(--vt-fw-text); 1914 cursor: pointer; 1915 transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; 1916 box-shadow: 0 10px 20px rgba(6, 10, 16, 0.2); 1917 } 1918 1919 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:hover { 1920 border-color: rgba(90, 176, 255, 0.32); 1921 transform: translateY(-1px); 1922 } 1923 1924 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:focus { 1925 outline: none; 1926 box-shadow: 0 0 0 3px rgba(90, 176, 255, 0.18), 0 14px 26px rgba(6, 10, 16, 0.3); 1927 } 1928 1929 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:disabled { 1930 opacity: 0.6; 1931 cursor: wait; 1932 transform: none; 1933 } 1934 1935 .vulntitan-wrapper--firewall .vulntitan-firewall-tab.is-active { 1936 border-color: rgba(90, 176, 255, 0.38); 1937 background: linear-gradient(180deg, rgba(16, 27, 40, 0.96), rgba(9, 16, 24, 0.98)); 1938 box-shadow: inset 0 0 0 1px rgba(51, 209, 160, 0.1), 0 18px 32px rgba(6, 10, 16, 0.35); 1939 transform: translateY(-2px); 1940 } 1941 1942 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-title { 1943 font-size: 12px; 1944 font-weight: 700; 1945 letter-spacing: 0.1em; 1946 text-transform: uppercase; 1947 color: #d7e8fb; 1948 } 1949 1950 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-desc { 1951 font-size: 12px; 1952 line-height: 1.5; 1953 color: var(--vt-fw-muted); 1954 } 1955 1956 .vulntitan-wrapper--firewall .vulntitan-firewall-tab.is-active .vulntitan-firewall-tab-desc { 1957 color: #cfe4ff; 1958 } 1959 1960 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panels { 1961 min-width: 0; 1962 } 1963 1964 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel { 1965 display: grid; 1966 grid-template-columns: repeat(2, minmax(0, 1fr)); 1967 gap: 16px; 1968 min-width: 0; 1969 } 1970 1971 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel[hidden] { 1972 display: none !important; 1973 } 1974 1975 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel-head { 1976 grid-column: 1 / -1; 1977 } 1978 1979 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel .vulntitan-firewall-section + .vulntitan-firewall-section { 1980 margin-top: 0; 1981 } 1982 1983 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel > .vulntitan-firewall-section:only-of-type { 1984 grid-column: 1 / -1; 1985 } 1986 1987 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel-head { 1988 padding: 16px 18px; 1989 border-radius: 14px; 1990 border: 1px solid rgba(90, 176, 255, 0.16); 1991 background: linear-gradient(135deg, rgba(51, 209, 160, 0.08), rgba(90, 176, 255, 0.08)), rgba(9, 14, 20, 0.72); 1992 display: grid; 1993 gap: 8px; 1994 } 1995 1787 1996 .vulntitan-firewall-section { 1788 padding: 1 2px 14px;1789 border-radius: 1 2px;1997 padding: 16px 18px; 1998 border-radius: 14px; 1790 1999 border: 1px solid rgba(90, 176, 255, 0.18); 1791 2000 background: rgba(9, 14, 20, 0.7); 1792 2001 display: grid; 1793 gap: 1 0px;2002 gap: 12px; 1794 2003 } 1795 2004 … … 1833 2042 .vulntitan-wrapper--firewall .vulntitan-firewall-field-help { 1834 2043 color: var(--vt-fw-muted); 2044 } 2045 2046 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid { 2047 display: grid; 2048 grid-template-columns: repeat(3, minmax(0, 1fr)); 2049 gap: 14px; 2050 } 2051 2052 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid .vulntitan-firewall-input { 2053 max-width: none; 1835 2054 } 1836 2055 … … 1866 2085 1867 2086 .vulntitan-wrapper--firewall .vulntitan-firewall-log-list { 1868 background: rgba(9, 14, 20, 0.5); 1869 border-radius: 12px; 1870 padding: 6px; 2087 margin-top: 0; 2088 max-height: 720px; 2089 background: transparent; 2090 border-radius: 0; 2091 padding: 0; 2092 gap: 14px; 1871 2093 } 1872 2094 1873 2095 .vulntitan-wrapper--firewall .vulntitan-firewall-log-item { 1874 border-radius: 12px; 1875 border: 1px solid rgba(90, 176, 255, 0.12); 1876 background: rgba(11, 17, 24, 0.7); 2096 border-radius: 16px; 2097 border: 1px solid rgba(90, 176, 255, 0.14); 2098 background: linear-gradient(180deg, rgba(8, 14, 21, 0.96), rgba(6, 11, 18, 0.98)); 2099 padding: 16px 18px; 2100 gap: 10px; 2101 box-shadow: inset 0 0 0 1px rgba(51, 209, 160, 0.03); 1877 2102 } 1878 2103 1879 2104 .vulntitan-wrapper--firewall .vulntitan-firewall-log-request { 1880 2105 color: #cde2f7; 2106 font-size: 14px; 2107 line-height: 1.5; 2108 } 2109 2110 .vulntitan-wrapper--firewall .vulntitan-firewall-log-meta { 2111 gap: 10px 18px; 2112 } 2113 2114 .vulntitan-wrapper--firewall .vulntitan-firewall-log-meta-item { 2115 font-size: 12px; 2116 } 2117 2118 .vulntitan-wrapper--firewall .vulntitan-firewall-log-reason { 2119 font-size: 13px; 2120 color: #d4dfeb; 1881 2121 } 1882 2122 … … 1885 2125 grid-template-columns: 1fr; 1886 2126 } 2127 2128 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel { 2129 grid-template-columns: 1fr; 2130 } 2131 } 2132 2133 @media (max-width: 1280px) { 2134 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs { 2135 grid-template-columns: repeat(2, minmax(0, 1fr)); 2136 } 1887 2137 } 1888 2138 1889 2139 @media (max-width: 782px) { 2140 .vulntitan-firewall-feedback { 2141 top: 58px; 2142 right: 12px; 2143 width: calc(100vw - 24px); 2144 } 2145 2146 .vulntitan-firewall-toast { 2147 grid-template-columns: 1fr auto; 2148 gap: 10px; 2149 } 2150 2151 .vulntitan-firewall-toast-badge { 2152 grid-column: 1 / 2; 2153 justify-self: start; 2154 } 2155 2156 .vulntitan-firewall-toast-message { 2157 grid-column: 1 / 2; 2158 padding-top: 0; 2159 } 2160 2161 .vulntitan-firewall-toast-close { 2162 grid-column: 2 / 3; 2163 grid-row: 1 / 2; 2164 } 2165 1890 2166 .vulntitan-wrapper--firewall { 1891 2167 padding: 1.25rem; 1892 2168 } 1893 } 2169 2170 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs, 2171 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid { 2172 grid-template-columns: 1fr; 2173 } 2174 2175 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings, 2176 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 2177 padding: 16px; 2178 } 2179 } -
vulntitan/tags/2.0.6/assets/css/admin.min.css
r3479599 r3481425 578 578 579 579 .vulntitan-firewall-feedback { 580 margin-top: 10px; 581 } 582 583 .vulntitan-firewall-notice { 584 padding: 10px 12px; 585 border-radius: 8px; 580 position: fixed; 581 top: 48px; 582 right: 20px; 583 z-index: 100000; 584 width: min(380px, calc(100vw - 32px)); 585 margin: 0; 586 pointer-events: none; 587 } 588 589 .vulntitan-firewall-toast { 590 pointer-events: auto; 591 display: grid; 592 grid-template-columns: auto minmax(0, 1fr) auto; 593 align-items: start; 594 gap: 12px; 595 padding: 14px 14px 14px 12px; 596 border-radius: 14px; 586 597 border: 1px solid #1f3346; 587 background: #101a24;598 background: rgba(10, 18, 28, 0.96); 588 599 color: #bfdbfe; 589 600 font-size: 12px; 590 line-height: 1.45; 591 } 592 593 .vulntitan-firewall-notice.is-success { 601 line-height: 1.5; 602 box-shadow: 0 18px 34px rgba(4, 9, 16, 0.38); 603 backdrop-filter: blur(14px); 604 opacity: 0; 605 transform: translateY(-8px); 606 transition: opacity 0.18s ease, transform 0.18s ease; 607 } 608 609 .vulntitan-firewall-feedback.is-visible .vulntitan-firewall-toast { 610 opacity: 1; 611 transform: translateY(0); 612 } 613 614 .vulntitan-firewall-toast-badge { 615 display: inline-flex; 616 align-items: center; 617 justify-content: center; 618 min-width: 58px; 619 min-height: 28px; 620 padding: 0 10px; 621 border-radius: 999px; 622 border: 1px solid rgba(90, 176, 255, 0.24); 623 background: rgba(90, 176, 255, 0.12); 624 color: #cfe4ff; 625 font-size: 10px; 626 font-weight: 700; 627 letter-spacing: 0.08em; 628 text-transform: uppercase; 629 } 630 631 .vulntitan-firewall-toast-message { 632 min-width: 0; 633 padding-top: 3px; 634 color: inherit; 635 } 636 637 .vulntitan-firewall-toast-close { 638 appearance: none; 639 width: 28px; 640 height: 28px; 641 border: 0; 642 border-radius: 999px; 643 background: rgba(255, 255, 255, 0.04); 644 color: #8fa7bf; 645 font-size: 12px; 646 font-weight: 700; 647 line-height: 1; 648 cursor: pointer; 649 transition: background-color 0.18s ease, color 0.18s ease; 650 } 651 652 .vulntitan-firewall-toast-close:hover, 653 .vulntitan-firewall-toast-close:focus { 654 outline: none; 655 background: rgba(255, 255, 255, 0.08); 656 color: #e2e8f0; 657 } 658 659 .vulntitan-firewall-toast.is-success { 594 660 border-color: #1f5131; 595 background: #0f1d16;661 background: rgba(11, 29, 21, 0.96); 596 662 color: #86efac; 597 663 } 598 664 599 .vulntitan-firewall-notice.is-error { 665 .vulntitan-firewall-toast.is-success .vulntitan-firewall-toast-badge { 666 border-color: rgba(91, 206, 122, 0.28); 667 background: rgba(31, 81, 49, 0.32); 668 color: #bbf7d0; 669 } 670 671 .vulntitan-firewall-toast.is-error { 600 672 border-color: #5f1f1f; 601 background: #221416;673 background: rgba(34, 20, 22, 0.97); 602 674 color: #fecaca; 603 675 } 604 676 605 .vulntitan-firewall-notice.is-warning { 677 .vulntitan-firewall-toast.is-error .vulntitan-firewall-toast-badge { 678 border-color: rgba(248, 113, 113, 0.28); 679 background: rgba(127, 29, 29, 0.28); 680 color: #fecaca; 681 } 682 683 .vulntitan-firewall-toast.is-warning { 606 684 border-color: #5f4a1f; 607 background: #221d13; 685 background: rgba(34, 29, 19, 0.97); 686 color: #fde68a; 687 } 688 689 .vulntitan-firewall-toast.is-warning .vulntitan-firewall-toast-badge { 690 border-color: rgba(245, 158, 11, 0.28); 691 background: rgba(95, 74, 31, 0.3); 608 692 color: #fde68a; 609 693 } … … 1755 1839 1756 1840 .vulntitan-wrapper--firewall .vulntitan-firewall-feedback { 1757 margin-top: 18px;1841 margin-top: 0; 1758 1842 } 1759 1843 1760 1844 .vulntitan-wrapper--firewall .vulntitan-firewall-grid { 1761 1845 margin-top: 22px; 1762 gap: 18px; 1846 grid-template-columns: minmax(0, 1fr); 1847 gap: 22px; 1763 1848 } 1764 1849 … … 1768 1853 border-radius: 16px; 1769 1854 box-shadow: 0 16px 30px rgba(6, 10, 16, 0.35); 1855 padding: 18px; 1856 } 1857 1858 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings, 1859 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 1860 min-width: 0; 1861 } 1862 1863 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings { 1864 padding: 20px; 1865 } 1866 1867 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 1868 padding: 20px; 1869 background: linear-gradient(180deg, rgba(11, 18, 28, 0.96), rgba(8, 14, 22, 0.98)); 1770 1870 } 1771 1871 1772 1872 .vulntitan-firewall-panel-head { 1773 1873 display: flex; 1774 align-items: center;1874 align-items: flex-start; 1775 1875 justify-content: space-between; 1776 1876 gap: 12px; 1777 1877 margin-bottom: 16px; 1878 flex-wrap: wrap; 1778 1879 } 1779 1880 … … 1785 1886 } 1786 1887 1888 .vulntitan-wrapper--firewall .vulntitan-firewall-workspace { 1889 display: grid; 1890 grid-template-columns: minmax(0, 1fr); 1891 gap: 18px; 1892 align-items: start; 1893 } 1894 1895 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs { 1896 display: grid; 1897 grid-template-columns: repeat(3, minmax(0, 1fr)); 1898 gap: 12px; 1899 align-content: start; 1900 } 1901 1902 .vulntitan-wrapper--firewall .vulntitan-firewall-tab { 1903 appearance: none; 1904 width: 100%; 1905 border: 1px solid rgba(90, 176, 255, 0.16); 1906 border-radius: 14px; 1907 background: linear-gradient(180deg, rgba(11, 17, 24, 0.94), rgba(7, 12, 18, 0.98)); 1908 min-height: 112px; 1909 padding: 16px 18px; 1910 text-align: left; 1911 display: grid; 1912 gap: 6px; 1913 color: var(--vt-fw-text); 1914 cursor: pointer; 1915 transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; 1916 box-shadow: 0 10px 20px rgba(6, 10, 16, 0.2); 1917 } 1918 1919 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:hover { 1920 border-color: rgba(90, 176, 255, 0.32); 1921 transform: translateY(-1px); 1922 } 1923 1924 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:focus { 1925 outline: none; 1926 box-shadow: 0 0 0 3px rgba(90, 176, 255, 0.18), 0 14px 26px rgba(6, 10, 16, 0.3); 1927 } 1928 1929 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:disabled { 1930 opacity: 0.6; 1931 cursor: wait; 1932 transform: none; 1933 } 1934 1935 .vulntitan-wrapper--firewall .vulntitan-firewall-tab.is-active { 1936 border-color: rgba(90, 176, 255, 0.38); 1937 background: linear-gradient(180deg, rgba(16, 27, 40, 0.96), rgba(9, 16, 24, 0.98)); 1938 box-shadow: inset 0 0 0 1px rgba(51, 209, 160, 0.1), 0 18px 32px rgba(6, 10, 16, 0.35); 1939 transform: translateY(-2px); 1940 } 1941 1942 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-title { 1943 font-size: 12px; 1944 font-weight: 700; 1945 letter-spacing: 0.1em; 1946 text-transform: uppercase; 1947 color: #d7e8fb; 1948 } 1949 1950 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-desc { 1951 font-size: 12px; 1952 line-height: 1.5; 1953 color: var(--vt-fw-muted); 1954 } 1955 1956 .vulntitan-wrapper--firewall .vulntitan-firewall-tab.is-active .vulntitan-firewall-tab-desc { 1957 color: #cfe4ff; 1958 } 1959 1960 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panels { 1961 min-width: 0; 1962 } 1963 1964 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel { 1965 display: grid; 1966 grid-template-columns: repeat(2, minmax(0, 1fr)); 1967 gap: 16px; 1968 min-width: 0; 1969 } 1970 1971 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel[hidden] { 1972 display: none !important; 1973 } 1974 1975 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel-head { 1976 grid-column: 1 / -1; 1977 } 1978 1979 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel .vulntitan-firewall-section + .vulntitan-firewall-section { 1980 margin-top: 0; 1981 } 1982 1983 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel > .vulntitan-firewall-section:only-of-type { 1984 grid-column: 1 / -1; 1985 } 1986 1987 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel-head { 1988 padding: 16px 18px; 1989 border-radius: 14px; 1990 border: 1px solid rgba(90, 176, 255, 0.16); 1991 background: linear-gradient(135deg, rgba(51, 209, 160, 0.08), rgba(90, 176, 255, 0.08)), rgba(9, 14, 20, 0.72); 1992 display: grid; 1993 gap: 8px; 1994 } 1995 1787 1996 .vulntitan-firewall-section { 1788 padding: 1 2px 14px;1789 border-radius: 1 2px;1997 padding: 16px 18px; 1998 border-radius: 14px; 1790 1999 border: 1px solid rgba(90, 176, 255, 0.18); 1791 2000 background: rgba(9, 14, 20, 0.7); 1792 2001 display: grid; 1793 gap: 1 0px;2002 gap: 12px; 1794 2003 } 1795 2004 … … 1833 2042 .vulntitan-wrapper--firewall .vulntitan-firewall-field-help { 1834 2043 color: var(--vt-fw-muted); 2044 } 2045 2046 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid { 2047 display: grid; 2048 grid-template-columns: repeat(3, minmax(0, 1fr)); 2049 gap: 14px; 2050 } 2051 2052 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid .vulntitan-firewall-input { 2053 max-width: none; 1835 2054 } 1836 2055 … … 1866 2085 1867 2086 .vulntitan-wrapper--firewall .vulntitan-firewall-log-list { 1868 background: rgba(9, 14, 20, 0.5); 1869 border-radius: 12px; 1870 padding: 6px; 2087 margin-top: 0; 2088 max-height: 720px; 2089 background: transparent; 2090 border-radius: 0; 2091 padding: 0; 2092 gap: 14px; 1871 2093 } 1872 2094 1873 2095 .vulntitan-wrapper--firewall .vulntitan-firewall-log-item { 1874 border-radius: 12px; 1875 border: 1px solid rgba(90, 176, 255, 0.12); 1876 background: rgba(11, 17, 24, 0.7); 2096 border-radius: 16px; 2097 border: 1px solid rgba(90, 176, 255, 0.14); 2098 background: linear-gradient(180deg, rgba(8, 14, 21, 0.96), rgba(6, 11, 18, 0.98)); 2099 padding: 16px 18px; 2100 gap: 10px; 2101 box-shadow: inset 0 0 0 1px rgba(51, 209, 160, 0.03); 1877 2102 } 1878 2103 1879 2104 .vulntitan-wrapper--firewall .vulntitan-firewall-log-request { 1880 2105 color: #cde2f7; 2106 font-size: 14px; 2107 line-height: 1.5; 2108 } 2109 2110 .vulntitan-wrapper--firewall .vulntitan-firewall-log-meta { 2111 gap: 10px 18px; 2112 } 2113 2114 .vulntitan-wrapper--firewall .vulntitan-firewall-log-meta-item { 2115 font-size: 12px; 2116 } 2117 2118 .vulntitan-wrapper--firewall .vulntitan-firewall-log-reason { 2119 font-size: 13px; 2120 color: #d4dfeb; 1881 2121 } 1882 2122 … … 1885 2125 grid-template-columns: 1fr; 1886 2126 } 2127 2128 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel { 2129 grid-template-columns: 1fr; 2130 } 2131 } 2132 2133 @media (max-width: 1280px) { 2134 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs { 2135 grid-template-columns: repeat(2, minmax(0, 1fr)); 2136 } 1887 2137 } 1888 2138 1889 2139 @media (max-width: 782px) { 2140 .vulntitan-firewall-feedback { 2141 top: 58px; 2142 right: 12px; 2143 width: calc(100vw - 24px); 2144 } 2145 2146 .vulntitan-firewall-toast { 2147 grid-template-columns: 1fr auto; 2148 gap: 10px; 2149 } 2150 2151 .vulntitan-firewall-toast-badge { 2152 grid-column: 1 / 2; 2153 justify-self: start; 2154 } 2155 2156 .vulntitan-firewall-toast-message { 2157 grid-column: 1 / 2; 2158 padding-top: 0; 2159 } 2160 2161 .vulntitan-firewall-toast-close { 2162 grid-column: 2 / 3; 2163 grid-row: 1 / 2; 2164 } 2165 1890 2166 .vulntitan-wrapper--firewall { 1891 2167 padding: 1.25rem; 1892 2168 } 1893 } 2169 2170 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs, 2171 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid { 2172 grid-template-columns: 1fr; 2173 } 2174 2175 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings, 2176 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 2177 padding: 16px; 2178 } 2179 } -
vulntitan/tags/2.0.6/assets/js/firewall.js
r3469591 r3481425 11 11 const $firewallEnabled = $('#vulntitan-firewall-enabled'); 12 12 const $firewallLoginProtection = $('#vulntitan-firewall-login-protection'); 13 const $firewallCustomLoginSlug = $('#vulntitan-firewall-custom-login-slug'); 14 const $firewallCustomLoginUrl = $('#vulntitan-firewall-custom-login-url'); 13 15 const $firewallWafSqliEnabled = $('#vulntitan-firewall-waf-sqli-enabled'); 14 16 const $firewallWafCommandEnabled = $('#vulntitan-firewall-waf-command-enabled'); … … 20 22 const $firewallRefresh = $('#vulntitan-firewall-refresh'); 21 23 const $firewallClearLogs = $('#vulntitan-firewall-clear-logs'); 24 const $firewallTabs = $firewallRoot.find('[data-firewall-tab]'); 25 const $firewallTabPanels = $firewallRoot.find('[data-firewall-tab-panel]'); 22 26 const i18n = (window.VulnTitan && window.VulnTitan.i18n) ? window.VulnTitan.i18n : {}; 23 27 let latestMuLoaderStatus = null; 28 let feedbackTimer = null; 29 let feedbackHideTimer = null; 24 30 25 31 function escapeHtml(str) { … … 35 41 } 36 42 43 function clearFeedbackTimers() { 44 if (feedbackTimer) { 45 window.clearTimeout(feedbackTimer); 46 feedbackTimer = null; 47 } 48 49 if (feedbackHideTimer) { 50 window.clearTimeout(feedbackHideTimer); 51 feedbackHideTimer = null; 52 } 53 } 54 55 function dismissFeedback() { 56 clearFeedbackTimers(); 57 58 $firewallFeedback.removeClass('is-visible'); 59 feedbackHideTimer = window.setTimeout(function () { 60 $firewallFeedback.hide().empty(); 61 feedbackHideTimer = null; 62 }, 180); 63 } 64 65 function getFeedbackConfig(type) { 66 switch (type) { 67 case 'success': 68 return { 69 label: i18n.firewall_toast_success || 'Saved', 70 duration: 3200 71 }; 72 case 'warning': 73 return { 74 label: i18n.firewall_toast_warning || 'Review', 75 duration: 5200 76 }; 77 case 'error': 78 return { 79 label: i18n.firewall_toast_error || 'Error', 80 duration: 6200 81 }; 82 default: 83 return { 84 label: i18n.firewall_toast_info || 'Working', 85 duration: 0 86 }; 87 } 88 } 89 37 90 function setFeedback(type, message) { 38 91 if (!message) { 39 $firewallFeedback.hide().empty();92 dismissFeedback(); 40 93 return; 41 94 } 42 95 43 96 const normalizedType = (type === 'success' || type === 'error' || type === 'warning') ? type : 'info'; 97 const config = getFeedbackConfig(normalizedType); 98 const liveMode = normalizedType === 'error' ? 'assertive' : 'polite'; 99 100 clearFeedbackTimers(); 44 101 45 102 $firewallFeedback 46 .html(`<div class="vulntitan-firewall-notice is-${normalizedType}">${escapeHtml(message)}</div>`) 103 .html(` 104 <div class="vulntitan-firewall-toast is-${normalizedType}" role="status" aria-live="${liveMode}"> 105 <span class="vulntitan-firewall-toast-badge">${escapeHtml(config.label)}</span> 106 <div class="vulntitan-firewall-toast-message">${escapeHtml(message)}</div> 107 <button type="button" class="vulntitan-firewall-toast-close" data-firewall-toast-close aria-label="${escapeHtml(i18n.firewall_toast_close || 'Dismiss notification')}">x</button> 108 </div> 109 `) 47 110 .show(); 111 112 window.requestAnimationFrame(function () { 113 $firewallFeedback.addClass('is-visible'); 114 }); 115 116 if (config.duration > 0) { 117 feedbackTimer = window.setTimeout(function () { 118 dismissFeedback(); 119 }, config.duration); 120 } 48 121 } 49 122 … … 122 195 } 123 196 197 function renderLoginAccess(loginAccess) { 198 const data = loginAccess || {}; 199 const customUrl = String(data.url || '').trim(); 200 201 if (!customUrl) { 202 $firewallCustomLoginUrl.text( 203 i18n.firewall_custom_login_disabled || 'Custom login URL is disabled. Leave the slug blank to keep the default WordPress login endpoints.' 204 ); 205 return; 206 } 207 208 $firewallCustomLoginUrl.text(customUrl); 209 } 210 211 function activateTab(tabId) { 212 const normalizedTab = String(tabId || '').trim(); 213 if (!normalizedTab) { 214 return; 215 } 216 217 $firewallTabs.each(function () { 218 const $tab = $(this); 219 const isActive = String($tab.data('firewallTab')) === normalizedTab; 220 221 $tab 222 .toggleClass('is-active', isActive) 223 .attr('aria-selected', isActive ? 'true' : 'false') 224 .attr('tabindex', isActive ? '0' : '-1'); 225 }); 226 227 $firewallTabPanels.each(function () { 228 const $panel = $(this); 229 const isActive = String($panel.data('firewallTabPanel')) === normalizedTab; 230 231 $panel 232 .toggleClass('is-active', isActive) 233 .prop('hidden', !isActive); 234 }); 235 } 236 124 237 function applySettings(settings) { 125 238 const data = settings || {}; … … 127 240 $firewallEnabled.prop('checked', !!Number(data.enabled || 0)); 128 241 $firewallLoginProtection.prop('checked', !!Number(data.login_protection_enabled || 0)); 242 $firewallCustomLoginSlug.val(String(data.custom_login_slug || '')); 129 243 $firewallWafSqliEnabled.prop('checked', !!Number(data.waf_sqli_enabled || 0)); 130 244 $firewallWafCommandEnabled.prop('checked', !!Number(data.waf_command_injection_enabled || 0)); … … 183 297 $firewallRefresh.prop('disabled', isBusy); 184 298 $firewallClearLogs.prop('disabled', isBusy); 299 $firewallTabs.prop('disabled', isBusy); 185 300 $firewallEnabled.prop('disabled', isBusy); 186 301 $firewallLoginProtection.prop('disabled', isBusy); 302 $firewallCustomLoginSlug.prop('disabled', isBusy); 187 303 $firewallWafSqliEnabled.prop('disabled', isBusy); 188 304 $firewallWafCommandEnabled.prop('disabled', isBusy); … … 197 313 198 314 if (showLoadingNotice) { 199 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');315 setFeedback('info', i18n.firewall_refreshing || 'Refreshing firewall data...'); 200 316 } 201 317 … … 215 331 latestMuLoaderStatus = payload.mu_loader || null; 216 332 applySettings(payload.settings || {}); 333 renderLoginAccess(payload.login_access || {}); 217 334 renderOverview(payload.summary || {}, latestMuLoaderStatus || {}); 218 335 renderLogs(payload.logs || []); … … 227 344 function saveSettings() { 228 345 setBusyState(true); 229 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');346 setFeedback('info', i18n.firewall_saving || 'Saving firewall settings...'); 230 347 231 348 $.post(VulnTitan.ajaxUrl, { … … 234 351 enabled: $firewallEnabled.is(':checked') ? 1 : 0, 235 352 login_protection_enabled: $firewallLoginProtection.is(':checked') ? 1 : 0, 353 custom_login_slug: String($firewallCustomLoginSlug.val() || ''), 236 354 waf_sqli_enabled: $firewallWafSqliEnabled.is(':checked') ? 1 : 0, 237 355 waf_command_injection_enabled: $firewallWafCommandEnabled.is(':checked') ? 1 : 0, … … 252 370 latestMuLoaderStatus = payload.mu_loader || latestMuLoaderStatus; 253 371 applySettings(payload.settings || {}); 372 renderLoginAccess(payload.login_access || {}); 254 373 renderOverview(payload.summary || {}, latestMuLoaderStatus || {}); 255 374 renderLogs(payload.logs || []); 256 375 257 376 const muInstall = payload.mu_loader_install || {}; 377 if (payload.notice) { 378 setFeedback('warning', payload.notice); 379 return; 380 } 381 258 382 if (muInstall.success === false && muInstall.error) { 259 383 setFeedback('warning', muInstall.error); … … 276 400 277 401 setBusyState(true); 278 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');402 setFeedback('info', i18n.firewall_clearing || 'Clearing firewall logs...'); 279 403 280 404 $.post(VulnTitan.ajaxUrl, { … … 313 437 }); 314 438 315 loadFirewallData(true); 439 $firewallFeedback.off('click', '[data-firewall-toast-close]').on('click', '[data-firewall-toast-close]', function () { 440 dismissFeedback(); 441 }); 442 443 $firewallTabs.off('click').on('click', function () { 444 activateTab($(this).data('firewallTab')); 445 }); 446 447 $firewallTabs.off('keydown').on('keydown', function (event) { 448 const keys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']; 449 if (keys.indexOf(event.key) === -1) { 450 return; 451 } 452 453 event.preventDefault(); 454 455 const currentIndex = $firewallTabs.index(this); 456 if (currentIndex < 0) { 457 return; 458 } 459 460 let nextIndex = currentIndex; 461 462 if (event.key === 'Home') { 463 nextIndex = 0; 464 } else if (event.key === 'End') { 465 nextIndex = $firewallTabs.length - 1; 466 } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { 467 nextIndex = currentIndex === 0 ? $firewallTabs.length - 1 : currentIndex - 1; 468 } else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { 469 nextIndex = currentIndex === $firewallTabs.length - 1 ? 0 : currentIndex + 1; 470 } 471 472 const $nextTab = $firewallTabs.eq(nextIndex); 473 activateTab($nextTab.data('firewallTab')); 474 $nextTab.trigger('focus'); 475 }); 476 477 activateTab($firewallTabs.filter('.is-active').first().data('firewallTab') || $firewallTabs.first().data('firewallTab')); 478 479 loadFirewallData(false); 316 480 }); -
vulntitan/tags/2.0.6/assets/js/firewall.min.js
r3469591 r3481425 11 11 const $firewallEnabled = $('#vulntitan-firewall-enabled'); 12 12 const $firewallLoginProtection = $('#vulntitan-firewall-login-protection'); 13 const $firewallCustomLoginSlug = $('#vulntitan-firewall-custom-login-slug'); 14 const $firewallCustomLoginUrl = $('#vulntitan-firewall-custom-login-url'); 13 15 const $firewallWafSqliEnabled = $('#vulntitan-firewall-waf-sqli-enabled'); 14 16 const $firewallWafCommandEnabled = $('#vulntitan-firewall-waf-command-enabled'); … … 20 22 const $firewallRefresh = $('#vulntitan-firewall-refresh'); 21 23 const $firewallClearLogs = $('#vulntitan-firewall-clear-logs'); 24 const $firewallTabs = $firewallRoot.find('[data-firewall-tab]'); 25 const $firewallTabPanels = $firewallRoot.find('[data-firewall-tab-panel]'); 22 26 const i18n = (window.VulnTitan && window.VulnTitan.i18n) ? window.VulnTitan.i18n : {}; 23 27 let latestMuLoaderStatus = null; 28 let feedbackTimer = null; 29 let feedbackHideTimer = null; 24 30 25 31 function escapeHtml(str) { … … 35 41 } 36 42 43 function clearFeedbackTimers() { 44 if (feedbackTimer) { 45 window.clearTimeout(feedbackTimer); 46 feedbackTimer = null; 47 } 48 49 if (feedbackHideTimer) { 50 window.clearTimeout(feedbackHideTimer); 51 feedbackHideTimer = null; 52 } 53 } 54 55 function dismissFeedback() { 56 clearFeedbackTimers(); 57 58 $firewallFeedback.removeClass('is-visible'); 59 feedbackHideTimer = window.setTimeout(function () { 60 $firewallFeedback.hide().empty(); 61 feedbackHideTimer = null; 62 }, 180); 63 } 64 65 function getFeedbackConfig(type) { 66 switch (type) { 67 case 'success': 68 return { 69 label: i18n.firewall_toast_success || 'Saved', 70 duration: 3200 71 }; 72 case 'warning': 73 return { 74 label: i18n.firewall_toast_warning || 'Review', 75 duration: 5200 76 }; 77 case 'error': 78 return { 79 label: i18n.firewall_toast_error || 'Error', 80 duration: 6200 81 }; 82 default: 83 return { 84 label: i18n.firewall_toast_info || 'Working', 85 duration: 0 86 }; 87 } 88 } 89 37 90 function setFeedback(type, message) { 38 91 if (!message) { 39 $firewallFeedback.hide().empty();92 dismissFeedback(); 40 93 return; 41 94 } 42 95 43 96 const normalizedType = (type === 'success' || type === 'error' || type === 'warning') ? type : 'info'; 97 const config = getFeedbackConfig(normalizedType); 98 const liveMode = normalizedType === 'error' ? 'assertive' : 'polite'; 99 100 clearFeedbackTimers(); 44 101 45 102 $firewallFeedback 46 .html(`<div class="vulntitan-firewall-notice is-${normalizedType}">${escapeHtml(message)}</div>`) 103 .html(` 104 <div class="vulntitan-firewall-toast is-${normalizedType}" role="status" aria-live="${liveMode}"> 105 <span class="vulntitan-firewall-toast-badge">${escapeHtml(config.label)}</span> 106 <div class="vulntitan-firewall-toast-message">${escapeHtml(message)}</div> 107 <button type="button" class="vulntitan-firewall-toast-close" data-firewall-toast-close aria-label="${escapeHtml(i18n.firewall_toast_close || 'Dismiss notification')}">x</button> 108 </div> 109 `) 47 110 .show(); 111 112 window.requestAnimationFrame(function () { 113 $firewallFeedback.addClass('is-visible'); 114 }); 115 116 if (config.duration > 0) { 117 feedbackTimer = window.setTimeout(function () { 118 dismissFeedback(); 119 }, config.duration); 120 } 48 121 } 49 122 … … 122 195 } 123 196 197 function renderLoginAccess(loginAccess) { 198 const data = loginAccess || {}; 199 const customUrl = String(data.url || '').trim(); 200 201 if (!customUrl) { 202 $firewallCustomLoginUrl.text( 203 i18n.firewall_custom_login_disabled || 'Custom login URL is disabled. Leave the slug blank to keep the default WordPress login endpoints.' 204 ); 205 return; 206 } 207 208 $firewallCustomLoginUrl.text(customUrl); 209 } 210 211 function activateTab(tabId) { 212 const normalizedTab = String(tabId || '').trim(); 213 if (!normalizedTab) { 214 return; 215 } 216 217 $firewallTabs.each(function () { 218 const $tab = $(this); 219 const isActive = String($tab.data('firewallTab')) === normalizedTab; 220 221 $tab 222 .toggleClass('is-active', isActive) 223 .attr('aria-selected', isActive ? 'true' : 'false') 224 .attr('tabindex', isActive ? '0' : '-1'); 225 }); 226 227 $firewallTabPanels.each(function () { 228 const $panel = $(this); 229 const isActive = String($panel.data('firewallTabPanel')) === normalizedTab; 230 231 $panel 232 .toggleClass('is-active', isActive) 233 .prop('hidden', !isActive); 234 }); 235 } 236 124 237 function applySettings(settings) { 125 238 const data = settings || {}; … … 127 240 $firewallEnabled.prop('checked', !!Number(data.enabled || 0)); 128 241 $firewallLoginProtection.prop('checked', !!Number(data.login_protection_enabled || 0)); 242 $firewallCustomLoginSlug.val(String(data.custom_login_slug || '')); 129 243 $firewallWafSqliEnabled.prop('checked', !!Number(data.waf_sqli_enabled || 0)); 130 244 $firewallWafCommandEnabled.prop('checked', !!Number(data.waf_command_injection_enabled || 0)); … … 183 297 $firewallRefresh.prop('disabled', isBusy); 184 298 $firewallClearLogs.prop('disabled', isBusy); 299 $firewallTabs.prop('disabled', isBusy); 185 300 $firewallEnabled.prop('disabled', isBusy); 186 301 $firewallLoginProtection.prop('disabled', isBusy); 302 $firewallCustomLoginSlug.prop('disabled', isBusy); 187 303 $firewallWafSqliEnabled.prop('disabled', isBusy); 188 304 $firewallWafCommandEnabled.prop('disabled', isBusy); … … 197 313 198 314 if (showLoadingNotice) { 199 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');315 setFeedback('info', i18n.firewall_refreshing || 'Refreshing firewall data...'); 200 316 } 201 317 … … 215 331 latestMuLoaderStatus = payload.mu_loader || null; 216 332 applySettings(payload.settings || {}); 333 renderLoginAccess(payload.login_access || {}); 217 334 renderOverview(payload.summary || {}, latestMuLoaderStatus || {}); 218 335 renderLogs(payload.logs || []); … … 227 344 function saveSettings() { 228 345 setBusyState(true); 229 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');346 setFeedback('info', i18n.firewall_saving || 'Saving firewall settings...'); 230 347 231 348 $.post(VulnTitan.ajaxUrl, { … … 234 351 enabled: $firewallEnabled.is(':checked') ? 1 : 0, 235 352 login_protection_enabled: $firewallLoginProtection.is(':checked') ? 1 : 0, 353 custom_login_slug: String($firewallCustomLoginSlug.val() || ''), 236 354 waf_sqli_enabled: $firewallWafSqliEnabled.is(':checked') ? 1 : 0, 237 355 waf_command_injection_enabled: $firewallWafCommandEnabled.is(':checked') ? 1 : 0, … … 252 370 latestMuLoaderStatus = payload.mu_loader || latestMuLoaderStatus; 253 371 applySettings(payload.settings || {}); 372 renderLoginAccess(payload.login_access || {}); 254 373 renderOverview(payload.summary || {}, latestMuLoaderStatus || {}); 255 374 renderLogs(payload.logs || []); 256 375 257 376 const muInstall = payload.mu_loader_install || {}; 377 if (payload.notice) { 378 setFeedback('warning', payload.notice); 379 return; 380 } 381 258 382 if (muInstall.success === false && muInstall.error) { 259 383 setFeedback('warning', muInstall.error); … … 276 400 277 401 setBusyState(true); 278 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');402 setFeedback('info', i18n.firewall_clearing || 'Clearing firewall logs...'); 279 403 280 404 $.post(VulnTitan.ajaxUrl, { … … 313 437 }); 314 438 315 loadFirewallData(true); 439 $firewallFeedback.off('click', '[data-firewall-toast-close]').on('click', '[data-firewall-toast-close]', function () { 440 dismissFeedback(); 441 }); 442 443 $firewallTabs.off('click').on('click', function () { 444 activateTab($(this).data('firewallTab')); 445 }); 446 447 $firewallTabs.off('keydown').on('keydown', function (event) { 448 const keys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']; 449 if (keys.indexOf(event.key) === -1) { 450 return; 451 } 452 453 event.preventDefault(); 454 455 const currentIndex = $firewallTabs.index(this); 456 if (currentIndex < 0) { 457 return; 458 } 459 460 let nextIndex = currentIndex; 461 462 if (event.key === 'Home') { 463 nextIndex = 0; 464 } else if (event.key === 'End') { 465 nextIndex = $firewallTabs.length - 1; 466 } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { 467 nextIndex = currentIndex === 0 ? $firewallTabs.length - 1 : currentIndex - 1; 468 } else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { 469 nextIndex = currentIndex === $firewallTabs.length - 1 ? 0 : currentIndex + 1; 470 } 471 472 const $nextTab = $firewallTabs.eq(nextIndex); 473 activateTab($nextTab.data('firewallTab')); 474 $nextTab.trigger('focus'); 475 }); 476 477 activateTab($firewallTabs.filter('.is-active').first().data('firewallTab') || $firewallTabs.first().data('firewallTab')); 478 479 loadFirewallData(false); 316 480 }); -
vulntitan/tags/2.0.6/includes/Admin/Admin.php
r3479599 r3481425 118 118 'firewall_mu_inactive' => esc_html__('MU loader inactive', 'vulntitan'), 119 119 'firewall_no_logs' => esc_html__('No firewall events recorded yet.', 'vulntitan'), 120 'firewall_custom_login_disabled' => esc_html__('Custom login URL is disabled. Leave the slug blank to keep the default WordPress login endpoints.', 'vulntitan'), 120 121 'firewall_event_unknown' => esc_html__('Unknown event', 'vulntitan'), 121 122 'firewall_event_login_failed' => esc_html__('Login failed', 'vulntitan'), -
vulntitan/tags/2.0.6/includes/Admin/Ajax.php
r3479541 r3481425 46 46 'summary' => FirewallService::getSummary(24), 47 47 'logs' => FirewallService::getRecentLogs(80), 48 'login_access' => FirewallService::getLoginAccessData(), 48 49 'mu_loader' => FirewallService::getMuLoaderStatus(), 49 50 ]); … … 57 58 wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403); 58 59 } 60 61 $customLoginValidation = FirewallService::validateCustomLoginSlug( 62 isset($_POST['custom_login_slug']) ? (string) wp_unslash($_POST['custom_login_slug']) : '' 63 ); 59 64 60 65 $input = [ 61 66 'enabled' => isset($_POST['enabled']) ? (int) wp_unslash($_POST['enabled']) : 1, 62 67 'login_protection_enabled' => isset($_POST['login_protection_enabled']) ? (int) wp_unslash($_POST['login_protection_enabled']) : 1, 68 'custom_login_slug' => $customLoginValidation['slug'], 63 69 'waf_sqli_enabled' => isset($_POST['waf_sqli_enabled']) ? (int) wp_unslash($_POST['waf_sqli_enabled']) : 1, 64 70 'waf_command_injection_enabled' => isset($_POST['waf_command_injection_enabled']) ? (int) wp_unslash($_POST['waf_command_injection_enabled']) : 1, … … 71 77 $settings = FirewallService::saveSettings($input); 72 78 $muLoaderResult = FirewallService::installMuLoader(); 79 $notice = ''; 80 81 if (($customLoginValidation['error'] ?? '') === 'invalid') { 82 $notice = esc_html__('Custom login slug was not saved because it is invalid or reserved.', 'vulntitan'); 83 } elseif (($customLoginValidation['error'] ?? '') === 'conflict') { 84 $notice = esc_html__('Custom login slug was not saved because that path is already used by existing WordPress content.', 'vulntitan'); 85 } 73 86 74 87 wp_send_json_success([ … … 76 89 'summary' => FirewallService::getSummary(24), 77 90 'logs' => FirewallService::getRecentLogs(80), 91 'login_access' => FirewallService::getLoginAccessData(), 78 92 'mu_loader' => FirewallService::getMuLoaderStatus(), 79 93 'mu_loader_install' => $muLoaderResult, 94 'notice' => $notice, 80 95 ]); 81 96 } -
vulntitan/tags/2.0.6/includes/Admin/Pages/Firewall.php
r3479599 r3481425 40 40 41 41 <div class="vulntitan-firewall-form"> 42 <div class="vulntitan-firewall-section"> 43 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Core Protection', 'vulntitan'); ?></div> 44 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Master switches for the firewall runtime and login shield.', 'vulntitan'); ?></div> 45 46 <label class="vulntitan-firewall-toggle"> 47 <input type="checkbox" id="vulntitan-firewall-enabled" class="vulntitan-firewall-checkbox" checked> 48 <span><?php esc_html_e('Enable Firewall', 'vulntitan'); ?></span> 49 </label> 50 51 <label class="vulntitan-firewall-toggle"> 52 <input type="checkbox" id="vulntitan-firewall-login-protection" class="vulntitan-firewall-checkbox" checked> 53 <span><?php esc_html_e('Enable Login Protection', 'vulntitan'); ?></span> 54 </label> 55 </div> 56 57 <div class="vulntitan-firewall-section"> 58 <div class="vulntitan-firewall-section-title"><?php esc_html_e('WAF Payload Protection', 'vulntitan'); ?></div> 59 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Inspect request parameters for common SQL injection and command injection payloads.', 'vulntitan'); ?></div> 60 61 <label class="vulntitan-firewall-toggle"> 62 <input type="checkbox" id="vulntitan-firewall-waf-sqli-enabled" class="vulntitan-firewall-checkbox" checked> 63 <span><?php esc_html_e('Enable SQL Injection Rule Set', 'vulntitan'); ?></span> 64 </label> 65 66 <label class="vulntitan-firewall-toggle"> 67 <input type="checkbox" id="vulntitan-firewall-waf-command-enabled" class="vulntitan-firewall-checkbox" checked> 68 <span><?php esc_html_e('Enable Command Injection Rule Set', 'vulntitan'); ?></span> 69 </label> 70 71 <label class="vulntitan-firewall-field"> 72 <span class="vulntitan-firewall-field-label"><?php esc_html_e('WAF Whitelist (Path patterns)', 'vulntitan'); ?></span> 73 <textarea id="vulntitan-firewall-waf-whitelist-paths" class="vulntitan-firewall-input vulntitan-firewall-textarea" rows="5" placeholder="/wp-json/my-plugin/* /custom-safe-endpoint"></textarea> 74 <small class="vulntitan-firewall-field-help"><?php esc_html_e('One pattern per line. Use * wildcard only when needed. Whitelist applies to SQLi/Command rules, not traversal/sensitive-file blocks.', 'vulntitan'); ?></small> 75 </label> 76 </div> 77 78 <div class="vulntitan-firewall-section"> 79 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Login Lockout Policy', 'vulntitan'); ?></div> 80 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Tighten brute force resilience with adaptive limits and retention.', 'vulntitan'); ?></div> 81 82 <label class="vulntitan-firewall-field"> 83 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Max failed attempts', 'vulntitan'); ?></span> 84 <input type="number" id="vulntitan-firewall-max-attempts" class="vulntitan-firewall-input" min="2" max="20" value="5"> 85 </label> 86 87 <label class="vulntitan-firewall-field"> 88 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Lockout duration (minutes)', 'vulntitan'); ?></span> 89 <input type="number" id="vulntitan-firewall-lockout-minutes" class="vulntitan-firewall-input" min="5" max="240" value="15"> 90 </label> 91 92 <label class="vulntitan-firewall-field"> 93 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Log retention (days)', 'vulntitan'); ?></span> 94 <input type="number" id="vulntitan-firewall-log-retention" class="vulntitan-firewall-input" min="1" max="365" value="30"> 95 </label> 42 <div class="vulntitan-firewall-workspace"> 43 <div class="vulntitan-firewall-tabs" role="tablist" aria-label="<?php esc_attr_e('Firewall setting groups', 'vulntitan'); ?>"> 44 <button 45 type="button" 46 class="vulntitan-firewall-tab is-active" 47 id="vulntitan-firewall-tab-access" 48 data-firewall-tab="access" 49 role="tab" 50 aria-selected="true" 51 aria-controls="vulntitan-firewall-panel-access" 52 > 53 <span class="vulntitan-firewall-tab-title"><?php esc_html_e('Access Shield', 'vulntitan'); ?></span> 54 <span class="vulntitan-firewall-tab-desc"><?php esc_html_e('Runtime, login gate, and hidden entry path.', 'vulntitan'); ?></span> 55 </button> 56 <button 57 type="button" 58 class="vulntitan-firewall-tab" 59 id="vulntitan-firewall-tab-waf" 60 data-firewall-tab="waf" 61 role="tab" 62 aria-selected="false" 63 aria-controls="vulntitan-firewall-panel-waf" 64 tabindex="-1" 65 > 66 <span class="vulntitan-firewall-tab-title"><?php esc_html_e('WAF Rules', 'vulntitan'); ?></span> 67 <span class="vulntitan-firewall-tab-desc"><?php esc_html_e('Payload inspection and safe route exceptions.', 'vulntitan'); ?></span> 68 </button> 69 <button 70 type="button" 71 class="vulntitan-firewall-tab" 72 id="vulntitan-firewall-tab-lockouts" 73 data-firewall-tab="lockouts" 74 role="tab" 75 aria-selected="false" 76 aria-controls="vulntitan-firewall-panel-lockouts" 77 tabindex="-1" 78 > 79 <span class="vulntitan-firewall-tab-title"><?php esc_html_e('Lockouts & Logs', 'vulntitan'); ?></span> 80 <span class="vulntitan-firewall-tab-desc"><?php esc_html_e('Brute-force thresholds, lock windows, and retention.', 'vulntitan'); ?></span> 81 </button> 82 </div> 83 84 <div class="vulntitan-firewall-tab-panels"> 85 <section 86 class="vulntitan-firewall-tab-panel is-active" 87 id="vulntitan-firewall-panel-access" 88 data-firewall-tab-panel="access" 89 role="tabpanel" 90 aria-labelledby="vulntitan-firewall-tab-access" 91 > 92 <div class="vulntitan-firewall-tab-panel-head"> 93 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Access Shield', 'vulntitan'); ?></div> 94 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Control the live firewall runtime and hide the default WordPress login endpoints behind a custom entry path.', 'vulntitan'); ?></div> 95 </div> 96 97 <div class="vulntitan-firewall-section"> 98 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Core Protection', 'vulntitan'); ?></div> 99 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Master switches for the firewall runtime and login shield.', 'vulntitan'); ?></div> 100 101 <label class="vulntitan-firewall-toggle"> 102 <input type="checkbox" id="vulntitan-firewall-enabled" class="vulntitan-firewall-checkbox" checked> 103 <span><?php esc_html_e('Enable Firewall', 'vulntitan'); ?></span> 104 </label> 105 106 <label class="vulntitan-firewall-toggle"> 107 <input type="checkbox" id="vulntitan-firewall-login-protection" class="vulntitan-firewall-checkbox" checked> 108 <span><?php esc_html_e('Enable Login Protection', 'vulntitan'); ?></span> 109 </label> 110 </div> 111 112 <div class="vulntitan-firewall-section"> 113 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Hidden Login Route', 'vulntitan'); ?></div> 114 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Replace the public login path with a private slug known only to administrators.', 'vulntitan'); ?></div> 115 116 <label class="vulntitan-firewall-field"> 117 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Custom Login Slug', 'vulntitan'); ?></span> 118 <input type="text" id="vulntitan-firewall-custom-login-slug" class="vulntitan-firewall-input" placeholder="secure-portal" autocomplete="off" spellcheck="false"> 119 <small class="vulntitan-firewall-field-help"><?php esc_html_e('Leave blank to keep the default WordPress login URLs. When set, logged-out visitors hitting wp-login.php or wp-admin will receive a 404 instead of the login screen.', 'vulntitan'); ?></small> 120 <small class="vulntitan-firewall-field-help"><?php esc_html_e('Use a unique slug that is not already used by an existing page.', 'vulntitan'); ?></small> 121 <small class="vulntitan-firewall-field-help"> 122 <?php esc_html_e('Active login URL:', 'vulntitan'); ?> 123 <code id="vulntitan-firewall-custom-login-url" class="vulntitan-firewall-runtime-path"><?php esc_html_e('Disabled', 'vulntitan'); ?></code> 124 </small> 125 </label> 126 </div> 127 </section> 128 129 <section 130 class="vulntitan-firewall-tab-panel" 131 id="vulntitan-firewall-panel-waf" 132 data-firewall-tab-panel="waf" 133 role="tabpanel" 134 aria-labelledby="vulntitan-firewall-tab-waf" 135 hidden 136 > 137 <div class="vulntitan-firewall-tab-panel-head"> 138 <div class="vulntitan-firewall-section-title"><?php esc_html_e('WAF Rules', 'vulntitan'); ?></div> 139 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Tune payload inspection so risky requests are blocked early while known-safe paths stay operational.', 'vulntitan'); ?></div> 140 </div> 141 142 <div class="vulntitan-firewall-section"> 143 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Payload Protection', 'vulntitan'); ?></div> 144 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Inspect request parameters for common SQL injection and command injection payloads.', 'vulntitan'); ?></div> 145 146 <label class="vulntitan-firewall-toggle"> 147 <input type="checkbox" id="vulntitan-firewall-waf-sqli-enabled" class="vulntitan-firewall-checkbox" checked> 148 <span><?php esc_html_e('Enable SQL Injection Rule Set', 'vulntitan'); ?></span> 149 </label> 150 151 <label class="vulntitan-firewall-toggle"> 152 <input type="checkbox" id="vulntitan-firewall-waf-command-enabled" class="vulntitan-firewall-checkbox" checked> 153 <span><?php esc_html_e('Enable Command Injection Rule Set', 'vulntitan'); ?></span> 154 </label> 155 </div> 156 157 <div class="vulntitan-firewall-section"> 158 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Trusted Exceptions', 'vulntitan'); ?></div> 159 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Whitelist routes that should bypass payload rules, without weakening traversal or sensitive-file protection.', 'vulntitan'); ?></div> 160 161 <label class="vulntitan-firewall-field"> 162 <span class="vulntitan-firewall-field-label"><?php esc_html_e('WAF Whitelist (Path patterns)', 'vulntitan'); ?></span> 163 <textarea id="vulntitan-firewall-waf-whitelist-paths" class="vulntitan-firewall-input vulntitan-firewall-textarea" rows="5" placeholder="/wp-json/my-plugin/* /custom-safe-endpoint"></textarea> 164 <small class="vulntitan-firewall-field-help"><?php esc_html_e('One pattern per line. Use * wildcard only when needed. Whitelist applies to SQLi/Command rules, not traversal/sensitive-file blocks.', 'vulntitan'); ?></small> 165 </label> 166 </div> 167 </section> 168 169 <section 170 class="vulntitan-firewall-tab-panel" 171 id="vulntitan-firewall-panel-lockouts" 172 data-firewall-tab-panel="lockouts" 173 role="tabpanel" 174 aria-labelledby="vulntitan-firewall-tab-lockouts" 175 hidden 176 > 177 <div class="vulntitan-firewall-tab-panel-head"> 178 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Lockouts & Logs', 'vulntitan'); ?></div> 179 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Set aggressive but safe thresholds for failed logins and determine how long firewall intelligence stays available in the log store.', 'vulntitan'); ?></div> 180 </div> 181 182 <div class="vulntitan-firewall-section"> 183 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Login Lockout Policy', 'vulntitan'); ?></div> 184 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Tighten brute force resilience with adaptive limits and retention.', 'vulntitan'); ?></div> 185 186 <div class="vulntitan-firewall-field-grid"> 187 <label class="vulntitan-firewall-field"> 188 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Max failed attempts', 'vulntitan'); ?></span> 189 <input type="number" id="vulntitan-firewall-max-attempts" class="vulntitan-firewall-input" min="2" max="20" value="5"> 190 </label> 191 192 <label class="vulntitan-firewall-field"> 193 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Lockout duration (minutes)', 'vulntitan'); ?></span> 194 <input type="number" id="vulntitan-firewall-lockout-minutes" class="vulntitan-firewall-input" min="5" max="240" value="15"> 195 </label> 196 197 <label class="vulntitan-firewall-field"> 198 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Log retention (days)', 'vulntitan'); ?></span> 199 <input type="number" id="vulntitan-firewall-log-retention" class="vulntitan-firewall-input" min="1" max="365" value="30"> 200 </label> 201 </div> 202 </div> 203 </section> 204 </div> 96 205 </div> 97 206 </div> -
vulntitan/tags/2.0.6/includes/Plugin.php
r3469591 r3481425 5 5 use VulnTitan\Admin\Admin; 6 6 use VulnTitan\Services\FirewallService; 7 use VulnTitan\Services\LoginAccessService; 7 8 8 9 class Plugin { … … 22 23 $this->ensure_firewall_components(); 23 24 $this->register_scheduled_events(); 25 LoginAccessService::boot(); 24 26 $this->admin = new Admin(); 25 27 $this->admin->init(); -
vulntitan/tags/2.0.6/includes/Services/FirewallService.php
r3469591 r3481425 21 21 'enabled' => 1, 22 22 'login_protection_enabled' => 1, 23 'custom_login_slug' => '', 23 24 'waf_sqli_enabled' => 1, 24 25 'waf_command_injection_enabled' => 1, … … 50 51 51 52 return $normalized; 53 } 54 55 public static function getCustomLoginSlug(): string 56 { 57 $settings = self::getSettings(); 58 59 return (string) ($settings['custom_login_slug'] ?? ''); 60 } 61 62 public static function isCustomLoginEnabled(): bool 63 { 64 return self::getCustomLoginSlug() !== ''; 65 } 66 67 public static function getCustomLoginUrl(array $queryArgs = [], string $scheme = 'login'): string 68 { 69 $slug = self::getCustomLoginSlug(); 70 if ($slug === '') { 71 return ''; 72 } 73 74 $url = home_url('/' . user_trailingslashit($slug), $scheme); 75 76 if ($queryArgs) { 77 $url = add_query_arg($queryArgs, $url); 78 } 79 80 return $url; 81 } 82 83 public static function getLoginAccessData(): array 84 { 85 $slug = self::getCustomLoginSlug(); 86 87 return [ 88 'enabled' => $slug !== '', 89 'slug' => $slug, 90 'url' => $slug !== '' ? self::getCustomLoginUrl() : '', 91 ]; 92 } 93 94 public static function validateCustomLoginSlug($input): array 95 { 96 $raw = is_scalar($input) ? trim((string) $input) : ''; 97 $slug = self::sanitizeCustomLoginSlug($input); 98 99 if ($raw !== '' && $slug === '') { 100 return [ 101 'slug' => '', 102 'error' => 'invalid', 103 ]; 104 } 105 106 if ($slug !== '' && self::customLoginSlugConflicts($slug)) { 107 return [ 108 'slug' => '', 109 'error' => 'conflict', 110 ]; 111 } 112 113 return [ 114 'slug' => $slug, 115 'error' => '', 116 ]; 52 117 } 53 118 … … 553 618 'enabled' => !empty($settings['enabled']) ? 1 : 0, 554 619 'login_protection_enabled' => !empty($settings['login_protection_enabled']) ? 1 : 0, 620 'custom_login_slug' => self::sanitizeCustomLoginSlug($settings['custom_login_slug'] ?? $defaults['custom_login_slug']), 555 621 'waf_sqli_enabled' => !empty($settings['waf_sqli_enabled']) ? 1 : 0, 556 622 'waf_command_injection_enabled' => !empty($settings['waf_command_injection_enabled']) ? 1 : 0, … … 562 628 } 563 629 630 public static function sanitizeCustomLoginSlug($input): string 631 { 632 if (!is_scalar($input)) { 633 return ''; 634 } 635 636 $value = trim((string) $input); 637 if ($value === '') { 638 return ''; 639 } 640 641 $value = preg_replace('/[\?#].*$/', '', $value); 642 $value = str_replace('\\', '/', (string) $value); 643 $value = preg_replace('#/+#', '/', (string) $value); 644 $value = trim((string) $value, "/ \t\n\r\0\x0B."); 645 646 if ($value === '') { 647 return ''; 648 } 649 650 $segments = explode('/', $value); 651 $normalized = []; 652 653 foreach ($segments as $segment) { 654 $segment = sanitize_title_with_dashes((string) $segment, '', 'save'); 655 $segment = trim($segment, '-'); 656 657 if ($segment === '') { 658 continue; 659 } 660 661 $normalized[] = substr($segment, 0, 40); 662 663 if (count($normalized) >= 4) { 664 break; 665 } 666 } 667 668 if (!$normalized) { 669 return ''; 670 } 671 672 $slug = implode('/', $normalized); 673 $slug = substr($slug, 0, 90); 674 $slug = trim($slug, '/'); 675 676 if ($slug === '') { 677 return ''; 678 } 679 680 $reservedPaths = [ 681 'admin', 682 'admin-ajax.php', 683 'admin-post.php', 684 'feed', 685 'index.php', 686 'login', 687 'robots.txt', 688 'sitemap.xml', 689 'wp-admin', 690 'wp-json', 691 'wp-login', 692 'wp-login.php', 693 'xmlrpc.php', 694 ]; 695 696 if (in_array($slug, $reservedPaths, true)) { 697 return ''; 698 } 699 700 if (preg_match('#(^|/)(?:wp-admin|wp-login(?:\.php)?|wp-json|xmlrpc(?:\.php)?|admin-ajax(?:\.php)?|admin-post(?:\.php)?)(/|$)#', $slug)) { 701 return ''; 702 } 703 704 return $slug; 705 } 706 564 707 protected static function sanitizeWhitelistPaths($input): array 565 708 { … … 598 741 599 742 return array_keys($normalized); 743 } 744 745 protected static function customLoginSlugConflicts(string $slug): bool 746 { 747 if ($slug === '') { 748 return false; 749 } 750 751 if (function_exists('url_to_postid')) { 752 $postId = url_to_postid(home_url('/' . user_trailingslashit($slug))); 753 if ($postId > 0) { 754 return true; 755 } 756 } 757 758 if (function_exists('get_page_by_path')) { 759 $page = get_page_by_path($slug, OBJECT, ['page']); 760 if ($page instanceof \WP_Post) { 761 return true; 762 } 763 } 764 765 return false; 600 766 } 601 767 -
vulntitan/tags/2.0.6/readme.txt
r3480442 r3481425 4 4 Tested up to: 6.9 5 5 Requires PHP: 7.4 6 Stable tag: 2.0. 56 Stable tag: 2.0.6 7 7 License: GPLv2 8 8 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 16 16 Instantly scan your WordPress site for malware infections and known vulnerabilities, review detailed results, and clean or remove malware safely using a guided fix workflow with automatic backups. 17 17 18 Unlike heavy security suites, VulnTitan focuses on practical protection: vulnerability detection, malware scanning and removal, file integrity monitoring, and essential firewall protection— without unnecessary bloat.18 Unlike heavy security suites, VulnTitan focuses on practical protection: vulnerability detection, malware scanning and removal, file integrity monitoring, essential firewall protection, and hidden custom login access — without unnecessary bloat. 19 19 20 20 = Malware Scanner = … … 47 47 = Firewall & Login Protection = 48 48 49 VulnTitan includes lightweight firewall and WAF protection to block common attack patterns .49 VulnTitan includes lightweight firewall and WAF protection to block common attack patterns and protect the WordPress login surface. 50 50 51 51 - Early MU-plugin runtime request guards … … 55 55 - Endpoint whitelisting controls 56 56 - Login lockout protection against brute-force attacks 57 - Configurable custom login slug so administrators can use a private login URL instead of the default `wp-login.php` 58 - Default `wp-login.php` and guest `wp-admin` access can be hidden behind a `404` response when custom login is enabled 57 59 58 60 = Security-First Architecture = … … 86 88 5. Firewall and WAF protection settings panel. 87 89 6. Vulnerability scan progress bar. 90 7. Firewall hidden custom login configuration and activity overview. 91 7. When custom login url is set and user goes to wp-login.php or wp-admin route. 88 92 89 93 == Installation == … … 130 134 == Changelog == 131 135 136 = v2.0.6 - 12 Mar, 2026 = 137 * Added configurable custom login slug support so administrators can use a private login URL instead of the default `wp-login.php` path. 138 * Hidden direct guest access to default `wp-login.php` and `wp-admin` entry points when custom login protection is enabled. 139 * Reworked the Firewall page with a tabbed settings layout, a wider recent events section, and toast-style action feedback. 140 132 141 = v2.0.4 - 10 Mar, 2026 = 133 142 * Redesigned the VulnTitan Dashboard into an elite, professional security command center layout. -
vulntitan/tags/2.0.6/vulntitan.php
r3480442 r3481425 4 4 * Plugin URI: https://vulntitan.com/vulntitan/ 5 5 * Description: VulnTitan is a lightweight WordPress security plugin with vulnerability scanning, malware detection, file integrity monitoring, and a built-in firewall with WAF payload rules and login protection. 6 * Version: 2.0. 56 * Version: 2.0.6 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.0. 5');32 define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.0.6'); 33 33 define('VULNTITAN_PLUGIN_BASENAME', plugin_basename(__FILE__)); 34 34 define('VULNTITAN_PLUGIN_DIR', untrailingslashit(plugin_dir_path(__FILE__))); -
vulntitan/trunk/CHANGELOG.md
r3480442 r3481425 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.0.6] - 2026-03-12 9 ### Added 10 - Added configurable custom login slug support so administrators can expose a private login URL instead of the default `wp-login.php` path. 11 12 ### Changed 13 - Reworked the Firewall admin page into a tabbed settings workspace with more breathing room between controls and a dedicated full-width recent events section. 14 - Replaced inline Firewall action notices with floating toast feedback for save, refresh, clear-log, warning, and error states. 15 16 ### Security 17 - Hidden login access now blocks direct guest access to `wp-login.php` and default `wp-admin` entry points with `404` responses while preserving the custom login route. 7 18 8 19 ## [2.0.5] - 2026-03-11 -
vulntitan/trunk/assets/css/admin.css
r3479599 r3481425 578 578 579 579 .vulntitan-firewall-feedback { 580 margin-top: 10px; 581 } 582 583 .vulntitan-firewall-notice { 584 padding: 10px 12px; 585 border-radius: 8px; 580 position: fixed; 581 top: 48px; 582 right: 20px; 583 z-index: 100000; 584 width: min(380px, calc(100vw - 32px)); 585 margin: 0; 586 pointer-events: none; 587 } 588 589 .vulntitan-firewall-toast { 590 pointer-events: auto; 591 display: grid; 592 grid-template-columns: auto minmax(0, 1fr) auto; 593 align-items: start; 594 gap: 12px; 595 padding: 14px 14px 14px 12px; 596 border-radius: 14px; 586 597 border: 1px solid #1f3346; 587 background: #101a24;598 background: rgba(10, 18, 28, 0.96); 588 599 color: #bfdbfe; 589 600 font-size: 12px; 590 line-height: 1.45; 591 } 592 593 .vulntitan-firewall-notice.is-success { 601 line-height: 1.5; 602 box-shadow: 0 18px 34px rgba(4, 9, 16, 0.38); 603 backdrop-filter: blur(14px); 604 opacity: 0; 605 transform: translateY(-8px); 606 transition: opacity 0.18s ease, transform 0.18s ease; 607 } 608 609 .vulntitan-firewall-feedback.is-visible .vulntitan-firewall-toast { 610 opacity: 1; 611 transform: translateY(0); 612 } 613 614 .vulntitan-firewall-toast-badge { 615 display: inline-flex; 616 align-items: center; 617 justify-content: center; 618 min-width: 58px; 619 min-height: 28px; 620 padding: 0 10px; 621 border-radius: 999px; 622 border: 1px solid rgba(90, 176, 255, 0.24); 623 background: rgba(90, 176, 255, 0.12); 624 color: #cfe4ff; 625 font-size: 10px; 626 font-weight: 700; 627 letter-spacing: 0.08em; 628 text-transform: uppercase; 629 } 630 631 .vulntitan-firewall-toast-message { 632 min-width: 0; 633 padding-top: 3px; 634 color: inherit; 635 } 636 637 .vulntitan-firewall-toast-close { 638 appearance: none; 639 width: 28px; 640 height: 28px; 641 border: 0; 642 border-radius: 999px; 643 background: rgba(255, 255, 255, 0.04); 644 color: #8fa7bf; 645 font-size: 12px; 646 font-weight: 700; 647 line-height: 1; 648 cursor: pointer; 649 transition: background-color 0.18s ease, color 0.18s ease; 650 } 651 652 .vulntitan-firewall-toast-close:hover, 653 .vulntitan-firewall-toast-close:focus { 654 outline: none; 655 background: rgba(255, 255, 255, 0.08); 656 color: #e2e8f0; 657 } 658 659 .vulntitan-firewall-toast.is-success { 594 660 border-color: #1f5131; 595 background: #0f1d16;661 background: rgba(11, 29, 21, 0.96); 596 662 color: #86efac; 597 663 } 598 664 599 .vulntitan-firewall-notice.is-error { 665 .vulntitan-firewall-toast.is-success .vulntitan-firewall-toast-badge { 666 border-color: rgba(91, 206, 122, 0.28); 667 background: rgba(31, 81, 49, 0.32); 668 color: #bbf7d0; 669 } 670 671 .vulntitan-firewall-toast.is-error { 600 672 border-color: #5f1f1f; 601 background: #221416;673 background: rgba(34, 20, 22, 0.97); 602 674 color: #fecaca; 603 675 } 604 676 605 .vulntitan-firewall-notice.is-warning { 677 .vulntitan-firewall-toast.is-error .vulntitan-firewall-toast-badge { 678 border-color: rgba(248, 113, 113, 0.28); 679 background: rgba(127, 29, 29, 0.28); 680 color: #fecaca; 681 } 682 683 .vulntitan-firewall-toast.is-warning { 606 684 border-color: #5f4a1f; 607 background: #221d13; 685 background: rgba(34, 29, 19, 0.97); 686 color: #fde68a; 687 } 688 689 .vulntitan-firewall-toast.is-warning .vulntitan-firewall-toast-badge { 690 border-color: rgba(245, 158, 11, 0.28); 691 background: rgba(95, 74, 31, 0.3); 608 692 color: #fde68a; 609 693 } … … 1755 1839 1756 1840 .vulntitan-wrapper--firewall .vulntitan-firewall-feedback { 1757 margin-top: 18px;1841 margin-top: 0; 1758 1842 } 1759 1843 1760 1844 .vulntitan-wrapper--firewall .vulntitan-firewall-grid { 1761 1845 margin-top: 22px; 1762 gap: 18px; 1846 grid-template-columns: minmax(0, 1fr); 1847 gap: 22px; 1763 1848 } 1764 1849 … … 1768 1853 border-radius: 16px; 1769 1854 box-shadow: 0 16px 30px rgba(6, 10, 16, 0.35); 1855 padding: 18px; 1856 } 1857 1858 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings, 1859 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 1860 min-width: 0; 1861 } 1862 1863 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings { 1864 padding: 20px; 1865 } 1866 1867 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 1868 padding: 20px; 1869 background: linear-gradient(180deg, rgba(11, 18, 28, 0.96), rgba(8, 14, 22, 0.98)); 1770 1870 } 1771 1871 1772 1872 .vulntitan-firewall-panel-head { 1773 1873 display: flex; 1774 align-items: center;1874 align-items: flex-start; 1775 1875 justify-content: space-between; 1776 1876 gap: 12px; 1777 1877 margin-bottom: 16px; 1878 flex-wrap: wrap; 1778 1879 } 1779 1880 … … 1785 1886 } 1786 1887 1888 .vulntitan-wrapper--firewall .vulntitan-firewall-workspace { 1889 display: grid; 1890 grid-template-columns: minmax(0, 1fr); 1891 gap: 18px; 1892 align-items: start; 1893 } 1894 1895 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs { 1896 display: grid; 1897 grid-template-columns: repeat(3, minmax(0, 1fr)); 1898 gap: 12px; 1899 align-content: start; 1900 } 1901 1902 .vulntitan-wrapper--firewall .vulntitan-firewall-tab { 1903 appearance: none; 1904 width: 100%; 1905 border: 1px solid rgba(90, 176, 255, 0.16); 1906 border-radius: 14px; 1907 background: linear-gradient(180deg, rgba(11, 17, 24, 0.94), rgba(7, 12, 18, 0.98)); 1908 min-height: 112px; 1909 padding: 16px 18px; 1910 text-align: left; 1911 display: grid; 1912 gap: 6px; 1913 color: var(--vt-fw-text); 1914 cursor: pointer; 1915 transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; 1916 box-shadow: 0 10px 20px rgba(6, 10, 16, 0.2); 1917 } 1918 1919 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:hover { 1920 border-color: rgba(90, 176, 255, 0.32); 1921 transform: translateY(-1px); 1922 } 1923 1924 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:focus { 1925 outline: none; 1926 box-shadow: 0 0 0 3px rgba(90, 176, 255, 0.18), 0 14px 26px rgba(6, 10, 16, 0.3); 1927 } 1928 1929 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:disabled { 1930 opacity: 0.6; 1931 cursor: wait; 1932 transform: none; 1933 } 1934 1935 .vulntitan-wrapper--firewall .vulntitan-firewall-tab.is-active { 1936 border-color: rgba(90, 176, 255, 0.38); 1937 background: linear-gradient(180deg, rgba(16, 27, 40, 0.96), rgba(9, 16, 24, 0.98)); 1938 box-shadow: inset 0 0 0 1px rgba(51, 209, 160, 0.1), 0 18px 32px rgba(6, 10, 16, 0.35); 1939 transform: translateY(-2px); 1940 } 1941 1942 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-title { 1943 font-size: 12px; 1944 font-weight: 700; 1945 letter-spacing: 0.1em; 1946 text-transform: uppercase; 1947 color: #d7e8fb; 1948 } 1949 1950 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-desc { 1951 font-size: 12px; 1952 line-height: 1.5; 1953 color: var(--vt-fw-muted); 1954 } 1955 1956 .vulntitan-wrapper--firewall .vulntitan-firewall-tab.is-active .vulntitan-firewall-tab-desc { 1957 color: #cfe4ff; 1958 } 1959 1960 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panels { 1961 min-width: 0; 1962 } 1963 1964 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel { 1965 display: grid; 1966 grid-template-columns: repeat(2, minmax(0, 1fr)); 1967 gap: 16px; 1968 min-width: 0; 1969 } 1970 1971 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel[hidden] { 1972 display: none !important; 1973 } 1974 1975 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel-head { 1976 grid-column: 1 / -1; 1977 } 1978 1979 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel .vulntitan-firewall-section + .vulntitan-firewall-section { 1980 margin-top: 0; 1981 } 1982 1983 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel > .vulntitan-firewall-section:only-of-type { 1984 grid-column: 1 / -1; 1985 } 1986 1987 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel-head { 1988 padding: 16px 18px; 1989 border-radius: 14px; 1990 border: 1px solid rgba(90, 176, 255, 0.16); 1991 background: linear-gradient(135deg, rgba(51, 209, 160, 0.08), rgba(90, 176, 255, 0.08)), rgba(9, 14, 20, 0.72); 1992 display: grid; 1993 gap: 8px; 1994 } 1995 1787 1996 .vulntitan-firewall-section { 1788 padding: 1 2px 14px;1789 border-radius: 1 2px;1997 padding: 16px 18px; 1998 border-radius: 14px; 1790 1999 border: 1px solid rgba(90, 176, 255, 0.18); 1791 2000 background: rgba(9, 14, 20, 0.7); 1792 2001 display: grid; 1793 gap: 1 0px;2002 gap: 12px; 1794 2003 } 1795 2004 … … 1833 2042 .vulntitan-wrapper--firewall .vulntitan-firewall-field-help { 1834 2043 color: var(--vt-fw-muted); 2044 } 2045 2046 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid { 2047 display: grid; 2048 grid-template-columns: repeat(3, minmax(0, 1fr)); 2049 gap: 14px; 2050 } 2051 2052 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid .vulntitan-firewall-input { 2053 max-width: none; 1835 2054 } 1836 2055 … … 1866 2085 1867 2086 .vulntitan-wrapper--firewall .vulntitan-firewall-log-list { 1868 background: rgba(9, 14, 20, 0.5); 1869 border-radius: 12px; 1870 padding: 6px; 2087 margin-top: 0; 2088 max-height: 720px; 2089 background: transparent; 2090 border-radius: 0; 2091 padding: 0; 2092 gap: 14px; 1871 2093 } 1872 2094 1873 2095 .vulntitan-wrapper--firewall .vulntitan-firewall-log-item { 1874 border-radius: 12px; 1875 border: 1px solid rgba(90, 176, 255, 0.12); 1876 background: rgba(11, 17, 24, 0.7); 2096 border-radius: 16px; 2097 border: 1px solid rgba(90, 176, 255, 0.14); 2098 background: linear-gradient(180deg, rgba(8, 14, 21, 0.96), rgba(6, 11, 18, 0.98)); 2099 padding: 16px 18px; 2100 gap: 10px; 2101 box-shadow: inset 0 0 0 1px rgba(51, 209, 160, 0.03); 1877 2102 } 1878 2103 1879 2104 .vulntitan-wrapper--firewall .vulntitan-firewall-log-request { 1880 2105 color: #cde2f7; 2106 font-size: 14px; 2107 line-height: 1.5; 2108 } 2109 2110 .vulntitan-wrapper--firewall .vulntitan-firewall-log-meta { 2111 gap: 10px 18px; 2112 } 2113 2114 .vulntitan-wrapper--firewall .vulntitan-firewall-log-meta-item { 2115 font-size: 12px; 2116 } 2117 2118 .vulntitan-wrapper--firewall .vulntitan-firewall-log-reason { 2119 font-size: 13px; 2120 color: #d4dfeb; 1881 2121 } 1882 2122 … … 1885 2125 grid-template-columns: 1fr; 1886 2126 } 2127 2128 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel { 2129 grid-template-columns: 1fr; 2130 } 2131 } 2132 2133 @media (max-width: 1280px) { 2134 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs { 2135 grid-template-columns: repeat(2, minmax(0, 1fr)); 2136 } 1887 2137 } 1888 2138 1889 2139 @media (max-width: 782px) { 2140 .vulntitan-firewall-feedback { 2141 top: 58px; 2142 right: 12px; 2143 width: calc(100vw - 24px); 2144 } 2145 2146 .vulntitan-firewall-toast { 2147 grid-template-columns: 1fr auto; 2148 gap: 10px; 2149 } 2150 2151 .vulntitan-firewall-toast-badge { 2152 grid-column: 1 / 2; 2153 justify-self: start; 2154 } 2155 2156 .vulntitan-firewall-toast-message { 2157 grid-column: 1 / 2; 2158 padding-top: 0; 2159 } 2160 2161 .vulntitan-firewall-toast-close { 2162 grid-column: 2 / 3; 2163 grid-row: 1 / 2; 2164 } 2165 1890 2166 .vulntitan-wrapper--firewall { 1891 2167 padding: 1.25rem; 1892 2168 } 1893 } 2169 2170 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs, 2171 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid { 2172 grid-template-columns: 1fr; 2173 } 2174 2175 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings, 2176 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 2177 padding: 16px; 2178 } 2179 } -
vulntitan/trunk/assets/css/admin.min.css
r3479599 r3481425 578 578 579 579 .vulntitan-firewall-feedback { 580 margin-top: 10px; 581 } 582 583 .vulntitan-firewall-notice { 584 padding: 10px 12px; 585 border-radius: 8px; 580 position: fixed; 581 top: 48px; 582 right: 20px; 583 z-index: 100000; 584 width: min(380px, calc(100vw - 32px)); 585 margin: 0; 586 pointer-events: none; 587 } 588 589 .vulntitan-firewall-toast { 590 pointer-events: auto; 591 display: grid; 592 grid-template-columns: auto minmax(0, 1fr) auto; 593 align-items: start; 594 gap: 12px; 595 padding: 14px 14px 14px 12px; 596 border-radius: 14px; 586 597 border: 1px solid #1f3346; 587 background: #101a24;598 background: rgba(10, 18, 28, 0.96); 588 599 color: #bfdbfe; 589 600 font-size: 12px; 590 line-height: 1.45; 591 } 592 593 .vulntitan-firewall-notice.is-success { 601 line-height: 1.5; 602 box-shadow: 0 18px 34px rgba(4, 9, 16, 0.38); 603 backdrop-filter: blur(14px); 604 opacity: 0; 605 transform: translateY(-8px); 606 transition: opacity 0.18s ease, transform 0.18s ease; 607 } 608 609 .vulntitan-firewall-feedback.is-visible .vulntitan-firewall-toast { 610 opacity: 1; 611 transform: translateY(0); 612 } 613 614 .vulntitan-firewall-toast-badge { 615 display: inline-flex; 616 align-items: center; 617 justify-content: center; 618 min-width: 58px; 619 min-height: 28px; 620 padding: 0 10px; 621 border-radius: 999px; 622 border: 1px solid rgba(90, 176, 255, 0.24); 623 background: rgba(90, 176, 255, 0.12); 624 color: #cfe4ff; 625 font-size: 10px; 626 font-weight: 700; 627 letter-spacing: 0.08em; 628 text-transform: uppercase; 629 } 630 631 .vulntitan-firewall-toast-message { 632 min-width: 0; 633 padding-top: 3px; 634 color: inherit; 635 } 636 637 .vulntitan-firewall-toast-close { 638 appearance: none; 639 width: 28px; 640 height: 28px; 641 border: 0; 642 border-radius: 999px; 643 background: rgba(255, 255, 255, 0.04); 644 color: #8fa7bf; 645 font-size: 12px; 646 font-weight: 700; 647 line-height: 1; 648 cursor: pointer; 649 transition: background-color 0.18s ease, color 0.18s ease; 650 } 651 652 .vulntitan-firewall-toast-close:hover, 653 .vulntitan-firewall-toast-close:focus { 654 outline: none; 655 background: rgba(255, 255, 255, 0.08); 656 color: #e2e8f0; 657 } 658 659 .vulntitan-firewall-toast.is-success { 594 660 border-color: #1f5131; 595 background: #0f1d16;661 background: rgba(11, 29, 21, 0.96); 596 662 color: #86efac; 597 663 } 598 664 599 .vulntitan-firewall-notice.is-error { 665 .vulntitan-firewall-toast.is-success .vulntitan-firewall-toast-badge { 666 border-color: rgba(91, 206, 122, 0.28); 667 background: rgba(31, 81, 49, 0.32); 668 color: #bbf7d0; 669 } 670 671 .vulntitan-firewall-toast.is-error { 600 672 border-color: #5f1f1f; 601 background: #221416;673 background: rgba(34, 20, 22, 0.97); 602 674 color: #fecaca; 603 675 } 604 676 605 .vulntitan-firewall-notice.is-warning { 677 .vulntitan-firewall-toast.is-error .vulntitan-firewall-toast-badge { 678 border-color: rgba(248, 113, 113, 0.28); 679 background: rgba(127, 29, 29, 0.28); 680 color: #fecaca; 681 } 682 683 .vulntitan-firewall-toast.is-warning { 606 684 border-color: #5f4a1f; 607 background: #221d13; 685 background: rgba(34, 29, 19, 0.97); 686 color: #fde68a; 687 } 688 689 .vulntitan-firewall-toast.is-warning .vulntitan-firewall-toast-badge { 690 border-color: rgba(245, 158, 11, 0.28); 691 background: rgba(95, 74, 31, 0.3); 608 692 color: #fde68a; 609 693 } … … 1755 1839 1756 1840 .vulntitan-wrapper--firewall .vulntitan-firewall-feedback { 1757 margin-top: 18px;1841 margin-top: 0; 1758 1842 } 1759 1843 1760 1844 .vulntitan-wrapper--firewall .vulntitan-firewall-grid { 1761 1845 margin-top: 22px; 1762 gap: 18px; 1846 grid-template-columns: minmax(0, 1fr); 1847 gap: 22px; 1763 1848 } 1764 1849 … … 1768 1853 border-radius: 16px; 1769 1854 box-shadow: 0 16px 30px rgba(6, 10, 16, 0.35); 1855 padding: 18px; 1856 } 1857 1858 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings, 1859 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 1860 min-width: 0; 1861 } 1862 1863 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings { 1864 padding: 20px; 1865 } 1866 1867 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 1868 padding: 20px; 1869 background: linear-gradient(180deg, rgba(11, 18, 28, 0.96), rgba(8, 14, 22, 0.98)); 1770 1870 } 1771 1871 1772 1872 .vulntitan-firewall-panel-head { 1773 1873 display: flex; 1774 align-items: center;1874 align-items: flex-start; 1775 1875 justify-content: space-between; 1776 1876 gap: 12px; 1777 1877 margin-bottom: 16px; 1878 flex-wrap: wrap; 1778 1879 } 1779 1880 … … 1785 1886 } 1786 1887 1888 .vulntitan-wrapper--firewall .vulntitan-firewall-workspace { 1889 display: grid; 1890 grid-template-columns: minmax(0, 1fr); 1891 gap: 18px; 1892 align-items: start; 1893 } 1894 1895 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs { 1896 display: grid; 1897 grid-template-columns: repeat(3, minmax(0, 1fr)); 1898 gap: 12px; 1899 align-content: start; 1900 } 1901 1902 .vulntitan-wrapper--firewall .vulntitan-firewall-tab { 1903 appearance: none; 1904 width: 100%; 1905 border: 1px solid rgba(90, 176, 255, 0.16); 1906 border-radius: 14px; 1907 background: linear-gradient(180deg, rgba(11, 17, 24, 0.94), rgba(7, 12, 18, 0.98)); 1908 min-height: 112px; 1909 padding: 16px 18px; 1910 text-align: left; 1911 display: grid; 1912 gap: 6px; 1913 color: var(--vt-fw-text); 1914 cursor: pointer; 1915 transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; 1916 box-shadow: 0 10px 20px rgba(6, 10, 16, 0.2); 1917 } 1918 1919 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:hover { 1920 border-color: rgba(90, 176, 255, 0.32); 1921 transform: translateY(-1px); 1922 } 1923 1924 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:focus { 1925 outline: none; 1926 box-shadow: 0 0 0 3px rgba(90, 176, 255, 0.18), 0 14px 26px rgba(6, 10, 16, 0.3); 1927 } 1928 1929 .vulntitan-wrapper--firewall .vulntitan-firewall-tab:disabled { 1930 opacity: 0.6; 1931 cursor: wait; 1932 transform: none; 1933 } 1934 1935 .vulntitan-wrapper--firewall .vulntitan-firewall-tab.is-active { 1936 border-color: rgba(90, 176, 255, 0.38); 1937 background: linear-gradient(180deg, rgba(16, 27, 40, 0.96), rgba(9, 16, 24, 0.98)); 1938 box-shadow: inset 0 0 0 1px rgba(51, 209, 160, 0.1), 0 18px 32px rgba(6, 10, 16, 0.35); 1939 transform: translateY(-2px); 1940 } 1941 1942 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-title { 1943 font-size: 12px; 1944 font-weight: 700; 1945 letter-spacing: 0.1em; 1946 text-transform: uppercase; 1947 color: #d7e8fb; 1948 } 1949 1950 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-desc { 1951 font-size: 12px; 1952 line-height: 1.5; 1953 color: var(--vt-fw-muted); 1954 } 1955 1956 .vulntitan-wrapper--firewall .vulntitan-firewall-tab.is-active .vulntitan-firewall-tab-desc { 1957 color: #cfe4ff; 1958 } 1959 1960 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panels { 1961 min-width: 0; 1962 } 1963 1964 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel { 1965 display: grid; 1966 grid-template-columns: repeat(2, minmax(0, 1fr)); 1967 gap: 16px; 1968 min-width: 0; 1969 } 1970 1971 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel[hidden] { 1972 display: none !important; 1973 } 1974 1975 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel-head { 1976 grid-column: 1 / -1; 1977 } 1978 1979 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel .vulntitan-firewall-section + .vulntitan-firewall-section { 1980 margin-top: 0; 1981 } 1982 1983 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel > .vulntitan-firewall-section:only-of-type { 1984 grid-column: 1 / -1; 1985 } 1986 1987 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel-head { 1988 padding: 16px 18px; 1989 border-radius: 14px; 1990 border: 1px solid rgba(90, 176, 255, 0.16); 1991 background: linear-gradient(135deg, rgba(51, 209, 160, 0.08), rgba(90, 176, 255, 0.08)), rgba(9, 14, 20, 0.72); 1992 display: grid; 1993 gap: 8px; 1994 } 1995 1787 1996 .vulntitan-firewall-section { 1788 padding: 1 2px 14px;1789 border-radius: 1 2px;1997 padding: 16px 18px; 1998 border-radius: 14px; 1790 1999 border: 1px solid rgba(90, 176, 255, 0.18); 1791 2000 background: rgba(9, 14, 20, 0.7); 1792 2001 display: grid; 1793 gap: 1 0px;2002 gap: 12px; 1794 2003 } 1795 2004 … … 1833 2042 .vulntitan-wrapper--firewall .vulntitan-firewall-field-help { 1834 2043 color: var(--vt-fw-muted); 2044 } 2045 2046 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid { 2047 display: grid; 2048 grid-template-columns: repeat(3, minmax(0, 1fr)); 2049 gap: 14px; 2050 } 2051 2052 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid .vulntitan-firewall-input { 2053 max-width: none; 1835 2054 } 1836 2055 … … 1866 2085 1867 2086 .vulntitan-wrapper--firewall .vulntitan-firewall-log-list { 1868 background: rgba(9, 14, 20, 0.5); 1869 border-radius: 12px; 1870 padding: 6px; 2087 margin-top: 0; 2088 max-height: 720px; 2089 background: transparent; 2090 border-radius: 0; 2091 padding: 0; 2092 gap: 14px; 1871 2093 } 1872 2094 1873 2095 .vulntitan-wrapper--firewall .vulntitan-firewall-log-item { 1874 border-radius: 12px; 1875 border: 1px solid rgba(90, 176, 255, 0.12); 1876 background: rgba(11, 17, 24, 0.7); 2096 border-radius: 16px; 2097 border: 1px solid rgba(90, 176, 255, 0.14); 2098 background: linear-gradient(180deg, rgba(8, 14, 21, 0.96), rgba(6, 11, 18, 0.98)); 2099 padding: 16px 18px; 2100 gap: 10px; 2101 box-shadow: inset 0 0 0 1px rgba(51, 209, 160, 0.03); 1877 2102 } 1878 2103 1879 2104 .vulntitan-wrapper--firewall .vulntitan-firewall-log-request { 1880 2105 color: #cde2f7; 2106 font-size: 14px; 2107 line-height: 1.5; 2108 } 2109 2110 .vulntitan-wrapper--firewall .vulntitan-firewall-log-meta { 2111 gap: 10px 18px; 2112 } 2113 2114 .vulntitan-wrapper--firewall .vulntitan-firewall-log-meta-item { 2115 font-size: 12px; 2116 } 2117 2118 .vulntitan-wrapper--firewall .vulntitan-firewall-log-reason { 2119 font-size: 13px; 2120 color: #d4dfeb; 1881 2121 } 1882 2122 … … 1885 2125 grid-template-columns: 1fr; 1886 2126 } 2127 2128 .vulntitan-wrapper--firewall .vulntitan-firewall-tab-panel { 2129 grid-template-columns: 1fr; 2130 } 2131 } 2132 2133 @media (max-width: 1280px) { 2134 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs { 2135 grid-template-columns: repeat(2, minmax(0, 1fr)); 2136 } 1887 2137 } 1888 2138 1889 2139 @media (max-width: 782px) { 2140 .vulntitan-firewall-feedback { 2141 top: 58px; 2142 right: 12px; 2143 width: calc(100vw - 24px); 2144 } 2145 2146 .vulntitan-firewall-toast { 2147 grid-template-columns: 1fr auto; 2148 gap: 10px; 2149 } 2150 2151 .vulntitan-firewall-toast-badge { 2152 grid-column: 1 / 2; 2153 justify-self: start; 2154 } 2155 2156 .vulntitan-firewall-toast-message { 2157 grid-column: 1 / 2; 2158 padding-top: 0; 2159 } 2160 2161 .vulntitan-firewall-toast-close { 2162 grid-column: 2 / 3; 2163 grid-row: 1 / 2; 2164 } 2165 1890 2166 .vulntitan-wrapper--firewall { 1891 2167 padding: 1.25rem; 1892 2168 } 1893 } 2169 2170 .vulntitan-wrapper--firewall .vulntitan-firewall-tabs, 2171 .vulntitan-wrapper--firewall .vulntitan-firewall-field-grid { 2172 grid-template-columns: 1fr; 2173 } 2174 2175 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--settings, 2176 .vulntitan-wrapper--firewall .vulntitan-firewall-panel--logs { 2177 padding: 16px; 2178 } 2179 } -
vulntitan/trunk/assets/js/firewall.js
r3469591 r3481425 11 11 const $firewallEnabled = $('#vulntitan-firewall-enabled'); 12 12 const $firewallLoginProtection = $('#vulntitan-firewall-login-protection'); 13 const $firewallCustomLoginSlug = $('#vulntitan-firewall-custom-login-slug'); 14 const $firewallCustomLoginUrl = $('#vulntitan-firewall-custom-login-url'); 13 15 const $firewallWafSqliEnabled = $('#vulntitan-firewall-waf-sqli-enabled'); 14 16 const $firewallWafCommandEnabled = $('#vulntitan-firewall-waf-command-enabled'); … … 20 22 const $firewallRefresh = $('#vulntitan-firewall-refresh'); 21 23 const $firewallClearLogs = $('#vulntitan-firewall-clear-logs'); 24 const $firewallTabs = $firewallRoot.find('[data-firewall-tab]'); 25 const $firewallTabPanels = $firewallRoot.find('[data-firewall-tab-panel]'); 22 26 const i18n = (window.VulnTitan && window.VulnTitan.i18n) ? window.VulnTitan.i18n : {}; 23 27 let latestMuLoaderStatus = null; 28 let feedbackTimer = null; 29 let feedbackHideTimer = null; 24 30 25 31 function escapeHtml(str) { … … 35 41 } 36 42 43 function clearFeedbackTimers() { 44 if (feedbackTimer) { 45 window.clearTimeout(feedbackTimer); 46 feedbackTimer = null; 47 } 48 49 if (feedbackHideTimer) { 50 window.clearTimeout(feedbackHideTimer); 51 feedbackHideTimer = null; 52 } 53 } 54 55 function dismissFeedback() { 56 clearFeedbackTimers(); 57 58 $firewallFeedback.removeClass('is-visible'); 59 feedbackHideTimer = window.setTimeout(function () { 60 $firewallFeedback.hide().empty(); 61 feedbackHideTimer = null; 62 }, 180); 63 } 64 65 function getFeedbackConfig(type) { 66 switch (type) { 67 case 'success': 68 return { 69 label: i18n.firewall_toast_success || 'Saved', 70 duration: 3200 71 }; 72 case 'warning': 73 return { 74 label: i18n.firewall_toast_warning || 'Review', 75 duration: 5200 76 }; 77 case 'error': 78 return { 79 label: i18n.firewall_toast_error || 'Error', 80 duration: 6200 81 }; 82 default: 83 return { 84 label: i18n.firewall_toast_info || 'Working', 85 duration: 0 86 }; 87 } 88 } 89 37 90 function setFeedback(type, message) { 38 91 if (!message) { 39 $firewallFeedback.hide().empty();92 dismissFeedback(); 40 93 return; 41 94 } 42 95 43 96 const normalizedType = (type === 'success' || type === 'error' || type === 'warning') ? type : 'info'; 97 const config = getFeedbackConfig(normalizedType); 98 const liveMode = normalizedType === 'error' ? 'assertive' : 'polite'; 99 100 clearFeedbackTimers(); 44 101 45 102 $firewallFeedback 46 .html(`<div class="vulntitan-firewall-notice is-${normalizedType}">${escapeHtml(message)}</div>`) 103 .html(` 104 <div class="vulntitan-firewall-toast is-${normalizedType}" role="status" aria-live="${liveMode}"> 105 <span class="vulntitan-firewall-toast-badge">${escapeHtml(config.label)}</span> 106 <div class="vulntitan-firewall-toast-message">${escapeHtml(message)}</div> 107 <button type="button" class="vulntitan-firewall-toast-close" data-firewall-toast-close aria-label="${escapeHtml(i18n.firewall_toast_close || 'Dismiss notification')}">x</button> 108 </div> 109 `) 47 110 .show(); 111 112 window.requestAnimationFrame(function () { 113 $firewallFeedback.addClass('is-visible'); 114 }); 115 116 if (config.duration > 0) { 117 feedbackTimer = window.setTimeout(function () { 118 dismissFeedback(); 119 }, config.duration); 120 } 48 121 } 49 122 … … 122 195 } 123 196 197 function renderLoginAccess(loginAccess) { 198 const data = loginAccess || {}; 199 const customUrl = String(data.url || '').trim(); 200 201 if (!customUrl) { 202 $firewallCustomLoginUrl.text( 203 i18n.firewall_custom_login_disabled || 'Custom login URL is disabled. Leave the slug blank to keep the default WordPress login endpoints.' 204 ); 205 return; 206 } 207 208 $firewallCustomLoginUrl.text(customUrl); 209 } 210 211 function activateTab(tabId) { 212 const normalizedTab = String(tabId || '').trim(); 213 if (!normalizedTab) { 214 return; 215 } 216 217 $firewallTabs.each(function () { 218 const $tab = $(this); 219 const isActive = String($tab.data('firewallTab')) === normalizedTab; 220 221 $tab 222 .toggleClass('is-active', isActive) 223 .attr('aria-selected', isActive ? 'true' : 'false') 224 .attr('tabindex', isActive ? '0' : '-1'); 225 }); 226 227 $firewallTabPanels.each(function () { 228 const $panel = $(this); 229 const isActive = String($panel.data('firewallTabPanel')) === normalizedTab; 230 231 $panel 232 .toggleClass('is-active', isActive) 233 .prop('hidden', !isActive); 234 }); 235 } 236 124 237 function applySettings(settings) { 125 238 const data = settings || {}; … … 127 240 $firewallEnabled.prop('checked', !!Number(data.enabled || 0)); 128 241 $firewallLoginProtection.prop('checked', !!Number(data.login_protection_enabled || 0)); 242 $firewallCustomLoginSlug.val(String(data.custom_login_slug || '')); 129 243 $firewallWafSqliEnabled.prop('checked', !!Number(data.waf_sqli_enabled || 0)); 130 244 $firewallWafCommandEnabled.prop('checked', !!Number(data.waf_command_injection_enabled || 0)); … … 183 297 $firewallRefresh.prop('disabled', isBusy); 184 298 $firewallClearLogs.prop('disabled', isBusy); 299 $firewallTabs.prop('disabled', isBusy); 185 300 $firewallEnabled.prop('disabled', isBusy); 186 301 $firewallLoginProtection.prop('disabled', isBusy); 302 $firewallCustomLoginSlug.prop('disabled', isBusy); 187 303 $firewallWafSqliEnabled.prop('disabled', isBusy); 188 304 $firewallWafCommandEnabled.prop('disabled', isBusy); … … 197 313 198 314 if (showLoadingNotice) { 199 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');315 setFeedback('info', i18n.firewall_refreshing || 'Refreshing firewall data...'); 200 316 } 201 317 … … 215 331 latestMuLoaderStatus = payload.mu_loader || null; 216 332 applySettings(payload.settings || {}); 333 renderLoginAccess(payload.login_access || {}); 217 334 renderOverview(payload.summary || {}, latestMuLoaderStatus || {}); 218 335 renderLogs(payload.logs || []); … … 227 344 function saveSettings() { 228 345 setBusyState(true); 229 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');346 setFeedback('info', i18n.firewall_saving || 'Saving firewall settings...'); 230 347 231 348 $.post(VulnTitan.ajaxUrl, { … … 234 351 enabled: $firewallEnabled.is(':checked') ? 1 : 0, 235 352 login_protection_enabled: $firewallLoginProtection.is(':checked') ? 1 : 0, 353 custom_login_slug: String($firewallCustomLoginSlug.val() || ''), 236 354 waf_sqli_enabled: $firewallWafSqliEnabled.is(':checked') ? 1 : 0, 237 355 waf_command_injection_enabled: $firewallWafCommandEnabled.is(':checked') ? 1 : 0, … … 252 370 latestMuLoaderStatus = payload.mu_loader || latestMuLoaderStatus; 253 371 applySettings(payload.settings || {}); 372 renderLoginAccess(payload.login_access || {}); 254 373 renderOverview(payload.summary || {}, latestMuLoaderStatus || {}); 255 374 renderLogs(payload.logs || []); 256 375 257 376 const muInstall = payload.mu_loader_install || {}; 377 if (payload.notice) { 378 setFeedback('warning', payload.notice); 379 return; 380 } 381 258 382 if (muInstall.success === false && muInstall.error) { 259 383 setFeedback('warning', muInstall.error); … … 276 400 277 401 setBusyState(true); 278 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');402 setFeedback('info', i18n.firewall_clearing || 'Clearing firewall logs...'); 279 403 280 404 $.post(VulnTitan.ajaxUrl, { … … 313 437 }); 314 438 315 loadFirewallData(true); 439 $firewallFeedback.off('click', '[data-firewall-toast-close]').on('click', '[data-firewall-toast-close]', function () { 440 dismissFeedback(); 441 }); 442 443 $firewallTabs.off('click').on('click', function () { 444 activateTab($(this).data('firewallTab')); 445 }); 446 447 $firewallTabs.off('keydown').on('keydown', function (event) { 448 const keys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']; 449 if (keys.indexOf(event.key) === -1) { 450 return; 451 } 452 453 event.preventDefault(); 454 455 const currentIndex = $firewallTabs.index(this); 456 if (currentIndex < 0) { 457 return; 458 } 459 460 let nextIndex = currentIndex; 461 462 if (event.key === 'Home') { 463 nextIndex = 0; 464 } else if (event.key === 'End') { 465 nextIndex = $firewallTabs.length - 1; 466 } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { 467 nextIndex = currentIndex === 0 ? $firewallTabs.length - 1 : currentIndex - 1; 468 } else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { 469 nextIndex = currentIndex === $firewallTabs.length - 1 ? 0 : currentIndex + 1; 470 } 471 472 const $nextTab = $firewallTabs.eq(nextIndex); 473 activateTab($nextTab.data('firewallTab')); 474 $nextTab.trigger('focus'); 475 }); 476 477 activateTab($firewallTabs.filter('.is-active').first().data('firewallTab') || $firewallTabs.first().data('firewallTab')); 478 479 loadFirewallData(false); 316 480 }); -
vulntitan/trunk/assets/js/firewall.min.js
r3469591 r3481425 11 11 const $firewallEnabled = $('#vulntitan-firewall-enabled'); 12 12 const $firewallLoginProtection = $('#vulntitan-firewall-login-protection'); 13 const $firewallCustomLoginSlug = $('#vulntitan-firewall-custom-login-slug'); 14 const $firewallCustomLoginUrl = $('#vulntitan-firewall-custom-login-url'); 13 15 const $firewallWafSqliEnabled = $('#vulntitan-firewall-waf-sqli-enabled'); 14 16 const $firewallWafCommandEnabled = $('#vulntitan-firewall-waf-command-enabled'); … … 20 22 const $firewallRefresh = $('#vulntitan-firewall-refresh'); 21 23 const $firewallClearLogs = $('#vulntitan-firewall-clear-logs'); 24 const $firewallTabs = $firewallRoot.find('[data-firewall-tab]'); 25 const $firewallTabPanels = $firewallRoot.find('[data-firewall-tab-panel]'); 22 26 const i18n = (window.VulnTitan && window.VulnTitan.i18n) ? window.VulnTitan.i18n : {}; 23 27 let latestMuLoaderStatus = null; 28 let feedbackTimer = null; 29 let feedbackHideTimer = null; 24 30 25 31 function escapeHtml(str) { … … 35 41 } 36 42 43 function clearFeedbackTimers() { 44 if (feedbackTimer) { 45 window.clearTimeout(feedbackTimer); 46 feedbackTimer = null; 47 } 48 49 if (feedbackHideTimer) { 50 window.clearTimeout(feedbackHideTimer); 51 feedbackHideTimer = null; 52 } 53 } 54 55 function dismissFeedback() { 56 clearFeedbackTimers(); 57 58 $firewallFeedback.removeClass('is-visible'); 59 feedbackHideTimer = window.setTimeout(function () { 60 $firewallFeedback.hide().empty(); 61 feedbackHideTimer = null; 62 }, 180); 63 } 64 65 function getFeedbackConfig(type) { 66 switch (type) { 67 case 'success': 68 return { 69 label: i18n.firewall_toast_success || 'Saved', 70 duration: 3200 71 }; 72 case 'warning': 73 return { 74 label: i18n.firewall_toast_warning || 'Review', 75 duration: 5200 76 }; 77 case 'error': 78 return { 79 label: i18n.firewall_toast_error || 'Error', 80 duration: 6200 81 }; 82 default: 83 return { 84 label: i18n.firewall_toast_info || 'Working', 85 duration: 0 86 }; 87 } 88 } 89 37 90 function setFeedback(type, message) { 38 91 if (!message) { 39 $firewallFeedback.hide().empty();92 dismissFeedback(); 40 93 return; 41 94 } 42 95 43 96 const normalizedType = (type === 'success' || type === 'error' || type === 'warning') ? type : 'info'; 97 const config = getFeedbackConfig(normalizedType); 98 const liveMode = normalizedType === 'error' ? 'assertive' : 'polite'; 99 100 clearFeedbackTimers(); 44 101 45 102 $firewallFeedback 46 .html(`<div class="vulntitan-firewall-notice is-${normalizedType}">${escapeHtml(message)}</div>`) 103 .html(` 104 <div class="vulntitan-firewall-toast is-${normalizedType}" role="status" aria-live="${liveMode}"> 105 <span class="vulntitan-firewall-toast-badge">${escapeHtml(config.label)}</span> 106 <div class="vulntitan-firewall-toast-message">${escapeHtml(message)}</div> 107 <button type="button" class="vulntitan-firewall-toast-close" data-firewall-toast-close aria-label="${escapeHtml(i18n.firewall_toast_close || 'Dismiss notification')}">x</button> 108 </div> 109 `) 47 110 .show(); 111 112 window.requestAnimationFrame(function () { 113 $firewallFeedback.addClass('is-visible'); 114 }); 115 116 if (config.duration > 0) { 117 feedbackTimer = window.setTimeout(function () { 118 dismissFeedback(); 119 }, config.duration); 120 } 48 121 } 49 122 … … 122 195 } 123 196 197 function renderLoginAccess(loginAccess) { 198 const data = loginAccess || {}; 199 const customUrl = String(data.url || '').trim(); 200 201 if (!customUrl) { 202 $firewallCustomLoginUrl.text( 203 i18n.firewall_custom_login_disabled || 'Custom login URL is disabled. Leave the slug blank to keep the default WordPress login endpoints.' 204 ); 205 return; 206 } 207 208 $firewallCustomLoginUrl.text(customUrl); 209 } 210 211 function activateTab(tabId) { 212 const normalizedTab = String(tabId || '').trim(); 213 if (!normalizedTab) { 214 return; 215 } 216 217 $firewallTabs.each(function () { 218 const $tab = $(this); 219 const isActive = String($tab.data('firewallTab')) === normalizedTab; 220 221 $tab 222 .toggleClass('is-active', isActive) 223 .attr('aria-selected', isActive ? 'true' : 'false') 224 .attr('tabindex', isActive ? '0' : '-1'); 225 }); 226 227 $firewallTabPanels.each(function () { 228 const $panel = $(this); 229 const isActive = String($panel.data('firewallTabPanel')) === normalizedTab; 230 231 $panel 232 .toggleClass('is-active', isActive) 233 .prop('hidden', !isActive); 234 }); 235 } 236 124 237 function applySettings(settings) { 125 238 const data = settings || {}; … … 127 240 $firewallEnabled.prop('checked', !!Number(data.enabled || 0)); 128 241 $firewallLoginProtection.prop('checked', !!Number(data.login_protection_enabled || 0)); 242 $firewallCustomLoginSlug.val(String(data.custom_login_slug || '')); 129 243 $firewallWafSqliEnabled.prop('checked', !!Number(data.waf_sqli_enabled || 0)); 130 244 $firewallWafCommandEnabled.prop('checked', !!Number(data.waf_command_injection_enabled || 0)); … … 183 297 $firewallRefresh.prop('disabled', isBusy); 184 298 $firewallClearLogs.prop('disabled', isBusy); 299 $firewallTabs.prop('disabled', isBusy); 185 300 $firewallEnabled.prop('disabled', isBusy); 186 301 $firewallLoginProtection.prop('disabled', isBusy); 302 $firewallCustomLoginSlug.prop('disabled', isBusy); 187 303 $firewallWafSqliEnabled.prop('disabled', isBusy); 188 304 $firewallWafCommandEnabled.prop('disabled', isBusy); … … 197 313 198 314 if (showLoadingNotice) { 199 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');315 setFeedback('info', i18n.firewall_refreshing || 'Refreshing firewall data...'); 200 316 } 201 317 … … 215 331 latestMuLoaderStatus = payload.mu_loader || null; 216 332 applySettings(payload.settings || {}); 333 renderLoginAccess(payload.login_access || {}); 217 334 renderOverview(payload.summary || {}, latestMuLoaderStatus || {}); 218 335 renderLogs(payload.logs || []); … … 227 344 function saveSettings() { 228 345 setBusyState(true); 229 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');346 setFeedback('info', i18n.firewall_saving || 'Saving firewall settings...'); 230 347 231 348 $.post(VulnTitan.ajaxUrl, { … … 234 351 enabled: $firewallEnabled.is(':checked') ? 1 : 0, 235 352 login_protection_enabled: $firewallLoginProtection.is(':checked') ? 1 : 0, 353 custom_login_slug: String($firewallCustomLoginSlug.val() || ''), 236 354 waf_sqli_enabled: $firewallWafSqliEnabled.is(':checked') ? 1 : 0, 237 355 waf_command_injection_enabled: $firewallWafCommandEnabled.is(':checked') ? 1 : 0, … … 252 370 latestMuLoaderStatus = payload.mu_loader || latestMuLoaderStatus; 253 371 applySettings(payload.settings || {}); 372 renderLoginAccess(payload.login_access || {}); 254 373 renderOverview(payload.summary || {}, latestMuLoaderStatus || {}); 255 374 renderLogs(payload.logs || []); 256 375 257 376 const muInstall = payload.mu_loader_install || {}; 377 if (payload.notice) { 378 setFeedback('warning', payload.notice); 379 return; 380 } 381 258 382 if (muInstall.success === false && muInstall.error) { 259 383 setFeedback('warning', muInstall.error); … … 276 400 277 401 setBusyState(true); 278 setFeedback('info', i18n.firewall_ loading || 'Loading firewall data...');402 setFeedback('info', i18n.firewall_clearing || 'Clearing firewall logs...'); 279 403 280 404 $.post(VulnTitan.ajaxUrl, { … … 313 437 }); 314 438 315 loadFirewallData(true); 439 $firewallFeedback.off('click', '[data-firewall-toast-close]').on('click', '[data-firewall-toast-close]', function () { 440 dismissFeedback(); 441 }); 442 443 $firewallTabs.off('click').on('click', function () { 444 activateTab($(this).data('firewallTab')); 445 }); 446 447 $firewallTabs.off('keydown').on('keydown', function (event) { 448 const keys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']; 449 if (keys.indexOf(event.key) === -1) { 450 return; 451 } 452 453 event.preventDefault(); 454 455 const currentIndex = $firewallTabs.index(this); 456 if (currentIndex < 0) { 457 return; 458 } 459 460 let nextIndex = currentIndex; 461 462 if (event.key === 'Home') { 463 nextIndex = 0; 464 } else if (event.key === 'End') { 465 nextIndex = $firewallTabs.length - 1; 466 } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { 467 nextIndex = currentIndex === 0 ? $firewallTabs.length - 1 : currentIndex - 1; 468 } else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { 469 nextIndex = currentIndex === $firewallTabs.length - 1 ? 0 : currentIndex + 1; 470 } 471 472 const $nextTab = $firewallTabs.eq(nextIndex); 473 activateTab($nextTab.data('firewallTab')); 474 $nextTab.trigger('focus'); 475 }); 476 477 activateTab($firewallTabs.filter('.is-active').first().data('firewallTab') || $firewallTabs.first().data('firewallTab')); 478 479 loadFirewallData(false); 316 480 }); -
vulntitan/trunk/includes/Admin/Admin.php
r3479599 r3481425 118 118 'firewall_mu_inactive' => esc_html__('MU loader inactive', 'vulntitan'), 119 119 'firewall_no_logs' => esc_html__('No firewall events recorded yet.', 'vulntitan'), 120 'firewall_custom_login_disabled' => esc_html__('Custom login URL is disabled. Leave the slug blank to keep the default WordPress login endpoints.', 'vulntitan'), 120 121 'firewall_event_unknown' => esc_html__('Unknown event', 'vulntitan'), 121 122 'firewall_event_login_failed' => esc_html__('Login failed', 'vulntitan'), -
vulntitan/trunk/includes/Admin/Ajax.php
r3479541 r3481425 46 46 'summary' => FirewallService::getSummary(24), 47 47 'logs' => FirewallService::getRecentLogs(80), 48 'login_access' => FirewallService::getLoginAccessData(), 48 49 'mu_loader' => FirewallService::getMuLoaderStatus(), 49 50 ]); … … 57 58 wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403); 58 59 } 60 61 $customLoginValidation = FirewallService::validateCustomLoginSlug( 62 isset($_POST['custom_login_slug']) ? (string) wp_unslash($_POST['custom_login_slug']) : '' 63 ); 59 64 60 65 $input = [ 61 66 'enabled' => isset($_POST['enabled']) ? (int) wp_unslash($_POST['enabled']) : 1, 62 67 'login_protection_enabled' => isset($_POST['login_protection_enabled']) ? (int) wp_unslash($_POST['login_protection_enabled']) : 1, 68 'custom_login_slug' => $customLoginValidation['slug'], 63 69 'waf_sqli_enabled' => isset($_POST['waf_sqli_enabled']) ? (int) wp_unslash($_POST['waf_sqli_enabled']) : 1, 64 70 'waf_command_injection_enabled' => isset($_POST['waf_command_injection_enabled']) ? (int) wp_unslash($_POST['waf_command_injection_enabled']) : 1, … … 71 77 $settings = FirewallService::saveSettings($input); 72 78 $muLoaderResult = FirewallService::installMuLoader(); 79 $notice = ''; 80 81 if (($customLoginValidation['error'] ?? '') === 'invalid') { 82 $notice = esc_html__('Custom login slug was not saved because it is invalid or reserved.', 'vulntitan'); 83 } elseif (($customLoginValidation['error'] ?? '') === 'conflict') { 84 $notice = esc_html__('Custom login slug was not saved because that path is already used by existing WordPress content.', 'vulntitan'); 85 } 73 86 74 87 wp_send_json_success([ … … 76 89 'summary' => FirewallService::getSummary(24), 77 90 'logs' => FirewallService::getRecentLogs(80), 91 'login_access' => FirewallService::getLoginAccessData(), 78 92 'mu_loader' => FirewallService::getMuLoaderStatus(), 79 93 'mu_loader_install' => $muLoaderResult, 94 'notice' => $notice, 80 95 ]); 81 96 } -
vulntitan/trunk/includes/Admin/Pages/Firewall.php
r3479599 r3481425 40 40 41 41 <div class="vulntitan-firewall-form"> 42 <div class="vulntitan-firewall-section"> 43 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Core Protection', 'vulntitan'); ?></div> 44 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Master switches for the firewall runtime and login shield.', 'vulntitan'); ?></div> 45 46 <label class="vulntitan-firewall-toggle"> 47 <input type="checkbox" id="vulntitan-firewall-enabled" class="vulntitan-firewall-checkbox" checked> 48 <span><?php esc_html_e('Enable Firewall', 'vulntitan'); ?></span> 49 </label> 50 51 <label class="vulntitan-firewall-toggle"> 52 <input type="checkbox" id="vulntitan-firewall-login-protection" class="vulntitan-firewall-checkbox" checked> 53 <span><?php esc_html_e('Enable Login Protection', 'vulntitan'); ?></span> 54 </label> 55 </div> 56 57 <div class="vulntitan-firewall-section"> 58 <div class="vulntitan-firewall-section-title"><?php esc_html_e('WAF Payload Protection', 'vulntitan'); ?></div> 59 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Inspect request parameters for common SQL injection and command injection payloads.', 'vulntitan'); ?></div> 60 61 <label class="vulntitan-firewall-toggle"> 62 <input type="checkbox" id="vulntitan-firewall-waf-sqli-enabled" class="vulntitan-firewall-checkbox" checked> 63 <span><?php esc_html_e('Enable SQL Injection Rule Set', 'vulntitan'); ?></span> 64 </label> 65 66 <label class="vulntitan-firewall-toggle"> 67 <input type="checkbox" id="vulntitan-firewall-waf-command-enabled" class="vulntitan-firewall-checkbox" checked> 68 <span><?php esc_html_e('Enable Command Injection Rule Set', 'vulntitan'); ?></span> 69 </label> 70 71 <label class="vulntitan-firewall-field"> 72 <span class="vulntitan-firewall-field-label"><?php esc_html_e('WAF Whitelist (Path patterns)', 'vulntitan'); ?></span> 73 <textarea id="vulntitan-firewall-waf-whitelist-paths" class="vulntitan-firewall-input vulntitan-firewall-textarea" rows="5" placeholder="/wp-json/my-plugin/* /custom-safe-endpoint"></textarea> 74 <small class="vulntitan-firewall-field-help"><?php esc_html_e('One pattern per line. Use * wildcard only when needed. Whitelist applies to SQLi/Command rules, not traversal/sensitive-file blocks.', 'vulntitan'); ?></small> 75 </label> 76 </div> 77 78 <div class="vulntitan-firewall-section"> 79 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Login Lockout Policy', 'vulntitan'); ?></div> 80 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Tighten brute force resilience with adaptive limits and retention.', 'vulntitan'); ?></div> 81 82 <label class="vulntitan-firewall-field"> 83 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Max failed attempts', 'vulntitan'); ?></span> 84 <input type="number" id="vulntitan-firewall-max-attempts" class="vulntitan-firewall-input" min="2" max="20" value="5"> 85 </label> 86 87 <label class="vulntitan-firewall-field"> 88 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Lockout duration (minutes)', 'vulntitan'); ?></span> 89 <input type="number" id="vulntitan-firewall-lockout-minutes" class="vulntitan-firewall-input" min="5" max="240" value="15"> 90 </label> 91 92 <label class="vulntitan-firewall-field"> 93 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Log retention (days)', 'vulntitan'); ?></span> 94 <input type="number" id="vulntitan-firewall-log-retention" class="vulntitan-firewall-input" min="1" max="365" value="30"> 95 </label> 42 <div class="vulntitan-firewall-workspace"> 43 <div class="vulntitan-firewall-tabs" role="tablist" aria-label="<?php esc_attr_e('Firewall setting groups', 'vulntitan'); ?>"> 44 <button 45 type="button" 46 class="vulntitan-firewall-tab is-active" 47 id="vulntitan-firewall-tab-access" 48 data-firewall-tab="access" 49 role="tab" 50 aria-selected="true" 51 aria-controls="vulntitan-firewall-panel-access" 52 > 53 <span class="vulntitan-firewall-tab-title"><?php esc_html_e('Access Shield', 'vulntitan'); ?></span> 54 <span class="vulntitan-firewall-tab-desc"><?php esc_html_e('Runtime, login gate, and hidden entry path.', 'vulntitan'); ?></span> 55 </button> 56 <button 57 type="button" 58 class="vulntitan-firewall-tab" 59 id="vulntitan-firewall-tab-waf" 60 data-firewall-tab="waf" 61 role="tab" 62 aria-selected="false" 63 aria-controls="vulntitan-firewall-panel-waf" 64 tabindex="-1" 65 > 66 <span class="vulntitan-firewall-tab-title"><?php esc_html_e('WAF Rules', 'vulntitan'); ?></span> 67 <span class="vulntitan-firewall-tab-desc"><?php esc_html_e('Payload inspection and safe route exceptions.', 'vulntitan'); ?></span> 68 </button> 69 <button 70 type="button" 71 class="vulntitan-firewall-tab" 72 id="vulntitan-firewall-tab-lockouts" 73 data-firewall-tab="lockouts" 74 role="tab" 75 aria-selected="false" 76 aria-controls="vulntitan-firewall-panel-lockouts" 77 tabindex="-1" 78 > 79 <span class="vulntitan-firewall-tab-title"><?php esc_html_e('Lockouts & Logs', 'vulntitan'); ?></span> 80 <span class="vulntitan-firewall-tab-desc"><?php esc_html_e('Brute-force thresholds, lock windows, and retention.', 'vulntitan'); ?></span> 81 </button> 82 </div> 83 84 <div class="vulntitan-firewall-tab-panels"> 85 <section 86 class="vulntitan-firewall-tab-panel is-active" 87 id="vulntitan-firewall-panel-access" 88 data-firewall-tab-panel="access" 89 role="tabpanel" 90 aria-labelledby="vulntitan-firewall-tab-access" 91 > 92 <div class="vulntitan-firewall-tab-panel-head"> 93 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Access Shield', 'vulntitan'); ?></div> 94 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Control the live firewall runtime and hide the default WordPress login endpoints behind a custom entry path.', 'vulntitan'); ?></div> 95 </div> 96 97 <div class="vulntitan-firewall-section"> 98 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Core Protection', 'vulntitan'); ?></div> 99 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Master switches for the firewall runtime and login shield.', 'vulntitan'); ?></div> 100 101 <label class="vulntitan-firewall-toggle"> 102 <input type="checkbox" id="vulntitan-firewall-enabled" class="vulntitan-firewall-checkbox" checked> 103 <span><?php esc_html_e('Enable Firewall', 'vulntitan'); ?></span> 104 </label> 105 106 <label class="vulntitan-firewall-toggle"> 107 <input type="checkbox" id="vulntitan-firewall-login-protection" class="vulntitan-firewall-checkbox" checked> 108 <span><?php esc_html_e('Enable Login Protection', 'vulntitan'); ?></span> 109 </label> 110 </div> 111 112 <div class="vulntitan-firewall-section"> 113 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Hidden Login Route', 'vulntitan'); ?></div> 114 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Replace the public login path with a private slug known only to administrators.', 'vulntitan'); ?></div> 115 116 <label class="vulntitan-firewall-field"> 117 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Custom Login Slug', 'vulntitan'); ?></span> 118 <input type="text" id="vulntitan-firewall-custom-login-slug" class="vulntitan-firewall-input" placeholder="secure-portal" autocomplete="off" spellcheck="false"> 119 <small class="vulntitan-firewall-field-help"><?php esc_html_e('Leave blank to keep the default WordPress login URLs. When set, logged-out visitors hitting wp-login.php or wp-admin will receive a 404 instead of the login screen.', 'vulntitan'); ?></small> 120 <small class="vulntitan-firewall-field-help"><?php esc_html_e('Use a unique slug that is not already used by an existing page.', 'vulntitan'); ?></small> 121 <small class="vulntitan-firewall-field-help"> 122 <?php esc_html_e('Active login URL:', 'vulntitan'); ?> 123 <code id="vulntitan-firewall-custom-login-url" class="vulntitan-firewall-runtime-path"><?php esc_html_e('Disabled', 'vulntitan'); ?></code> 124 </small> 125 </label> 126 </div> 127 </section> 128 129 <section 130 class="vulntitan-firewall-tab-panel" 131 id="vulntitan-firewall-panel-waf" 132 data-firewall-tab-panel="waf" 133 role="tabpanel" 134 aria-labelledby="vulntitan-firewall-tab-waf" 135 hidden 136 > 137 <div class="vulntitan-firewall-tab-panel-head"> 138 <div class="vulntitan-firewall-section-title"><?php esc_html_e('WAF Rules', 'vulntitan'); ?></div> 139 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Tune payload inspection so risky requests are blocked early while known-safe paths stay operational.', 'vulntitan'); ?></div> 140 </div> 141 142 <div class="vulntitan-firewall-section"> 143 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Payload Protection', 'vulntitan'); ?></div> 144 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Inspect request parameters for common SQL injection and command injection payloads.', 'vulntitan'); ?></div> 145 146 <label class="vulntitan-firewall-toggle"> 147 <input type="checkbox" id="vulntitan-firewall-waf-sqli-enabled" class="vulntitan-firewall-checkbox" checked> 148 <span><?php esc_html_e('Enable SQL Injection Rule Set', 'vulntitan'); ?></span> 149 </label> 150 151 <label class="vulntitan-firewall-toggle"> 152 <input type="checkbox" id="vulntitan-firewall-waf-command-enabled" class="vulntitan-firewall-checkbox" checked> 153 <span><?php esc_html_e('Enable Command Injection Rule Set', 'vulntitan'); ?></span> 154 </label> 155 </div> 156 157 <div class="vulntitan-firewall-section"> 158 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Trusted Exceptions', 'vulntitan'); ?></div> 159 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Whitelist routes that should bypass payload rules, without weakening traversal or sensitive-file protection.', 'vulntitan'); ?></div> 160 161 <label class="vulntitan-firewall-field"> 162 <span class="vulntitan-firewall-field-label"><?php esc_html_e('WAF Whitelist (Path patterns)', 'vulntitan'); ?></span> 163 <textarea id="vulntitan-firewall-waf-whitelist-paths" class="vulntitan-firewall-input vulntitan-firewall-textarea" rows="5" placeholder="/wp-json/my-plugin/* /custom-safe-endpoint"></textarea> 164 <small class="vulntitan-firewall-field-help"><?php esc_html_e('One pattern per line. Use * wildcard only when needed. Whitelist applies to SQLi/Command rules, not traversal/sensitive-file blocks.', 'vulntitan'); ?></small> 165 </label> 166 </div> 167 </section> 168 169 <section 170 class="vulntitan-firewall-tab-panel" 171 id="vulntitan-firewall-panel-lockouts" 172 data-firewall-tab-panel="lockouts" 173 role="tabpanel" 174 aria-labelledby="vulntitan-firewall-tab-lockouts" 175 hidden 176 > 177 <div class="vulntitan-firewall-tab-panel-head"> 178 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Lockouts & Logs', 'vulntitan'); ?></div> 179 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Set aggressive but safe thresholds for failed logins and determine how long firewall intelligence stays available in the log store.', 'vulntitan'); ?></div> 180 </div> 181 182 <div class="vulntitan-firewall-section"> 183 <div class="vulntitan-firewall-section-title"><?php esc_html_e('Login Lockout Policy', 'vulntitan'); ?></div> 184 <div class="vulntitan-firewall-section-desc"><?php esc_html_e('Tighten brute force resilience with adaptive limits and retention.', 'vulntitan'); ?></div> 185 186 <div class="vulntitan-firewall-field-grid"> 187 <label class="vulntitan-firewall-field"> 188 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Max failed attempts', 'vulntitan'); ?></span> 189 <input type="number" id="vulntitan-firewall-max-attempts" class="vulntitan-firewall-input" min="2" max="20" value="5"> 190 </label> 191 192 <label class="vulntitan-firewall-field"> 193 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Lockout duration (minutes)', 'vulntitan'); ?></span> 194 <input type="number" id="vulntitan-firewall-lockout-minutes" class="vulntitan-firewall-input" min="5" max="240" value="15"> 195 </label> 196 197 <label class="vulntitan-firewall-field"> 198 <span class="vulntitan-firewall-field-label"><?php esc_html_e('Log retention (days)', 'vulntitan'); ?></span> 199 <input type="number" id="vulntitan-firewall-log-retention" class="vulntitan-firewall-input" min="1" max="365" value="30"> 200 </label> 201 </div> 202 </div> 203 </section> 204 </div> 96 205 </div> 97 206 </div> -
vulntitan/trunk/includes/Plugin.php
r3469591 r3481425 5 5 use VulnTitan\Admin\Admin; 6 6 use VulnTitan\Services\FirewallService; 7 use VulnTitan\Services\LoginAccessService; 7 8 8 9 class Plugin { … … 22 23 $this->ensure_firewall_components(); 23 24 $this->register_scheduled_events(); 25 LoginAccessService::boot(); 24 26 $this->admin = new Admin(); 25 27 $this->admin->init(); -
vulntitan/trunk/includes/Services/FirewallService.php
r3469591 r3481425 21 21 'enabled' => 1, 22 22 'login_protection_enabled' => 1, 23 'custom_login_slug' => '', 23 24 'waf_sqli_enabled' => 1, 24 25 'waf_command_injection_enabled' => 1, … … 50 51 51 52 return $normalized; 53 } 54 55 public static function getCustomLoginSlug(): string 56 { 57 $settings = self::getSettings(); 58 59 return (string) ($settings['custom_login_slug'] ?? ''); 60 } 61 62 public static function isCustomLoginEnabled(): bool 63 { 64 return self::getCustomLoginSlug() !== ''; 65 } 66 67 public static function getCustomLoginUrl(array $queryArgs = [], string $scheme = 'login'): string 68 { 69 $slug = self::getCustomLoginSlug(); 70 if ($slug === '') { 71 return ''; 72 } 73 74 $url = home_url('/' . user_trailingslashit($slug), $scheme); 75 76 if ($queryArgs) { 77 $url = add_query_arg($queryArgs, $url); 78 } 79 80 return $url; 81 } 82 83 public static function getLoginAccessData(): array 84 { 85 $slug = self::getCustomLoginSlug(); 86 87 return [ 88 'enabled' => $slug !== '', 89 'slug' => $slug, 90 'url' => $slug !== '' ? self::getCustomLoginUrl() : '', 91 ]; 92 } 93 94 public static function validateCustomLoginSlug($input): array 95 { 96 $raw = is_scalar($input) ? trim((string) $input) : ''; 97 $slug = self::sanitizeCustomLoginSlug($input); 98 99 if ($raw !== '' && $slug === '') { 100 return [ 101 'slug' => '', 102 'error' => 'invalid', 103 ]; 104 } 105 106 if ($slug !== '' && self::customLoginSlugConflicts($slug)) { 107 return [ 108 'slug' => '', 109 'error' => 'conflict', 110 ]; 111 } 112 113 return [ 114 'slug' => $slug, 115 'error' => '', 116 ]; 52 117 } 53 118 … … 553 618 'enabled' => !empty($settings['enabled']) ? 1 : 0, 554 619 'login_protection_enabled' => !empty($settings['login_protection_enabled']) ? 1 : 0, 620 'custom_login_slug' => self::sanitizeCustomLoginSlug($settings['custom_login_slug'] ?? $defaults['custom_login_slug']), 555 621 'waf_sqli_enabled' => !empty($settings['waf_sqli_enabled']) ? 1 : 0, 556 622 'waf_command_injection_enabled' => !empty($settings['waf_command_injection_enabled']) ? 1 : 0, … … 562 628 } 563 629 630 public static function sanitizeCustomLoginSlug($input): string 631 { 632 if (!is_scalar($input)) { 633 return ''; 634 } 635 636 $value = trim((string) $input); 637 if ($value === '') { 638 return ''; 639 } 640 641 $value = preg_replace('/[\?#].*$/', '', $value); 642 $value = str_replace('\\', '/', (string) $value); 643 $value = preg_replace('#/+#', '/', (string) $value); 644 $value = trim((string) $value, "/ \t\n\r\0\x0B."); 645 646 if ($value === '') { 647 return ''; 648 } 649 650 $segments = explode('/', $value); 651 $normalized = []; 652 653 foreach ($segments as $segment) { 654 $segment = sanitize_title_with_dashes((string) $segment, '', 'save'); 655 $segment = trim($segment, '-'); 656 657 if ($segment === '') { 658 continue; 659 } 660 661 $normalized[] = substr($segment, 0, 40); 662 663 if (count($normalized) >= 4) { 664 break; 665 } 666 } 667 668 if (!$normalized) { 669 return ''; 670 } 671 672 $slug = implode('/', $normalized); 673 $slug = substr($slug, 0, 90); 674 $slug = trim($slug, '/'); 675 676 if ($slug === '') { 677 return ''; 678 } 679 680 $reservedPaths = [ 681 'admin', 682 'admin-ajax.php', 683 'admin-post.php', 684 'feed', 685 'index.php', 686 'login', 687 'robots.txt', 688 'sitemap.xml', 689 'wp-admin', 690 'wp-json', 691 'wp-login', 692 'wp-login.php', 693 'xmlrpc.php', 694 ]; 695 696 if (in_array($slug, $reservedPaths, true)) { 697 return ''; 698 } 699 700 if (preg_match('#(^|/)(?:wp-admin|wp-login(?:\.php)?|wp-json|xmlrpc(?:\.php)?|admin-ajax(?:\.php)?|admin-post(?:\.php)?)(/|$)#', $slug)) { 701 return ''; 702 } 703 704 return $slug; 705 } 706 564 707 protected static function sanitizeWhitelistPaths($input): array 565 708 { … … 598 741 599 742 return array_keys($normalized); 743 } 744 745 protected static function customLoginSlugConflicts(string $slug): bool 746 { 747 if ($slug === '') { 748 return false; 749 } 750 751 if (function_exists('url_to_postid')) { 752 $postId = url_to_postid(home_url('/' . user_trailingslashit($slug))); 753 if ($postId > 0) { 754 return true; 755 } 756 } 757 758 if (function_exists('get_page_by_path')) { 759 $page = get_page_by_path($slug, OBJECT, ['page']); 760 if ($page instanceof \WP_Post) { 761 return true; 762 } 763 } 764 765 return false; 600 766 } 601 767 -
vulntitan/trunk/readme.txt
r3480442 r3481425 4 4 Tested up to: 6.9 5 5 Requires PHP: 7.4 6 Stable tag: 2.0. 56 Stable tag: 2.0.6 7 7 License: GPLv2 8 8 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 16 16 Instantly scan your WordPress site for malware infections and known vulnerabilities, review detailed results, and clean or remove malware safely using a guided fix workflow with automatic backups. 17 17 18 Unlike heavy security suites, VulnTitan focuses on practical protection: vulnerability detection, malware scanning and removal, file integrity monitoring, and essential firewall protection— without unnecessary bloat.18 Unlike heavy security suites, VulnTitan focuses on practical protection: vulnerability detection, malware scanning and removal, file integrity monitoring, essential firewall protection, and hidden custom login access — without unnecessary bloat. 19 19 20 20 = Malware Scanner = … … 47 47 = Firewall & Login Protection = 48 48 49 VulnTitan includes lightweight firewall and WAF protection to block common attack patterns .49 VulnTitan includes lightweight firewall and WAF protection to block common attack patterns and protect the WordPress login surface. 50 50 51 51 - Early MU-plugin runtime request guards … … 55 55 - Endpoint whitelisting controls 56 56 - Login lockout protection against brute-force attacks 57 - Configurable custom login slug so administrators can use a private login URL instead of the default `wp-login.php` 58 - Default `wp-login.php` and guest `wp-admin` access can be hidden behind a `404` response when custom login is enabled 57 59 58 60 = Security-First Architecture = … … 86 88 5. Firewall and WAF protection settings panel. 87 89 6. Vulnerability scan progress bar. 90 7. Firewall hidden custom login configuration and activity overview. 91 7. When custom login url is set and user goes to wp-login.php or wp-admin route. 88 92 89 93 == Installation == … … 130 134 == Changelog == 131 135 136 = v2.0.6 - 12 Mar, 2026 = 137 * Added configurable custom login slug support so administrators can use a private login URL instead of the default `wp-login.php` path. 138 * Hidden direct guest access to default `wp-login.php` and `wp-admin` entry points when custom login protection is enabled. 139 * Reworked the Firewall page with a tabbed settings layout, a wider recent events section, and toast-style action feedback. 140 132 141 = v2.0.4 - 10 Mar, 2026 = 133 142 * Redesigned the VulnTitan Dashboard into an elite, professional security command center layout. -
vulntitan/trunk/vulntitan.php
r3480442 r3481425 4 4 * Plugin URI: https://vulntitan.com/vulntitan/ 5 5 * Description: VulnTitan is a lightweight WordPress security plugin with vulnerability scanning, malware detection, file integrity monitoring, and a built-in firewall with WAF payload rules and login protection. 6 * Version: 2.0. 56 * Version: 2.0.6 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.0. 5');32 define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.0.6'); 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.