Plugin Directory

Changeset 3473194


Ignore:
Timestamp:
03/03/2026 02:17:26 AM (5 days ago)
Author:
shift8
Message:

Bug fixes, test coverage, improvements

Location:
atomic-edge-security/trunk
Files:
1 added
12 edited

Legend:

Unmodified
Added
Removed
  • atomic-edge-security/trunk/admin/css/admin.css

    r3454914 r3473194  
    15931593}
    15941594
     1595/* ──── Source badges for Access Control blacklist ──── */
     1596.atomicedge-source-badge {
     1597    display: inline-block;
     1598    padding: 2px 8px;
     1599    border-radius: 3px;
     1600    font-size: 12px;
     1601    font-weight: 600;
     1602    line-height: 1.4;
     1603    white-space: nowrap;
     1604}
     1605
     1606.atomicedge-source-manual {
     1607    background: #f0f0f1;
     1608    color: #50575e;
     1609}
     1610
     1611.atomicedge-source-waf {
     1612    background: #fef0e5;
     1613    color: #9a6700;
     1614}
     1615
     1616.atomicedge-source-actor {
     1617    background: #e5f0fe;
     1618    color: #1d4ed8;
     1619}
     1620
     1621.atomicedge-source-detection {
     1622    background: #fce8e8;
     1623    color: #b32d2e;
     1624}
     1625
     1626.atomicedge-source-ad {
     1627    background: #f0e5fe;
     1628    color: #7928ca;
     1629}
     1630
     1631/* ──── Blocked badge in WAF logs ──── */
     1632.atomicedge-blocked-badge {
     1633    display: inline-flex;
     1634    align-items: center;
     1635    gap: 2px;
     1636}
     1637
  • atomic-edge-security/trunk/admin/js/adaptive-defense.js

    r3454914 r3473194  
    1515        /** Current page for each section */
    1616        pages: {
    17             blocked: 1,
    1817            actors: 1,
    1918            detections: 1
     
    4241            if ($('#atomicedge-ad-status-card').length) {
    4342                this.loadStatusTab();
    44             } else if ($('#atomicedge-ad-blocked-card').length) {
    45                 this.loadBlockedIps();
    4643            } else if ($('#atomicedge-ad-actors-card').length) {
    4744                this.loadActorProfiles();
     
    6562            });
    6663
    67             // Blocked IPs tab
    68             $('#atomicedge-ad-blocked-refresh').on('click', function() {
    69                 self.loadBlockedIps();
    70             });
    71             // Block IP button click (not form submit)
    72             $('#atomicedge-ad-block-btn').on('click', function(e) {
    73                 e.preventDefault();
    74                 self.blockIpFromForm();
    75             });
    76             $(document).on('click', '.atomicedge-ad-unblock-btn', function() {
    77                 var ip = $(this).data('ip');
    78                 self.unblockIp(ip);
    79             });
    80 
    8164            // Actor Profiles tab
    8265            $('#atomicedge-ad-actors-refresh').on('click', function() {
     
    9982            });
    10083            $(document).on('click', '.atomicedge-ad-block-actor-btn', function() {
    101                 var ip = $(this).data('ip');
    102                 self.blockIp(ip, 'actor');
     84                var $btn = $(this);
     85                var ip = $btn.data('ip');
     86                self.blockIpToBlacklist(ip, 'actor', $btn);
    10387            });
    10488            $(document).on('click', '.atomicedge-ad-delete-actor-btn', function() {
     
    122106            });
    123107            $(document).on('click', '.atomicedge-ad-block-detection-btn', function() {
    124                 var ip = $(this).data('ip');
    125                 self.blockIp(ip, 'detection');
     108                var $btn = $(this);
     109                var ip = $btn.data('ip');
     110                self.blockIpToBlacklist(ip, 'detection', $btn);
    126111            });
    127112            $(document).on('click', '.atomicedge-ad-dismiss-btn', function() {
    128                 var id = $(this).data('id');
    129                 self.dismissDetection(id);
     113                var $btn = $(this);
     114                var id = $btn.data('id');
     115                self.dismissDetection(id, $btn);
    130116            });
    131117            $(document).on('click', '.atomicedge-ad-detail-close', function() {
     
    140126               
    141127                switch (section) {
    142                     case 'blocked':
    143                         self.loadBlockedIps();
    144                         break;
    145128                    case 'actors':
    146129                        self.loadActorProfiles();
     
    171154                case 'status':
    172155                    this.loadStatusTab();
    173                     break;
    174                 case 'blocked':
    175                     this.loadBlockedIps();
    176156                    break;
    177157                case 'actors':
     
    302282
    303283        /**
    304          * Load Blocked IPs
    305          */
    306         loadBlockedIps: function() {
    307             var self = this;
    308             var $loading = $('#atomicedge-ad-blocked-loading');
    309             var $wrapper = $('#atomicedge-ad-blocked-table-wrapper');
    310 
    311             $loading.show();
    312             $wrapper.hide();
     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.
     300         *
     301         * @param {string} ip      IP address to block.
     302         * @param {string} source  Source context (actor, detection).
     303         * @param {jQuery} $button Optional button element to update.
     304         */
     305        blockIpToBlacklist: function(ip, source, $button) {
     306            var self = this;
     307            source = source || 'adaptive_defense';
     308
     309            if (!ip) {
     310                self.showNotice('Please enter an IP address', 'error');
     311                return;
     312            }
     313
     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.
     326            if ($button) {
     327                $button.prop('disabled', true);
     328            }
    313329
    314330            $.ajax({
     
    316332                type: 'POST',
    317333                data: {
    318                     action: 'atomicedge_get_adaptive_defense',
    319                     nonce: atomicedge_admin.nonce
    320                 },
    321                 success: function(response) {
    322                     $loading.hide();
    323                     $wrapper.show();
    324 
    325                     if (response.success && response.data && response.data.blocked_ips) {
    326                         self.renderBlockedIps(response.data.blocked_ips);
    327                     } else {
    328                         self.renderBlockedIps([]);
    329                     }
    330                 },
    331                 error: function(xhr, status, error) {
    332                     $loading.hide();
    333                     $wrapper.show();
    334                     self.showTableError('#atomicedge-ad-blocked-body', 'Network error: ' + error);
    335                 }
    336             });
    337         },
    338 
    339         /**
    340          * Render Blocked IPs table
    341          *
    342          * @param {Array} blockedIps List of blocked IPs
    343          */
    344         renderBlockedIps: function(blockedIps) {
    345             var $tbody = $('#atomicedge-ad-blocked-body');
    346             var $empty = $('#atomicedge-ad-blocked-empty');
    347             var $table = $('#atomicedge-ad-blocked-table');
    348 
    349             $tbody.empty();
    350 
    351             if (!blockedIps || blockedIps.length === 0) {
    352                 $table.hide();
    353                 $empty.show();
    354                 return;
    355             }
    356 
    357             $table.show();
    358             $empty.hide();
    359 
    360             var self = this;
    361             blockedIps.forEach(function(blocked) {
    362                 var ipAddress = blocked.ip_address || blocked.ip || '';
    363                 var html = '<tr>';
    364                 html += '<td>' + self.escapeHtml(ipAddress) + '</td>';
    365                 html += '<td>' + self.formatScore(blocked.score || 0) + '</td>';
    366                 html += '<td>' + (blocked.waf_hits || 0) + '</td>';
    367                 // Expires column
    368                 if (blocked.is_permanent) {
    369                     html += '<td>Never (Permanent)</td>';
    370                 } else if (blocked.expires) {
    371                     html += '<td>' + self.formatDate(blocked.expires) + '</td>';
    372                 } else {
    373                     html += '<td>&mdash;</td>';
    374                 }
    375                 html += '<td>';
    376                 html += '<button type="button" class="button button-small atomicedge-ad-unblock-btn" data-ip="' + self.escapeHtml(ipAddress) + '">';
    377                 html += '<span class="dashicons dashicons-unlock" style="margin-top: 3px;"></span> Unblock</button>';
    378                 html += '</td>';
    379                 html += '</tr>';
    380                 $tbody.append(html);
    381             });
    382         },
    383 
    384         /**
    385          * Block an IP address from the form
    386          */
    387         blockIpFromForm: function() {
    388             var self = this;
    389             var ip = $('#atomicedge-ad-block-ip').val().trim();
    390             var durationValue = $('#atomicedge-ad-block-duration').val();
    391            
    392             if (!ip) {
    393                 alert('Please enter an IP address');
    394                 return;
    395             }
    396 
    397             var permanent = durationValue === 'permanent';
    398             var durationHours = permanent ? 24 : parseInt(durationValue, 10);
    399 
    400             var $btn = $('#atomicedge-ad-block-btn');
    401             $btn.prop('disabled', true).text('Blocking...');
    402 
    403             $.ajax({
    404                 url: atomicedge_admin.ajax_url,
    405                 type: 'POST',
    406                 data: {
    407                     action: 'atomicedge_block_ip',
     334                    action: 'atomicedge_add_ip_blacklist',
    408335                    nonce: atomicedge_admin.nonce,
    409336                    ip: ip,
    410                     duration_hours: durationHours,
    411                     permanent: permanent ? 'true' : 'false'
     337                    description: description
    412338                },
    413339                success: function(response) {
    414                     $btn.prop('disabled', false).html('<span class="dashicons dashicons-lock" style="margin-top: 3px;"></span> Block IP');
    415                    
    416340                    if (response.success) {
    417                         $('#atomicedge-ad-block-ip').val('');
    418                         self.showNotice('IP address blocked successfully', 'success');
    419                         self.loadBlockedIps();
    420                     } else {
    421                         self.showNotice(response.data ? response.data.message : 'Failed to block IP', 'error');
    422                     }
    423                 },
    424                 error: function(xhr, status, error) {
    425                     $btn.prop('disabled', false).html('<span class="dashicons dashicons-lock" style="margin-top: 3px;"></span> Block IP');
    426                     self.showNotice('Network error: ' + error, 'error');
    427                 }
    428             });
    429         },
    430 
    431         /**
    432          * Block an IP address
    433          *
    434          * @param {string} ip Optional IP address (from form if not provided)
    435          * @param {string} source Source context (form, actor, detection)
    436          */
    437         blockIp: function(ip, source) {
    438             var self = this;
    439             source = source || 'form';
    440            
    441             if (!ip && source === 'form') {
    442                 ip = $('#atomicedge-ad-block-ip').val().trim();
    443             }
    444 
    445             if (!ip) {
    446                 alert('Please enter an IP address');
    447                 return;
    448             }
    449 
    450             $.ajax({
    451                 url: atomicedge_admin.ajax_url,
    452                 type: 'POST',
    453                 data: {
    454                     action: 'atomicedge_block_ip',
    455                     nonce: atomicedge_admin.nonce,
    456                     ip: ip,
    457                     duration_hours: 24,
    458                     permanent: 'false'
    459                 },
    460                 beforeSend: function() {
    461                     if (source === 'form') {
    462                         $('#atomicedge-ad-block-ip-submit').prop('disabled', true).text('Blocking...');
    463                     }
    464                 },
    465                 success: function(response) {
    466                     if (source === 'form') {
    467                         $('#atomicedge-ad-block-ip-submit').prop('disabled', false).text('Block IP');
    468                     }
    469 
    470                     if (response.success) {
    471                         if (source === 'form') {
    472                             $('#atomicedge-ad-block-ip').val('');
    473                             $('#atomicedge-ad-block-reason').val('');
     341                        // Update button to show blocked state.
     342                        if ($button) {
     343                            $button.prop('disabled', true)
     344                                .html('<span class="dashicons dashicons-yes-alt" style="margin-top:3px;color:#00a32a;"></span> Blocked');
    474345                        }
    475                         self.showNotice('IP address blocked successfully', 'success');
    476                         self.loadBlockedIps();
    477                        
    478                         // Refresh other tabs if needed
     346
     347                        self.showNotice(
     348                            ip + ' has been added to the blacklist. Manage blocks in Access Control.',
     349                            'success'
     350                        );
     351
     352                        // Refresh source tab to show updated state.
    479353                        if (source === 'actor') {
    480354                            self.loadActorProfiles();
     
    483357                        }
    484358                    } else {
    485                         self.showNotice(response.data ? response.data.message : 'Failed to block IP', 'error');
     359                        // Re-enable on failure.
     360                        if ($button) {
     361                            $button.prop('disabled', false);
     362                        }
     363                        var message = (response.data && response.data.message) ? response.data.message : 'Failed to block IP';
     364                        self.showNotice(message, 'error');
    486365                    }
    487366                },
    488367                error: function(xhr, status, error) {
    489                     if (source === 'form') {
    490                         $('#atomicedge-ad-block-ip-submit').prop('disabled', false).text('Block IP');
     368                    if ($button) {
     369                        $button.prop('disabled', false);
    491370                    }
    492                     self.showNotice('Network error: ' + error, 'error');
    493                 }
    494             });
    495         },
    496 
    497         /**
    498          * Unblock an IP address
    499          *
    500          * @param {string} ip IP address to unblock
    501          */
    502         unblockIp: function(ip) {
    503             var self = this;
    504 
    505             if (!confirm('Are you sure you want to unblock ' + ip + '?')) {
    506                 return;
    507             }
    508 
    509             $.ajax({
    510                 url: atomicedge_admin.ajax_url,
    511                 type: 'POST',
    512                 data: {
    513                     action: 'atomicedge_unblock_ip',
    514                     nonce: atomicedge_admin.nonce,
    515                     ip: ip
    516                 },
    517                 success: function(response) {
    518                     if (response.success) {
    519                         self.showNotice('IP address unblocked successfully', 'success');
    520                         self.loadBlockedIps();
    521                     } else {
    522                         self.showNotice(response.data ? response.data.message : 'Failed to unblock IP', 'error');
    523                     }
    524                 },
    525                 error: function(xhr, status, error) {
    526371                    self.showNotice('Network error: ' + error, 'error');
    527372                }
     
    737582                html += '<button type="button" class="button button-small atomicedge-ad-view-detection-btn" data-id="' + detection.id + '" title="View details">';
    738583                html += '<span class="dashicons dashicons-visibility" style="margin-top: 3px;"></span></button> ';
    739                 if (detection.status !== 'blocked') {
     584                var isBlocked = (detection.status === 'blocked' || detection.status === 'auto_blocked' || detection.status === 'user_blocked');
     585                if (!isBlocked) {
    740586                    html += '<button type="button" class="button button-small atomicedge-ad-block-detection-btn" data-ip="' + self.escapeHtml(ipAddress) + '" title="Block IP">';
    741587                    html += '<span class="dashicons dashicons-shield" style="margin-top: 3px;"></span></button> ';
     
    770616            $('.atomicedge-ad-detail-row').remove();
    771617
    772             // Clone the detail template and insert it
    773             var $template = $('#atomicedge-ad-detection-detail-template').find('tr').clone();
     618            // Clone the detail template and insert it.
     619            // Uses <template> element so the browser preserves <tr>/<td> structure.
     620            var templateEl = document.getElementById('atomicedge-ad-detection-detail-template');
     621            if (!templateEl || !templateEl.content) {
     622                return;
     623            }
     624            var $template = $(templateEl.content.querySelector('.atomicedge-ad-detail-row')).clone();
    774625            $row.after($template);
    775626
     
    820671            $detailRow.find('.atomicedge-ad-detail-confidence').text((detection.confidence || 0) + '%');
    821672            $detailRow.find('.atomicedge-ad-detail-status').html(this.formatDetectionStatus(detection.status || 'pending'));
    822             $detailRow.find('.atomicedge-ad-detail-detected-at').text(this.formatDate(detection.created_at));
     673            $detailRow.find('.atomicedge-ad-detail-detected-at').text(this.formatDate(detection.detected_at || detection.created_at));
    823674
    824675            // Actor details
    825             $detailRow.find('.atomicedge-ad-detail-ip').text(actor.ip_address || detection.ip_address || 'N/A');
     676            $detailRow.find('.atomicedge-ad-detail-ip').text(actor.ip || actor.ip_address || detection.ip_address || 'N/A');
    826677            $detailRow.find('.atomicedge-ad-detail-requests').text(actor.total_requests || 0);
    827             $detailRow.find('.atomicedge-ad-detail-waf-hits').text(actor.waf_hits || 0);
    828             $detailRow.find('.atomicedge-ad-detail-errors').text((actor.error_4xx || 0) + ' / ' + (actor.error_5xx || 0));
    829             $detailRow.find('.atomicedge-ad-detail-first-seen').text(this.formatDate(actor.first_seen_at));
    830             $detailRow.find('.atomicedge-ad-detail-last-seen').text(this.formatDate(actor.last_seen_at || actor.updated_at));
     678            $detailRow.find('.atomicedge-ad-detail-waf-hits').text(actor.total_waf_hits || actor.waf_hits || 0);
     679            $detailRow.find('.atomicedge-ad-detail-errors').text((actor.total_4xx_errors || actor.error_4xx || 0) + ' / ' + (actor.total_5xx_errors || actor.error_5xx || 0));
     680            $detailRow.find('.atomicedge-ad-detail-first-seen').text(this.formatDate(actor.first_seen || actor.first_seen_at));
     681            $detailRow.find('.atomicedge-ad-detail-last-seen').text(this.formatDate(actor.last_seen || actor.last_seen_at || actor.updated_at));
    831682
    832683            // Reasons
     
    861712         * Dismiss a threat detection
    862713         *
    863          * @param {number} id Detection ID
    864          */
    865         dismissDetection: function(id) {
     714         * @param {number} id      Detection ID.
     715         * @param {jQuery} $button Optional button element to update.
     716         */
     717        dismissDetection: function(id, $button) {
    866718            var self = this;
    867719
    868720            if (!confirm('Are you sure you want to dismiss this detection?')) {
    869721                return;
     722            }
     723
     724            // Disable button immediately.
     725            if ($button) {
     726                $button.prop('disabled', true);
    870727            }
    871728
     
    880737                success: function(response) {
    881738                    if (response.success) {
     739                        // Update the row inline to show dismissed state.
     740                        if ($button) {
     741                            var $row = $button.closest('tr');
     742                            $row.find('.atomicedge-ad-status-badge')
     743                                .removeClass('atomicedge-ad-status-pending atomicedge-ad-status-pending_review')
     744                                .addClass('atomicedge-ad-status-dismissed')
     745                                .text('Dismissed');
     746                            // Remove action buttons (dismiss + block) from this row.
     747                            $button.closest('td').find('.atomicedge-ad-dismiss-btn, .atomicedge-ad-block-detection-btn').remove();
     748                        }
    882749                        self.showNotice('Detection dismissed successfully', 'success');
    883                         self.loadThreatDetections();
     750                        // Refresh after a short delay so user sees the state change.
     751                        setTimeout(function() {
     752                            self.loadThreatDetections();
     753                        }, 1500);
    884754                    } else {
     755                        if ($button) {
     756                            $button.prop('disabled', false);
     757                        }
    885758                        self.showNotice(response.data ? response.data.message : 'Failed to dismiss detection', 'error');
    886759                    }
    887760                },
    888761                error: function(xhr, status, error) {
     762                    if ($button) {
     763                        $button.prop('disabled', false);
     764                    }
    889765                    self.showNotice('Network error: ' + error, 'error');
    890766                }
     
    976852            var labels = {
    977853                'pending': 'Pending',
     854                'pending_review': 'Pending Review',
     855                'auto_blocked': 'Blocked',
     856                'user_blocked': 'Blocked',
    978857                'blocked': 'Blocked',
    979                 'dismissed': 'Dismissed'
     858                'dismissed': 'Dismissed',
     859                'expired': 'Expired'
    980860            };
    981             return '<span class="atomicedge-ad-status-badge atomicedge-ad-status-' + status + '">' +
     861            // Map status to CSS class (normalize blocked variants).
     862            var cssClass = status;
     863            if (status === 'auto_blocked' || status === 'user_blocked') {
     864                cssClass = 'blocked';
     865            } else if (status === 'pending_review') {
     866                cssClass = 'pending';
     867            }
     868            return '<span class="atomicedge-ad-status-badge atomicedge-ad-status-' + cssClass + '">' +
    982869                   (labels[status] || status) + '</span>';
    983870        },
  • atomic-edge-security/trunk/admin/js/admin.js

    r3452356 r3473194  
    6767                $('#' + tab).addClass('atomicedge-tab-active');
    6868            });
     69
     70            // Support ?tab= query param to pre-select a tab on page load.
     71            var params = new URLSearchParams(window.location.search);
     72            var requestedTab = params.get('tab');
     73            if (requestedTab) {
     74                var $target = $('.atomicedge-tabs .nav-tab[data-tab="ip-' + requestedTab + '"]');
     75                if ($target.length === 0) {
     76                    $target = $('.atomicedge-tabs .nav-tab[data-tab="' + requestedTab + '"]');
     77                }
     78                if ($target.length) {
     79                    $target.trigger('click');
     80                }
     81            }
    6982        },
    7083
     
    380393            var self = this;
    381394            logs.forEach(function(log) {
     395                var actionCell;
     396                if (log.is_blocked) {
     397                    actionCell = '<span class="atomicedge-blocked-badge" title="This IP is in your blacklist" style="color:#b32d2e;font-weight:600;">' +
     398                        '<span class="dashicons dashicons-lock" style="font-size:14px;width:14px;height:14px;margin-top:3px;"></span> Blocked</span>';
     399                } else {
     400                    actionCell = '<button type="button" class="button button-small atomicedge-block-ip" data-ip="' + self.escapeHtml(log.client_ip || '') + '">Block IP</button>';
     401                }
    382402                var row = '<tr>' +
    383403                    '<td>' + self.escapeHtml(log.event_timestamp || '') + '</td>' +
     
    386406                    '<td><code>' + self.escapeHtml(log.waf_rule_id || '') + '</code></td>' +
    387407                    '<td>' + self.escapeHtml(log.group || '') + '</td>' +
    388                     '<td><button type="button" class="button button-small atomicedge-block-ip" data-ip="' + self.escapeHtml(log.client_ip || '') + '">Block IP</button></td>' +
     408                    '<td>' + actionCell + '</td>' +
    389409                    '</tr>';
    390410                $tbody.append(row);
     
    393413            // Bind block IP buttons
    394414            $tbody.find('.atomicedge-block-ip').on('click', function() {
    395                 var ip = $(this).data('ip');
     415                var $btn = $(this);
     416                var ip = $btn.data('ip');
    396417                if (confirm(atomicedgeAdmin.strings.confirm)) {
    397                     self.addIpBlacklist(ip, 'Blocked from WAF logs');
     418                    self.addIpBlacklist(ip, 'Blocked from WAF logs on ' + self.formatTimestamp(), $btn);
    398419                }
    399420            });
     
    425446
    426447                if (!self.validateIp(ip)) {
    427                     alert(atomicedgeAdmin.strings.invalidIp);
     448                    self.showNotice(atomicedgeAdmin.strings.invalidIp, 'error');
    428449                    return;
    429450                }
     
    439460
    440461                if (!self.validateIp(ip)) {
    441                     alert(atomicedgeAdmin.strings.invalidIp);
     462                    self.showNotice(atomicedgeAdmin.strings.invalidIp, 'error');
    442463                    return;
    443464                }
     
    474495
    475496        /**
     497         * Parse source label from a blacklist description.
     498         *
     499         * @param  {string} description e.g. "Blocked from WAF logs on 2026-02-27 14:35 UTC"
     500         * @return {string} e.g. "WAF Logs" or "Manual"
     501         */
     502        parseBlockSource: function(description) {
     503            if (!description) {
     504                return 'Manual';
     505            }
     506            var lower = description.toLowerCase();
     507            if (lower.indexOf('waf log') !== -1)              return 'WAF Logs';
     508            if (lower.indexOf('actor profile') !== -1)        return 'Actor Profiles';
     509            if (lower.indexOf('threat detection') !== -1)     return 'Threat Detections';
     510            if (lower.indexOf('adaptive defense') !== -1)     return 'Adaptive Defense';
     511            return 'Manual';
     512        },
     513
     514        /**
    476515         * Render IP list
    477516         */
    478517        renderIpList: function(type, ips) {
    479518            var $tbody = $('#atomicedge-' + type + '-body');
     519            var isBlacklist = (type === 'blacklist');
     520            var cols = isBlacklist ? 4 : 3;
    480521            $tbody.empty();
    481522
    482523            if (ips.length === 0) {
    483                 $tbody.html('<tr><td colspan="3">No IPs in ' + type + '</td></tr>');
     524                $tbody.html('<tr><td colspan="' + cols + '">No IPs in ' + type + '</td></tr>');
    484525                return;
    485526            }
     
    487528            var self = this;
    488529            ips.forEach(function(item) {
     530                var sourceCell = '';
     531                if (isBlacklist) {
     532                    var sourceLabel = self.parseBlockSource(item.description);
     533                    var badgeClass = 'atomicedge-source-manual';
     534                    if (sourceLabel === 'WAF Logs')           badgeClass = 'atomicedge-source-waf';
     535                    if (sourceLabel === 'Actor Profiles')     badgeClass = 'atomicedge-source-actor';
     536                    if (sourceLabel === 'Threat Detections')  badgeClass = 'atomicedge-source-detection';
     537                    if (sourceLabel === 'Adaptive Defense')   badgeClass = 'atomicedge-source-ad';
     538                    sourceCell = '<td><span class="atomicedge-source-badge ' + badgeClass + '">' + self.escapeHtml(sourceLabel) + '</span></td>';
     539                }
    489540                var row = '<tr>' +
    490541                    '<td><code>' + self.escapeHtml(item.ip) + '</code></td>' +
     542                    sourceCell +
    491543                    '<td>' + self.escapeHtml(item.description || '') + '</td>' +
    492544                    '<td><button type="button" class="button button-small atomicedge-remove-ip" data-ip="' + self.escapeHtml(item.ip) + '" data-type="' + type + '">Remove</button></td>' +
     
    519571        /**
    520572         * Add IP to blacklist
    521          */
    522         addIpBlacklist: function(ip, description) {
    523             var self = this;
     573         *
     574         * @param {string}  ip          IP address or CIDR.
     575         * @param {string}  description Optional description.
     576         * @param {jQuery}  $button     Optional button element to update on success.
     577         */
     578        addIpBlacklist: function(ip, description, $button) {
     579            var self = this;
     580
     581            if ($button) {
     582                $button.prop('disabled', true).text(atomicedgeAdmin.strings.loading);
     583            }
     584
    524585            this.ajax('atomicedge_add_ip_blacklist', { ip: ip, description: description }, function() {
     586                // Clear form fields if on access control page.
    525587                $('#blacklist-ip').val('');
    526588                $('#blacklist-description').val('');
    527589                self.loadIpRules();
    528             });
     590
     591                // If on WAF logs page, reload the table so is_blocked badges
     592                // reflect the authoritative server state (cache was invalidated
     593                // server-side by the add_ip_blacklist API call).
     594                if ($('#atomicedge-waf-table').length) {
     595                    self.loadWafLogs();
     596                }
     597
     598                // Update button to show blocked state (immediate feedback before
     599                // the WAF logs reload finishes).
     600                if ($button) {
     601                    $button.prop('disabled', true)
     602                        .removeClass('button-small')
     603                        .addClass('atomicedge-blocked-btn')
     604                        .html('<span class="dashicons dashicons-yes-alt" style="margin-top:3px;color:#00a32a;"></span> Blocked');
     605                }
     606
     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                }
     612                self.showNotice(noticeMsg, 'success');
     613            }, function(errData) {
     614                if ($button) {
     615                    $button.prop('disabled', false).text('Block IP');
     616                }
     617                var message = (errData && errData.message) ? errData.message : atomicedgeAdmin.strings.error;
     618                self.showNotice(message, 'error');
     619            });
     620        },
     621
     622        /**
     623         * Format current date/time as a human-readable timestamp.
     624         *
     625         * @return {string} e.g. "2026-02-27 14:35 UTC"
     626         */
     627        formatTimestamp: function() {
     628            var d = new Date();
     629            var pad = function(n) { return n < 10 ? '0' + n : n; };
     630            return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1) + '-' + pad(d.getUTCDate()) +
     631                ' ' + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ' UTC';
    529632        },
    530633
     
    536639            this.ajax('atomicedge_remove_ip', { ip: ip, type: type }, function() {
    537640                self.loadIpRules();
     641                self.showNotice(self.escapeHtml(ip) + ' has been removed from the ' + self.escapeHtml(type) + '.', 'success');
    538642            });
    539643        },
     
    12081312         */
    12091313        ajax: function(action, data, success, error) {
     1314            var self = this;
    12101315            data = data || {};
    12111316            data.action = action;
     
    12251330                            error(response.data);
    12261331                        } else {
    1227                             alert(response.data.message || atomicedgeAdmin.strings.error);
     1332                            var message = (response.data && response.data.message) ? response.data.message : atomicedgeAdmin.strings.error;
     1333                            self.showNotice(message, 'error');
    12281334                        }
    12291335                    }
     
    12331339                        error();
    12341340                    } else {
    1235                         alert(atomicedgeAdmin.strings.error);
     1341                        self.showNotice(atomicedgeAdmin.strings.error, 'error');
    12361342                    }
    12371343                }
     
    12561362        formatNumber: function(num) {
    12571363            return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
     1364        },
     1365
     1366        /**
     1367         * Show a temporary admin notice.
     1368         *
     1369         * @param {string} message Notice message.
     1370         * @param {string} type    Notice type (success, error, warning, info).
     1371         */
     1372        showNotice: function(message, type) {
     1373            type = type || 'info';
     1374            var $notice = $(
     1375                '<div class="notice notice-' + type + ' is-dismissible atomicedge-notice">' +
     1376                '<p>' + this.escapeHtml(message) + '</p>' +
     1377                '<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>' +
     1378                '</div>'
     1379            );
     1380
     1381            // Insert at top of page content.
     1382            $('.wrap h1').first().after($notice);
     1383
     1384            // Bind dismiss handler.
     1385            $notice.find('.notice-dismiss').on('click', function() {
     1386                $notice.fadeOut(200, function() { $(this).remove(); });
     1387            });
     1388
     1389            // Auto dismiss after 5 seconds.
     1390            setTimeout(function() {
     1391                $notice.fadeOut(200, function() { $(this).remove(); });
     1392            }, 5000);
    12581393        },
    12591394
  • atomic-edge-security/trunk/admin/views/access-control.php

    r3449543 r3473194  
    108108                    <tr>
    109109                        <th class="column-ip"><?php esc_html_e( 'IP/CIDR', 'atomic-edge-security' ); ?></th>
     110                        <th class="column-source"><?php esc_html_e( 'Source', 'atomic-edge-security' ); ?></th>
    110111                        <th class="column-description"><?php esc_html_e( 'Description', 'atomic-edge-security' ); ?></th>
    111112                        <th class="column-actions"><?php esc_html_e( 'Actions', 'atomic-edge-security' ); ?></th>
     
    114115                <tbody id="atomicedge-blacklist-body">
    115116                    <tr class="atomicedge-loading-row">
    116                         <td colspan="3">
     117                        <td colspan="4">
    117118                            <span class="spinner is-active"></span>
    118119                            <?php esc_html_e( 'Loading...', 'atomic-edge-security' ); ?>
  • atomic-edge-security/trunk/admin/views/adaptive-defense.php

    r3454914 r3473194  
    2323// Tab navigation.
    2424$current_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'status';
    25 $valid_tabs  = array( 'status', 'blocked', 'actors', 'detections' );
     25$valid_tabs  = array( 'status', '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',
    3935    ),
    4036    'actors'     => array(
     
    9894                <?php
    9995                switch ( $current_tab ) {
    100                     case 'blocked':
    101                         include ATOMICEDGE_PLUGIN_DIR . 'admin/views/partials/adaptive-defense-blocked-tab.php';
    102                         break;
     96
    10397                    case 'actors':
    10498                        include ATOMICEDGE_PLUGIN_DIR . 'admin/views/partials/adaptive-defense-actors-tab.php';
  • atomic-edge-security/trunk/admin/views/partials/adaptive-defense-detections-tab.php

    r3454914 r3473194  
    7979
    8080<!-- Detection Detail Panel (inline expandable row approach per WordPress patterns) -->
    81 <div id="atomicedge-ad-detection-detail-template" style="display: none;">
     81<!-- Uses <template> so the browser doesn't strip <tr>/<td> during HTML parsing -->
     82<template id="atomicedge-ad-detection-detail-template">
    8283    <tr class="atomicedge-ad-detail-row">
    8384        <td colspan="7">
     
    170171        </td>
    171172    </tr>
    172 </div>
     173</template>
    173174
    174175<style>
  • atomic-edge-security/trunk/atomicedge.php

    r3470783 r3473194  
    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.5
     6 * Version: 2.4.6
    77 * Requires at least: 5.8
    88 * Requires PHP: 7.4
     
    2626
    2727// Plugin constants.
    28 define( 'ATOMICEDGE_VERSION', '2.4.5' );
     28define( 'ATOMICEDGE_VERSION', '2.4.6' );
    2929define( 'ATOMICEDGE_PLUGIN_FILE', __FILE__ );
    3030define( 'ATOMICEDGE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • atomic-edge-security/trunk/includes/class-atomicedge-ajax.php

    r3454914 r3473194  
    875875        $this->get_verified_post_fields( array() );
    876876
     877        // Dev mode: return simulated data.
     878        if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     879            wp_send_json_success( AtomicEdge_Dev_Mode::get_simulated_adaptive_defense() );
     880        }
     881
    877882        $response = $this->api->get_adaptive_defense();
    878883
     
    902907        }
    903908
     909        // Dev mode: return simulated data.
     910        if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     911            wp_send_json_success( AtomicEdge_Dev_Mode::get_simulated_actor_profiles( $args ) );
     912        }
     913
    904914        $response = $this->api->get_actor_profiles( $args );
    905915
     
    925935        );
    926936
     937        // Dev mode: return simulated data.
     938        if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     939            wp_send_json_success( AtomicEdge_Dev_Mode::get_simulated_threat_detections( $args ) );
     940        }
     941
    927942        $response = $this->api->get_threat_detections( $args );
    928943
     
    946961        }
    947962
     963        // Dev mode: return simulated detail data.
     964        if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     965            $detail = AtomicEdge_Dev_Mode::get_simulated_threat_detection_detail( absint( $post['detection_id'] ) );
     966            if ( $detail ) {
     967                wp_send_json_success( $detail );
     968            }
     969            wp_send_json_error( array( 'message' => __( 'Detection not found.', 'atomic-edge-security' ) ) );
     970        }
     971
    948972        $response = $this->api->get_threat_detection_detail( absint( $post['detection_id'] ) );
    949973
     
    961985     */
    962986    public function ajax_block_ip() {
    963         $post = $this->get_verified_post_fields( array( 'ip', 'duration_hours', 'permanent' ) );
     987        $post = $this->get_verified_post_fields( array( 'ip', 'duration_hours', 'permanent', 'reason' ) );
    964988
    965989        if ( empty( $post['ip'] ) ) {
     
    970994        $duration_hours = isset( $post['duration_hours'] ) ? absint( $post['duration_hours'] ) : 24;
    971995        $permanent      = isset( $post['permanent'] ) && 'true' === $post['permanent'];
    972 
    973         $response = $this->api->block_ip( $ip, $duration_hours, $permanent );
     996        $reason         = isset( $post['reason'] ) ? sanitize_text_field( $post['reason'] ) : '';
     997
     998        // Dev mode: return simulated success.
     999        if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     1000            wp_send_json_success( array(
     1001                'message' => sprintf(
     1002                    /* translators: %s: IP address */
     1003                    __( '[Dev Mode] IP address %s has been blocked.', 'atomic-edge-security' ),
     1004                    esc_html( $ip )
     1005                ),
     1006                'data'    => AtomicEdge_Dev_Mode::simulate_block_ip( $ip ),
     1007            ) );
     1008        }
     1009
     1010        $response = $this->api->block_ip( $ip, $duration_hours, $permanent, $reason );
    9741011
    9751012        if ( $response['success'] ) {
     
    9991036        }
    10001037
     1038        // Dev mode: return simulated success.
     1039        if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     1040            wp_send_json_success( array(
     1041                'message' => sprintf(
     1042                    /* translators: %s: IP address */
     1043                    __( '[Dev Mode] IP address %s has been unblocked.', 'atomic-edge-security' ),
     1044                    esc_html( $post['ip'] )
     1045                ),
     1046            ) );
     1047        }
     1048
    10011049        $response = $this->api->unblock_ip( $post['ip'] );
    10021050
     
    10261074        }
    10271075
     1076        // Dev mode: return simulated success.
     1077        if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     1078            wp_send_json_success( array(
     1079                'message' => __( '[Dev Mode] Actor profile has been deleted.', 'atomic-edge-security' ),
     1080            ) );
     1081        }
     1082
    10281083        $response = $this->api->delete_actor_profile( absint( $post['actor_id'] ) );
    10291084
     
    10471102        if ( empty( $post['detection_id'] ) ) {
    10481103            wp_send_json_error( array( 'message' => __( 'Detection ID is required.', 'atomic-edge-security' ) ) );
     1104        }
     1105
     1106        // Dev mode: return simulated success.
     1107        if ( AtomicEdge_Dev_Mode::is_enabled() ) {
     1108            wp_send_json_success( array(
     1109                'message' => __( '[Dev Mode] Threat detection has been dismissed.', 'atomic-edge-security' ),
     1110            ) );
    10491111        }
    10501112
  • atomic-edge-security/trunk/includes/class-atomicedge-api.php

    r3460160 r3473194  
    219219
    220220    /**
     221     * Get the current WAF log cache generation.
     222     *
     223     * The generation counter is included in the WAF log cache key so that
     224     * all cached WAF log queries are invalidated when the IP blacklist or
     225     * whitelist changes (since is_blocked status depends on the blacklist).
     226     *
     227     * @return int Current generation counter.
     228     */
     229    private function get_waf_cache_generation() {
     230        return (int) get_option( 'atomicedge_waf_cache_gen', 0 );
     231    }
     232
     233    /**
     234     * Invalidate all WAF log caches by incrementing the generation counter.
     235     *
     236     * Old transient keys become orphaned and will expire naturally.
     237     *
     238     * @return void
     239     */
     240    private function invalidate_waf_log_cache() {
     241        update_option( 'atomicedge_waf_cache_gen', time(), false );
     242    }
     243
     244    /**
    221245     * Get WAF logs.
    222246     *
     
    236260        }
    237261
    238         $cache_key = 'atomicedge_waf_logs_' . hash( 'sha256', (string) wp_json_encode( $args ) );
     262        $gen       = $this->get_waf_cache_generation();
     263        $cache_key = 'atomicedge_waf_logs_' . $gen . '_' . hash( 'sha256', (string) wp_json_encode( $args ) );
    239264        $cached    = get_transient( $cache_key );
    240265
     
    293318        if ( $response['success'] ) {
    294319            delete_transient( 'atomicedge_ip_rules' );
     320            $this->invalidate_waf_log_cache();
    295321            do_action( 'atomicedge_ip_added', $ip, 'whitelist' );
    296322        }
     
    316342        if ( $response['success'] ) {
    317343            delete_transient( 'atomicedge_ip_rules' );
     344            $this->invalidate_waf_log_cache();
    318345            do_action( 'atomicedge_ip_added', $ip, 'blacklist' );
    319346        }
     
    330357     */
    331358    public function remove_ip( $ip, $type ) {
    332         $endpoint = '/access/ip/' . sanitize_key( $type ) . '/' . rawurlencode( $ip );
    333         $response = $this->request( 'DELETE', $endpoint );
     359        $response = $this->request(
     360            'DELETE',
     361            '/ip-rules',
     362            array(
     363                'ip'   => $ip,
     364                'type' => $type,
     365            )
     366        );
    334367
    335368        if ( $response['success'] ) {
    336369            delete_transient( 'atomicedge_ip_rules' );
     370            $this->invalidate_waf_log_cache();
    337371            do_action( 'atomicedge_ip_removed', $ip, $type );
    338372        }
     
    579613     * @return array Result.
    580614     */
    581     public function block_ip( $ip, $duration_hours = 24, $permanent = false ) {
     615    public function block_ip( $ip, $duration_hours = 24, $permanent = false, $reason = '' ) {
    582616        $data = array(
    583617            'ip'             => $ip,
     
    585619            'permanent'      => $permanent,
    586620        );
     621
     622        if ( ! empty( $reason ) ) {
     623            $data['reason'] = $reason;
     624        }
    587625
    588626        $response = $this->request( 'POST', '/adaptive-defense/block', $data );
  • atomic-edge-security/trunk/includes/class-atomicedge-dev-mode.php

    r3449543 r3473194  
    320320        );
    321321    }
     322
     323    // =========================================================================
     324    // Adaptive Defense Simulated Data
     325    // =========================================================================
     326
     327    /**
     328     * Get simulated Adaptive Defense overview data.
     329     *
     330     * @return array
     331     */
     332    public static function get_simulated_adaptive_defense() {
     333        return array(
     334            'settings'         => array(
     335                'enabled'     => true,
     336                'mode'        => 'auto_enforce',
     337                'sensitivity' => 'balanced',
     338            ),
     339            'threat_level'     => 'medium',
     340            'stats'            => array(
     341                'total_actors'       => 25,
     342                'blocked_ips'        => 3,
     343                'pending_detections' => 5,
     344                'high_threat_count'  => 2,
     345                'ai_budget_used'     => 35,
     346                'ai_budget_total'    => 200,
     347            ),
     348            'high_risk_actors' => self::get_simulated_high_risk_actors(),
     349        );
     350    }
     351
     352    /**
     353     * Get simulated high-risk actors for the status tab.
     354     *
     355     * @return array
     356     */
     357    private static function get_simulated_high_risk_actors() {
     358        return array(
     359            array(
     360                'id'              => 1001,
     361                'ip'              => '45.33.32.156',
     362                'ip_address'      => '45.33.32.156',
     363                'country_code'    => 'US',
     364                'total_requests'  => 1523,
     365                'total_waf_hits'  => 89,
     366                'total_waf_events' => 89,
     367                'threat_score'    => 92,
     368                'is_blocked'      => true,
     369                'first_seen'      => gmdate( 'c', time() - 86400 * 3 ),
     370                'first_seen_at'   => gmdate( 'c', time() - 86400 * 3 ),
     371                'last_seen'       => gmdate( 'c', time() - 3600 ),
     372                'last_seen_at'    => gmdate( 'c', time() - 3600 ),
     373            ),
     374            array(
     375                'id'              => 1002,
     376                'ip'              => '103.235.46.39',
     377                'ip_address'      => '103.235.46.39',
     378                'country_code'    => 'CN',
     379                'total_requests'  => 856,
     380                'total_waf_hits'  => 45,
     381                'total_waf_events' => 45,
     382                'threat_score'    => 78,
     383                'is_blocked'      => false,
     384                'first_seen'      => gmdate( 'c', time() - 86400 * 5 ),
     385                'first_seen_at'   => gmdate( 'c', time() - 86400 * 5 ),
     386                'last_seen'       => gmdate( 'c', time() - 7200 ),
     387                'last_seen_at'    => gmdate( 'c', time() - 7200 ),
     388            ),
     389        );
     390    }
     391
     392    /**
     393     * Get simulated actor profiles (paginated).
     394     *
     395     * @param array $args Query arguments (page, per_page, filter, search).
     396     * @return array
     397     */
     398    public static function get_simulated_actor_profiles( $args = array() ) {
     399        $actors = array(
     400            array(
     401                'id'               => 1001,
     402                'ip'               => '45.33.32.156',
     403                'ip_address'       => '45.33.32.156',
     404                'country_code'     => 'US',
     405                'total_requests'   => 1523,
     406                'total_waf_hits'   => 89,
     407                'total_waf_events' => 89,
     408                'total_4xx_errors' => 34,
     409                'total_5xx_errors' => 2,
     410                'error_4xx'        => 34,
     411                'error_5xx'        => 2,
     412                'threat_score'     => 92,
     413                'is_blocked'       => true,
     414                'blocked_at'       => gmdate( 'c', time() - 7200 ),
     415                'block_expires_at' => gmdate( 'c', time() + 86400 ),
     416                'first_seen'       => gmdate( 'c', time() - 86400 * 3 ),
     417                'first_seen_at'    => gmdate( 'c', time() - 86400 * 3 ),
     418                'last_seen'        => gmdate( 'c', time() - 3600 ),
     419                'last_seen_at'     => gmdate( 'c', time() - 3600 ),
     420                'updated_at'       => gmdate( 'c', time() - 3600 ),
     421                'user_agents'      => array( 'python-requests/2.28.1', 'curl/7.88.1' ),
     422            ),
     423            array(
     424                'id'               => 1002,
     425                'ip'               => '103.235.46.39',
     426                'ip_address'       => '103.235.46.39',
     427                'country_code'     => 'CN',
     428                'total_requests'   => 856,
     429                'total_waf_hits'   => 45,
     430                'total_waf_events' => 45,
     431                'total_4xx_errors' => 12,
     432                'total_5xx_errors' => 0,
     433                'error_4xx'        => 12,
     434                'error_5xx'        => 0,
     435                'threat_score'     => 78,
     436                'is_blocked'       => false,
     437                'first_seen'       => gmdate( 'c', time() - 86400 * 5 ),
     438                'first_seen_at'    => gmdate( 'c', time() - 86400 * 5 ),
     439                'last_seen'        => gmdate( 'c', time() - 7200 ),
     440                'last_seen_at'     => gmdate( 'c', time() - 7200 ),
     441                'updated_at'       => gmdate( 'c', time() - 7200 ),
     442                'user_agents'      => array( 'Mozilla/5.0 (compatible; Googlebot/2.1)' ),
     443            ),
     444            array(
     445                'id'               => 1003,
     446                'ip'               => '198.51.100.42',
     447                'ip_address'       => '198.51.100.42',
     448                'country_code'     => 'DE',
     449                'total_requests'   => 324,
     450                'total_waf_hits'   => 8,
     451                'total_waf_events' => 8,
     452                'total_4xx_errors' => 3,
     453                'total_5xx_errors' => 0,
     454                'error_4xx'        => 3,
     455                'error_5xx'        => 0,
     456                'threat_score'     => 35,
     457                'is_blocked'       => false,
     458                'first_seen'       => gmdate( 'c', time() - 86400 * 7 ),
     459                'first_seen_at'    => gmdate( 'c', time() - 86400 * 7 ),
     460                'last_seen'        => gmdate( 'c', time() - 14400 ),
     461                'last_seen_at'     => gmdate( 'c', time() - 14400 ),
     462                'updated_at'       => gmdate( 'c', time() - 14400 ),
     463                'user_agents'      => array( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' ),
     464            ),
     465            array(
     466                'id'               => 1004,
     467                'ip'               => '203.0.113.88',
     468                'ip_address'       => '203.0.113.88',
     469                'country_code'     => 'RU',
     470                'total_requests'   => 2100,
     471                'total_waf_hits'   => 156,
     472                'total_waf_events' => 156,
     473                'total_4xx_errors' => 78,
     474                'total_5xx_errors' => 5,
     475                'error_4xx'        => 78,
     476                'error_5xx'        => 5,
     477                'threat_score'     => 95,
     478                'is_blocked'       => true,
     479                'blocked_at'       => gmdate( 'c', time() - 3600 ),
     480                'block_expires_at' => null,
     481                'first_seen'       => gmdate( 'c', time() - 86400 * 2 ),
     482                'first_seen_at'    => gmdate( 'c', time() - 86400 * 2 ),
     483                'last_seen'        => gmdate( 'c', time() - 1800 ),
     484                'last_seen_at'     => gmdate( 'c', time() - 1800 ),
     485                'updated_at'       => gmdate( 'c', time() - 1800 ),
     486                'user_agents'      => array( 'sqlmap/1.7', 'nikto/2.1.6' ),
     487            ),
     488            array(
     489                'id'               => 1005,
     490                'ip'               => '192.0.2.200',
     491                'ip_address'       => '192.0.2.200',
     492                'country_code'     => 'BR',
     493                'total_requests'   => 98,
     494                'total_waf_hits'   => 3,
     495                'total_waf_events' => 3,
     496                'total_4xx_errors' => 1,
     497                'total_5xx_errors' => 0,
     498                'error_4xx'        => 1,
     499                'error_5xx'        => 0,
     500                'threat_score'     => 15,
     501                'is_blocked'       => false,
     502                'first_seen'       => gmdate( 'c', time() - 86400 * 10 ),
     503                'first_seen_at'    => gmdate( 'c', time() - 86400 * 10 ),
     504                'last_seen'        => gmdate( 'c', time() - 43200 ),
     505                'last_seen_at'     => gmdate( 'c', time() - 43200 ),
     506                'updated_at'       => gmdate( 'c', time() - 43200 ),
     507                'user_agents'      => array( 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' ),
     508            ),
     509        );
     510
     511        // Apply filter.
     512        $filter = isset( $args['filter'] ) ? $args['filter'] : 'all';
     513        if ( 'blocked' === $filter ) {
     514            $actors = array_values( array_filter( $actors, function ( $a ) {
     515                return ! empty( $a['is_blocked'] );
     516            } ) );
     517        } elseif ( 'high_risk' === $filter ) {
     518            $actors = array_values( array_filter( $actors, function ( $a ) {
     519                return ( $a['threat_score'] ?? 0 ) >= 70;
     520            } ) );
     521        }
     522
     523        // Apply search.
     524        if ( ! empty( $args['search'] ) ) {
     525            $search = $args['search'];
     526            $actors = array_values( array_filter( $actors, function ( $a ) use ( $search ) {
     527                return stripos( $a['ip'], $search ) !== false;
     528            } ) );
     529        }
     530
     531        $total = count( $actors );
     532        $page  = isset( $args['page'] ) ? max( 1, (int) $args['page'] ) : 1;
     533        $per   = isset( $args['per_page'] ) ? max( 1, (int) $args['per_page'] ) : 25;
     534        $slice = array_slice( $actors, ( $page - 1 ) * $per, $per );
     535
     536        return array(
     537            'actors'     => $slice,
     538            'pagination' => array(
     539                'current_page' => $page,
     540                'per_page'     => $per,
     541                'total'        => $total,
     542                'total_pages'  => max( 1, (int) ceil( $total / $per ) ),
     543            ),
     544        );
     545    }
     546
     547    /**
     548     * Get simulated threat detections (paginated).
     549     *
     550     * @param array $args Query arguments (page, per_page, status).
     551     * @return array
     552     */
     553    public static function get_simulated_threat_detections( $args = array() ) {
     554        $detections = array(
     555            array(
     556                'id'              => 2001,
     557                'score'           => 92,
     558                'confidence'      => 87,
     559                'threat_level'    => 'critical',
     560                'status'          => 'auto_blocked',
     561                'ip_address'      => '45.33.32.156',
     562                'created_at'      => gmdate( 'c', time() - 7200 ),
     563                'detected_at'     => gmdate( 'c', time() - 7200 ),
     564                'reasons'         => array( 'SQL injection patterns', 'High WAF hit rate', 'Known malicious user agent' ),
     565                'key_indicators'  => array( 'SQL injection patterns', 'High WAF hit rate', 'Known malicious user agent' ),
     566                'reasons_summary' => array( 'SQL injection patterns', 'High WAF hit rate' ),
     567                'actor'           => array(
     568                    'id'               => 1001,
     569                    'ip'               => '45.33.32.156',
     570                    'ip_address'       => '45.33.32.156',
     571                    'country_code'     => 'US',
     572                    'total_requests'   => 1523,
     573                    'total_waf_hits'   => 89,
     574                    'waf_hits'         => 89,
     575                    'total_4xx_errors' => 34,
     576                    'total_5xx_errors' => 2,
     577                    'error_4xx'        => 34,
     578                    'error_5xx'        => 2,
     579                    'first_seen'       => gmdate( 'c', time() - 86400 * 3 ),
     580                    'first_seen_at'    => gmdate( 'c', time() - 86400 * 3 ),
     581                    'last_seen'        => gmdate( 'c', time() - 3600 ),
     582                    'last_seen_at'     => gmdate( 'c', time() - 3600 ),
     583                    'updated_at'       => gmdate( 'c', time() - 3600 ),
     584                ),
     585            ),
     586            array(
     587                'id'              => 2002,
     588                'score'           => 78,
     589                'confidence'      => 72,
     590                'threat_level'    => 'high',
     591                'status'          => 'pending_review',
     592                'ip_address'      => '103.235.46.39',
     593                'created_at'      => gmdate( 'c', time() - 14400 ),
     594                'detected_at'     => gmdate( 'c', time() - 14400 ),
     595                'reasons'         => array( 'Directory traversal attempts', 'Suspicious user agent rotation' ),
     596                'key_indicators'  => array( 'Directory traversal attempts', 'Suspicious user agent rotation' ),
     597                'reasons_summary' => array( 'Directory traversal attempts' ),
     598                'actor'           => array(
     599                    'id'               => 1002,
     600                    'ip'               => '103.235.46.39',
     601                    'ip_address'       => '103.235.46.39',
     602                    'country_code'     => 'CN',
     603                    'total_requests'   => 856,
     604                    'total_waf_hits'   => 45,
     605                    'waf_hits'         => 45,
     606                    'total_4xx_errors' => 12,
     607                    'total_5xx_errors' => 0,
     608                    'error_4xx'        => 12,
     609                    'error_5xx'        => 0,
     610                    'first_seen'       => gmdate( 'c', time() - 86400 * 5 ),
     611                    'first_seen_at'    => gmdate( 'c', time() - 86400 * 5 ),
     612                    'last_seen'        => gmdate( 'c', time() - 7200 ),
     613                    'last_seen_at'     => gmdate( 'c', time() - 7200 ),
     614                    'updated_at'       => gmdate( 'c', time() - 7200 ),
     615                ),
     616            ),
     617            array(
     618                'id'              => 2003,
     619                'score'           => 95,
     620                'confidence'      => 91,
     621                'threat_level'    => 'critical',
     622                'status'          => 'user_blocked',
     623                'ip_address'      => '203.0.113.88',
     624                'created_at'      => gmdate( 'c', time() - 3600 ),
     625                'detected_at'     => gmdate( 'c', time() - 3600 ),
     626                'reasons'         => array( 'Automated SQL injection tool', 'Nikto scan detected', 'Extremely high WAF events' ),
     627                'key_indicators'  => array( 'Automated SQL injection tool', 'Nikto scan detected', 'Extremely high WAF events' ),
     628                'reasons_summary' => array( 'Automated SQL injection tool', 'Nikto scan detected' ),
     629                'ai_analysis'     => 'This IP exhibits classic automated attack tool behavior. The sqlmap user agent combined with high-frequency WAF triggers indicates an active SQL injection campaign. Immediate blocking recommended.',
     630                'actor'           => array(
     631                    'id'               => 1004,
     632                    'ip'               => '203.0.113.88',
     633                    'ip_address'       => '203.0.113.88',
     634                    'country_code'     => 'RU',
     635                    'total_requests'   => 2100,
     636                    'total_waf_hits'   => 156,
     637                    'waf_hits'         => 156,
     638                    'total_4xx_errors' => 78,
     639                    'total_5xx_errors' => 5,
     640                    'error_4xx'        => 78,
     641                    'error_5xx'        => 5,
     642                    'first_seen'       => gmdate( 'c', time() - 86400 * 2 ),
     643                    'first_seen_at'    => gmdate( 'c', time() - 86400 * 2 ),
     644                    'last_seen'        => gmdate( 'c', time() - 1800 ),
     645                    'last_seen_at'     => gmdate( 'c', time() - 1800 ),
     646                    'updated_at'       => gmdate( 'c', time() - 1800 ),
     647                ),
     648            ),
     649            array(
     650                'id'              => 2004,
     651                'score'           => 55,
     652                'confidence'      => 60,
     653                'threat_level'    => 'medium',
     654                'status'          => 'pending_review',
     655                'ip_address'      => '198.51.100.42',
     656                'created_at'      => gmdate( 'c', time() - 28800 ),
     657                'detected_at'     => gmdate( 'c', time() - 28800 ),
     658                'reasons'         => array( 'Elevated error rate', 'Minor WAF triggers' ),
     659                'key_indicators'  => array( 'Elevated error rate', 'Minor WAF triggers' ),
     660                'reasons_summary' => array( 'Elevated error rate' ),
     661                'actor'           => array(
     662                    'id'               => 1003,
     663                    'ip'               => '198.51.100.42',
     664                    'ip_address'       => '198.51.100.42',
     665                    'country_code'     => 'DE',
     666                    'total_requests'   => 324,
     667                    'total_waf_hits'   => 8,
     668                    'waf_hits'         => 8,
     669                    'total_4xx_errors' => 3,
     670                    'total_5xx_errors' => 0,
     671                    'error_4xx'        => 3,
     672                    'error_5xx'        => 0,
     673                    'first_seen'       => gmdate( 'c', time() - 86400 * 7 ),
     674                    'first_seen_at'    => gmdate( 'c', time() - 86400 * 7 ),
     675                    'last_seen'        => gmdate( 'c', time() - 14400 ),
     676                    'last_seen_at'     => gmdate( 'c', time() - 14400 ),
     677                    'updated_at'       => gmdate( 'c', time() - 14400 ),
     678                ),
     679            ),
     680            array(
     681                'id'              => 2005,
     682                'score'           => 40,
     683                'confidence'      => 45,
     684                'threat_level'    => 'low',
     685                'status'          => 'dismissed',
     686                'ip_address'      => '192.0.2.200',
     687                'created_at'      => gmdate( 'c', time() - 86400 ),
     688                'detected_at'     => gmdate( 'c', time() - 86400 ),
     689                'reasons'         => array( 'Unusual request patterns' ),
     690                'key_indicators'  => array( 'Unusual request patterns' ),
     691                'reasons_summary' => array( 'Unusual request patterns' ),
     692                'actor'           => array(
     693                    'id'               => 1005,
     694                    'ip'               => '192.0.2.200',
     695                    'ip_address'       => '192.0.2.200',
     696                    'country_code'     => 'BR',
     697                    'total_requests'   => 98,
     698                    'total_waf_hits'   => 3,
     699                    'waf_hits'         => 3,
     700                    'total_4xx_errors' => 1,
     701                    'total_5xx_errors' => 0,
     702                    'error_4xx'        => 1,
     703                    'error_5xx'        => 0,
     704                    'first_seen'       => gmdate( 'c', time() - 86400 * 10 ),
     705                    'first_seen_at'    => gmdate( 'c', time() - 86400 * 10 ),
     706                    'last_seen'        => gmdate( 'c', time() - 43200 ),
     707                    'last_seen_at'     => gmdate( 'c', time() - 43200 ),
     708                    'updated_at'       => gmdate( 'c', time() - 43200 ),
     709                ),
     710            ),
     711        );
     712
     713        // Apply status filter.
     714        $status = isset( $args['status'] ) ? $args['status'] : 'all';
     715        if ( 'all' !== $status && ! empty( $status ) ) {
     716            $detections = array_values( array_filter( $detections, function ( $d ) use ( $status ) {
     717                return $d['status'] === $status;
     718            } ) );
     719        }
     720
     721        $total = count( $detections );
     722        $page  = isset( $args['page'] ) ? max( 1, (int) $args['page'] ) : 1;
     723        $per   = isset( $args['per_page'] ) ? max( 1, (int) $args['per_page'] ) : 25;
     724        $slice = array_slice( $detections, ( $page - 1 ) * $per, $per );
     725
     726        return array(
     727            'detections' => $slice,
     728            'pagination' => array(
     729                'current_page' => $page,
     730                'per_page'     => $per,
     731                'total'        => $total,
     732                'total_pages'  => max( 1, (int) ceil( $total / $per ) ),
     733            ),
     734        );
     735    }
     736
     737    /**
     738     * Get simulated threat detection detail.
     739     *
     740     * @param int $detection_id The detection ID.
     741     * @return array|null Detection detail or null if not found.
     742     */
     743    public static function get_simulated_threat_detection_detail( $detection_id ) {
     744        $all = self::get_simulated_threat_detections();
     745        foreach ( $all['detections'] as $detection ) {
     746            if ( (int) $detection['id'] === (int) $detection_id ) {
     747                return array(
     748                    'detection' => $detection,
     749                    'actor'     => $detection['actor'],
     750                );
     751            }
     752        }
     753
     754        // Return the first detection as fallback for any unknown ID.
     755        if ( ! empty( $all['detections'] ) ) {
     756            $first = $all['detections'][0];
     757            return array(
     758                'detection' => $first,
     759                'actor'     => $first['actor'],
     760            );
     761        }
     762
     763        return null;
     764    }
     765
     766    /**
     767     * Simulate a successful block IP response.
     768     *
     769     * @param string $ip IP address.
     770     * @return array
     771     */
     772    public static function simulate_block_ip( $ip ) {
     773        return array(
     774            'message'          => sprintf(
     775                /* translators: %s: IP address */
     776                __( '[Dev Mode] IP %s has been blocked.', 'atomic-edge-security' ),
     777                $ip
     778            ),
     779            'ip'               => $ip,
     780            'is_blocked'       => true,
     781            'blocked_at'       => gmdate( 'c' ),
     782            'block_expires_at' => gmdate( 'c', time() + 86400 ),
     783        );
     784    }
     785
     786    /**
     787     * Simulate a successful unblock IP response.
     788     *
     789     * @param string $ip IP address.
     790     * @return array
     791     */
     792    public static function simulate_unblock_ip( $ip ) {
     793        return array(
     794            'message' => sprintf(
     795                /* translators: %s: IP address */
     796                __( '[Dev Mode] IP %s has been unblocked.', 'atomic-edge-security' ),
     797                $ip
     798            ),
     799            'ip'      => $ip,
     800        );
     801    }
     802
     803    /**
     804     * Simulate a successful dismiss detection response.
     805     *
     806     * @param int $detection_id Detection ID.
     807     * @return array
     808     */
     809    public static function simulate_dismiss_detection( $detection_id ) {
     810        return array(
     811            'message' => sprintf(
     812                /* translators: %d: Detection ID */
     813                __( '[Dev Mode] Detection #%d has been dismissed.', 'atomic-edge-security' ),
     814                $detection_id
     815            ),
     816            'id'      => $detection_id,
     817            'status'  => 'dismissed',
     818        );
     819    }
     820
     821    /**
     822     * Simulate a successful delete actor response.
     823     *
     824     * @param int $actor_id Actor profile ID.
     825     * @return array
     826     */
     827    public static function simulate_delete_actor( $actor_id ) {
     828        return array(
     829            'message' => sprintf(
     830                /* translators: %d: Actor profile ID */
     831                __( '[Dev Mode] Actor profile #%d has been deleted.', 'atomic-edge-security' ),
     832                $actor_id
     833            ),
     834            'id'      => $actor_id,
     835        );
     836    }
    322837}
  • atomic-edge-security/trunk/includes/class-atomicedge.php

    r3460905 r3473194  
    189189            'atomicedgeAdmin',
    190190            array(
    191                 'ajaxUrl'   => admin_url( 'admin-ajax.php' ),
    192                 'nonce'     => wp_create_nonce( 'atomicedge_ajax' ),
    193                 'connected' => $this->api->is_connected(),
    194                 'strings'   => array(
     191                'ajaxUrl'          => admin_url( 'admin-ajax.php' ),
     192                'nonce'            => wp_create_nonce( 'atomicedge_ajax' ),
     193                'connected'        => $this->api->is_connected(),
     194                'accessControlUrl' => admin_url( 'admin.php?page=atomicedge-access-control&tab=blacklist' ),
     195                'strings'          => array(
    195196                    'loading'      => esc_html__( 'Loading...', 'atomic-edge-security' ),
    196197                    'error'        => esc_html__( 'An error occurred. Please try again.', 'atomic-edge-security' ),
     
    220221                'atomicedge_admin',
    221222                array(
    222                     'ajax_url' => admin_url( 'admin-ajax.php' ),
    223                     'nonce'    => wp_create_nonce( 'atomicedge_ajax' ),
     223                    'ajax_url'           => admin_url( 'admin-ajax.php' ),
     224                    'nonce'              => wp_create_nonce( 'atomicedge_ajax' ),
     225                    'access_control_url' => admin_url( 'admin.php?page=atomicedge-access-control&tab=blacklist' ),
    224226                )
    225227            );
  • atomic-edge-security/trunk/readme.txt

    r3470783 r3473194  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 2.4.5
     7Stable tag: 2.4.6
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    112112
    113113== Changelog ==
     114
     115= 2.4.6 =
     116* FIX: Adaptive Defense dev mode now provides simulated data for all 8 AJAX endpoints (overview, actor profiles, threat detections, detection detail, block/unblock IP, dismiss detection, delete actor)
     117* FIX: Fixed duplicate detail rows appending on repeated "View Details" clicks in Threat Detections tab by replacing invalid <div>-wrapped <tr> template with HTML5 <template> element
     118* FIX: Added JS field name fallback chains for API response compatibility across versions
     119* NEW: Added 36 new tests for Adaptive Defense dev mode simulation and AJAX interception
    114120
    115121= 2.4.5 =
Note: See TracChangeset for help on using the changeset viewer.