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 / admin.js

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

1,300 lines 47.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * AtomicEdge Admin JavaScript
3 *
4 * @package AtomicEdge
5 */
6
7 /* eslint-env browser */
8 /* global atomicedgeAdmin, Chart, jQuery, document, setTimeout, clearTimeout, setInterval, clearInterval, confirm, alert, location */
9
10 (function($) {
11 'use strict';
12
13 /**
14 * AtomicEdge Admin Module
15 */
16 var AtomicEdge = {
17 /**
18 * Charts instances
19 */
20 charts: {},
21
22 /**
23 * Current state
24 */
25 state: {
26 analyticsPage: 1,
27 wafPage: 1,
28 wafPerPage: 50
29 },
30
31 /**
32 * Initialize
33 */
34 init: function() {
35 this.bindEvents();
36 this.initTabs();
37 this.initDashboard();
38 this.initAnalytics();
39 this.initWafLogs();
40 this.initAccessControl();
41 this.initScanner();
42 this.initSettings();
43 },
44
45 /**
46 * Bind global events
47 */
48 bindEvents: function() {
49 // Clear cache button
50 $('#atomicedge-clear-cache').on('click', this.clearCache.bind(this));
51 },
52
53 /**
54 * Initialize tabs
55 */
56 initTabs: function() {
57 $('.atomicedge-tabs .nav-tab').on('click', function(e) {
58 e.preventDefault();
59 var tab = $(this).data('tab');
60
61 // Update active tab
62 $('.atomicedge-tabs .nav-tab').removeClass('nav-tab-active');
63 $(this).addClass('nav-tab-active');
64
65 // Show tab content
66 $('.atomicedge-tab-content').removeClass('atomicedge-tab-active');
67 $('#' + tab).addClass('atomicedge-tab-active');
68 });
69 },
70
71 /**
72 * Initialize dashboard
73 */
74 initDashboard: function() {
75 if ($('#atomicedge-summary-widget').length === 0) {
76 return;
77 }
78
79 if (atomicedgeAdmin.connected) {
80 this.loadDashboardSummary();
81 }
82 },
83
84 /**
85 * Load dashboard summary
86 */
87 loadDashboardSummary: function() {
88 var self = this;
89
90 this.ajax('atomicedge_get_analytics', { period: '24h' }, function(data) {
91 var $widget = $('#atomicedge-summary-widget .atomicedge-widget-content');
92 $widget.removeClass('atomicedge-loading');
93
94 if (data.total_requests !== undefined) {
95 $widget.html(
96 '<div class="atomicedge-summary-stats">' +
97 '<p><strong>' + atomicedgeAdmin.strings.loading.replace('Loading...', 'Total Requests:') + '</strong> ' + self.formatNumber(data.total_requests) + '</p>' +
98 '<p><strong>Blocked:</strong> ' + self.formatNumber(data.requests_blocked || 0) + '</p>' +
99 '</div>'
100 );
101
102 // Initialize charts
103 if (data.hourly_data) {
104 self.initDashboardCharts(data.hourly_data);
105 }
106 } else {
107 $widget.html('<p class="atomicedge-error">' + atomicedgeAdmin.strings.error + '</p>');
108 }
109 }, function(errorData) {
110 $('#atomicedge-summary-widget .atomicedge-widget-content')
111 .removeClass('atomicedge-loading')
112 .html('<p class="atomicedge-error">' + self.escapeHtml(errorData && errorData.message ? errorData.message : atomicedgeAdmin.strings.error) + '</p>');
113 });
114 },
115
116 /**
117 * Initialize dashboard charts
118 */
119 initDashboardCharts: function(data) {
120 var labels = [];
121 var requests = [];
122 var blocked = [];
123
124 data.forEach(function(item) {
125 labels.push(new Date(item.hour).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
126 requests.push(item.requests || 0);
127 blocked.push(item.blocked || 0);
128 });
129
130 // Traffic chart
131 var trafficCtx = document.getElementById('atomicedge-traffic-chart');
132 if (trafficCtx) {
133 this.charts.traffic = new Chart(trafficCtx, {
134 type: 'line',
135 data: {
136 labels: labels,
137 datasets: [{
138 label: 'Requests',
139 data: requests,
140 borderColor: '#2271b1',
141 backgroundColor: 'rgba(34, 113, 177, 0.1)',
142 fill: true,
143 tension: 0.3
144 }]
145 },
146 options: this.getChartOptions()
147 });
148 }
149
150 // Attacks chart
151 var attacksCtx = document.getElementById('atomicedge-attacks-chart');
152 if (attacksCtx) {
153 this.charts.attacks = new Chart(attacksCtx, {
154 type: 'line',
155 data: {
156 labels: labels,
157 datasets: [{
158 label: 'Blocked',
159 data: blocked,
160 borderColor: '#d63638',
161 backgroundColor: 'rgba(214, 54, 56, 0.1)',
162 fill: true,
163 tension: 0.3
164 }]
165 },
166 options: this.getChartOptions()
167 });
168 }
169 },
170
171 /**
172 * Initialize analytics page
173 */
174 initAnalytics: function() {
175 var self = this;
176
177 if ($('#atomicedge-period').length === 0) {
178 return;
179 }
180
181 // Period change
182 $('#atomicedge-period').on('change', function() {
183 self.loadAnalytics($(this).val());
184 });
185
186 // Refresh button
187 $('#atomicedge-refresh-analytics').on('click', function() {
188 self.loadAnalytics($('#atomicedge-period').val());
189 });
190
191 // Initial load
192 this.loadAnalytics('24h');
193 },
194
195 /**
196 * Load analytics data
197 */
198 loadAnalytics: function(period) {
199 var self = this;
200
201 $('#atomicedge-analytics-loading').show();
202 $('#atomicedge-analytics-error').hide();
203
204 this.ajax('atomicedge_get_analytics', { period: period }, function(data) {
205 $('#atomicedge-analytics-loading').hide();
206 self.updateAnalyticsStats(data);
207 self.updateAnalyticsCharts(data.hourly_data || []);
208 }, function(errorData) {
209 $('#atomicedge-analytics-loading').hide();
210 if (errorData && errorData.message) {
211 $('#atomicedge-analytics-error').find('span').last().text(errorData.message);
212 }
213 $('#atomicedge-analytics-error').show();
214 });
215 },
216
217 /**
218 * Update analytics stats
219 */
220 updateAnalyticsStats: function(data) {
221 $('#stat-total-requests').text(this.formatNumber(data.total_requests || 0));
222 $('#stat-unique-visitors').text(this.formatNumber(data.unique_visitors || 0));
223 $('#stat-blocked-requests').text(this.formatNumber(data.requests_blocked || 0));
224
225 var blockRate = data.total_requests > 0
226 ? ((data.requests_blocked / data.total_requests) * 100).toFixed(1) + '%'
227 : '0%';
228 $('#stat-block-rate').text(blockRate);
229 },
230
231 /**
232 * Update analytics charts
233 */
234 updateAnalyticsCharts: function(data) {
235 var labels = [];
236 var requests = [];
237 var blocked = [];
238
239 data.forEach(function(item) {
240 labels.push(new Date(item.hour).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
241 requests.push(item.requests || 0);
242 blocked.push(item.blocked || 0);
243 });
244
245 // Destroy existing charts
246 if (this.charts.analyticsTraffic) {
247 this.charts.analyticsTraffic.destroy();
248 }
249 if (this.charts.analyticsBlocked) {
250 this.charts.analyticsBlocked.destroy();
251 }
252
253 // Traffic chart
254 var trafficCtx = document.getElementById('atomicedge-traffic-chart');
255 if (trafficCtx) {
256 this.charts.analyticsTraffic = new Chart(trafficCtx, {
257 type: 'line',
258 data: {
259 labels: labels,
260 datasets: [{
261 label: 'Requests',
262 data: requests,
263 borderColor: '#2271b1',
264 backgroundColor: 'rgba(34, 113, 177, 0.1)',
265 fill: true,
266 tension: 0.3
267 }]
268 },
269 options: this.getChartOptions()
270 });
271 }
272
273 // Blocked chart
274 var blockedCtx = document.getElementById('atomicedge-blocked-chart');
275 if (blockedCtx) {
276 this.charts.analyticsBlocked = new Chart(blockedCtx, {
277 type: 'line',
278 data: {
279 labels: labels,
280 datasets: [{
281 label: 'Blocked',
282 data: blocked,
283 borderColor: '#d63638',
284 backgroundColor: 'rgba(214, 54, 56, 0.1)',
285 fill: true,
286 tension: 0.3
287 }]
288 },
289 options: this.getChartOptions()
290 });
291 }
292 },
293
294 /**
295 * Initialize WAF logs page
296 */
297 initWafLogs: function() {
298 var self = this;
299
300 if ($('#atomicedge-waf-table').length === 0) {
301 return;
302 }
303
304 // Search
305 var searchTimeout;
306 $('#atomicedge-waf-search').on('input', function() {
307 clearTimeout(searchTimeout);
308 searchTimeout = setTimeout(function() {
309 self.state.wafPage = 1;
310 self.loadWafLogs();
311 }, 500);
312 });
313
314 // Per page change
315 $('#atomicedge-waf-per-page').on('change', function() {
316 self.state.wafPerPage = parseInt($(this).val(), 10);
317 self.state.wafPage = 1;
318 self.loadWafLogs();
319 });
320
321 // Refresh
322 $('#atomicedge-waf-refresh').on('click', function() {
323 self.loadWafLogs();
324 });
325
326 // Pagination
327 $('#atomicedge-waf-prev').on('click', function() {
328 if (self.state.wafPage > 1) {
329 self.state.wafPage--;
330 self.loadWafLogs();
331 }
332 });
333
334 $('#atomicedge-waf-next').on('click', function() {
335 self.state.wafPage++;
336 self.loadWafLogs();
337 });
338
339 // Initial load
340 this.loadWafLogs();
341 },
342
343 /**
344 * Load WAF logs
345 */
346 loadWafLogs: function() {
347 var self = this;
348 var $tbody = $('#atomicedge-waf-logs-body');
349
350 $tbody.html('<tr class="atomicedge-loading-row"><td colspan="6"><span class="spinner is-active"></span> ' + atomicedgeAdmin.strings.loading + '</td></tr>');
351 $('#atomicedge-waf-no-results').hide();
352 $('#atomicedge-waf-error').hide();
353
354 this.ajax('atomicedge_get_waf_logs', {
355 page: this.state.wafPage,
356 per_page: this.state.wafPerPage,
357 search: $('#atomicedge-waf-search').val() || ''
358 }, function(data) {
359 self.renderWafLogs(data);
360 }, function() {
361 $tbody.empty();
362 $('#atomicedge-waf-error').show();
363 });
364 },
365
366 /**
367 * Render WAF logs table
368 */
369 renderWafLogs: function(data) {
370 var $tbody = $('#atomicedge-waf-logs-body');
371 $tbody.empty();
372
373 var logs = data.logs || [];
374
375 if (logs.length === 0) {
376 $('#atomicedge-waf-no-results').show();
377 return;
378 }
379
380 var self = this;
381 logs.forEach(function(log) {
382 var row = '<tr>' +
383 '<td>' + self.escapeHtml(log.event_timestamp || '') + '</td>' +
384 '<td><code>' + self.escapeHtml(log.client_ip || '') + '</code></td>' +
385 '<td>' + self.escapeHtml(log.uri || '').substring(0, 50) + '</td>' +
386 '<td><code>' + self.escapeHtml(log.waf_rule_id || '') + '</code></td>' +
387 '<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>' +
389 '</tr>';
390 $tbody.append(row);
391 });
392
393 // Bind block IP buttons
394 $tbody.find('.atomicedge-block-ip').on('click', function() {
395 var ip = $(this).data('ip');
396 if (confirm(atomicedgeAdmin.strings.confirm)) {
397 self.addIpBlacklist(ip, 'Blocked from WAF logs');
398 }
399 });
400
401 // Update pagination
402 $('#atomicedge-waf-page-info').text('Page ' + this.state.wafPage);
403 $('#atomicedge-waf-prev').prop('disabled', this.state.wafPage <= 1);
404 $('#atomicedge-waf-next').prop('disabled', logs.length < this.state.wafPerPage);
405 },
406
407 /**
408 * Initialize access control page
409 */
410 initAccessControl: function() {
411 var self = this;
412
413 if ($('#atomicedge-whitelist-table').length === 0) {
414 return;
415 }
416
417 // Load IP rules
418 this.loadIpRules();
419
420 // Whitelist form
421 $('#atomicedge-add-whitelist-form').on('submit', function(e) {
422 e.preventDefault();
423 var ip = $('#whitelist-ip').val();
424 var desc = $('#whitelist-description').val();
425
426 if (!self.validateIp(ip)) {
427 alert(atomicedgeAdmin.strings.invalidIp);
428 return;
429 }
430
431 self.addIpWhitelist(ip, desc);
432 });
433
434 // Blacklist form
435 $('#atomicedge-add-blacklist-form').on('submit', function(e) {
436 e.preventDefault();
437 var ip = $('#blacklist-ip').val();
438 var desc = $('#blacklist-description').val();
439
440 if (!self.validateIp(ip)) {
441 alert(atomicedgeAdmin.strings.invalidIp);
442 return;
443 }
444
445 self.addIpBlacklist(ip, desc);
446 });
447
448 // Geo form
449 $('#geo-enabled').on('change', function() {
450 $('#geo-options').toggle($(this).is(':checked'));
451 });
452
453 // Load geo rules
454 this.loadGeoRules();
455
456 // Geo form submit
457 $('#atomicedge-geo-form').on('submit', function(e) {
458 e.preventDefault();
459 self.updateGeoRules();
460 });
461 },
462
463 /**
464 * Load IP rules
465 */
466 loadIpRules: function() {
467 var self = this;
468
469 this.ajax('atomicedge_get_ip_rules', {}, function(data) {
470 self.renderIpList('whitelist', data.whitelist || []);
471 self.renderIpList('blacklist', data.blacklist || []);
472 });
473 },
474
475 /**
476 * Render IP list
477 */
478 renderIpList: function(type, ips) {
479 var $tbody = $('#atomicedge-' + type + '-body');
480 $tbody.empty();
481
482 if (ips.length === 0) {
483 $tbody.html('<tr><td colspan="3">No IPs in ' + type + '</td></tr>');
484 return;
485 }
486
487 var self = this;
488 ips.forEach(function(item) {
489 var row = '<tr>' +
490 '<td><code>' + self.escapeHtml(item.ip) + '</code></td>' +
491 '<td>' + self.escapeHtml(item.description || '') + '</td>' +
492 '<td><button type="button" class="button button-small atomicedge-remove-ip" data-ip="' + self.escapeHtml(item.ip) + '" data-type="' + type + '">Remove</button></td>' +
493 '</tr>';
494 $tbody.append(row);
495 });
496
497 // Bind remove buttons
498 $tbody.find('.atomicedge-remove-ip').on('click', function() {
499 var ip = $(this).data('ip');
500 var ipType = $(this).data('type');
501 if (confirm(atomicedgeAdmin.strings.confirmIp)) {
502 self.removeIp(ip, ipType);
503 }
504 });
505 },
506
507 /**
508 * Add IP to whitelist
509 */
510 addIpWhitelist: function(ip, description) {
511 var self = this;
512 this.ajax('atomicedge_add_ip_whitelist', { ip: ip, description: description }, function() {
513 $('#whitelist-ip').val('');
514 $('#whitelist-description').val('');
515 self.loadIpRules();
516 });
517 },
518
519 /**
520 * Add IP to blacklist
521 */
522 addIpBlacklist: function(ip, description) {
523 var self = this;
524 this.ajax('atomicedge_add_ip_blacklist', { ip: ip, description: description }, function() {
525 $('#blacklist-ip').val('');
526 $('#blacklist-description').val('');
527 self.loadIpRules();
528 });
529 },
530
531 /**
532 * Remove IP
533 */
534 removeIp: function(ip, type) {
535 var self = this;
536 this.ajax('atomicedge_remove_ip', { ip: ip, type: type }, function() {
537 self.loadIpRules();
538 });
539 },
540
541 /**
542 * Load geo rules
543 */
544 loadGeoRules: function() {
545 // Populate countries list
546 this.populateCountries();
547
548 this.ajax('atomicedge_get_geo_rules', {}, function(data) {
549 $('#geo-enabled').prop('checked', data.enabled || false);
550 $('#geo-mode').val(data.mode || 'blacklist');
551
552 if (data.countries && data.countries.length) {
553 $('#geo-countries').val(data.countries);
554 }
555
556 $('#geo-options').toggle(data.enabled || false);
557 });
558 },
559
560 /**
561 * Populate countries dropdown
562 */
563 populateCountries: function() {
564 var countries = {
565 'AF': 'Afghanistan', 'AL': 'Albania', 'DZ': 'Algeria', 'AR': 'Argentina',
566 'AU': 'Australia', 'AT': 'Austria', 'BE': 'Belgium', 'BR': 'Brazil',
567 'CA': 'Canada', 'CN': 'China', 'CO': 'Colombia', 'CZ': 'Czech Republic',
568 'DK': 'Denmark', 'EG': 'Egypt', 'FI': 'Finland', 'FR': 'France',
569 'DE': 'Germany', 'GR': 'Greece', 'HK': 'Hong Kong', 'HU': 'Hungary',
570 'IN': 'India', 'ID': 'Indonesia', 'IR': 'Iran', 'IQ': 'Iraq',
571 'IE': 'Ireland', 'IL': 'Israel', 'IT': 'Italy', 'JP': 'Japan',
572 'KR': 'South Korea', 'KP': 'North Korea', 'MY': 'Malaysia', 'MX': 'Mexico',
573 'NL': 'Netherlands', 'NZ': 'New Zealand', 'NG': 'Nigeria', 'NO': 'Norway',
574 'PK': 'Pakistan', 'PH': 'Philippines', 'PL': 'Poland', 'PT': 'Portugal',
575 'RO': 'Romania', 'RU': 'Russia', 'SA': 'Saudi Arabia', 'SG': 'Singapore',
576 'ZA': 'South Africa', 'ES': 'Spain', 'SE': 'Sweden', 'CH': 'Switzerland',
577 'TW': 'Taiwan', 'TH': 'Thailand', 'TR': 'Turkey', 'UA': 'Ukraine',
578 'AE': 'United Arab Emirates', 'GB': 'United Kingdom', 'US': 'United States',
579 'VN': 'Vietnam'
580 };
581
582 var $select = $('#geo-countries');
583 $.each(countries, function(code, name) {
584 $select.append('<option value="' + code + '">' + name + ' (' + code + ')</option>');
585 });
586 },
587
588 /**
589 * Update geo rules
590 */
591 updateGeoRules: function() {
592 var data = {
593 enabled: $('#geo-enabled').is(':checked') ? 'true' : 'false',
594 mode: $('#geo-mode').val(),
595 countries: $('#geo-countries').val() || []
596 };
597
598 this.ajax('atomicedge_update_geo_rules', data, function() {
599 alert(atomicedgeAdmin.strings.success);
600 });
601 },
602
603 /**
604 * Initialize scanner page (malware scanner)
605 */
606 initScanner: function() {
607 var self = this;
608
609 // Initialize pagination for results tables (works on both scanner pages)
610 this.initScannerPagination();
611
612 // Malware scan button
613 if ($('#atomicedge-run-scan').length > 0) {
614 $('#atomicedge-run-scan').on('click', function() {
615 self.runScan();
616 });
617 }
618
619 if ($('#atomicedge-cancel-scan').length > 0) {
620 $('#atomicedge-cancel-scan').on('click', function() {
621 self.cancelScan();
622 });
623 }
624
625 if ($('#atomicedge-reset-scan').length > 0) {
626 $('#atomicedge-reset-scan').on('click', function() {
627 self.resetScan();
628 });
629 }
630
631 // Vulnerability scanner button (on separate page)
632 if ($('#atomicedge-run-vuln-scan').length > 0) {
633 $('#atomicedge-run-vuln-scan').on('click', function() {
634 self.runVulnerabilityScan();
635 });
636 }
637
638 if ($('#atomicedge-reset-vuln-results').length > 0) {
639 $('#atomicedge-reset-vuln-results').on('click', function() {
640 self.resetVulnerabilityResults();
641 });
642 }
643
644 if ($('.atomicedge-vuln-filter').length > 0) {
645 self.initVulnerabilitySeverityFilters();
646 }
647
648 // Debug test button (only present when WP_DEBUG is true)
649 if ($('#atomicedge-debug-test').length > 0) {
650 $('#atomicedge-debug-test').on('click', function() {
651 self.runDebugTest();
652 });
653 }
654 },
655
656 /**
657 * Run debug scan test (500 files) - only visible when WP_DEBUG is true.
658 */
659 runDebugTest: function() {
660 var self = this;
661 var $button = $('#atomicedge-debug-test');
662 var $results = $('#atomicedge-debug-results');
663 var $output = $('#atomicedge-debug-output');
664
665 if ($button.prop('disabled')) {
666 return;
667 }
668
669 $button.prop('disabled', true).text('Testing...');
670 $results.show();
671 $output.text('Running debug scan on 500 files...\n');
672
673 this.ajax('atomicedge_scan_debug_test', {}, function(data) {
674 $button.prop('disabled', false).text('Debug Test (500 files)');
675
676 // Format and display results
677 var output = '=== Debug Scan Results ===\n\n';
678 output += 'Files Found: ' + (data.files_found || 0) + '\n';
679 output += 'Files Scanned: ' + (data.files_scanned || 0) + '\n';
680 output += 'Quick Rejected: ' + (data.files_quick_rejected || 0) + '\n';
681 output += 'Regex Scanned: ' + (data.files_regex_scanned || 0) + '\n';
682 output += 'Quick Rejection Rate: ' + (data.quick_rejection_rate || 'N/A') + '\n\n';
683
684 if (data.timing) {
685 output += '--- Timing ---\n';
686 output += 'Enumeration: ' + (data.timing.enumeration_ms || 0) + ' ms\n';
687 output += 'Scanning: ' + (data.timing.scanning_ms || 0) + ' ms\n';
688 output += 'Total: ' + (data.timing.total_seconds || 0) + ' seconds\n';
689 }
690
691 if (data.files_per_second) {
692 output += 'Rate: ' + data.files_per_second + ' files/sec\n';
693 }
694
695 if (data.issues_found && data.issues_found.length > 0) {
696 output += '\n--- Issues Found: ' + data.issues_found.length + ' ---\n';
697 data.issues_found.slice(0, 10).forEach(function(issue) {
698 output += ' ' + self.escapeHtml(issue.file) + ' (' + self.escapeHtml(issue.type) + ')\n';
699 });
700 if (data.issues_found.length > 10) {
701 output += ' ... and ' + (data.issues_found.length - 10) + ' more\n';
702 }
703 } else {
704 output += '\nNo issues found.\n';
705 }
706
707 if (data.server_info) {
708 output += '\n--- Server Info ---\n';
709 output += 'PHP: ' + (data.server_info.php_version || 'N/A') + '\n';
710 output += 'Max Execution: ' + (data.server_info.max_execution_time || 'N/A') + 's\n';
711 output += 'Memory Limit: ' + (data.server_info.memory_limit || 'N/A') + '\n';
712 }
713
714 $output.text(output);
715 }, function(err) {
716 $button.prop('disabled', false).text('Debug Test (500 files)');
717 $output.text('Error: ' + self.escapeHtml((err && err.message) ? err.message : 'Unknown error'));
718 });
719 },
720
721 /**
722 * Initialize settings page
723 */
724 initSettings: function() {
725 // Settings page initialization
726 // WPScan token functionality removed - vulnerability scanning now uses AtomicEdge API
727 },
728
729 /**
730 * Run vulnerability scan
731 */
732 runVulnerabilityScan: function() {
733 var $button = $('#atomicedge-run-vuln-scan');
734 var $progress = $('#atomicedge-vuln-progress');
735
736 $button.prop('disabled', true);
737 $progress.show();
738
739 // Animate progress bar
740 var $progressFill = $progress.find('.atomicedge-progress-fill');
741 $progressFill.css('width', '0%');
742
743 var progress = 0;
744 var progressInterval = setInterval(function() {
745 progress = Math.min(progress + Math.random() * 8, 90);
746 $progressFill.css('width', progress + '%');
747 }, 600);
748
749 this.ajax('atomicedge_run_vulnerability_scan', { force_refresh: 'true' }, function() {
750 clearInterval(progressInterval);
751 $progressFill.css('width', '100%');
752
753 setTimeout(function() {
754 $progress.hide();
755 $button.prop('disabled', false);
756 // Reload page to show results
757 location.reload();
758 }, 500);
759 }, function(data) {
760 clearInterval(progressInterval);
761 $progress.hide();
762 $button.prop('disabled', false);
763
764 if (data && data.need_connection) {
765 alert('Please connect your site to AtomicEdge in the Settings page first.');
766 } else {
767 alert(data.message || atomicedgeAdmin.strings.error);
768 }
769 });
770 },
771
772 /**
773 * Reset vulnerability scan results (options + transient).
774 */
775 resetVulnerabilityResults: function() {
776 var $resetButton = $('#atomicedge-reset-vuln-results');
777
778 if ($resetButton.prop('disabled')) {
779 return;
780 }
781
782 if (!confirm('Reset vulnerability scan results? This will clear saved results.')) {
783 return;
784 }
785
786 $resetButton.prop('disabled', true);
787
788 this.ajax('atomicedge_reset_vulnerability_results', {}, function() {
789 location.reload();
790 }, function(err) {
791 $resetButton.prop('disabled', false);
792 alert((err && err.message) ? err.message : atomicedgeAdmin.strings.error);
793 });
794 },
795
796 /**
797 * Client-side severity filtering for vulnerability items.
798 */
799 initVulnerabilitySeverityFilters: function() {
800 var applyFilters = function() {
801 var allowed = {};
802 $('.atomicedge-vuln-filter:checked').each(function() {
803 allowed[$(this).val()] = true;
804 });
805
806 $('.atomicedge-vuln-item').each(function() {
807 var $item = $(this);
808 var match = false;
809
810 for (var sev in allowed) {
811 if (Object.prototype.hasOwnProperty.call(allowed, sev) && $item.hasClass('atomicedge-severity-' + sev)) {
812 match = true;
813 break;
814 }
815 }
816
817 $item.toggle(match);
818 });
819 };
820
821 $('.atomicedge-vuln-filter').on('change', function() {
822 applyFilters();
823 });
824
825 applyFilters();
826 },
827
828 /**
829 * Initialize pagination for scanner results tables
830 */
831 initScannerPagination: function() {
832 var self = this;
833
834 $('[data-paginate="true"]').each(function() {
835 var $section = $(this);
836 var $table = $section.find('.atomicedge-paginated-table');
837 var $pagination = $section.find('.atomicedge-pagination');
838 var perPage = parseInt($section.data('per-page'), 10) || 10;
839 var $rows = $table.find('tbody tr');
840 var totalRows = $rows.length;
841 var totalPages = Math.ceil(totalRows / perPage);
842
843 if (totalPages <= 1) {
844 return; // No pagination needed
845 }
846
847 // Store pagination state
848 $section.data('currentPage', 1);
849 $section.data('totalPages', totalPages);
850 $section.data('perPage', perPage);
851
852 // Build pagination UI
853 self.buildPaginationUI($section, $pagination, totalRows, perPage, totalPages);
854
855 // Show first page
856 self.showPage($section, 1);
857 });
858 },
859
860 /**
861 * Build pagination UI
862 */
863 buildPaginationUI: function($section, $pagination, totalRows, perPage, totalPages) {
864 var self = this;
865 var html = '<div class="atomicedge-pagination-info">';
866 html += 'Showing <span class="showing-start">1</span>-<span class="showing-end">' + Math.min(perPage, totalRows) + '</span> of ' + totalRows + ' items';
867 html += '</div>';
868 html += '<div class="atomicedge-pagination-buttons">';
869 html += '<button type="button" class="button pagination-prev" disabled>&laquo; Prev</button>';
870
871 for (var i = 1; i <= totalPages; i++) {
872 html += '<button type="button" class="button pagination-page' + (i === 1 ? ' current' : '') + '" data-page="' + i + '">' + i + '</button>';
873 }
874
875 html += '<button type="button" class="button pagination-next"' + (totalPages <= 1 ? ' disabled' : '') + '>Next &raquo;</button>';
876 html += '</div>';
877
878 $pagination.html(html);
879
880 // Bind events
881 $pagination.find('.pagination-prev').on('click', function() {
882 var currentPage = $section.data('currentPage');
883 if (currentPage > 1) {
884 self.showPage($section, currentPage - 1);
885 }
886 });
887
888 $pagination.find('.pagination-next').on('click', function() {
889 var currentPage = $section.data('currentPage');
890 var totalPages = $section.data('totalPages');
891 if (currentPage < totalPages) {
892 self.showPage($section, currentPage + 1);
893 }
894 });
895
896 $pagination.find('.pagination-page').on('click', function() {
897 var page = parseInt($(this).data('page'), 10);
898 self.showPage($section, page);
899 });
900 },
901
902 /**
903 * Show specific page of results
904 */
905 showPage: function($section, page) {
906 var $table = $section.find('.atomicedge-paginated-table');
907 var $pagination = $section.find('.atomicedge-pagination');
908 var perPage = $section.data('perPage');
909 var totalPages = $section.data('totalPages');
910 var $rows = $table.find('tbody tr');
911 var totalRows = $rows.length;
912
913 // Update current page
914 $section.data('currentPage', page);
915
916 // Show/hide rows
917 var startIndex = (page - 1) * perPage;
918 var endIndex = startIndex + perPage;
919
920 $rows.each(function(index) {
921 if (index >= startIndex && index < endIndex) {
922 $(this).removeClass('hidden-row');
923 } else {
924 $(this).addClass('hidden-row');
925 }
926 });
927
928 // Update pagination info
929 $pagination.find('.showing-start').text(startIndex + 1);
930 $pagination.find('.showing-end').text(Math.min(endIndex, totalRows));
931
932 // Update button states
933 $pagination.find('.pagination-prev').prop('disabled', page === 1);
934 $pagination.find('.pagination-next').prop('disabled', page === totalPages);
935 $pagination.find('.pagination-page').removeClass('current');
936 $pagination.find('.pagination-page[data-page="' + page + '"]').addClass('current');
937 },
938
939 /**
940 * Run malware scan
941 */
942 runScan: function() {
943 var self = this;
944 var $button = $('#atomicedge-run-scan');
945 var $cancelButton = $('#atomicedge-cancel-scan');
946 var $resetButton = $('#atomicedge-reset-scan');
947 var $mode = $('#atomicedge-scan-mode');
948 var $verifyIntegrity = $('#atomicedge-verify-integrity');
949 var $progress = $('#atomicedge-scan-progress');
950 var $results = $('#atomicedge-scan-results');
951 var $logSection = $('#atomicedge-scan-log');
952 var $logLines = $logSection.find('.atomicedge-scan-log-lines');
953 var $progressText = $progress.find('.atomicedge-progress-text');
954
955 var hasRealProgress = false;
956 var lastDisplayedProgress = 0;
957
958 if (!this.state.scan) {
959 this.state.scan = {};
960 }
961 this.state.scan.cancelled = false;
962 this.state.scan.runId = null;
963 this.state.scan.pollTimeout = null;
964 this.state.scan.progressInterval = null;
965
966 $button.prop('disabled', true);
967 $cancelButton.prop('disabled', false);
968 $resetButton.prop('disabled', true);
969 $progress.show();
970 $results.hide();
971 $logSection.show();
972 $logLines.text('');
973
974 // Animate progress bar until we get real progress values.
975 var $progressFill = $progress.find('.atomicedge-progress-fill');
976 $progressFill.css('width', '0%');
977
978 var progress = 0;
979 var progressInterval = setInterval(function() {
980 progress = Math.min(progress + Math.random() * 10, 90);
981 lastDisplayedProgress = Math.max(lastDisplayedProgress, progress);
982 $progressFill.css('width', lastDisplayedProgress + '%');
983 }, 500);
984
985 this.state.scan.progressInterval = progressInterval;
986
987 var runId = null;
988
989 var renderStatus = function(stepData) {
990 if (!stepData) {
991 return;
992 }
993
994 if (stepData.progress !== undefined) {
995 var p = parseInt(stepData.progress, 10);
996 if (!isNaN(p)) {
997 if (!hasRealProgress) {
998 hasRealProgress = true;
999 clearInterval(progressInterval);
1000 }
1001
1002 p = Math.min(Math.max(p, 0), 100);
1003 lastDisplayedProgress = Math.max(lastDisplayedProgress, p);
1004 $progressFill.css('width', lastDisplayedProgress + '%');
1005 }
1006 }
1007
1008 var stage = stepData.stage || '';
1009 var currentItem = stepData.current_item || null;
1010 var scanMode = stepData.scan_mode || '';
1011 var scanStats = stepData.results && stepData.results.scan_stats ? stepData.results.scan_stats : null;
1012
1013 var parts = [];
1014 if (stage) {
1015 parts.push('Stage: ' + stage);
1016 }
1017 if (scanMode) {
1018 parts.push('Mode: ' + scanMode);
1019 }
1020 if (scanStats && scanStats.files_total) {
1021 var scannedCount = parseInt(scanStats.files_scanned || 0, 10);
1022 var totalCount = parseInt(scanStats.files_total || 0, 10);
1023 var remainingCount = Math.max(0, totalCount - scannedCount);
1024 parts.push('Files: ' + scannedCount + '/' + totalCount);
1025 parts.push('Remaining: ' + remainingCount);
1026 }
1027 if (currentItem && currentItem.path) {
1028 parts.push('Now: ' + currentItem.path);
1029 }
1030 if (stepData.eta_label) {
1031 parts.push('ETA: ' + stepData.eta_label);
1032 } else if (scanStats && scanStats.files_total) {
1033 parts.push('ETA: calculating...');
1034 }
1035 if (parts.length) {
1036 $progressText.text(parts.join(' · '));
1037 }
1038
1039 if (stepData.log && Array.isArray(stepData.log) && stepData.log.length) {
1040 $logLines.text(stepData.log.join('\n'));
1041 var el = $logLines.get(0);
1042 if (el && el.scrollHeight !== undefined) {
1043 el.scrollTop = el.scrollHeight;
1044 }
1045 }
1046 };
1047
1048 var pollStep = function() {
1049 if (self.state.scan && self.state.scan.cancelled) {
1050 return;
1051 }
1052 self.ajax('atomicedge_scan_step', { run_id: runId || '' }, function(stepData) {
1053 renderStatus(stepData);
1054
1055 if (stepData && stepData.status === 'complete') {
1056 clearInterval(progressInterval);
1057 lastDisplayedProgress = 100;
1058 $progressFill.css('width', '100%');
1059 setTimeout(function() {
1060 $progress.hide();
1061 $logSection.hide();
1062 $button.prop('disabled', false);
1063 $cancelButton.prop('disabled', true);
1064 $resetButton.prop('disabled', false);
1065 location.reload();
1066 }, 500);
1067 return;
1068 }
1069
1070 // Adaptive polling: wait a fraction of the server's time budget.
1071 // For short budgets (5s), poll quickly (300ms).
1072 // For longer budgets (20s), poll less frequently (800ms).
1073 var timeBudget = stepData && stepData.time_budget ? parseInt(stepData.time_budget, 10) : 8;
1074 var pollDelay = Math.min(800, Math.max(300, timeBudget * 40));
1075 self.state.scan.pollTimeout = setTimeout(pollStep, pollDelay);
1076 }, function(err) {
1077 clearInterval(progressInterval);
1078 $progress.hide();
1079 $logSection.hide();
1080 $button.prop('disabled', false);
1081 $cancelButton.prop('disabled', true);
1082 $resetButton.prop('disabled', false);
1083 alert((err && err.message) ? err.message : atomicedgeAdmin.strings.error);
1084 });
1085 };
1086
1087 var selectedMode = ($mode.length ? String($mode.val() || '') : '');
1088 if (selectedMode !== 'php' && selectedMode !== 'all') {
1089 selectedMode = 'all';
1090 }
1091
1092 var verifyIntegrity = ($verifyIntegrity.length && $verifyIntegrity.is(':checked')) ? 1 : 0;
1093
1094 this.ajax('atomicedge_run_scan', { scan_mode: selectedMode, verify_integrity: verifyIntegrity }, function(data) {
1095 runId = data && data.run_id ? data.run_id : null;
1096 if (self.state.scan) {
1097 self.state.scan.runId = runId;
1098 }
1099 renderStatus(data);
1100 pollStep();
1101 }, function(err) {
1102 clearInterval(progressInterval);
1103 $progress.hide();
1104 $logSection.hide();
1105 $button.prop('disabled', false);
1106 $cancelButton.prop('disabled', true);
1107 $resetButton.prop('disabled', false);
1108 alert((err && err.message) ? err.message : atomicedgeAdmin.strings.error);
1109 });
1110 },
1111
1112 /**
1113 * Cancel an in-progress scan.
1114 */
1115 cancelScan: function() {
1116 var $button = $('#atomicedge-run-scan');
1117 var $cancelButton = $('#atomicedge-cancel-scan');
1118 var $resetButton = $('#atomicedge-reset-scan');
1119 var $progress = $('#atomicedge-scan-progress');
1120 var $logSection = $('#atomicedge-scan-log');
1121
1122 if (!confirm('Cancel the current scan?')) {
1123 return;
1124 }
1125
1126 if (this.state.scan) {
1127 this.state.scan.cancelled = true;
1128 if (this.state.scan.pollTimeout) {
1129 clearTimeout(this.state.scan.pollTimeout);
1130 }
1131 if (this.state.scan.progressInterval) {
1132 clearInterval(this.state.scan.progressInterval);
1133 }
1134 }
1135
1136 $cancelButton.prop('disabled', true);
1137
1138 this.ajax('atomicedge_cancel_scan', { run_id: (this.state.scan && this.state.scan.runId) ? this.state.scan.runId : '' }, function() {
1139 $progress.hide();
1140 $logSection.hide();
1141 $button.prop('disabled', false);
1142 $resetButton.prop('disabled', false);
1143 location.reload();
1144 }, function(err) {
1145 $progress.hide();
1146 $logSection.hide();
1147 $button.prop('disabled', false);
1148 $resetButton.prop('disabled', false);
1149 alert((err && err.message) ? err.message : atomicedgeAdmin.strings.error);
1150 });
1151 },
1152
1153 /**
1154 * Reset scan state/cache (transients + queue) so a new scan starts fresh.
1155 */
1156 resetScan: function() {
1157 var $button = $('#atomicedge-run-scan');
1158 var $cancelButton = $('#atomicedge-cancel-scan');
1159 var $resetButton = $('#atomicedge-reset-scan');
1160
1161 if (!confirm('Reset the scan state? This will clear any in-progress scan and start fresh.')) {
1162 return;
1163 }
1164
1165 if (this.state.scan) {
1166 this.state.scan.cancelled = true;
1167 if (this.state.scan.pollTimeout) {
1168 clearTimeout(this.state.scan.pollTimeout);
1169 }
1170 if (this.state.scan.progressInterval) {
1171 clearInterval(this.state.scan.progressInterval);
1172 }
1173 }
1174
1175 $button.prop('disabled', true);
1176 $cancelButton.prop('disabled', true);
1177 $resetButton.prop('disabled', true);
1178
1179 this.ajax('atomicedge_reset_scan', {}, function() {
1180 location.reload();
1181 }, function(err) {
1182 $button.prop('disabled', false);
1183 $cancelButton.prop('disabled', true);
1184 $resetButton.prop('disabled', false);
1185 alert((err && err.message) ? err.message : atomicedgeAdmin.strings.error);
1186 });
1187 },
1188
1189 /**
1190 * Clear API cache
1191 */
1192 clearCache: function() {
1193 var $status = $('#atomicedge-cache-status');
1194 $status.text(atomicedgeAdmin.strings.loading);
1195
1196 this.ajax('atomicedge_clear_cache', {}, function() {
1197 $status.text(atomicedgeAdmin.strings.success);
1198 setTimeout(function() {
1199 $status.text('');
1200 }, 3000);
1201 }, function() {
1202 $status.text(atomicedgeAdmin.strings.error);
1203 });
1204 },
1205
1206 /**
1207 * AJAX helper
1208 */
1209 ajax: function(action, data, success, error) {
1210 data = data || {};
1211 data.action = action;
1212 data.nonce = atomicedgeAdmin.nonce;
1213
1214 $.ajax({
1215 url: atomicedgeAdmin.ajaxUrl,
1216 type: 'POST',
1217 data: data,
1218 success: function(response) {
1219 if (response.success) {
1220 if (typeof success === 'function') {
1221 success(response.data);
1222 }
1223 } else {
1224 if (typeof error === 'function') {
1225 error(response.data);
1226 } else {
1227 alert(response.data.message || atomicedgeAdmin.strings.error);
1228 }
1229 }
1230 },
1231 error: function() {
1232 if (typeof error === 'function') {
1233 error();
1234 } else {
1235 alert(atomicedgeAdmin.strings.error);
1236 }
1237 }
1238 });
1239 },
1240
1241 /**
1242 * Validate IP address or CIDR
1243 */
1244 validateIp: function(ip) {
1245 // IPv4
1246 var ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
1247 // IPv6 (simplified)
1248 var ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}(\/\d{1,3})?$/;
1249
1250 return ipv4Regex.test(ip) || ipv6Regex.test(ip);
1251 },
1252
1253 /**
1254 * Format number with commas
1255 */
1256 formatNumber: function(num) {
1257 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
1258 },
1259
1260 /**
1261 * Escape HTML
1262 */
1263 escapeHtml: function(str) {
1264 if (!str) return '';
1265 var div = document.createElement('div');
1266 div.textContent = str;
1267 return div.innerHTML;
1268 },
1269
1270 /**
1271 * Get chart options
1272 */
1273 getChartOptions: function() {
1274 return {
1275 responsive: true,
1276 maintainAspectRatio: false,
1277 plugins: {
1278 legend: {
1279 display: false
1280 }
1281 },
1282 scales: {
1283 y: {
1284 beginAtZero: true,
1285 ticks: {
1286 precision: 0
1287 }
1288 }
1289 }
1290 };
1291 }
1292 };
1293
1294 // Initialize on document ready
1295 $(document).ready(function() {
1296 AtomicEdge.init();
1297 });
1298
1299 })(jQuery);
1300