Changeset 3476055
- Timestamp:
- 03/06/2026 04:21:12 AM (2 days ago)
- Location:
- atomic-edge-security/trunk
- Files:
-
- 11 edited
-
admin/js/adaptive-defense.js (modified) (21 diffs)
-
admin/js/admin.js (modified) (5 diffs)
-
admin/views/adaptive-defense.php (modified) (3 diffs)
-
admin/views/partials/adaptive-defense-actors-tab.php (modified) (1 diff)
-
admin/views/partials/adaptive-defense-blocked-tab.php (modified) (1 diff)
-
admin/views/partials/adaptive-defense-detections-tab.php (modified) (1 diff)
-
atomicedge.php (modified) (2 diffs)
-
includes/class-atomicedge-ajax.php (modified) (14 diffs)
-
includes/class-atomicedge-api.php (modified) (8 diffs)
-
includes/class-atomicedge-dev-mode.php (modified) (19 diffs)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
atomic-edge-security/trunk/admin/js/adaptive-defense.js
r3473217 r3476055 15 15 /** Current page for each section */ 16 16 pages: { 17 blocked: 1, 17 18 actors: 1, 18 19 detections: 1 … … 41 42 if ($('#atomicedge-ad-status-card').length) { 42 43 this.loadStatusTab(); 44 } else if ($('#atomicedge-ad-blocked-card').length) { 45 this.loadBlockedIps(); 43 46 } else if ($('#atomicedge-ad-actors-card').length) { 44 47 this.loadActorProfiles(); … … 62 65 }); 63 66 67 // Blocked IPs tab 68 $('#atomicedge-ad-blocked-refresh').on('click', function() { 69 self.loadBlockedIps(); 70 }); 71 $('#atomicedge-ad-block-btn').on('click', function() { 72 self.blockIpFromForm(); 73 }); 74 $('#atomicedge-ad-block-ip').on('keypress', function(e) { 75 if (e.which === 13) { 76 e.preventDefault(); 77 self.blockIpFromForm(); 78 } 79 }); 80 $(document).on('click', '.atomicedge-ad-unblock-btn', function() { 81 var ip = $(this).data('ip'); 82 self.unblockIp(ip); 83 }); 84 $(document).on('click', '.atomicedge-ad-extend-block-btn', function() { 85 var ip = $(this).data('ip'); 86 self.extendBlock(ip); 87 }); 88 $(document).on('click', '.atomicedge-ad-make-permanent-btn', function() { 89 var ip = $(this).data('ip'); 90 self.makePermanent(ip); 91 }); 92 64 93 // Actor Profiles tab 65 94 $('#atomicedge-ad-actors-refresh').on('click', function() { … … 84 113 var $btn = $(this); 85 114 var ip = $btn.data('ip'); 86 self.blockIp ToBlacklist(ip, 'actor', $btn);115 self.blockIpViaAD(ip, 'actor', $btn); 87 116 }); 88 117 $(document).on('click', '.atomicedge-ad-delete-actor-btn', function() { … … 108 137 var $btn = $(this); 109 138 var ip = $btn.data('ip'); 110 self.blockIp ToBlacklist(ip, 'detection', $btn);139 self.blockIpViaAD(ip, 'detection', $btn); 111 140 }); 112 141 $(document).on('click', '.atomicedge-ad-dismiss-btn', function() { … … 126 155 127 156 switch (section) { 157 case 'blocked': 158 self.loadBlockedIps(); 159 break; 128 160 case 'actors': 129 161 self.loadActorProfiles(); … … 154 186 case 'status': 155 187 this.loadStatusTab(); 188 break; 189 case 'blocked': 190 this.loadBlockedIps(); 156 191 break; 157 192 case 'actors': … … 180 215 data: { 181 216 action: 'atomicedge_get_adaptive_defense', 182 nonce: atomicedge_admin.nonce 217 nonce: atomicedge_admin.nonce, 218 force_refresh: 'true' 183 219 }, 184 220 success: function(response) { … … 268 304 var ipAddress = actor.ip_address || actor.ip || ''; 269 305 var html = '<tr>'; 270 html += '<td>' + self. escapeHtml(ipAddress) + '</td>';306 html += '<td>' + self.formatIpWithFlag(ipAddress, actor) + '</td>'; 271 307 html += '<td>' + self.formatScore(actor.threat_score || actor.score || 0) + '</td>'; 272 308 html += '<td>' + (actor.requests || actor.total_requests || 0) + '</td>'; … … 282 318 283 319 /** 284 * Format current date/time as a human-readable timestamp. 285 * 286 * @return {string} e.g. "2026-02-27 14:35 UTC" 287 */ 288 formatTimestamp: function() { 289 var d = new Date(); 290 var pad = function(n) { return n < 10 ? '0' + n : n; }; 291 return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1) + '-' + pad(d.getUTCDate()) + 292 ' ' + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ' UTC'; 293 }, 294 295 /** 296 * Block an IP by adding it to the blacklist (unified across all sources). 297 * 298 * All block actions route through the blacklist in Access Control. 299 * Source context is captured in the description field. 320 * Block an IP via Adaptive Defense (application-layer blocking). 321 * 322 * Routes through the dashboard's Adaptive Defense system, NOT 323 * the global IP blacklist in Access Control. 300 324 * 301 325 * @param {string} ip IP address to block. 302 * @param {string} source Source context (actor, detection ).326 * @param {string} source Source context (actor, detection, blocked). 303 327 * @param {jQuery} $button Optional button element to update. 304 328 */ 305 blockIp ToBlacklist: function(ip, source, $button) {329 blockIpViaAD: function(ip, source, $button) { 306 330 var self = this; 307 331 source = source || 'adaptive_defense'; … … 312 336 } 313 337 314 if (!confirm('Are you sure you want to block ' + ip + '?')) { 315 return; 316 } 317 318 // Build source-contextual description. 319 var sourceLabels = { 320 actor: 'Actor Profiles', 321 detection: 'Threat Detections' 322 }; 323 var description = 'Blocked from ' + (sourceLabels[source] || 'Adaptive Defense') + ' on ' + self.formatTimestamp(); 324 325 // Disable the button immediately to prevent double-clicks. 338 if (!confirm('Are you sure you want to block ' + ip + ' via Adaptive Defense?')) { 339 return; 340 } 341 326 342 if ($button) { 327 343 $button.prop('disabled', true); … … 332 348 type: 'POST', 333 349 data: { 334 action: 'atomicedge_ add_ip_blacklist',350 action: 'atomicedge_block_ip', 335 351 nonce: atomicedge_admin.nonce, 336 352 ip: ip, 337 description: description 353 duration_hours: 24, 354 permanent: 'false', 355 reason: 'Blocked from ' + source + ' tab' 338 356 }, 339 357 success: function(response) { 340 358 if (response.success) { 341 // Update button to show blocked state.342 359 if ($button) { 343 360 $button.prop('disabled', true) … … 346 363 347 364 self.showNotice( 348 ip + ' has been added to the blacklist. Manage blocks in Access Control.',365 ip + ' has been blocked via Adaptive Defense.', 349 366 'success' 350 367 ); 351 368 352 // Refresh source tab to show updated state.369 // Refresh the source tab. 353 370 if (source === 'actor') { 354 371 self.loadActorProfiles(); 355 372 } else if (source === 'detection') { 356 373 self.loadThreatDetections(); 374 } else if (source === 'blocked') { 375 self.loadBlockedIps(); 357 376 } 358 377 } else { 359 // Re-enable on failure.360 378 if ($button) { 361 379 $button.prop('disabled', false); … … 375 393 376 394 /** 377 * Load Actor Profiles 378 */ 379 loadActorProfiles: function() { 380 var self = this; 381 var $loading = $('#atomicedge-ad-actors-loading'); 382 var $wrapper = $('#atomicedge-ad-actors-table-wrapper'); 383 var filter = $('#atomicedge-ad-actors-filter').val(); 384 var search = $('#atomicedge-ad-actors-search').val().trim(); 395 * Block an IP from the Blocked IPs tab form. 396 */ 397 blockIpFromForm: function() { 398 var self = this; 399 var ip = $('#atomicedge-ad-block-ip').val().trim(); 400 var duration = $('#atomicedge-ad-block-duration').val(); 401 402 if (!ip) { 403 self.showNotice('Please enter an IP address', 'error'); 404 return; 405 } 406 407 if (!confirm('Are you sure you want to block ' + ip + '?')) { 408 return; 409 } 410 411 var isPermanent = (duration === 'permanent'); 412 var durationHours = isPermanent ? 0 : parseInt(duration, 10); 413 414 var $btn = $('#atomicedge-ad-block-btn'); 415 $btn.prop('disabled', true); 416 417 $.ajax({ 418 url: atomicedge_admin.ajax_url, 419 type: 'POST', 420 data: { 421 action: 'atomicedge_block_ip', 422 nonce: atomicedge_admin.nonce, 423 ip: ip, 424 duration_hours: durationHours, 425 permanent: isPermanent ? 'true' : 'false', 426 reason: 'Manually blocked from Blocked IPs tab' 427 }, 428 success: function(response) { 429 $btn.prop('disabled', false); 430 if (response.success) { 431 $('#atomicedge-ad-block-ip').val(''); 432 self.showNotice(ip + ' has been blocked.', 'success'); 433 self.loadBlockedIps(); 434 } else { 435 var message = (response.data && response.data.message) ? response.data.message : 'Failed to block IP'; 436 self.showNotice(message, 'error'); 437 } 438 }, 439 error: function(xhr, status, error) { 440 $btn.prop('disabled', false); 441 self.showNotice('Network error: ' + error, 'error'); 442 } 443 }); 444 }, 445 446 /** 447 * Load Blocked IPs tab data. 448 * 449 * Reuses the actor profiles endpoint with filter=blocked. 450 */ 451 loadBlockedIps: function() { 452 var self = this; 453 var $loading = $('#atomicedge-ad-blocked-loading'); 454 var $wrapper = $('#atomicedge-ad-blocked-table-wrapper'); 385 455 386 456 $loading.show(); … … 393 463 action: 'atomicedge_get_actor_profiles', 394 464 nonce: atomicedge_admin.nonce, 465 force_refresh: 'true', 466 filter: 'blocked', 467 page: self.pages.blocked, 468 per_page: self.perPage 469 }, 470 success: function(response) { 471 $loading.hide(); 472 $wrapper.show(); 473 474 if (response.success && response.data) { 475 self.renderBlockedIps(response.data.actors || response.data); 476 self.renderPagination('blocked', response.data.pagination); 477 } else { 478 self.renderBlockedIps([]); 479 } 480 }, 481 error: function(xhr, status, error) { 482 $loading.hide(); 483 $wrapper.show(); 484 self.showTableError('#atomicedge-ad-blocked-body', 'Network error: ' + error); 485 } 486 }); 487 }, 488 489 /** 490 * Render Blocked IPs table rows. 491 * 492 * Columns: IP Address, Threat Score, WAF Hits, Type, Blocked, Expires, Actions 493 * 494 * @param {Array} actors Blocked actor profiles 495 */ 496 renderBlockedIps: function(actors) { 497 var $tbody = $('#atomicedge-ad-blocked-body'); 498 var $empty = $('#atomicedge-ad-blocked-empty'); 499 var $table = $('#atomicedge-ad-blocked-table'); 500 501 $tbody.empty(); 502 503 if (!actors || actors.length === 0) { 504 $table.hide(); 505 $empty.show(); 506 return; 507 } 508 509 $table.show(); 510 $empty.hide(); 511 512 var self = this; 513 actors.forEach(function(actor) { 514 var ip = actor.ip_address || actor.ip || ''; 515 var score = actor.score || actor.threat_score || 0; 516 var wafHits = actor.waf_hits || actor.total_waf_hits || 0; 517 var blockedAt = actor.blocked_at || null; 518 var expiresAt = actor.block_expires_at || null; 519 var isPermanent = actor.is_blocked && !expiresAt; 520 521 var html = '<tr>'; 522 // IP Address with flag 523 html += '<td>' + self.formatIpWithFlag(ip, actor) + '</td>'; 524 // Threat Score 525 html += '<td>' + self.formatScore(score) + '</td>'; 526 // WAF Hits 527 html += '<td>' + wafHits + '</td>'; 528 // Type (Permanent / Timed) 529 html += '<td>'; 530 if (isPermanent) { 531 html += '<span class="atomicedge-ad-status-badge atomicedge-ad-status-blocked" style="font-size:11px;">Permanent</span>'; 532 } else { 533 html += '<span class="atomicedge-ad-status-badge atomicedge-ad-status-pending" style="font-size:11px;">Timed</span>'; 534 } 535 html += '</td>'; 536 // Blocked (relative time) 537 html += '<td>' + self.formatRelativeTime(blockedAt) + '</td>'; 538 // Expires 539 html += '<td>'; 540 if (isPermanent) { 541 html += '<strong>Never</strong>'; 542 } else { 543 html += self.formatRelativeTime(expiresAt); 544 } 545 html += '</td>'; 546 // Actions 547 html += '<td>'; 548 if (!isPermanent) { 549 html += '<button type="button" class="button button-small atomicedge-ad-extend-block-btn" data-ip="' + self.escapeHtml(ip) + '" title="Extend +1 day">'; 550 html += '<span class="dashicons dashicons-clock" style="margin-top:3px;"></span></button> '; 551 html += '<button type="button" class="button button-small atomicedge-ad-make-permanent-btn" data-ip="' + self.escapeHtml(ip) + '" title="Make permanent">'; 552 html += '<span class="dashicons dashicons-lock" style="margin-top:3px;"></span></button> '; 553 } 554 html += '<button type="button" class="button button-small atomicedge-ad-unblock-btn" data-ip="' + self.escapeHtml(ip) + '" title="Unblock">'; 555 html += '<span class="dashicons dashicons-unlock" style="margin-top:3px;"></span></button>'; 556 html += '</td>'; 557 html += '</tr>'; 558 $tbody.append(html); 559 }); 560 }, 561 562 /** 563 * Unblock a blocked IP address. 564 * 565 * @param {string} ip IP address 566 */ 567 unblockIp: function(ip) { 568 var self = this; 569 570 if (!confirm('Are you sure you want to unblock ' + ip + '?')) { 571 return; 572 } 573 574 $.ajax({ 575 url: atomicedge_admin.ajax_url, 576 type: 'POST', 577 data: { 578 action: 'atomicedge_unblock_ip', 579 nonce: atomicedge_admin.nonce, 580 ip: ip 581 }, 582 success: function(response) { 583 if (response.success) { 584 self.showNotice(ip + ' has been unblocked.', 'success'); 585 self.loadBlockedIps(); 586 } else { 587 self.showNotice(response.data ? response.data.message : 'Failed to unblock IP', 'error'); 588 } 589 }, 590 error: function(xhr, status, error) { 591 self.showNotice('Network error: ' + error, 'error'); 592 } 593 }); 594 }, 595 596 /** 597 * Extend a timed block by 1 day. 598 * 599 * @param {string} ip IP address 600 */ 601 extendBlock: function(ip) { 602 var self = this; 603 604 $.ajax({ 605 url: atomicedge_admin.ajax_url, 606 type: 'POST', 607 data: { 608 action: 'atomicedge_extend_block', 609 nonce: atomicedge_admin.nonce, 610 ip: ip, 611 days: 1 612 }, 613 success: function(response) { 614 if (response.success) { 615 self.showNotice('Block for ' + ip + ' extended by 1 day.', 'success'); 616 self.loadBlockedIps(); 617 } else { 618 self.showNotice(response.data ? response.data.message : 'Failed to extend block', 'error'); 619 } 620 }, 621 error: function(xhr, status, error) { 622 self.showNotice('Network error: ' + error, 'error'); 623 } 624 }); 625 }, 626 627 /** 628 * Make a timed block permanent. 629 * 630 * @param {string} ip IP address 631 */ 632 makePermanent: function(ip) { 633 var self = this; 634 635 if (!confirm('Make the block for ' + ip + ' permanent?')) { 636 return; 637 } 638 639 $.ajax({ 640 url: atomicedge_admin.ajax_url, 641 type: 'POST', 642 data: { 643 action: 'atomicedge_make_permanent', 644 nonce: atomicedge_admin.nonce, 645 ip: ip 646 }, 647 success: function(response) { 648 if (response.success) { 649 self.showNotice('Block for ' + ip + ' is now permanent.', 'success'); 650 self.loadBlockedIps(); 651 } else { 652 self.showNotice(response.data ? response.data.message : 'Failed to make block permanent', 'error'); 653 } 654 }, 655 error: function(xhr, status, error) { 656 self.showNotice('Network error: ' + error, 'error'); 657 } 658 }); 659 }, 660 661 /** 662 * Load Actor Profiles 663 */ 664 loadActorProfiles: function() { 665 var self = this; 666 var $loading = $('#atomicedge-ad-actors-loading'); 667 var $wrapper = $('#atomicedge-ad-actors-table-wrapper'); 668 var filter = $('#atomicedge-ad-actors-filter').val(); 669 var search = $('#atomicedge-ad-actors-search').val().trim(); 670 671 $loading.show(); 672 $wrapper.hide(); 673 674 $.ajax({ 675 url: atomicedge_admin.ajax_url, 676 type: 'POST', 677 data: { 678 action: 'atomicedge_get_actor_profiles', 679 nonce: atomicedge_admin.nonce, 680 force_refresh: 'true', 395 681 filter: filter, 396 682 search: search, … … 443 729 var ipAddress = actor.ip_address || actor.ip || ''; 444 730 var html = '<tr>'; 445 html += '<td>' + self. escapeHtml(ipAddress) + '</td>';731 html += '<td>' + self.formatIpWithFlag(ipAddress, actor) + '</td>'; 446 732 html += '<td>' + self.formatScore(score) + '</td>'; 447 733 html += '<td>' + (actor.total_requests || actor.requests || 0) + '</td>'; … … 525 811 action: 'atomicedge_get_threat_detections', 526 812 nonce: atomicedge_admin.nonce, 813 force_refresh: 'true', 527 814 status: status !== 'all' ? status : '', 528 815 page: self.pages.detections, … … 573 860 var ipAddress = detection.ip_address || detection.ip || (detection.actor && detection.actor.ip_address) || 'N/A'; 574 861 var html = '<tr data-detection-id="' + detection.id + '">'; 575 html += '<td>' + self. escapeHtml(ipAddress) + '</td>';862 html += '<td>' + self.formatIpWithFlag(ipAddress, detection) + '</td>'; 576 863 html += '<td>' + self.formatScore(detection.score || 0) + '</td>'; 577 864 html += '<td>' + self.formatThreatLevel(detection.threat_level || 'low') + '</td>'; … … 676 963 677 964 // Actor details 678 $detailRow.find('.atomicedge-ad-detail-ip').text(actor.ip || actor.ip_address || detection.ip_address || 'N/A'); 965 var actorIp = actor.ip || actor.ip_address || detection.ip_address || 'N/A'; 966 var actorFlag = this.countryCodeToFlag(actor.country_code || detection.country_code || null); 967 var flagHtml = actorFlag ? '<span title="' + this.escapeHtml(actor.country_code || detection.country_code || '') + '" style="margin-right: 4px;">' + actorFlag + '</span>' : ''; 968 $detailRow.find('.atomicedge-ad-detail-ip').html(flagHtml + this.escapeHtml(actorIp)); 679 969 $detailRow.find('.atomicedge-ad-detail-requests').text(actor.total_requests || 0); 680 970 $detailRow.find('.atomicedge-ad-detail-waf-hits').text(actor.total_waf_hits || actor.waf_hits || 0); … … 974 1264 }, 975 1265 1266 /** 1267 * Format a date as a relative time string (e.g., "2 hours ago", "in 3 days"). 1268 * 1269 * @param {string|null} dateString ISO date string 1270 * @return {string} Relative time or em-dash 1271 */ 1272 formatRelativeTime: function(dateString) { 1273 if (!dateString) { 1274 return '—'; 1275 } 1276 try { 1277 var date = new Date(dateString); 1278 var now = new Date(); 1279 var diffMs = date.getTime() - now.getTime(); 1280 var absDiffMs = Math.abs(diffMs); 1281 var seconds = Math.floor(absDiffMs / 1000); 1282 var minutes = Math.floor(seconds / 60); 1283 var hours = Math.floor(minutes / 60); 1284 var days = Math.floor(hours / 24); 1285 1286 var label; 1287 if (days > 0) { 1288 label = days + ' day' + (days > 1 ? 's' : ''); 1289 } else if (hours > 0) { 1290 label = hours + ' hour' + (hours > 1 ? 's' : ''); 1291 } else if (minutes > 0) { 1292 label = minutes + ' min' + (minutes > 1 ? 's' : ''); 1293 } else { 1294 label = 'just now'; 1295 return label; 1296 } 1297 1298 return diffMs < 0 ? label + ' ago' : 'in ' + label; 1299 } catch (e) { 1300 return dateString; 1301 } 1302 }, 1303 976 1304 /* ============================ 977 1305 * Utility Helpers … … 991 1319 div.textContent = str; 992 1320 return div.innerHTML; 1321 }, 1322 1323 /** 1324 * Convert ISO 3166-1 alpha-2 country code to flag emoji. 1325 * 1326 * Uses Unicode Regional Indicator Symbols (same approach as Laravel backend). 1327 * 1328 * @param {string|null} countryCode Two-letter country code (e.g., 'US', 'CN') 1329 * @return {string} Flag emoji or empty string 1330 */ 1331 countryCodeToFlag: function(countryCode) { 1332 if (!countryCode || countryCode.length !== 2) { 1333 return ''; 1334 } 1335 var code = countryCode.toUpperCase(); 1336 var base = 0x1F1E6 - 'A'.charCodeAt(0); 1337 return String.fromCodePoint(base + code.charCodeAt(0)) + 1338 String.fromCodePoint(base + code.charCodeAt(1)); 1339 }, 1340 1341 /** 1342 * Format an IP address with an optional country flag prefix. 1343 * 1344 * @param {string} ip The IP address (already escaped) 1345 * @param {Object} dataObj The data object that may contain country_code or country_flag_emoji 1346 * @return {string} HTML string with flag + IP 1347 */ 1348 formatIpWithFlag: function(ip, dataObj) { 1349 var flag = ''; 1350 if (dataObj) { 1351 // Prefer pre-computed emoji from API, fall back to client-side conversion 1352 flag = dataObj.country_flag_emoji || this.countryCodeToFlag(dataObj.country_code || null); 1353 } 1354 var countryCode = (dataObj && dataObj.country_code) ? dataObj.country_code : ''; 1355 if (flag) { 1356 return '<span title="' + this.escapeHtml(countryCode) + '" style="margin-right: 4px;">' + flag + '</span>' + this.escapeHtml(ip); 1357 } 1358 return this.escapeHtml(ip); 993 1359 }, 994 1360 -
atomic-edge-security/trunk/admin/js/admin.js
r3473194 r3476055 395 395 var actionCell; 396 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;">' +397 actionCell = '<span class="atomicedge-blocked-badge" title="This IP is blocked" style="color:#b32d2e;font-weight:600;">' + 398 398 '<span class="dashicons dashicons-lock" style="font-size:14px;width:14px;height:14px;margin-top:3px;"></span> Blocked</span>'; 399 399 } else { … … 411 411 }); 412 412 413 // Bind block IP buttons 413 // Bind block IP buttons — blocks go to Adaptive Defense (not IP blacklist). 414 414 $tbody.find('.atomicedge-block-ip').on('click', function() { 415 415 var $btn = $(this); 416 416 var ip = $btn.data('ip'); 417 417 if (confirm(atomicedgeAdmin.strings.confirm)) { 418 self. addIpBlacklist(ip, 'Blocked from WAF logs on ' + self.formatTimestamp(), $btn);418 self.blockIpFromWafLogs(ip, $btn); 419 419 } 420 420 }); … … 484 484 /** 485 485 * Load IP rules 486 */ 487 loadIpRules: function() { 488 var self = this; 489 490 this.ajax('atomicedge_get_ip_rules', {}, function(data) { 486 * 487 * @param {boolean} forceRefresh Bypass transient cache (default true) 488 */ 489 loadIpRules: function(forceRefresh) { 490 var self = this; 491 var data = {}; 492 493 if (forceRefresh !== false) { 494 data.force_refresh = 'true'; 495 } 496 497 this.ajax('atomicedge_get_ip_rules', data, function(data) { 491 498 self.renderIpList('whitelist', data.whitelist || []); 492 499 self.renderIpList('blacklist', data.blacklist || []); … … 570 577 571 578 /** 572 * Add IP to blacklist 579 * Block an IP from the WAF logs page via Adaptive Defense. 580 * 581 * This sends the block to the AD system (ActorProfile), NOT the 582 * Access Control IP blacklist (SiteSettings). The existing 583 * ajax_block_ip AJAX handler + api->block_ip() method are reused. 584 * 585 * @param {string} ip IP address. 586 * @param {jQuery} $button The button element to update on success. 587 */ 588 blockIpFromWafLogs: function(ip, $button) { 589 var self = this; 590 591 if ($button) { 592 $button.prop('disabled', true).text(atomicedgeAdmin.strings.loading); 593 } 594 595 this.ajax('atomicedge_block_ip', { 596 ip: ip, 597 duration_hours: 24, 598 reason: 'Blocked from WAF logs' 599 }, function() { 600 // Reload WAF logs so the is_blocked badge appears. 601 if ($('#atomicedge-waf-table').length) { 602 self.loadWafLogs(); 603 } 604 605 // Immediate visual feedback. 606 if ($button) { 607 $button.prop('disabled', true) 608 .removeClass('button-small') 609 .addClass('atomicedge-blocked-btn') 610 .html('<span class="dashicons dashicons-yes-alt" style="margin-top:3px;color:#00a32a;"></span> Blocked'); 611 } 612 613 self.showNotice(ip + ' has been blocked via Adaptive Defense.', 'success'); 614 }, function(errData) { 615 if ($button) { 616 $button.prop('disabled', false).text('Block IP'); 617 } 618 var message = (errData && errData.message) ? errData.message : atomicedgeAdmin.strings.error; 619 self.showNotice(message, 'error'); 620 }); 621 }, 622 623 /** 624 * Add IP to blacklist (Access Control page only). 573 625 * 574 626 * @param {string} ip IP address or CIDR. … … 605 657 } 606 658 607 var noticeMsg = ip + ' has been added to the block list.'; 608 // If blocked from outside Access Control, direct user there. 609 if ($button) { 610 noticeMsg += ' Manage blocks in Access Control.'; 611 } 659 var noticeMsg = ip + ' has been added to the IP blacklist.'; 612 660 self.showNotice(noticeMsg, 'success'); 613 661 }, function(errData) { 614 662 if ($button) { 615 $button.prop('disabled', false).text(' Block IP');663 $button.prop('disabled', false).text('Add to Blacklist'); 616 664 } 617 665 var message = (errData && errData.message) ? errData.message : atomicedgeAdmin.strings.error; -
atomic-edge-security/trunk/admin/views/adaptive-defense.php
r3473194 r3476055 23 23 // Tab navigation. 24 24 $current_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'status'; 25 $valid_tabs = array( 'status', ' actors', 'detections' );25 $valid_tabs = array( 'status', 'blocked', 'actors', 'detections' ); 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', 35 39 ), 36 40 'actors' => array( … … 95 99 switch ( $current_tab ) { 96 100 101 case 'blocked': 102 include ATOMICEDGE_PLUGIN_DIR . 'admin/views/partials/adaptive-defense-blocked-tab.php'; 103 break; 97 104 case 'actors': 98 105 include ATOMICEDGE_PLUGIN_DIR . 'admin/views/partials/adaptive-defense-actors-tab.php'; -
atomic-edge-security/trunk/admin/views/partials/adaptive-defense-actors-tab.php
r3454914 r3476055 56 56 <thead> 57 57 <tr> 58 <th style="width: 1 60px;"><?php esc_html_e( 'IP Address', 'atomic-edge-security' ); ?></th>58 <th style="width: 185px;"><?php esc_html_e( 'IP Address', 'atomic-edge-security' ); ?></th> 59 59 <th style="width: 80px;"><?php esc_html_e( 'Score', 'atomic-edge-security' ); ?></th> 60 60 <th style="width: 90px;"><?php esc_html_e( 'Requests', 'atomic-edge-security' ); ?></th> -
atomic-edge-security/trunk/admin/views/partials/adaptive-defense-blocked-tab.php
r3454914 r3476055 60 60 <tr> 61 61 <th style="width: 180px;"><?php esc_html_e( 'IP Address', 'atomic-edge-security' ); ?></th> 62 <th style="width: 80px;"><?php esc_html_e( 'Score', 'atomic-edge-security' ); ?></th> 63 <th style="width: 100px;"><?php esc_html_e( 'WAF Hits', 'atomic-edge-security' ); ?></th> 64 <th><?php esc_html_e( 'Expires', 'atomic-edge-security' ); ?></th> 65 <th style="width: 180px;"><?php esc_html_e( 'Actions', 'atomic-edge-security' ); ?></th> 62 <th style="width: 80px;"><?php esc_html_e( 'Threat Score', 'atomic-edge-security' ); ?></th> 63 <th style="width: 80px;"><?php esc_html_e( 'WAF Hits', 'atomic-edge-security' ); ?></th> 64 <th style="width: 90px;"><?php esc_html_e( 'Type', 'atomic-edge-security' ); ?></th> 65 <th style="width: 110px;"><?php esc_html_e( 'Blocked', 'atomic-edge-security' ); ?></th> 66 <th style="width: 110px;"><?php esc_html_e( 'Expires', 'atomic-edge-security' ); ?></th> 67 <th style="width: 200px;"><?php esc_html_e( 'Actions', 'atomic-edge-security' ); ?></th> 66 68 </tr> 67 69 </thead> -
atomic-edge-security/trunk/admin/views/partials/adaptive-defense-detections-tab.php
r3473194 r3476055 51 51 <thead> 52 52 <tr> 53 <th style="width: 1 60px;"><?php esc_html_e( 'IP Address', 'atomic-edge-security' ); ?></th>53 <th style="width: 185px;"><?php esc_html_e( 'IP Address', 'atomic-edge-security' ); ?></th> 54 54 <th style="width: 80px;"><?php esc_html_e( 'Score', 'atomic-edge-security' ); ?></th> 55 55 <th style="width: 100px;"><?php esc_html_e( 'Threat Level', 'atomic-edge-security' ); ?></th> -
atomic-edge-security/trunk/atomicedge.php
r3473217 r3476055 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. 76 * Version: 2.4.8 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. 7' );28 define( 'ATOMICEDGE_VERSION', '2.4.8' ); 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
r3473194 r3476055 99 99 add_action( 'wp_ajax_atomicedge_block_ip', array( $this, 'ajax_block_ip' ) ); 100 100 add_action( 'wp_ajax_atomicedge_unblock_ip', array( $this, 'ajax_unblock_ip' ) ); 101 add_action( 'wp_ajax_atomicedge_extend_block', array( $this, 'ajax_extend_block' ) ); 102 add_action( 'wp_ajax_atomicedge_make_permanent', array( $this, 'ajax_make_permanent' ) ); 101 103 add_action( 'wp_ajax_atomicedge_delete_actor', array( $this, 'ajax_delete_actor' ) ); 102 104 add_action( 'wp_ajax_atomicedge_dismiss_detection', array( $this, 'ajax_dismiss_detection' ) ); … … 236 238 */ 237 239 public function ajax_get_ip_rules() { 238 $this->get_verified_post_fields( array() ); 239 240 $result = $this->api->get_ip_rules(); 240 $post = $this->get_verified_post_fields( array( 'force_refresh' ) ); 241 242 $force_refresh = ! empty( $post['force_refresh'] ) && 'true' === $post['force_refresh']; 243 $result = $this->api->get_ip_rules( $force_refresh ); 241 244 242 245 if ( $result['success'] ) { … … 863 866 } 864 867 868 /** 869 * Check whether Adaptive Defense handlers should use dev mode simulation. 870 * 871 * Dev mode provides simulated data for local development environments 872 * that have NO API key configured. If an API key exists (even when the 873 * `atomicedge_connected` flag is not set), the real API should be used. 874 * 875 * @return bool True if dev mode simulation should be used. 876 */ 877 private function should_use_dev_mode() { 878 if ( ! \AtomicEdge_Dev_Mode::is_enabled() ) { 879 return false; 880 } 881 882 // If an API key is configured, always use the real API. 883 if ( $this->api->get_api_key() ) { 884 return false; 885 } 886 887 return true; 888 } 889 865 890 // ========================================================================= 866 891 // Adaptive Defense AJAX Handlers … … 873 898 */ 874 899 public function ajax_get_adaptive_defense() { 875 $ this->get_verified_post_fields( array() );876 877 // Dev mode: return simulated data .878 if ( AtomicEdge_Dev_Mode::is_enabled() ) {900 $post = $this->get_verified_post_fields( array( 'force_refresh' ) ); 901 902 // Dev mode: return simulated data (only when not connected to real API). 903 if ( $this->should_use_dev_mode() ) { 879 904 wp_send_json_success( AtomicEdge_Dev_Mode::get_simulated_adaptive_defense() ); 880 905 } 881 906 882 $response = $this->api->get_adaptive_defense(); 907 $force_refresh = ! empty( $post['force_refresh'] ) && 'true' === $post['force_refresh']; 908 $response = $this->api->get_adaptive_defense( $force_refresh ); 883 909 884 910 if ( $response['success'] ) { … … 895 921 */ 896 922 public function ajax_get_actor_profiles() { 897 $post = $this->get_verified_post_fields( array( 'page', 'per_page', 'filter', 'search' ) );923 $post = $this->get_verified_post_fields( array( 'page', 'per_page', 'filter', 'search', 'force_refresh' ) ); 898 924 899 925 $args = array( … … 907 933 } 908 934 909 // Dev mode: return simulated data .910 if ( AtomicEdge_Dev_Mode::is_enabled() ) {935 // Dev mode: return simulated data (only when not connected to real API). 936 if ( $this->should_use_dev_mode() ) { 911 937 wp_send_json_success( AtomicEdge_Dev_Mode::get_simulated_actor_profiles( $args ) ); 912 938 } 913 939 914 $response = $this->api->get_actor_profiles( $args ); 940 $force_refresh = ! empty( $post['force_refresh'] ) && 'true' === $post['force_refresh']; 941 $response = $this->api->get_actor_profiles( $args, $force_refresh ); 915 942 916 943 if ( $response['success'] ) { … … 927 954 */ 928 955 public function ajax_get_threat_detections() { 929 $post = $this->get_verified_post_fields( array( 'page', 'per_page', 'status' ) );956 $post = $this->get_verified_post_fields( array( 'page', 'per_page', 'status', 'force_refresh' ) ); 930 957 931 958 $args = array( … … 935 962 ); 936 963 937 // Dev mode: return simulated data .938 if ( AtomicEdge_Dev_Mode::is_enabled() ) {964 // Dev mode: return simulated data (only when not connected to real API). 965 if ( $this->should_use_dev_mode() ) { 939 966 wp_send_json_success( AtomicEdge_Dev_Mode::get_simulated_threat_detections( $args ) ); 940 967 } 941 968 942 $response = $this->api->get_threat_detections( $args ); 969 $force_refresh = ! empty( $post['force_refresh'] ) && 'true' === $post['force_refresh']; 970 $response = $this->api->get_threat_detections( $args, $force_refresh ); 943 971 944 972 if ( $response['success'] ) { … … 961 989 } 962 990 963 // Dev mode: return simulated detail data .964 if ( AtomicEdge_Dev_Mode::is_enabled() ) {991 // Dev mode: return simulated detail data (only when not connected to real API). 992 if ( $this->should_use_dev_mode() ) { 965 993 $detail = AtomicEdge_Dev_Mode::get_simulated_threat_detection_detail( absint( $post['detection_id'] ) ); 966 994 if ( $detail ) { … … 996 1024 $reason = isset( $post['reason'] ) ? sanitize_text_field( $post['reason'] ) : ''; 997 1025 998 // Dev mode: return simulated success .999 if ( AtomicEdge_Dev_Mode::is_enabled() ) {1026 // Dev mode: return simulated success (only when not connected to real API). 1027 if ( $this->should_use_dev_mode() ) { 1000 1028 wp_send_json_success( array( 1001 1029 'message' => sprintf( … … 1036 1064 } 1037 1065 1038 // Dev mode: return simulated success .1039 if ( AtomicEdge_Dev_Mode::is_enabled() ) {1066 // Dev mode: return simulated success (only when not connected to real API). 1067 if ( $this->should_use_dev_mode() ) { 1040 1068 wp_send_json_success( array( 1041 1069 'message' => sprintf( … … 1063 1091 1064 1092 /** 1093 * Extend the block duration for a blocked IP. 1094 * 1095 * @return void 1096 */ 1097 public function ajax_extend_block() { 1098 $post = $this->get_verified_post_fields( array( 'ip', 'days' ) ); 1099 1100 if ( empty( $post['ip'] ) ) { 1101 wp_send_json_error( array( 'message' => __( 'IP address is required.', 'atomic-edge-security' ) ) ); 1102 } 1103 1104 $ip = $post['ip']; 1105 $days = isset( $post['days'] ) ? max( 1, absint( $post['days'] ) ) : 1; 1106 1107 // Dev mode: return simulated success (only when not connected to real API). 1108 if ( $this->should_use_dev_mode() ) { 1109 wp_send_json_success( array( 1110 'message' => sprintf( 1111 /* translators: 1: IP address, 2: number of days */ 1112 __( '[Dev Mode] Block for %1$s extended by %2$d day(s).', 'atomic-edge-security' ), 1113 esc_html( $ip ), 1114 $days 1115 ), 1116 'data' => AtomicEdge_Dev_Mode::simulate_extend_block( $ip, $days ), 1117 ) ); 1118 } 1119 1120 $response = $this->api->extend_block( $ip, $days ); 1121 1122 if ( $response['success'] ) { 1123 wp_send_json_success( array( 1124 'message' => sprintf( 1125 /* translators: 1: IP address, 2: number of days */ 1126 __( 'Block for %1$s extended by %2$d day(s).', 'atomic-edge-security' ), 1127 esc_html( $ip ), 1128 $days 1129 ), 1130 'data' => $response['data'] ?? array(), 1131 ) ); 1132 } else { 1133 wp_send_json_error( array( 'message' => $response['error'] ?? __( 'Failed to extend block.', 'atomic-edge-security' ) ) ); 1134 } 1135 } 1136 1137 /** 1138 * Make a timed block permanent. 1139 * 1140 * @return void 1141 */ 1142 public function ajax_make_permanent() { 1143 $post = $this->get_verified_post_fields( array( 'ip' ) ); 1144 1145 if ( empty( $post['ip'] ) ) { 1146 wp_send_json_error( array( 'message' => __( 'IP address is required.', 'atomic-edge-security' ) ) ); 1147 } 1148 1149 $ip = $post['ip']; 1150 1151 // Dev mode: return simulated success (only when not connected to real API). 1152 if ( $this->should_use_dev_mode() ) { 1153 wp_send_json_success( array( 1154 'message' => sprintf( 1155 /* translators: %s: IP address */ 1156 __( '[Dev Mode] Block for %s is now permanent.', 'atomic-edge-security' ), 1157 esc_html( $ip ) 1158 ), 1159 'data' => AtomicEdge_Dev_Mode::simulate_make_permanent( $ip ), 1160 ) ); 1161 } 1162 1163 $response = $this->api->make_permanent( $ip ); 1164 1165 if ( $response['success'] ) { 1166 wp_send_json_success( array( 1167 'message' => sprintf( 1168 /* translators: %s: IP address */ 1169 __( 'Block for %s is now permanent.', 'atomic-edge-security' ), 1170 esc_html( $ip ) 1171 ), 1172 'data' => $response['data'] ?? array(), 1173 ) ); 1174 } else { 1175 wp_send_json_error( array( 'message' => $response['error'] ?? __( 'Failed to make block permanent.', 'atomic-edge-security' ) ) ); 1176 } 1177 } 1178 1179 /** 1065 1180 * Delete an actor profile. 1066 1181 * … … 1074 1189 } 1075 1190 1076 // Dev mode: return simulated success .1077 if ( AtomicEdge_Dev_Mode::is_enabled() ) {1191 // Dev mode: return simulated success (only when not connected to real API). 1192 if ( $this->should_use_dev_mode() ) { 1078 1193 wp_send_json_success( array( 1079 1194 'message' => __( '[Dev Mode] Actor profile has been deleted.', 'atomic-edge-security' ), … … 1104 1219 } 1105 1220 1106 // Dev mode: return simulated success .1107 if ( AtomicEdge_Dev_Mode::is_enabled() ) {1221 // Dev mode: return simulated success (only when not connected to real API). 1222 if ( $this->should_use_dev_mode() ) { 1108 1223 wp_send_json_success( array( 1109 1224 'message' => __( '[Dev Mode] Threat detection has been dismissed.', 'atomic-edge-security' ), -
atomic-edge-security/trunk/includes/class-atomicedge-api.php
r3473194 r3476055 283 283 * @return array IP rules or error. 284 284 */ 285 public function get_ip_rules( ) {285 public function get_ip_rules( $force_refresh = false ) { 286 286 $cache_key = 'atomicedge_ip_rules'; 287 $cached = get_transient( $cache_key ); 288 289 if ( false !== $cached ) { 290 return $cached; 287 288 if ( ! $force_refresh ) { 289 $cached = get_transient( $cache_key ); 290 if ( false !== $cached ) { 291 return $cached; 292 } 291 293 } 292 294 … … 516 518 * @return array Status data or error. 517 519 */ 518 public function get_adaptive_defense( ) {520 public function get_adaptive_defense( $force_refresh = false ) { 519 521 $cache_key = 'atomicedge_adaptive_defense'; 520 $cached = get_transient( $cache_key ); 521 522 if ( false !== $cached ) { 523 return $cached; 522 523 if ( ! $force_refresh ) { 524 $cached = get_transient( $cache_key ); 525 if ( false !== $cached ) { 526 return $cached; 527 } 524 528 } 525 529 … … 540 544 * @return array Actor profiles or error. 541 545 */ 542 public function get_actor_profiles( $args = array() ) {546 public function get_actor_profiles( $args = array(), $force_refresh = false ) { 543 547 $defaults = array( 544 548 'page' => 1, … … 549 553 550 554 $cache_key = 'atomicedge_actors_' . hash( 'sha256', (string) wp_json_encode( $args ) ); 551 $cached = get_transient( $cache_key ); 552 553 if ( false !== $cached ) { 554 return $cached; 555 556 if ( ! $force_refresh ) { 557 $cached = get_transient( $cache_key ); 558 if ( false !== $cached ) { 559 return $cached; 560 } 555 561 } 556 562 … … 570 576 * @return array Threat detections or error. 571 577 */ 572 public function get_threat_detections( $args = array() ) {578 public function get_threat_detections( $args = array(), $force_refresh = false ) { 573 579 $defaults = array( 574 580 'page' => 1, … … 579 585 580 586 $cache_key = 'atomicedge_detections_' . hash( 'sha256', (string) wp_json_encode( $args ) ); 581 $cached = get_transient( $cache_key ); 582 583 if ( false !== $cached ) { 584 return $cached; 587 588 if ( ! $force_refresh ) { 589 $cached = get_transient( $cache_key ); 590 if ( false !== $cached ) { 591 return $cached; 592 } 585 593 } 586 594 … … 627 635 628 636 if ( $response['success'] ) { 629 // Clear caches. 637 // Clear caches — AD cache + WAF log cache (since WAF logs 638 // show is_blocked status from both blacklist and AD). 630 639 $this->clear_adaptive_defense_cache(); 640 $this->invalidate_waf_log_cache(); 631 641 } 632 642 … … 642 652 public function unblock_ip( $ip ) { 643 653 $response = $this->request( 'POST', '/adaptive-defense/unblock', array( 'ip' => $ip ) ); 654 655 if ( $response['success'] ) { 656 $this->clear_adaptive_defense_cache(); 657 $this->invalidate_waf_log_cache(); 658 } 659 660 return $response; 661 } 662 663 /** 664 * Extend the block duration for a blocked IP address. 665 * 666 * @param string $ip IP address. 667 * @param int $days Number of days to extend (default 1). 668 * @return array Result. 669 */ 670 public function extend_block( $ip, $days = 1 ) { 671 $response = $this->request( 'POST', '/adaptive-defense/extend-block', array( 672 'ip' => $ip, 673 'days' => $days, 674 ) ); 675 676 if ( $response['success'] ) { 677 $this->clear_adaptive_defense_cache(); 678 } 679 680 return $response; 681 } 682 683 /** 684 * Make a timed block permanent. 685 * 686 * @param string $ip IP address. 687 * @return array Result. 688 */ 689 public function make_permanent( $ip ) { 690 $response = $this->request( 'POST', '/adaptive-defense/make-permanent', array( 'ip' => $ip ) ); 644 691 645 692 if ( $response['success'] ) { -
atomic-edge-security/trunk/includes/class-atomicedge-dev-mode.php
r3473217 r3476055 82 82 // Default to enabled for local environments, can be disabled via option. 83 83 return get_option( 'atomicedge_dev_mode', true ); 84 } 85 86 /** 87 * Convert an ISO 3166-1 alpha-2 country code to a flag emoji. 88 * 89 * Uses Unicode Regional Indicator Symbols — same approach as 90 * ActorProfile::getCountryFlagEmojiAttribute() on the Laravel side. 91 * 92 * @param string|null $country_code Two-letter code (e.g. 'US'). 93 * @return string Flag emoji or empty string. 94 */ 95 public static function country_code_to_flag( $country_code ) { 96 if ( empty( $country_code ) || strlen( $country_code ) !== 2 ) { 97 return ''; 98 } 99 $code = strtoupper( $country_code ); 100 $base = 0x1F1E6 - ord( 'A' ); 101 // mb_chr requires PHP 7.2+ (WordPress 5.x minimum). 102 return mb_chr( $base + ord( $code[0] ) ) . mb_chr( $base + ord( $code[1] ) ); 84 103 } 85 104 … … 358 377 return array( 359 378 array( 360 'id' => 1001, 361 'ip' => '45.33.32.156', 362 'ip_address' => '45.33.32.156', 363 'country_code' => 'US', 379 'id' => 1001, 380 'ip' => '45.33.32.156', 381 'ip_address' => '45.33.32.156', 382 'country_code' => 'US', 383 'country_flag_emoji' => self::country_code_to_flag( 'US' ), 364 384 'total_requests' => 1523, 365 385 'total_waf_hits' => 89, … … 373 393 ), 374 394 array( 375 'id' => 1002, 376 'ip' => '103.235.46.39', 377 'ip_address' => '103.235.46.39', 378 'country_code' => 'CN', 395 'id' => 1002, 396 'ip' => '103.235.46.39', 397 'ip_address' => '103.235.46.39', 398 'country_code' => 'CN', 399 'country_flag_emoji' => self::country_code_to_flag( 'CN' ), 379 400 'total_requests' => 856, 380 401 'total_waf_hits' => 45, … … 403 424 'ip_address' => '45.33.32.156', 404 425 'country_code' => 'US', 426 'country_flag_emoji' => self::country_code_to_flag( 'US' ), 405 427 'total_requests' => 1523, 406 428 'total_waf_hits' => 89, … … 426 448 'ip_address' => '103.235.46.39', 427 449 'country_code' => 'CN', 450 'country_flag_emoji' => self::country_code_to_flag( 'CN' ), 428 451 'total_requests' => 856, 429 452 'total_waf_hits' => 45, … … 447 470 'ip_address' => '198.51.100.42', 448 471 'country_code' => 'DE', 472 'country_flag_emoji' => self::country_code_to_flag( 'DE' ), 449 473 'total_requests' => 324, 450 474 'total_waf_hits' => 8, … … 468 492 'ip_address' => '203.0.113.88', 469 493 'country_code' => 'RU', 494 'country_flag_emoji' => self::country_code_to_flag( 'RU' ), 470 495 'total_requests' => 2100, 471 496 'total_waf_hits' => 156, … … 491 516 'ip_address' => '192.0.2.200', 492 517 'country_code' => 'BR', 518 'country_flag_emoji' => self::country_code_to_flag( 'BR' ), 493 519 'total_requests' => 98, 494 520 'total_waf_hits' => 3, … … 560 586 'status' => 'auto_blocked', 561 587 'ip_address' => '45.33.32.156', 588 'country_code' => 'US', 589 'country_flag_emoji' => self::country_code_to_flag( 'US' ), 562 590 'created_at' => gmdate( 'c', time() - 7200 ), 563 591 'detected_at' => gmdate( 'c', time() - 7200 ), … … 570 598 'ip_address' => '45.33.32.156', 571 599 'country_code' => 'US', 600 'country_flag_emoji' => self::country_code_to_flag( 'US' ), 572 601 'total_requests' => 1523, 573 602 'total_waf_hits' => 89, … … 591 620 'status' => 'pending_review', 592 621 'ip_address' => '103.235.46.39', 622 'country_code' => 'CN', 623 'country_flag_emoji' => self::country_code_to_flag( 'CN' ), 593 624 'created_at' => gmdate( 'c', time() - 14400 ), 594 625 'detected_at' => gmdate( 'c', time() - 14400 ), … … 601 632 'ip_address' => '103.235.46.39', 602 633 'country_code' => 'CN', 634 'country_flag_emoji' => self::country_code_to_flag( 'CN' ), 603 635 'total_requests' => 856, 604 636 'total_waf_hits' => 45, … … 622 654 'status' => 'user_blocked', 623 655 'ip_address' => '203.0.113.88', 656 'country_code' => 'RU', 657 'country_flag_emoji' => self::country_code_to_flag( 'RU' ), 624 658 'created_at' => gmdate( 'c', time() - 3600 ), 625 659 'detected_at' => gmdate( 'c', time() - 3600 ), … … 633 667 'ip_address' => '203.0.113.88', 634 668 'country_code' => 'RU', 669 'country_flag_emoji' => self::country_code_to_flag( 'RU' ), 635 670 'total_requests' => 2100, 636 671 'total_waf_hits' => 156, … … 654 689 'status' => 'pending_review', 655 690 'ip_address' => '198.51.100.42', 691 'country_code' => 'DE', 692 'country_flag_emoji' => self::country_code_to_flag( 'DE' ), 656 693 'created_at' => gmdate( 'c', time() - 28800 ), 657 694 'detected_at' => gmdate( 'c', time() - 28800 ), … … 664 701 'ip_address' => '198.51.100.42', 665 702 'country_code' => 'DE', 703 'country_flag_emoji' => self::country_code_to_flag( 'DE' ), 666 704 'total_requests' => 324, 667 705 'total_waf_hits' => 8, … … 685 723 'status' => 'dismissed', 686 724 'ip_address' => '192.0.2.200', 725 'country_code' => 'BR', 726 'country_flag_emoji' => self::country_code_to_flag( 'BR' ), 687 727 'created_at' => gmdate( 'c', time() - 86400 ), 688 728 'detected_at' => gmdate( 'c', time() - 86400 ), … … 695 735 'ip_address' => '192.0.2.200', 696 736 'country_code' => 'BR', 737 'country_flag_emoji' => self::country_code_to_flag( 'BR' ), 697 738 'total_requests' => 98, 698 739 'total_waf_hits' => 3, … … 802 843 803 844 /** 845 * Simulate a successful extend block response. 846 * 847 * @param string $ip IP address. 848 * @param int $days Number of days to extend. 849 * @return array 850 */ 851 public static function simulate_extend_block( $ip, $days = 1 ) { 852 return array( 853 'message' => sprintf( 854 /* translators: 1: IP address, 2: number of days */ 855 __( '[Dev Mode] Block for %1$s extended by %2$d day(s).', 'atomic-edge-security' ), 856 $ip, 857 $days 858 ), 859 'ip' => $ip, 860 'is_blocked' => true, 861 'blocked_at' => gmdate( 'c', time() - 3600 ), 862 'block_expires_at' => gmdate( 'c', time() + ( $days * 86400 ) ), 863 ); 864 } 865 866 /** 867 * Simulate a successful make permanent response. 868 * 869 * @param string $ip IP address. 870 * @return array 871 */ 872 public static function simulate_make_permanent( $ip ) { 873 return array( 874 'message' => sprintf( 875 /* translators: %s: IP address */ 876 __( '[Dev Mode] Block for %s is now permanent.', 'atomic-edge-security' ), 877 $ip 878 ), 879 'ip' => $ip, 880 'is_blocked' => true, 881 'blocked_at' => gmdate( 'c', time() - 3600 ), 882 'block_expires_at' => null, 883 ); 884 } 885 886 /** 804 887 * Simulate a successful dismiss detection response. 805 888 * -
atomic-edge-security/trunk/readme.txt
r3473217 r3476055 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 2.4. 77 Stable tag: 2.4.8 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.8 = 116 * NEW: Added Blocked IPs tab to Adaptive Defense with IP Address, Threat Score, WAF Hits, Type, Blocked, Expires columns and actions (Extend, Make Permanent, Unblock) 117 * FIX: Adaptive Defense block actions now route through dashboard Blocked IPs (application-layer) instead of Access Control IP blacklist (edge config) 118 * NEW: Manual block form on Blocked IPs tab with configurable duration (1h, 6h, 24h, 7d, 30d, permanent) 119 * NEW: Extend block (+1 day) and Make Permanent actions for timed blocks 120 * CHANGE: WAF Logs "Block IP" button renamed to "Blacklist IP" to clarify it adds to edge-level IP blacklist 121 * NEW: Added extend_block() and make_permanent() API methods and AJAX handlers with dev mode support 114 122 115 123 = 2.4.7 =
Note: See TracChangeset
for help on using the changeset viewer.