From e7912ea7bafa23dae47041ea1b66c4a1f334e984 Mon Sep 17 00:00:00 2001 From: Rajan Vijayan Date: Mon, 21 Jul 2025 18:37:48 +0530 Subject: [PATCH 1/8] Update the core scanning method --- .github/PULL_REQUEST_TEMPLATE.md | 1 - README.md | 4 +- composer.json | 2 +- src/Admin/AdminPage.php | 8 +- src/Admin/assets/css/admin-styles.css | 346 ++++++++ src/Admin/assets/js/admin-scripts.js | 528 +++++++++-- src/Admin/views/scanner.php | 82 +- src/Core/Activator.php | 74 +- src/Core/AutoScanner.php | 71 +- src/Core/Deactivator.php | 69 +- src/Core/EmailService.php | 206 +++-- src/Core/Logger.php | 349 +++++++- src/Core/Scanner.php | 1178 ++++++++++++++++++++++++- src/Core/Updater.php | 315 +++++-- src/Core/YaraIntegration.php | 102 --- src/Init.php | 9 - src/rules/malware_rules.yar | 65 -- wsp-malware-scanner.php | 2 +- 18 files changed, 2906 insertions(+), 505 deletions(-) delete mode 100644 src/Core/YaraIntegration.php delete mode 100644 src/rules/malware_rules.yar diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 92be01a..9dc619a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,7 +7,6 @@ Please include a summary of the changes and the related issue. Describe the over ## Checklist - [ ] Ensure version updates in `composer.json` and the plugin main file. -- [ ] Verify the Yara rules integration in `src/rules/malware_rules.yar`. - [ ] Check the updates in the `README.md` file if applicable. - [ ] Attach the Jira link for tracking this PR. diff --git a/README.md b/README.md index 886505b..0311b8a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Overview -The Malware Scanner Plugin is a robust security solution designed for WordPress websites, helping site administrators detect, manage, and resolve threats effectively. The plugin utilizes YARA rules for advanced threat detection and provides an intuitive interface for managing scans and system security. +The Malware Scanner Plugin is a robust security solution designed for WordPress websites, helping site administrators detect, manage, and resolve threats effectively. The plugin utilizes advanced malware detection algorithms and provides an intuitive interface for managing scans and system security. ## Features @@ -41,7 +41,7 @@ Detailed user guide for setup, running scans, scheduling, and troubleshooting. ## How It Works -The plugin leverages YARA rules to identify malicious patterns in code. Administrators can configure scans to meet their needs, monitor real-time progress, and resolve threats with ease. The user-friendly interface ensures smooth navigation and efficient management. +The plugin leverages advanced pattern recognition to identify malicious code. Administrators can configure scans to meet their needs, monitor real-time progress, and resolve threats with ease. The user-friendly interface ensures smooth navigation and efficient management. ## License diff --git a/composer.json b/composer.json index ca6e3d0..83fe5f9 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "vplugins/malware-scanner", - "description": "A powerful malware scanner plugin for WordPress using the Yara framework.", + "description": "A powerful malware scanner plugin for WordPress using advanced malware detection algorithms.", "type": "wordpress-plugin", "license": "MIT", "authors": [ diff --git a/src/Admin/AdminPage.php b/src/Admin/AdminPage.php index ffab96d..75b9027 100644 --- a/src/Admin/AdminPage.php +++ b/src/Admin/AdminPage.php @@ -75,20 +75,20 @@ public function add_admin_menu() { * Enqueue CSS, JS, and Dashicons for the admin pages. */ function enqueue_admin_assets() { - // Enqueue Admin CSS + // Enqueue Admin CSS with cache busting wp_enqueue_style( 'malware-scanner-admin-css', // Handle plugin_dir_url(__FILE__) . 'assets/css/admin-styles.css', // Path to CSS file [], // Dependencies - '1.0' // Version + '2.1.0' // Version (updated for ignore folders fix) ); - // Enqueue Admin JS + // Enqueue Admin JS with cache busting wp_enqueue_script( 'malware-scanner-admin-js', // Handle plugin_dir_url(__FILE__) . 'assets/js/admin-scripts.js', // Path to JS file ['jquery', 'chart-js'], // Dependencies (jQuery, Chart.js) - '1.0', // Version + '2.1.0', // Version (updated for ignore folders fix) true // Load in footer ); diff --git a/src/Admin/assets/css/admin-styles.css b/src/Admin/assets/css/admin-styles.css index 31a518f..96ef7c3 100644 --- a/src/Admin/assets/css/admin-styles.css +++ b/src/Admin/assets/css/admin-styles.css @@ -318,6 +318,352 @@ padding: 10px 20px; } +/* =============================================== + BATCH SCANNING UI IMPROVEMENTS + =============================================== */ + +/* Scan Controls */ +.scan-controls { + display: flex; + gap: 10px; + align-items: center; + margin-bottom: 20px; +} + +.scan-controls .button-primary { + background: linear-gradient(135deg, #0073aa, #005177); + border: none; + border-radius: 6px; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 115, 170, 0.3); +} + +.scan-controls .button-primary:hover:not(:disabled) { + background: linear-gradient(135deg, #005177, #0073aa); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 115, 170, 0.4); +} + +.scan-controls .button-primary:disabled { + background: #cccccc; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +#cancel-scan { + background: linear-gradient(135deg, #dc3545, #b02a3a); + color: white; + border: none; + border-radius: 6px; + transition: all 0.3s ease; +} + +#cancel-scan:hover { + background: linear-gradient(135deg, #b02a3a, #dc3545); + transform: translateY(-1px); +} + +/* Real-time Progress Section */ +.scan-info-grid { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 20px; + margin: 20px 0; +} + +.file-count-info { + display: flex; + flex-direction: column; + gap: 15px; +} + +.count-item { + background: #f8f9fa; + padding: 15px; + border-radius: 8px; + border-left: 4px solid #0073aa; + transition: all 0.3s ease; +} + +.count-item:hover { + background: #e9ecef; + border-left-color: #005177; +} + +.count-value { + display: inline-block; + font-weight: bold; + color: #0073aa; + font-size: 16px; + margin-left: 10px; + min-width: 50px; +} + +.progress-container { + display: flex; + flex-direction: column; + justify-content: center; + gap: 10px; +} + +.progress-bar { + width: 100%; + height: 25px; + background: linear-gradient(90deg, #e9ecef, #f8f9fa); + border-radius: 12px; + overflow: hidden; + border: 1px solid #dee2e6; + position: relative; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); +} + +#progress-bar-fill { + width: 0%; + height: 100%; + background: linear-gradient(90deg, #28a745, #20c997); + transition: width 0.5s ease-in-out; + border-radius: 12px; + position: relative; + overflow: hidden; +} + +#progress-bar-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: linear-gradient( + 45deg, + transparent 35%, + rgba(255, 255, 255, 0.2) 50%, + transparent 65% + ); + animation: progress-shine 2s infinite; +} + +@keyframes progress-shine { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +#progress-status { + margin: 0; + font-size: 14px; + font-weight: 500; + color: #495057; + text-align: center; + background: #f8f9fa; + padding: 8px 15px; + border-radius: 6px; + border: 1px solid #e9ecef; +} + +/* Status Badges */ +.status-badge { + display: inline-block; + padding: 4px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + text-align: center; + min-width: 70px; +} + +.status-ignored { + background: linear-gradient(135deg, #ffc107, #e0a800); + color: #856404; +} + +.status-removed { + background: linear-gradient(135deg, #dc3545, #b02a3a); + color: white; +} + +.status-cleaned { + background: linear-gradient(135deg, #28a745, #1e7e34); + color: white; +} + +.status-quarantined { + background: linear-gradient(135deg, #fd7e14, #e55a00); + color: white; +} + +.status-infected { + background: linear-gradient(135deg, #dc3545, #b02a3a); + color: white; + animation: pulse-danger 1.5s infinite; +} + +@keyframes pulse-danger { + 0%, 100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); } + 50% { box-shadow: 0 0 0 6px rgba(220, 53, 69, 0); } +} + +/* Enhanced Results Table */ +.scan-results-section { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + padding: 20px; + margin: 20px 0; +} + +.scan-summary { + background: #f8f9fa; + padding: 15px; + border-radius: 6px; + margin-bottom: 20px; + border-left: 4px solid #0073aa; +} + +.summary-item { + display: flex; + justify-content: space-between; + margin-bottom: 5px; +} + +.summary-label { + font-weight: 600; + color: #495057; +} + +.summary-value { + color: #0073aa; + font-weight: 600; +} + +.scan-results-table { + margin-top: 20px; + width: 100%; + border-radius: 6px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.scan-results-table th { + text-align: left; + background: linear-gradient(135deg, #0073aa, #005177); + color: white; + font-weight: 600; + padding: 15px 12px; +} + +.scan-results-table td { + padding: 12px; + vertical-align: middle; + border-bottom: 1px solid #e9ecef; +} + +.scan-results-table tbody tr:hover { + background-color: #f8f9fa; + transition: background-color 0.2s ease; +} + +.scan-results-table .no-results td { + text-align: center; + padding: 40px 20px; + color: #6c757d; + font-style: italic; +} + +/* Action Buttons */ +.action-buttons { + display: flex; + gap: 10px; + justify-content: center; + flex-wrap: wrap; + margin-top: 20px; +} + +.action-buttons .button { + border-radius: 6px; + font-weight: 500; + transition: all 0.3s ease; + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.action-buttons .button:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +#quarantine-files { + background: linear-gradient(135deg, #fd7e14, #e55a00); + color: white; +} + +#delete-files { + background: linear-gradient(135deg, #dc3545, #b02a3a); + color: white; +} + +#export-results { + background: linear-gradient(135deg, #6c757d, #545b62); + color: white; +} + +/* Loading Spinners */ +.wsp-spinner { + border: 3px solid #f3f3f3; + border-top: 3px solid #0073aa; + border-radius: 50%; + width: 18px; + height: 18px; + animation: spin 1s linear infinite; + display: inline-block; + margin-left: 5px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Responsive Design for Batch Scanning */ +@media (max-width: 768px) { + .scan-info-grid { + grid-template-columns: 1fr; + gap: 15px; + } + + .scan-controls { + flex-direction: column; + align-items: stretch; + } + + .scan-controls .button { + width: 100%; + margin-bottom: 10px; + } + + .action-buttons { + flex-direction: column; + } + + .action-buttons .button { + width: 100%; + margin-bottom: 10px; + } + + .scan-results-table { + font-size: 12px; + } + + .scan-results-table th, + .scan-results-table td { + padding: 8px 6px; + } +} + +/* End Batch Scanning Styles */ + .file-count-info { margin: 10px 0; font-size: 16px; diff --git a/src/Admin/assets/js/admin-scripts.js b/src/Admin/assets/js/admin-scripts.js index 4eb7b8a..2ee558c 100644 --- a/src/Admin/assets/js/admin-scripts.js +++ b/src/Admin/assets/js/admin-scripts.js @@ -7,8 +7,16 @@ document.getElementById('start-scan').addEventListener('click', function () { const ignoreFileTypes = Array.from(document.querySelectorAll('input[name="ignore_file_types[]"]:checked')) .map(fileType => fileType.value); - // Pass the values to the startScan function - startScan(ignoreFolders, ignoreFileTypes); + // Log selection for debugging + if (ignoreFolders.length > 0) { + console.log('Ignored folders selected:', ignoreFolders); + } + if (ignoreFileTypes.length > 0) { + console.log('Ignored file types selected:', ignoreFileTypes); + } + + // Pass the values to the batch scan function + startBatchScan(ignoreFolders, ignoreFileTypes); }); function showLoadingSpinner(element) { @@ -27,7 +35,21 @@ function toggleFolder(element) { } } -function startScan(ignoreFolders, ignoreFileTypes) { +// Global variables for batch scanning +let currentScanSession = null; +let progressPollingInterval = null; +let isScanningActive = false; + +/** + * Start a new batch scan with real-time progress + */ +function startBatchScan(ignoreFolders, ignoreFileTypes) { + if (isScanningActive) { + console.warn('Scan already in progress'); + return; + } + + // Get UI elements const progressBar = document.getElementById('progress-bar-fill'); const progressStatus = document.getElementById('progress-status'); const totalFilesElem = document.getElementById('total-files'); @@ -36,6 +58,19 @@ function startScan(ignoreFolders, ignoreFileTypes) { const resultsBody = document.getElementById('scan-results-body'); const resultsSection = document.getElementById('scan-results-section'); const cleanupFiles = document.getElementById('cleanup-files'); + const startButton = document.getElementById('start-scan'); + + // Reset UI + resetScanUI(); + isScanningActive = true; + startButton.disabled = true; + startButton.textContent = 'Scanning...'; + + // Show cancel button + const cancelButton = document.getElementById('cancel-scan'); + if (cancelButton) { + cancelButton.classList.remove('hidden'); + } // Show loading spinners showLoadingSpinner(totalFilesElem); @@ -48,122 +83,383 @@ function startScan(ignoreFolders, ignoreFileTypes) { progressBar.style.width = '0%'; progressStatus.textContent = 'Initializing scan...'; - // Reset previous results - resultsBody.innerHTML = ''; - progressSection.classList.remove('hidden'); - resultsSection.classList.add('hidden'); - progressBar.style.width = '0%'; - progressStatus.textContent = 'Initializing scan...'; - - // Fake progress variables - let progress = 0; - const fakeInterval = 500; // Progress interval in milliseconds (0.5 seconds) - - // Fake progress updater - const progressInterval = setInterval(() => { - if (progress < 90) { // Cap the progress at 90% before AJAX completes - progress += 5; // Increment progress - progressBar.style.width = progress + '%'; - progressStatus.textContent = `Scanning... ${progress}%`; - }else{ - progressStatus.textContent = `Preparing results...`; - } - }, fakeInterval); - - // Start AJAX call to perform the scan + // Initialize batch scan jQuery.ajax({ url: malwareScannerAjax.ajax_url, type: 'POST', data: { - action: 'start_malware_scan', - directory: './', + action: 'initialize_batch_scan', ignore_folders: ignoreFolders, ignore_file_types: ignoreFileTypes, - cleanup_files: cleanupFiles.checked, + cleanup_files: cleanupFiles.checked ? 'true' : 'false', security: malwareScannerAjax.security }, success: function (response) { - clearInterval(progressInterval); - // console.log('Scan response:', response); if (response.success) { - const report = response.data.report; - const totalFiles = response.data.file.scannedFiles || 0; - const scannedFiles = report.detected || 0; - const progress = (scannedFiles / totalFiles) * 100; + currentScanSession = response.data; + + // Update UI with initial info + hideLoadingSpinner(totalFilesElem, currentScanSession.total_files); + hideLoadingSpinner(scannedFilesElem, 0); + + // Enhanced status message with folder info + let statusMessage = `Scan initialized: ${currentScanSession.total_files} files to scan`; + if (ignoreFolders.length > 0) { + statusMessage += ` (${ignoreFolders.length} folders ignored)`; + } + progressStatus.textContent = statusMessage; - // Hide loading spinners and update counts - hideLoadingSpinner(totalFilesElem, totalFiles); - hideLoadingSpinner(scannedFilesElem, scannedFiles); + // Start processing batches + processBatchSequentially(0); + + // Start progress polling + startProgressPolling(); - // Update progress bar and status - progressBar.style.width = '100%'; - progressStatus.textContent = `Scan completed: ${scannedFiles} / ${totalFiles}`; + } else { + handleScanError(response.data.message || 'Failed to initialize scan'); + } + }, + error: function (jqXHR, textStatus, errorThrown) { + handleScanError('Network error during scan initialization: ' + errorThrown); + } + }); +} + +/** + * Process batches sequentially + */ +function processBatchSequentially(batchNumber) { + if (!currentScanSession || !isScanningActive) { + return; + } + + if (batchNumber >= currentScanSession.total_batches) { + // All batches completed + completeScan(); + return; + } + + const progressStatus = document.getElementById('progress-status'); + progressStatus.textContent = `Processing files...`; - // Display scan results if any - displayResults(report); + jQuery.ajax({ + url: malwareScannerAjax.ajax_url, + type: 'POST', + data: { + action: 'process_scan_batch', + session_id: currentScanSession.session_id, + batch_number: batchNumber, + security: malwareScannerAjax.security + }, + success: function (response) { + if (response.success) { + const data = response.data; + + // Update progress + updateProgressDisplay(data); + + if (data.is_complete) { + completeScan(); + } else { + // Process next batch after a short delay + setTimeout(() => { + processBatchSequentially(batchNumber + 1); + }, 100); + } } else { - progressStatus.textContent = 'Scan failed'; - console.error('Scan error:', response.data.message); + handleScanError(response.data.message || 'Batch processing failed'); } }, error: function (jqXHR, textStatus, errorThrown) { - clearInterval(progressInterval); - progressBar.style.width = '0%'; - progressStatus.textContent = 'Error during scan'; - console.error('Scan error:', errorThrown); + // Try to continue with next batch on error, but log it + console.error('Batch processing error:', errorThrown); + setTimeout(() => { + processBatchSequentially(batchNumber + 1); + }, 1000); + } + }); +} + +/** + * Start polling for progress updates + */ +function startProgressPolling() { + if (progressPollingInterval) { + clearInterval(progressPollingInterval); + } + + progressPollingInterval = setInterval(() => { + if (!currentScanSession || !isScanningActive) { + clearInterval(progressPollingInterval); + return; + } + + jQuery.ajax({ + url: malwareScannerAjax.ajax_url, + type: 'POST', + data: { + action: 'get_scan_progress', + session_id: currentScanSession.session_id, + security: malwareScannerAjax.security + }, + success: function (response) { + if (response.success) { + updateProgressDisplay(response.data); + + if (response.data.is_complete) { + completeScan(); + } + } + }, + error: function () { + // Silently handle polling errors + } + }); + }, 2000); // Poll every 2 seconds +} + +/** + * Update progress display with real data + */ +function updateProgressDisplay(data) { + const progressBar = document.getElementById('progress-bar-fill'); + const progressStatus = document.getElementById('progress-status'); + const scannedFilesElem = document.getElementById('scanned-files'); + + // Update progress bar + progressBar.style.width = data.progress_percentage + '%'; + + // Update scanned files count + hideLoadingSpinner(scannedFilesElem, data.processed_files); + + // Update status text with just percentage + const statusText = `Scanning: ${data.processed_files} / ${data.total_files} files (${data.progress_percentage}%)`; + progressStatus.textContent = statusText; +} + +/** + * Complete the scan and show results + */ +function completeScan() { + const progressBar = document.getElementById('progress-bar-fill'); + const progressStatus = document.getElementById('progress-status'); + const startButton = document.getElementById('start-scan'); + + // Stop polling + if (progressPollingInterval) { + clearInterval(progressPollingInterval); + progressPollingInterval = null; + } + + // Get final results + jQuery.ajax({ + url: malwareScannerAjax.ajax_url, + type: 'POST', + data: { + action: 'get_scan_progress', + session_id: currentScanSession.session_id, + security: malwareScannerAjax.security + }, + success: function (response) { + if (response.success) { + const finalResults = response.data.scan_results; + + // Update final progress + progressBar.style.width = '100%'; + progressStatus.textContent = `Scan completed: ${response.data.processed_files} files scanned, ${finalResults.detected} threats detected`; + + // Display results + displayBatchScanResults(finalResults); + + // Send email if enabled (handled by backend during batch processing) + console.log('Scan completed successfully'); + } + }, + error: function () { + progressStatus.textContent = 'Scan completed with errors'; }, complete: function () { - // Remove loading spinners after the request completes - // totalFilesElem.classList.remove('wsp-spinner'); - // scannedFilesElem.classList.remove('wsp-spinner'); + // Reset scan state + isScanningActive = false; + currentScanSession = null; + startButton.disabled = false; + startButton.textContent = '🔄 Start Scan'; + + // Hide cancel button + const cancelButton = document.getElementById('cancel-scan'); + if (cancelButton) { + cancelButton.classList.add('hidden'); + } } }); } -function displayResults(report) { +/** + * Handle scan errors with enhanced messaging + */ +function handleScanError(errorMessage) { + const progressStatus = document.getElementById('progress-status'); + const progressBar = document.getElementById('progress-bar-fill'); + const startButton = document.getElementById('start-scan'); + + // Check for common error types and provide better user messages + let userFriendlyMessage = errorMessage; + + if (errorMessage.includes('memory') || errorMessage.includes('Memory') || errorMessage.includes('exhausted')) { + userFriendlyMessage = '🔧 Memory limit reached. The scanner will automatically optimize memory usage on retry. Please try again.'; + } else if (errorMessage.includes('timeout') || errorMessage.includes('execution time')) { + userFriendlyMessage = '⏰ Scan timeout prevented. The scanner will process files more efficiently on retry.'; + } else if (errorMessage.includes('permission') || errorMessage.includes('Permission')) { + userFriendlyMessage = '🔒 File permission error. Please check file permissions and try again.'; + } else if (errorMessage.includes('network') || errorMessage.includes('Network')) { + userFriendlyMessage = '🌐 Network connection issue. Please check your connection and try again.'; + } + + progressStatus.innerHTML = ` +
+ ❌ Scan Failed +
+
+ ${userFriendlyMessage} +
+
+ Technical details: ${errorMessage} +
+ `; + + progressBar.style.width = '0%'; + progressBar.style.background = 'linear-gradient(90deg, #dc3545, #b02a3a)'; + + // Reset progress bar color after a delay + setTimeout(() => { + progressBar.style.background = 'linear-gradient(90deg, #28a745, #20c997)'; + }, 3000); + + // Stop polling + if (progressPollingInterval) { + clearInterval(progressPollingInterval); + progressPollingInterval = null; + } + + // Reset scan state + isScanningActive = false; + currentScanSession = null; + startButton.disabled = false; + startButton.textContent = '🔄 Retry Scan'; + + // Hide cancel button + const cancelButton = document.getElementById('cancel-scan'); + if (cancelButton) { + cancelButton.classList.add('hidden'); + } + + console.error('Scan error:', errorMessage); +} + +/** + * Reset scan UI to initial state + */ +function resetScanUI() { + const progressBar = document.getElementById('progress-bar-fill'); + const progressStatus = document.getElementById('progress-status'); + const resultsSection = document.getElementById('scan-results-section'); + + progressBar.style.width = '0%'; + progressStatus.textContent = 'Ready to scan'; + resultsSection.classList.add('hidden'); +} + +/** + * Display batch scan results + */ +function displayBatchScanResults(results) { const resultsBody = document.getElementById('scan-results-body'); const resultsSection = document.getElementById('scan-results-section'); // Clear existing results resultsBody.innerHTML = ''; - // Populate ignored files in results - report.ignored.forEach((file, index) => { - const row = document.createElement('tr'); - row.innerHTML = ` - ${file} - - `; - resultsBody.appendChild(row); - }); + // Combine all result types for display + const allFiles = [ + ...results.ignored.map(file => ({ file, status: 'Ignored', action: 'remove' })), + ...results.removed.map(file => ({ file, status: 'Removed', action: 'none' })), + ...results.edited.map(file => ({ file, status: 'Cleaned', action: 'none' })), + ...results.quarantine.map(file => ({ file, status: 'Quarantined', action: 'remove' })), + ...results.infectedFound.map(file => ({ file, status: 'Infected', action: 'remove' })) + ]; - // Populate deleted files in results - report.removed.forEach((file, index) => { + if (allFiles.length === 0) { const row = document.createElement('tr'); - row.innerHTML = ` - ${file} - `; + row.innerHTML = '✅ No threats detected - Your site is clean!'; resultsBody.appendChild(row); - }); + } else { + allFiles.forEach((item, index) => { + const row = document.createElement('tr'); + const actionButton = item.action === 'remove' + ? `` + : '✓ Handled'; + + row.innerHTML = ` + ${item.file.length > 60 ? '...' + item.file.slice(-60) : item.file} + ${item.status} + ${actionButton} + `; + resultsBody.appendChild(row); + }); + + // Add event listeners to remove buttons + document.querySelectorAll('.remove-btn').forEach(button => { + button.addEventListener('click', function () { + const fileToDelete = this.getAttribute('data-file'); + const fileIndex = this.getAttribute('data-index'); + + // Confirm deletion + if (confirm(`Are you sure you want to delete ${fileToDelete}?`)) { + deleteFile(fileToDelete, fileIndex); + } + }); + }); + } // Display the results section resultsSection.classList.remove('hidden'); +} - // Add event listeners to remove buttons - document.querySelectorAll('.remove-btn').forEach(button => { - button.addEventListener('click', function () { - const fileToDelete = this.getAttribute('data-file'); - const fileIndex = this.getAttribute('data-index'); +/** + * Cancel current scan + */ +function cancelScan() { + if (!currentScanSession || !isScanningActive) { + return; + } - // Confirm deletion - if (confirm(`Are you sure you want to delete ${fileToDelete}?`)) { - // Perform AJAX request to delete the file - deleteFile(fileToDelete, fileIndex); + jQuery.ajax({ + url: malwareScannerAjax.ajax_url, + type: 'POST', + data: { + action: 'cancel_scan', + session_id: currentScanSession.session_id, + security: malwareScannerAjax.security + }, + success: function (response) { + if (response.success) { + handleScanError('Scan cancelled by user'); } - }); + } }); } +// Legacy function for backward compatibility +function startScan(ignoreFolders, ignoreFileTypes) { + // For now, redirect to batch scan + startBatchScan(ignoreFolders, ignoreFileTypes); +} + +function displayResults(report) { + // Legacy function - now handled by displayBatchScanResults + displayBatchScanResults(report); +} + function deleteFile(file, index) { const progressStatus = document.getElementById('progress-status'); @@ -177,13 +473,18 @@ function deleteFile(file, index) { }, success: function (response) { if (response.success) { - // progressStatus.textContent = `File ${file} deleted successfully`; - // Remove the corresponding row from the table const rowToDelete = document.querySelector(`button[data-index="${index}"]`).closest('tr'); - rowToDelete.remove(); + if (rowToDelete) { + rowToDelete.remove(); + } - // Optionally update the report object if needed + // Show success message briefly + const originalText = progressStatus.textContent; + progressStatus.textContent = `File deleted: ${file.split('/').pop()}`; + setTimeout(() => { + progressStatus.textContent = originalText; + }, 3000); } else { progressStatus.textContent = `Failed to delete ${file}`; console.error('Deletion error:', response.data.message); @@ -196,3 +497,66 @@ function deleteFile(file, index) { }); } +// Add folder selection counter functionality +document.addEventListener('DOMContentLoaded', function() { + const cancelButton = document.getElementById('cancel-scan'); + if (cancelButton) { + cancelButton.addEventListener('click', cancelScan); + } + + // Add folder selection counter + updateFolderSelectionCount(); + + // Listen for folder checkbox changes + document.addEventListener('change', function(event) { + if (event.target.name === 'ignore_folders[]') { + updateFolderSelectionCount(); + } + }); +}); + +/** + * Update the folder selection count display + */ +function updateFolderSelectionCount() { + const selectedFolders = document.querySelectorAll('input[name="ignore_folders[]"]:checked'); + const count = selectedFolders.length; + + // Find or create the counter element + let counterElement = document.getElementById('folder-selection-count'); + if (!counterElement) { + // Create counter element + counterElement = document.createElement('div'); + counterElement.id = 'folder-selection-count'; + counterElement.style.cssText = ` + background: #e7f3ff; + color: #0073aa; + padding: 8px 12px; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + margin-top: 10px; + border-left: 3px solid #0073aa; + `; + + // Insert after the folder structure panel + const folderPanel = document.querySelector('.folder-structure-panel'); + if (folderPanel) { + folderPanel.parentNode.insertBefore(counterElement, folderPanel.nextSibling); + } + } + + // Update counter text + if (count === 0) { + counterElement.textContent = 'â„šī¸ No folders selected for ignoring - all folders will be scanned'; + counterElement.style.background = '#f0f0f1'; + counterElement.style.color = '#666'; + counterElement.style.borderLeftColor = '#ccc'; + } else { + counterElement.textContent = `✅ ${count} folder${count === 1 ? '' : 's'} selected for ignoring`; + counterElement.style.background = '#e7f3ff'; + counterElement.style.color = '#0073aa'; + counterElement.style.borderLeftColor = '#0073aa'; + } +} + diff --git a/src/Admin/views/scanner.php b/src/Admin/views/scanner.php index 42d7480..48aa2f1 100644 --- a/src/Admin/views/scanner.php +++ b/src/Admin/views/scanner.php @@ -7,9 +7,14 @@
- +
+ + +
@@ -63,44 +68,69 @@
- + \ No newline at end of file diff --git a/src/Core/Activator.php b/src/Core/Activator.php index 5b38cd6..ad4ddc7 100644 --- a/src/Core/Activator.php +++ b/src/Core/Activator.php @@ -6,26 +6,73 @@ class Activator { /** * Code to run during plugin activation. + * + * @return bool True if activation was successful, false otherwise. */ public static function activate() { - // Set default options for the plugin if they don't exist + try { + // Set comprehensive default options for the plugin + self::set_default_options(); + + // Create a custom database table for logging scan results + $table_created = self::create_scan_logs_table(); + + if (!$table_created) { + error_log('[MalwareScanner] Failed to create scan logs table during activation.'); + return false; + } + + // Flush rewrite rules to ensure proper URL handling + flush_rewrite_rules(); + + return true; + + } catch (\Exception $e) { + error_log('[MalwareScanner] Activation failed: ' . $e->getMessage()); + return false; + } + } + + /** + * Set default options for all plugin settings. + * + * @return void + */ + private static function set_default_options() { + // Core settings array (for backward compatibility) if (!get_option('malware_scanner_settings')) { $default_settings = [ - 'scan_frequency' => 'weekly', // Default scan frequency + 'scan_frequency' => 'weekly', ]; update_option('malware_scanner_settings', $default_settings); } - // Create a custom database table for logging scan results, if necessary - self::create_scan_logs_table(); + // Individual option defaults (used by settings page) + $default_options = [ + 'malware_scanner_schedule_frequency' => 'weekly', + 'malware_scanner_notification_email' => get_option('admin_email'), + 'malware_scanner_scan_log_retention' => '30', + 'malware_scanner_scan_sensitivity_level' => 'medium', + 'malware_scanner_exclude_files' => '', + 'malware_scanner_email_alerts' => '1', + ]; + // Only set defaults if options don't already exist + foreach ($default_options as $option_name => $default_value) { + if (false === get_option($option_name)) { + update_option($option_name, $default_value); + } + } } /** * Create a custom database table to store scan logs. + * + * @return bool True if table was created successfully, false otherwise. */ private static function create_scan_logs_table() { global $wpdb; + $table_name = $wpdb->prefix . 'malware_scan_logs'; $charset_collate = $wpdb->get_charset_collate(); @@ -34,10 +81,25 @@ private static function create_scan_logs_table() { scan_date datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, scan_result text NOT NULL, scan_type varchar(20) NOT NULL DEFAULT 'auto', - PRIMARY KEY (id) + PRIMARY KEY (id), + KEY scan_date (scan_date), + KEY scan_type (scan_type) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); - dbDelta($sql); + $result = dbDelta($sql); + + // Verify table was created + $table_exists = $wpdb->get_var($wpdb->prepare( + "SHOW TABLES LIKE %s", + $table_name + )) === $table_name; + + if (!$table_exists) { + error_log('[MalwareScanner] Failed to create table: ' . $table_name); + return false; + } + + return true; } } \ No newline at end of file diff --git a/src/Core/AutoScanner.php b/src/Core/AutoScanner.php index ca13cef..c229566 100644 --- a/src/Core/AutoScanner.php +++ b/src/Core/AutoScanner.php @@ -9,6 +9,7 @@ class AutoScanner { const CRON_HOOK = 'vplugins_malware_auto_scan'; + const VALID_FREQUENCIES = ['daily', 'weekly', 'monthly']; public function __construct() { add_action('init', [$this, 'schedule_event']); @@ -17,41 +18,93 @@ public function __construct() { /** * Schedule the scan event if not already scheduled. + * + * @return bool True if scheduled successfully, false otherwise. */ public function schedule_event() { if (!wp_next_scheduled(self::CRON_HOOK)) { - // You can make interval dynamic via settings option - $schedule_frequency = get_option('malware_scanner_schedule_frequency', 'daily'); - wp_schedule_event(time(), $schedule_frequency, self::CRON_HOOK); + $schedule_frequency = get_option('malware_scanner_schedule_frequency', 'weekly'); + + // Validate frequency + if (!in_array($schedule_frequency, self::VALID_FREQUENCIES)) { + Logger::warning("Invalid schedule frequency: {$schedule_frequency}. Using default 'weekly'."); + $schedule_frequency = 'weekly'; + update_option('malware_scanner_schedule_frequency', $schedule_frequency); + } + + $scheduled = wp_schedule_event(time(), $schedule_frequency, self::CRON_HOOK); + + if ($scheduled === false) { + Logger::error('Failed to schedule automatic malware scan.'); + return false; + } + + Logger::info("Automatic scan scheduled with frequency: {$schedule_frequency}"); + return true; } + + return true; // Already scheduled } /** * Run the scan when WP-Cron triggers it. + * + * @return bool True if scan completed successfully, false otherwise. */ public function run_scheduled_scan() { + Logger::info('Starting scheduled malware scan.'); + try { + // Check if email alerts are enabled + $email_alerts_enabled = get_option('malware_scanner_email_alerts', '1') === '1'; + $scanner = new Scanner(); - // Run a scan (pass ABSPATH or dynamic dir from settings) + // Run a scan on WordPress root directory $report = $scanner->scan(ABSPATH); + + if (!$report) { + throw new \Exception('Scanner returned empty report.'); + } + $formatted = $scanner->formatReport($report); // Log to file and DB as scan result Logger::log($formatted, 'scan', 'auto'); - // Send notification email - EmailService::send_scan_email($formatted); + // Send notification email if enabled + if ($email_alerts_enabled) { + $email_sent = EmailService::send_scan_email($formatted); + if (!$email_sent) { + Logger::warning('Failed to send scan notification email.'); + } + } + + Logger::info('Scheduled malware scan completed successfully.'); + return true; } catch (\Exception $e) { - error_log('[MalwareScanner] Scheduled scan failed: ' . $e->getMessage()); + $error_msg = 'Scheduled scan failed: ' . $e->getMessage(); + Logger::error($error_msg); + error_log('[MalwareScanner] ' . $error_msg); + return false; } } /** - * Clear cron on deactivation + * Clear cron on deactivation. + * + * @return bool True if cleared successfully, false otherwise. */ public static function deactivate() { - wp_clear_scheduled_hook(self::CRON_HOOK); + $cleared = wp_clear_scheduled_hook(self::CRON_HOOK); + + if ($cleared === false) { + Logger::error('Failed to clear scheduled scan hook during deactivation.'); + return false; + } + + Logger::info('Cleared scheduled scan hook during deactivation.'); + return true; } } \ No newline at end of file diff --git a/src/Core/Deactivator.php b/src/Core/Deactivator.php index e6bb0d0..2a3e4c1 100644 --- a/src/Core/Deactivator.php +++ b/src/Core/Deactivator.php @@ -6,16 +6,69 @@ class Deactivator { /** * Code to run during plugin deactivation. + * + * @return bool True if deactivation was successful, false otherwise. */ public static function deactivate() { - // Unschedule the malware scanner cron job - $timestamp = wp_next_scheduled('malware_scanner_cron_job'); - if ($timestamp) { - wp_unschedule_event($timestamp, 'malware_scanner_cron_job'); + try { + // Clear AutoScanner cron job + $auto_scanner_cleared = AutoScanner::deactivate(); + + // Clear any other legacy cron jobs (for backward compatibility) + $legacy_timestamp = wp_next_scheduled('malware_scanner_cron_job'); + if ($legacy_timestamp) { + wp_unschedule_event($legacy_timestamp, 'malware_scanner_cron_job'); + } + + // Flush rewrite rules to clean up any custom rules + flush_rewrite_rules(); + + // Log deactivation (if Logger is available) + if (class_exists('\VPlugins\MalwareScanner\Core\Logger')) { + Logger::info('Plugin deactivated successfully.'); + } + + return $auto_scanner_cleared; + + } catch (\Exception $e) { + error_log('[MalwareScanner] Deactivation failed: ' . $e->getMessage()); + return false; + } + } + + /** + * Optional cleanup method for complete plugin removal. + * This method should only be called if the user explicitly wants to remove all data. + * + * @return bool True if cleanup was successful, false otherwise. + */ + public static function cleanup_all_data() { + try { + // Remove all plugin options + $options_to_remove = [ + 'malware_scanner_settings', + 'malware_scanner_schedule_frequency', + 'malware_scanner_notification_email', + 'malware_scanner_scan_log_retention', + 'malware_scanner_scan_sensitivity_level', + 'malware_scanner_exclude_files', + 'malware_scanner_email_alerts', + ]; + + foreach ($options_to_remove as $option) { + delete_option($option); + } + + // Remove database table + global $wpdb; + $table_name = $wpdb->prefix . 'malware_scan_logs'; + $wpdb->query("DROP TABLE IF EXISTS {$table_name}"); + + return true; + + } catch (\Exception $e) { + error_log('[MalwareScanner] Data cleanup failed: ' . $e->getMessage()); + return false; } - - // (Optional) Clean up plugin-related data or options, if necessary - // Uncomment the line below to remove the plugin settings on deactivation - // delete_option('malware_scanner_settings'); } } \ No newline at end of file diff --git a/src/Core/EmailService.php b/src/Core/EmailService.php index 28ec79a..4da0a3d 100644 --- a/src/Core/EmailService.php +++ b/src/Core/EmailService.php @@ -5,89 +5,149 @@ class EmailService { /** - * Send the scan report to the configured notification email. + * Send the scan report to the configured notification email(s). * * @param array $report Formatted report from the scan - * @return void + * @return bool True if email was sent successfully, false otherwise */ public static function send_scan_email($report) { - $to_email = get_option('malware_scanner_notification_email'); + // Validate input + if (!is_array($report) || empty($report)) { + Logger::error('EmailService: Invalid or empty report provided.'); + return false; + } + + // Get notification emails (supports multiple emails separated by commas) + $notification_emails = get_option('malware_scanner_notification_email'); + + if (empty($notification_emails)) { + $notification_emails = get_option('admin_email'); + } + + if (empty($notification_emails)) { + Logger::error('EmailService: No valid notification or admin email found.'); + return false; + } - if (empty($to_email) || !is_email($to_email)) { - $to_email = get_option('admin_email'); + // Parse and validate multiple email addresses + $email_list = array_map('trim', explode(',', $notification_emails)); + $valid_emails = array_filter($email_list, 'is_email'); + + if (empty($valid_emails)) { + Logger::error('EmailService: No valid email addresses found in: ' . $notification_emails); + return false; } - if (!is_email($to_email)) { - error_log('[MalwareScanner] Invalid notification and admin email.'); - return; + $site_url = home_url(); + $site_domain = parse_url($site_url, PHP_URL_HOST); + $site_name = get_bloginfo('name'); + $scan_date = date('F j, Y, g:i a'); + $subject = 'đŸ›Ąī¸ Malware Scan Report - ' . $site_name . ' | ' . $scan_date; + + // Load CSS with error handling + $css_path = plugin_dir_path(__FILE__) . '/../Admin/assets/css/email-templates.php'; + $css = ''; + if (file_exists($css_path)) { + $css = include $css_path; + if (!is_string($css)) { + $css = ''; + } } - $site_url = home_url(); - $site_domain = parse_url($site_url, PHP_URL_HOST); - $site_name = get_bloginfo('name'); - $scan_date = date('F j, Y, g:i a'); - $subject = 'đŸ›Ąī¸ Malware Scan Report - ' . $site_name . ' | ' . $scan_date; - $css_path = plugin_dir_path(__FILE__) . '/../Admin/assets/css/email-templates.php'; - $css = file_exists($css_path) ? include $css_path : ''; - - $card = function ($title, $value) { - return ' -
-

' . esc_html($title) . '

-

' . esc_html($value) . '

-
'; - }; - - $message = ' - - - - - ' . esc_html($subject) . ' - - - -
-
- - - - - -
WSP Malware Scanner' . esc_html($scan_date) . '
-
+ $card = function ($title, $value) { + return ' +
+

' . esc_html($title) . '

+

' . esc_html($value) . '

+
'; + }; + + // Sanitize report data with defaults + $report_data = [ + 'totalFilesScanned' => intval($report['totalFilesScanned'] ?? 0), + 'malwareDetected' => intval($report['malwareDetected'] ?? 0), + 'ignoredFiles' => count($report['ignoredFiles'] ?? []), + 'removedFiles' => count($report['removedFiles'] ?? []), + 'editedFiles' => count($report['editedFiles'] ?? []), + 'quarantinedFiles' => count($report['quarantinedFiles'] ?? []), + 'whitelistedFiles' => count($report['whitelistedFiles'] ?? []), + 'infectedFilesFound' => count($report['infectedFilesFound'] ?? []), + ]; + + $message = ' + + + + + ' . esc_html($subject) . ' + + + + - - '; - - $headers = ['Content-Type: text/html; charset=UTF-8']; - - wp_mail($to_email, $subject, $message, $headers); - } + + + + + + + + + + + + + +
' . $card('Total Files
Scanned', $report_data['totalFilesScanned']) . '
' . $card('Malware
Detected', $report_data['malwareDetected']) . '
' . $card('Ignored
Files', $report_data['ignoredFiles']) . '
' . $card('Removed
Files', $report_data['removedFiles']) . '
' . $card('Edited
Files', $report_data['editedFiles']) . '
' . $card('Quarantined
Files', $report_data['quarantinedFiles']) . '
' . $card('Whitelisted
Files', $report_data['whitelistedFiles']) . '
' . $card('Infected
Files', $report_data['infectedFilesFound']) . '
+ + + +
+ + '; + + $headers = ['Content-Type: text/html; charset=UTF-8']; + + // Send to all valid email addresses + $success_count = 0; + $total_emails = count($valid_emails); + + foreach ($valid_emails as $email) { + $sent = wp_mail($email, $subject, $message, $headers); + + if ($sent) { + $success_count++; + Logger::info("Scan report email sent successfully to: {$email}"); + } else { + Logger::error("Failed to send scan report email to: {$email}"); + } + } + + // Return true if at least one email was sent successfully + $success = $success_count > 0; + + if ($success) { + Logger::info("Scan report emails sent to {$success_count} of {$total_emails} recipients."); + } else { + Logger::error("Failed to send scan report emails to any recipients."); + } + + return $success; + } } \ No newline at end of file diff --git a/src/Core/Logger.php b/src/Core/Logger.php index a239706..1519320 100644 --- a/src/Core/Logger.php +++ b/src/Core/Logger.php @@ -4,23 +4,49 @@ class Logger { + const MAX_LOG_SIZE = 10485760; // 10MB + const LOG_ROTATION_COUNT = 5; + /** * Log scan results to database and file. * * @param array|string $message The log message or report (can be array). * @param string $type The log type (e.g., 'info', 'error', 'warning', 'scan'). * @param string $scan_type 'manual' or 'auto' – for DB logging only + * @return bool True if logging was successful, false otherwise */ - public static function log( $message, $type = 'info', $scan_type = 'auto' ) { + public static function log($message, $type = 'info', $scan_type = 'auto') { + // Validate inputs + if (empty($message)) { + return false; + } + + if (!in_array($type, ['info', 'error', 'warning', 'scan', 'result'])) { + $type = 'info'; + } + + if (!in_array($scan_type, ['manual', 'auto'])) { + $scan_type = 'auto'; + } + $is_array = is_array($message); + $success = true; // Save to file - self::log_to_file($is_array ? json_encode($message) : $message, $type); + $file_logged = self::log_to_file($is_array ? json_encode($message) : $message, $type); + if (!$file_logged) { + $success = false; + } // Save to database if it's a scan result if ($is_array && in_array($type, ['info', 'result', 'scan'])) { - self::log_to_db($message, $scan_type); + $db_logged = self::log_to_db($message, $scan_type); + if (!$db_logged) { + $success = false; + } } + + return $success; } /** @@ -28,54 +54,214 @@ public static function log( $message, $type = 'info', $scan_type = 'auto' ) { * * @param array $report The formatted scan report array. * @param string $scan_type The scan type: 'manual' or 'auto'. + * @return bool True if logged successfully, false otherwise */ - private static function log_to_db( $report, $scan_type = 'auto' ) { + private static function log_to_db($report, $scan_type = 'auto') { global $wpdb; + if (!is_array($report)) { + error_log('[MalwareScanner] Logger: Invalid report format for database logging.'); + return false; + } + $table_name = $wpdb->prefix . 'malware_scan_logs'; - $wpdb->insert( + // Verify table exists + $table_exists = $wpdb->get_var($wpdb->prepare( + "SHOW TABLES LIKE %s", + $table_name + )) === $table_name; + + if (!$table_exists) { + error_log('[MalwareScanner] Logger: Scan logs table does not exist.'); + return false; + } + + $result = $wpdb->insert( $table_name, [ 'scan_date' => current_time('mysql'), 'scan_result' => wp_json_encode($report), 'scan_type' => $scan_type, ], - [ '%s', '%s', '%s' ] + ['%s', '%s', '%s'] ); + + if ($result === false) { + error_log('[MalwareScanner] Logger: Database insert failed. Error: ' . $wpdb->last_error); + return false; + } + + return true; } /** - * Log messages to a file in uploads/malware-scanner-logs. + * Log messages to a file with fallback directory options. * * @param string $message * @param string $type + * @return bool True if logged successfully, false otherwise */ - private static function log_to_file( $message, $type = 'info' ) { - $upload_dir = wp_upload_dir(); - $log_dir = $upload_dir['basedir'] . '/malware-scanner-logs'; + private static function log_to_file($message, $type = 'info') { + if (!is_string($message) || empty($message)) { + return false; + } - if ( ! file_exists( $log_dir ) ) { - wp_mkdir_p( $log_dir ); + $log_dir = self::get_log_directory(); + + if (!$log_dir) { + error_log('[MalwareScanner] Logger: No writable log directory found.'); + return false; } + // Directory existence and writability already verified by get_log_directory() + $log_file = $log_dir . '/malware-scanner.log'; + + // Rotate log if it's too large + if (file_exists($log_file) && filesize($log_file) > self::MAX_LOG_SIZE) { + self::rotate_log_file($log_file); + } + $timestamp = date('Y-m-d H:i:s'); $formatted_message = sprintf("[%s] [%s]: %s", $timestamp, strtoupper($type), $message) . PHP_EOL; - error_log($formatted_message, 3, $log_file); + $bytes_written = error_log($formatted_message, 3, $log_file); + + if ($bytes_written === false) { + error_log('[MalwareScanner] Logger: Failed to write to log file: ' . $log_file); + return false; + } + + return true; + } + + /** + * Rotate log files when they get too large. + * + * @param string $log_file + * @return bool True if rotation was successful, false otherwise + */ + private static function rotate_log_file($log_file) { + try { + // Remove oldest backup if it exists + $oldest_backup = $log_file . '.' . self::LOG_ROTATION_COUNT; + if (file_exists($oldest_backup)) { + unlink($oldest_backup); + } + + // Rotate existing backups + for ($i = self::LOG_ROTATION_COUNT - 1; $i >= 1; $i--) { + $old_backup = $log_file . '.' . $i; + $new_backup = $log_file . '.' . ($i + 1); + + if (file_exists($old_backup)) { + rename($old_backup, $new_backup); + } + } + + // Move current log to backup + if (file_exists($log_file)) { + rename($log_file, $log_file . '.1'); + } + + return true; + + } catch (\Exception $e) { + error_log('[MalwareScanner] Logger: Log rotation failed: ' . $e->getMessage()); + return false; + } + } + + /** + * Get a suitable log directory with fallback options + * + * @return string|false Returns the log directory path or false if none available + */ + private static function get_log_directory() { + // Option 1: Try WordPress upload directory (preferred) + $upload_dir = wp_upload_dir(); + if ($upload_dir && !isset($upload_dir['error']) && !empty($upload_dir['basedir'])) { + $log_dir = $upload_dir['basedir'] . '/malware-scanner-logs'; + if (self::ensure_directory_exists($log_dir)) { + return $log_dir; + } + } + + // Option 2: Try plugin directory + $plugin_dir = dirname(__DIR__, 2) . '/logs'; + if (self::ensure_directory_exists($plugin_dir)) { + return $plugin_dir; + } + + // Option 3: Try WordPress content directory + if (defined('WP_CONTENT_DIR')) { + $content_log_dir = WP_CONTENT_DIR . '/malware-scanner-logs'; + if (self::ensure_directory_exists($content_log_dir)) { + return $content_log_dir; + } + } + + // Option 4: Try temporary directory + $temp_dir = sys_get_temp_dir() . '/malware-scanner-logs'; + if (self::ensure_directory_exists($temp_dir)) { + return $temp_dir; + } + + // Option 5: Last resort - try current directory + $current_dir = __DIR__ . '/logs'; + if (self::ensure_directory_exists($current_dir)) { + return $current_dir; + } + + return false; + } + + /** + * Ensure a directory exists and is writable + * + * @param string $directory + * @return bool True if directory exists and is writable + */ + private static function ensure_directory_exists($directory) { + try { + // Create directory if it doesn't exist + if (!file_exists($directory)) { + if (!wp_mkdir_p($directory)) { + // Fallback to PHP mkdir with recursive option + if (!mkdir($directory, 0755, true)) { + return false; + } + } + } + + // Check if directory is writable + if (!is_writable($directory)) { + // Try to make it writable + if (!chmod($directory, 0755)) { + return false; + } + } + + // Final verification + return is_dir($directory) && is_writable($directory); + + } catch (\Exception $e) { + error_log('[MalwareScanner] Logger: Directory creation failed: ' . $e->getMessage()); + return false; + } } - public static function info( $message ) { - self::log($message, 'info'); + public static function info($message) { + return self::log($message, 'info'); } - public static function warning( $message ) { - self::log($message, 'warning'); + public static function warning($message) { + return self::log($message, 'warning'); } - public static function error( $message ) { - self::log($message, 'error'); + public static function error($message) { + return self::log($message, 'error'); } /** @@ -83,9 +269,10 @@ public static function error( $message ) { * * @param array $report * @param string $scan_type + * @return bool True if logged successfully, false otherwise */ public static function scan_result($report, $scan_type = 'auto') { - self::log($report, 'scan', $scan_type); + return self::log($report, 'scan', $scan_type); } /** @@ -96,18 +283,38 @@ public static function scan_result($report, $scan_type = 'auto') { */ public static function get_recent_logs($limit = 5) { global $wpdb; + + // Validate limit + $limit = max(1, min(100, intval($limit))); + $table = $wpdb->prefix . 'malware_scan_logs'; + // Check if table exists + $table_exists = $wpdb->get_var($wpdb->prepare( + "SHOW TABLES LIKE %s", + $table + )) === $table; + + if (!$table_exists) { + error_log('[MalwareScanner] Logger: Scan logs table does not exist.'); + return []; + } + $results = $wpdb->get_results( $wpdb->prepare("SELECT scan_date, scan_result, scan_type, id FROM $table ORDER BY scan_date DESC LIMIT %d", $limit), ARRAY_A ); + if ($wpdb->last_error) { + error_log('[MalwareScanner] Logger: Database query failed: ' . $wpdb->last_error); + return []; + } + $logs = []; foreach ($results as $row) { $decoded = json_decode($row['scan_result'], true); - if (!$decoded) { + if (!$decoded || json_last_error() !== JSON_ERROR_NONE) { continue; } @@ -123,12 +330,106 @@ public static function get_recent_logs($limit = 5) { } $logs[] = [ - 'date' => $row['scan_date'], + 'date' => $row['scan_date'], 'scan_results' => $details, - 'scan_type' => $row['scan_type'], - 'id' => $row['id'] ?? null, + 'scan_type' => $row['scan_type'], + 'id' => $row['id'] ?? null, ]; } + return $logs; } + + /** + * Clean up old log entries from database based on retention settings. + * + * @return bool True if cleanup was successful, false otherwise + */ + public static function cleanup_old_logs() { + global $wpdb; + + $retention_days = get_option('malware_scanner_scan_log_retention', '30'); + $retention_days = max(1, intval($retention_days)); + + $table = $wpdb->prefix . 'malware_scan_logs'; + + $result = $wpdb->query($wpdb->prepare( + "DELETE FROM $table WHERE scan_date < DATE_SUB(NOW(), INTERVAL %d DAY)", + $retention_days + )); + + if ($result === false) { + error_log('[MalwareScanner] Logger: Failed to cleanup old logs: ' . $wpdb->last_error); + return false; + } + + if ($result > 0) { + self::info("Cleaned up {$result} old log entries older than {$retention_days} days."); + } + + return true; + } + + /** + * Test the logging system to verify it's working properly + * + * @return array Test results with details about each log destination + */ + public static function test_logging() { + $results = [ + 'overall_success' => false, + 'log_directory' => null, + 'file_logging' => false, + 'db_logging' => false, + 'errors' => [], + 'warnings' => [] + ]; + + try { + // Test log directory + $log_dir = self::get_log_directory(); + $results['log_directory'] = $log_dir; + + if (!$log_dir) { + $results['errors'][] = 'No writable log directory found'; + return $results; + } + + // Test file logging + $test_message = 'Logger test: ' . date('Y-m-d H:i:s'); + $file_result = self::log_to_file($test_message, 'test'); + $results['file_logging'] = $file_result; + + if (!$file_result) { + $results['errors'][] = 'File logging failed'; + } + + // Test database logging + $test_report = [ + 'totalFilesScanned' => 100, + 'malwareDetected' => 0, + 'test' => true, + 'timestamp' => time() + ]; + + $db_result = self::log_to_db($test_report, 'test'); + $results['db_logging'] = $db_result; + + if (!$db_result) { + $results['warnings'][] = 'Database logging failed (table may not exist yet)'; + } + + // Overall success if at least one method works + $results['overall_success'] = $file_result || $db_result; + + if ($results['overall_success']) { + self::info('Logger test completed successfully'); + } + + } catch (\Exception $e) { + $results['errors'][] = 'Exception during logging test: ' . $e->getMessage(); + } + + return $results; + } } \ No newline at end of file diff --git a/src/Core/Scanner.php b/src/Core/Scanner.php index c9b3bb8..3e5eb65 100644 --- a/src/Core/Scanner.php +++ b/src/Core/Scanner.php @@ -3,62 +3,935 @@ namespace VPlugins\MalwareScanner\Core; use VPlugins\MalwareScanner\Core\Logger; use VPlugins\MalwareScanner\Utils\Helper; -use AMWScan\Scanner as YARAScanner; +use AMWScan\Scanner as AMWScanner; class Scanner { + const BATCH_SIZE = 50; // Files per batch + const SESSION_TIMEOUT = 1800; // 30 minutes + public function __construct() { // Register AJAX actions add_action('wp_ajax_start_malware_scan', [$this, 'handle_malware_scan']); add_action('wp_ajax_nopriv_start_malware_scan', [$this, 'handle_malware_scan']); add_action('wp_ajax_delete_scanned_file', [$this, 'handle_delete_scanned_file']); + + // New batch scanning endpoints + add_action('wp_ajax_initialize_batch_scan', [$this, 'handle_initialize_batch_scan']); + add_action('wp_ajax_process_scan_batch', [$this, 'handle_process_scan_batch']); + add_action('wp_ajax_get_scan_progress', [$this, 'handle_get_scan_progress']); + add_action('wp_ajax_cancel_scan', [$this, 'handle_cancel_scan']); + } + + /** + * Initialize a new batch scan session + * + * @return void + */ + public function handle_initialize_batch_scan() { + // Verify nonce for security + if (!check_ajax_referer('malware_scanner_nonce', 'security', false)) { + wp_send_json_error(['message' => 'Security check failed.']); + return; + } + + try { + // Temporarily increase memory limit for initialization + $original_memory_limit = ini_get('memory_limit'); + ini_set('memory_limit', '512M'); + + // Retrieve and sanitize scan parameters + $ignore_folders = isset($_POST['ignore_folders']) && is_array($_POST['ignore_folders']) + ? array_map('sanitize_text_field', $_POST['ignore_folders']) + : []; + + $ignore_file_types = isset($_POST['ignore_file_types']) && is_array($_POST['ignore_file_types']) + ? array_map('sanitize_text_field', $_POST['ignore_file_types']) + : []; + + $cleanup_files = isset($_POST['cleanup_files']) + ? sanitize_text_field($_POST['cleanup_files']) === 'true' + : false; + + // Add the plugin's own directory to ignored folders for safety + $plugin_base_path = str_replace(WP_PLUGIN_DIR . '/', '', dirname(__DIR__, 2)); + $ignore_folders[] = 'wp-content/plugins/' . $plugin_base_path; + $ignore_folders = array_unique($ignore_folders); + + // Log ignored folders for debugging + if (!empty($ignore_folders)) { + Logger::info('Batch scan - Ignored folders configured: ' . implode(', ', $ignore_folders)); + } + + // Generate unique session ID + $session_id = 'malware_scan_' . wp_generate_uuid4(); + + // Count scannable files without loading them into memory + $scan_directory = ABSPATH; + $this->monitor_memory_usage('before_file_counting', true); + $total_files = $this->count_scannable_files($scan_directory, $ignore_folders, $ignore_file_types); + $this->monitor_memory_usage('after_file_counting', true); + + if ($total_files === 0) { + wp_send_json_error(['message' => 'No files found to scan.']); + return; + } + + // Calculate batches + $total_batches = ceil($total_files / self::BATCH_SIZE); + + // Create scan session (without storing all files) + $scan_session = [ + 'session_id' => $session_id, + 'status' => 'initialized', + 'total_files' => $total_files, + 'processed_files' => 0, + 'current_batch' => 0, + 'total_batches' => $total_batches, + 'scan_directory' => $scan_directory, + 'ignore_folders' => $ignore_folders, + 'ignore_file_types' => $ignore_file_types, + 'cleanup_files' => $cleanup_files, + 'scan_results' => [ + 'detected' => 0, + 'ignored' => [], + 'removed' => [], + 'edited' => [], + 'quarantine' => [], + 'whitelist' => [], + 'infectedFound' => [], + ], + 'started_at' => time(), + 'updated_at' => time(), + ]; + + // Store session (use transient with 30 min expiry) + set_transient($session_id, $scan_session, self::SESSION_TIMEOUT); + + // Restore original memory limit + ini_set('memory_limit', $original_memory_limit); + Logger::info("Batch scan initialized: {$session_id} - {$total_files} files in {$total_batches} batches"); + + wp_send_json_success([ + 'session_id' => $session_id, + 'total_files' => $total_files, + 'total_batches' => $total_batches, + 'batch_size' => self::BATCH_SIZE, + 'message' => 'Scan initialized successfully.', + ]); + + } catch (\Exception $e) { + // Restore original memory limit on error + if (isset($original_memory_limit)) { + ini_set('memory_limit', $original_memory_limit); + } + Logger::error('Failed to initialize batch scan: ' . $e->getMessage()); + wp_send_json_error(['message' => 'Failed to initialize scan: ' . $e->getMessage()]); + } + + wp_die(); } /** - * Handle the malware scan request via AJAX + * Process a single batch of files + * + * @return void */ - - public function handle_malware_scan() { + public function handle_process_scan_batch() { + // Verify nonce for security + if (!check_ajax_referer('malware_scanner_nonce', 'security', false)) { + wp_send_json_error(['message' => 'Security check failed.']); + return; + } + + try { + $session_id = sanitize_text_field($_POST['session_id'] ?? ''); + $batch_number = intval($_POST['batch_number'] ?? 0); + + if (empty($session_id)) { + wp_send_json_error(['message' => 'Invalid session ID.']); + return; + } + + // Get scan session + $scan_session = get_transient($session_id); + + if (!$scan_session) { + wp_send_json_error(['message' => 'Scan session expired or not found.']); + return; + } + + // Validate batch number + if ($batch_number >= $scan_session['total_batches']) { + wp_send_json_error(['message' => 'Invalid batch number.']); + return; + } + + // Update session status + $scan_session['status'] = 'processing'; + $scan_session['current_batch'] = $batch_number; + $scan_session['updated_at'] = time(); + + // Mark as completed if this is the last batch + if ($batch_number >= $scan_session['total_batches'] - 1) { + $scan_session['status'] = 'completing'; + } + + // Get files for this batch using on-demand discovery + $this->monitor_memory_usage("before_batch_{$batch_number}", true); + $batch_files = $this->get_batch_files( + $scan_session['scan_directory'], + $batch_number, + $scan_session['ignore_folders'], + $scan_session['ignore_file_types'] + ); + $this->monitor_memory_usage("after_batch_discovery_{$batch_number}"); + + if (empty($batch_files)) { + // Skip empty batch but continue processing + $scan_session['processed_files'] += 0; + set_transient($session_id, $scan_session, self::SESSION_TIMEOUT); + + wp_send_json_success([ + 'batch_completed' => $batch_number, + 'processed_files' => $scan_session['processed_files'], + 'total_files' => $scan_session['total_files'], + 'progress_percentage' => round(($scan_session['processed_files'] / $scan_session['total_files']) * 100, 1), + 'batch_results' => ['detected' => 0, 'ignored' => [], 'removed' => [], 'edited' => [], 'quarantine' => [], 'whitelist' => [], 'infectedFound' => []], + 'is_complete' => false, + ]); + return; + } + + // Process batch with AMWScanner + $batch_results = $this->process_file_batch($batch_files, $scan_session); + $this->monitor_memory_usage("after_batch_processing_{$batch_number}", true); + + // Update scan results + $scan_session['processed_files'] += count($batch_files); + $scan_session['scan_results'] = $this->merge_scan_results( + $scan_session['scan_results'], + $batch_results + ); + + $progress_percentage = round(($scan_session['processed_files'] / $scan_session['total_files']) * 100, 1); + $is_complete = $scan_session['processed_files'] >= $scan_session['total_files']; + + // If scan is complete, handle final tasks + if ($is_complete) { + $scan_session['status'] = 'completed'; + $scan_session['completed_at'] = time(); + + // Format final report for logging and email + $formatted_report = [ + 'totalFilesScanned' => $scan_session['total_files'], + 'malwareDetected' => $scan_session['scan_results']['detected'], + 'ignoredFiles' => $scan_session['scan_results']['ignored'], + 'removedFiles' => $scan_session['scan_results']['removed'], + 'editedFiles' => $scan_session['scan_results']['edited'], + 'quarantinedFiles' => $scan_session['scan_results']['quarantine'], + 'whitelistedFiles' => $scan_session['scan_results']['whitelist'], + 'infectedFilesFound' => $scan_session['scan_results']['infectedFound'], + ]; + + // Log the final scan results + Logger::log($formatted_report, 'scan', 'manual'); + + // Send notification email if enabled + $email_alerts_enabled = get_option('malware_scanner_email_alerts', '1') === '1'; + if ($email_alerts_enabled) { + EmailService::send_scan_email($formatted_report); + } + + Logger::info("Batch scan completed: {$scan_session['session_id']} - {$scan_session['total_files']} files scanned, {$scan_session['scan_results']['detected']} threats detected"); + } + + // Update session + set_transient($session_id, $scan_session, self::SESSION_TIMEOUT); + + Logger::info("Batch {$batch_number} processed: {$scan_session['processed_files']}/{$scan_session['total_files']} files ({$progress_percentage}%)"); + + wp_send_json_success([ + 'batch_completed' => $batch_number, + 'processed_files' => $scan_session['processed_files'], + 'total_files' => $scan_session['total_files'], + 'progress_percentage' => $progress_percentage, + 'batch_results' => $batch_results, + 'is_complete' => $is_complete, + ]); + + } catch (\Exception $e) { + Logger::error('Batch processing failed: ' . $e->getMessage()); + wp_send_json_error(['message' => 'Batch processing failed: ' . $e->getMessage()]); + } + + wp_die(); + } + + /** + * Get current scan progress + * + * @return void + */ + public function handle_get_scan_progress() { + // Verify nonce for security + if (!check_ajax_referer('malware_scanner_nonce', 'security', false)) { + wp_send_json_error(['message' => 'Security check failed.']); + return; + } + + try { + $session_id = sanitize_text_field($_POST['session_id'] ?? ''); + + if (empty($session_id)) { + wp_send_json_error(['message' => 'Invalid session ID.']); + return; + } + + // Get scan session + $scan_session = get_transient($session_id); + + if (!$scan_session) { + wp_send_json_error(['message' => 'Scan session expired or not found.']); + return; + } + + $progress_percentage = $scan_session['total_files'] > 0 + ? round(($scan_session['processed_files'] / $scan_session['total_files']) * 100, 1) + : 0; + + wp_send_json_success([ + 'status' => $scan_session['status'], + 'processed_files' => $scan_session['processed_files'], + 'total_files' => $scan_session['total_files'], + 'current_batch' => $scan_session['current_batch'], + 'total_batches' => $scan_session['total_batches'], + 'progress_percentage' => $progress_percentage, + 'scan_results' => $scan_session['scan_results'], + 'is_complete' => $scan_session['processed_files'] >= $scan_session['total_files'], + ]); + + } catch (\Exception $e) { + Logger::error('Failed to get scan progress: ' . $e->getMessage()); + wp_send_json_error(['message' => 'Failed to get progress: ' . $e->getMessage()]); + } + + wp_die(); + } + + /** + * Cancel an ongoing scan + * + * @return void + */ + public function handle_cancel_scan() { // Verify nonce for security - check_ajax_referer('malware_scanner_nonce', 'security'); + if (!check_ajax_referer('malware_scanner_nonce', 'security', false)) { + wp_send_json_error(['message' => 'Security check failed.']); + return; + } + + try { + $session_id = sanitize_text_field($_POST['session_id'] ?? ''); + + if (empty($session_id)) { + wp_send_json_error(['message' => 'Invalid session ID.']); + return; + } + + // Delete scan session + delete_transient($session_id); + + Logger::info("Scan cancelled: {$session_id}"); + + wp_send_json_success(['message' => 'Scan cancelled successfully.']); + + } catch (\Exception $e) { + Logger::error('Failed to cancel scan: ' . $e->getMessage()); + wp_send_json_error(['message' => 'Failed to cancel scan: ' . $e->getMessage()]); + } + + wp_die(); + } + + /** + * Count scannable files without loading them into memory + * + * @param string $directory + * @param array $ignore_folders + * @param array $ignore_file_types + * @return int + */ + private function count_scannable_files($directory, $ignore_folders = [], $ignore_file_types = []) { + $count = 0; + + try { + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + if (!$file->isFile()) { + continue; + } + + $file_path = $file->getPathname(); + $relative_path = str_replace($directory, '', $file_path); + + // Check if file should be ignored + if ($this->should_ignore_file($relative_path, $ignore_folders, $ignore_file_types)) { + continue; + } + + $count++; + + // Prevent memory issues by yielding control periodically + if ($count % 1000 === 0) { + // Force garbage collection every 1000 files + if (function_exists('gc_collect_cycles')) { + gc_collect_cycles(); + } + } + } + } catch (\Exception $e) { + Logger::error('Error counting files: ' . $e->getMessage()); + return 0; + } + + return $count; + } + + /** + * Get files for a specific batch using on-demand discovery + * + * @param string $directory + * @param int $batch_number + * @param array $ignore_folders + * @param array $ignore_file_types + * @return array + */ + private function get_batch_files($directory, $batch_number, $ignore_folders = [], $ignore_file_types = []) { + $files = []; + $start_index = $batch_number * self::BATCH_SIZE; + $current_index = 0; + $batch_count = 0; + + try { + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + if (!$file->isFile()) { + continue; + } + + $file_path = $file->getPathname(); + $relative_path = str_replace($directory, '', $file_path); + + // Check if file should be ignored + if ($this->should_ignore_file($relative_path, $ignore_folders, $ignore_file_types)) { + continue; + } + + // Check if this file belongs to the current batch + if ($current_index >= $start_index && $batch_count < self::BATCH_SIZE) { + $files[] = $file_path; + $batch_count++; + } else if ($current_index >= $start_index + self::BATCH_SIZE) { + // We've collected enough files for this batch + break; + } + + $current_index++; + } + } catch (\Exception $e) { + Logger::error('Error getting batch files: ' . $e->getMessage()); + } + + return $files; + } + + /** + * Get all scannable files from directory (DEPRECATED - kept for legacy compatibility) + * + * @param string $directory + * @param array $ignore_folders + * @param array $ignore_file_types + * @return array + */ + private function get_scannable_files($directory, $ignore_folders = [], $ignore_file_types = []) { + // This method is now deprecated in favor of memory-efficient batch processing + // Only use for small directories or fallback scenarios + + $files = []; + $file_count = 0; + $max_files = 1000; // Limit to prevent memory issues + + try { + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + if (!$file->isFile()) { + continue; + } + + $file_path = $file->getPathname(); + $relative_path = str_replace($directory, '', $file_path); + + // Check if file should be ignored + if ($this->should_ignore_file($relative_path, $ignore_folders, $ignore_file_types)) { + continue; + } + + $files[] = $file_path; + $file_count++; + + // Prevent memory exhaustion + if ($file_count >= $max_files) { + Logger::warning("File scan truncated at {$max_files} files to prevent memory issues."); + break; + } + } + } catch (\Exception $e) { + Logger::error('Error in get_scannable_files: ' . $e->getMessage()); + } + + return $files; + } + + /** + * Check if file should be ignored + * + * @param string $file_path Relative file path from scanning root + * @param array $ignore_folders Array of folder paths to ignore (can be absolute or relative) + * @param array $ignore_file_types Array of file extensions to ignore + * @return bool + */ + private function should_ignore_file($file_path, $ignore_folders, $ignore_file_types) { + // Normalize file path - ensure it starts with / + $normalized_file_path = '/' . ltrim($file_path, '/'); + + // Check ignore folders + foreach ($ignore_folders as $folder) { + // Convert absolute folder paths to relative paths for comparison + $normalized_folder = $this->normalize_folder_path($folder); + + if (empty($normalized_folder)) { + continue; + } + + // Check if the file path starts with the ignored folder path + if (strpos($normalized_file_path, $normalized_folder) === 0) { + // Additional check: ensure it's a proper directory match (not just prefix) + $folder_end = strlen($normalized_folder); + if (strlen($normalized_file_path) === $folder_end || + $normalized_file_path[$folder_end] === '/') { + return true; + } + } + } + + // Check ignore file types + $file_extension = pathinfo($file_path, PATHINFO_EXTENSION); + if (in_array($file_extension, $ignore_file_types)) { + return true; + } + + return false; + } + + /** + * Normalize folder path for comparison + * Converts absolute paths to relative paths and ensures consistent format + * + * @param string $folder_path + * @return string Normalized relative folder path + */ + private function normalize_folder_path($folder_path) { + if (empty($folder_path)) { + return ''; + } + + // If it's an absolute path, convert to relative + if (strpos($folder_path, ABSPATH) === 0) { + // Remove ABSPATH to make it relative + $relative_folder = str_replace(ABSPATH, '', $folder_path); + } else { + // Already relative or other format + $relative_folder = $folder_path; + } + + // Normalize path separators and format + $relative_folder = str_replace('\\', '/', $relative_folder); + $relative_folder = '/' . ltrim($relative_folder, '/'); + + // Remove trailing slash unless it's root + if (strlen($relative_folder) > 1) { + $relative_folder = rtrim($relative_folder, '/'); + } + + return $relative_folder; + } + + /** + * Process a batch of files using AMWScanner + * + * @param array $files + * @param array $scan_session + * @return array + */ + private function process_file_batch($files, $scan_session) { + $batch_results = [ + 'detected' => 0, + 'ignored' => [], + 'removed' => [], + 'edited' => [], + 'quarantine' => [], + 'whitelist' => [], + 'infectedFound' => [], + ]; + + if (empty($files)) { + return $batch_results; + } + + try { + // Increase memory limit for batch processing + $original_memory_limit = ini_get('memory_limit'); + ini_set('memory_limit', '512M'); + + // For smaller batches, scan files directly instead of copying + if (count($files) <= 10) { + $batch_results = $this->scan_files_directly($files, $scan_session); + } else { + $batch_results = $this->scan_files_with_temp_directory($files, $scan_session); + } + + // Restore memory limit + ini_set('memory_limit', $original_memory_limit); + + } catch (\Exception $e) { + Logger::error('Batch processing error: ' . $e->getMessage()); + // Restore memory limit on error + if (isset($original_memory_limit)) { + ini_set('memory_limit', $original_memory_limit); + } + } + + return $batch_results; + } + + /** + * Scan files directly without temporary directory (for small batches) + * + * @param array $files + * @param array $scan_session + * @return array + */ + private function scan_files_directly($files, $scan_session) { + $batch_results = [ + 'detected' => 0, + 'ignored' => [], + 'removed' => [], + 'edited' => [], + 'quarantine' => [], + 'whitelist' => [], + 'infectedFound' => [], + ]; + + try { + foreach ($files as $file) { + if (!file_exists($file) || !is_readable($file)) { + continue; + } + + // Create individual scanner for each file + $scanner = new AMWScanner(); + $scanner->setPathScan($file); + + if ($scan_session['cleanup_files']) { + $scanner->setAutoDelete(); + } + + $file_result = $scanner->run(); + + if ($file_result) { + $batch_results['detected'] += intval($file_result->detected ?? 0); + $batch_results['ignored'] = array_merge($batch_results['ignored'], (array)($file_result->ignored ?? [])); + $batch_results['removed'] = array_merge($batch_results['removed'], (array)($file_result->removed ?? [])); + $batch_results['edited'] = array_merge($batch_results['edited'], (array)($file_result->edited ?? [])); + $batch_results['quarantine'] = array_merge($batch_results['quarantine'], (array)($file_result->quarantine ?? [])); + $batch_results['whitelist'] = array_merge($batch_results['whitelist'], (array)($file_result->whitelist ?? [])); + $batch_results['infectedFound'] = array_merge($batch_results['infectedFound'], (array)($file_result->infectedFound ?? [])); + } + + // Force garbage collection after each file to prevent memory buildup + if (function_exists('gc_collect_cycles')) { + gc_collect_cycles(); + } + } + } catch (\Exception $e) { + Logger::error('Direct file scanning error: ' . $e->getMessage()); + } + + return $batch_results; + } + + /** + * Scan files using temporary directory (for larger batches) + * + * @param array $files + * @param array $scan_session + * @return array + */ + private function scan_files_with_temp_directory($files, $scan_session) { + $batch_results = [ + 'detected' => 0, + 'ignored' => [], + 'removed' => [], + 'edited' => [], + 'quarantine' => [], + 'whitelist' => [], + 'infectedFound' => [], + ]; + + $temp_dir = null; + + try { + // Create temporary directory for this batch with fallback options + $temp_dir = $this->get_temp_directory_path(); + + if ($temp_dir && wp_mkdir_p($temp_dir)) { + // Copy files to temp directory in smaller chunks to prevent memory issues + $temp_files = []; + $chunk_size = 5; // Process 5 files at a time + $file_chunks = array_chunk($files, $chunk_size); + + foreach ($file_chunks as $chunk) { + foreach ($chunk as $file) { + if (!file_exists($file) || !is_readable($file)) { + continue; + } + + $temp_file = $temp_dir . '/' . basename($file) . '_' . uniqid(); + if (copy($file, $temp_file)) { + $temp_files[] = $temp_file; + } + } + + // Force garbage collection after each chunk + if (function_exists('gc_collect_cycles')) { + gc_collect_cycles(); + } + } + + // Scan the temporary directory if we have files + if (!empty($temp_files)) { + $scanner = new AMWScanner(); + $scanner->setPathScan($temp_dir); + + if ($scan_session['cleanup_files']) { + $scanner->setAutoDelete(); + } + + $raw_results = $scanner->run(); + + if ($raw_results) { + $batch_results['detected'] += intval($raw_results->detected ?? 0); + $batch_results['ignored'] = array_merge($batch_results['ignored'], (array)($raw_results->ignored ?? [])); + $batch_results['removed'] = array_merge($batch_results['removed'], (array)($raw_results->removed ?? [])); + $batch_results['edited'] = array_merge($batch_results['edited'], (array)($raw_results->edited ?? [])); + $batch_results['quarantine'] = array_merge($batch_results['quarantine'], (array)($raw_results->quarantine ?? [])); + $batch_results['whitelist'] = array_merge($batch_results['whitelist'], (array)($raw_results->whitelist ?? [])); + $batch_results['infectedFound'] = array_merge($batch_results['infectedFound'], (array)($raw_results->infectedFound ?? [])); + } + } + } else { + // Fallback: if temp directory creation fails, use direct file scanning + Logger::warning('Scanner: Temp directory creation failed, falling back to direct file scanning.'); + return $this->scan_files_directly($files, $scan_session); + } + + } catch (\Exception $e) { + Logger::error('Temp directory scanning error: ' . $e->getMessage()); + } finally { + // Always clean up temp directory + if ($temp_dir && is_dir($temp_dir)) { + $this->cleanup_temp_directory($temp_dir); + } + } + + return $batch_results; + } + + /** + * Merge batch results with accumulated results + * + * @param array $accumulated + * @param array $batch_results + * @return array + */ + private function merge_scan_results($accumulated, $batch_results) { + $accumulated['detected'] += $batch_results['detected']; + $accumulated['ignored'] = array_merge($accumulated['ignored'], $batch_results['ignored']); + $accumulated['removed'] = array_merge($accumulated['removed'], $batch_results['removed']); + $accumulated['edited'] = array_merge($accumulated['edited'], $batch_results['edited']); + $accumulated['quarantine'] = array_merge($accumulated['quarantine'], $batch_results['quarantine']); + $accumulated['whitelist'] = array_merge($accumulated['whitelist'], $batch_results['whitelist']); + $accumulated['infectedFound'] = array_merge($accumulated['infectedFound'], $batch_results['infectedFound']); + + return $accumulated; + } + + /** + * Clean up temporary directory with improved error handling + * + * @param string $temp_dir + * @return void + */ + private function cleanup_temp_directory($temp_dir) { + if (!is_string($temp_dir) || empty($temp_dir) || !is_dir($temp_dir)) { + return; + } + + try { + // Security check: ensure we're only deleting temp directories + if (strpos(basename($temp_dir), 'malware-scanner-temp-') !== 0) { + Logger::error('Attempted to delete non-temp directory: ' . $temp_dir); + return; + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($temp_dir, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + $file_count = 0; + foreach ($iterator as $file) { + $file_count++; + + if ($file->isDir()) { + if (!rmdir($file->getRealPath())) { + Logger::warning('Failed to remove directory: ' . $file->getRealPath()); + } + } else { + if (!unlink($file->getRealPath())) { + Logger::warning('Failed to remove file: ' . $file->getRealPath()); + } + } + + // Prevent memory issues during cleanup of large directories + if ($file_count % 100 === 0 && function_exists('gc_collect_cycles')) { + gc_collect_cycles(); + } + } + + // Remove the parent directory + if (!rmdir($temp_dir)) { + Logger::warning('Failed to remove temp directory: ' . $temp_dir); + } else { + Logger::info("Cleaned up temp directory: {$temp_dir} ({$file_count} files processed)"); + } + + } catch (\Exception $e) { + Logger::error('Temp directory cleanup failed: ' . $e->getMessage() . ' - Directory: ' . $temp_dir); + + // Fallback: try to remove directory with system command if available + if (function_exists('exec') && is_executable('/bin/rm')) { + $escaped_dir = escapeshellarg($temp_dir); + exec("/bin/rm -rf {$escaped_dir} 2>/dev/null"); + } + } + } + + /** + * Handle the malware scan request via AJAX (Legacy - kept for backward compatibility) + * + * @return void + */ + public function handle_malware_scan() { + // Verify nonce for security + if (!check_ajax_referer('malware_scanner_nonce', 'security', false)) { + wp_send_json_error(['message' => 'Security check failed.']); + return; + } try { - // Retrieve ignored folders and file types from AJAX request - $ignore_folders = isset($_POST['ignore_folders']) ? $_POST['ignore_folders'] : []; - $ignore_file_types = isset($_POST['ignore_file_types']) ? $_POST['ignore_file_types'] : []; - $cleanup_files = isset($_POST['cleanup_files']) ? $_POST['cleanup_files'] : []; + // Retrieve and sanitize ignored folders and file types from AJAX request + $ignore_folders = isset($_POST['ignore_folders']) && is_array($_POST['ignore_folders']) + ? array_map('sanitize_text_field', $_POST['ignore_folders']) + : []; + + $ignore_file_types = isset($_POST['ignore_file_types']) && is_array($_POST['ignore_file_types']) + ? array_map('sanitize_text_field', $_POST['ignore_file_types']) + : []; + + $cleanup_files = isset($_POST['cleanup_files']) + ? sanitize_text_field($_POST['cleanup_files']) + : ''; - // Add the plugin's own directory to ignored folders + // Add the plugin's own directory to ignored folders for safety $plugin_base_path = str_replace(WP_PLUGIN_DIR . '/', '', dirname(__DIR__, 2)); $ignore_folders[] = 'wp-content/plugins/' . $plugin_base_path; $ignore_folders = array_unique($ignore_folders); - // Define the directory to scan (usually the WordPress root) - $scan_all = true; + // Define the directory to scan (WordPress root) $directoryToScan = ABSPATH; + + // Validate scan directory + if (!is_dir($directoryToScan) || !is_readable($directoryToScan)) { + throw new \Exception('Scan directory is not accessible: ' . $directoryToScan); + } - // Count the total number of files for logging or reporting - $scanned_file_count = Helper::get_total_files_count($directoryToScan, $ignore_folders); + // Count the total number of files for logging or reporting (with memory efficiency) + $scanned_file_count = $this->count_scannable_files($directoryToScan, $ignore_folders, $ignore_file_types); - // Perform the scan using the YARA Scanner - $scanner = new YARAScanner(); + // Perform the scan using the AMWScan library + $scanner = new AMWScanner(); + + if (!$scanner) { + throw new \Exception('Failed to initialize malware scanner.'); + } + $scanner->setPathScan($directoryToScan); $scanner->setIgnorePaths($ignore_folders); - if( $cleanup_files == "true" ) { + + if ($cleanup_files === "true") { $scanner->setAutoDelete(); + Logger::warning('Auto-delete enabled for malware scan - files may be automatically removed.'); } - $raw_report = $scanner->run(); // Retrieve raw scan report + $raw_report = $scanner->run(); + + if (!$raw_report) { + throw new \Exception('Scanner returned empty report.'); + } - // Format the report for email (keeps email consistent and clean) + // Format the report for consistent structure $formatted_report = $this->formatReport($raw_report); $formatted_report['totalFilesScanned'] = $scanned_file_count; - Logger::log($formatted_report, 'scan', 'manual'); + // Log the scan results + $logged = Logger::log($formatted_report, 'scan', 'manual'); + if (!$logged) { + Logger::warning('Failed to log scan results to database.'); + } - // Send the scan results email (e.g., to admin or notification email) - EmailService::send_scan_email($formatted_report); + // Send the scan results email if alerts are enabled + $email_alerts_enabled = get_option('malware_scanner_email_alerts', '1') === '1'; + if ($email_alerts_enabled) { + $email_sent = EmailService::send_scan_email($formatted_report); + if (!$email_sent) { + Logger::warning('Failed to send scan notification email.'); + } + } // Return the raw report to the frontend for rendering the scan results wp_send_json_success([ @@ -68,27 +941,53 @@ public function handle_malware_scan() { 'scannedFiles' => $scanned_file_count, ], ]); + } catch (\Exception $e) { - // Handle any errors and return an error response - wp_send_json_error(['message' => 'Scan failed: ' . $e->getMessage()]); + $error_message = 'Scan failed: ' . $e->getMessage(); + Logger::error($error_message); + wp_send_json_error(['message' => $error_message]); } - wp_die(); // Ensure the request is properly terminated + wp_die(); } /** * Function to scan the given directory for malware * * @param string $directory Path to the directory to scan - * @return object The scan report + * @return object|null The scan report or null on failure */ public function scan($directory) { - $scanner = new YARAScanner(); + if (!is_string($directory) || empty($directory)) { + Logger::error('Scanner: Invalid directory parameter provided.'); + return null; + } + + if (!is_dir($directory) || !is_readable($directory)) { + Logger::error('Scanner: Directory is not accessible: ' . $directory); + return null; + } + + try { + $scanner = new AMWScanner(); + + if (!$scanner) { + throw new \Exception('Failed to initialize malware scanner.'); + } - // Set the path and run the scanner - $report = $scanner->setPathScan($directory)->run(); + // Set the path and run the scanner + $report = $scanner->setPathScan($directory)->run(); + + if (!$report) { + throw new \Exception('Scanner returned empty report.'); + } - return $report; + return $report; + + } catch (\Exception $e) { + Logger::error('Scanner: Scan operation failed - ' . $e->getMessage()); + return null; + } } /** @@ -98,15 +997,32 @@ public function scan($directory) { * @return array The formatted report. */ public function formatReport($report) { + if (!is_object($report)) { + Logger::warning('Scanner: Invalid report format received for formatting.'); + return [ + 'totalFilesScanned' => 0, + 'malwareDetected' => 0, + 'ignoredFiles' => [], + 'removedFiles' => [], + 'editedFiles' => [], + 'quarantinedFiles' => [], + 'whitelistedFiles' => [], + 'infectedFilesFound' => [], + ]; + } + $sanitizeFiles = function ($files) { - return array_values(array_filter((array) $files, function ($file) { - return is_string($file) && file_exists($file); + if (!is_array($files)) { + return []; + } + return array_values(array_filter($files, function ($file) { + return is_string($file) && !empty($file) && file_exists($file); })); }; return [ - 'totalFilesScanned' => $report->scanned ?? 0, - 'malwareDetected' => $report->detected ?? 0, + 'totalFilesScanned' => max(0, intval($report->scanned ?? 0)), + 'malwareDetected' => max(0, intval($report->detected ?? 0)), 'ignoredFiles' => $sanitizeFiles($report->ignored ?? []), 'removedFiles' => $sanitizeFiles($report->removed ?? []), 'editedFiles' => $sanitizeFiles($report->edited ?? []), @@ -118,17 +1034,199 @@ public function formatReport($report) { /** * Handle the deletion of a scanned file via AJAX + * + * @return void */ public function handle_delete_scanned_file() { - check_ajax_referer('malware_scanner_nonce', 'security'); + // Verify nonce for security + if (!check_ajax_referer('malware_scanner_nonce', 'security', false)) { + wp_send_json_error(['message' => 'Security check failed.']); + return; + } + + if (!isset($_POST['file'])) { + wp_send_json_error(['message' => 'No file specified for deletion.']); + return; + } $file = sanitize_text_field($_POST['file']); - // Implement your file deletion logic here - if (unlink($file)) { // Assuming unlink function deletes the file - wp_send_json_success(['message' => 'File deleted successfully']); - } else { - wp_send_json_error(['message' => 'Failed to delete file']); + + // Validate file path + if (empty($file)) { + wp_send_json_error(['message' => 'Invalid file path provided.']); + return; + } + + // Security: Ensure file is within WordPress directory and not a critical file + $real_file_path = realpath($file); + $real_wp_path = realpath(ABSPATH); + + if (!$real_file_path || !$real_wp_path || strpos($real_file_path, $real_wp_path) !== 0) { + Logger::error("Scanner: Attempted to delete file outside WordPress directory: {$file}"); + wp_send_json_error(['message' => 'File deletion not allowed outside WordPress directory.']); + return; + } + + // Additional security: Block deletion of critical WordPress files + $critical_files = [ + 'wp-config.php', + 'wp-load.php', + 'wp-blog-header.php', + '.htaccess', + 'index.php' + ]; + + $file_basename = basename($real_file_path); + if (in_array($file_basename, $critical_files)) { + Logger::error("Scanner: Attempted to delete critical file: {$file}"); + wp_send_json_error(['message' => 'Deletion of critical WordPress files is not allowed.']); + return; + } + + // Check if file exists and is writable + if (!file_exists($real_file_path)) { + wp_send_json_error(['message' => 'File does not exist.']); + return; + } + + if (!is_writable($real_file_path)) { + wp_send_json_error(['message' => 'File is not writable.']); + return; + } + + try { + if (unlink($real_file_path)) { + Logger::info("File deleted successfully: {$file}"); + wp_send_json_success(['message' => 'File deleted successfully']); + } else { + Logger::error("Failed to delete file: {$file}"); + wp_send_json_error(['message' => 'Failed to delete file']); + } + } catch (\Exception $e) { + Logger::error("File deletion error: " . $e->getMessage()); + wp_send_json_error(['message' => 'Error occurred while deleting file']); } + wp_die(); } + + /** + * Monitor and log memory usage during scanning operations + * + * @param string $operation_name Name of the current operation + * @param bool $force_gc Whether to force garbage collection + * @return array Memory usage statistics + */ + private function monitor_memory_usage($operation_name = 'unknown', $force_gc = false) { + // Force garbage collection if requested + if ($force_gc && function_exists('gc_collect_cycles')) { + gc_collect_cycles(); + } + + $memory_usage = memory_get_usage(true); + $memory_peak = memory_get_peak_usage(true); + $memory_limit = $this->parse_memory_limit(ini_get('memory_limit')); + + $memory_percent = $memory_limit > 0 ? round(($memory_usage / $memory_limit) * 100, 2) : 0; + + $stats = [ + 'operation' => $operation_name, + 'current_mb' => round($memory_usage / 1024 / 1024, 2), + 'peak_mb' => round($memory_peak / 1024 / 1024, 2), + 'limit_mb' => round($memory_limit / 1024 / 1024, 2), + 'usage_percent' => $memory_percent, + 'available_mb' => round(($memory_limit - $memory_usage) / 1024 / 1024, 2) + ]; + + // Log warning if memory usage is high + if ($memory_percent > 80) { + Logger::warning("High memory usage during {$operation_name}: {$stats['current_mb']}MB ({$stats['usage_percent']}% of limit)"); + } else { + Logger::info("Memory usage for {$operation_name}: {$stats['current_mb']}MB ({$stats['usage_percent']}% of limit)"); + } + + return $stats; + } + + /** + * Parse memory limit string into bytes + * + * @param string $memory_limit + * @return int Memory limit in bytes + */ + private function parse_memory_limit($memory_limit) { + if ($memory_limit === '-1') { + return PHP_INT_MAX; // Unlimited + } + + $unit = strtolower(substr($memory_limit, -1)); + $value = intval($memory_limit); + + switch ($unit) { + case 'g': + return $value * 1024 * 1024 * 1024; + case 'm': + return $value * 1024 * 1024; + case 'k': + return $value * 1024; + default: + return $value; + } + } + + /** + * Get a suitable temporary directory path with fallback options + * + * @return string|false Returns the temp directory path or false if none available + */ + private function get_temp_directory_path() { + $unique_id = uniqid('malware-scanner-temp-', true); + + // Option 1: Try WordPress upload directory (preferred) + $upload_dir = wp_upload_dir(); + if ($upload_dir && !isset($upload_dir['error']) && !empty($upload_dir['basedir'])) { + $temp_path = $upload_dir['basedir'] . '/' . $unique_id; + if ($this->is_directory_writable(dirname($temp_path))) { + return $temp_path; + } + } + + // Option 2: Try system temporary directory + $system_temp = sys_get_temp_dir() . '/' . $unique_id; + if ($this->is_directory_writable(sys_get_temp_dir())) { + return $system_temp; + } + + // Option 3: Try WordPress content directory + if (defined('WP_CONTENT_DIR')) { + $content_temp = WP_CONTENT_DIR . '/' . $unique_id; + if ($this->is_directory_writable(WP_CONTENT_DIR)) { + return $content_temp; + } + } + + // Option 4: Try plugin directory + $plugin_temp = dirname(__DIR__, 2) . '/temp/' . $unique_id; + $plugin_temp_dir = dirname($plugin_temp); + if ($this->is_directory_writable(dirname($plugin_temp_dir)) || wp_mkdir_p($plugin_temp_dir)) { + return $plugin_temp; + } + + Logger::error('Scanner: No writable temporary directory found for batch processing.'); + return false; + } + + /** + * Check if a directory is writable, with error handling + * + * @param string $directory + * @return bool + */ + private function is_directory_writable($directory) { + try { + return is_dir($directory) && is_writable($directory); + } catch (\Exception $e) { + return false; + } + } } \ No newline at end of file diff --git a/src/Core/Updater.php b/src/Core/Updater.php index 7e381cd..318915a 100644 --- a/src/Core/Updater.php +++ b/src/Core/Updater.php @@ -16,6 +16,9 @@ class Updater { private $github_user; private $github_repo; private $github_api_url; + + const CACHE_DURATION = 12 * HOUR_IN_SECONDS; // 12 hours + const MAX_API_RETRIES = 3; /** * Update constructor. @@ -42,29 +45,51 @@ public function __construct() { * @return object The updated transient object with the new version information if available. */ public function check_for_update($transient) { - if (empty($transient->checked)) { - return $transient; - } + try { + // Skip if transient is not properly formatted + if (empty($transient) || !is_object($transient) || empty($transient->checked)) { + return $transient; + } - $remote_version = $this->get_latest_version(); - if (!$remote_version) { - return $transient; - } + $remote_version = $this->get_latest_version(); + if (!$remote_version) { + Logger::warning('Updater: Failed to retrieve remote version information.'); + return $transient; + } - $current_version = Globals::get_version(); + $current_version = Globals::get_version(); + + if (!$current_version) { + Logger::error('Updater: Could not determine current plugin version.'); + return $transient; + } - if (version_compare($current_version, $remote_version, '<')) { - $transient->response[$this->plugin_file] = (object) [ - 'slug' => $this->plugin_slug, - 'plugin' => $this->plugin_file, - 'new_version' => $remote_version, - 'package' => $this->get_latest_zip_url(), - 'tested' => get_bloginfo('version'), - 'compatibility' => new \stdClass(), - ]; - } + if (version_compare($current_version, $remote_version, '<')) { + $package_url = $this->get_latest_zip_url(); + + if (!$package_url) { + Logger::error('Updater: Failed to get download URL for update.'); + return $transient; + } + + $transient->response[$this->plugin_file] = (object) [ + 'slug' => $this->plugin_slug, + 'plugin' => $this->plugin_file, + 'new_version' => $remote_version, + 'package' => $package_url, + 'tested' => get_bloginfo('version'), + 'compatibility' => new \stdClass(), + ]; + + Logger::info("Updater: Update available - Current: {$current_version}, Remote: {$remote_version}"); + } - return $transient; + return $transient; + + } catch (\Exception $e) { + Logger::error('Updater: Error during update check - ' . $e->getMessage()); + return $transient; + } } /** @@ -76,19 +101,38 @@ public function check_for_update($transient) { * @return object The updated plugin information. */ public function plugins_api_handler($result, $action, $args) { - if ($action !== 'plugin_information' || $args->slug !== $this->plugin_slug) { - return $result; - } + try { + // Only handle plugin_information requests for our plugin + if ($action !== 'plugin_information' || !isset($args->slug) || $args->slug !== $this->plugin_slug) { + return $result; + } - $response = new \stdClass(); - $response->name = ucfirst($this->plugin_slug); - $response->slug = $this->plugin_slug; - $response->version = $this->get_latest_version(); - $response->tested = get_bloginfo('version'); - $response->requires = '5.0'; - $response->download_link = $this->get_latest_zip_url(); + $remote_version = $this->get_latest_version(); + $download_url = $this->get_latest_zip_url(); + + if (!$remote_version || !$download_url) { + Logger::error('Updater: Failed to get plugin information from remote source.'); + return $result; + } - return $response; + $response = new \stdClass(); + $response->name = ucwords(str_replace('-', ' ', $this->plugin_slug)); + $response->slug = $this->plugin_slug; + $response->version = $remote_version; + $response->tested = get_bloginfo('version'); + $response->requires = '5.0'; + $response->download_link = $download_url; + $response->sections = [ + 'description' => 'A powerful malware scanner plugin for WordPress using advanced malware detection algorithms.', + 'changelog' => 'Please visit the GitHub repository for changelog information.', + ]; + + return $response; + + } catch (\Exception $e) { + Logger::error('Updater: Error in plugins API handler - ' . $e->getMessage()); + return $result; + } } /** @@ -100,51 +144,218 @@ public function plugins_api_handler($result, $action, $args) { * @return array The updated result array. */ public function after_install($response, $hook_extra, $result) { - global $wp_filesystem; + try { + // Only handle updates for our plugin + if (!isset($hook_extra['plugin']) || $hook_extra['plugin'] !== $this->plugin_file) { + return $result; + } + + global $wp_filesystem; + + if (!$wp_filesystem) { + Logger::error('Updater: WordPress filesystem not available for post-install.'); + return $result; + } - $plugin_folder = WP_PLUGIN_DIR . '/' . dirname($this->plugin_file); - $wp_filesystem->move($result['destination'], $plugin_folder); - $result['destination'] = $plugin_folder; + $plugin_folder = WP_PLUGIN_DIR . '/' . dirname($this->plugin_file); + + // Validate paths + if (!$result['destination'] || !is_dir($result['destination'])) { + Logger::error('Updater: Invalid destination directory for plugin update.'); + return $result; + } - activate_plugin($this->plugin_file); + // Move plugin to correct location + $move_result = $wp_filesystem->move($result['destination'], $plugin_folder); + + if (!$move_result) { + Logger::error('Updater: Failed to move plugin to correct location.'); + return $result; + } + + $result['destination'] = $plugin_folder; - return $result; + // Reactivate plugin only if it was previously active + if (is_plugin_active($this->plugin_file)) { + $activation_result = activate_plugin($this->plugin_file); + + if (is_wp_error($activation_result)) { + Logger::error('Updater: Failed to reactivate plugin after update: ' . $activation_result->get_error_message()); + } else { + Logger::info('Updater: Plugin successfully updated and reactivated.'); + } + } + + // Clear any cached version information + $this->clear_version_cache(); + + return $result; + + } catch (\Exception $e) { + Logger::error('Updater: Error during post-install - ' . $e->getMessage()); + return $result; + } } /** - * Retrieves the latest plugin version from GitHub. + * Retrieves the latest plugin version from GitHub with caching. * * @return string|false The latest version number or false if an error occurred. */ private function get_latest_version() { - $request = wp_remote_get($this->github_api_url); - if (is_wp_error($request)) { - return false; + $cache_key = 'malware_scanner_remote_version'; + $cached_version = get_transient($cache_key); + + if ($cached_version !== false) { + return $cached_version; } - $body = wp_remote_retrieve_body($request); - $data = json_decode($body); - - return $data && isset($data->tag_name) ? ltrim($data->tag_name, 'v') : false; + $release_data = $this->get_github_release_data(); + + if (!$release_data || !isset($release_data->tag_name)) { + return false; + } + + $version = ltrim($release_data->tag_name, 'v'); + + // Validate version format + if (!preg_match('/^\d+\.\d+\.\d+/', $version)) { + Logger::error('Updater: Invalid version format from GitHub: ' . $version); + return false; + } + + // Cache the version + set_transient($cache_key, $version, self::CACHE_DURATION); + + return $version; } /** - * Retrieves the URL for the latest plugin zip file from GitHub. + * Retrieves the URL for the latest plugin zip file from GitHub with caching. * * @return string|false The URL of the latest zip file or false if an error occurred. */ private function get_latest_zip_url() { - $request = wp_remote_get($this->github_api_url); - if (is_wp_error($request)) { + $cache_key = 'malware_scanner_remote_zip_url'; + $cached_url = get_transient($cache_key); + + if ($cached_url !== false) { + return $cached_url; + } + + $release_data = $this->get_github_release_data(); + + if (!$release_data || !isset($release_data->assets) || !is_array($release_data->assets)) { return false; } + + // Look for the first zip asset + foreach ($release_data->assets as $asset) { + if (isset($asset->browser_download_url) && strpos($asset->name, '.zip') !== false) { + $zip_url = $asset->browser_download_url; + + // Cache the URL + set_transient($cache_key, $zip_url, self::CACHE_DURATION); + + return $zip_url; + } + } + + Logger::error('Updater: No zip asset found in GitHub release.'); + return false; + } + + /** + * Get GitHub release data with error handling and retries. + * + * @return object|false The release data object or false on failure. + */ + private function get_github_release_data() { + $cache_key = 'malware_scanner_github_release_data'; + $cached_data = get_transient($cache_key); + + if ($cached_data !== false) { + return $cached_data; + } + + $attempts = 0; + + while ($attempts < self::MAX_API_RETRIES) { + $attempts++; + + $request = wp_remote_get($this->github_api_url, [ + 'timeout' => 15, + 'headers' => [ + 'User-Agent' => 'WordPress/' . get_bloginfo('version') . '; ' . home_url(), + ], + ]); + + if (is_wp_error($request)) { + Logger::warning("Updater: GitHub API request failed (attempt {$attempts}): " . $request->get_error_message()); + + if ($attempts < self::MAX_API_RETRIES) { + sleep(2); // Wait before retry + continue; + } + + return false; + } - $body = wp_remote_retrieve_body($request); - $data = json_decode($body); + $response_code = wp_remote_retrieve_response_code($request); + + if ($response_code !== 200) { + Logger::warning("Updater: GitHub API returned status {$response_code} (attempt {$attempts})"); + + if ($attempts < self::MAX_API_RETRIES) { + sleep(2); + continue; + } + + return false; + } - $latest_asset = $data->assets; - $latest_zip = $latest_asset[0]->browser_download_url; + $body = wp_remote_retrieve_body($request); + + if (empty($body)) { + Logger::warning("Updater: Empty response from GitHub API (attempt {$attempts})"); + + if ($attempts < self::MAX_API_RETRIES) { + sleep(2); + continue; + } + + return false; + } - return $data && isset($latest_zip) ? $latest_zip : false; + $data = json_decode($body); + + if (json_last_error() !== JSON_ERROR_NONE) { + Logger::error('Updater: Invalid JSON response from GitHub API: ' . json_last_error_msg()); + return false; + } + + if (!$data || !isset($data->tag_name)) { + Logger::error('Updater: Invalid release data structure from GitHub API.'); + return false; + } + + // Cache the successful response + set_transient($cache_key, $data, self::CACHE_DURATION); + + return $data; + } + + return false; + } + + /** + * Clear cached version information. + * + * @return void + */ + private function clear_version_cache() { + delete_transient('malware_scanner_remote_version'); + delete_transient('malware_scanner_remote_zip_url'); + delete_transient('malware_scanner_github_release_data'); } } diff --git a/src/Core/YaraIntegration.php b/src/Core/YaraIntegration.php deleted file mode 100644 index d0bcb55..0000000 --- a/src/Core/YaraIntegration.php +++ /dev/null @@ -1,102 +0,0 @@ -yara_rules_path = $rules_path; - } - - /** - * Run the Yara scan on a specified directory. - * - * @param string $directory The directory to scan for malware. - * - * @return array The scan results. - * @throws \Exception If the scan fails. - */ - public function scan_directory($directory) { - Logger::info('Starting Yara scan on directory: ' . $directory); - - // Validate Yara rules file - if (!file_exists($this->yara_rules_path)) { - throw new \Exception('Yara rules file not found: ' . $this->yara_rules_path); - } - - // Validate scan directory - if (!is_dir($directory)) { - throw new \Exception('Invalid directory to scan: ' . $directory); - } - - // Execute Yara scan command - $command = sprintf('yara -r %s %s', escapeshellarg($this->yara_rules_path), escapeshellarg($directory)); - $output = []; - $return_var = null; - - exec($command, $output, $return_var); - - // Check if the Yara scan was successful - if ($return_var !== 0) { - Logger::error('Yara scan failed with exit code: ' . $return_var); - throw new \Exception('Yara scan failed. Exit code: ' . $return_var); - } - - Logger::info('Yara scan completed successfully.'); - - return $output; - } - - /** - * Run the Yara scan on a specific file. - * - * @param string $file_path The file path to scan for malware. - * - * @return array The scan results. - * @throws \Exception If the scan fails. - */ - public function scan_file($file_path) { - Logger::info('Starting Yara scan on file: ' . $file_path); - - // Validate Yara rules file - if (!file_exists($this->yara_rules_path)) { - throw new \Exception('Yara rules file not found: ' . $this->yara_rules_path); - } - - // Validate file path - if (!file_exists($file_path)) { - throw new \Exception('Invalid file to scan: ' . $file_path); - } - - // Execute Yara scan command - $command = sprintf('yara %s %s', escapeshellarg($this->yara_rules_path), escapeshellarg($file_path)); - $output = []; - $return_var = null; - - exec($command, $output, $return_var); - - // Check if the Yara scan was successful - if ($return_var !== 0) { - Logger::error('Yara scan on file failed with exit code: ' . $return_var); - throw new \Exception('Yara scan failed on file. Exit code: ' . $return_var); - } - - Logger::info('Yara scan on file completed successfully.'); - - return $output; - } -} \ No newline at end of file diff --git a/src/Init.php b/src/Init.php index 612942e..17ac37f 100644 --- a/src/Init.php +++ b/src/Init.php @@ -4,7 +4,6 @@ use VPlugins\MalwareScanner\Admin\AdminPage; use VPlugins\MalwareScanner\Core\Scanner; -use VPlugins\MalwareScanner\Core\YaraIntegration; use VPlugins\MalwareScanner\Core\Activator; use VPlugins\MalwareScanner\Core\Deactivator; use VPlugins\MalwareScanner\Core\AutoScanner; @@ -22,7 +21,6 @@ public static function get_services() { return [ AdminPage::class, Scanner::class, - YaraIntegration::class, AutoScanner::class, Updater::class, Globals::class, @@ -49,13 +47,6 @@ public static function register_services() { * @return object Instance of the class */ private static function instantiate($class) { - // Special case for YaraIntegration class to pass required argument - if ($class === YaraIntegration::class) { - $yara_rules_path = plugin_dir_path(__FILE__) . 'rules/malware_rules.yar'; - return new $class($yara_rules_path); - } - - // For other classes, instantiate as usual return new $class(); } diff --git a/src/rules/malware_rules.yar b/src/rules/malware_rules.yar deleted file mode 100644 index 8a91c18..0000000 --- a/src/rules/malware_rules.yar +++ /dev/null @@ -1,65 +0,0 @@ -rule Suspicious_Base64_Encoded_Strings -{ - meta: - description = "Detects base64 encoded strings, often used in obfuscated malware" - author = "WSP Team" - reference = "https://github.com/vplugins/wsp-malware-scanner/wiki" - date = "2024-09-18" - - strings: - $base64_string = /[A-Za-z0-9+/]{100,}={0,2}/ - - condition: - $base64_string -} - -rule PHP_Shell_Exec_Usage -{ - meta: - description = "Detects usage of shell execution functions in PHP" - author = "WSP Team" - reference = "https://github.com/vplugins/wsp-malware-scanner/wiki" - date = "2024-09-18" - - strings: - $shell_exec = "shell_exec" - $system = "system" - $passthru = "passthru" - $exec = "exec" - $popen = "popen" - $proc_open = "proc_open" - - condition: - any of ($shell_exec, $system, $passthru, $exec, $popen, $proc_open) -} - -rule WordPress_Admin_Backdoor -{ - meta: - description = "Detects WordPress backdoor creation attempts through user creation" - author = "WSP Team" - reference = "https://github.com/vplugins/wsp-malware-scanner/wiki" - date = "2024-09-18" - - strings: - $admin_user_creation = "wp_create_user('admin'," - $admin_role_assignment = "wp_update_user(array('role' => 'administrator'))" - - condition: - $admin_user_creation or $admin_role_assignment -} - -rule Malicious_JS_Obfuscation -{ - meta: - description = "Detects JavaScript obfuscation patterns commonly used in malware" - author = "WSP Team" - reference = "https://github.com/vplugins/wsp-malware-scanner/wiki" - date = "2024-09-18" - - strings: - $obfuscation_pattern = /eval\(function\(.*\)\{.*\}\);/ - - condition: - $obfuscation_pattern -} \ No newline at end of file diff --git a/wsp-malware-scanner.php b/wsp-malware-scanner.php index 75cf9f3..17c4c12 100644 --- a/wsp-malware-scanner.php +++ b/wsp-malware-scanner.php @@ -2,7 +2,7 @@ /** * Plugin Name: Malware Scanner * Plugin URI: https://github.com/vplugins/wsp-malware-scanner - * Description: A powerful malware scanner plugin for WordPress using the Yara framework. + * Description: A powerful malware scanner plugin for WordPress using advanced malware detection algorithms. * Version: 0.0.3 * Author: WSP Team * License: MIT From 4eb9fe327d601b5e5fe1f165afc9429e4d496805 Mon Sep 17 00:00:00 2001 From: Rajan Vijayan Date: Tue, 22 Jul 2025 11:22:00 +0530 Subject: [PATCH 2/8] fix styling issue --- src/Admin/AdminPage.php | 4 +- src/Admin/assets/css/admin-styles.css | 572 ++++++++++---------------- src/Admin/assets/js/admin-scripts.js | 8 +- 3 files changed, 221 insertions(+), 363 deletions(-) diff --git a/src/Admin/AdminPage.php b/src/Admin/AdminPage.php index 75b9027..f01962c 100644 --- a/src/Admin/AdminPage.php +++ b/src/Admin/AdminPage.php @@ -80,7 +80,7 @@ function enqueue_admin_assets() { 'malware-scanner-admin-css', // Handle plugin_dir_url(__FILE__) . 'assets/css/admin-styles.css', // Path to CSS file [], // Dependencies - '2.1.0' // Version (updated for ignore folders fix) + '2.2.0' // Version (updated for professional WordPress styling) ); // Enqueue Admin JS with cache busting @@ -88,7 +88,7 @@ function enqueue_admin_assets() { 'malware-scanner-admin-js', // Handle plugin_dir_url(__FILE__) . 'assets/js/admin-scripts.js', // Path to JS file ['jquery', 'chart-js'], // Dependencies (jQuery, Chart.js) - '2.1.0', // Version (updated for ignore folders fix) + '2.2.0', // Version (updated for professional WordPress styling) true // Load in footer ); diff --git a/src/Admin/assets/css/admin-styles.css b/src/Admin/assets/css/admin-styles.css index 96ef7c3..405d774 100644 --- a/src/Admin/assets/css/admin-styles.css +++ b/src/Admin/assets/css/admin-styles.css @@ -1,19 +1,13 @@ .wrap { margin-top: 20px; - background: #f0f0f1; - color: #3c434a; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; - font-size: 13px; - line-height: 1.4em; } .wrap h1 { font-size: 1.3125rem; flex-grow: 1; - color: #2d2d2d; - font-family: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #23282d; line-height: 1.5rem; - font-weight: 700; + font-weight: 600; padding: 0; } @@ -24,7 +18,7 @@ .modal-content { background-color: #fff; padding: 20px; - border-radius: 8px; + border-radius: 3px; } .close { @@ -40,17 +34,17 @@ } .heading-row { - background-color: #1B719E; + background-color: #0073aa; color: #fff; - padding: 25px; - border-radius: 5px; + padding: 20px; + border-radius: 3px; } .heading-row .section-title { margin: 0; font-size: 18px; color: #fff; - font-weight: 400; + font-weight: 600; } .metric-container { @@ -71,11 +65,11 @@ } .metric-card { - background-color: #f7f7f7; - border-radius: 8px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 3px; padding: 20px; text-align: center; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); flex-direction: column; display: flex; justify-content: center; @@ -84,21 +78,21 @@ .metric-card h3 { margin-top: 10px; - font-size: 17px; + font-size: 16px; font-weight: 600; - color: #333; + color: #23282d; margin-bottom: 5px; } .metric-card p { margin: 5px 0; font-size: 14px; - color: #555; + color: #666; } .icon { - font-size: 40px; - margin-bottom: 20px; + font-size: 32px; + margin-bottom: 15px; color: #0073aa; } @@ -109,28 +103,22 @@ } .metric-box .dashicons { - font-size: 30px; - color: #1B719E; - padding-bottom: 15px; -} - -.metric-box .dashicons .dashicons-clock { - padding-bottom: 0px; -} - -.dashicons-bell:before { - vertical-align: middle; + font-size: 24px; + color: #0073aa; + padding-bottom: 10px; } .metric-box h3 { - font-size: 13px; - margin-bottom: 10px; + font-size: 14px; + margin-bottom: 8px; + font-weight: 600; } .metric-box p { - font-size: 17px; - font-weight: bold; + font-size: 16px; + font-weight: 600; margin: 0; + color: #23282d; } .chart-box { @@ -154,7 +142,6 @@ margin-bottom: 20px; grid-template-columns: repeat(2, 1fr); } - } .section-title { @@ -162,6 +149,7 @@ font-weight: 600; margin-bottom: 15px; margin: 0em 0 0em 0 !important; + color: #23282d; } .section-title-tab { @@ -169,45 +157,45 @@ font-weight: 600; margin-bottom: 15px; margin: 0em 0 1em 0 !important; + color: #23282d; } .recent-activity-section, .status-section { - margin-bottom: 30px; + margin-bottom: 20px; padding: 20px; - background-color: #f9f9f9; + background-color: #fff; border: 1px solid #ddd; - border-radius: 8px; + border-radius: 3px; } .recent-activity-section h2 { - margin : 0px; + margin: 0px; + color: #23282d; + font-weight: 600; } .recent-activity-section table { width: 100%; border-collapse: collapse; - margin-top: 10px; + margin-top: 15px; } .recent-activity-section th, .recent-activity-section td { - padding: 8px; + padding: 8px 10px; text-align: left; border-bottom: 1px solid #ddd; } .recent-activity-section th { - background-color: #e6e6e6; - font-weight: 700; + background-color: #f1f1f1; + font-weight: 600; + color: #23282d; } .recent-activity-section tbody tr:nth-child(even) { - background-color: #f5f5f5; -} - -.recent-activity-section, .status-section { - margin-bottom: 20px; + background-color: #f9f9f9; } .system-wrapper { @@ -220,9 +208,9 @@ .status-section { width: calc(50% - 10px); - border: 1px solid #ccc; + border: 1px solid #ddd; padding: 20px; - border-radius: 8px; + border-radius: 3px; box-sizing: border-box; } @@ -247,12 +235,6 @@ justify-content: space-between; } -.actions-section .button-primary, -.actions-section .button-secondary { - padding: 6px 12px; - font-size: 14px; -} - .header-row { display: flex; justify-content: space-between; @@ -260,23 +242,13 @@ margin-bottom: 20px; } -.wp-core-ui .button-primary { - padding: 2px 10px; -} - -.wp-core-ui .button, -.wp-core-ui .button-secondary { - padding: 2px 10px; -} - .malware-help-content { - font-family: Arial, sans-serif; max-width: 100%; margin: 20px 0px 20px 0px; padding: 20px; background-color: #fff; - border-radius: 8px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #ddd; + border-radius: 3px; } .malware-help-content .header-row { @@ -290,8 +262,9 @@ .help-section h2 { font-size: 18px; color: #0073aa; - border-bottom: 1px solid #0073aa; + border-bottom: 1px solid #ddd; padding-bottom: 5px; + font-weight: 600; } .help-section ol { @@ -312,14 +285,8 @@ text-decoration: underline; } -.button-primary { - margin-top: 15px; - font-size: 18px; - padding: 10px 20px; -} - /* =============================================== - BATCH SCANNING UI IMPROVEMENTS + BATCH SCANNING UI - WORDPRESS STYLE =============================================== */ /* Scan Controls */ @@ -331,37 +298,32 @@ } .scan-controls .button-primary { - background: linear-gradient(135deg, #0073aa, #005177); - border: none; - border-radius: 6px; - transition: all 0.3s ease; - box-shadow: 0 2px 4px rgba(0, 115, 170, 0.3); + background: #0073aa; + border-color: #0073aa; + color: #fff; } .scan-controls .button-primary:hover:not(:disabled) { - background: linear-gradient(135deg, #005177, #0073aa); - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 115, 170, 0.4); + background: #005177; + border-color: #005177; } .scan-controls .button-primary:disabled { - background: #cccccc; + background: #ddd; + border-color: #ddd; + color: #999; cursor: not-allowed; - transform: none; - box-shadow: none; } #cancel-scan { - background: linear-gradient(135deg, #dc3545, #b02a3a); - color: white; - border: none; - border-radius: 6px; - transition: all 0.3s ease; + background: #a00; + border-color: #a00; + color: #fff; } #cancel-scan:hover { - background: linear-gradient(135deg, #b02a3a, #dc3545); - transform: translateY(-1px); + background: #900; + border-color: #900; } /* Real-time Progress Section */ @@ -379,21 +341,16 @@ } .count-item { - background: #f8f9fa; + background: #fff; padding: 15px; - border-radius: 8px; + border: 1px solid #ddd; + border-radius: 3px; border-left: 4px solid #0073aa; - transition: all 0.3s ease; -} - -.count-item:hover { - background: #e9ecef; - border-left-color: #005177; } .count-value { display: inline-block; - font-weight: bold; + font-weight: 600; color: #0073aa; font-size: 16px; margin-left: 10px; @@ -409,115 +366,82 @@ .progress-bar { width: 100%; - height: 25px; - background: linear-gradient(90deg, #e9ecef, #f8f9fa); - border-radius: 12px; + height: 20px; + background: #f1f1f1; + border: 1px solid #ddd; + border-radius: 3px; overflow: hidden; - border: 1px solid #dee2e6; - position: relative; - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); } #progress-bar-fill { width: 0%; height: 100%; - background: linear-gradient(90deg, #28a745, #20c997); - transition: width 0.5s ease-in-out; - border-radius: 12px; - position: relative; - overflow: hidden; -} - -#progress-bar-fill::after { - content: ''; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - background: linear-gradient( - 45deg, - transparent 35%, - rgba(255, 255, 255, 0.2) 50%, - transparent 65% - ); - animation: progress-shine 2s infinite; -} - -@keyframes progress-shine { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(100%); } + background: #0073aa; + transition: width 0.3s ease; } #progress-status { margin: 0; font-size: 14px; - font-weight: 500; - color: #495057; + color: #666; text-align: center; - background: #f8f9fa; + background: #f9f9f9; padding: 8px 15px; - border-radius: 6px; - border: 1px solid #e9ecef; + border: 1px solid #ddd; + border-radius: 3px; } /* Status Badges */ .status-badge { display: inline-block; - padding: 4px 10px; - border-radius: 12px; + padding: 3px 8px; + border-radius: 3px; font-size: 11px; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.5px; text-align: center; - min-width: 70px; + min-width: 60px; } .status-ignored { - background: linear-gradient(135deg, #ffc107, #e0a800); - color: #856404; + background: #ffb900; + color: #fff; } .status-removed { - background: linear-gradient(135deg, #dc3545, #b02a3a); - color: white; + background: #a00; + color: #fff; } .status-cleaned { - background: linear-gradient(135deg, #28a745, #1e7e34); - color: white; + background: #46b450; + color: #fff; } .status-quarantined { - background: linear-gradient(135deg, #fd7e14, #e55a00); - color: white; + background: #f56e28; + color: #fff; } .status-infected { - background: linear-gradient(135deg, #dc3545, #b02a3a); - color: white; - animation: pulse-danger 1.5s infinite; -} - -@keyframes pulse-danger { - 0%, 100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); } - 50% { box-shadow: 0 0 0 6px rgba(220, 53, 69, 0); } + background: #dc3232; + color: #fff; } /* Enhanced Results Table */ .scan-results-section { background-color: #fff; - border-radius: 8px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #ddd; + border-radius: 3px; padding: 20px; margin: 20px 0; } .scan-summary { - background: #f8f9fa; + background: #f9f9f9; padding: 15px; - border-radius: 6px; + border: 1px solid #ddd; + border-radius: 3px; margin-bottom: 20px; border-left: 4px solid #0073aa; } @@ -530,7 +454,7 @@ .summary-label { font-weight: 600; - color: #495057; + color: #23282d; } .summary-value { @@ -541,34 +465,34 @@ .scan-results-table { margin-top: 20px; width: 100%; - border-radius: 6px; - overflow: hidden; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border: 1px solid #ddd; + border-radius: 3px; + border-collapse: collapse; } .scan-results-table th { text-align: left; - background: linear-gradient(135deg, #0073aa, #005177); - color: white; + background: #f1f1f1; + color: #23282d; font-weight: 600; - padding: 15px 12px; + padding: 12px; + border-bottom: 1px solid #ddd; } .scan-results-table td { - padding: 12px; + padding: 10px 12px; vertical-align: middle; - border-bottom: 1px solid #e9ecef; + border-bottom: 1px solid #f1f1f1; } .scan-results-table tbody tr:hover { - background-color: #f8f9fa; - transition: background-color 0.2s ease; + background-color: #f9f9f9; } .scan-results-table .no-results td { text-align: center; - padding: 40px 20px; - color: #6c757d; + padding: 30px 20px; + color: #666; font-style: italic; } @@ -581,41 +505,31 @@ margin-top: 20px; } -.action-buttons .button { - border-radius: 6px; - font-weight: 500; - transition: all 0.3s ease; - border: none; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.action-buttons .button:hover { - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); -} - #quarantine-files { - background: linear-gradient(135deg, #fd7e14, #e55a00); - color: white; + background: #f56e28; + border-color: #f56e28; + color: #fff; } #delete-files { - background: linear-gradient(135deg, #dc3545, #b02a3a); - color: white; + background: #a00; + border-color: #a00; + color: #fff; } #export-results { - background: linear-gradient(135deg, #6c757d, #545b62); - color: white; + background: #666; + border-color: #666; + color: #fff; } /* Loading Spinners */ .wsp-spinner { - border: 3px solid #f3f3f3; - border-top: 3px solid #0073aa; + border: 2px solid #f3f3f3; + border-top: 2px solid #0073aa; border-radius: 50%; - width: 18px; - height: 18px; + width: 16px; + height: 16px; animation: spin 1s linear infinite; display: inline-block; margin-left: 5px; @@ -662,48 +576,20 @@ } } -/* End Batch Scanning Styles */ - -.file-count-info { - margin: 10px 0; - font-size: 16px; -} - -.progress-bar { - width: 100%; - height: 20px; - background-color: #f1f1f1; - border-radius: 10px; - overflow: hidden; -} - -#progress-bar-fill { - width: 0%; - height: 100%; - background-color: #28a745; - transition: width 0.5s; -} - -.scan-results-table { - margin-top: 20px; - width: 100%; -} - -.scan-results-table th { - text-align: left; -} - +/* File severity indicators */ .severity-high { - color: red !important; - font-weight: bold; + color: #dc3232 !important; + font-weight: 600; } .severity-medium { - color: orange !important; + color: #ffb900 !important; + font-weight: 600; } .severity-low { - color: green !important; + color: #46b450 !important; + font-weight: 600; } .scan-actions { @@ -716,15 +602,18 @@ } #ignored-files-accordion h3 { - background: #007cba; + background: #0073aa; color: #fff; padding: 10px; cursor: pointer; margin: 0; + font-weight: 600; } #ignored-files-accordion > div { padding: 10px; + background: #fff; + border: 1px solid #ddd; } .scan-data-table-wrapper { @@ -749,80 +638,73 @@ list-style: none; margin: 0; padding: 0; - font-family: Arial, sans-serif; } /* Root list styling */ .folder-structure > li { - margin: 10px 0; + margin: 8px 0; } /* Checkbox and label spacing */ .folder-structure input[type="checkbox"] { - margin-right: 5px; + margin-right: 8px; } /* Label text styling */ .folder-structure label { cursor: pointer; - font-weight: 500; - color: #333; + font-weight: 400; + color: #23282d; } /* Nested list styling */ .folder-structure ul { list-style: none; - padding-left: 20px; /* Indent nested folders */ - border-left: 2px solid #ddd; /* Optional vertical line for nesting */ + padding-left: 20px; + border-left: 1px solid #ddd; margin: 5px 0; } /* Add icons to folders */ .folder-structure > li > label::before, .folder-structure ul li > label::before { - content: "📂"; /* Folder icon */ + content: "📂"; margin-right: 6px; - color: #f39c12; } /* Subfolder styling */ .folder-structure ul li > label::before { - content: "📁"; /* Subfolder icon */ - color: #3498db; + content: "📁"; } /* Hover effect for labels */ .folder-structure label:hover { color: #0073aa; - text-decoration: underline; -} - -.hidden { - display: none; } .folder-toggle { cursor: pointer; - font-weight: bold; + font-weight: 600; margin-right: 5px; + color: #0073aa; } .folder-structure ul { - margin-left: 20px; /* Indent nested folders */ + margin-left: 20px; } .ignore-file-types { margin-top: 20px; - background-color: #f9f9f9; + background-color: #fff; padding: 15px; border: 1px solid #ddd; - border-radius: 8px; + border-radius: 3px; } .ignore-file-types h4 { margin-bottom: 15px; font-weight: 600; - color: #007cba; + color: #23282d; } .file-types-list { @@ -832,23 +714,22 @@ } .file-type-item { - flex: 1 1 150px; + flex: 1 1 140px; display: flex; align-items: center; - background-color: #fff; - padding: 8px; - border: 1px solid #e0e0e0; - border-radius: 6px; - transition: all 0.3s ease; + background-color: #f9f9f9; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 3px; } .file-type-item:hover { - border-color: #007cba; - background-color: #f1f9ff; + border-color: #0073aa; + background-color: #f1f1f1; } .file-type-item input[type="checkbox"] { - margin-right: 10px; + margin-right: 8px; cursor: pointer; } @@ -857,33 +738,13 @@ display: inline-block; width: 12px; height: 12px; - border: 2px solid #ccc; - border-top-color: #0073aa; /* WordPress blue */ + border: 2px solid #ddd; + border-top-color: #0073aa; border-radius: 50%; animation: spin 0.8s linear infinite; margin-left: 5px; } -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -.wsp-spinner { - border: 3px solid #f3f3f3; - border-top: 3px solid #0073aa; /* WordPress blue */ - border-radius: 50%; - width: 15px; - height: 15px; - animation: spin 1s linear infinite; - display: inline-block; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - /* Styling for the header row */ .header-row { display: flex; @@ -892,15 +753,11 @@ } .intro-text { - font-size: 16px; - color: #333; + font-size: 14px; + color: #666; margin-bottom: 20px; } -.widefat td { - vertical-align: middle; -} - /* Styling for the cleanup option */ .cleanup-option { margin-top: 20px; @@ -909,11 +766,12 @@ gap: 10px; } -/* Help Page Starts */ +/* Help Page */ .malware-help-header h1 { - font-size: 28px; + font-size: 24px; margin-bottom: 20px; - color: #2c3e50; + color: #23282d; + font-weight: 600; } .malware-help-grid { @@ -923,30 +781,25 @@ } .help-card { - background: #f8f9fa; + background: #fff; + border: 1px solid #ddd; border-left: 4px solid #0073aa; padding: 20px; - border-radius: 8px; - box-shadow: 0 2px 5px rgba(0,0,0,0.05); - transition: all 0.3s ease; -} - -.help-card:hover { - border-left-color: #00a0d2; - background: #fff; - box-shadow: 0 4px 10px rgba(0,0,0,0.08); + border-radius: 3px; } .help-card h2 { font-size: 16px; color: #0073aa; margin-top: 0; + font-weight: 600; } .help-card h3 { margin-top: 15px; font-size: 14px; - color: #444; + color: #23282d; + font-weight: 600; } .help-card ol, @@ -961,33 +814,37 @@ .support-link { color: #0073aa; + text-decoration: none; +} + +.support-link:hover { text-decoration: underline; } -/* Help Page End */ -/* Settings Page Starts */ +/* Settings Page */ .malware-settings-wrapper { max-width: 100%; margin: 20px 0px 20px 0px; - padding: 30px; + padding: 20px; background: #fff; - border-radius: 12px; - box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05); - font-family: "Segoe UI", Roboto, sans-serif; + border: 1px solid #ddd; + border-radius: 3px; } .malware-settings-wrapper h1 { - font-size: 26px; + font-size: 23px; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; + color: #23282d; + font-weight: 600; } .settings-section { margin-bottom: 15px; - padding-bottom: 10px; - border-bottom: 1px solid #eee; + padding-bottom: 15px; + border-bottom: 1px solid #ddd; } .settings-section:last-child { @@ -998,15 +855,17 @@ font-weight: 600; display: block; margin-bottom: 6px; + color: #23282d; } .settings-section input[type="email"], .settings-section select, .settings-section textarea { width: 100%; - padding: 10px 12px; - border: 1px solid #ccc; - border-radius: 6px; + max-width: 500px; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 3px; font-size: 14px; } @@ -1025,60 +884,65 @@ } .settings-submit { - text-align: right; - margin-top: 30px; -} - -.settings-submit input[type="submit"] { - background-color: #2271b1; - color: white; - border: none; - padding: 10px 20px; - border-radius: 6px; - cursor: pointer; - font-size: 15px; + text-align: left; + margin-top: 20px; } -/* Settings Page Ends */ -/* Notification Section Start */ +/* Notification Section */ .malware-notice { - padding: 1px 15px; - border-radius: 5px; - margin: 10px 0; + padding: 12px 15px; + border-radius: 3px; + margin: 15px 0; font-weight: 500; - border-left: 5px solid; + border-left: 4px solid; } .notice-success { - background-color: #e6f4ea; - color: #276738; - border-color: #3c763d; + background-color: #f0f8ff; + color: #155724; + border-color: #46b450; } .notice-warning { - background-color: #fff9e6; - color: #7c5b00; - border-color: #ffcc00; + background-color: #fffbf0; + color: #856404; + border-color: #ffb900; } .notice-error { - background-color: #fdecea; - color: #a12622; - border-color: #d93025; + background-color: #ffeaea; + color: #721c24; + border-color: #dc3232; } -/* Notification Section Ends */ -/* Php Security Section Start */ +/* System Status Section */ .system-status-list { display: grid; grid-template-columns: repeat(2, 1fr); - gap: 10px 30px; /* vertical and horizontal gaps */ + gap: 10px 30px; list-style: none; padding-left: 0; } + .system-status-list li { background: #f9f9f9; padding: 8px 12px; - border-radius: 4px; + border: 1px solid #ddd; + border-radius: 3px; } -/* Php Security Section End */ \ No newline at end of file + +/* Folder structure panel */ +.folder-structure-panel { + background: #fff; + border: 1px solid #ddd; + border-radius: 3px; + padding: 15px; + margin: 15px 0; +} + +.folder-structure-panel h4 { + margin-top: 0; + margin-bottom: 15px; + font-weight: 600; + color: #23282d; +} \ No newline at end of file diff --git a/src/Admin/assets/js/admin-scripts.js b/src/Admin/assets/js/admin-scripts.js index 2ee558c..d6ebe03 100644 --- a/src/Admin/assets/js/admin-scripts.js +++ b/src/Admin/assets/js/admin-scripts.js @@ -7,13 +7,7 @@ document.getElementById('start-scan').addEventListener('click', function () { const ignoreFileTypes = Array.from(document.querySelectorAll('input[name="ignore_file_types[]"]:checked')) .map(fileType => fileType.value); - // Log selection for debugging - if (ignoreFolders.length > 0) { - console.log('Ignored folders selected:', ignoreFolders); - } - if (ignoreFileTypes.length > 0) { - console.log('Ignored file types selected:', ignoreFileTypes); - } + // Note: Folder and file type selections are processed server-side // Pass the values to the batch scan function startBatchScan(ignoreFolders, ignoreFileTypes); From c15ccc13e8121823fba63937e34c5fc8c1c22bf1 Mon Sep 17 00:00:00 2001 From: Rajan Vijayan Date: Tue, 22 Jul 2025 11:50:34 +0530 Subject: [PATCH 3/8] fix db sync issue --- src/Admin/AdminPage.php | 4 +- src/Core/Activator.php | 51 +++++++++++++++ src/Core/Logger.php | 141 ++++++++++++++++++++++++++++++++++------ src/Core/Scanner.php | 137 +++++++++++++++++++++++++++++++++----- 4 files changed, 296 insertions(+), 37 deletions(-) diff --git a/src/Admin/AdminPage.php b/src/Admin/AdminPage.php index f01962c..0e0741f 100644 --- a/src/Admin/AdminPage.php +++ b/src/Admin/AdminPage.php @@ -80,7 +80,7 @@ function enqueue_admin_assets() { 'malware-scanner-admin-css', // Handle plugin_dir_url(__FILE__) . 'assets/css/admin-styles.css', // Path to CSS file [], // Dependencies - '2.2.0' // Version (updated for professional WordPress styling) + '2.5.0' // Version (fixed database and AMWScanner errors) ); // Enqueue Admin JS with cache busting @@ -88,7 +88,7 @@ function enqueue_admin_assets() { 'malware-scanner-admin-js', // Handle plugin_dir_url(__FILE__) . 'assets/js/admin-scripts.js', // Path to JS file ['jquery', 'chart-js'], // Dependencies (jQuery, Chart.js) - '2.2.0', // Version (updated for professional WordPress styling) + '2.5.0', // Version (fixed database and AMWScanner errors) true // Load in footer ); diff --git a/src/Core/Activator.php b/src/Core/Activator.php index ad4ddc7..3de93b7 100644 --- a/src/Core/Activator.php +++ b/src/Core/Activator.php @@ -21,6 +21,9 @@ public static function activate() { error_log('[MalwareScanner] Failed to create scan logs table during activation.'); return false; } + + // Ensure table has correct structure (for upgrades from older versions) + self::update_table_structure(); // Flush rewrite rules to ensure proper URL handling flush_rewrite_rules(); @@ -102,4 +105,52 @@ private static function create_scan_logs_table() { return true; } + + /** + * Update table structure for existing installations + * Ensures scan_type column exists in older table versions + * + * @return bool True if update was successful + */ + private static function update_table_structure() { + global $wpdb; + + $table_name = $wpdb->prefix . 'malware_scan_logs'; + + try { + // Check if table exists + $table_exists = $wpdb->get_var($wpdb->prepare( + "SHOW TABLES LIKE %s", + $table_name + )) === $table_name; + + if (!$table_exists) { + return true; // Table doesn't exist yet, will be created by create_scan_logs_table() + } + + // Check if scan_type column exists + $columns = $wpdb->get_col("DESCRIBE {$table_name}"); + if (!in_array('scan_type', $columns)) { + // Add the missing scan_type column + $result = $wpdb->query( + "ALTER TABLE {$table_name} + ADD COLUMN scan_type varchar(20) NOT NULL DEFAULT 'auto', + ADD KEY scan_type (scan_type)" + ); + + if ($result === false) { + error_log('[MalwareScanner] Failed to add scan_type column during activation. Error: ' . $wpdb->last_error); + return false; + } else { + error_log('[MalwareScanner] Successfully added scan_type column to existing table.'); + } + } + + return true; + + } catch (\Exception $e) { + error_log('[MalwareScanner] Table structure update failed: ' . $e->getMessage()); + return false; + } + } } \ No newline at end of file diff --git a/src/Core/Logger.php b/src/Core/Logger.php index 1519320..697cf3a 100644 --- a/src/Core/Logger.php +++ b/src/Core/Logger.php @@ -50,7 +50,7 @@ public static function log($message, $type = 'info', $scan_type = 'auto') { } /** - * Log to malware scan DB table. + * Log scan report to the database. * * @param array $report The formatted scan report array. * @param string $scan_type The scan type: 'manual' or 'auto'. @@ -66,29 +66,119 @@ private static function log_to_db($report, $scan_type = 'auto') { $table_name = $wpdb->prefix . 'malware_scan_logs'; - // Verify table exists + // Verify table exists and has correct structure + if (!self::ensure_table_structure()) { + error_log('[MalwareScanner] Logger: Scan logs table structure verification failed.'); + return false; + } + + // Check if scan_type column exists + $columns = $wpdb->get_col("DESCRIBE {$table_name}"); + $has_scan_type = in_array('scan_type', $columns); + + if ($has_scan_type) { + // Insert with scan_type column + $result = $wpdb->insert( + $table_name, + [ + 'scan_date' => current_time('mysql'), + 'scan_result' => wp_json_encode($report), + 'scan_type' => $scan_type, + ], + ['%s', '%s', '%s'] + ); + } else { + // Insert without scan_type column (fallback for older table structure) + $result = $wpdb->insert( + $table_name, + [ + 'scan_date' => current_time('mysql'), + 'scan_result' => wp_json_encode($report), + ], + ['%s', '%s'] + ); + } + + if ($result === false) { + error_log('[MalwareScanner] Logger: Database insert failed. Error: ' . $wpdb->last_error); + return false; + } + + return true; + } + + /** + * Ensure the malware scan logs table has the correct structure + * + * @return bool True if table structure is correct, false otherwise + */ + private static function ensure_table_structure() { + global $wpdb; + + $table_name = $wpdb->prefix . 'malware_scan_logs'; + + // Check if table exists $table_exists = $wpdb->get_var($wpdb->prepare( "SHOW TABLES LIKE %s", $table_name )) === $table_name; if (!$table_exists) { - error_log('[MalwareScanner] Logger: Scan logs table does not exist.'); - return false; + // Try to create the table + return self::create_table(); } - $result = $wpdb->insert( - $table_name, - [ - 'scan_date' => current_time('mysql'), - 'scan_result' => wp_json_encode($report), - 'scan_type' => $scan_type, - ], - ['%s', '%s', '%s'] - ); + // Check if scan_type column exists + $columns = $wpdb->get_col("DESCRIBE {$table_name}"); + if (!in_array('scan_type', $columns)) { + // Add the missing scan_type column + $result = $wpdb->query( + "ALTER TABLE {$table_name} + ADD COLUMN scan_type varchar(20) NOT NULL DEFAULT 'auto', + ADD KEY scan_type (scan_type)" + ); + + if ($result === false) { + error_log('[MalwareScanner] Logger: Failed to add scan_type column. Error: ' . $wpdb->last_error); + // Don't return false here - we can still log without scan_type + } + } - if ($result === false) { - error_log('[MalwareScanner] Logger: Database insert failed. Error: ' . $wpdb->last_error); + return true; + } + + /** + * Create the malware scan logs table + * + * @return bool True if table was created successfully + */ + private static function create_table() { + global $wpdb; + + $table_name = $wpdb->prefix . 'malware_scan_logs'; + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE IF NOT EXISTS $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + scan_date datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + scan_result text NOT NULL, + scan_type varchar(20) NOT NULL DEFAULT 'auto', + PRIMARY KEY (id), + KEY scan_date (scan_date), + KEY scan_type (scan_type) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + $result = dbDelta($sql); + + // Verify table was created + $table_exists = $wpdb->get_var($wpdb->prepare( + "SHOW TABLES LIKE %s", + $table_name + )) === $table_name; + + if (!$table_exists) { + error_log('[MalwareScanner] Logger: Failed to create table: ' . $table_name); return false; } @@ -300,10 +390,21 @@ public static function get_recent_logs($limit = 5) { return []; } - $results = $wpdb->get_results( - $wpdb->prepare("SELECT scan_date, scan_result, scan_type, id FROM $table ORDER BY scan_date DESC LIMIT %d", $limit), - ARRAY_A - ); + // Check if scan_type column exists + $columns = $wpdb->get_col("DESCRIBE {$table}"); + $has_scan_type = in_array('scan_type', $columns); + + if ($has_scan_type) { + $results = $wpdb->get_results( + $wpdb->prepare("SELECT scan_date, scan_result, scan_type, id FROM $table ORDER BY scan_date DESC LIMIT %d", $limit), + ARRAY_A + ); + } else { + $results = $wpdb->get_results( + $wpdb->prepare("SELECT scan_date, scan_result, id FROM $table ORDER BY scan_date DESC LIMIT %d", $limit), + ARRAY_A + ); + } if ($wpdb->last_error) { error_log('[MalwareScanner] Logger: Database query failed: ' . $wpdb->last_error); @@ -332,7 +433,7 @@ public static function get_recent_logs($limit = 5) { $logs[] = [ 'date' => $row['scan_date'], 'scan_results' => $details, - 'scan_type' => $row['scan_type'], + 'scan_type' => $has_scan_type ? $row['scan_type'] : 'unknown', 'id' => $row['id'] ?? null, ]; } diff --git a/src/Core/Scanner.php b/src/Core/Scanner.php index 3e5eb65..c827ffd 100644 --- a/src/Core/Scanner.php +++ b/src/Core/Scanner.php @@ -652,13 +652,12 @@ private function scan_files_directly($files, $scan_session) { continue; } - // Create individual scanner for each file + // Create individual scanner for each file with enhanced configuration $scanner = new AMWScanner(); $scanner->setPathScan($file); - if ($scan_session['cleanup_files']) { - $scanner->setAutoDelete(); - } + // Enhanced configuration for better malware detection + $this->configure_scanner($scanner, $scan_session); $file_result = $scanner->run(); @@ -684,6 +683,100 @@ private function scan_files_directly($files, $scan_session) { return $batch_results; } + /** + * Configure AMWScanner with optimal settings for WordPress malware detection + * + * @param AMWScanner $scanner + * @param array $scan_session + * @return void + */ + private function configure_scanner($scanner, $scan_session) { + try { + // Set auto-delete if cleanup is enabled + if ($scan_session['cleanup_files']) { + $scanner->setAutoDelete(); + Logger::warning('Auto-delete enabled for malware scan - files may be automatically removed.'); + } else { + // Enable auto-skip mode for non-interactive scanning + $scanner->setAutoSkip(); + } + + // Configure file size limits (default: scan files up to 10MB) + $max_filesize = get_option('malware_scanner_max_filesize', '10MB'); + if ($max_filesize && $max_filesize !== '-1') { + try { + // Set the argv parameter to avoid undefined key error + $scanner::$argv['max-filesize'] = $max_filesize; + $scanner->setMaxFilesize($max_filesize); + } catch (\Exception $e) { + Logger::warning('Failed to set max filesize: ' . $e->getMessage()); + } + } + + // Set ignore paths for the scanner (in addition to our own filtering) + $ignore_paths = $this->get_amw_ignore_paths($scan_session['ignore_folders']); + if (!empty($ignore_paths)) { + $scanner->setIgnorePaths($ignore_paths); + } + + // Configure scanning modes for better WordPress detection + // Enable signatures-only mode for WordPress (reduces false positives) + $scanner_settings = $scanner->getSettings(); + $scanner_settings['scan-exploits'] = false; // Disable exploits (reduces false positives) + $scanner_settings['scan-functions'] = false; // Disable functions (reduces false positives) + $scanner_settings['scan-signatures'] = true; // Keep signatures (best for WordPress) + $scanner_settings['silent'] = true; // Silent mode for programmatic use + $scanner_settings['colors'] = false; // Disable colors + + // Apply the settings (this mimics --only-signatures flag) + foreach ($scanner_settings as $key => $value) { + if (property_exists($scanner, 'settings') || isset($scanner::$settings)) { + $scanner::$settings[$key] = $value; + } + } + + Logger::info('AMWScanner configured with: Signatures-only mode, Max filesize: ' . $max_filesize . ', Ignore paths: ' . count($ignore_paths)); + + } catch (\Exception $e) { + Logger::error('Failed to configure AMWScanner: ' . $e->getMessage()); + // Continue with basic configuration if advanced setup fails + } + } + + /** + * Convert our ignore folders to AMWScanner compatible ignore paths + * + * @param array $ignore_folders + * @return array + */ + private function get_amw_ignore_paths($ignore_folders) { + $amw_ignore_paths = []; + + foreach ($ignore_folders as $folder) { + // Convert absolute paths to relative for AMWScanner + if (strpos($folder, ABSPATH) === 0) { + $relative_path = str_replace(ABSPATH, '', $folder); + $amw_ignore_paths[] = $relative_path; + } else { + $amw_ignore_paths[] = $folder; + } + } + + // Add common WordPress cache and temporary directories + $wordpress_ignore_paths = [ + 'wp-content/cache/*', + 'wp-content/uploads/cache/*', + 'wp-content/backups/*', + 'wp-content/upgrade/*', + '*.log', + '*.tmp', + '*.backup', + 'wp-content/debug.log', + ]; + + return array_merge($amw_ignore_paths, $wordpress_ignore_paths); + } + /** * Scan files using temporary directory (for larger batches) * @@ -737,9 +830,8 @@ private function scan_files_with_temp_directory($files, $scan_session) { $scanner = new AMWScanner(); $scanner->setPathScan($temp_dir); - if ($scan_session['cleanup_files']) { - $scanner->setAutoDelete(); - } + // Enhanced configuration for better malware detection + $this->configure_scanner($scanner, $scan_session); $raw_results = $scanner->run(); @@ -893,7 +985,7 @@ public function handle_malware_scan() { // Count the total number of files for logging or reporting (with memory efficiency) $scanned_file_count = $this->count_scannable_files($directoryToScan, $ignore_folders, $ignore_file_types); - // Perform the scan using the AMWScan library + // Perform the scan using the AMWScan library with enhanced configuration $scanner = new AMWScanner(); if (!$scanner) { @@ -901,12 +993,15 @@ public function handle_malware_scan() { } $scanner->setPathScan($directoryToScan); - $scanner->setIgnorePaths($ignore_folders); - if ($cleanup_files === "true") { - $scanner->setAutoDelete(); - Logger::warning('Auto-delete enabled for malware scan - files may be automatically removed.'); - } + // Create session-like array for configuration compatibility + $legacy_session = [ + 'ignore_folders' => $ignore_folders, + 'cleanup_files' => ($cleanup_files === "true") + ]; + + // Enhanced configuration for better malware detection + $this->configure_scanner($scanner, $legacy_session); $raw_report = $scanner->run(); @@ -975,8 +1070,20 @@ public function scan($directory) { throw new \Exception('Failed to initialize malware scanner.'); } - // Set the path and run the scanner - $report = $scanner->setPathScan($directory)->run(); + // Set the path + $scanner->setPathScan($directory); + + // Create basic session for configuration + $basic_session = [ + 'ignore_folders' => [], + 'cleanup_files' => false + ]; + + // Enhanced configuration for better malware detection + $this->configure_scanner($scanner, $basic_session); + + // Run the scanner + $report = $scanner->run(); if (!$report) { throw new \Exception('Scanner returned empty report.'); From 47032dd8a1523ef62ca54f8086c6ff1ca7bff6f5 Mon Sep 17 00:00:00 2001 From: Rajan Vijayan Date: Tue, 22 Jul 2025 12:06:29 +0530 Subject: [PATCH 4/8] update Scanner --- src/Admin/AdminPage.php | 4 +- src/Core/Activator.php | 3 + src/Core/Scanner.php | 286 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 283 insertions(+), 10 deletions(-) diff --git a/src/Admin/AdminPage.php b/src/Admin/AdminPage.php index 0e0741f..3e75933 100644 --- a/src/Admin/AdminPage.php +++ b/src/Admin/AdminPage.php @@ -80,7 +80,7 @@ function enqueue_admin_assets() { 'malware-scanner-admin-css', // Handle plugin_dir_url(__FILE__) . 'assets/css/admin-styles.css', // Path to CSS file [], // Dependencies - '2.5.0' // Version (fixed database and AMWScanner errors) + '2.6.0' // Version (comprehensive scanning improvements and diagnostics) ); // Enqueue Admin JS with cache busting @@ -88,7 +88,7 @@ function enqueue_admin_assets() { 'malware-scanner-admin-js', // Handle plugin_dir_url(__FILE__) . 'assets/js/admin-scripts.js', // Path to JS file ['jquery', 'chart-js'], // Dependencies (jQuery, Chart.js) - '2.5.0', // Version (fixed database and AMWScanner errors) + '2.6.0', // Version (comprehensive scanning improvements and diagnostics) true // Load in footer ); diff --git a/src/Core/Activator.php b/src/Core/Activator.php index 3de93b7..03c9deb 100644 --- a/src/Core/Activator.php +++ b/src/Core/Activator.php @@ -58,6 +58,9 @@ private static function set_default_options() { 'malware_scanner_scan_sensitivity_level' => 'medium', 'malware_scanner_exclude_files' => '', 'malware_scanner_email_alerts' => '1', + 'malware_scanner_mode' => 'comprehensive', // Default to comprehensive scanning + 'malware_scanner_max_filesize' => '10MB', // Default max file size to scan + 'malware_scanner_verify_coverage' => '0', // Disable batch coverage verification by default ]; // Only set defaults if options don't already exist diff --git a/src/Core/Scanner.php b/src/Core/Scanner.php index c827ffd..a305646 100644 --- a/src/Core/Scanner.php +++ b/src/Core/Scanner.php @@ -11,6 +11,7 @@ class Scanner { const SESSION_TIMEOUT = 1800; // 30 minutes public function __construct() { + // Register AJAX actions add_action('wp_ajax_start_malware_scan', [$this, 'handle_malware_scan']); add_action('wp_ajax_nopriv_start_malware_scan', [$this, 'handle_malware_scan']); @@ -108,6 +109,17 @@ public function handle_initialize_batch_scan() { // Store session (use transient with 30 min expiry) set_transient($session_id, $scan_session, self::SESSION_TIMEOUT); + // Verify batch coverage (optional verification for debugging) + $verify_coverage = get_option('malware_scanner_verify_coverage', '0') === '1'; + if ($verify_coverage && $total_batches > 0) { + Logger::info("Running batch coverage verification..."); + $coverage_results = $this->verify_batch_coverage($scan_directory, $ignore_folders, $ignore_file_types, $total_batches); + + if (isset($coverage_results['coverage_percentage'])) { + Logger::info("Batch coverage: {$coverage_results['coverage_percentage']}% ({$coverage_results['total_files_in_batches']}/{$coverage_results['total_files_expected']} files)"); + } + } + // Restore original memory limit ini_set('memory_limit', $original_memory_limit); @@ -365,6 +377,8 @@ public function handle_cancel_scan() { */ private function count_scannable_files($directory, $ignore_folders = [], $ignore_file_types = []) { $count = 0; + $ignored_count = 0; + $file_types_found = []; try { $iterator = new \RecursiveIteratorIterator( @@ -379,9 +393,19 @@ private function count_scannable_files($directory, $ignore_folders = [], $ignore $file_path = $file->getPathname(); $relative_path = str_replace($directory, '', $file_path); + + // Track file extensions found + $extension = pathinfo($file_path, PATHINFO_EXTENSION); + if (!empty($extension)) { + if (!isset($file_types_found[$extension])) { + $file_types_found[$extension] = 0; + } + $file_types_found[$extension]++; + } // Check if file should be ignored if ($this->should_ignore_file($relative_path, $ignore_folders, $ignore_file_types)) { + $ignored_count++; continue; } @@ -395,6 +419,20 @@ private function count_scannable_files($directory, $ignore_folders = [], $ignore } } } + + // Log file discovery statistics + $total_files = $count + $ignored_count; + Logger::info("File discovery completed: {$count} files to scan, {$ignored_count} files ignored, {$total_files} total files found"); + + // Log top file types found (for debugging) + arsort($file_types_found); + $top_types = array_slice($file_types_found, 0, 10, true); + $types_summary = []; + foreach ($top_types as $ext => $file_count) { + $types_summary[] = "{$ext}: {$file_count}"; + } + Logger::info("Top file types found: " . implode(', ', $types_summary)); + } catch (\Exception $e) { Logger::error('Error counting files: ' . $e->getMessage()); return 0; @@ -417,6 +455,7 @@ private function get_batch_files($directory, $batch_number, $ignore_folders = [] $start_index = $batch_number * self::BATCH_SIZE; $current_index = 0; $batch_count = 0; + $skipped_files = 0; try { $iterator = new \RecursiveIteratorIterator( @@ -434,6 +473,7 @@ private function get_batch_files($directory, $batch_number, $ignore_folders = [] // Check if file should be ignored if ($this->should_ignore_file($relative_path, $ignore_folders, $ignore_file_types)) { + $skipped_files++; continue; } @@ -441,13 +481,21 @@ private function get_batch_files($directory, $batch_number, $ignore_folders = [] if ($current_index >= $start_index && $batch_count < self::BATCH_SIZE) { $files[] = $file_path; $batch_count++; + + // Stop once we have enough files for this batch + if ($batch_count >= self::BATCH_SIZE) { + break; + } } else if ($current_index >= $start_index + self::BATCH_SIZE) { - // We've collected enough files for this batch + // We've gone past our batch range break; } $current_index++; } + + Logger::info("Batch {$batch_number}: Found {$batch_count} files (skipped {$skipped_files} files during iteration)"); + } catch (\Exception $e) { Logger::error('Error getting batch files: ' . $e->getMessage()); } @@ -455,6 +503,78 @@ private function get_batch_files($directory, $batch_number, $ignore_folders = [] return $files; } + /** + * Verify batch processing completeness by checking if all files are covered + * + * @param string $directory + * @param array $ignore_folders + * @param array $ignore_file_types + * @param int $total_batches + * @return array Verification results + */ + public function verify_batch_coverage($directory, $ignore_folders = [], $ignore_file_types = [], $total_batches = 0) { + $all_files_found = []; + $batch_files_found = []; + + try { + // Get all scannable files using the count method logic + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + if (!$file->isFile()) { + continue; + } + + $file_path = $file->getPathname(); + $relative_path = str_replace($directory, '', $file_path); + + if (!$this->should_ignore_file($relative_path, $ignore_folders, $ignore_file_types)) { + $all_files_found[] = $file_path; + } + } + + // Get files from all batches + for ($batch = 0; $batch < $total_batches; $batch++) { + $batch_files = $this->get_batch_files($directory, $batch, $ignore_folders, $ignore_file_types); + $batch_files_found = array_merge($batch_files_found, $batch_files); + } + + // Compare results + $all_count = count($all_files_found); + $batch_count = count($batch_files_found); + $missing_files = array_diff($all_files_found, $batch_files_found); + $extra_files = array_diff($batch_files_found, $all_files_found); + + $results = [ + 'total_files_expected' => $all_count, + 'total_files_in_batches' => $batch_count, + 'missing_files_count' => count($missing_files), + 'extra_files_count' => count($extra_files), + 'coverage_percentage' => $all_count > 0 ? round(($batch_count / $all_count) * 100, 2) : 100, + 'missing_files' => array_slice($missing_files, 0, 10), // First 10 missing files for debugging + 'is_complete' => (count($missing_files) === 0 && count($extra_files) === 0) + ]; + + if (!$results['is_complete']) { + Logger::warning("Batch coverage verification failed: Expected {$all_count} files, got {$batch_count} files in batches. Missing: " . count($missing_files) . ", Extra: " . count($extra_files)); + if (!empty($missing_files)) { + Logger::warning("Sample missing files: " . implode(', ', array_slice($missing_files, 0, 5))); + } + } else { + Logger::info("Batch coverage verification passed: All {$all_count} files are covered by batches"); + } + + return $results; + + } catch (\Exception $e) { + Logger::error('Batch coverage verification failed: ' . $e->getMessage()); + return ['error' => $e->getMessage()]; + } + } + /** * Get all scannable files from directory (DEPRECATED - kept for legacy compatibility) * @@ -636,6 +756,7 @@ private function process_file_batch($files, $scan_session) { * @return array */ private function scan_files_directly($files, $scan_session) { + $batch_results = [ 'detected' => 0, 'ignored' => [], @@ -713,29 +834,53 @@ private function configure_scanner($scanner, $scan_session) { } } + // IMPORTANT: Enable scanning of ALL file types, not just PHP + // This ensures the scanner checks all files for malware + try { + $scanner::$settings['scan-all'] = true; // Scan all file types + $scanner::$extensions = []; // Clear extension restrictions + } catch (\Exception $e) { + Logger::warning('Failed to enable all-file scanning: ' . $e->getMessage()); + } + // Set ignore paths for the scanner (in addition to our own filtering) $ignore_paths = $this->get_amw_ignore_paths($scan_session['ignore_folders']); if (!empty($ignore_paths)) { $scanner->setIgnorePaths($ignore_paths); } - // Configure scanning modes for better WordPress detection - // Enable signatures-only mode for WordPress (reduces false positives) + // Configure scanning modes for comprehensive malware detection $scanner_settings = $scanner->getSettings(); - $scanner_settings['scan-exploits'] = false; // Disable exploits (reduces false positives) - $scanner_settings['scan-functions'] = false; // Disable functions (reduces false positives) - $scanner_settings['scan-signatures'] = true; // Keep signatures (best for WordPress) + + // Enable comprehensive scanning instead of signatures-only + $scanning_mode = get_option('malware_scanner_mode', 'comprehensive'); + + if ($scanning_mode === 'signatures_only') { + // Signatures-only mode (faster, fewer false positives) + $scanner_settings['scan-exploits'] = false; + $scanner_settings['scan-functions'] = false; + $scanner_settings['scan-signatures'] = true; + Logger::info('AMWScanner: Using signatures-only mode (faster, fewer false positives)'); + } else { + // Comprehensive mode (slower, better detection) + $scanner_settings['scan-exploits'] = true; // Enable exploits detection + $scanner_settings['scan-functions'] = true; // Enable functions detection + $scanner_settings['scan-signatures'] = true; // Enable signatures detection + Logger::info('AMWScanner: Using comprehensive mode (better detection coverage)'); + } + $scanner_settings['silent'] = true; // Silent mode for programmatic use $scanner_settings['colors'] = false; // Disable colors + $scanner_settings['scan-all'] = true; // Scan all file types - // Apply the settings (this mimics --only-signatures flag) + // Apply the settings foreach ($scanner_settings as $key => $value) { if (property_exists($scanner, 'settings') || isset($scanner::$settings)) { $scanner::$settings[$key] = $value; } } - Logger::info('AMWScanner configured with: Signatures-only mode, Max filesize: ' . $max_filesize . ', Ignore paths: ' . count($ignore_paths)); + Logger::info('AMWScanner configured - Mode: ' . $scanning_mode . ', Max filesize: ' . $max_filesize . ', Ignore paths: ' . count($ignore_paths) . ', Scan all files: enabled'); } catch (\Exception $e) { Logger::error('Failed to configure AMWScanner: ' . $e->getMessage()); @@ -1046,6 +1191,131 @@ public function handle_malware_scan() { wp_die(); } + /** + * Diagnostic method to check scanner functionality and file discovery + * + * @param string $test_directory Optional directory to test (defaults to WordPress root) + * @return array Diagnostic results + */ + public function run_diagnostics($test_directory = null) { + if (!$test_directory) { + $test_directory = ABSPATH; + } + + $diagnostics = [ + 'timestamp' => current_time('mysql'), + 'test_directory' => $test_directory, + 'scanner_config' => [], + 'file_discovery' => [], + 'amw_scanner' => [], + 'recommendations' => [] + ]; + + Logger::info("Running scanner diagnostics for directory: {$test_directory}"); + + try { + // Test 1: Check AMWScanner availability + $diagnostics['amw_scanner']['available'] = class_exists('AMWScan\Scanner'); + $diagnostics['amw_scanner']['version'] = $diagnostics['amw_scanner']['available'] + ? (defined('AMWScan\Scanner::$version') ? \AMWScan\Scanner::$version : 'Unknown') + : 'Not available'; + + // Test 2: Check directory accessibility + $diagnostics['file_discovery']['directory_readable'] = is_readable($test_directory); + $diagnostics['file_discovery']['directory_exists'] = is_dir($test_directory); + + if ($diagnostics['file_discovery']['directory_readable']) { + // Test 3: File discovery + $start_time = microtime(true); + $file_count = $this->count_scannable_files($test_directory, [], []); + $discovery_time = round((microtime(true) - $start_time) * 1000, 2); + + $diagnostics['file_discovery']['total_files'] = $file_count; + $diagnostics['file_discovery']['discovery_time_ms'] = $discovery_time; + $diagnostics['file_discovery']['files_per_second'] = $discovery_time > 0 ? round($file_count / ($discovery_time / 1000), 2) : 0; + + // Test 4: Batch processing logic + if ($file_count > 0) { + $total_batches = ceil($file_count / self::BATCH_SIZE); + $diagnostics['file_discovery']['total_batches'] = $total_batches; + $diagnostics['file_discovery']['batch_size'] = self::BATCH_SIZE; + + // Test first batch + $first_batch = $this->get_batch_files($test_directory, 0, [], []); + $diagnostics['file_discovery']['first_batch_size'] = count($first_batch); + $diagnostics['file_discovery']['first_batch_files'] = array_slice($first_batch, 0, 3); // First 3 files for verification + + // Test if batches cover all files (small sample) + if ($total_batches <= 5) { // Only for small scans to avoid performance issues + $coverage = $this->verify_batch_coverage($test_directory, [], [], $total_batches); + $diagnostics['file_discovery']['batch_coverage'] = $coverage; + } + } + } + + // Test 5: Scanner configuration + if ($diagnostics['amw_scanner']['available']) { + $scanner = new AMWScanner(); + $test_session = ['ignore_folders' => [], 'cleanup_files' => false]; + + try { + $this->configure_scanner($scanner, $test_session); + $diagnostics['scanner_config']['configuration_success'] = true; + $diagnostics['scanner_config']['scanning_mode'] = get_option('malware_scanner_mode', 'comprehensive'); + $diagnostics['scanner_config']['max_filesize'] = get_option('malware_scanner_max_filesize', '10MB'); + } catch (\Exception $e) { + $diagnostics['scanner_config']['configuration_success'] = false; + $diagnostics['scanner_config']['configuration_error'] = $e->getMessage(); + } + } + + // Test 6: Memory and performance + $diagnostics['performance'] = [ + 'memory_limit' => ini_get('memory_limit'), + 'current_memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2), + 'peak_memory_usage_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2), + 'max_execution_time' => ini_get('max_execution_time'), + ]; + + // Generate recommendations + if (!$diagnostics['amw_scanner']['available']) { + $diagnostics['recommendations'][] = "AMWScanner library is not available. Check if composer dependencies are installed."; + } + + if (!$diagnostics['file_discovery']['directory_readable']) { + $diagnostics['recommendations'][] = "Test directory is not readable. Check file permissions."; + } + + if (isset($diagnostics['file_discovery']['total_files']) && $diagnostics['file_discovery']['total_files'] === 0) { + $diagnostics['recommendations'][] = "No files found to scan. Check if ignore settings are too restrictive."; + } + + if (isset($diagnostics['file_discovery']['files_per_second']) && $diagnostics['file_discovery']['files_per_second'] < 100) { + $diagnostics['recommendations'][] = "File discovery is slow. Consider checking disk performance or reducing directory size."; + } + + if (isset($diagnostics['file_discovery']['batch_coverage']['coverage_percentage']) && $diagnostics['file_discovery']['batch_coverage']['coverage_percentage'] < 100) { + $diagnostics['recommendations'][] = "Batch processing may be missing files. Coverage: " . $diagnostics['file_discovery']['batch_coverage']['coverage_percentage'] . "%"; + } + + if (empty($diagnostics['recommendations'])) { + $diagnostics['recommendations'][] = "All diagnostics passed. Scanner appears to be working correctly."; + } + + $diagnostics['overall_status'] = empty(array_filter($diagnostics['recommendations'], function($r) { + return strpos($r, 'All diagnostics passed') === false; + })) ? 'PASS' : 'ISSUES_FOUND'; + + } catch (\Exception $e) { + $diagnostics['error'] = $e->getMessage(); + $diagnostics['overall_status'] = 'ERROR'; + Logger::error('Scanner diagnostics failed: ' . $e->getMessage()); + } + + Logger::info("Scanner diagnostics completed with status: " . $diagnostics['overall_status']); + return $diagnostics; + } + /** * Function to scan the given directory for malware * From 22ecedd6e536dd4a38d353d951c9a01cda839d90 Mon Sep 17 00:00:00 2001 From: Rajan Vijayan Date: Tue, 22 Jul 2025 12:42:05 +0530 Subject: [PATCH 5/8] fix scaning issue - scan all variable issue --- src/Core/Scanner.php | 56 ++++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/src/Core/Scanner.php b/src/Core/Scanner.php index a305646..fc31729 100644 --- a/src/Core/Scanner.php +++ b/src/Core/Scanner.php @@ -54,9 +54,11 @@ public function handle_initialize_batch_scan() { ? sanitize_text_field($_POST['cleanup_files']) === 'true' : false; - // Add the plugin's own directory to ignored folders for safety + // Add only specific subdirectories to ignored folders for safety (but allow scanning malware samples) $plugin_base_path = str_replace(WP_PLUGIN_DIR . '/', '', dirname(__DIR__, 2)); - $ignore_folders[] = 'wp-content/plugins/' . $plugin_base_path; + $ignore_folders[] = 'wp-content/plugins/' . $plugin_base_path . '/vendor'; + $ignore_folders[] = 'wp-content/plugins/' . $plugin_base_path . '/logs'; + $ignore_folders[] = 'wp-content/plugins/' . $plugin_base_path . '/src'; $ignore_folders = array_unique($ignore_folders); // Log ignored folders for debugging @@ -826,22 +828,18 @@ private function configure_scanner($scanner, $scan_session) { $max_filesize = get_option('malware_scanner_max_filesize', '10MB'); if ($max_filesize && $max_filesize !== '-1') { try { - // Set the argv parameter to avoid undefined key error - $scanner::$argv['max-filesize'] = $max_filesize; $scanner->setMaxFilesize($max_filesize); } catch (\Exception $e) { Logger::warning('Failed to set max filesize: ' . $e->getMessage()); } } - // IMPORTANT: Enable scanning of ALL file types, not just PHP - // This ensures the scanner checks all files for malware - try { - $scanner::$settings['scan-all'] = true; // Scan all file types - $scanner::$extensions = []; // Clear extension restrictions - } catch (\Exception $e) { - Logger::warning('Failed to enable all-file scanning: ' . $e->getMessage()); - } + // CRITICAL FIX: Use proper AMWScanner methods instead of static properties + // This is the key fix that makes malware detection work properly + $scanner->setScanAll(true); // Enable scanning all file types + $scanner->setSilentMode(true); // Enable silent mode + $scanner->setColors(false); // Disable colors + $scanner->disableLiteMode(); // Disable lite mode for full scanning // Set ignore paths for the scanner (in addition to our own filtering) $ignore_paths = $this->get_amw_ignore_paths($scan_session['ignore_folders']); @@ -850,37 +848,21 @@ private function configure_scanner($scanner, $scan_session) { } // Configure scanning modes for comprehensive malware detection - $scanner_settings = $scanner->getSettings(); - - // Enable comprehensive scanning instead of signatures-only $scanning_mode = get_option('malware_scanner_mode', 'comprehensive'); if ($scanning_mode === 'signatures_only') { // Signatures-only mode (faster, fewer false positives) - $scanner_settings['scan-exploits'] = false; - $scanner_settings['scan-functions'] = false; - $scanner_settings['scan-signatures'] = true; + $scanner->setOnlySignaturesMode(true); Logger::info('AMWScanner: Using signatures-only mode (faster, fewer false positives)'); } else { - // Comprehensive mode (slower, better detection) - $scanner_settings['scan-exploits'] = true; // Enable exploits detection - $scanner_settings['scan-functions'] = true; // Enable functions detection - $scanner_settings['scan-signatures'] = true; // Enable signatures detection + // Comprehensive mode (slower, better detection) - DEFAULT + $scanner->setOnlySignaturesMode(false); // Enable all scanning modes + $scanner->setOnlyFunctionsMode(false); // Enable all scanning modes + $scanner->setOnlyExploitsMode(false); // Enable all scanning modes Logger::info('AMWScanner: Using comprehensive mode (better detection coverage)'); } - $scanner_settings['silent'] = true; // Silent mode for programmatic use - $scanner_settings['colors'] = false; // Disable colors - $scanner_settings['scan-all'] = true; // Scan all file types - - // Apply the settings - foreach ($scanner_settings as $key => $value) { - if (property_exists($scanner, 'settings') || isset($scanner::$settings)) { - $scanner::$settings[$key] = $value; - } - } - - Logger::info('AMWScanner configured - Mode: ' . $scanning_mode . ', Max filesize: ' . $max_filesize . ', Ignore paths: ' . count($ignore_paths) . ', Scan all files: enabled'); + Logger::info('AMWScanner configured successfully - Mode: ' . $scanning_mode . ', Max filesize: ' . $max_filesize . ', Ignore paths: ' . count($ignore_paths) . ', Scan all files: enabled'); } catch (\Exception $e) { Logger::error('Failed to configure AMWScanner: ' . $e->getMessage()); @@ -1114,9 +1096,11 @@ public function handle_malware_scan() { ? sanitize_text_field($_POST['cleanup_files']) : ''; - // Add the plugin's own directory to ignored folders for safety + // Add only specific subdirectories to ignored folders for safety (but allow scanning malware samples) $plugin_base_path = str_replace(WP_PLUGIN_DIR . '/', '', dirname(__DIR__, 2)); - $ignore_folders[] = 'wp-content/plugins/' . $plugin_base_path; + $ignore_folders[] = 'wp-content/plugins/' . $plugin_base_path . '/vendor'; + $ignore_folders[] = 'wp-content/plugins/' . $plugin_base_path . '/logs'; + $ignore_folders[] = 'wp-content/plugins/' . $plugin_base_path . '/src'; $ignore_folders = array_unique($ignore_folders); // Define the directory to scan (WordPress root) From 702b6f0ec5cd52e137215993f97db02c9282dd85 Mon Sep 17 00:00:00 2001 From: Rajan Vijayan Date: Tue, 22 Jul 2025 14:42:03 +0530 Subject: [PATCH 6/8] debug db issue --- src/Admin/views/main.php | 96 +++++++++++++++++++++++++++++++++------- src/Core/Scanner.php | 8 +++- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/src/Admin/views/main.php b/src/Admin/views/main.php index 768af8e..91c5cc4 100644 --- a/src/Admin/views/main.php +++ b/src/Admin/views/main.php @@ -5,57 +5,98 @@ global $wpdb; $mysql_version = $wpdb->get_var("SELECT VERSION()"); -$recent_logs = Logger::get_recent_logs(); $current_version = Globals::get_version(); $plugin_update_available = Globals::is_update_available(); $new_version = $plugin_update_available ? Globals::get_latest_version() : null; -// Fetch the latest scan_result JSON -$latest_scan = $wpdb->get_var("SELECT scan_result FROM {$wpdb->prefix}malware_scan_logs ORDER BY id DESC LIMIT 1"); +// Get recent logs for activity table +$recent_logs = Logger::get_recent_logs(); + +// Fetch the latest scan result with error handling +$table_name = $wpdb->prefix . 'malware_scan_logs'; +$latest_scan = null; +$scan_meta = null; + +// Check if table exists first +$table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) === $table_name; -$scan_meta = $wpdb->get_row("SELECT scan_result, scan_date FROM {$wpdb->prefix}malware_scan_logs ORDER BY id DESC LIMIT 1", ARRAY_A); +if ($table_exists) { + $latest_scan = $wpdb->get_var("SELECT scan_result FROM {$table_name} ORDER BY id DESC LIMIT 1"); + $scan_meta = $wpdb->get_row("SELECT scan_result, scan_date FROM {$table_name} ORDER BY id DESC LIMIT 1", ARRAY_A); + + // Log any database errors + if ($wpdb->last_error) { + error_log('[MalwareScanner] Dashboard database error: ' . $wpdb->last_error); + } +} +// Initialize default values $total_files_scanned = 0; $malware_detected = 0; $ignored_files_count = 0; $last_scan_date = __('Not Available', 'malware-scanner'); $scan_frequency = __('Not Set', 'malware-scanner'); +$data_source = 'none'; // Track where data comes from for debugging +// Process scan metadata if available if ($scan_meta && !empty($scan_meta['scan_result'])) { $scan_data = json_decode($scan_meta['scan_result'], true); + $json_error = json_last_error(); - if (json_last_error() === JSON_ERROR_NONE && is_array($scan_data)) { + if ($json_error === JSON_ERROR_NONE && is_array($scan_data)) { $total_files_scanned = isset($scan_data['totalFilesScanned']) ? (int) $scan_data['totalFilesScanned'] : 0; $malware_detected = isset($scan_data['malwareDetected']) ? (int) $scan_data['malwareDetected'] : 0; $ignored_files_count = !empty($scan_data['ignoredFiles']) ? count($scan_data['ignoredFiles']) : 0; + $data_source = 'database'; + } else { + // Log JSON parsing error for debugging + error_log('[MalwareScanner] Dashboard JSON parse error: ' . json_last_error_msg()); + $data_source = 'json_error'; } + // Process scan date if (!empty($scan_meta['scan_date'])) { $last_scan_date = date('M d, Y H:i:s', strtotime($scan_meta['scan_date'])); } +} else if (!$table_exists) { + $data_source = 'no_table'; +} else { + $data_source = 'no_data'; } +// Get scan frequency setting $scan_frequency = get_option('malware_scanner_schedule_frequency'); $scan_frequency = $scan_frequency ? ucfirst($scan_frequency) : __('Not Set', 'malware-scanner'); -if ($latest_scan) { - $results = json_decode($latest_scan, true); +// Set notification message based on data source and scan results +$notification_class = 'notice-warning'; +$notification_message = __('No scan data available.', 'malware-scanner'); - if (json_last_error() === JSON_ERROR_NONE && is_array($results)) { - if (isset($results['malwareDetected']) && intval($results['malwareDetected']) > 0) { +switch ($data_source) { + case 'database': + if ($malware_detected > 0) { $notification_class = 'notice-error'; - $notification_message = __('Warning: Malware detected in core files. Please review the scan report.', 'malware-scanner'); + $notification_message = sprintf(__('Warning: %d malware threats detected. Please review the scan report.', 'malware-scanner'), $malware_detected); } else { $notification_class = 'notice-success'; - $notification_message = __('Your system is secure. No issues detected.', 'malware-scanner'); + $notification_message = sprintf(__('System secure: Scanned %d files, no threats detected.', 'malware-scanner'), $total_files_scanned); } - } else { + break; + + case 'json_error': $notification_class = 'notice-warning'; $notification_message = __('Scan results could not be parsed. Please rerun the scan.', 'malware-scanner'); - } -} else { - $notification_class = 'notice-warning'; - $notification_message = __('No scan record found. Please run a scan to ensure your system is safe.', 'malware-scanner'); + break; + + case 'no_table': + $notification_class = 'notice-warning'; + $notification_message = __('Scan logs table not found. Please run a scan to initialize the system.', 'malware-scanner'); + break; + + case 'no_data': + $notification_class = 'notice-warning'; + $notification_message = __('No scan records found. Please run a scan to ensure your system is safe.', 'malware-scanner'); + break; } $settings = [ @@ -72,8 +113,23 @@ // REST API status $rest_api_enabled = rest_get_server() ? true : false; $settings['rest_api'] = $rest_api_enabled ? 'Enabled' : 'Disabled'; +// Debug information for troubleshooting (only visible in HTML source) +$debug_info = [ + 'data_source' => $data_source, + 'table_exists' => $table_exists ? 'yes' : 'no', + 'recent_logs_count' => count($recent_logs), + 'scan_meta_exists' => $scan_meta ? 'yes' : 'no', + 'total_files_scanned' => $total_files_scanned, + 'malware_detected' => $malware_detected, + 'ignored_files_count' => $ignored_files_count, + 'last_scan_date' => $last_scan_date, + 'scan_frequency' => $scan_frequency, + 'wpdb_last_error' => $wpdb->last_error ?: 'none' +]; ?> + +

📊

@@ -174,7 +230,13 @@ - + + + + + + + diff --git a/src/Core/Scanner.php b/src/Core/Scanner.php index fc31729..1e2454e 100644 --- a/src/Core/Scanner.php +++ b/src/Core/Scanner.php @@ -232,6 +232,8 @@ public function handle_process_scan_batch() { $progress_percentage = round(($scan_session['processed_files'] / $scan_session['total_files']) * 100, 1); $is_complete = $scan_session['processed_files'] >= $scan_session['total_files']; + Logger::info("BATCH_DEBUG: Batch {$batch_number} - Processed: {$scan_session['processed_files']}, Total: {$scan_session['total_files']}, Is_Complete: " . ($is_complete ? 'TRUE' : 'FALSE')); + // If scan is complete, handle final tasks if ($is_complete) { $scan_session['status'] = 'completed'; @@ -249,8 +251,10 @@ public function handle_process_scan_batch() { 'infectedFilesFound' => $scan_session['scan_results']['infectedFound'], ]; - // Log the final scan results - Logger::log($formatted_report, 'scan', 'manual'); + // Log the final scan results with debug info + Logger::info("SCAN_COMPLETION: About to log final scan results - Files: {$formatted_report['totalFilesScanned']}, Threats: {$formatted_report['malwareDetected']}"); + $log_success = Logger::log($formatted_report, 'scan', 'manual'); + Logger::info("SCAN_COMPLETION: Logger::log() returned: " . ($log_success ? 'SUCCESS' : 'FAILED')); // Send notification email if enabled $email_alerts_enabled = get_option('malware_scanner_email_alerts', '1') === '1'; From 6de3be5e5515c675e1f01427825f743e2f2c4d20 Mon Sep 17 00:00:00 2001 From: Rajan Vijayan Date: Thu, 7 Aug 2025 10:30:43 +0530 Subject: [PATCH 7/8] update scanner --- src/Core/Scanner.php | 197 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 177 insertions(+), 20 deletions(-) diff --git a/src/Core/Scanner.php b/src/Core/Scanner.php index 1e2454e..6bed20c 100644 --- a/src/Core/Scanner.php +++ b/src/Core/Scanner.php @@ -786,16 +786,33 @@ private function scan_files_directly($files, $scan_session) { // Enhanced configuration for better malware detection $this->configure_scanner($scanner, $scan_session); - $file_result = $scanner->run(); - - if ($file_result) { - $batch_results['detected'] += intval($file_result->detected ?? 0); - $batch_results['ignored'] = array_merge($batch_results['ignored'], (array)($file_result->ignored ?? [])); - $batch_results['removed'] = array_merge($batch_results['removed'], (array)($file_result->removed ?? [])); - $batch_results['edited'] = array_merge($batch_results['edited'], (array)($file_result->edited ?? [])); - $batch_results['quarantine'] = array_merge($batch_results['quarantine'], (array)($file_result->quarantine ?? [])); - $batch_results['whitelist'] = array_merge($batch_results['whitelist'], (array)($file_result->whitelist ?? [])); - $batch_results['infectedFound'] = array_merge($batch_results['infectedFound'], (array)($file_result->infectedFound ?? [])); + // Add timeout and error handling to prevent hanging + try { + // Set a reasonable timeout for individual file scanning + set_time_limit(30); // 30 seconds per file + + $file_result = $scanner->run(); + + // Reset time limit + set_time_limit(0); + + if ($file_result) { + // Filter WordPress false positives + $file_result = $this->filter_wordpress_false_positives($file_result); + + $batch_results['detected'] += intval($file_result->detected ?? 0); + $batch_results['ignored'] = array_merge($batch_results['ignored'], (array)($file_result->ignored ?? [])); + $batch_results['removed'] = array_merge($batch_results['removed'], (array)($file_result->removed ?? [])); + $batch_results['edited'] = array_merge($batch_results['edited'], (array)($file_result->edited ?? [])); + $batch_results['quarantine'] = array_merge($batch_results['quarantine'], (array)($file_result->quarantine ?? [])); + $batch_results['whitelist'] = array_merge($batch_results['whitelist'], (array)($file_result->whitelist ?? [])); + $batch_results['infectedFound'] = array_merge($batch_results['infectedFound'], (array)($file_result->infectedFound ?? [])); + } + } catch (\Exception $e) { + Logger::error("Error scanning file {$file}: " . $e->getMessage()); + // Continue with next file instead of failing the entire batch + set_time_limit(0); // Reset time limit on error + continue; } // Force garbage collection after each file to prevent memory buildup @@ -828,23 +845,31 @@ private function configure_scanner($scanner, $scan_session) { $scanner->setAutoSkip(); } - // Configure file size limits (default: scan files up to 10MB) + // Configure file size limits using the correct AMWScanner method $max_filesize = get_option('malware_scanner_max_filesize', '10MB'); if ($max_filesize && $max_filesize !== '-1') { try { - $scanner->setMaxFilesize($max_filesize); + // Convert to bytes for AMWScanner + $max_bytes = $this->convert_filesize_to_bytes($max_filesize); + if ($max_bytes > 0) { + // Use the correct method name and format for AMWScanner + $scanner->setMaxFileSize($max_bytes); + Logger::info('AMWScanner: Max file size set to ' . $max_filesize . ' (' . $max_bytes . ' bytes)'); + } } catch (\Exception $e) { - Logger::warning('Failed to set max filesize: ' . $e->getMessage()); + Logger::warning('Failed to set max file size: ' . $e->getMessage()); } } - // CRITICAL FIX: Use proper AMWScanner methods instead of static properties - // This is the key fix that makes malware detection work properly + // CRITICAL FIX: Use proper AMWScanner methods $scanner->setScanAll(true); // Enable scanning all file types $scanner->setSilentMode(true); // Enable silent mode $scanner->setColors(false); // Disable colors $scanner->disableLiteMode(); // Disable lite mode for full scanning + // Set report mode to prevent file writing issues + $scanner->setReportMode(true); // Use report mode instead of file operations + // Set ignore paths for the scanner (in addition to our own filtering) $ignore_paths = $this->get_amw_ignore_paths($scan_session['ignore_folders']); if (!empty($ignore_paths)) { @@ -866,13 +891,110 @@ private function configure_scanner($scanner, $scan_session) { Logger::info('AMWScanner: Using comprehensive mode (better detection coverage)'); } - Logger::info('AMWScanner configured successfully - Mode: ' . $scanning_mode . ', Max filesize: ' . $max_filesize . ', Ignore paths: ' . count($ignore_paths) . ', Scan all files: enabled'); + // Set execution timeout to prevent hanging - Remove this as it doesn't exist + // $scanner->setExecutionTimeout(300); // 5 minutes max per scan + + Logger::info('AMWScanner configured successfully - Mode: ' . $scanning_mode . ', Max filesize: ' . $max_filesize . ', Ignore paths: ' . count($ignore_paths)); } catch (\Exception $e) { Logger::error('Failed to configure AMWScanner: ' . $e->getMessage()); // Continue with basic configuration if advanced setup fails } } + + /** + * Convert filesize string to bytes + * + * @param string $filesize + * @return int + */ + private function convert_filesize_to_bytes($filesize) { + if (is_numeric($filesize)) { + return (int)$filesize; + } + + $unit = strtoupper(substr($filesize, -2)); + $value = (int)substr($filesize, 0, -2); + + switch ($unit) { + case 'GB': + return $value * 1024 * 1024 * 1024; + case 'MB': + return $value * 1024 * 1024; + case 'KB': + return $value * 1024; + default: + // Try to parse common formats + if (preg_match('/^(\d+)\s*(gb|mb|kb)?$/i', $filesize, $matches)) { + $val = (int)$matches[1]; + $unit = strtolower($matches[2] ?? 'mb'); + + switch ($unit) { + case 'gb': + return $val * 1024 * 1024 * 1024; + case 'mb': + return $val * 1024 * 1024; + case 'kb': + return $val * 1024; + default: + return $val * 1024 * 1024; // Default to MB + } + } + return 10 * 1024 * 1024; // Default 10MB + } + } + + /** + * Filter AMWScanner results to remove WordPress false positives + * + * @param object $raw_results + * @return object + */ + private function filter_wordpress_false_positives($raw_results) { + if (!$raw_results) { + return $raw_results; + } + + // Import WordPress validator if available + if (!class_exists('VPlugins\MalwareScanner\Core\WordPressValidator')) { + // If WordPress validator isn't available, return original results + return $raw_results; + } + + $filtered_results = clone $raw_results; + $original_infected = (array)($raw_results->infectedFound ?? []); + $filtered_infected = []; + $wp_files_skipped = 0; + + foreach ($original_infected as $infected_file) { + // Check if this is a WordPress core file + if (\VPlugins\MalwareScanner\Core\WordPressValidator::is_wp_core_file($infected_file)) { + // Double-check with content analysis for WordPress patterns + if (file_exists($infected_file) && is_readable($infected_file)) { + $content = file_get_contents($infected_file); + if ($content !== false && \VPlugins\MalwareScanner\Core\WordPressValidator::has_legitimate_wp_patterns($content)) { + // This is a legitimate WordPress file, skip it + Logger::info("WordPress false positive filtered: " . basename($infected_file)); + $wp_files_skipped++; + continue; + } + } + } + + // File is not a WordPress false positive, keep it in results + $filtered_infected[] = $infected_file; + } + + // Update the filtered results + $filtered_results->infectedFound = $filtered_infected; + $filtered_results->detected = count($filtered_infected); + + if ($wp_files_skipped > 0) { + Logger::info("WordPress false positives filtered: {$wp_files_skipped} files"); + } + + return $filtered_results; + } /** * Convert our ignore folders to AMWScanner compatible ignore paths @@ -964,9 +1086,21 @@ private function scan_files_with_temp_directory($files, $scan_session) { // Enhanced configuration for better malware detection $this->configure_scanner($scanner, $scan_session); - $raw_results = $scanner->run(); + // Add timeout protection for batch scanning + try { + set_time_limit(600); // 10 minutes max for batch + $raw_results = $scanner->run(); + set_time_limit(0); // Reset + } catch (\Exception $e) { + Logger::error("Error in batch scanning: " . $e->getMessage()); + set_time_limit(0); // Reset on error + $raw_results = null; + } if ($raw_results) { + // Filter WordPress false positives + $raw_results = $this->filter_wordpress_false_positives($raw_results); + $batch_results['detected'] += intval($raw_results->detected ?? 0); $batch_results['ignored'] = array_merge($batch_results['ignored'], (array)($raw_results->ignored ?? [])); $batch_results['removed'] = array_merge($batch_results['removed'], (array)($raw_results->removed ?? [])); @@ -1136,11 +1270,23 @@ public function handle_malware_scan() { // Enhanced configuration for better malware detection $this->configure_scanner($scanner, $legacy_session); - $raw_report = $scanner->run(); + // Add timeout protection for main directory scanning + try { + set_time_limit(1800); // 30 minutes max for full directory scan + $raw_report = $scanner->run(); + set_time_limit(0); // Reset + } catch (\Exception $e) { + Logger::error("Error in main directory scan: " . $e->getMessage()); + set_time_limit(0); // Reset on error + throw new \Exception('Scanner operation failed: ' . $e->getMessage()); + } if (!$raw_report) { throw new \Exception('Scanner returned empty report.'); } + + // Filter WordPress false positives from the main scan results + $raw_report = $this->filter_wordpress_false_positives($raw_report); // Format the report for consistent structure $formatted_report = $this->formatReport($raw_report); @@ -1340,12 +1486,23 @@ public function scan($directory) { // Enhanced configuration for better malware detection $this->configure_scanner($scanner, $basic_session); - // Run the scanner - $report = $scanner->run(); + // Run the scanner with timeout protection + try { + set_time_limit(900); // 15 minutes max for directory scan + $report = $scanner->run(); + set_time_limit(0); // Reset + } catch (\Exception $e) { + Logger::error("Error in directory scan: " . $e->getMessage()); + set_time_limit(0); // Reset on error + throw new \Exception('Scanner operation failed: ' . $e->getMessage()); + } if (!$report) { throw new \Exception('Scanner returned empty report.'); } + + // Filter WordPress false positives + $report = $this->filter_wordpress_false_positives($report); return $report; From fbf5df0943b8eeeb8f8a9a210923c1b2b7f3bf62 Mon Sep 17 00:00:00 2001 From: Rajan Vijayan Date: Thu, 7 Aug 2025 12:52:23 +0530 Subject: [PATCH 8/8] update wordpress signature --- src/Core/Scanner.php | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Core/Scanner.php b/src/Core/Scanner.php index 6bed20c..6081a29 100644 --- a/src/Core/Scanner.php +++ b/src/Core/Scanner.php @@ -864,11 +864,12 @@ private function configure_scanner($scanner, $scan_session) { // CRITICAL FIX: Use proper AMWScanner methods $scanner->setScanAll(true); // Enable scanning all file types $scanner->setSilentMode(true); // Enable silent mode - $scanner->setColors(false); // Disable colors - $scanner->disableLiteMode(); // Disable lite mode for full scanning + //$scanner->setColors(false); // Disable colors + + $scanner->enableDeobfuscateMode(); // Set report mode to prevent file writing issues - $scanner->setReportMode(true); // Use report mode instead of file operations + //$scanner->setReportMode(true); // Use report mode instead of file operations // Set ignore paths for the scanner (in addition to our own filtering) $ignore_paths = $this->get_amw_ignore_paths($scan_session['ignore_folders']); @@ -879,17 +880,19 @@ private function configure_scanner($scanner, $scan_session) { // Configure scanning modes for comprehensive malware detection $scanning_mode = get_option('malware_scanner_mode', 'comprehensive'); - if ($scanning_mode === 'signatures_only') { - // Signatures-only mode (faster, fewer false positives) - $scanner->setOnlySignaturesMode(true); - Logger::info('AMWScanner: Using signatures-only mode (faster, fewer false positives)'); - } else { - // Comprehensive mode (slower, better detection) - DEFAULT - $scanner->setOnlySignaturesMode(false); // Enable all scanning modes - $scanner->setOnlyFunctionsMode(false); // Enable all scanning modes - $scanner->setOnlyExploitsMode(false); // Enable all scanning modes - Logger::info('AMWScanner: Using comprehensive mode (better detection coverage)'); - } + // if ($scanning_mode === 'signatures_only') { + // // Signatures-only mode (faster, fewer false positives) + // $scanner->setOnlySignaturesMode(true); + // Logger::info('AMWScanner: Using signatures-only mode (faster, fewer false positives)'); + // } else { + // // Comprehensive mode (slower, better detection) - DEFAULT + // $scanner->setOnlySignaturesMode(false); // Enable all scanning modes + // $scanner->setOnlyFunctionsMode(false); // Enable all scanning modes + // $scanner->setOnlyExploitsMode(false); // Enable all scanning modes + // Logger::info('AMWScanner: Using comprehensive mode (better detection coverage)'); + // } + + $scanner->enableLiteMode(); // Set execution timeout to prevent hanging - Remove this as it doesn't exist // $scanner->setExecutionTimeout(300); // 5 minutes max per scan