Plugin Directory

Changeset 3476055


Ignore:
Timestamp:
03/06/2026 04:21:12 AM (2 days ago)
Author:
shift8
Message:

Bug fixes, improvements with blocking ips

Location:
atomic-edge-security/trunk
Files:
11 edited

Legend:

Unmodified
Added
Removed
  • atomic-edge-security/trunk/admin/js/adaptive-defense.js

    r3473217 r3476055  
    1515        /** Current page for each section */
    1616        pages: {
     17            blocked: 1,
    1718            actors: 1,
    1819            detections: 1
     
    4142            if ($('#atomicedge-ad-status-card').length) {
    4243                this.loadStatusTab();
     44            } else if ($('#atomicedge-ad-blocked-card').length) {
     45                this.loadBlockedIps();
    4346            } else if ($('#atomicedge-ad-actors-card').length) {
    4447                this.loadActorProfiles();
     
    6265            });
    6366
     67            // Blocked IPs tab
     68            $('#atomicedge-ad-blocked-refresh').on('click', function() {
     69                self.loadBlockedIps();
     70            });
     71            $('#atomicedge-ad-block-btn').on('click', function() {
     72                self.blockIpFromForm();
     73            });
     74            $('#atomicedge-ad-block-ip').on('keypress', function(e) {
     75                if (e.which === 13) {
     76                    e.preventDefault();
     77                    self.blockIpFromForm();
     78                }
     79            });
     80            $(document).on('click', '.atomicedge-ad-unblock-btn', function() {
     81                var ip = $(this).data('ip');
     82                self.unblockIp(ip);
     83            });
     84            $(document).on('click', '.atomicedge-ad-extend-block-btn', function() {
     85                var ip = $(this).data('ip');
     86                self.extendBlock(ip);
     87            });
     88            $(document).on('click', '.atomicedge-ad-make-permanent-btn', function() {
     89                var ip = $(this).data('ip');
     90                self.makePermanent(ip);
     91            });
     92
    6493            // Actor Profiles tab
    6594            $('#atomicedge-ad-actors-refresh').on('click', function() {
     
    84113                var $btn = $(this);
    85114                var ip = $btn.data('ip');
    86                 self.blockIpToBlacklist(ip, 'actor', $btn);
     115                self.blockIpViaAD(ip, 'actor', $btn);
    87116            });
    88117            $(document).on('click', '.atomicedge-ad-delete-actor-btn', function() {
     
    108137                var $btn = $(this);
    109138                var ip = $btn.data('ip');
    110                 self.blockIpToBlacklist(ip, 'detection', $btn);
     139                self.blockIpViaAD(ip, 'detection', $btn);
    111140            });
    112141            $(document).on('click', '.atomicedge-ad-dismiss-btn', function() {
     
    126155               
    127156                switch (section) {
     157                    case 'blocked':
     158                        self.loadBlockedIps();
     159                        break;
    128160                    case 'actors':
    129161                        self.loadActorProfiles();
     
    154186                case 'status':
    155187                    this.loadStatusTab();
     188                    break;
     189                case 'blocked':
     190                    this.loadBlockedIps();
    156191                    break;
    157192                case 'actors':
     
    180215                data: {
    181216                    action: 'atomicedge_get_adaptive_defense',
    182                     nonce: atomicedge_admin.nonce
     217                    nonce: atomicedge_admin.nonce,
     218                    force_refresh: 'true'
    183219                },
    184220                success: function(response) {
     
    268304                var ipAddress = actor.ip_address || actor.ip || '';
    269305                var html = '<tr>';
    270                 html += '<td>' + self.escapeHtml(ipAddress) + '</td>';
     306                html += '<td>' + self.formatIpWithFlag(ipAddress, actor) + '</td>';
    271307                html += '<td>' + self.formatScore(actor.threat_score || actor.score || 0) + '</td>';
    272308                html += '<td>' + (actor.requests || actor.total_requests || 0) + '</td>';
     
    282318
    283319        /**
    284          * Format current date/time as a human-readable timestamp.
    285          *
    286          * @return {string} e.g. "2026-02-27 14:35 UTC"
    287          */
    288         formatTimestamp: function() {
    289             var d = new Date();
    290             var pad = function(n) { return n < 10 ? '0' + n : n; };
    291             return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1) + '-' + pad(d.getUTCDate()) +
    292                 ' ' + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ' UTC';
    293         },
    294 
    295         /**
    296          * Block an IP by adding it to the blacklist (unified across all sources).
    297          *
    298          * All block actions route through the blacklist in Access Control.
    299          * Source context is captured in the description field.
     320         * Block an IP via Adaptive Defense (application-layer blocking).
     321         *
     322         * Routes through the dashboard's Adaptive Defense system, NOT
     323         * the global IP blacklist in Access Control.
    300324         *
    301325         * @param {string} ip      IP address to block.
    302          * @param {string} source  Source context (actor, detection).
     326         * @param {string} source  Source context (actor, detection, blocked).
    303327         * @param {jQuery} $button Optional button element to update.
    304328         */
    305         blockIpToBlacklist: function(ip, source, $button) {
     329        blockIpViaAD: function(ip, source, $button) {
    306330            var self = this;
    307331            source = source || 'adaptive_defense';
     
    312336            }
    313337
    314             if (!confirm('Are you sure you want to block ' + ip + '?')) {
    315                 return;
    316             }
    317 
    318             // Build source-contextual description.
    319             var sourceLabels = {
    320                 actor: 'Actor Profiles',
    321                 detection: 'Threat Detections'
    322             };
    323             var description = 'Blocked from ' + (sourceLabels[source] || 'Adaptive Defense') + ' on ' + self.formatTimestamp();
    324 
    325             // Disable the button immediately to prevent double-clicks.
     338            if (!confirm('Are you sure you want to block ' + ip + ' via Adaptive Defense?')) {
     339                return;
     340            }
     341
    326342            if ($button) {
    327343                $button.prop('disabled', true);
     
    332348                type: 'POST',
    333349                data: {
    334                     action: 'atomicedge_add_ip_blacklist',
     350                    action: 'atomicedge_block_ip',
    335351                    nonce: atomicedge_admin.nonce,
    336352                    ip: ip,
    337                     description: description
     353                    duration_hours: 24,
     354                    permanent: 'false',
     355                    reason: 'Blocked from ' + source + ' tab'
    338356                },
    339357                success: function(response) {
    340358                    if (response.success) {
    341                         // Update button to show blocked state.
    342359                        if ($button) {
    343360                            $button.prop('disabled', true)
     
    346363
    347364                        self.showNotice(
    348                             ip + ' has been added to the blacklist. Manage blocks in Access Control.',
     365                            ip + ' has been blocked via Adaptive Defense.',
    349366                            'success'
    350367                        );
    351368
    352                         // Refresh source tab to show updated state.
     369                        // Refresh the source tab.
    353370                        if (source === 'actor') {
    354371                            self.loadActorProfiles();
    355372                        } else if (source === 'detection') {
    356373                            self.loadThreatDetections();
     374                        } else if (source === 'blocked') {
     375                            self.loadBlockedIps();
    357376                        }
    358377                    } else {
    359                         // Re-enable on failure.
    360378                        if ($button) {
    361379                            $button.prop('disabled', false);
     
    375393
    376394        /**
    377          * Load Actor Profiles
    378          */
    379         loadActorProfiles: function() {
    380             var self = this;
    381             var $loading = $('#atomicedge-ad-actors-loading');
    382             var $wrapper = $('#atomicedge-ad-actors-table-wrapper');
    383             var filter = $('#atomicedge-ad-actors-filter').val();
    384             var search = $('#atomicedge-ad-actors-search').val().trim();
     395         * Block an IP from the Blocked IPs tab form.
     396         */
     397        blockIpFromForm: function() {
     398            var self = this;
     399            var ip = $('#atomicedge-ad-block-ip').val().trim();
     400            var duration = $('#atomicedge-ad-block-duration').val();
     401
     402            if (!ip) {
     403                self.showNotice('Please enter an IP address', 'error');
     404                return;
     405            }
     406
     407            if (!confirm('Are you sure you want to block ' + ip + '?')) {
     408                return;
     409            }
     410
     411            var isPermanent = (duration === 'permanent');
     412            var durationHours = isPermanent ? 0 : parseInt(duration, 10);
     413
     414            var $btn = $('#atomicedge-ad-block-btn');
     415            $btn.prop('disabled', true);
     416
     417            $.ajax({
     418                url: atomicedge_admin.ajax_url,
     419                type: 'POST',
     420                data: {
     421                    action: 'atomicedge_block_ip',
     422                    nonce: atomicedge_admin.nonce,
     423                    ip: ip,
     424                    duration_hours: durationHours,
     425                    permanent: isPermanent ? 'true' : 'false',
     426                    reason: 'Manually blocked from Blocked IPs tab'
     427                },
     428                success: function(response) {
     429                    $btn.prop('disabled', false);
     430                    if (response.success) {
     431                        $('#atomicedge-ad-block-ip').val('');
     432                        self.showNotice(ip + ' has been blocked.', 'success');
     433                        self.loadBlockedIps();
     434                    } else {
     435                        var message = (response.data && response.data.message) ? response.data.message : 'Failed to block IP';
     436                        self.showNotice(message, 'error');
     437                    }
     438                },
     439                error: function(xhr, status, error) {
     440                    $btn.prop('disabled', false);
     441                    self.showNotice('Network error: ' + error, 'error');
     442                }
     443            });
     444        },
     445
     446        /**
     447         * Load Blocked IPs tab data.
     448         *
     449         * Reuses the actor profiles endpoint with filter=blocked.
     450         */
     451        loadBlockedIps: function() {
     452            var self = this;
     453            var $loading = $('#atomicedge-ad-blocked-loading');
     454            var $wrapper = $('#atomicedge-ad-blocked-table-wrapper');
    385455
    386456            $loading.show();
     
    393463                    action: 'atomicedge_get_actor_profiles',
    394464                    nonce: atomicedge_admin.nonce,
     465                    force_refresh: 'true',
     466                    filter: 'blocked',
     467                    page: self.pages.blocked,
     468                    per_page: self.perPage
     469                },
     470                success: function(response) {
     471                    $loading.hide();
     472                    $wrapper.show();
     473
     474                    if (response.success && response.data) {
     475                        self.renderBlockedIps(response.data.actors || response.data);
     476                        self.renderPagination('blocked', response.data.pagination);
     477                    } else {
     478                        self.renderBlockedIps([]);
     479                    }
     480                },
     481                error: function(xhr, status, error) {
     482                    $loading.hide();
     483                    $wrapper.show();
     484                    self.showTableError('#atomicedge-ad-blocked-body', 'Network error: ' + error);
     485                }
     486            });
     487        },
     488
     489        /**
     490         * Render Blocked IPs table rows.
     491         *
     492         * Columns: IP Address, Threat Score, WAF Hits, Type, Blocked, Expires, Actions
     493         *
     494         * @param {Array} actors Blocked actor profiles
     495         */
     496        renderBlockedIps: function(actors) {
     497            var $tbody = $('#atomicedge-ad-blocked-body');
     498            var $empty = $('#atomicedge-ad-blocked-empty');
     499            var $table = $('#atomicedge-ad-blocked-table');
     500
     501            $tbody.empty();
     502
     503            if (!actors || actors.length === 0) {
     504                $table.hide();
     505                $empty.show();
     506                return;
     507            }
     508
     509            $table.show();
     510            $empty.hide();
     511
     512            var self = this;
     513            actors.forEach(function(actor) {
     514                var ip = actor.ip_address || actor.ip || '';
     515                var score = actor.score || actor.threat_score || 0;
     516                var wafHits = actor.waf_hits || actor.total_waf_hits || 0;
     517                var blockedAt = actor.blocked_at || null;
     518                var expiresAt = actor.block_expires_at || null;
     519                var isPermanent = actor.is_blocked && !expiresAt;
     520
     521                var html = '<tr>';
     522                // IP Address with flag
     523                html += '<td>' + self.formatIpWithFlag(ip, actor) + '</td>';
     524                // Threat Score
     525                html += '<td>' + self.formatScore(score) + '</td>';
     526                // WAF Hits
     527                html += '<td>' + wafHits + '</td>';
     528                // Type (Permanent / Timed)
     529                html += '<td>';
     530                if (isPermanent) {
     531                    html += '<span class="atomicedge-ad-status-badge atomicedge-ad-status-blocked" style="font-size:11px;">Permanent</span>';
     532                } else {
     533                    html += '<span class="atomicedge-ad-status-badge atomicedge-ad-status-pending" style="font-size:11px;">Timed</span>';
     534                }
     535                html += '</td>';
     536                // Blocked (relative time)
     537                html += '<td>' + self.formatRelativeTime(blockedAt) + '</td>';
     538                // Expires
     539                html += '<td>';
     540                if (isPermanent) {
     541                    html += '<strong>Never</strong>';
     542                } else {
     543                    html += self.formatRelativeTime(expiresAt);
     544                }
     545                html += '</td>';
     546                // Actions
     547                html += '<td>';
     548                if (!isPermanent) {
     549                    html += '<button type="button" class="button button-small atomicedge-ad-extend-block-btn" data-ip="' + self.escapeHtml(ip) + '" title="Extend +1 day">';
     550                    html += '<span class="dashicons dashicons-clock" style="margin-top:3px;"></span></button> ';
     551                    html += '<button type="button" class="button button-small atomicedge-ad-make-permanent-btn" data-ip="' + self.escapeHtml(ip) + '" title="Make permanent">';
     552                    html += '<span class="dashicons dashicons-lock" style="margin-top:3px;"></span></button> ';
     553                }
     554                html += '<button type="button" class="button button-small atomicedge-ad-unblock-btn" data-ip="' + self.escapeHtml(ip) + '" title="Unblock">';
     555                html += '<span class="dashicons dashicons-unlock" style="margin-top:3px;"></span></button>';
     556                html += '</td>';
     557                html += '</tr>';
     558                $tbody.append(html);
     559            });
     560        },
     561
     562        /**
     563         * Unblock a blocked IP address.
     564         *
     565         * @param {string} ip IP address
     566         */
     567        unblockIp: function(ip) {
     568            var self = this;
     569
     570            if (!confirm('Are you sure you want to unblock ' + ip + '?')) {
     571                return;
     572            }
     573
     574            $.ajax({
     575                url: atomicedge_admin.ajax_url,
     576                type: 'POST',
     577                data: {
     578                    action: 'atomicedge_unblock_ip',
     579                    nonce: atomicedge_admin.nonce,
     580                    ip: ip
     581                },
     582                success: function(response) {
     583                    if (response.success) {
     584                        self.showNotice(ip + ' has been unblocked.', 'success');
     585                        self.loadBlockedIps();
     586                    } else {
     587                        self.showNotice(response.data ? response.data.message : 'Failed to unblock IP', 'error');
     588                    }
     589                },
     590                error: function(xhr, status, error) {
     591                    self.showNotice('Network error: ' + error, 'error');
     592                }
     593            });
     594        },
     595
     596        /**
     597         * Extend a timed block by 1 day.
     598         *
     599         * @param {string} ip IP address
     600         */
     601        extendBlock: function(ip) {
     602            var self = this;
     603
     604            $.ajax({
     605                url: atomicedge_admin.ajax_url,
     606                type: 'POST',
     607                data: {
     608                    action: 'atomicedge_extend_block',
     609                    nonce: atomicedge_admin.nonce,
     610                    ip: ip,
     611                    days: 1
     612                },
     613                success: function(response) {
     614                    if (response.success) {
     615                        self.showNotice('Block for ' + ip + ' extended by 1 day.', 'success');
     616                        self.loadBlockedIps();
     617                    } else {
     618                        self.showNotice(response.data ? response.data.message : 'Failed to extend block', 'error');
     619                    }
     620                },
     621                error: function(xhr, status, error) {
     622                    self.showNotice('Network error: ' + error, 'error');
     623                }
     624            });
     625        },
     626
     627        /**
     628         * Make a timed block permanent.
     629         *
     630         * @param {string} ip IP address
     631         */
     632        makePermanent: function(ip) {
     633            var self = this;
     634
     635            if (!confirm('Make the block for ' + ip + ' permanent?')) {
     636                return;
     637            }
     638
     639            $.ajax({
     640                url: atomicedge_admin.ajax_url,
     641                type: 'POST',
     642                data: {
     643                    action: 'atomicedge_make_permanent',
     644                    nonce: atomicedge_admin.nonce,
     645                    ip: ip
     646                },
     647                success: function(response) {
     648                    if (response.success) {
     649                        self.showNotice('Block for ' + ip + ' is now permanent.', 'success');
     650                        self.loadBlockedIps();
     651                    } else {
     652                        self.showNotice(response.data ? response.data.message : 'Failed to make block permanent', 'error');
     653                    }
     654                },
     655                error: function(xhr, status, error) {
     656                    self.showNotice('Network error: ' + error, 'error');
     657                }
     658            });
     659        },
     660
     661        /**
     662         * Load Actor Profiles
     663         */
     664        loadActorProfiles: function() {
     665            var self = this;
     666            var $loading = $('#atomicedge-ad-actors-loading');
     667            var $wrapper = $('#atomicedge-ad-actors-table-wrapper');
     668            var filter = $('#atomicedge-ad-actors-filter').val();
     669            var search = $('#atomicedge-ad-actors-search').val().trim();
     670
     671            $loading.show();
     672            $wrapper.hide();
     673
     674            $.ajax({
     675                url: atomicedge_admin.ajax_url,
     676                type: 'POST',
     677                data: {
     678                    action: 'atomicedge_get_actor_profiles',
     679                    nonce: atomicedge_admin.nonce,
     680                    force_refresh: 'true',
    395681                    filter: filter,
    396682                    search: search,
     
    443729                var ipAddress = actor.ip_address || actor.ip || '';
    444730                var html = '<tr>';
    445                 html += '<td>' + self.escapeHtml(ipAddress) + '</td>';
     731                html += '<td>' + self.formatIpWithFlag(ipAddress, actor) + '</td>';
    446732                html += '<td>' + self.formatScore(score) + '</td>';
    447733                html += '<td>' + (actor.total_requests || actor.requests || 0) + '</td>';
     
    525811                    action: 'atomicedge_get_threat_detections',
    526812                    nonce: atomicedge_admin.nonce,
     813                    force_refresh: 'true',
    527814                    status: status !== 'all' ? status : '',
    528815                    page: self.pages.detections,
     
    573860                var ipAddress = detection.ip_address || detection.ip || (detection.actor && detection.actor.ip_address) || 'N/A';
    574861                var html = '<tr data-detection-id="' + detection.id + '">';
    575                 html += '<td>' + self.escapeHtml(ipAddress) + '</td>';
     862                html += '<td>' + self.formatIpWithFlag(ipAddress, detection) + '</td>';
    576863                html += '<td>' + self.formatScore(detection.score || 0) + '</td>';
    577864                html += '<td>' + self.formatThreatLevel(detection.threat_level || 'low') + '</td>';
     
    676963
    677964            // Actor details
    678             $detailRow.find('.atomicedge-ad-detail-ip').text(actor.ip || actor.ip_address || detection.ip_address || 'N/A');
     965            var actorIp = actor.ip || actor.ip_address || detection.ip_address || 'N/A';
     966            var actorFlag = this.countryCodeToFlag(actor.country_code || detection.country_code || null);
     967            var flagHtml = actorFlag ? '<span title="' + this.escapeHtml(actor.country_code || detection.country_code || '') + '" style="margin-right: 4px;">' + actorFlag + '</span>' : '';
     968            $detailRow.find('.atomicedge-ad-detail-ip').html(flagHtml + this.escapeHtml(actorIp));
    679969            $detailRow.find('.atomicedge-ad-detail-requests').text(actor.total_requests || 0);
    680970            $detailRow.find('.atomicedge-ad-detail-waf-hits').text(actor.total_waf_hits || actor.waf_hits || 0);
     
    9741264        },
    9751265
     1266        /**
     1267         * Format a date as a relative time string (e.g., "2 hours ago", "in 3 days").
     1268         *
     1269         * @param {string|null} dateString ISO date string
     1270         * @return {string} Relative time or em-dash
     1271         */
     1272        formatRelativeTime: function(dateString) {
     1273            if (!dateString) {
     1274                return '—';
     1275            }
     1276            try {
     1277                var date = new Date(dateString);
     1278                var now = new Date();
     1279                var diffMs = date.getTime() - now.getTime();
     1280                var absDiffMs = Math.abs(diffMs);
     1281                var seconds = Math.floor(absDiffMs / 1000);
     1282                var minutes = Math.floor(seconds / 60);
     1283                var hours = Math.floor(minutes / 60);
     1284                var days = Math.floor(hours / 24);
     1285
     1286                var label;
     1287                if (days > 0) {
     1288                    label = days + ' day' + (days > 1 ? 's' : '');
     1289                } else if (hours > 0) {
     1290                    label = hours + ' hour' + (hours > 1 ? 's' : '');
     1291                } else if (minutes > 0) {
     1292                    label = minutes + ' min' + (minutes > 1 ? 's' : '');
     1293                } else {
     1294                    label = 'just now';
     1295                    return label;
     1296                }
     1297
     1298                return diffMs < 0 ? label + ' ago' : 'in ' + label;
     1299            } catch (e) {
     1300                return dateString;
     1301            }
     1302        },
     1303
    9761304        /* ============================
    9771305         * Utility Helpers
     
    9911319            div.textContent = str;
    9921320            return div.innerHTML;
     1321        },
     1322
     1323        /**
     1324         * Convert ISO 3166-1 alpha-2 country code to flag emoji.
     1325         *
     1326         * Uses Unicode Regional Indicator Symbols (same approach as Laravel backend).
     1327         *
     1328         * @param {string|null} countryCode Two-letter country code (e.g., 'US', 'CN')
     1329         * @return {string} Flag emoji or empty string
     1330         */
     1331        countryCodeToFlag: function(countryCode) {
     1332            if (!countryCode || countryCode.length !== 2) {
     1333                return '';
     1334            }
     1335            var code = countryCode.toUpperCase();
     1336            var base = 0x1F1E6 - 'A'.charCodeAt(0);
     1337            return String.fromCodePoint(base + code.charCodeAt(0)) +
     1338                   String.fromCodePoint(base + code.charCodeAt(1));
     1339        },
     1340
     1341        /**
     1342         * Format an IP address with an optional country flag prefix.
     1343         *
     1344         * @param {string} ip       The IP address (already escaped)
     1345         * @param {Object} dataObj  The data object that may contain country_code or country_flag_emoji
     1346         * @return {string} HTML string with flag + IP
     1347         */
     1348        formatIpWithFlag: function(ip, dataObj) {
     1349            var flag = '';
     1350            if (dataObj) {
     1351                // Prefer pre-computed emoji from API, fall back to client-side conversion
     1352                flag = dataObj.country_flag_emoji || this.countryCodeToFlag(dataObj.country_code || null);
     1353            }
     1354            var countryCode = (dataObj && dataObj.country_code) ? dataObj.country_code : '';
     1355            if (flag) {
     1356                return '<span title="' + this.escapeHtml(countryCode) + '" style="margin-right: 4px;">' + flag + '</span>' + this.escapeHtml(ip);
     1357            }
     1358            return this.escapeHtml(ip);
    9931359        },
    9941360
  • atomic-edge-security/trunk/admin/js/admin.js

    r3473194 r3476055  
    395395                var actionCell;
    396396                if (log.is_blocked) {
    397                     actionCell = '<span class="atomicedge-blocked-badge" title="This IP is in your blacklist" style="color:#b32d2e;font-weight:600;">' +
     397                    actionCell = '<span class="atomicedge-blocked-badge" title="This IP is blocked" style="color:#b32d2e;font-weight:600;">' +
    398398                        '<span class="dashicons dashicons-lock" style="font-size:14px;width:14px;height:14px;margin-top:3px;"></span> Blocked</span>';
    399399                } else {
     
    411411            });
    412412
    413             // Bind block IP buttons
     413            // Bind block IP buttons — blocks go to Adaptive Defense (not IP blacklist).
    414414            $tbody.find('.atomicedge-block-ip').on('click', function() {
    415415                var $btn = $(this);
    416416                var ip = $btn.data('ip');
    417417                if (confirm(atomicedgeAdmin.strings.confirm)) {
    418                     self.addIpBlacklist(ip, 'Blocked from WAF logs on ' + self.formatTimestamp(), $btn);
     418                    self.blockIpFromWafLogs(ip, $btn);
    419419                }
    420420            });
     
    484484        /**
    485485         * Load IP rules
    486          */
    487         loadIpRules: function() {
    488             var self = this;
    489 
    490             this.ajax('atomicedge_get_ip_rules', {}, function(data) {
     486         *
     487         * @param {boolean} forceRefresh Bypass transient cache (default true)
     488         */
     489        loadIpRules: function(forceRefresh) {
     490            var self = this;
     491            var data = {};
     492
     493            if (forceRefresh !== false) {
     494                data.force_refresh = 'true';
     495            }
     496
     497            this.ajax('atomicedge_get_ip_rules', data, function(data) {
    491498                self.renderIpList('whitelist', data.whitelist || []);
    492499                self.renderIpList('blacklist', data.blacklist || []);
     
    570577
    571578        /**
    572          * Add IP to blacklist
     579         * Block an IP from the WAF logs page via Adaptive Defense.
     580         *
     581         * This sends the block to the AD system (ActorProfile), NOT the
     582         * Access Control IP blacklist (SiteSettings).  The existing
     583         * ajax_block_ip AJAX handler + api->block_ip() method are reused.
     584         *
     585         * @param {string} ip      IP address.
     586         * @param {jQuery} $button The button element to update on success.
     587         */
     588        blockIpFromWafLogs: function(ip, $button) {
     589            var self = this;
     590
     591            if ($button) {
     592                $button.prop('disabled', true).text(atomicedgeAdmin.strings.loading);
     593            }
     594
     595            this.ajax('atomicedge_block_ip', {
     596                ip: ip,
     597                duration_hours: 24,
     598                reason: 'Blocked from WAF logs'
     599            }, function() {
     600                // Reload WAF logs so the is_blocked badge appears.
     601                if ($('#atomicedge-waf-table').length) {
     602                    self.loadWafLogs();
     603                }
     604
     605                // Immediate visual feedback.
     606                if ($button) {
     607                    $button.prop('disabled', true)
     608                        .removeClass('button-small')
     609                        .addClass('atomicedge-blocked-btn')
     610                        .html('<span class="dashicons dashicons-yes-alt" style="margin-top:3px;color:#00a32a;"></span> Blocked');
     611                }
     612
     613                self.showNotice(ip + ' has been blocked via Adaptive Defense.', 'success');
     614            }, function(errData) {
     615                if ($button) {
     616                    $button.prop('disabled', false).text('Block IP');
     617                }
     618                var message = (errData && errData.message) ? errData.message : atomicedgeAdmin.strings.error;
     619                self.showNotice(message, 'error');
     620            });
     621        },
     622
     623        /**
     624         * Add IP to blacklist (Access Control page only).
    573625         *
    574626         * @param {string}  ip          IP address or CIDR.
     
    605657                }
    606658
    607                 var noticeMsg = ip + ' has been added to the block list.';
    608                 // If blocked from outside Access Control, direct user there.
    609                 if ($button) {
    610                     noticeMsg += ' Manage blocks in Access Control.';
    611                 }
     659                var noticeMsg = ip + ' has been added to the IP blacklist.';
    612660                self.showNotice(noticeMsg, 'success');
    613661            }, function(errData) {
    614662                if ($button) {
    615                     $button.prop('disabled', false).text('Block IP');
     663                    $button.prop('disabled', false).text('Add to Blacklist');
    616664                }
    617665                var message = (errData && errData.message) ? errData.message : atomicedgeAdmin.strings.error;
  • atomic-edge-security/trunk/admin/views/adaptive-defense.php

    r3473194 r3476055  
    2323// Tab navigation.
    2424$current_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'status';
    25 $valid_tabs  = array( 'status', 'actors', 'detections' );
     25$valid_tabs  = array( 'status', 'blocked', 'actors', 'detections' );
    2626if ( ! in_array( $current_tab, $valid_tabs, true ) ) {
    2727    $current_tab = 'status';
     
    3333        'label' => __( 'Status', 'atomic-edge-security' ),
    3434        'icon'  => 'dashicons-shield-alt',
     35    ),
     36    'blocked'    => array(
     37        'label' => __( 'Blocked IPs', 'atomic-edge-security' ),
     38        'icon'  => 'dashicons-dismiss',
    3539    ),
    3640    'actors'     => array(
     
    9599                switch ( $current_tab ) {
    96100
     101                    case 'blocked':
     102                        include ATOMICEDGE_PLUGIN_DIR . 'admin/views/partials/adaptive-defense-blocked-tab.php';
     103                        break;
    97104                    case 'actors':
    98105                        include ATOMICEDGE_PLUGIN_DIR . 'admin/views/partials/adaptive-defense-actors-tab.php';
  • atomic-edge-security/trunk/admin/views/partials/adaptive-defense-actors-tab.php

    r3454914 r3476055  
    5656            <thead>
    5757                <tr>
    58                     <th style="width: 160px;"><?php esc_html_e( 'IP Address', 'atomic-edge-security' ); ?></th>
     58                    <th style="width: 185px;"><?php esc_html_e( 'IP Address', 'atomic-edge-security' ); ?></th>
    5959                    <th style="width: 80px;"><?php esc_html_e( 'Score', 'atomic-edge-security' ); ?></th>
    6060                    <th style="width: 90px;"><?php esc_html_e( 'Requests', 'atomic-edge-security' ); ?></th>
  • atomic-edge-security/trunk/admin/views/partials/adaptive-defense-blocked-tab.php

    r3454914 r3476055  
    6060                <tr>
    6161                    <th style="width: 180px;"><?php esc_html_e( 'IP Address', 'atomic-edge-security' ); ?></th>
    62                     <th style="width: 80px;"><?php esc_html_e( 'Score', 'atomic-edge-security' ); ?></th>
    63                     <th style="width: 100px;"><?php esc_html_e( 'WAF Hits', 'atomic-edge-security' ); ?></th>
    64                     <th><?php esc_html_e( 'Expires', 'atomic-edge-security' ); ?></th>
    65                     <th style="width: 180px;"><?php esc_html_e( 'Actions', 'atomic-edge-security' ); ?></th>
     62                    <th style="width: 80px;"><?php esc_html_e( 'Threat Score', 'atomic-edge-security' ); ?></th>
     63                    <th style="width: 80px;"><?php esc_html_e( 'WAF Hits', 'atomic-edge-security' ); ?></th>
     64                    <th style="width: 90px;"><?php esc_html_e( 'Type', 'atomic-edge-security' ); ?></th>
     65                    <th style="width: 110px;"><?php esc_html_e( 'Blocked', 'atomic-edge-security' ); ?></th>
     66                    <th style="width: 110px;"><?php esc_html_e( 'Expires', 'atomic-edge-security' ); ?></th>
     67                    <th style="width: 200px;"><?php esc_html_e( 'Actions', 'atomic-edge-security' ); ?></th>
    6668                </tr>
    6769            </thead>
  • atomic-edge-security/trunk/admin/views/partials/adaptive-defense-detections-tab.php

    r3473194 r3476055  
    5151            <thead>
    5252                <tr>
    53                     <th style="width: 160px;"><?php esc_html_e( 'IP Address', 'atomic-edge-security' ); ?></th>
     53                    <th style="width: 185px;"><?php esc_html_e( 'IP Address', 'atomic-edge-security' ); ?></th>
    5454                    <th style="width: 80px;"><?php esc_html_e( 'Score', 'atomic-edge-security' ); ?></th>
    5555                    <th style="width: 100px;"><?php esc_html_e( 'Threat Level', 'atomic-edge-security' ); ?></th>
  • atomic-edge-security/trunk/atomicedge.php

    r3473217 r3476055  
    44 * Plugin URI: https://atomicedge.io/wordpress
    55 * Description: Connect your WordPress site to Atomic Edge WAF/CDN for advanced security protection, analytics, and access control management.
    6  * Version: 2.4.7
     6 * Version: 2.4.8
    77 * Requires at least: 5.8
    88 * Requires PHP: 7.4
     
    2626
    2727// Plugin constants.
    28 define( 'ATOMICEDGE_VERSION', '2.4.7' );
     28define( 'ATOMICEDGE_VERSION', '2.4.8' );
    2929define( 'ATOMICEDGE_PLUGIN_FILE', __FILE__ );
    3030define( 'ATOMICEDGE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • atomic-edge-security/trunk/includes/class-atomicedge-ajax.php

    r3473194 r3476055  
    9999        add_action( 'wp_ajax_atomicedge_block_ip', array( $this, 'ajax_block_ip' ) );
    100100        add_action( 'wp_ajax_atomicedge_unblock_ip', array( $this, 'ajax_unblock_ip' ) );
     101        add_action( 'wp_ajax_atomicedge_extend_block', array( $this, 'ajax_extend_block' ) );
     102        add_action( 'wp_ajax_atomicedge_make_permanent', array( $this, 'ajax_make_permanent' ) );
    101103        add_action( 'wp_ajax_atomicedge_delete_actor', array( $this, 'ajax_delete_actor' ) );
    102104        add_action( 'wp_ajax_atomicedge_dismiss_detection', array( $this, 'ajax_dismiss_detection' ) );
     
    236238     */
    237239    public function ajax_get_ip_rules() {
    238         $this->get_verified_post_fields( array() );
    239 
    240         $result = $this->api->get_ip_rules();
     240        $post = $this->get_verified_post_fields( array( 'force_refresh' ) );
     241
     242        $force_refresh = ! empty( $post['force_refresh'] ) && 'true' === $post['force_refresh'];
     243        $result        = $this->api->get_ip_rules( $force_refresh );
    241244
    242245        if ( $result['success'] ) {
     
    863866    }
    864867
     868    /**
     869     * Check whether Adaptive Defense handlers should use dev mode simulation.
     870     *
     871     * Dev mode provides simulated data for local development environments
     872     * that have NO API key configured. If an API key exists (even when the
     873     * `atomicedge_connected` flag is not set), the real API should be used.
     874     *
     875     * @return bool True if dev mode simulation should be used.
     876     */
     877    private function should_use_dev_mode() {
     878        if ( ! \AtomicEdge_Dev_Mode::is_enabled() ) {
     879            return false;
     880        }
     881
     882        // If an API key is configured, always use the real API.
     883        if ( $this->api->get_api_key() ) {
     884            return false;
     885        }
     886
     887        return true;
     888    }
     889
    865890    // =========================================================================
    866891    // Adaptive Defense AJAX Handlers
     
    873898     */
    874899    public function ajax_get_adaptive_defense() {
    875         $this->get_verified_post_fields( array() );
    876 
    877         // Dev mode: return simulated data.
    878         if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     900        $post = $this->get_verified_post_fields( array( 'force_refresh' ) );
     901
     902        // Dev mode: return simulated data (only when not connected to real API).
     903        if ( $this->should_use_dev_mode() ) {
    879904            wp_send_json_success( AtomicEdge_Dev_Mode::get_simulated_adaptive_defense() );
    880905        }
    881906
    882         $response = $this->api->get_adaptive_defense();
     907        $force_refresh = ! empty( $post['force_refresh'] ) && 'true' === $post['force_refresh'];
     908        $response      = $this->api->get_adaptive_defense( $force_refresh );
    883909
    884910        if ( $response['success'] ) {
     
    895921     */
    896922    public function ajax_get_actor_profiles() {
    897         $post = $this->get_verified_post_fields( array( 'page', 'per_page', 'filter', 'search' ) );
     923        $post = $this->get_verified_post_fields( array( 'page', 'per_page', 'filter', 'search', 'force_refresh' ) );
    898924
    899925        $args = array(
     
    907933        }
    908934
    909         // Dev mode: return simulated data.
    910         if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     935        // Dev mode: return simulated data (only when not connected to real API).
     936        if ( $this->should_use_dev_mode() ) {
    911937            wp_send_json_success( AtomicEdge_Dev_Mode::get_simulated_actor_profiles( $args ) );
    912938        }
    913939
    914         $response = $this->api->get_actor_profiles( $args );
     940        $force_refresh = ! empty( $post['force_refresh'] ) && 'true' === $post['force_refresh'];
     941        $response      = $this->api->get_actor_profiles( $args, $force_refresh );
    915942
    916943        if ( $response['success'] ) {
     
    927954     */
    928955    public function ajax_get_threat_detections() {
    929         $post = $this->get_verified_post_fields( array( 'page', 'per_page', 'status' ) );
     956        $post = $this->get_verified_post_fields( array( 'page', 'per_page', 'status', 'force_refresh' ) );
    930957
    931958        $args = array(
     
    935962        );
    936963
    937         // Dev mode: return simulated data.
    938         if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     964        // Dev mode: return simulated data (only when not connected to real API).
     965        if ( $this->should_use_dev_mode() ) {
    939966            wp_send_json_success( AtomicEdge_Dev_Mode::get_simulated_threat_detections( $args ) );
    940967        }
    941968
    942         $response = $this->api->get_threat_detections( $args );
     969        $force_refresh = ! empty( $post['force_refresh'] ) && 'true' === $post['force_refresh'];
     970        $response      = $this->api->get_threat_detections( $args, $force_refresh );
    943971
    944972        if ( $response['success'] ) {
     
    961989        }
    962990
    963         // Dev mode: return simulated detail data.
    964         if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     991        // Dev mode: return simulated detail data (only when not connected to real API).
     992        if ( $this->should_use_dev_mode() ) {
    965993            $detail = AtomicEdge_Dev_Mode::get_simulated_threat_detection_detail( absint( $post['detection_id'] ) );
    966994            if ( $detail ) {
     
    9961024        $reason         = isset( $post['reason'] ) ? sanitize_text_field( $post['reason'] ) : '';
    9971025
    998         // Dev mode: return simulated success.
    999         if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     1026        // Dev mode: return simulated success (only when not connected to real API).
     1027        if ( $this->should_use_dev_mode() ) {
    10001028            wp_send_json_success( array(
    10011029                'message' => sprintf(
     
    10361064        }
    10371065
    1038         // Dev mode: return simulated success.
    1039         if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     1066        // Dev mode: return simulated success (only when not connected to real API).
     1067        if ( $this->should_use_dev_mode() ) {
    10401068            wp_send_json_success( array(
    10411069                'message' => sprintf(
     
    10631091
    10641092    /**
     1093     * Extend the block duration for a blocked IP.
     1094     *
     1095     * @return void
     1096     */
     1097    public function ajax_extend_block() {
     1098        $post = $this->get_verified_post_fields( array( 'ip', 'days' ) );
     1099
     1100        if ( empty( $post['ip'] ) ) {
     1101            wp_send_json_error( array( 'message' => __( 'IP address is required.', 'atomic-edge-security' ) ) );
     1102        }
     1103
     1104        $ip   = $post['ip'];
     1105        $days = isset( $post['days'] ) ? max( 1, absint( $post['days'] ) ) : 1;
     1106
     1107        // Dev mode: return simulated success (only when not connected to real API).
     1108        if ( $this->should_use_dev_mode() ) {
     1109            wp_send_json_success( array(
     1110                'message' => sprintf(
     1111                    /* translators: 1: IP address, 2: number of days */
     1112                    __( '[Dev Mode] Block for %1$s extended by %2$d day(s).', 'atomic-edge-security' ),
     1113                    esc_html( $ip ),
     1114                    $days
     1115                ),
     1116                'data' => AtomicEdge_Dev_Mode::simulate_extend_block( $ip, $days ),
     1117            ) );
     1118        }
     1119
     1120        $response = $this->api->extend_block( $ip, $days );
     1121
     1122        if ( $response['success'] ) {
     1123            wp_send_json_success( array(
     1124                'message' => sprintf(
     1125                    /* translators: 1: IP address, 2: number of days */
     1126                    __( 'Block for %1$s extended by %2$d day(s).', 'atomic-edge-security' ),
     1127                    esc_html( $ip ),
     1128                    $days
     1129                ),
     1130                'data' => $response['data'] ?? array(),
     1131            ) );
     1132        } else {
     1133            wp_send_json_error( array( 'message' => $response['error'] ?? __( 'Failed to extend block.', 'atomic-edge-security' ) ) );
     1134        }
     1135    }
     1136
     1137    /**
     1138     * Make a timed block permanent.
     1139     *
     1140     * @return void
     1141     */
     1142    public function ajax_make_permanent() {
     1143        $post = $this->get_verified_post_fields( array( 'ip' ) );
     1144
     1145        if ( empty( $post['ip'] ) ) {
     1146            wp_send_json_error( array( 'message' => __( 'IP address is required.', 'atomic-edge-security' ) ) );
     1147        }
     1148
     1149        $ip = $post['ip'];
     1150
     1151        // Dev mode: return simulated success (only when not connected to real API).
     1152        if ( $this->should_use_dev_mode() ) {
     1153            wp_send_json_success( array(
     1154                'message' => sprintf(
     1155                    /* translators: %s: IP address */
     1156                    __( '[Dev Mode] Block for %s is now permanent.', 'atomic-edge-security' ),
     1157                    esc_html( $ip )
     1158                ),
     1159                'data' => AtomicEdge_Dev_Mode::simulate_make_permanent( $ip ),
     1160            ) );
     1161        }
     1162
     1163        $response = $this->api->make_permanent( $ip );
     1164
     1165        if ( $response['success'] ) {
     1166            wp_send_json_success( array(
     1167                'message' => sprintf(
     1168                    /* translators: %s: IP address */
     1169                    __( 'Block for %s is now permanent.', 'atomic-edge-security' ),
     1170                    esc_html( $ip )
     1171                ),
     1172                'data' => $response['data'] ?? array(),
     1173            ) );
     1174        } else {
     1175            wp_send_json_error( array( 'message' => $response['error'] ?? __( 'Failed to make block permanent.', 'atomic-edge-security' ) ) );
     1176        }
     1177    }
     1178
     1179    /**
    10651180     * Delete an actor profile.
    10661181     *
     
    10741189        }
    10751190
    1076         // Dev mode: return simulated success.
    1077         if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     1191        // Dev mode: return simulated success (only when not connected to real API).
     1192        if ( $this->should_use_dev_mode() ) {
    10781193            wp_send_json_success( array(
    10791194                'message' => __( '[Dev Mode] Actor profile has been deleted.', 'atomic-edge-security' ),
     
    11041219        }
    11051220
    1106         // Dev mode: return simulated success.
    1107         if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     1221        // Dev mode: return simulated success (only when not connected to real API).
     1222        if ( $this->should_use_dev_mode() ) {
    11081223            wp_send_json_success( array(
    11091224                'message' => __( '[Dev Mode] Threat detection has been dismissed.', 'atomic-edge-security' ),
  • atomic-edge-security/trunk/includes/class-atomicedge-api.php

    r3473194 r3476055  
    283283     * @return array IP rules or error.
    284284     */
    285     public function get_ip_rules() {
     285    public function get_ip_rules( $force_refresh = false ) {
    286286        $cache_key = 'atomicedge_ip_rules';
    287         $cached    = get_transient( $cache_key );
    288 
    289         if ( false !== $cached ) {
    290             return $cached;
     287
     288        if ( ! $force_refresh ) {
     289            $cached = get_transient( $cache_key );
     290            if ( false !== $cached ) {
     291                return $cached;
     292            }
    291293        }
    292294
     
    516518     * @return array Status data or error.
    517519     */
    518     public function get_adaptive_defense() {
     520    public function get_adaptive_defense( $force_refresh = false ) {
    519521        $cache_key = 'atomicedge_adaptive_defense';
    520         $cached    = get_transient( $cache_key );
    521 
    522         if ( false !== $cached ) {
    523             return $cached;
     522
     523        if ( ! $force_refresh ) {
     524            $cached = get_transient( $cache_key );
     525            if ( false !== $cached ) {
     526                return $cached;
     527            }
    524528        }
    525529
     
    540544     * @return array Actor profiles or error.
    541545     */
    542     public function get_actor_profiles( $args = array() ) {
     546    public function get_actor_profiles( $args = array(), $force_refresh = false ) {
    543547        $defaults = array(
    544548            'page'     => 1,
     
    549553
    550554        $cache_key = 'atomicedge_actors_' . hash( 'sha256', (string) wp_json_encode( $args ) );
    551         $cached    = get_transient( $cache_key );
    552 
    553         if ( false !== $cached ) {
    554             return $cached;
     555
     556        if ( ! $force_refresh ) {
     557            $cached = get_transient( $cache_key );
     558            if ( false !== $cached ) {
     559                return $cached;
     560            }
    555561        }
    556562
     
    570576     * @return array Threat detections or error.
    571577     */
    572     public function get_threat_detections( $args = array() ) {
     578    public function get_threat_detections( $args = array(), $force_refresh = false ) {
    573579        $defaults = array(
    574580            'page'     => 1,
     
    579585
    580586        $cache_key = 'atomicedge_detections_' . hash( 'sha256', (string) wp_json_encode( $args ) );
    581         $cached    = get_transient( $cache_key );
    582 
    583         if ( false !== $cached ) {
    584             return $cached;
     587
     588        if ( ! $force_refresh ) {
     589            $cached = get_transient( $cache_key );
     590            if ( false !== $cached ) {
     591                return $cached;
     592            }
    585593        }
    586594
     
    627635
    628636        if ( $response['success'] ) {
    629             // Clear caches.
     637            // Clear caches — AD cache + WAF log cache (since WAF logs
     638            // show is_blocked status from both blacklist and AD).
    630639            $this->clear_adaptive_defense_cache();
     640            $this->invalidate_waf_log_cache();
    631641        }
    632642
     
    642652    public function unblock_ip( $ip ) {
    643653        $response = $this->request( 'POST', '/adaptive-defense/unblock', array( 'ip' => $ip ) );
     654
     655        if ( $response['success'] ) {
     656            $this->clear_adaptive_defense_cache();
     657            $this->invalidate_waf_log_cache();
     658        }
     659
     660        return $response;
     661    }
     662
     663    /**
     664     * Extend the block duration for a blocked IP address.
     665     *
     666     * @param string $ip   IP address.
     667     * @param int    $days Number of days to extend (default 1).
     668     * @return array Result.
     669     */
     670    public function extend_block( $ip, $days = 1 ) {
     671        $response = $this->request( 'POST', '/adaptive-defense/extend-block', array(
     672            'ip'   => $ip,
     673            'days' => $days,
     674        ) );
     675
     676        if ( $response['success'] ) {
     677            $this->clear_adaptive_defense_cache();
     678        }
     679
     680        return $response;
     681    }
     682
     683    /**
     684     * Make a timed block permanent.
     685     *
     686     * @param string $ip IP address.
     687     * @return array Result.
     688     */
     689    public function make_permanent( $ip ) {
     690        $response = $this->request( 'POST', '/adaptive-defense/make-permanent', array( 'ip' => $ip ) );
    644691
    645692        if ( $response['success'] ) {
  • atomic-edge-security/trunk/includes/class-atomicedge-dev-mode.php

    r3473217 r3476055  
    8282        // Default to enabled for local environments, can be disabled via option.
    8383        return get_option( 'atomicedge_dev_mode', true );
     84    }
     85
     86    /**
     87     * Convert an ISO 3166-1 alpha-2 country code to a flag emoji.
     88     *
     89     * Uses Unicode Regional Indicator Symbols — same approach as
     90     * ActorProfile::getCountryFlagEmojiAttribute() on the Laravel side.
     91     *
     92     * @param string|null $country_code Two-letter code (e.g. 'US').
     93     * @return string Flag emoji or empty string.
     94     */
     95    public static function country_code_to_flag( $country_code ) {
     96        if ( empty( $country_code ) || strlen( $country_code ) !== 2 ) {
     97            return '';
     98        }
     99        $code = strtoupper( $country_code );
     100        $base = 0x1F1E6 - ord( 'A' );
     101        // mb_chr requires PHP 7.2+ (WordPress 5.x minimum).
     102        return mb_chr( $base + ord( $code[0] ) ) . mb_chr( $base + ord( $code[1] ) );
    84103    }
    85104
     
    358377        return array(
    359378            array(
    360                 'id'              => 1001,
    361                 'ip'              => '45.33.32.156',
    362                 'ip_address'      => '45.33.32.156',
    363                 'country_code'    => 'US',
     379                'id'                  => 1001,
     380                'ip'                  => '45.33.32.156',
     381                'ip_address'          => '45.33.32.156',
     382                'country_code'        => 'US',
     383                'country_flag_emoji'  => self::country_code_to_flag( 'US' ),
    364384                'total_requests'  => 1523,
    365385                'total_waf_hits'  => 89,
     
    373393            ),
    374394            array(
    375                 'id'              => 1002,
    376                 'ip'              => '103.235.46.39',
    377                 'ip_address'      => '103.235.46.39',
    378                 'country_code'    => 'CN',
     395                'id'                  => 1002,
     396                'ip'                  => '103.235.46.39',
     397                'ip_address'          => '103.235.46.39',
     398                'country_code'        => 'CN',
     399                'country_flag_emoji'  => self::country_code_to_flag( 'CN' ),
    379400                'total_requests'  => 856,
    380401                'total_waf_hits'  => 45,
     
    403424                'ip_address'       => '45.33.32.156',
    404425                'country_code'     => 'US',
     426                'country_flag_emoji' => self::country_code_to_flag( 'US' ),
    405427                'total_requests'   => 1523,
    406428                'total_waf_hits'   => 89,
     
    426448                'ip_address'       => '103.235.46.39',
    427449                'country_code'     => 'CN',
     450                'country_flag_emoji' => self::country_code_to_flag( 'CN' ),
    428451                'total_requests'   => 856,
    429452                'total_waf_hits'   => 45,
     
    447470                'ip_address'       => '198.51.100.42',
    448471                'country_code'     => 'DE',
     472                'country_flag_emoji' => self::country_code_to_flag( 'DE' ),
    449473                'total_requests'   => 324,
    450474                'total_waf_hits'   => 8,
     
    468492                'ip_address'       => '203.0.113.88',
    469493                'country_code'     => 'RU',
     494                'country_flag_emoji' => self::country_code_to_flag( 'RU' ),
    470495                'total_requests'   => 2100,
    471496                'total_waf_hits'   => 156,
     
    491516                'ip_address'       => '192.0.2.200',
    492517                'country_code'     => 'BR',
     518                'country_flag_emoji' => self::country_code_to_flag( 'BR' ),
    493519                'total_requests'   => 98,
    494520                'total_waf_hits'   => 3,
     
    560586                'status'          => 'auto_blocked',
    561587                'ip_address'      => '45.33.32.156',
     588                'country_code'    => 'US',
     589                'country_flag_emoji' => self::country_code_to_flag( 'US' ),
    562590                'created_at'      => gmdate( 'c', time() - 7200 ),
    563591                'detected_at'     => gmdate( 'c', time() - 7200 ),
     
    570598                    'ip_address'       => '45.33.32.156',
    571599                    'country_code'     => 'US',
     600                    'country_flag_emoji' => self::country_code_to_flag( 'US' ),
    572601                    'total_requests'   => 1523,
    573602                    'total_waf_hits'   => 89,
     
    591620                'status'          => 'pending_review',
    592621                'ip_address'      => '103.235.46.39',
     622                'country_code'    => 'CN',
     623                'country_flag_emoji' => self::country_code_to_flag( 'CN' ),
    593624                'created_at'      => gmdate( 'c', time() - 14400 ),
    594625                'detected_at'     => gmdate( 'c', time() - 14400 ),
     
    601632                    'ip_address'       => '103.235.46.39',
    602633                    'country_code'     => 'CN',
     634                    'country_flag_emoji' => self::country_code_to_flag( 'CN' ),
    603635                    'total_requests'   => 856,
    604636                    'total_waf_hits'   => 45,
     
    622654                'status'          => 'user_blocked',
    623655                'ip_address'      => '203.0.113.88',
     656                'country_code'    => 'RU',
     657                'country_flag_emoji' => self::country_code_to_flag( 'RU' ),
    624658                'created_at'      => gmdate( 'c', time() - 3600 ),
    625659                'detected_at'     => gmdate( 'c', time() - 3600 ),
     
    633667                    'ip_address'       => '203.0.113.88',
    634668                    'country_code'     => 'RU',
     669                    'country_flag_emoji' => self::country_code_to_flag( 'RU' ),
    635670                    'total_requests'   => 2100,
    636671                    'total_waf_hits'   => 156,
     
    654689                'status'          => 'pending_review',
    655690                'ip_address'      => '198.51.100.42',
     691                'country_code'    => 'DE',
     692                'country_flag_emoji' => self::country_code_to_flag( 'DE' ),
    656693                'created_at'      => gmdate( 'c', time() - 28800 ),
    657694                'detected_at'     => gmdate( 'c', time() - 28800 ),
     
    664701                    'ip_address'       => '198.51.100.42',
    665702                    'country_code'     => 'DE',
     703                    'country_flag_emoji' => self::country_code_to_flag( 'DE' ),
    666704                    'total_requests'   => 324,
    667705                    'total_waf_hits'   => 8,
     
    685723                'status'          => 'dismissed',
    686724                'ip_address'      => '192.0.2.200',
     725                'country_code'    => 'BR',
     726                'country_flag_emoji' => self::country_code_to_flag( 'BR' ),
    687727                'created_at'      => gmdate( 'c', time() - 86400 ),
    688728                'detected_at'     => gmdate( 'c', time() - 86400 ),
     
    695735                    'ip_address'       => '192.0.2.200',
    696736                    'country_code'     => 'BR',
     737                    'country_flag_emoji' => self::country_code_to_flag( 'BR' ),
    697738                    'total_requests'   => 98,
    698739                    'total_waf_hits'   => 3,
     
    802843
    803844    /**
     845     * Simulate a successful extend block response.
     846     *
     847     * @param string $ip   IP address.
     848     * @param int    $days Number of days to extend.
     849     * @return array
     850     */
     851    public static function simulate_extend_block( $ip, $days = 1 ) {
     852        return array(
     853            'message'          => sprintf(
     854                /* translators: 1: IP address, 2: number of days */
     855                __( '[Dev Mode] Block for %1$s extended by %2$d day(s).', 'atomic-edge-security' ),
     856                $ip,
     857                $days
     858            ),
     859            'ip'               => $ip,
     860            'is_blocked'       => true,
     861            'blocked_at'       => gmdate( 'c', time() - 3600 ),
     862            'block_expires_at' => gmdate( 'c', time() + ( $days * 86400 ) ),
     863        );
     864    }
     865
     866    /**
     867     * Simulate a successful make permanent response.
     868     *
     869     * @param string $ip IP address.
     870     * @return array
     871     */
     872    public static function simulate_make_permanent( $ip ) {
     873        return array(
     874            'message'          => sprintf(
     875                /* translators: %s: IP address */
     876                __( '[Dev Mode] Block for %s is now permanent.', 'atomic-edge-security' ),
     877                $ip
     878            ),
     879            'ip'               => $ip,
     880            'is_blocked'       => true,
     881            'blocked_at'       => gmdate( 'c', time() - 3600 ),
     882            'block_expires_at' => null,
     883        );
     884    }
     885
     886    /**
    804887     * Simulate a successful dismiss detection response.
    805888     *
  • atomic-edge-security/trunk/readme.txt

    r3473217 r3476055  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 2.4.7
     7Stable tag: 2.4.8
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    112112
    113113== Changelog ==
     114
     115= 2.4.8 =
     116* NEW: Added Blocked IPs tab to Adaptive Defense with IP Address, Threat Score, WAF Hits, Type, Blocked, Expires columns and actions (Extend, Make Permanent, Unblock)
     117* FIX: Adaptive Defense block actions now route through dashboard Blocked IPs (application-layer) instead of Access Control IP blacklist (edge config)
     118* NEW: Manual block form on Blocked IPs tab with configurable duration (1h, 6h, 24h, 7d, 30d, permanent)
     119* NEW: Extend block (+1 day) and Make Permanent actions for timed blocks
     120* CHANGE: WAF Logs "Block IP" button renamed to "Blacklist IP" to clarify it adds to edge-level IP blacklist
     121* NEW: Added extend_block() and make_permanent() API methods and AJAX handlers with dev mode support
    114122
    115123= 2.4.7 =
Note: See TracChangeset for help on using the changeset viewer.