Changeset 3473194
- Timestamp:
- 03/03/2026 02:17:26 AM (5 days ago)
- Location:
- atomic-edge-security/trunk
- Files:
-
- 1 added
- 12 edited
-
admin/css/admin.css (modified) (1 diff)
-
admin/js/adaptive-defense.js (modified) (16 diffs)
-
admin/js/admin.js (modified) (14 diffs)
-
admin/views/access-control.php (modified) (2 diffs)
-
admin/views/adaptive-defense.php (modified) (3 diffs)
-
admin/views/partials/adaptive-defense-detections-tab.php (modified) (2 diffs)
-
atomicedge.php (modified) (2 diffs)
-
includes/class-atomicedge-ajax.php (modified) (9 diffs)
-
includes/class-atomicedge-api.php (modified) (7 diffs)
-
includes/class-atomicedge-dev-mode.php (modified) (1 diff)
-
includes/class-atomicedge.php (modified) (2 diffs)
-
languages/atomic-edge-security-en_CA.po (added)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
atomic-edge-security/trunk/admin/css/admin.css
r3454914 r3473194 1593 1593 } 1594 1594 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 15 15 /** Current page for each section */ 16 16 pages: { 17 blocked: 1,18 17 actors: 1, 19 18 detections: 1 … … 42 41 if ($('#atomicedge-ad-status-card').length) { 43 42 this.loadStatusTab(); 44 } else if ($('#atomicedge-ad-blocked-card').length) {45 this.loadBlockedIps();46 43 } else if ($('#atomicedge-ad-actors-card').length) { 47 44 this.loadActorProfiles(); … … 65 62 }); 66 63 67 // Blocked IPs tab68 $('#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 81 64 // Actor Profiles tab 82 65 $('#atomicedge-ad-actors-refresh').on('click', function() { … … 99 82 }); 100 83 $(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); 103 87 }); 104 88 $(document).on('click', '.atomicedge-ad-delete-actor-btn', function() { … … 122 106 }); 123 107 $(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); 126 111 }); 127 112 $(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); 130 116 }); 131 117 $(document).on('click', '.atomicedge-ad-detail-close', function() { … … 140 126 141 127 switch (section) { 142 case 'blocked':143 self.loadBlockedIps();144 break;145 128 case 'actors': 146 129 self.loadActorProfiles(); … … 171 154 case 'status': 172 155 this.loadStatusTab(); 173 break;174 case 'blocked':175 this.loadBlockedIps();176 156 break; 177 157 case 'actors': … … 302 282 303 283 /** 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 } 313 329 314 330 $.ajax({ … … 316 332 type: 'POST', 317 333 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>—</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', 408 335 nonce: atomicedge_admin.nonce, 409 336 ip: ip, 410 duration_hours: durationHours, 411 permanent: permanent ? 'true' : 'false' 337 description: description 412 338 }, 413 339 success: function(response) { 414 $btn.prop('disabled', false).html('<span class="dashicons dashicons-lock" style="margin-top: 3px;"></span> Block IP');415 416 340 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'); 474 345 } 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. 479 353 if (source === 'actor') { 480 354 self.loadActorProfiles(); … … 483 357 } 484 358 } 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'); 486 365 } 487 366 }, 488 367 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); 491 370 } 492 self.showNotice('Network error: ' + error, 'error');493 }494 });495 },496 497 /**498 * Unblock an IP address499 *500 * @param {string} ip IP address to unblock501 */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: ip516 },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) {526 371 self.showNotice('Network error: ' + error, 'error'); 527 372 } … … 737 582 html += '<button type="button" class="button button-small atomicedge-ad-view-detection-btn" data-id="' + detection.id + '" title="View details">'; 738 583 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) { 740 586 html += '<button type="button" class="button button-small atomicedge-ad-block-detection-btn" data-ip="' + self.escapeHtml(ipAddress) + '" title="Block IP">'; 741 587 html += '<span class="dashicons dashicons-shield" style="margin-top: 3px;"></span></button> '; … … 770 616 $('.atomicedge-ad-detail-row').remove(); 771 617 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(); 774 625 $row.after($template); 775 626 … … 820 671 $detailRow.find('.atomicedge-ad-detail-confidence').text((detection.confidence || 0) + '%'); 821 672 $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)); 823 674 824 675 // 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'); 826 677 $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)); 831 682 832 683 // Reasons … … 861 712 * Dismiss a threat detection 862 713 * 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) { 866 718 var self = this; 867 719 868 720 if (!confirm('Are you sure you want to dismiss this detection?')) { 869 721 return; 722 } 723 724 // Disable button immediately. 725 if ($button) { 726 $button.prop('disabled', true); 870 727 } 871 728 … … 880 737 success: function(response) { 881 738 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 } 882 749 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); 884 754 } else { 755 if ($button) { 756 $button.prop('disabled', false); 757 } 885 758 self.showNotice(response.data ? response.data.message : 'Failed to dismiss detection', 'error'); 886 759 } 887 760 }, 888 761 error: function(xhr, status, error) { 762 if ($button) { 763 $button.prop('disabled', false); 764 } 889 765 self.showNotice('Network error: ' + error, 'error'); 890 766 } … … 976 852 var labels = { 977 853 'pending': 'Pending', 854 'pending_review': 'Pending Review', 855 'auto_blocked': 'Blocked', 856 'user_blocked': 'Blocked', 978 857 'blocked': 'Blocked', 979 'dismissed': 'Dismissed' 858 'dismissed': 'Dismissed', 859 'expired': 'Expired' 980 860 }; 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 + '">' + 982 869 (labels[status] || status) + '</span>'; 983 870 }, -
atomic-edge-security/trunk/admin/js/admin.js
r3452356 r3473194 67 67 $('#' + tab).addClass('atomicedge-tab-active'); 68 68 }); 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 } 69 82 }, 70 83 … … 380 393 var self = this; 381 394 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 } 382 402 var row = '<tr>' + 383 403 '<td>' + self.escapeHtml(log.event_timestamp || '') + '</td>' + … … 386 406 '<td><code>' + self.escapeHtml(log.waf_rule_id || '') + '</code></td>' + 387 407 '<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>' + 389 409 '</tr>'; 390 410 $tbody.append(row); … … 393 413 // Bind block IP buttons 394 414 $tbody.find('.atomicedge-block-ip').on('click', function() { 395 var ip = $(this).data('ip'); 415 var $btn = $(this); 416 var ip = $btn.data('ip'); 396 417 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); 398 419 } 399 420 }); … … 425 446 426 447 if (!self.validateIp(ip)) { 427 alert(atomicedgeAdmin.strings.invalidIp);448 self.showNotice(atomicedgeAdmin.strings.invalidIp, 'error'); 428 449 return; 429 450 } … … 439 460 440 461 if (!self.validateIp(ip)) { 441 alert(atomicedgeAdmin.strings.invalidIp);462 self.showNotice(atomicedgeAdmin.strings.invalidIp, 'error'); 442 463 return; 443 464 } … … 474 495 475 496 /** 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 /** 476 515 * Render IP list 477 516 */ 478 517 renderIpList: function(type, ips) { 479 518 var $tbody = $('#atomicedge-' + type + '-body'); 519 var isBlacklist = (type === 'blacklist'); 520 var cols = isBlacklist ? 4 : 3; 480 521 $tbody.empty(); 481 522 482 523 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>'); 484 525 return; 485 526 } … … 487 528 var self = this; 488 529 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 } 489 540 var row = '<tr>' + 490 541 '<td><code>' + self.escapeHtml(item.ip) + '</code></td>' + 542 sourceCell + 491 543 '<td>' + self.escapeHtml(item.description || '') + '</td>' + 492 544 '<td><button type="button" class="button button-small atomicedge-remove-ip" data-ip="' + self.escapeHtml(item.ip) + '" data-type="' + type + '">Remove</button></td>' + … … 519 571 /** 520 572 * 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 524 585 this.ajax('atomicedge_add_ip_blacklist', { ip: ip, description: description }, function() { 586 // Clear form fields if on access control page. 525 587 $('#blacklist-ip').val(''); 526 588 $('#blacklist-description').val(''); 527 589 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'; 529 632 }, 530 633 … … 536 639 this.ajax('atomicedge_remove_ip', { ip: ip, type: type }, function() { 537 640 self.loadIpRules(); 641 self.showNotice(self.escapeHtml(ip) + ' has been removed from the ' + self.escapeHtml(type) + '.', 'success'); 538 642 }); 539 643 }, … … 1208 1312 */ 1209 1313 ajax: function(action, data, success, error) { 1314 var self = this; 1210 1315 data = data || {}; 1211 1316 data.action = action; … … 1225 1330 error(response.data); 1226 1331 } 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'); 1228 1334 } 1229 1335 } … … 1233 1339 error(); 1234 1340 } else { 1235 alert(atomicedgeAdmin.strings.error);1341 self.showNotice(atomicedgeAdmin.strings.error, 'error'); 1236 1342 } 1237 1343 } … … 1256 1362 formatNumber: function(num) { 1257 1363 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); 1258 1393 }, 1259 1394 -
atomic-edge-security/trunk/admin/views/access-control.php
r3449543 r3473194 108 108 <tr> 109 109 <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> 110 111 <th class="column-description"><?php esc_html_e( 'Description', 'atomic-edge-security' ); ?></th> 111 112 <th class="column-actions"><?php esc_html_e( 'Actions', 'atomic-edge-security' ); ?></th> … … 114 115 <tbody id="atomicedge-blacklist-body"> 115 116 <tr class="atomicedge-loading-row"> 116 <td colspan=" 3">117 <td colspan="4"> 117 118 <span class="spinner is-active"></span> 118 119 <?php esc_html_e( 'Loading...', 'atomic-edge-security' ); ?> -
atomic-edge-security/trunk/admin/views/adaptive-defense.php
r3454914 r3473194 23 23 // Tab navigation. 24 24 $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' ); 26 26 if ( ! in_array( $current_tab, $valid_tabs, true ) ) { 27 27 $current_tab = 'status'; … … 33 33 'label' => __( 'Status', 'atomic-edge-security' ), 34 34 'icon' => 'dashicons-shield-alt', 35 ),36 'blocked' => array(37 'label' => __( 'Blocked IPs', 'atomic-edge-security' ),38 'icon' => 'dashicons-dismiss',39 35 ), 40 36 'actors' => array( … … 98 94 <?php 99 95 switch ( $current_tab ) { 100 case 'blocked': 101 include ATOMICEDGE_PLUGIN_DIR . 'admin/views/partials/adaptive-defense-blocked-tab.php'; 102 break; 96 103 97 case 'actors': 104 98 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 79 79 80 80 <!-- 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"> 82 83 <tr class="atomicedge-ad-detail-row"> 83 84 <td colspan="7"> … … 170 171 </td> 171 172 </tr> 172 </ div>173 </template> 173 174 174 175 <style> -
atomic-edge-security/trunk/atomicedge.php
r3470783 r3473194 4 4 * Plugin URI: https://atomicedge.io/wordpress 5 5 * Description: Connect your WordPress site to Atomic Edge WAF/CDN for advanced security protection, analytics, and access control management. 6 * Version: 2.4. 56 * Version: 2.4.6 7 7 * Requires at least: 5.8 8 8 * Requires PHP: 7.4 … … 26 26 27 27 // Plugin constants. 28 define( 'ATOMICEDGE_VERSION', '2.4. 5' );28 define( 'ATOMICEDGE_VERSION', '2.4.6' ); 29 29 define( 'ATOMICEDGE_PLUGIN_FILE', __FILE__ ); 30 30 define( 'ATOMICEDGE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); -
atomic-edge-security/trunk/includes/class-atomicedge-ajax.php
r3454914 r3473194 875 875 $this->get_verified_post_fields( array() ); 876 876 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 877 882 $response = $this->api->get_adaptive_defense(); 878 883 … … 902 907 } 903 908 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 904 914 $response = $this->api->get_actor_profiles( $args ); 905 915 … … 925 935 ); 926 936 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 927 942 $response = $this->api->get_threat_detections( $args ); 928 943 … … 946 961 } 947 962 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 948 972 $response = $this->api->get_threat_detection_detail( absint( $post['detection_id'] ) ); 949 973 … … 961 985 */ 962 986 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' ) ); 964 988 965 989 if ( empty( $post['ip'] ) ) { … … 970 994 $duration_hours = isset( $post['duration_hours'] ) ? absint( $post['duration_hours'] ) : 24; 971 995 $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 ); 974 1011 975 1012 if ( $response['success'] ) { … … 999 1036 } 1000 1037 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 1001 1049 $response = $this->api->unblock_ip( $post['ip'] ); 1002 1050 … … 1026 1074 } 1027 1075 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 1028 1083 $response = $this->api->delete_actor_profile( absint( $post['actor_id'] ) ); 1029 1084 … … 1047 1102 if ( empty( $post['detection_id'] ) ) { 1048 1103 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 ) ); 1049 1111 } 1050 1112 -
atomic-edge-security/trunk/includes/class-atomicedge-api.php
r3460160 r3473194 219 219 220 220 /** 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 /** 221 245 * Get WAF logs. 222 246 * … … 236 260 } 237 261 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 ) ); 239 264 $cached = get_transient( $cache_key ); 240 265 … … 293 318 if ( $response['success'] ) { 294 319 delete_transient( 'atomicedge_ip_rules' ); 320 $this->invalidate_waf_log_cache(); 295 321 do_action( 'atomicedge_ip_added', $ip, 'whitelist' ); 296 322 } … … 316 342 if ( $response['success'] ) { 317 343 delete_transient( 'atomicedge_ip_rules' ); 344 $this->invalidate_waf_log_cache(); 318 345 do_action( 'atomicedge_ip_added', $ip, 'blacklist' ); 319 346 } … … 330 357 */ 331 358 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 ); 334 367 335 368 if ( $response['success'] ) { 336 369 delete_transient( 'atomicedge_ip_rules' ); 370 $this->invalidate_waf_log_cache(); 337 371 do_action( 'atomicedge_ip_removed', $ip, $type ); 338 372 } … … 579 613 * @return array Result. 580 614 */ 581 public function block_ip( $ip, $duration_hours = 24, $permanent = false ) {615 public function block_ip( $ip, $duration_hours = 24, $permanent = false, $reason = '' ) { 582 616 $data = array( 583 617 'ip' => $ip, … … 585 619 'permanent' => $permanent, 586 620 ); 621 622 if ( ! empty( $reason ) ) { 623 $data['reason'] = $reason; 624 } 587 625 588 626 $response = $this->request( 'POST', '/adaptive-defense/block', $data ); -
atomic-edge-security/trunk/includes/class-atomicedge-dev-mode.php
r3449543 r3473194 320 320 ); 321 321 } 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 } 322 837 } -
atomic-edge-security/trunk/includes/class-atomicedge.php
r3460905 r3473194 189 189 'atomicedgeAdmin', 190 190 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( 195 196 'loading' => esc_html__( 'Loading...', 'atomic-edge-security' ), 196 197 'error' => esc_html__( 'An error occurred. Please try again.', 'atomic-edge-security' ), … … 220 221 'atomicedge_admin', 221 222 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' ), 224 226 ) 225 227 ); -
atomic-edge-security/trunk/readme.txt
r3470783 r3473194 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 2.4. 57 Stable tag: 2.4.6 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 112 112 113 113 == 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 114 120 115 121 = 2.4.5 =
Note: See TracChangeset
for help on using the changeset viewer.