Plugin Directory

Changeset 3481425


Ignore:
Timestamp:
03/12/2026 07:08:56 PM (3 weeks ago)
Author:
jerryscg
Message:

Release 2.0.6: add custom login protection and firewall UX updates

Location:
vulntitan
Files:
3 added
21 edited
4 copied

Legend:

Unmodified
Added
Removed
  • vulntitan/tags/2.0.6/CHANGELOG.md

    r3480442 r3481425  
    55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
    66and 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.
    718
    819## [2.0.5] - 2026-03-11
  • vulntitan/tags/2.0.6/assets/css/admin.css

    r3479599 r3481425  
    578578
    579579.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;
    586597    border: 1px solid #1f3346;
    587     background: #101a24;
     598    background: rgba(10, 18, 28, 0.96);
    588599    color: #bfdbfe;
    589600    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 {
    594660    border-color: #1f5131;
    595     background: #0f1d16;
     661    background: rgba(11, 29, 21, 0.96);
    596662    color: #86efac;
    597663}
    598664
    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 {
    600672    border-color: #5f1f1f;
    601     background: #221416;
     673    background: rgba(34, 20, 22, 0.97);
    602674    color: #fecaca;
    603675}
    604676
    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 {
    606684    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);
    608692    color: #fde68a;
    609693}
     
    17551839
    17561840.vulntitan-wrapper--firewall .vulntitan-firewall-feedback {
    1757     margin-top: 18px;
     1841    margin-top: 0;
    17581842}
    17591843
    17601844.vulntitan-wrapper--firewall .vulntitan-firewall-grid {
    17611845    margin-top: 22px;
    1762     gap: 18px;
     1846    grid-template-columns: minmax(0, 1fr);
     1847    gap: 22px;
    17631848}
    17641849
     
    17681853    border-radius: 16px;
    17691854    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));
    17701870}
    17711871
    17721872.vulntitan-firewall-panel-head {
    17731873    display: flex;
    1774     align-items: center;
     1874    align-items: flex-start;
    17751875    justify-content: space-between;
    17761876    gap: 12px;
    17771877    margin-bottom: 16px;
     1878    flex-wrap: wrap;
    17781879}
    17791880
     
    17851886}
    17861887
     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
    17871996.vulntitan-firewall-section {
    1788     padding: 12px 14px;
    1789     border-radius: 12px;
     1997    padding: 16px 18px;
     1998    border-radius: 14px;
    17901999    border: 1px solid rgba(90, 176, 255, 0.18);
    17912000    background: rgba(9, 14, 20, 0.7);
    17922001    display: grid;
    1793     gap: 10px;
     2002    gap: 12px;
    17942003}
    17952004
     
    18332042.vulntitan-wrapper--firewall .vulntitan-firewall-field-help {
    18342043    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;
    18352054}
    18362055
     
    18662085
    18672086.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;
    18712093}
    18722094
    18732095.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);
    18772102}
    18782103
    18792104.vulntitan-wrapper--firewall .vulntitan-firewall-log-request {
    18802105    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;
    18812121}
    18822122
     
    18852125        grid-template-columns: 1fr;
    18862126    }
     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    }
    18872137}
    18882138
    18892139@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
    18902166    .vulntitan-wrapper--firewall {
    18912167        padding: 1.25rem;
    18922168    }
    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  
    578578
    579579.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;
    586597    border: 1px solid #1f3346;
    587     background: #101a24;
     598    background: rgba(10, 18, 28, 0.96);
    588599    color: #bfdbfe;
    589600    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 {
    594660    border-color: #1f5131;
    595     background: #0f1d16;
     661    background: rgba(11, 29, 21, 0.96);
    596662    color: #86efac;
    597663}
    598664
    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 {
    600672    border-color: #5f1f1f;
    601     background: #221416;
     673    background: rgba(34, 20, 22, 0.97);
    602674    color: #fecaca;
    603675}
    604676
    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 {
    606684    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);
    608692    color: #fde68a;
    609693}
     
    17551839
    17561840.vulntitan-wrapper--firewall .vulntitan-firewall-feedback {
    1757     margin-top: 18px;
     1841    margin-top: 0;
    17581842}
    17591843
    17601844.vulntitan-wrapper--firewall .vulntitan-firewall-grid {
    17611845    margin-top: 22px;
    1762     gap: 18px;
     1846    grid-template-columns: minmax(0, 1fr);
     1847    gap: 22px;
    17631848}
    17641849
     
    17681853    border-radius: 16px;
    17691854    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));
    17701870}
    17711871
    17721872.vulntitan-firewall-panel-head {
    17731873    display: flex;
    1774     align-items: center;
     1874    align-items: flex-start;
    17751875    justify-content: space-between;
    17761876    gap: 12px;
    17771877    margin-bottom: 16px;
     1878    flex-wrap: wrap;
    17781879}
    17791880
     
    17851886}
    17861887
     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
    17871996.vulntitan-firewall-section {
    1788     padding: 12px 14px;
    1789     border-radius: 12px;
     1997    padding: 16px 18px;
     1998    border-radius: 14px;
    17901999    border: 1px solid rgba(90, 176, 255, 0.18);
    17912000    background: rgba(9, 14, 20, 0.7);
    17922001    display: grid;
    1793     gap: 10px;
     2002    gap: 12px;
    17942003}
    17952004
     
    18332042.vulntitan-wrapper--firewall .vulntitan-firewall-field-help {
    18342043    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;
    18352054}
    18362055
     
    18662085
    18672086.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;
    18712093}
    18722094
    18732095.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);
    18772102}
    18782103
    18792104.vulntitan-wrapper--firewall .vulntitan-firewall-log-request {
    18802105    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;
    18812121}
    18822122
     
    18852125        grid-template-columns: 1fr;
    18862126    }
     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    }
    18872137}
    18882138
    18892139@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
    18902166    .vulntitan-wrapper--firewall {
    18912167        padding: 1.25rem;
    18922168    }
    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  
    1111    const $firewallEnabled = $('#vulntitan-firewall-enabled');
    1212    const $firewallLoginProtection = $('#vulntitan-firewall-login-protection');
     13    const $firewallCustomLoginSlug = $('#vulntitan-firewall-custom-login-slug');
     14    const $firewallCustomLoginUrl = $('#vulntitan-firewall-custom-login-url');
    1315    const $firewallWafSqliEnabled = $('#vulntitan-firewall-waf-sqli-enabled');
    1416    const $firewallWafCommandEnabled = $('#vulntitan-firewall-waf-command-enabled');
     
    2022    const $firewallRefresh = $('#vulntitan-firewall-refresh');
    2123    const $firewallClearLogs = $('#vulntitan-firewall-clear-logs');
     24    const $firewallTabs = $firewallRoot.find('[data-firewall-tab]');
     25    const $firewallTabPanels = $firewallRoot.find('[data-firewall-tab-panel]');
    2226    const i18n = (window.VulnTitan && window.VulnTitan.i18n) ? window.VulnTitan.i18n : {};
    2327    let latestMuLoaderStatus = null;
     28    let feedbackTimer = null;
     29    let feedbackHideTimer = null;
    2430
    2531    function escapeHtml(str) {
     
    3541    }
    3642
     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
    3790    function setFeedback(type, message) {
    3891        if (!message) {
    39             $firewallFeedback.hide().empty();
     92            dismissFeedback();
    4093            return;
    4194        }
    4295
    4396        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();
    44101
    45102        $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            `)
    47110            .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        }
    48121    }
    49122
     
    122195    }
    123196
     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
    124237    function applySettings(settings) {
    125238        const data = settings || {};
     
    127240        $firewallEnabled.prop('checked', !!Number(data.enabled || 0));
    128241        $firewallLoginProtection.prop('checked', !!Number(data.login_protection_enabled || 0));
     242        $firewallCustomLoginSlug.val(String(data.custom_login_slug || ''));
    129243        $firewallWafSqliEnabled.prop('checked', !!Number(data.waf_sqli_enabled || 0));
    130244        $firewallWafCommandEnabled.prop('checked', !!Number(data.waf_command_injection_enabled || 0));
     
    183297        $firewallRefresh.prop('disabled', isBusy);
    184298        $firewallClearLogs.prop('disabled', isBusy);
     299        $firewallTabs.prop('disabled', isBusy);
    185300        $firewallEnabled.prop('disabled', isBusy);
    186301        $firewallLoginProtection.prop('disabled', isBusy);
     302        $firewallCustomLoginSlug.prop('disabled', isBusy);
    187303        $firewallWafSqliEnabled.prop('disabled', isBusy);
    188304        $firewallWafCommandEnabled.prop('disabled', isBusy);
     
    197313
    198314        if (showLoadingNotice) {
    199             setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     315            setFeedback('info', i18n.firewall_refreshing || 'Refreshing firewall data...');
    200316        }
    201317
     
    215331            latestMuLoaderStatus = payload.mu_loader || null;
    216332            applySettings(payload.settings || {});
     333            renderLoginAccess(payload.login_access || {});
    217334            renderOverview(payload.summary || {}, latestMuLoaderStatus || {});
    218335            renderLogs(payload.logs || []);
     
    227344    function saveSettings() {
    228345        setBusyState(true);
    229         setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     346        setFeedback('info', i18n.firewall_saving || 'Saving firewall settings...');
    230347
    231348        $.post(VulnTitan.ajaxUrl, {
     
    234351            enabled: $firewallEnabled.is(':checked') ? 1 : 0,
    235352            login_protection_enabled: $firewallLoginProtection.is(':checked') ? 1 : 0,
     353            custom_login_slug: String($firewallCustomLoginSlug.val() || ''),
    236354            waf_sqli_enabled: $firewallWafSqliEnabled.is(':checked') ? 1 : 0,
    237355            waf_command_injection_enabled: $firewallWafCommandEnabled.is(':checked') ? 1 : 0,
     
    252370            latestMuLoaderStatus = payload.mu_loader || latestMuLoaderStatus;
    253371            applySettings(payload.settings || {});
     372            renderLoginAccess(payload.login_access || {});
    254373            renderOverview(payload.summary || {}, latestMuLoaderStatus || {});
    255374            renderLogs(payload.logs || []);
    256375
    257376            const muInstall = payload.mu_loader_install || {};
     377            if (payload.notice) {
     378                setFeedback('warning', payload.notice);
     379                return;
     380            }
     381
    258382            if (muInstall.success === false && muInstall.error) {
    259383                setFeedback('warning', muInstall.error);
     
    276400
    277401        setBusyState(true);
    278         setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     402        setFeedback('info', i18n.firewall_clearing || 'Clearing firewall logs...');
    279403
    280404        $.post(VulnTitan.ajaxUrl, {
     
    313437    });
    314438
    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);
    316480});
  • vulntitan/tags/2.0.6/assets/js/firewall.min.js

    r3469591 r3481425  
    1111    const $firewallEnabled = $('#vulntitan-firewall-enabled');
    1212    const $firewallLoginProtection = $('#vulntitan-firewall-login-protection');
     13    const $firewallCustomLoginSlug = $('#vulntitan-firewall-custom-login-slug');
     14    const $firewallCustomLoginUrl = $('#vulntitan-firewall-custom-login-url');
    1315    const $firewallWafSqliEnabled = $('#vulntitan-firewall-waf-sqli-enabled');
    1416    const $firewallWafCommandEnabled = $('#vulntitan-firewall-waf-command-enabled');
     
    2022    const $firewallRefresh = $('#vulntitan-firewall-refresh');
    2123    const $firewallClearLogs = $('#vulntitan-firewall-clear-logs');
     24    const $firewallTabs = $firewallRoot.find('[data-firewall-tab]');
     25    const $firewallTabPanels = $firewallRoot.find('[data-firewall-tab-panel]');
    2226    const i18n = (window.VulnTitan && window.VulnTitan.i18n) ? window.VulnTitan.i18n : {};
    2327    let latestMuLoaderStatus = null;
     28    let feedbackTimer = null;
     29    let feedbackHideTimer = null;
    2430
    2531    function escapeHtml(str) {
     
    3541    }
    3642
     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
    3790    function setFeedback(type, message) {
    3891        if (!message) {
    39             $firewallFeedback.hide().empty();
     92            dismissFeedback();
    4093            return;
    4194        }
    4295
    4396        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();
    44101
    45102        $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            `)
    47110            .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        }
    48121    }
    49122
     
    122195    }
    123196
     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
    124237    function applySettings(settings) {
    125238        const data = settings || {};
     
    127240        $firewallEnabled.prop('checked', !!Number(data.enabled || 0));
    128241        $firewallLoginProtection.prop('checked', !!Number(data.login_protection_enabled || 0));
     242        $firewallCustomLoginSlug.val(String(data.custom_login_slug || ''));
    129243        $firewallWafSqliEnabled.prop('checked', !!Number(data.waf_sqli_enabled || 0));
    130244        $firewallWafCommandEnabled.prop('checked', !!Number(data.waf_command_injection_enabled || 0));
     
    183297        $firewallRefresh.prop('disabled', isBusy);
    184298        $firewallClearLogs.prop('disabled', isBusy);
     299        $firewallTabs.prop('disabled', isBusy);
    185300        $firewallEnabled.prop('disabled', isBusy);
    186301        $firewallLoginProtection.prop('disabled', isBusy);
     302        $firewallCustomLoginSlug.prop('disabled', isBusy);
    187303        $firewallWafSqliEnabled.prop('disabled', isBusy);
    188304        $firewallWafCommandEnabled.prop('disabled', isBusy);
     
    197313
    198314        if (showLoadingNotice) {
    199             setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     315            setFeedback('info', i18n.firewall_refreshing || 'Refreshing firewall data...');
    200316        }
    201317
     
    215331            latestMuLoaderStatus = payload.mu_loader || null;
    216332            applySettings(payload.settings || {});
     333            renderLoginAccess(payload.login_access || {});
    217334            renderOverview(payload.summary || {}, latestMuLoaderStatus || {});
    218335            renderLogs(payload.logs || []);
     
    227344    function saveSettings() {
    228345        setBusyState(true);
    229         setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     346        setFeedback('info', i18n.firewall_saving || 'Saving firewall settings...');
    230347
    231348        $.post(VulnTitan.ajaxUrl, {
     
    234351            enabled: $firewallEnabled.is(':checked') ? 1 : 0,
    235352            login_protection_enabled: $firewallLoginProtection.is(':checked') ? 1 : 0,
     353            custom_login_slug: String($firewallCustomLoginSlug.val() || ''),
    236354            waf_sqli_enabled: $firewallWafSqliEnabled.is(':checked') ? 1 : 0,
    237355            waf_command_injection_enabled: $firewallWafCommandEnabled.is(':checked') ? 1 : 0,
     
    252370            latestMuLoaderStatus = payload.mu_loader || latestMuLoaderStatus;
    253371            applySettings(payload.settings || {});
     372            renderLoginAccess(payload.login_access || {});
    254373            renderOverview(payload.summary || {}, latestMuLoaderStatus || {});
    255374            renderLogs(payload.logs || []);
    256375
    257376            const muInstall = payload.mu_loader_install || {};
     377            if (payload.notice) {
     378                setFeedback('warning', payload.notice);
     379                return;
     380            }
     381
    258382            if (muInstall.success === false && muInstall.error) {
    259383                setFeedback('warning', muInstall.error);
     
    276400
    277401        setBusyState(true);
    278         setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     402        setFeedback('info', i18n.firewall_clearing || 'Clearing firewall logs...');
    279403
    280404        $.post(VulnTitan.ajaxUrl, {
     
    313437    });
    314438
    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);
    316480});
  • vulntitan/tags/2.0.6/includes/Admin/Admin.php

    r3479599 r3481425  
    118118                    'firewall_mu_inactive' => esc_html__('MU loader inactive', 'vulntitan'),
    119119                    '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'),
    120121                    'firewall_event_unknown' => esc_html__('Unknown event', 'vulntitan'),
    121122                    'firewall_event_login_failed' => esc_html__('Login failed', 'vulntitan'),
  • vulntitan/tags/2.0.6/includes/Admin/Ajax.php

    r3479541 r3481425  
    4646            'summary' => FirewallService::getSummary(24),
    4747            'logs' => FirewallService::getRecentLogs(80),
     48            'login_access' => FirewallService::getLoginAccessData(),
    4849            'mu_loader' => FirewallService::getMuLoaderStatus(),
    4950        ]);
     
    5758            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
    5859        }
     60
     61        $customLoginValidation = FirewallService::validateCustomLoginSlug(
     62            isset($_POST['custom_login_slug']) ? (string) wp_unslash($_POST['custom_login_slug']) : ''
     63        );
    5964
    6065        $input = [
    6166            'enabled' => isset($_POST['enabled']) ? (int) wp_unslash($_POST['enabled']) : 1,
    6267            'login_protection_enabled' => isset($_POST['login_protection_enabled']) ? (int) wp_unslash($_POST['login_protection_enabled']) : 1,
     68            'custom_login_slug' => $customLoginValidation['slug'],
    6369            'waf_sqli_enabled' => isset($_POST['waf_sqli_enabled']) ? (int) wp_unslash($_POST['waf_sqli_enabled']) : 1,
    6470            'waf_command_injection_enabled' => isset($_POST['waf_command_injection_enabled']) ? (int) wp_unslash($_POST['waf_command_injection_enabled']) : 1,
     
    7177        $settings = FirewallService::saveSettings($input);
    7278        $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        }
    7386
    7487        wp_send_json_success([
     
    7689            'summary' => FirewallService::getSummary(24),
    7790            'logs' => FirewallService::getRecentLogs(80),
     91            'login_access' => FirewallService::getLoginAccessData(),
    7892            'mu_loader' => FirewallService::getMuLoaderStatus(),
    7993            'mu_loader_install' => $muLoaderResult,
     94            'notice' => $notice,
    8095        ]);
    8196    }
  • vulntitan/tags/2.0.6/includes/Admin/Pages/Firewall.php

    r3479599 r3481425  
    4040
    4141                                <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/*&#10;/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/*&#10;/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>
    96205                                    </div>
    97206                                </div>
  • vulntitan/tags/2.0.6/includes/Plugin.php

    r3469591 r3481425  
    55use VulnTitan\Admin\Admin;
    66use VulnTitan\Services\FirewallService;
     7use VulnTitan\Services\LoginAccessService;
    78
    89class Plugin {
     
    2223        $this->ensure_firewall_components();
    2324        $this->register_scheduled_events();
     25        LoginAccessService::boot();
    2426        $this->admin = new Admin();
    2527        $this->admin->init();
  • vulntitan/tags/2.0.6/includes/Services/FirewallService.php

    r3469591 r3481425  
    2121            'enabled' => 1,
    2222            'login_protection_enabled' => 1,
     23            'custom_login_slug' => '',
    2324            'waf_sqli_enabled' => 1,
    2425            'waf_command_injection_enabled' => 1,
     
    5051
    5152        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        ];
    52117    }
    53118
     
    553618            'enabled' => !empty($settings['enabled']) ? 1 : 0,
    554619            'login_protection_enabled' => !empty($settings['login_protection_enabled']) ? 1 : 0,
     620            'custom_login_slug' => self::sanitizeCustomLoginSlug($settings['custom_login_slug'] ?? $defaults['custom_login_slug']),
    555621            'waf_sqli_enabled' => !empty($settings['waf_sqli_enabled']) ? 1 : 0,
    556622            'waf_command_injection_enabled' => !empty($settings['waf_command_injection_enabled']) ? 1 : 0,
     
    562628    }
    563629
     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
    564707    protected static function sanitizeWhitelistPaths($input): array
    565708    {
     
    598741
    599742        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;
    600766    }
    601767
  • vulntitan/tags/2.0.6/readme.txt

    r3480442 r3481425  
    44Tested up to: 6.9
    55Requires PHP: 7.4
    6 Stable tag: 2.0.5
     6Stable tag: 2.0.6
    77License: GPLv2
    88License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    1616Instantly 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.
    1717
    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.
     18Unlike 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.
    1919
    2020= Malware Scanner =
     
    4747= Firewall & Login Protection =
    4848
    49 VulnTitan includes lightweight firewall and WAF protection to block common attack patterns.
     49VulnTitan includes lightweight firewall and WAF protection to block common attack patterns and protect the WordPress login surface.
    5050
    5151- Early MU-plugin runtime request guards
     
    5555- Endpoint whitelisting controls
    5656- 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
    5759
    5860= Security-First Architecture =
     
    86885. Firewall and WAF protection settings panel.
    87896. Vulnerability scan progress bar.
     907. Firewall hidden custom login configuration and activity overview.
     917. When custom login url is set and user goes to wp-login.php or wp-admin route.
    8892
    8993== Installation ==
     
    130134== Changelog ==
    131135
     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
    132141= v2.0.4 - 10 Mar, 2026 =
    133142* Redesigned the VulnTitan Dashboard into an elite, professional security command center layout.
  • vulntitan/tags/2.0.6/vulntitan.php

    r3480442 r3481425  
    44 * Plugin URI: https://vulntitan.com/vulntitan/
    55 * 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.5
     6 * Version: 2.0.6
    77 * Author: Jaroslav Svetlik
    88 * Author URI: https://vulntitan.com
     
    3030
    3131// Define plugin constants
    32 define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.0.5');
     32define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.0.6');
    3333define('VULNTITAN_PLUGIN_BASENAME', plugin_basename(__FILE__));
    3434define('VULNTITAN_PLUGIN_DIR', untrailingslashit(plugin_dir_path(__FILE__)));
  • vulntitan/trunk/CHANGELOG.md

    r3480442 r3481425  
    55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
    66and 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.
    718
    819## [2.0.5] - 2026-03-11
  • vulntitan/trunk/assets/css/admin.css

    r3479599 r3481425  
    578578
    579579.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;
    586597    border: 1px solid #1f3346;
    587     background: #101a24;
     598    background: rgba(10, 18, 28, 0.96);
    588599    color: #bfdbfe;
    589600    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 {
    594660    border-color: #1f5131;
    595     background: #0f1d16;
     661    background: rgba(11, 29, 21, 0.96);
    596662    color: #86efac;
    597663}
    598664
    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 {
    600672    border-color: #5f1f1f;
    601     background: #221416;
     673    background: rgba(34, 20, 22, 0.97);
    602674    color: #fecaca;
    603675}
    604676
    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 {
    606684    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);
    608692    color: #fde68a;
    609693}
     
    17551839
    17561840.vulntitan-wrapper--firewall .vulntitan-firewall-feedback {
    1757     margin-top: 18px;
     1841    margin-top: 0;
    17581842}
    17591843
    17601844.vulntitan-wrapper--firewall .vulntitan-firewall-grid {
    17611845    margin-top: 22px;
    1762     gap: 18px;
     1846    grid-template-columns: minmax(0, 1fr);
     1847    gap: 22px;
    17631848}
    17641849
     
    17681853    border-radius: 16px;
    17691854    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));
    17701870}
    17711871
    17721872.vulntitan-firewall-panel-head {
    17731873    display: flex;
    1774     align-items: center;
     1874    align-items: flex-start;
    17751875    justify-content: space-between;
    17761876    gap: 12px;
    17771877    margin-bottom: 16px;
     1878    flex-wrap: wrap;
    17781879}
    17791880
     
    17851886}
    17861887
     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
    17871996.vulntitan-firewall-section {
    1788     padding: 12px 14px;
    1789     border-radius: 12px;
     1997    padding: 16px 18px;
     1998    border-radius: 14px;
    17901999    border: 1px solid rgba(90, 176, 255, 0.18);
    17912000    background: rgba(9, 14, 20, 0.7);
    17922001    display: grid;
    1793     gap: 10px;
     2002    gap: 12px;
    17942003}
    17952004
     
    18332042.vulntitan-wrapper--firewall .vulntitan-firewall-field-help {
    18342043    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;
    18352054}
    18362055
     
    18662085
    18672086.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;
    18712093}
    18722094
    18732095.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);
    18772102}
    18782103
    18792104.vulntitan-wrapper--firewall .vulntitan-firewall-log-request {
    18802105    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;
    18812121}
    18822122
     
    18852125        grid-template-columns: 1fr;
    18862126    }
     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    }
    18872137}
    18882138
    18892139@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
    18902166    .vulntitan-wrapper--firewall {
    18912167        padding: 1.25rem;
    18922168    }
    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  
    578578
    579579.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;
    586597    border: 1px solid #1f3346;
    587     background: #101a24;
     598    background: rgba(10, 18, 28, 0.96);
    588599    color: #bfdbfe;
    589600    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 {
    594660    border-color: #1f5131;
    595     background: #0f1d16;
     661    background: rgba(11, 29, 21, 0.96);
    596662    color: #86efac;
    597663}
    598664
    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 {
    600672    border-color: #5f1f1f;
    601     background: #221416;
     673    background: rgba(34, 20, 22, 0.97);
    602674    color: #fecaca;
    603675}
    604676
    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 {
    606684    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);
    608692    color: #fde68a;
    609693}
     
    17551839
    17561840.vulntitan-wrapper--firewall .vulntitan-firewall-feedback {
    1757     margin-top: 18px;
     1841    margin-top: 0;
    17581842}
    17591843
    17601844.vulntitan-wrapper--firewall .vulntitan-firewall-grid {
    17611845    margin-top: 22px;
    1762     gap: 18px;
     1846    grid-template-columns: minmax(0, 1fr);
     1847    gap: 22px;
    17631848}
    17641849
     
    17681853    border-radius: 16px;
    17691854    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));
    17701870}
    17711871
    17721872.vulntitan-firewall-panel-head {
    17731873    display: flex;
    1774     align-items: center;
     1874    align-items: flex-start;
    17751875    justify-content: space-between;
    17761876    gap: 12px;
    17771877    margin-bottom: 16px;
     1878    flex-wrap: wrap;
    17781879}
    17791880
     
    17851886}
    17861887
     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
    17871996.vulntitan-firewall-section {
    1788     padding: 12px 14px;
    1789     border-radius: 12px;
     1997    padding: 16px 18px;
     1998    border-radius: 14px;
    17901999    border: 1px solid rgba(90, 176, 255, 0.18);
    17912000    background: rgba(9, 14, 20, 0.7);
    17922001    display: grid;
    1793     gap: 10px;
     2002    gap: 12px;
    17942003}
    17952004
     
    18332042.vulntitan-wrapper--firewall .vulntitan-firewall-field-help {
    18342043    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;
    18352054}
    18362055
     
    18662085
    18672086.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;
    18712093}
    18722094
    18732095.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);
    18772102}
    18782103
    18792104.vulntitan-wrapper--firewall .vulntitan-firewall-log-request {
    18802105    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;
    18812121}
    18822122
     
    18852125        grid-template-columns: 1fr;
    18862126    }
     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    }
    18872137}
    18882138
    18892139@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
    18902166    .vulntitan-wrapper--firewall {
    18912167        padding: 1.25rem;
    18922168    }
    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  
    1111    const $firewallEnabled = $('#vulntitan-firewall-enabled');
    1212    const $firewallLoginProtection = $('#vulntitan-firewall-login-protection');
     13    const $firewallCustomLoginSlug = $('#vulntitan-firewall-custom-login-slug');
     14    const $firewallCustomLoginUrl = $('#vulntitan-firewall-custom-login-url');
    1315    const $firewallWafSqliEnabled = $('#vulntitan-firewall-waf-sqli-enabled');
    1416    const $firewallWafCommandEnabled = $('#vulntitan-firewall-waf-command-enabled');
     
    2022    const $firewallRefresh = $('#vulntitan-firewall-refresh');
    2123    const $firewallClearLogs = $('#vulntitan-firewall-clear-logs');
     24    const $firewallTabs = $firewallRoot.find('[data-firewall-tab]');
     25    const $firewallTabPanels = $firewallRoot.find('[data-firewall-tab-panel]');
    2226    const i18n = (window.VulnTitan && window.VulnTitan.i18n) ? window.VulnTitan.i18n : {};
    2327    let latestMuLoaderStatus = null;
     28    let feedbackTimer = null;
     29    let feedbackHideTimer = null;
    2430
    2531    function escapeHtml(str) {
     
    3541    }
    3642
     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
    3790    function setFeedback(type, message) {
    3891        if (!message) {
    39             $firewallFeedback.hide().empty();
     92            dismissFeedback();
    4093            return;
    4194        }
    4295
    4396        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();
    44101
    45102        $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            `)
    47110            .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        }
    48121    }
    49122
     
    122195    }
    123196
     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
    124237    function applySettings(settings) {
    125238        const data = settings || {};
     
    127240        $firewallEnabled.prop('checked', !!Number(data.enabled || 0));
    128241        $firewallLoginProtection.prop('checked', !!Number(data.login_protection_enabled || 0));
     242        $firewallCustomLoginSlug.val(String(data.custom_login_slug || ''));
    129243        $firewallWafSqliEnabled.prop('checked', !!Number(data.waf_sqli_enabled || 0));
    130244        $firewallWafCommandEnabled.prop('checked', !!Number(data.waf_command_injection_enabled || 0));
     
    183297        $firewallRefresh.prop('disabled', isBusy);
    184298        $firewallClearLogs.prop('disabled', isBusy);
     299        $firewallTabs.prop('disabled', isBusy);
    185300        $firewallEnabled.prop('disabled', isBusy);
    186301        $firewallLoginProtection.prop('disabled', isBusy);
     302        $firewallCustomLoginSlug.prop('disabled', isBusy);
    187303        $firewallWafSqliEnabled.prop('disabled', isBusy);
    188304        $firewallWafCommandEnabled.prop('disabled', isBusy);
     
    197313
    198314        if (showLoadingNotice) {
    199             setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     315            setFeedback('info', i18n.firewall_refreshing || 'Refreshing firewall data...');
    200316        }
    201317
     
    215331            latestMuLoaderStatus = payload.mu_loader || null;
    216332            applySettings(payload.settings || {});
     333            renderLoginAccess(payload.login_access || {});
    217334            renderOverview(payload.summary || {}, latestMuLoaderStatus || {});
    218335            renderLogs(payload.logs || []);
     
    227344    function saveSettings() {
    228345        setBusyState(true);
    229         setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     346        setFeedback('info', i18n.firewall_saving || 'Saving firewall settings...');
    230347
    231348        $.post(VulnTitan.ajaxUrl, {
     
    234351            enabled: $firewallEnabled.is(':checked') ? 1 : 0,
    235352            login_protection_enabled: $firewallLoginProtection.is(':checked') ? 1 : 0,
     353            custom_login_slug: String($firewallCustomLoginSlug.val() || ''),
    236354            waf_sqli_enabled: $firewallWafSqliEnabled.is(':checked') ? 1 : 0,
    237355            waf_command_injection_enabled: $firewallWafCommandEnabled.is(':checked') ? 1 : 0,
     
    252370            latestMuLoaderStatus = payload.mu_loader || latestMuLoaderStatus;
    253371            applySettings(payload.settings || {});
     372            renderLoginAccess(payload.login_access || {});
    254373            renderOverview(payload.summary || {}, latestMuLoaderStatus || {});
    255374            renderLogs(payload.logs || []);
    256375
    257376            const muInstall = payload.mu_loader_install || {};
     377            if (payload.notice) {
     378                setFeedback('warning', payload.notice);
     379                return;
     380            }
     381
    258382            if (muInstall.success === false && muInstall.error) {
    259383                setFeedback('warning', muInstall.error);
     
    276400
    277401        setBusyState(true);
    278         setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     402        setFeedback('info', i18n.firewall_clearing || 'Clearing firewall logs...');
    279403
    280404        $.post(VulnTitan.ajaxUrl, {
     
    313437    });
    314438
    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);
    316480});
  • vulntitan/trunk/assets/js/firewall.min.js

    r3469591 r3481425  
    1111    const $firewallEnabled = $('#vulntitan-firewall-enabled');
    1212    const $firewallLoginProtection = $('#vulntitan-firewall-login-protection');
     13    const $firewallCustomLoginSlug = $('#vulntitan-firewall-custom-login-slug');
     14    const $firewallCustomLoginUrl = $('#vulntitan-firewall-custom-login-url');
    1315    const $firewallWafSqliEnabled = $('#vulntitan-firewall-waf-sqli-enabled');
    1416    const $firewallWafCommandEnabled = $('#vulntitan-firewall-waf-command-enabled');
     
    2022    const $firewallRefresh = $('#vulntitan-firewall-refresh');
    2123    const $firewallClearLogs = $('#vulntitan-firewall-clear-logs');
     24    const $firewallTabs = $firewallRoot.find('[data-firewall-tab]');
     25    const $firewallTabPanels = $firewallRoot.find('[data-firewall-tab-panel]');
    2226    const i18n = (window.VulnTitan && window.VulnTitan.i18n) ? window.VulnTitan.i18n : {};
    2327    let latestMuLoaderStatus = null;
     28    let feedbackTimer = null;
     29    let feedbackHideTimer = null;
    2430
    2531    function escapeHtml(str) {
     
    3541    }
    3642
     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
    3790    function setFeedback(type, message) {
    3891        if (!message) {
    39             $firewallFeedback.hide().empty();
     92            dismissFeedback();
    4093            return;
    4194        }
    4295
    4396        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();
    44101
    45102        $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            `)
    47110            .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        }
    48121    }
    49122
     
    122195    }
    123196
     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
    124237    function applySettings(settings) {
    125238        const data = settings || {};
     
    127240        $firewallEnabled.prop('checked', !!Number(data.enabled || 0));
    128241        $firewallLoginProtection.prop('checked', !!Number(data.login_protection_enabled || 0));
     242        $firewallCustomLoginSlug.val(String(data.custom_login_slug || ''));
    129243        $firewallWafSqliEnabled.prop('checked', !!Number(data.waf_sqli_enabled || 0));
    130244        $firewallWafCommandEnabled.prop('checked', !!Number(data.waf_command_injection_enabled || 0));
     
    183297        $firewallRefresh.prop('disabled', isBusy);
    184298        $firewallClearLogs.prop('disabled', isBusy);
     299        $firewallTabs.prop('disabled', isBusy);
    185300        $firewallEnabled.prop('disabled', isBusy);
    186301        $firewallLoginProtection.prop('disabled', isBusy);
     302        $firewallCustomLoginSlug.prop('disabled', isBusy);
    187303        $firewallWafSqliEnabled.prop('disabled', isBusy);
    188304        $firewallWafCommandEnabled.prop('disabled', isBusy);
     
    197313
    198314        if (showLoadingNotice) {
    199             setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     315            setFeedback('info', i18n.firewall_refreshing || 'Refreshing firewall data...');
    200316        }
    201317
     
    215331            latestMuLoaderStatus = payload.mu_loader || null;
    216332            applySettings(payload.settings || {});
     333            renderLoginAccess(payload.login_access || {});
    217334            renderOverview(payload.summary || {}, latestMuLoaderStatus || {});
    218335            renderLogs(payload.logs || []);
     
    227344    function saveSettings() {
    228345        setBusyState(true);
    229         setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     346        setFeedback('info', i18n.firewall_saving || 'Saving firewall settings...');
    230347
    231348        $.post(VulnTitan.ajaxUrl, {
     
    234351            enabled: $firewallEnabled.is(':checked') ? 1 : 0,
    235352            login_protection_enabled: $firewallLoginProtection.is(':checked') ? 1 : 0,
     353            custom_login_slug: String($firewallCustomLoginSlug.val() || ''),
    236354            waf_sqli_enabled: $firewallWafSqliEnabled.is(':checked') ? 1 : 0,
    237355            waf_command_injection_enabled: $firewallWafCommandEnabled.is(':checked') ? 1 : 0,
     
    252370            latestMuLoaderStatus = payload.mu_loader || latestMuLoaderStatus;
    253371            applySettings(payload.settings || {});
     372            renderLoginAccess(payload.login_access || {});
    254373            renderOverview(payload.summary || {}, latestMuLoaderStatus || {});
    255374            renderLogs(payload.logs || []);
    256375
    257376            const muInstall = payload.mu_loader_install || {};
     377            if (payload.notice) {
     378                setFeedback('warning', payload.notice);
     379                return;
     380            }
     381
    258382            if (muInstall.success === false && muInstall.error) {
    259383                setFeedback('warning', muInstall.error);
     
    276400
    277401        setBusyState(true);
    278         setFeedback('info', i18n.firewall_loading || 'Loading firewall data...');
     402        setFeedback('info', i18n.firewall_clearing || 'Clearing firewall logs...');
    279403
    280404        $.post(VulnTitan.ajaxUrl, {
     
    313437    });
    314438
    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);
    316480});
  • vulntitan/trunk/includes/Admin/Admin.php

    r3479599 r3481425  
    118118                    'firewall_mu_inactive' => esc_html__('MU loader inactive', 'vulntitan'),
    119119                    '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'),
    120121                    'firewall_event_unknown' => esc_html__('Unknown event', 'vulntitan'),
    121122                    'firewall_event_login_failed' => esc_html__('Login failed', 'vulntitan'),
  • vulntitan/trunk/includes/Admin/Ajax.php

    r3479541 r3481425  
    4646            'summary' => FirewallService::getSummary(24),
    4747            'logs' => FirewallService::getRecentLogs(80),
     48            'login_access' => FirewallService::getLoginAccessData(),
    4849            'mu_loader' => FirewallService::getMuLoaderStatus(),
    4950        ]);
     
    5758            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
    5859        }
     60
     61        $customLoginValidation = FirewallService::validateCustomLoginSlug(
     62            isset($_POST['custom_login_slug']) ? (string) wp_unslash($_POST['custom_login_slug']) : ''
     63        );
    5964
    6065        $input = [
    6166            'enabled' => isset($_POST['enabled']) ? (int) wp_unslash($_POST['enabled']) : 1,
    6267            'login_protection_enabled' => isset($_POST['login_protection_enabled']) ? (int) wp_unslash($_POST['login_protection_enabled']) : 1,
     68            'custom_login_slug' => $customLoginValidation['slug'],
    6369            'waf_sqli_enabled' => isset($_POST['waf_sqli_enabled']) ? (int) wp_unslash($_POST['waf_sqli_enabled']) : 1,
    6470            'waf_command_injection_enabled' => isset($_POST['waf_command_injection_enabled']) ? (int) wp_unslash($_POST['waf_command_injection_enabled']) : 1,
     
    7177        $settings = FirewallService::saveSettings($input);
    7278        $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        }
    7386
    7487        wp_send_json_success([
     
    7689            'summary' => FirewallService::getSummary(24),
    7790            'logs' => FirewallService::getRecentLogs(80),
     91            'login_access' => FirewallService::getLoginAccessData(),
    7892            'mu_loader' => FirewallService::getMuLoaderStatus(),
    7993            'mu_loader_install' => $muLoaderResult,
     94            'notice' => $notice,
    8095        ]);
    8196    }
  • vulntitan/trunk/includes/Admin/Pages/Firewall.php

    r3479599 r3481425  
    4040
    4141                                <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/*&#10;/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/*&#10;/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>
    96205                                    </div>
    97206                                </div>
  • vulntitan/trunk/includes/Plugin.php

    r3469591 r3481425  
    55use VulnTitan\Admin\Admin;
    66use VulnTitan\Services\FirewallService;
     7use VulnTitan\Services\LoginAccessService;
    78
    89class Plugin {
     
    2223        $this->ensure_firewall_components();
    2324        $this->register_scheduled_events();
     25        LoginAccessService::boot();
    2426        $this->admin = new Admin();
    2527        $this->admin->init();
  • vulntitan/trunk/includes/Services/FirewallService.php

    r3469591 r3481425  
    2121            'enabled' => 1,
    2222            'login_protection_enabled' => 1,
     23            'custom_login_slug' => '',
    2324            'waf_sqli_enabled' => 1,
    2425            'waf_command_injection_enabled' => 1,
     
    5051
    5152        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        ];
    52117    }
    53118
     
    553618            'enabled' => !empty($settings['enabled']) ? 1 : 0,
    554619            'login_protection_enabled' => !empty($settings['login_protection_enabled']) ? 1 : 0,
     620            'custom_login_slug' => self::sanitizeCustomLoginSlug($settings['custom_login_slug'] ?? $defaults['custom_login_slug']),
    555621            'waf_sqli_enabled' => !empty($settings['waf_sqli_enabled']) ? 1 : 0,
    556622            'waf_command_injection_enabled' => !empty($settings['waf_command_injection_enabled']) ? 1 : 0,
     
    562628    }
    563629
     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
    564707    protected static function sanitizeWhitelistPaths($input): array
    565708    {
     
    598741
    599742        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;
    600766    }
    601767
  • vulntitan/trunk/readme.txt

    r3480442 r3481425  
    44Tested up to: 6.9
    55Requires PHP: 7.4
    6 Stable tag: 2.0.5
     6Stable tag: 2.0.6
    77License: GPLv2
    88License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    1616Instantly 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.
    1717
    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.
     18Unlike 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.
    1919
    2020= Malware Scanner =
     
    4747= Firewall & Login Protection =
    4848
    49 VulnTitan includes lightweight firewall and WAF protection to block common attack patterns.
     49VulnTitan includes lightweight firewall and WAF protection to block common attack patterns and protect the WordPress login surface.
    5050
    5151- Early MU-plugin runtime request guards
     
    5555- Endpoint whitelisting controls
    5656- 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
    5759
    5860= Security-First Architecture =
     
    86885. Firewall and WAF protection settings panel.
    87896. Vulnerability scan progress bar.
     907. Firewall hidden custom login configuration and activity overview.
     917. When custom login url is set and user goes to wp-login.php or wp-admin route.
    8892
    8993== Installation ==
     
    130134== Changelog ==
    131135
     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
    132141= v2.0.4 - 10 Mar, 2026 =
    133142* Redesigned the VulnTitan Dashboard into an elite, professional security command center layout.
  • vulntitan/trunk/vulntitan.php

    r3480442 r3481425  
    44 * Plugin URI: https://vulntitan.com/vulntitan/
    55 * 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.5
     6 * Version: 2.0.6
    77 * Author: Jaroslav Svetlik
    88 * Author URI: https://vulntitan.com
     
    3030
    3131// Define plugin constants
    32 define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.0.5');
     32define('VULNTITAN_PLUGIN_VERSION', VULNTITAN_DEVELOPMENT ? uniqid() : '2.0.6');
    3333define('VULNTITAN_PLUGIN_BASENAME', plugin_basename(__FILE__));
    3434define('VULNTITAN_PLUGIN_DIR', untrailingslashit(plugin_dir_path(__FILE__)));
Note: See TracChangeset for help on using the changeset viewer.