Implemented comprehensive scan comparison functionality with historical analysis and improved user experience for scan triggering. Features Added: - Scan comparison engine with ports, services, and certificates analysis - Drift score calculation (0.0-1.0 scale) for infrastructure changes - Side-by-side comparison UI with color-coded changes (added/removed/changed) - Historical trend charts showing port counts over time - "Compare with Previous" button on scan detail pages - Scan history API endpoint for trending data API Endpoints: - GET /api/scans/<id1>/compare/<id2> - Compare two scans - GET /api/stats/scan-history/<id> - Historical scan data for charts UI Improvements: - Replaced config file text inputs with dropdown selectors - Added config file selection to dashboard and scans pages - Improved delete scan confirmation with proper async handling - Enhanced error messages with detailed validation feedback - Added 2-second delay before redirect to ensure deletion completes Comparison Features: - Port changes: tracks added, removed, and unchanged ports - Service changes: detects version updates and service modifications - Certificate changes: monitors SSL/TLS certificate updates - Interactive historical charts with clickable data points - Automatic detection of previous scan for comparison Bug Fixes: - Fixed scan deletion UI alert appearing on successful deletion - Prevented config file path duplication (configs/configs/...) - Improved error handling for failed API responses - Added proper JSON response parsing with fallback handling Testing: - Created comprehensive test suite for comparison functionality - Tests cover comparison API, service methods, and drift scoring - Added edge case tests for identical scans and missing data
165 lines
3.8 KiB
Python
165 lines
3.8 KiB
Python
"""
|
|
Main web routes for SneakyScanner.
|
|
|
|
Provides dashboard and scan viewing pages.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from flask import Blueprint, current_app, redirect, render_template, url_for
|
|
|
|
from web.auth.decorators import login_required
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
bp = Blueprint('main', __name__)
|
|
|
|
|
|
@bp.route('/')
|
|
def index():
|
|
"""
|
|
Root route - redirect to dashboard.
|
|
|
|
Returns:
|
|
Redirect to dashboard
|
|
"""
|
|
return redirect(url_for('main.dashboard'))
|
|
|
|
|
|
@bp.route('/dashboard')
|
|
@login_required
|
|
def dashboard():
|
|
"""
|
|
Dashboard page - shows recent scans and statistics.
|
|
|
|
Returns:
|
|
Rendered dashboard template
|
|
"""
|
|
import os
|
|
|
|
# Get list of available config files
|
|
configs_dir = '/app/configs'
|
|
config_files = []
|
|
|
|
try:
|
|
if os.path.exists(configs_dir):
|
|
config_files = [f for f in os.listdir(configs_dir) if f.endswith(('.yaml', '.yml'))]
|
|
config_files.sort()
|
|
except Exception as e:
|
|
logger.error(f"Error listing config files: {e}")
|
|
|
|
return render_template('dashboard.html', config_files=config_files)
|
|
|
|
|
|
@bp.route('/scans')
|
|
@login_required
|
|
def scans():
|
|
"""
|
|
Scans list page - shows all scans with pagination.
|
|
|
|
Returns:
|
|
Rendered scans list template
|
|
"""
|
|
import os
|
|
|
|
# Get list of available config files
|
|
configs_dir = '/app/configs'
|
|
config_files = []
|
|
|
|
try:
|
|
if os.path.exists(configs_dir):
|
|
config_files = [f for f in os.listdir(configs_dir) if f.endswith(('.yaml', '.yml'))]
|
|
config_files.sort()
|
|
except Exception as e:
|
|
logger.error(f"Error listing config files: {e}")
|
|
|
|
return render_template('scans.html', config_files=config_files)
|
|
|
|
|
|
@bp.route('/scans/<int:scan_id>')
|
|
@login_required
|
|
def scan_detail(scan_id):
|
|
"""
|
|
Scan detail page - shows full scan results.
|
|
|
|
Args:
|
|
scan_id: Scan ID to display
|
|
|
|
Returns:
|
|
Rendered scan detail template
|
|
"""
|
|
# TODO: Phase 5 - Implement scan detail page
|
|
return render_template('scan_detail.html', scan_id=scan_id)
|
|
|
|
|
|
@bp.route('/scans/<int:scan_id1>/compare/<int:scan_id2>')
|
|
@login_required
|
|
def compare_scans(scan_id1, scan_id2):
|
|
"""
|
|
Scan comparison page - shows differences between two scans.
|
|
|
|
Args:
|
|
scan_id1: First (older) scan ID
|
|
scan_id2: Second (newer) scan ID
|
|
|
|
Returns:
|
|
Rendered comparison template
|
|
"""
|
|
return render_template('scan_compare.html', scan_id1=scan_id1, scan_id2=scan_id2)
|
|
|
|
|
|
@bp.route('/schedules')
|
|
@login_required
|
|
def schedules():
|
|
"""
|
|
Schedules list page - shows all scheduled scans.
|
|
|
|
Returns:
|
|
Rendered schedules list template
|
|
"""
|
|
return render_template('schedules.html')
|
|
|
|
|
|
@bp.route('/schedules/create')
|
|
@login_required
|
|
def create_schedule():
|
|
"""
|
|
Create new schedule form page.
|
|
|
|
Returns:
|
|
Rendered schedule create template with available config files
|
|
"""
|
|
import os
|
|
|
|
# Get list of available config files
|
|
configs_dir = '/app/configs'
|
|
config_files = []
|
|
|
|
try:
|
|
if os.path.exists(configs_dir):
|
|
config_files = [f for f in os.listdir(configs_dir) if f.endswith('.yaml')]
|
|
config_files.sort()
|
|
except Exception as e:
|
|
logger.error(f"Error listing config files: {e}")
|
|
|
|
return render_template('schedule_create.html', config_files=config_files)
|
|
|
|
|
|
@bp.route('/schedules/<int:schedule_id>/edit')
|
|
@login_required
|
|
def edit_schedule(schedule_id):
|
|
"""
|
|
Edit existing schedule form page.
|
|
|
|
Args:
|
|
schedule_id: Schedule ID to edit
|
|
|
|
Returns:
|
|
Rendered schedule edit template
|
|
"""
|
|
from flask import flash
|
|
|
|
# Note: Schedule data is loaded via AJAX in the template
|
|
# This just renders the page with the schedule_id in the URL
|
|
return render_template('schedule_edit.html', schedule_id=schedule_id)
|