From e3b647521e5a5728e8ac3a00dc3558894a1e0d41 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Thu, 20 Nov 2025 08:41:02 -0600 Subject: [PATCH 1/7] Fix scan output file paths and improve notification system - Save JSON/HTML/ZIP paths to database when scans complete - Remove orphaned scan-config-id reference causing JS errors - Add showAlert function to scan_detail.html and scans.html - Increase notification z-index to 9999 for modal visibility - Replace inline alert creation with consistent toast notifications --- app/web/jobs/scan_job.py | 4 +-- app/web/services/scan_service.py | 12 ++++++++- app/web/templates/base.html | 2 +- app/web/templates/scan_detail.html | 22 ++++++++++++++-- app/web/templates/scans.html | 42 +++++++++++++++++------------- 5 files changed, 58 insertions(+), 24 deletions(-) diff --git a/app/web/jobs/scan_job.py b/app/web/jobs/scan_job.py index ace3f8c..d7fcb72 100644 --- a/app/web/jobs/scan_job.py +++ b/app/web/jobs/scan_job.py @@ -77,12 +77,12 @@ def execute_scan(scan_id: int, config_id: int, db_url: str = None): # Generate output files (JSON, HTML, ZIP) logger.info(f"Scan {scan_id}: Generating output files...") - scanner.generate_outputs(report, timestamp) + output_paths = scanner.generate_outputs(report, timestamp) # Save results to database logger.info(f"Scan {scan_id}: Saving results to database...") scan_service = ScanService(session) - scan_service._save_scan_to_db(report, scan_id, status='completed') + scan_service._save_scan_to_db(report, scan_id, status='completed', output_paths=output_paths) # Evaluate alert rules logger.info(f"Scan {scan_id}: Evaluating alert rules...") diff --git a/app/web/services/scan_service.py b/app/web/services/scan_service.py index 7caedf6..4564986 100644 --- a/app/web/services/scan_service.py +++ b/app/web/services/scan_service.py @@ -308,7 +308,7 @@ class ScanService: return count def _save_scan_to_db(self, report: Dict[str, Any], scan_id: int, - status: str = 'completed') -> None: + status: str = 'completed', output_paths: Dict = None) -> None: """ Save scan results to database. @@ -319,6 +319,7 @@ class ScanService: report: Scan report dictionary from scanner scan_id: Scan ID to update status: Final scan status (completed or failed) + output_paths: Dictionary with paths to generated files {'json': Path, 'html': Path, 'zip': Path} """ scan = self.db.query(Scan).filter(Scan.id == scan_id).first() if not scan: @@ -329,6 +330,15 @@ class ScanService: scan.duration = report.get('scan_duration') scan.completed_at = datetime.utcnow() + # Save output file paths + if output_paths: + if 'json' in output_paths: + scan.json_path = str(output_paths['json']) + if 'html' in output_paths: + scan.html_path = str(output_paths['html']) + if 'zip' in output_paths: + scan.zip_path = str(output_paths['zip']) + # Map report data to database models self._map_report_to_models(report, scan) diff --git a/app/web/templates/base.html b/app/web/templates/base.html index b811060..606a6d6 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -108,7 +108,7 @@ -
+
{% block scripts %}{% endblock %} diff --git a/app/web/templates/scan_detail.html b/app/web/templates/scan_detail.html index 2111203..926a9ca 100644 --- a/app/web/templates/scan_detail.html +++ b/app/web/templates/scan_detail.html @@ -162,6 +162,25 @@ let scanData = null; let historyChart = null; // Store chart instance to prevent duplicates + // Show alert notification + function showAlert(type, message) { + const container = document.getElementById('notification-container'); + const notification = document.createElement('div'); + notification.className = `alert alert-${type} alert-dismissible fade show mb-2`; + + notification.innerHTML = ` + ${message} + + `; + + container.appendChild(notification); + + // Auto-dismiss after 5 seconds + setTimeout(() => { + notification.remove(); + }, 5000); + } + // Load scan on page load document.addEventListener('DOMContentLoaded', function() { loadScan().then(() => { @@ -218,7 +237,6 @@ document.getElementById('scan-timestamp').textContent = new Date(scan.timestamp).toLocaleString(); document.getElementById('scan-duration').textContent = scan.duration ? `${scan.duration.toFixed(1)}s` : '-'; document.getElementById('scan-triggered-by').textContent = scan.triggered_by || 'manual'; - document.getElementById('scan-config-id').textContent = scan.config_id || '-'; // Status badge let statusBadge = ''; @@ -439,7 +457,7 @@ window.location.href = '{{ url_for("main.scans") }}'; } catch (error) { console.error('Error deleting scan:', error); - alert(`Failed to delete scan: ${error.message}`); + showAlert('danger', `Failed to delete scan: ${error.message}`); // Re-enable button on error deleteBtn.disabled = false; diff --git a/app/web/templates/scans.html b/app/web/templates/scans.html index d15705b..3e5ac3b 100644 --- a/app/web/templates/scans.html +++ b/app/web/templates/scans.html @@ -151,6 +151,25 @@ let statusFilter = ''; let totalCount = 0; + // Show alert notification + function showAlert(type, message) { + const container = document.getElementById('notification-container'); + const notification = document.createElement('div'); + notification.className = `alert alert-${type} alert-dismissible fade show mb-2`; + + notification.innerHTML = ` + ${message} + + `; + + container.appendChild(notification); + + // Auto-dismiss after 5 seconds + setTimeout(() => { + notification.remove(); + }, 5000); + } + // Load initial data when page loads document.addEventListener('DOMContentLoaded', function() { loadScans(); @@ -456,15 +475,7 @@ bootstrap.Modal.getInstance(document.getElementById('triggerScanModal')).hide(); // Show success message - const alertDiv = document.createElement('div'); - alertDiv.className = 'alert alert-success alert-dismissible fade show mt-3'; - alertDiv.innerHTML = ` - Scan triggered successfully! (ID: ${data.scan_id}) - - `; - // Insert at the beginning of container-fluid - const container = document.querySelector('.container-fluid'); - container.insertBefore(alertDiv, container.firstChild); + showAlert('success', `Scan triggered successfully! (ID: ${data.scan_id})`); // Refresh scans loadScans(); @@ -490,23 +501,18 @@ }); if (!response.ok) { - throw new Error('Failed to delete scan'); + const data = await response.json(); + throw new Error(data.message || 'Failed to delete scan'); } // Show success message - const alertDiv = document.createElement('div'); - alertDiv.className = 'alert alert-success alert-dismissible fade show mt-3'; - alertDiv.innerHTML = ` - Scan ${scanId} deleted successfully. - - `; - document.querySelector('.container-fluid').insertBefore(alertDiv, document.querySelector('.row')); + showAlert('success', `Scan ${scanId} deleted successfully.`); // Refresh scans loadScans(); } catch (error) { console.error('Error deleting scan:', error); - alert('Failed to delete scan. Please try again.'); + showAlert('danger', `Failed to delete scan: ${error.message}`); } } From 9804f9c032c7ee0e3a1943cc5a528b27833f8a42 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Thu, 20 Nov 2025 09:32:28 -0600 Subject: [PATCH 2/7] Add route to serve scan output files Output files (JSON, HTML, ZIP) are stored outside the static directory, so download links in scan_detail.html were broken. This adds a /output/ route that serves files from the output directory using send_from_directory for secure file access. Route requires authentication. --- app/web/routes/main.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/web/routes/main.py b/app/web/routes/main.py index ac15a97..583fdc5 100644 --- a/app/web/routes/main.py +++ b/app/web/routes/main.py @@ -5,8 +5,9 @@ Provides dashboard and scan viewing pages. """ import logging +import os -from flask import Blueprint, current_app, redirect, render_template, url_for +from flask import Blueprint, current_app, redirect, render_template, send_from_directory, url_for from web.auth.decorators import login_required @@ -244,3 +245,19 @@ def alert_rules(): 'alert_rules.html', rules=rules ) + + +@bp.route('/output/') +@login_required +def serve_output_file(filename): + """ + Serve output files (JSON, HTML, ZIP) from the output directory. + + Args: + filename: Name of the file to serve + + Returns: + The requested file + """ + output_dir = os.environ.get('OUTPUT_DIR', '/app/output') + return send_from_directory(output_dir, filename) From cc3758f92dbde8d6f47b15136a746c1aab27a6fd Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Thu, 20 Nov 2025 09:35:13 -0600 Subject: [PATCH 3/7] Add acknowledge all alerts feature Add POST /api/alerts/acknowledge-all endpoint to bulk acknowledge all unacknowledged alerts. Add "Ack All" button to alerts page header with confirmation dialog for quick dismissal of all pending alerts. --- app/web/api/alerts.py | 41 +++++++++++++++++++++++++++++++++++ app/web/templates/alerts.html | 40 +++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/app/web/api/alerts.py b/app/web/api/alerts.py index 8315dae..18446d2 100644 --- a/app/web/api/alerts.py +++ b/app/web/api/alerts.py @@ -146,6 +146,47 @@ def acknowledge_alert(alert_id): }), 400 +@bp.route('/acknowledge-all', methods=['POST']) +@api_auth_required +def acknowledge_all_alerts(): + """ + Acknowledge all unacknowledged alerts. + + Returns: + JSON response with count of acknowledged alerts + """ + acknowledged_by = request.json.get('acknowledged_by', 'api') if request.json else 'api' + + try: + # Get all unacknowledged alerts + unacked_alerts = current_app.db_session.query(Alert).filter( + Alert.acknowledged == False + ).all() + + count = 0 + for alert in unacked_alerts: + alert.acknowledged = True + alert.acknowledged_at = datetime.now(timezone.utc) + alert.acknowledged_by = acknowledged_by + count += 1 + + current_app.db_session.commit() + + return jsonify({ + 'status': 'success', + 'message': f'Acknowledged {count} alerts', + 'count': count, + 'acknowledged_by': acknowledged_by + }) + + except Exception as e: + current_app.db_session.rollback() + return jsonify({ + 'status': 'error', + 'message': f'Failed to acknowledge alerts: {str(e)}' + }), 500 + + @bp.route('/rules', methods=['GET']) @api_auth_required def list_alert_rules(): diff --git a/app/web/templates/alerts.html b/app/web/templates/alerts.html index 4d71640..b6fe6ab 100644 --- a/app/web/templates/alerts.html +++ b/app/web/templates/alerts.html @@ -6,9 +6,14 @@

