diff --git a/app/requirements-web.txt b/app/requirements-web.txt index f2625f2..fc4ef9e 100644 --- a/app/requirements-web.txt +++ b/app/requirements-web.txt @@ -12,7 +12,7 @@ alembic==1.13.0 # Authentication & Security Flask-Login==0.6.3 bcrypt==4.1.2 -cryptography==41.0.7 +cryptography>=46.0.0 # API & Serialization Flask-CORS==4.0.0 @@ -34,4 +34,4 @@ python-dotenv==1.0.0 # Development & Testing pytest==7.4.3 -pytest-flask==1.3.0 +pytest-flask==1.3.0 \ No newline at end of file diff --git a/app/requirements.txt b/app/requirements.txt index 5f19147..dbb9e6c 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.1 python-libnmap==0.7.3 -sslyze==6.0.0 +sslyze==6.2.0 playwright==1.40.0 Jinja2==3.1.2 diff --git a/app/src/scanner.py b/app/src/scanner.py index 69027f5..3964cb3 100644 --- a/app/src/scanner.py +++ b/app/src/scanner.py @@ -1054,6 +1054,8 @@ class SneakyScanner: # Preserve directory structure in ZIP arcname = f"{screenshot_dir.name}/{screenshot_file.name}" zipf.write(screenshot_file, arcname) + # Track screenshot directory for database storage + output_paths['screenshots'] = screenshot_dir output_paths['zip'] = zip_path print(f"ZIP archive saved to: {zip_path}", flush=True) 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/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/routes/main.py b/app/web/routes/main.py index ac15a97..8c5f366 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,31 @@ def alert_rules(): 'alert_rules.html', rules=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): + """ + 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) diff --git a/app/web/services/scan_service.py b/app/web/services/scan_service.py index 7caedf6..9aea0b8 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,17 @@ 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']) + if 'screenshots' in output_paths: + scan.screenshot_dir = str(output_paths['screenshots']) + # Map report data to database models self._map_report_to_models(report, scan) @@ -439,9 +451,10 @@ class ScanService: # Process certificate and TLS info if present http_info = service_data.get('http_info', {}) - if http_info.get('certificate'): + ssl_tls = http_info.get('ssl_tls', {}) + if ssl_tls.get('certificate'): self._process_certificate( - http_info['certificate'], + ssl_tls, scan_obj.id, service.id ) @@ -479,16 +492,19 @@ class ScanService: return service return None - def _process_certificate(self, cert_data: Dict[str, Any], scan_id: int, + def _process_certificate(self, ssl_tls_data: Dict[str, Any], scan_id: int, service_id: int) -> None: """ Process certificate and TLS version data. Args: - cert_data: Certificate data dictionary + ssl_tls_data: SSL/TLS data dictionary containing 'certificate' and 'tls_versions' scan_id: Scan ID service_id: Service ID """ + # Extract certificate data from ssl_tls structure + cert_data = ssl_tls_data.get('certificate', {}) + # Create ScanCertificate record cert = ScanCertificate( scan_id=scan_id, @@ -506,7 +522,7 @@ class ScanService: self.db.flush() # Process TLS versions - tls_versions = cert_data.get('tls_versions', {}) + tls_versions = ssl_tls_data.get('tls_versions', {}) for version, version_data in tls_versions.items(): tls = ScanTLSVersion( scan_id=scan_id, 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 diff --git a/app/web/templates/base.html b/app/web/templates/base.html index b811060..8496d07 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -77,6 +77,12 @@