PluginProbe
Atomic Edge Security – Firewall, Malware Scan and Login Security / 2.2.2
Atomic Edge Security – Firewall, Malware Scan and Login Security v2.2.2
trunk 2.0.0 2.1.0 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 All 29 releases
atomic-edge-security / admin / js / adaptive-defense.js

adaptive-defense.js in Atomic Edge Security – Firewall, Malware Scan and Login Security 2.2.2, at admin/js/adaptive-defense.js

1,170 lines 44.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * AtomicEdge Adaptive Defense JavaScript
3 *
4 * Handles all AJAX interactions for the Adaptive Defense admin page.
5 *
6 * @package AtomicEdge
7 * @since 2.1.0
8 */
9
10 /* global jQuery, atomicedge_admin */
11 (function($) {
12 'use strict';
13
14 var AtomicEdgeAD = {
15 /** Current page for each section */
16 pages: {
17 blocked: 1,
18 actors: 1,
19 detections: 1
20 },
21
22 /** Items per page */
23 perPage: 20,
24
25 /**
26 * Initialize the Adaptive Defense module
27 */
28 init: function() {
29 if ($('#atomicedge-adaptive-defense-page').length === 0) {
30 return;
31 }
32
33 this.bindEvents();
34 this.loadCurrentTab();
35 },
36
37 /**
38 * Load content for the currently visible tab (server-side rendered)
39 */
40 loadCurrentTab: function() {
41 // Detect which tab is visible by checking for tab-specific elements
42 if ($('#atomicedge-ad-status-card').length) {
43 this.loadStatusTab();
44 } else if ($('#atomicedge-ad-blocked-card').length) {
45 this.loadBlockedIps();
46 } else if ($('#atomicedge-ad-actors-card').length) {
47 this.loadActorProfiles();
48 } else if ($('#atomicedge-ad-detections-card').length) {
49 this.loadThreatDetections();
50 }
51 },
52
53 /**
54 * Bind event handlers
55 */
56 bindEvents: function() {
57 var self = this;
58
59 // Tab navigation is handled server-side, no need to intercept clicks
60 // Just bind refresh buttons and other interactive elements
61
62 // Status tab refresh
63 $('#atomicedge-ad-status-refresh').on('click', function() {
64 self.loadStatusTab();
65 });
66
67 // Blocked IPs tab
68 $('#atomicedge-ad-blocked-refresh').on('click', function() {
69 self.loadBlockedIps();
70 });
71 // Block IP button click (not form submit)
72 $('#atomicedge-ad-block-btn').on('click', function(e) {
73 e.preventDefault();
74 self.blockIpFromForm();
75 });
76 $(document).on('click', '.atomicedge-ad-unblock-btn', function() {
77 var ip = $(this).data('ip');
78 self.unblockIp(ip);
79 });
80
81 // Actor Profiles tab
82 $('#atomicedge-ad-actors-refresh').on('click', function() {
83 self.loadActorProfiles();
84 });
85 $('#atomicedge-ad-actors-filter').on('change', function() {
86 self.pages.actors = 1;
87 self.loadActorProfiles();
88 });
89 $('#atomicedge-ad-actors-search').on('keypress', function(e) {
90 if (e.which === 13) {
91 self.pages.actors = 1;
92 self.loadActorProfiles();
93 }
94 });
95 // Search button click handler
96 $('#atomicedge-ad-actors-search-btn').on('click', function() {
97 self.pages.actors = 1;
98 self.loadActorProfiles();
99 });
100 $(document).on('click', '.atomicedge-ad-block-actor-btn', function() {
101 var ip = $(this).data('ip');
102 self.blockIp(ip, 'actor');
103 });
104 $(document).on('click', '.atomicedge-ad-delete-actor-btn', function() {
105 var id = $(this).data('id');
106 var ip = $(this).data('ip');
107 self.deleteActor(id, ip);
108 });
109
110 // Threat Detections tab
111 $('#atomicedge-ad-detections-refresh').on('click', function() {
112 self.loadThreatDetections();
113 });
114 $('#atomicedge-ad-detections-status').on('change', function() {
115 self.pages.detections = 1;
116 self.loadThreatDetections();
117 });
118 $(document).on('click', '.atomicedge-ad-view-detection-btn', function() {
119 var id = $(this).data('id');
120 var $row = $(this).closest('tr');
121 self.toggleDetectionDetail(id, $row);
122 });
123 $(document).on('click', '.atomicedge-ad-block-detection-btn', function() {
124 var ip = $(this).data('ip');
125 self.blockIp(ip, 'detection');
126 });
127 $(document).on('click', '.atomicedge-ad-dismiss-btn', function() {
128 var id = $(this).data('id');
129 self.dismissDetection(id);
130 });
131 $(document).on('click', '.atomicedge-ad-detail-close', function() {
132 $(this).closest('.atomicedge-ad-detail-row').remove();
133 });
134
135 // Pagination
136 $(document).on('click', '.atomicedge-ad-page-btn', function() {
137 var section = $(this).data('section');
138 var page = $(this).data('page');
139 self.pages[section] = page;
140
141 switch (section) {
142 case 'blocked':
143 self.loadBlockedIps();
144 break;
145 case 'actors':
146 self.loadActorProfiles();
147 break;
148 case 'detections':
149 self.loadThreatDetections();
150 break;
151 }
152 });
153 },
154
155 /**
156 * Switch to a tab
157 *
158 * @param {string} tab Tab identifier
159 */
160 switchTab: function(tab) {
161 // Update nav tabs
162 $('.nav-tab').removeClass('nav-tab-active');
163 $('.nav-tab[data-tab="' + tab + '"]').addClass('nav-tab-active');
164
165 // Update tab panels
166 $('.atomicedge-ad-tab-panel').removeClass('active');
167 $('#atomicedge-ad-tab-' + tab).addClass('active');
168
169 // Load tab content
170 switch (tab) {
171 case 'status':
172 this.loadStatusTab();
173 break;
174 case 'blocked':
175 this.loadBlockedIps();
176 break;
177 case 'actors':
178 this.loadActorProfiles();
179 break;
180 case 'detections':
181 this.loadThreatDetections();
182 break;
183 }
184 },
185
186 /**
187 * Load Status tab content
188 */
189 loadStatusTab: function() {
190 var self = this;
191 var $loading = $('#atomicedge-ad-status-loading');
192 var $content = $('#atomicedge-ad-status-content');
193
194 $loading.show();
195 $content.hide();
196
197 $.ajax({
198 url: atomicedge_admin.ajax_url,
199 type: 'POST',
200 data: {
201 action: 'atomicedge_get_adaptive_defense',
202 nonce: atomicedge_admin.nonce
203 },
204 success: function(response) {
205 $loading.hide();
206
207 if (response.success && response.data) {
208 self.renderStatusTab(response.data);
209 $content.show();
210 } else {
211 self.showError($content, response.data ? response.data.message : 'Failed to load status');
212 $content.show();
213 }
214 },
215 error: function(xhr, status, error) {
216 $loading.hide();
217 self.showError($content, 'Network error: ' + error);
218 $content.show();
219 }
220 });
221 },
222
223 /**
224 * Render Status tab content
225 *
226 * @param {Object} data API response data
227 */
228 renderStatusTab: function(data) {
229 // Update status badge
230 var $status = $('#atomicedge-ad-status-badge');
231 var enabled = data.settings && data.settings.enabled;
232 $status.removeClass('atomicedge-ad-badge-enabled atomicedge-ad-badge-disabled');
233
234 if (enabled) {
235 $status.addClass('atomicedge-ad-badge-enabled').text('Enabled');
236 } else {
237 $status.addClass('atomicedge-ad-badge-disabled').text('Disabled');
238 }
239
240 // Update threat level
241 var threatLevel = data.threat_level || 'low';
242 var $threatLevel = $('#atomicedge-ad-threat-level');
243 $threatLevel.html(this.formatThreatLevel(threatLevel));
244
245 // Update stats - IDs must match those in adaptive-defense-status-tab.php
246 var stats = data.stats || {};
247 $('#atomicedge-ad-stat-actors').text(stats.total_actors || 0);
248 $('#atomicedge-ad-stat-blocked').text(stats.blocked_ips || stats.blocked_count || 0);
249 $('#atomicedge-ad-stat-pending').text(stats.pending_detections || stats.pending_reviews || 0);
250 $('#atomicedge-ad-stat-high-risk').text(stats.high_threat_count || 0);
251
252 // Update AI budget in settings section
253 $('#atomicedge-ad-budget-used').text(stats.budget_used || stats.ai_budget_used || 0);
254 $('#atomicedge-ad-budget-total').text(stats.ai_budget_total || 0);
255
256 // Update settings display
257 var settings = data.settings || {};
258 $('#atomicedge-ad-mode').text(this.formatMode(settings.mode || 'monitor'));
259 $('#atomicedge-ad-sensitivity').text(this.formatSensitivity(settings.sensitivity || 'balanced'));
260
261 // High-risk actors preview
262 this.renderHighRiskActors(data.high_risk_actors || []);
263 },
264
265 /**
266 * Render high-risk actors preview
267 *
268 * @param {Array} actors High-risk actors list
269 */
270 renderHighRiskActors: function(actors) {
271 var $section = $('#atomicedge-ad-high-risk-section');
272 var $tbody = $('#atomicedge-ad-high-risk-body');
273 var $table = $('#atomicedge-ad-high-risk-table');
274
275 $tbody.empty();
276
277 if (!actors || actors.length === 0) {
278 $section.hide();
279 return;
280 }
281
282 // Show section and table when we have high-risk actors
283 $section.show();
284 $table.show();
285
286 var self = this;
287 actors.slice(0, 5).forEach(function(actor) {
288 var ipAddress = actor.ip_address || actor.ip || '';
289 var html = '<tr>';
290 html += '<td>' + self.escapeHtml(ipAddress) + '</td>';
291 html += '<td>' + self.formatScore(actor.threat_score || actor.score || 0) + '</td>';
292 html += '<td>' + (actor.requests || actor.total_requests || 0) + '</td>';
293 html += '<td>' + (actor.waf_hits || actor.total_waf_hits || 0) + '</td>';
294 html += '<td>';
295 html += '<button type="button" class="button button-small atomicedge-ad-block-actor-btn" data-ip="' + self.escapeHtml(ipAddress) + '" title="Block IP">';
296 html += '<span class="dashicons dashicons-shield" style="margin-top: 3px;"></span></button>';
297 html += '</td>';
298 html += '</tr>';
299 $tbody.append(html);
300 });
301 },
302
303 /**
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();
313
314 $.ajax({
315 url: atomicedge_admin.ajax_url,
316 type: 'POST',
317 data: {
318 action: 'atomicedge_get_adaptive_defense',
319 nonce: atomicedge_admin.nonce
320 },
321 success: function(response) {
322 $loading.hide();
323 $wrapper.show();
324
325 if (response.success && response.data && response.data.blocked_ips) {
326 self.renderBlockedIps(response.data.blocked_ips);
327 } else {
328 self.renderBlockedIps([]);
329 }
330 },
331 error: function(xhr, status, error) {
332 $loading.hide();
333 $wrapper.show();
334 self.showTableError('#atomicedge-ad-blocked-body', 'Network error: ' + error);
335 }
336 });
337 },
338
339 /**
340 * Render Blocked IPs table
341 *
342 * @param {Array} blockedIps List of blocked IPs
343 */
344 renderBlockedIps: function(blockedIps) {
345 var $tbody = $('#atomicedge-ad-blocked-body');
346 var $empty = $('#atomicedge-ad-blocked-empty');
347 var $table = $('#atomicedge-ad-blocked-table');
348
349 $tbody.empty();
350
351 if (!blockedIps || blockedIps.length === 0) {
352 $table.hide();
353 $empty.show();
354 return;
355 }
356
357 $table.show();
358 $empty.hide();
359
360 var self = this;
361 blockedIps.forEach(function(blocked) {
362 var ipAddress = blocked.ip_address || blocked.ip || '';
363 var html = '<tr>';
364 html += '<td>' + self.escapeHtml(ipAddress) + '</td>';
365 html += '<td>' + self.formatScore(blocked.score || 0) + '</td>';
366 html += '<td>' + (blocked.waf_hits || 0) + '</td>';
367 // Expires column
368 if (blocked.is_permanent) {
369 html += '<td>Never (Permanent)</td>';
370 } else if (blocked.expires) {
371 html += '<td>' + self.formatDate(blocked.expires) + '</td>';
372 } else {
373 html += '<td>&mdash;</td>';
374 }
375 html += '<td>';
376 html += '<button type="button" class="button button-small atomicedge-ad-unblock-btn" data-ip="' + self.escapeHtml(ipAddress) + '">';
377 html += '<span class="dashicons dashicons-unlock" style="margin-top: 3px;"></span> Unblock</button>';
378 html += '</td>';
379 html += '</tr>';
380 $tbody.append(html);
381 });
382 },
383
384 /**
385 * Block an IP address from the form
386 */
387 blockIpFromForm: function() {
388 var self = this;
389 var ip = $('#atomicedge-ad-block-ip').val().trim();
390 var durationValue = $('#atomicedge-ad-block-duration').val();
391
392 if (!ip) {
393 alert('Please enter an IP address');
394 return;
395 }
396
397 var permanent = durationValue === 'permanent';
398 var durationHours = permanent ? 24 : parseInt(durationValue, 10);
399
400 var $btn = $('#atomicedge-ad-block-btn');
401 $btn.prop('disabled', true).text('Blocking...');
402
403 $.ajax({
404 url: atomicedge_admin.ajax_url,
405 type: 'POST',
406 data: {
407 action: 'atomicedge_block_ip',
408 nonce: atomicedge_admin.nonce,
409 ip: ip,
410 duration_hours: durationHours,
411 permanent: permanent ? 'true' : 'false'
412 },
413 success: function(response) {
414 $btn.prop('disabled', false).html('<span class="dashicons dashicons-lock" style="margin-top: 3px;"></span> Block IP');
415
416 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('');
474 }
475 self.showNotice('IP address blocked successfully', 'success');
476 self.loadBlockedIps();
477
478 // Refresh other tabs if needed
479 if (source === 'actor') {
480 self.loadActorProfiles();
481 } else if (source === 'detection') {
482 self.loadThreatDetections();
483 }
484 } else {
485 self.showNotice(response.data ? response.data.message : 'Failed to block IP', 'error');
486 }
487 },
488 error: function(xhr, status, error) {
489 if (source === 'form') {
490 $('#atomicedge-ad-block-ip-submit').prop('disabled', false).text('Block IP');
491 }
492 self.showNotice('Network error: ' + error, 'error');
493 }
494 });
495 },
496
497 /**
498 * Unblock an IP address
499 *
500 * @param {string} ip IP address to unblock
501 */
502 unblockIp: function(ip) {
503 var self = this;
504
505 if (!confirm('Are you sure you want to unblock ' + ip + '?')) {
506 return;
507 }
508
509 $.ajax({
510 url: atomicedge_admin.ajax_url,
511 type: 'POST',
512 data: {
513 action: 'atomicedge_unblock_ip',
514 nonce: atomicedge_admin.nonce,
515 ip: ip
516 },
517 success: function(response) {
518 if (response.success) {
519 self.showNotice('IP address unblocked successfully', 'success');
520 self.loadBlockedIps();
521 } else {
522 self.showNotice(response.data ? response.data.message : 'Failed to unblock IP', 'error');
523 }
524 },
525 error: function(xhr, status, error) {
526 self.showNotice('Network error: ' + error, 'error');
527 }
528 });
529 },
530
531 /**
532 * Load Actor Profiles
533 */
534 loadActorProfiles: function() {
535 var self = this;
536 var $loading = $('#atomicedge-ad-actors-loading');
537 var $wrapper = $('#atomicedge-ad-actors-table-wrapper');
538 var filter = $('#atomicedge-ad-actors-filter').val();
539 var search = $('#atomicedge-ad-actors-search').val().trim();
540
541 $loading.show();
542 $wrapper.hide();
543
544 $.ajax({
545 url: atomicedge_admin.ajax_url,
546 type: 'POST',
547 data: {
548 action: 'atomicedge_get_actor_profiles',
549 nonce: atomicedge_admin.nonce,
550 filter: filter,
551 search: search,
552 page: self.pages.actors,
553 per_page: self.perPage
554 },
555 success: function(response) {
556 $loading.hide();
557 $wrapper.show();
558
559 if (response.success && response.data) {
560 self.renderActorProfiles(response.data.actors || response.data);
561 self.renderPagination('actors', response.data.pagination);
562 } else {
563 self.renderActorProfiles([]);
564 }
565 },
566 error: function(xhr, status, error) {
567 $loading.hide();
568 $wrapper.show();
569 self.showTableError('#atomicedge-ad-actors-body', 'Network error: ' + error);
570 }
571 });
572 },
573
574 /**
575 * Render Actor Profiles table
576 *
577 * @param {Array} actors List of actor profiles
578 */
579 renderActorProfiles: function(actors) {
580 var $tbody = $('#atomicedge-ad-actors-body');
581 var $empty = $('#atomicedge-ad-actors-empty');
582 var $table = $('#atomicedge-ad-actors-table');
583
584 $tbody.empty();
585
586 if (!actors || actors.length === 0) {
587 $table.hide();
588 $empty.show();
589 return;
590 }
591
592 $table.show();
593 $empty.hide();
594
595 var self = this;
596 actors.forEach(function(actor) {
597 var score = actor.score || actor.threat_score || 0;
598 var ipAddress = actor.ip_address || actor.ip || '';
599 var html = '<tr>';
600 html += '<td>' + self.escapeHtml(ipAddress) + '</td>';
601 html += '<td>' + self.formatScore(score) + '</td>';
602 html += '<td>' + (actor.total_requests || actor.requests || 0) + '</td>';
603 html += '<td>' + (actor.waf_hits || 0) + '</td>';
604 html += '<td>' + (actor.error_4xx || 0) + '/' + (actor.error_5xx || 0) + '</td>';
605 // Status column
606 html += '<td>';
607 if (actor.is_blocked) {
608 html += '<span class="atomicedge-ad-status-blocked">Blocked</span>';
609 } else if (score >= 70) {
610 html += '<span class="atomicedge-ad-status-high-risk">High Risk</span>';
611 } else {
612 html += '<span class="atomicedge-ad-status-normal">&mdash;</span>';
613 }
614 html += '</td>';
615 html += '<td>' + self.formatDate(actor.last_seen_at || actor.last_seen || actor.updated_at) + '</td>';
616 html += '<td>';
617 if (!actor.is_blocked) {
618 html += '<button type="button" class="button button-small atomicedge-ad-block-actor-btn" data-ip="' + self.escapeHtml(ipAddress) + '" title="Block this IP">';
619 html += '<span class="dashicons dashicons-shield" style="margin-top: 3px;"></span></button> ';
620 }
621 html += '<button type="button" class="button button-small atomicedge-ad-delete-actor-btn" data-id="' + actor.id + '" data-ip="' + self.escapeHtml(ipAddress) + '" title="Delete actor profile">';
622 html += '<span class="dashicons dashicons-trash" style="margin-top: 3px;"></span></button>';
623 html += '</td>';
624 html += '</tr>';
625 $tbody.append(html);
626 });
627 },
628
629 /**
630 * Delete an actor profile
631 *
632 * @param {number} id Actor ID
633 * @param {string} ip Actor IP address
634 */
635 deleteActor: function(id, ip) {
636 var self = this;
637
638 if (!confirm('Are you sure you want to delete the actor profile for ' + ip + '? This will also delete associated threat detections.')) {
639 return;
640 }
641
642 $.ajax({
643 url: atomicedge_admin.ajax_url,
644 type: 'POST',
645 data: {
646 action: 'atomicedge_delete_actor',
647 nonce: atomicedge_admin.nonce,
648 actor_id: id
649 },
650 success: function(response) {
651 if (response.success) {
652 self.showNotice('Actor profile deleted successfully', 'success');
653 self.loadActorProfiles();
654 } else {
655 self.showNotice(response.data ? response.data.message : 'Failed to delete actor profile', 'error');
656 }
657 },
658 error: function(xhr, status, error) {
659 self.showNotice('Network error: ' + error, 'error');
660 }
661 });
662 },
663
664 /**
665 * Load Threat Detections
666 */
667 loadThreatDetections: function() {
668 var self = this;
669 var $loading = $('#atomicedge-ad-detections-loading');
670 var $wrapper = $('#atomicedge-ad-detections-table-wrapper');
671 var status = $('#atomicedge-ad-detections-status').val();
672
673 $loading.show();
674 $wrapper.hide();
675
676 $.ajax({
677 url: atomicedge_admin.ajax_url,
678 type: 'POST',
679 data: {
680 action: 'atomicedge_get_threat_detections',
681 nonce: atomicedge_admin.nonce,
682 status: status !== 'all' ? status : '',
683 page: self.pages.detections,
684 per_page: self.perPage
685 },
686 success: function(response) {
687 $loading.hide();
688 $wrapper.show();
689
690 if (response.success && response.data) {
691 self.renderThreatDetections(response.data.detections || response.data);
692 self.renderPagination('detections', response.data.pagination);
693 } else {
694 self.renderThreatDetections([]);
695 }
696 },
697 error: function(xhr, status, error) {
698 $loading.hide();
699 $wrapper.show();
700 self.showTableError('#atomicedge-ad-detections-body', 'Network error: ' + error);
701 }
702 });
703 },
704
705 /**
706 * Render Threat Detections table
707 *
708 * @param {Array} detections List of threat detections
709 */
710 renderThreatDetections: function(detections) {
711 var $tbody = $('#atomicedge-ad-detections-body');
712 var $empty = $('#atomicedge-ad-detections-empty');
713 var $table = $('#atomicedge-ad-detections-table');
714
715 $tbody.empty();
716
717 if (!detections || detections.length === 0) {
718 $table.hide();
719 $empty.show();
720 return;
721 }
722
723 $table.show();
724 $empty.hide();
725
726 var self = this;
727 detections.forEach(function(detection) {
728 var ipAddress = detection.ip_address || detection.ip || (detection.actor && detection.actor.ip_address) || 'N/A';
729 var html = '<tr data-detection-id="' + detection.id + '">';
730 html += '<td>' + self.escapeHtml(ipAddress) + '</td>';
731 html += '<td>' + self.formatScore(detection.score || 0) + '</td>';
732 html += '<td>' + self.formatThreatLevel(detection.threat_level || 'low') + '</td>';
733 html += '<td>' + self.formatIndicators(detection.reasons || detection.reasons_summary || detection.key_indicators || []) + '</td>';
734 html += '<td>' + self.formatDetectionStatus(detection.status || 'pending') + '</td>';
735 html += '<td>' + self.formatDate(detection.created_at || detection.detected_at) + '</td>';
736 html += '<td>';
737 html += '<button type="button" class="button button-small atomicedge-ad-view-detection-btn" data-id="' + detection.id + '" title="View details">';
738 html += '<span class="dashicons dashicons-visibility" style="margin-top: 3px;"></span></button> ';
739 if (detection.status !== 'blocked') {
740 html += '<button type="button" class="button button-small atomicedge-ad-block-detection-btn" data-ip="' + self.escapeHtml(ipAddress) + '" title="Block IP">';
741 html += '<span class="dashicons dashicons-shield" style="margin-top: 3px;"></span></button> ';
742 }
743 if (detection.status === 'pending' || detection.status === 'pending_review') {
744 html += '<button type="button" class="button button-small atomicedge-ad-dismiss-btn" data-id="' + detection.id + '" title="Dismiss">';
745 html += '<span class="dashicons dashicons-dismiss" style="margin-top: 3px;"></span></button>';
746 }
747 html += '</td>';
748 html += '</tr>';
749 $tbody.append(html);
750 });
751 },
752
753 /**
754 * Toggle detection detail view (inline expandable row)
755 *
756 * @param {number} id Detection ID
757 * @param {jQuery} $row Table row element
758 */
759 toggleDetectionDetail: function(id, $row) {
760 var self = this;
761 var $existingDetail = $row.next('.atomicedge-ad-detail-row');
762
763 // If detail row already exists, toggle it
764 if ($existingDetail.length) {
765 $existingDetail.remove();
766 return;
767 }
768
769 // Remove any other open detail rows
770 $('.atomicedge-ad-detail-row').remove();
771
772 // Clone the detail template and insert it
773 var $template = $('#atomicedge-ad-detection-detail-template').find('tr').clone();
774 $row.after($template);
775
776 // Load detail data
777 $.ajax({
778 url: atomicedge_admin.ajax_url,
779 type: 'POST',
780 data: {
781 action: 'atomicedge_get_threat_detection_detail',
782 nonce: atomicedge_admin.nonce,
783 detection_id: id
784 },
785 success: function(response) {
786 var $detailRow = $row.next('.atomicedge-ad-detail-row');
787 $detailRow.find('.atomicedge-ad-detail-loading').hide();
788
789 if (response.success && response.data) {
790 self.renderDetectionDetail($detailRow, response.data);
791 $detailRow.find('.atomicedge-ad-detail-content').show();
792 } else {
793 $detailRow.find('.atomicedge-ad-detail-content').html(
794 '<p style="color: #d63638;">Failed to load detection details.</p>'
795 ).show();
796 }
797 },
798 error: function(xhr, status, error) {
799 var $detailRow = $row.next('.atomicedge-ad-detail-row');
800 $detailRow.find('.atomicedge-ad-detail-loading').hide();
801 $detailRow.find('.atomicedge-ad-detail-content').html(
802 '<p style="color: #d63638;">Network error: ' + self.escapeHtml(error) + '</p>'
803 ).show();
804 }
805 });
806 },
807
808 /**
809 * Render detection detail in the expanded row
810 *
811 * @param {jQuery} $detailRow The detail row element
812 * @param {Object} data Detection detail data
813 */
814 renderDetectionDetail: function($detailRow, data) {
815 var detection = data.detection || data;
816 var actor = detection.actor || data.actor || {};
817
818 // Detection details
819 $detailRow.find('.atomicedge-ad-detail-score').html(this.formatScore(detection.score || 0));
820 $detailRow.find('.atomicedge-ad-detail-confidence').text((detection.confidence || 0) + '%');
821 $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));
823
824 // Actor details
825 $detailRow.find('.atomicedge-ad-detail-ip').text(actor.ip_address || detection.ip_address || 'N/A');
826 $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));
831
832 // Reasons
833 var $reasons = $detailRow.find('.atomicedge-ad-detail-reasons');
834 $reasons.empty();
835
836 var reasons = detection.reasons || detection.key_indicators || [];
837 if (reasons.length > 0) {
838 var self = this;
839 reasons.forEach(function(reason) {
840 if (typeof reason === 'string') {
841 $reasons.append('<li>' + self.escapeHtml(reason) + '</li>');
842 } else if (reason.indicator && reason.value) {
843 $reasons.append('<li><strong>' + self.escapeHtml(reason.indicator) + ':</strong> ' + self.escapeHtml(reason.value) + '</li>');
844 }
845 });
846 } else {
847 $reasons.append('<li>No specific indicators recorded</li>');
848 }
849
850 // AI Analysis
851 var $aiSection = $detailRow.find('.atomicedge-ad-detail-ai-section');
852 if (detection.ai_analysis || detection.ai_response) {
853 $aiSection.find('.atomicedge-ad-detail-ai-content').text(detection.ai_analysis || detection.ai_response);
854 $aiSection.show();
855 } else {
856 $aiSection.hide();
857 }
858 },
859
860 /**
861 * Dismiss a threat detection
862 *
863 * @param {number} id Detection ID
864 */
865 dismissDetection: function(id) {
866 var self = this;
867
868 if (!confirm('Are you sure you want to dismiss this detection?')) {
869 return;
870 }
871
872 $.ajax({
873 url: atomicedge_admin.ajax_url,
874 type: 'POST',
875 data: {
876 action: 'atomicedge_dismiss_detection',
877 nonce: atomicedge_admin.nonce,
878 detection_id: id
879 },
880 success: function(response) {
881 if (response.success) {
882 self.showNotice('Detection dismissed successfully', 'success');
883 self.loadThreatDetections();
884 } else {
885 self.showNotice(response.data ? response.data.message : 'Failed to dismiss detection', 'error');
886 }
887 },
888 error: function(xhr, status, error) {
889 self.showNotice('Network error: ' + error, 'error');
890 }
891 });
892 },
893
894 /**
895 * Render pagination controls
896 *
897 * @param {string} section Section identifier
898 * @param {Object} pagination Pagination data
899 */
900 renderPagination: function(section, pagination) {
901 var $container = $('#atomicedge-ad-' + section + '-pagination');
902 $container.empty();
903
904 if (!pagination || pagination.last_page <= 1) {
905 return;
906 }
907
908 var html = '<div class="tablenav-pages">';
909 html += '<span class="displaying-num">' + pagination.total + ' items</span>';
910 html += '<span class="pagination-links">';
911
912 // Previous
913 if (pagination.current_page > 1) {
914 html += '<button class="button atomicedge-ad-page-btn" data-section="' + section + '" data-page="' + (pagination.current_page - 1) + '">&laquo; Previous</button> ';
915 }
916
917 // Page indicator
918 html += '<span class="paging-input">' + pagination.current_page + ' of ' + pagination.last_page + '</span>';
919
920 // Next
921 if (pagination.current_page < pagination.last_page) {
922 html += ' <button class="button atomicedge-ad-page-btn" data-section="' + section + '" data-page="' + (pagination.current_page + 1) + '">Next &raquo;</button>';
923 }
924
925 html += '</span></div>';
926 $container.html(html);
927 },
928
929 /* ============================
930 * Formatting Helpers
931 * ============================ */
932
933 /**
934 * Format score as colored badge
935 *
936 * @param {number} score Threat score
937 * @return {string} HTML string
938 */
939 formatScore: function(score) {
940 var className = 'atomicedge-ad-score-low';
941 if (score >= 80) {
942 className = 'atomicedge-ad-score-critical';
943 } else if (score >= 60) {
944 className = 'atomicedge-ad-score-high';
945 } else if (score >= 40) {
946 className = 'atomicedge-ad-score-medium';
947 }
948 return '<span class="atomicedge-ad-score ' + className + '">' + score + '</span>';
949 },
950
951 /**
952 * Format threat level badge
953 *
954 * @param {string} level Threat level
955 * @return {string} HTML string
956 */
957 formatThreatLevel: function(level) {
958 var labels = {
959 'critical': 'Critical',
960 'high': 'High',
961 'medium': 'Medium',
962 'low': 'Low',
963 'minimal': 'Minimal'
964 };
965 return '<span class="atomicedge-ad-threat-level atomicedge-ad-threat-' + level + '">' +
966 (labels[level] || level) + '</span>';
967 },
968
969 /**
970 * Format detection status badge
971 *
972 * @param {string} status Detection status
973 * @return {string} HTML string
974 */
975 formatDetectionStatus: function(status) {
976 var labels = {
977 'pending': 'Pending',
978 'blocked': 'Blocked',
979 'dismissed': 'Dismissed'
980 };
981 return '<span class="atomicedge-ad-status-badge atomicedge-ad-status-' + status + '">' +
982 (labels[status] || status) + '</span>';
983 },
984
985 /**
986 * Format key indicators (truncated list)
987 *
988 * @param {Array} indicators List of indicators
989 * @return {string} HTML string
990 */
991 formatIndicators: function(indicators) {
992 if (!indicators || !Array.isArray(indicators) || indicators.length === 0) {
993 return '<span style="color: #646970;">—</span>';
994 }
995
996 var display = [];
997 var self = this;
998
999 // Filter out null/undefined values first
1000 var validIndicators = indicators.filter(function(ind) {
1001 return ind !== null && ind !== undefined;
1002 });
1003
1004 if (validIndicators.length === 0) {
1005 return '<span style="color: #646970;">—</span>';
1006 }
1007
1008 validIndicators.slice(0, 2).forEach(function(ind) {
1009 if (typeof ind === 'string') {
1010 display.push(self.escapeHtml(ind));
1011 } else if (ind && ind.indicator) {
1012 display.push(self.escapeHtml(ind.indicator));
1013 } else if (ind && ind.reason) {
1014 display.push(self.escapeHtml(ind.reason));
1015 }
1016 });
1017
1018 var html = display.join(', ');
1019 if (validIndicators.length > 2) {
1020 html += ' <span style="color: #646970;">+' + (validIndicators.length - 2) + ' more</span>';
1021 }
1022 return html || '<span style="color: #646970;">—</span>';
1023 },
1024
1025 /**
1026 * Format operating mode
1027 *
1028 * @param {string} mode Mode value
1029 * @return {string} Formatted mode
1030 */
1031 formatMode: function(mode) {
1032 var modes = {
1033 'monitor': 'Monitor Only',
1034 'auto_enforce': 'Auto Enforce'
1035 };
1036 return modes[mode] || mode;
1037 },
1038
1039 /**
1040 * Format sensitivity level
1041 *
1042 * @param {string} sensitivity Sensitivity value
1043 * @return {string} Formatted sensitivity
1044 */
1045 formatSensitivity: function(sensitivity) {
1046 var levels = {
1047 'low': 'Low',
1048 'balanced': 'Balanced',
1049 'high': 'High',
1050 'aggressive': 'Aggressive'
1051 };
1052 return levels[sensitivity] || sensitivity;
1053 },
1054
1055 /**
1056 * Format duration in hours
1057 *
1058 * @param {number} hours Hours
1059 * @return {string} Formatted duration
1060 */
1061 formatDuration: function(hours) {
1062 if (hours >= 24) {
1063 var days = Math.floor(hours / 24);
1064 return days + ' day' + (days > 1 ? 's' : '');
1065 }
1066 return hours + ' hour' + (hours > 1 ? 's' : '');
1067 },
1068
1069 /**
1070 * Format date string
1071 *
1072 * @param {string} dateString ISO date string
1073 * @return {string} Formatted date
1074 */
1075 formatDate: function(dateString) {
1076 if (!dateString) {
1077 return '';
1078 }
1079 try {
1080 var date = new Date(dateString);
1081 return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
1082 } catch (e) {
1083 return dateString;
1084 }
1085 },
1086
1087 /* ============================
1088 * Utility Helpers
1089 * ============================ */
1090
1091 /**
1092 * Escape HTML to prevent XSS
1093 *
1094 * @param {string} str String to escape
1095 * @return {string} Escaped string
1096 */
1097 escapeHtml: function(str) {
1098 if (!str) {
1099 return '';
1100 }
1101 var div = document.createElement('div');
1102 div.textContent = str;
1103 return div.innerHTML;
1104 },
1105
1106 /**
1107 * Show an error message in a table body
1108 *
1109 * @param {string} selector Table body selector
1110 * @param {string} message Error message
1111 */
1112 showTableError: function(selector, message) {
1113 $(selector).html(
1114 '<tr><td colspan="7" style="text-align: center; color: #d63638; padding: 20px;">' +
1115 '<span class="dashicons dashicons-warning"></span> ' + this.escapeHtml(message) +
1116 '</td></tr>'
1117 );
1118 },
1119
1120 /**
1121 * Show an error in a content area
1122 *
1123 * @param {jQuery} $container Container element
1124 * @param {string} message Error message
1125 */
1126 showError: function($container, message) {
1127 $container.html(
1128 '<div class="notice notice-error" style="margin: 15px 0;">' +
1129 '<p><span class="dashicons dashicons-warning"></span> ' + this.escapeHtml(message) + '</p>' +
1130 '</div>'
1131 );
1132 },
1133
1134 /**
1135 * Show a temporary admin notice
1136 *
1137 * @param {string} message Notice message
1138 * @param {string} type Notice type (success, error, warning, info)
1139 */
1140 showNotice: function(message, type) {
1141 type = type || 'info';
1142 var $notice = $(
1143 '<div class="notice notice-' + type + ' is-dismissible atomicedge-ad-notice">' +
1144 '<p>' + this.escapeHtml(message) + '</p>' +
1145 '<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>' +
1146 '</div>'
1147 );
1148
1149 // Insert at top of page
1150 $('.wrap h1').first().after($notice);
1151
1152 // Bind dismiss handler
1153 $notice.find('.notice-dismiss').on('click', function() {
1154 $notice.fadeOut(200, function() { $(this).remove(); });
1155 });
1156
1157 // Auto dismiss after 5 seconds
1158 setTimeout(function() {
1159 $notice.fadeOut(200, function() { $(this).remove(); });
1160 }, 5000);
1161 }
1162 };
1163
1164 // Initialize on document ready
1165 $(document).ready(function() {
1166 AtomicEdgeAD.init();
1167 });
1168
1169 })(jQuery);
1170