Alert History

- - Manage Alert Rules - +
+ + + Manage Alert Rules + +
@@ -265,5 +270,34 @@ function acknowledgeAlert(alertId) { alert('Failed to acknowledge alert'); }); } + +function acknowledgeAllAlerts() { + if (!confirm('Acknowledge all unacknowledged alerts?')) { + return; + } + + fetch('/api/alerts/acknowledge-all', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': localStorage.getItem('api_key') || '' + }, + body: JSON.stringify({ + acknowledged_by: 'web_user' + }) + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + location.reload(); + } else { + alert('Failed to acknowledge alerts: ' + (data.message || 'Unknown error')); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Failed to acknowledge alerts'); + }); +} {% endblock %} \ No newline at end of file From 12d5aff7a5b41ca52c424cf0498011c4673aa887 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Thu, 20 Nov 2025 09:59:35 -0600 Subject: [PATCH 4/7] Add help page with user documentation Create comprehensive help page covering: - Getting started workflow - Sites and IP management - Scan configuration - Running scans manually - Scheduling automated scans - Scan comparisons - Alerts and alert rules - Webhook configuration Add Help link with icon to navigation bar. --- app/web/routes/main.py | 12 ++ app/web/templates/base.html | 6 + app/web/templates/help.html | 375 ++++++++++++++++++++++++++++++++++++ 3 files changed, 393 insertions(+) create mode 100644 app/web/templates/help.html diff --git a/app/web/routes/main.py b/app/web/routes/main.py index 583fdc5..8c5f366 100644 --- a/app/web/routes/main.py +++ b/app/web/routes/main.py @@ -247,6 +247,18 @@ def alert_rules(): ) +@bp.route('/help') +@login_required +def help(): + """ + Help page - explains how to use the application. + + Returns: + Rendered help template + """ + return render_template('help.html') + + @bp.route('/output/') @login_required def serve_output_file(filename): diff --git a/app/web/templates/base.html b/app/web/templates/base.html index 606a6d6..8496d07 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -77,6 +77,12 @@