Merge branch 'nightly' into beta

This commit is contained in:
2025-11-20 11:34:34 -06:00
13 changed files with 752 additions and 52 deletions

View File

@@ -12,7 +12,7 @@ alembic==1.13.0
# Authentication & Security # Authentication & Security
Flask-Login==0.6.3 Flask-Login==0.6.3
bcrypt==4.1.2 bcrypt==4.1.2
cryptography==41.0.7 cryptography>=46.0.0
# API & Serialization # API & Serialization
Flask-CORS==4.0.0 Flask-CORS==4.0.0

View File

@@ -1,5 +1,5 @@
PyYAML==6.0.1 PyYAML==6.0.1
python-libnmap==0.7.3 python-libnmap==0.7.3
sslyze==6.0.0 sslyze==6.2.0
playwright==1.40.0 playwright==1.40.0
Jinja2==3.1.2 Jinja2==3.1.2

View File

@@ -1054,6 +1054,8 @@ class SneakyScanner:
# Preserve directory structure in ZIP # Preserve directory structure in ZIP
arcname = f"{screenshot_dir.name}/{screenshot_file.name}" arcname = f"{screenshot_dir.name}/{screenshot_file.name}"
zipf.write(screenshot_file, arcname) zipf.write(screenshot_file, arcname)
# Track screenshot directory for database storage
output_paths['screenshots'] = screenshot_dir
output_paths['zip'] = zip_path output_paths['zip'] = zip_path
print(f"ZIP archive saved to: {zip_path}", flush=True) print(f"ZIP archive saved to: {zip_path}", flush=True)

View File

@@ -146,6 +146,47 @@ def acknowledge_alert(alert_id):
}), 400 }), 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']) @bp.route('/rules', methods=['GET'])
@api_auth_required @api_auth_required
def list_alert_rules(): def list_alert_rules():

View File

@@ -77,12 +77,12 @@ def execute_scan(scan_id: int, config_id: int, db_url: str = None):
# Generate output files (JSON, HTML, ZIP) # Generate output files (JSON, HTML, ZIP)
logger.info(f"Scan {scan_id}: Generating output files...") 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 # Save results to database
logger.info(f"Scan {scan_id}: Saving results to database...") logger.info(f"Scan {scan_id}: Saving results to database...")
scan_service = ScanService(session) 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 # Evaluate alert rules
logger.info(f"Scan {scan_id}: Evaluating alert rules...") logger.info(f"Scan {scan_id}: Evaluating alert rules...")

View File

@@ -5,8 +5,9 @@ Provides dashboard and scan viewing pages.
""" """
import logging 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 from web.auth.decorators import login_required
@@ -244,3 +245,31 @@ def alert_rules():
'alert_rules.html', 'alert_rules.html',
rules=rules 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/<path:filename>')
@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)

View File

@@ -308,7 +308,7 @@ class ScanService:
return count return count
def _save_scan_to_db(self, report: Dict[str, Any], scan_id: int, 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. Save scan results to database.
@@ -319,6 +319,7 @@ class ScanService:
report: Scan report dictionary from scanner report: Scan report dictionary from scanner
scan_id: Scan ID to update scan_id: Scan ID to update
status: Final scan status (completed or failed) 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() scan = self.db.query(Scan).filter(Scan.id == scan_id).first()
if not scan: if not scan:
@@ -329,6 +330,17 @@ class ScanService:
scan.duration = report.get('scan_duration') scan.duration = report.get('scan_duration')
scan.completed_at = datetime.utcnow() 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 # Map report data to database models
self._map_report_to_models(report, scan) self._map_report_to_models(report, scan)
@@ -439,9 +451,10 @@ class ScanService:
# Process certificate and TLS info if present # Process certificate and TLS info if present
http_info = service_data.get('http_info', {}) 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( self._process_certificate(
http_info['certificate'], ssl_tls,
scan_obj.id, scan_obj.id,
service.id service.id
) )
@@ -479,16 +492,19 @@ class ScanService:
return service return service
return None 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: service_id: int) -> None:
""" """
Process certificate and TLS version data. Process certificate and TLS version data.
Args: Args:
cert_data: Certificate data dictionary ssl_tls_data: SSL/TLS data dictionary containing 'certificate' and 'tls_versions'
scan_id: Scan ID scan_id: Scan ID
service_id: Service ID service_id: Service ID
""" """
# Extract certificate data from ssl_tls structure
cert_data = ssl_tls_data.get('certificate', {})
# Create ScanCertificate record # Create ScanCertificate record
cert = ScanCertificate( cert = ScanCertificate(
scan_id=scan_id, scan_id=scan_id,
@@ -506,7 +522,7 @@ class ScanService:
self.db.flush() self.db.flush()
# Process TLS versions # 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(): for version, version_data in tls_versions.items():
tls = ScanTLSVersion( tls = ScanTLSVersion(
scan_id=scan_id, scan_id=scan_id,

View File

@@ -6,10 +6,15 @@
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12 d-flex justify-content-between align-items-center mb-4"> <div class="col-12 d-flex justify-content-between align-items-center mb-4">
<h1>Alert History</h1> <h1>Alert History</h1>
<div>
<button class="btn btn-success me-2" onclick="acknowledgeAllAlerts()">
<i class="bi bi-check-all"></i> Ack All
</button>
<a href="{{ url_for('main.alert_rules') }}" class="btn btn-primary"> <a href="{{ url_for('main.alert_rules') }}" class="btn btn-primary">
<i class="bi bi-gear"></i> Manage Alert Rules <i class="bi bi-gear"></i> Manage Alert Rules
</a> </a>
</div> </div>
</div>
</div> </div>
<!-- Alert Statistics --> <!-- Alert Statistics -->
@@ -265,5 +270,34 @@ function acknowledgeAlert(alertId) {
alert('Failed to acknowledge alert'); 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');
});
}
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -77,6 +77,12 @@
</li> </li>
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.help' %}active{% endif %}"
href="{{ url_for('main.help') }}">
<i class="bi bi-question-circle"></i> Help
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a> <a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a>
</li> </li>
@@ -108,7 +114,7 @@
</div> </div>
<!-- Global notification container - always above modals --> <!-- Global notification container - always above modals -->
<div id="notification-container" style="position: fixed; top: 20px; right: 20px; z-index: 1100; min-width: 300px;"></div> <div id="notification-container" style="position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px;"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}

375
app/web/templates/help.html Normal file
View File

@@ -0,0 +1,375 @@
{% extends "base.html" %}
{% block title %}Help - SneakyScanner{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-12">
<h1 class="mb-4"><i class="bi bi-question-circle"></i> Help & Documentation</h1>
<p class="text-muted">Learn how to use SneakyScanner to manage your network scanning operations.</p>
</div>
</div>
<!-- Quick Navigation -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-compass"></i> Quick Navigation</h5>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-md-3 col-6">
<a href="#getting-started" class="btn btn-outline-primary w-100">Getting Started</a>
</div>
<div class="col-md-3 col-6">
<a href="#sites" class="btn btn-outline-primary w-100">Sites</a>
</div>
<div class="col-md-3 col-6">
<a href="#scan-configs" class="btn btn-outline-primary w-100">Scan Configs</a>
</div>
<div class="col-md-3 col-6">
<a href="#running-scans" class="btn btn-outline-primary w-100">Running Scans</a>
</div>
<div class="col-md-3 col-6">
<a href="#scheduling" class="btn btn-outline-primary w-100">Scheduling</a>
</div>
<div class="col-md-3 col-6">
<a href="#comparisons" class="btn btn-outline-primary w-100">Comparisons</a>
</div>
<div class="col-md-3 col-6">
<a href="#alerts" class="btn btn-outline-primary w-100">Alerts</a>
</div>
<div class="col-md-3 col-6">
<a href="#webhooks" class="btn btn-outline-primary w-100">Webhooks</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Getting Started -->
<div class="row mb-4" id="getting-started">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-rocket-takeoff"></i> Getting Started</h5>
</div>
<div class="card-body">
<p>SneakyScanner helps you perform network vulnerability scans and track changes over time. Here's the typical workflow:</p>
<div class="alert alert-info">
<strong>Basic Workflow:</strong>
<ol class="mb-0 mt-2">
<li><strong>Create a Site</strong> - Define a logical grouping for your targets</li>
<li><strong>Add IPs</strong> - Add IP addresses or ranges to your site</li>
<li><strong>Create a Scan Config</strong> - Configure how scans should run using your site</li>
<li><strong>Run a Scan</strong> - Execute scans manually or on a schedule</li>
<li><strong>Review Results</strong> - Analyze findings and compare scans over time</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<!-- Sites -->
<div class="row mb-4" id="sites">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-globe"></i> Creating Sites & Adding IPs</h5>
</div>
<div class="card-body">
<h6>What is a Site?</h6>
<p>A Site is a logical grouping of IP addresses that you want to scan together. For example, you might create separate sites for "Production Servers", "Development Environment", or "Office Network".</p>
<h6>Creating a Site</h6>
<ol>
<li>Navigate to <strong>Configs → Sites</strong> in the navigation menu</li>
<li>Click the <strong>Create Site</strong> button</li>
<li>Enter a descriptive name for your site</li>
<li>Optionally add a description to help identify the site's purpose</li>
<li>Click <strong>Create</strong> to save the site</li>
</ol>
<h6>Adding IP Addresses</h6>
<p>After creating a site, you need to add the IP addresses you want to scan:</p>
<ol>
<li>Find your site in the Sites list</li>
<li>Click the <strong>Manage IPs</strong> button (or the site name)</li>
<li>Click <strong>Add IP</strong></li>
<li>Enter the IP address or CIDR range (e.g., <code>192.168.1.1</code> or <code>192.168.1.0/24</code>)</li>
<li>Click <strong>Add</strong> to save</li>
</ol>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i> <strong>Note:</strong> You can add individual IPs or CIDR notation ranges. Large ranges will result in longer scan times.
</div>
</div>
</div>
</div>
</div>
<!-- Scan Configs -->
<div class="row mb-4" id="scan-configs">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-gear"></i> Creating Scan Configurations</h5>
</div>
<div class="card-body">
<h6>What is a Scan Config?</h6>
<p>A Scan Configuration defines how a scan should be performed. It links to a Site and specifies scanning parameters like ports to scan, timing options, and other settings.</p>
<h6>Creating a Scan Config</h6>
<ol>
<li>Navigate to <strong>Configs → Scan Configs</strong> in the navigation menu</li>
<li>Click the <strong>Create Config</strong> button</li>
<li>Enter a name for the configuration</li>
<li>Select the <strong>Site</strong> to associate with this config</li>
<li>Configure scan parameters:
<ul>
<li><strong>Ports</strong> - Specify ports to scan (e.g., <code>22,80,443</code> or <code>1-1000</code>)</li>
<li><strong>Timing</strong> - Set scan speed/aggressiveness</li>
<li><strong>Additional Options</strong> - Configure other nmap parameters as needed</li>
</ul>
</li>
<li>Click <strong>Create</strong> to save the configuration</li>
</ol>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> <strong>Tip:</strong> Create different configs for different purposes - a quick config for daily checks and a thorough config for weekly deep scans.
</div>
</div>
</div>
</div>
</div>
<!-- Running Scans -->
<div class="row mb-4" id="running-scans">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-play-circle"></i> Running Scans</h5>
</div>
<div class="card-body">
<h6>Starting a Manual Scan</h6>
<ol>
<li>Navigate to <strong>Scans</strong> in the navigation menu</li>
<li>Click the <strong>New Scan</strong> button</li>
<li>Select the <strong>Scan Config</strong> you want to use</li>
<li>Click <strong>Start Scan</strong></li>
</ol>
<h6>Monitoring Scan Progress</h6>
<p>While a scan is running:</p>
<ul>
<li>The scan will appear in the Scans list with a <span class="badge badge-warning">Running</span> status</li>
<li>You can view live progress by clicking on the scan</li>
<li>The Dashboard also shows active scans</li>
</ul>
<h6>Viewing Scan Results</h6>
<ol>
<li>Once complete, click on a scan in the Scans list</li>
<li>View discovered hosts, open ports, and services</li>
<li>Export results or compare with previous scans</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Scheduling -->
<div class="row mb-4" id="scheduling">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-calendar-check"></i> Scheduling Scans</h5>
</div>
<div class="card-body">
<h6>Why Schedule Scans?</h6>
<p>Scheduled scans allow you to automatically run scans at regular intervals, ensuring continuous monitoring of your network without manual intervention.</p>
<h6>Creating a Schedule</h6>
<ol>
<li>Navigate to <strong>Schedules</strong> in the navigation menu</li>
<li>Click the <strong>Create Schedule</strong> button</li>
<li>Enter a name for the schedule</li>
<li>Select the <strong>Scan Config</strong> to use</li>
<li>Configure the schedule:
<ul>
<li><strong>Frequency</strong> - How often to run (daily, weekly, monthly, custom cron)</li>
<li><strong>Time</strong> - When to start the scan</li>
<li><strong>Days</strong> - Which days to run (for weekly schedules)</li>
</ul>
</li>
<li>Enable/disable the schedule as needed</li>
<li>Click <strong>Create</strong> to save</li>
</ol>
<h6>Managing Schedules</h6>
<ul>
<li><strong>Enable/Disable</strong> - Toggle schedules on or off without deleting them</li>
<li><strong>Edit</strong> - Modify the schedule timing or associated config</li>
<li><strong>Delete</strong> - Remove schedules you no longer need</li>
<li><strong>View History</strong> - See past runs triggered by the schedule</li>
</ul>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> <strong>Tip:</strong> Schedule comprehensive scans during off-peak hours to minimize network impact.
</div>
</div>
</div>
</div>
</div>
<!-- Scan Comparisons -->
<div class="row mb-4" id="comparisons">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-arrow-left-right"></i> Scan Comparisons</h5>
</div>
<div class="card-body">
<h6>Why Compare Scans?</h6>
<p>Comparing scans helps you identify changes in your network over time - new hosts, closed ports, new services, or potential security issues.</p>
<h6>Comparing Two Scans</h6>
<ol>
<li>Navigate to <strong>Scans</strong> in the navigation menu</li>
<li>Find the scan you want to use as the baseline</li>
<li>Click on the scan to view its details</li>
<li>Click the <strong>Compare</strong> button</li>
<li>Select another scan to compare against</li>
<li>Review the comparison results</li>
</ol>
<h6>Understanding Comparison Results</h6>
<p>The comparison view shows:</p>
<ul>
<li><span class="badge badge-success">New</span> - Hosts or ports that appear in the newer scan but not the older one</li>
<li><span class="badge badge-danger">Removed</span> - Hosts or ports that were in the older scan but not the newer one</li>
<li><span class="badge badge-warning">Changed</span> - Services or states that differ between scans</li>
<li><span class="badge badge-info">Unchanged</span> - Items that remain the same</li>
</ul>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle"></i> <strong>Security Note:</strong> Pay close attention to unexpected new open ports or services - these could indicate unauthorized changes or potential compromises.
</div>
</div>
</div>
</div>
</div>
<!-- Alerts -->
<div class="row mb-4" id="alerts">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-bell"></i> Alerts & Alert Rules</h5>
</div>
<div class="card-body">
<h6>Understanding Alerts</h6>
<p>Alerts notify you when scan results match certain conditions you define. This helps you stay informed about important changes without manually reviewing every scan.</p>
<h6>Viewing Alert History</h6>
<ol>
<li>Navigate to <strong>Alerts → Alert History</strong></li>
<li>View all triggered alerts with timestamps and details</li>
<li>Filter alerts by severity, date, or type</li>
<li>Click on an alert to see full details and the associated scan</li>
</ol>
<h6>Creating Alert Rules</h6>
<ol>
<li>Navigate to <strong>Alerts → Alert Rules</strong></li>
<li>Click <strong>Create Rule</strong></li>
<li>Configure the rule:
<ul>
<li><strong>Name</strong> - A descriptive name for the rule</li>
<li><strong>Condition</strong> - What triggers the alert (e.g., new open port, new host, specific service detected)</li>
<li><strong>Severity</strong> - How critical is this alert (Info, Warning, Critical)</li>
<li><strong>Scope</strong> - Which sites or configs this rule applies to</li>
</ul>
</li>
<li>Enable the rule</li>
<li>Click <strong>Create</strong> to save</li>
</ol>
<h6>Common Alert Rule Examples</h6>
<ul>
<li><strong>New Host Detected</strong> - Alert when a previously unknown host appears</li>
<li><strong>New Open Port</strong> - Alert when a new port opens on any host</li>
<li><strong>Critical Port Open</strong> - Alert for specific high-risk ports (e.g., 23/Telnet, 3389/RDP)</li>
<li><strong>Service Change</strong> - Alert when a service version changes</li>
<li><strong>Host Offline</strong> - Alert when an expected host stops responding</li>
</ul>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> <strong>Tip:</strong> Start with a few important rules and refine them over time to avoid alert fatigue.
</div>
</div>
</div>
</div>
</div>
<!-- Webhooks -->
<div class="row mb-4" id="webhooks">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-broadcast"></i> Webhooks</h5>
</div>
<div class="card-body">
<h6>What are Webhooks?</h6>
<p>Webhooks allow SneakyScanner to send notifications to external services when events occur, such as scan completion or alert triggers. This enables integration with tools like Slack, Discord, Microsoft Teams, or custom systems.</p>
<h6>Creating a Webhook</h6>
<ol>
<li>Navigate to <strong>Alerts → Webhooks</strong></li>
<li>Click <strong>Create Webhook</strong></li>
<li>Configure the webhook:
<ul>
<li><strong>Name</strong> - A descriptive name</li>
<li><strong>URL</strong> - The endpoint to send notifications to</li>
<li><strong>Events</strong> - Which events trigger this webhook</li>
<li><strong>Secret</strong> - Optional secret for request signing</li>
</ul>
</li>
<li>Test the webhook to verify it works</li>
<li>Click <strong>Create</strong> to save</li>
</ol>
<h6>Webhook Events</h6>
<ul>
<li><strong>Scan Started</strong> - When a scan begins</li>
<li><strong>Scan Completed</strong> - When a scan finishes</li>
<li><strong>Scan Failed</strong> - When a scan encounters an error</li>
<li><strong>Alert Triggered</strong> - When an alert rule matches</li>
</ul>
<h6>Integration Examples</h6>
<ul>
<li><strong>Slack</strong> - Use a Slack Incoming Webhook URL</li>
<li><strong>Discord</strong> - Use a Discord Webhook URL</li>
<li><strong>Microsoft Teams</strong> - Use a Teams Incoming Webhook</li>
<li><strong>Custom API</strong> - Send to your own endpoint for custom processing</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Back to Top -->
<div class="row mb-4">
<div class="col-12 text-center">
<a href="#" class="btn btn-outline-secondary">
<i class="bi bi-arrow-up"></i> Back to Top
</a>
</div>
</div>
{% endblock %}

View File

@@ -154,6 +154,67 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Certificate Details Modal -->
<div class="modal fade" id="certificateModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content" style="background-color: #1e293b; border: 1px solid #334155;">
<div class="modal-header" style="border-bottom: 1px solid #334155;">
<h5 class="modal-title" style="color: #60a5fa;">
<i class="bi bi-shield-lock"></i> Certificate Details
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label text-muted">Subject</label>
<div id="cert-subject" class="mono" style="word-break: break-all;">-</div>
</div>
<div class="col-md-6">
<label class="form-label text-muted">Issuer</label>
<div id="cert-issuer" class="mono" style="word-break: break-all;">-</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label text-muted">Valid From</label>
<div id="cert-valid-from" class="mono">-</div>
</div>
<div class="col-md-4">
<label class="form-label text-muted">Valid Until</label>
<div id="cert-valid-until" class="mono">-</div>
</div>
<div class="col-md-4">
<label class="form-label text-muted">Days Until Expiry</label>
<div id="cert-days-expiry">-</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label text-muted">Serial Number</label>
<div id="cert-serial" class="mono" style="word-break: break-all;">-</div>
</div>
<div class="col-md-6">
<label class="form-label text-muted">Self-Signed</label>
<div id="cert-self-signed">-</div>
</div>
</div>
<div class="mb-3">
<label class="form-label text-muted">Subject Alternative Names (SANs)</label>
<div id="cert-sans">-</div>
</div>
<div class="mb-3">
<label class="form-label text-muted">TLS Version Support</label>
<div id="cert-tls-versions">-</div>
</div>
</div>
<div class="modal-footer" style="border-top: 1px solid #334155;">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
@@ -162,6 +223,25 @@
let scanData = null; let scanData = null;
let historyChart = null; // Store chart instance to prevent duplicates 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}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
container.appendChild(notification);
// Auto-dismiss after 5 seconds
setTimeout(() => {
notification.remove();
}, 5000);
}
// Load scan on page load // Load scan on page load
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
loadScan().then(() => { loadScan().then(() => {
@@ -218,7 +298,6 @@
document.getElementById('scan-timestamp').textContent = new Date(scan.timestamp).toLocaleString(); 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-duration').textContent = scan.duration ? `${scan.duration.toFixed(1)}s` : '-';
document.getElementById('scan-triggered-by').textContent = scan.triggered_by || 'manual'; document.getElementById('scan-triggered-by').textContent = scan.triggered_by || 'manual';
document.getElementById('scan-config-id').textContent = scan.config_id || '-';
// Status badge // Status badge
let statusBadge = ''; let statusBadge = '';
@@ -313,6 +392,8 @@
<th>Product</th> <th>Product</th>
<th>Version</th> <th>Version</th>
<th>Status</th> <th>Status</th>
<th>Screenshot</th>
<th>Certificate</th>
</tr> </tr>
</thead> </thead>
<tbody id="site-${siteIdx}-ip-${ipIdx}-ports"></tbody> <tbody id="site-${siteIdx}-ip-${ipIdx}-ports"></tbody>
@@ -326,10 +407,12 @@
const ports = ip.ports || []; const ports = ip.ports || [];
if (ports.length === 0) { if (ports.length === 0) {
portsContainer.innerHTML = '<tr class="scan-row"><td colspan="7" class="text-center text-muted">No ports found</td></tr>'; portsContainer.innerHTML = '<tr class="scan-row"><td colspan="9" class="text-center text-muted">No ports found</td></tr>';
} else { } else {
ports.forEach(port => { ports.forEach(port => {
const service = port.services && port.services.length > 0 ? port.services[0] : null; const service = port.services && port.services.length > 0 ? port.services[0] : null;
const screenshotPath = service && service.screenshot_path ? service.screenshot_path : null;
const certificate = service && service.certificates && service.certificates.length > 0 ? service.certificates[0] : null;
const row = document.createElement('tr'); const row = document.createElement('tr');
row.classList.add('scan-row'); // Fix white row bug row.classList.add('scan-row'); // Fix white row bug
@@ -341,6 +424,8 @@
<td>${service ? service.product || '-' : '-'}</td> <td>${service ? service.product || '-' : '-'}</td>
<td class="mono">${service ? service.version || '-' : '-'}</td> <td class="mono">${service ? service.version || '-' : '-'}</td>
<td>${port.expected ? '<span class="badge badge-good">Expected</span>' : '<span class="badge badge-warning">Unexpected</span>'}</td> <td>${port.expected ? '<span class="badge badge-good">Expected</span>' : '<span class="badge badge-warning">Unexpected</span>'}</td>
<td>${screenshotPath ? `<a href="/output/${screenshotPath.replace(/^\/?(?:app\/)?output\/?/, '')}" target="_blank" class="btn btn-sm btn-outline-primary" title="View Screenshot"><i class="bi bi-image"></i></a>` : '-'}</td>
<td>${certificate ? `<button class="btn btn-sm btn-outline-info" onclick='showCertificateModal(${JSON.stringify(certificate).replace(/'/g, "&#39;")})' title="View Certificate"><i class="bi bi-shield-lock"></i></button>` : '-'}</td>
`; `;
portsContainer.appendChild(row); portsContainer.appendChild(row);
}); });
@@ -439,7 +524,7 @@
window.location.href = '{{ url_for("main.scans") }}'; window.location.href = '{{ url_for("main.scans") }}';
} catch (error) { } catch (error) {
console.error('Error deleting scan:', 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 // Re-enable button on error
deleteBtn.disabled = false; deleteBtn.disabled = false;
@@ -593,5 +678,97 @@
console.error('Error loading historical chart:', error); console.error('Error loading historical chart:', error);
} }
} }
// Show certificate details modal
function showCertificateModal(cert) {
// Populate modal fields
document.getElementById('cert-subject').textContent = cert.subject || '-';
document.getElementById('cert-issuer').textContent = cert.issuer || '-';
document.getElementById('cert-serial').textContent = cert.serial_number || '-';
// Format dates
document.getElementById('cert-valid-from').textContent = cert.not_valid_before
? new Date(cert.not_valid_before).toLocaleString()
: '-';
document.getElementById('cert-valid-until').textContent = cert.not_valid_after
? new Date(cert.not_valid_after).toLocaleString()
: '-';
// Days until expiry with color coding
if (cert.days_until_expiry !== null && cert.days_until_expiry !== undefined) {
let badgeClass = 'badge-success';
if (cert.days_until_expiry < 0) {
badgeClass = 'badge-danger';
} else if (cert.days_until_expiry < 30) {
badgeClass = 'badge-warning';
}
document.getElementById('cert-days-expiry').innerHTML =
`<span class="badge ${badgeClass}">${cert.days_until_expiry} days</span>`;
} else {
document.getElementById('cert-days-expiry').textContent = '-';
}
// Self-signed indicator
document.getElementById('cert-self-signed').innerHTML = cert.is_self_signed
? '<span class="badge badge-warning">Yes</span>'
: '<span class="badge badge-success">No</span>';
// SANs
if (cert.sans && cert.sans.length > 0) {
document.getElementById('cert-sans').innerHTML = cert.sans
.map(san => `<span class="badge bg-secondary me-1 mb-1">${san}</span>`)
.join('');
} else {
document.getElementById('cert-sans').textContent = 'None';
}
// TLS versions
if (cert.tls_versions && cert.tls_versions.length > 0) {
let tlsHtml = '<div class="table-responsive"><table class="table table-sm mb-0">';
tlsHtml += '<thead><tr><th>Version</th><th>Status</th><th>Cipher Suites</th></tr></thead><tbody>';
cert.tls_versions.forEach(tls => {
const statusBadge = tls.supported
? '<span class="badge badge-success">Supported</span>'
: '<span class="badge badge-danger">Not Supported</span>';
let ciphers = '-';
if (tls.cipher_suites && tls.cipher_suites.length > 0) {
ciphers = `<small class="text-muted">${tls.cipher_suites.length} cipher(s)</small>
<button class="btn btn-sm btn-link p-0 ms-1" onclick="toggleCiphers(this, '${tls.tls_version}')" data-ciphers='${JSON.stringify(tls.cipher_suites).replace(/'/g, "&#39;")}'>
<i class="bi bi-chevron-down"></i>
</button>
<div class="cipher-list" style="display:none; font-size: 0.75rem; max-height: 100px; overflow-y: auto;"></div>`;
}
tlsHtml += `<tr class="scan-row"><td>${tls.tls_version}</td><td>${statusBadge}</td><td>${ciphers}</td></tr>`;
});
tlsHtml += '</tbody></table></div>';
document.getElementById('cert-tls-versions').innerHTML = tlsHtml;
} else {
document.getElementById('cert-tls-versions').textContent = 'No TLS information available';
}
// Show modal
const modal = new bootstrap.Modal(document.getElementById('certificateModal'));
modal.show();
}
// Toggle cipher suites display
function toggleCiphers(btn, version) {
const cipherList = btn.nextElementSibling;
const icon = btn.querySelector('i');
if (cipherList.style.display === 'none') {
const ciphers = JSON.parse(btn.dataset.ciphers);
cipherList.innerHTML = ciphers.map(c => `<div class="mono">${c}</div>`).join('');
cipherList.style.display = 'block';
icon.className = 'bi bi-chevron-up';
} else {
cipherList.style.display = 'none';
icon.className = 'bi bi-chevron-down';
}
}
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -151,6 +151,25 @@
let statusFilter = ''; let statusFilter = '';
let totalCount = 0; 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}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
container.appendChild(notification);
// Auto-dismiss after 5 seconds
setTimeout(() => {
notification.remove();
}, 5000);
}
// Load initial data when page loads // Load initial data when page loads
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
loadScans(); loadScans();
@@ -456,15 +475,7 @@
bootstrap.Modal.getInstance(document.getElementById('triggerScanModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('triggerScanModal')).hide();
// Show success message // Show success message
const alertDiv = document.createElement('div'); showAlert('success', `Scan triggered successfully! (ID: ${data.scan_id})`);
alertDiv.className = 'alert alert-success alert-dismissible fade show mt-3';
alertDiv.innerHTML = `
Scan triggered successfully! (ID: ${data.scan_id})
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Insert at the beginning of container-fluid
const container = document.querySelector('.container-fluid');
container.insertBefore(alertDiv, container.firstChild);
// Refresh scans // Refresh scans
loadScans(); loadScans();
@@ -490,23 +501,18 @@
}); });
if (!response.ok) { 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 // Show success message
const alertDiv = document.createElement('div'); showAlert('success', `Scan ${scanId} deleted successfully.`);
alertDiv.className = 'alert alert-success alert-dismissible fade show mt-3';
alertDiv.innerHTML = `
Scan ${scanId} deleted successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.container-fluid').insertBefore(alertDiv, document.querySelector('.row'));
// Refresh scans // Refresh scans
loadScans(); loadScans();
} catch (error) { } catch (error) {
console.error('Error deleting scan:', error); console.error('Error deleting scan:', error);
alert('Failed to delete scan. Please try again.'); showAlert('danger', `Failed to delete scan: ${error.message}`);
} }
} }

View File

@@ -720,7 +720,7 @@ Retrieve a paginated list of scans with optional status filtering.
"duration": 125.5, "duration": 125.5,
"status": "completed", "status": "completed",
"title": "Production Network Scan", "title": "Production Network Scan",
"config_id": "/app/configs/production.yaml", "config_id": 1,
"triggered_by": "manual", "triggered_by": "manual",
"started_at": "2025-11-14T10:30:00Z", "started_at": "2025-11-14T10:30:00Z",
"completed_at": "2025-11-14T10:32:05Z" "completed_at": "2025-11-14T10:32:05Z"
@@ -731,7 +731,7 @@ Retrieve a paginated list of scans with optional status filtering.
"duration": 98.2, "duration": 98.2,
"status": "completed", "status": "completed",
"title": "Development Network Scan", "title": "Development Network Scan",
"config_id": "/app/configs/dev.yaml", "config_id": 2,
"triggered_by": "scheduled", "triggered_by": "scheduled",
"started_at": "2025-11-13T15:00:00Z", "started_at": "2025-11-13T15:00:00Z",
"completed_at": "2025-11-13T15:01:38Z" "completed_at": "2025-11-13T15:01:38Z"
@@ -793,7 +793,7 @@ Retrieve complete details for a specific scan, including all sites, IPs, ports,
"duration": 125.5, "duration": 125.5,
"status": "completed", "status": "completed",
"title": "Production Network Scan", "title": "Production Network Scan",
"config_id": "/app/configs/production.yaml", "config_id": 1,
"json_path": "/app/output/scan_report_20251114_103000.json", "json_path": "/app/output/scan_report_20251114_103000.json",
"html_path": "/app/output/scan_report_20251114_103000.html", "html_path": "/app/output/scan_report_20251114_103000.html",
"zip_path": "/app/output/scan_report_20251114_103000.zip", "zip_path": "/app/output/scan_report_20251114_103000.zip",
@@ -968,7 +968,8 @@ Delete a scan and all associated files (JSON, HTML, ZIP, screenshots).
**Success Response (200 OK):** **Success Response (200 OK):**
```json ```json
{ {
"message": "Scan 42 deleted successfully" "scan_id": 42,
"message": "Scan deleted successfully"
} }
``` ```
@@ -1111,7 +1112,7 @@ Retrieve a list of all schedules with pagination and filtering.
{ {
"id": 1, "id": 1,
"name": "Daily Production Scan", "name": "Daily Production Scan",
"config_id": "/app/configs/prod-scan.yaml", "config_id": 1,
"cron_expression": "0 2 * * *", "cron_expression": "0 2 * * *",
"enabled": true, "enabled": true,
"created_at": "2025-11-01T10:00:00Z", "created_at": "2025-11-01T10:00:00Z",
@@ -1157,7 +1158,7 @@ Retrieve details for a specific schedule including execution history.
{ {
"id": 1, "id": 1,
"name": "Daily Production Scan", "name": "Daily Production Scan",
"config_id": "/app/configs/prod-scan.yaml", "config_id": 1,
"cron_expression": "0 2 * * *", "cron_expression": "0 2 * * *",
"enabled": true, "enabled": true,
"created_at": "2025-11-01T10:00:00Z", "created_at": "2025-11-01T10:00:00Z",
@@ -1201,7 +1202,7 @@ Create a new scheduled scan.
```json ```json
{ {
"name": "Daily Production Scan", "name": "Daily Production Scan",
"config_id": "/app/configs/prod-scan.yaml", "config_id": 1,
"cron_expression": "0 2 * * *", "cron_expression": "0 2 * * *",
"enabled": true "enabled": true
} }
@@ -1215,7 +1216,7 @@ Create a new scheduled scan.
"schedule": { "schedule": {
"id": 1, "id": 1,
"name": "Daily Production Scan", "name": "Daily Production Scan",
"config_id": "/app/configs/prod-scan.yaml", "config_id": 1,
"cron_expression": "0 2 * * *", "cron_expression": "0 2 * * *",
"enabled": true, "enabled": true,
"created_at": "2025-11-01T10:00:00Z" "created_at": "2025-11-01T10:00:00Z"
@@ -1236,7 +1237,7 @@ Create a new scheduled scan.
```bash ```bash
curl -X POST http://localhost:5000/api/schedules \ curl -X POST http://localhost:5000/api/schedules \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"name":"Daily Scan","config_id":"/app/configs/prod.yaml","cron_expression":"0 2 * * *"}' \ -d '{"name":"Daily Scan","config_id":1,"cron_expression":"0 2 * * *"}' \
-b cookies.txt -b cookies.txt
``` ```
@@ -1270,7 +1271,7 @@ Update an existing schedule.
"schedule": { "schedule": {
"id": 1, "id": 1,
"name": "Updated Schedule Name", "name": "Updated Schedule Name",
"config_id": "/app/configs/prod-scan.yaml", "config_id": 1,
"cron_expression": "0 3 * * *", "cron_expression": "0 3 * * *",
"enabled": false, "enabled": false,
"updated_at": "2025-11-15T10:00:00Z" "updated_at": "2025-11-15T10:00:00Z"
@@ -1512,7 +1513,7 @@ Get historical trend data for scans with the same configuration.
], ],
"labels": ["2025-11-10 12:00", "2025-11-15 12:00"], "labels": ["2025-11-10 12:00", "2025-11-15 12:00"],
"port_counts": [25, 26], "port_counts": [25, 26],
"config_id": "/app/configs/prod-scan.yaml" "config_id": 1
} }
``` ```
@@ -1632,7 +1633,8 @@ Retrieve a specific setting by key.
{ {
"status": "success", "status": "success",
"key": "smtp_server", "key": "smtp_server",
"value": "smtp.gmail.com" "value": "smtp.gmail.com",
"read_only": false
} }
``` ```
@@ -2342,6 +2344,9 @@ List all configured webhooks with pagination.
"severity_filter": ["critical", "warning"], "severity_filter": ["critical", "warning"],
"timeout": 10, "timeout": 10,
"retry_count": 3, "retry_count": 3,
"template": null,
"template_format": "json",
"content_type_override": null,
"created_at": "2025-11-18T10:00:00Z", "created_at": "2025-11-18T10:00:00Z",
"updated_at": "2025-11-18T10:00:00Z" "updated_at": "2025-11-18T10:00:00Z"
} }
@@ -2393,6 +2398,9 @@ Get details for a specific webhook.
"severity_filter": ["critical"], "severity_filter": ["critical"],
"timeout": 10, "timeout": 10,
"retry_count": 3, "retry_count": 3,
"template": null,
"template_format": "json",
"content_type_override": null,
"created_at": "2025-11-18T10:00:00Z", "created_at": "2025-11-18T10:00:00Z",
"updated_at": "2025-11-18T10:00:00Z" "updated_at": "2025-11-18T10:00:00Z"
} }
@@ -2475,6 +2483,9 @@ Create a new webhook configuration.
"custom_headers": null, "custom_headers": null,
"timeout": 10, "timeout": 10,
"retry_count": 3, "retry_count": 3,
"template": null,
"template_format": "json",
"content_type_override": null,
"created_at": "2025-11-18T10:00:00Z" "created_at": "2025-11-18T10:00:00Z"
} }
} }
@@ -2577,6 +2588,9 @@ Update an existing webhook configuration.
"custom_headers": null, "custom_headers": null,
"timeout": 15, "timeout": 15,
"retry_count": 3, "retry_count": 3,
"template": null,
"template_format": "json",
"content_type_override": null,
"updated_at": "2025-11-18T11:00:00Z" "updated_at": "2025-11-18T11:00:00Z"
} }
} }
@@ -3310,9 +3324,9 @@ API versioning will be implemented in future releases. The API is considered sta
- **Webhooks API** - Webhook management, delivery tracking, authentication support, retry logic - **Webhooks API** - Webhook management, delivery tracking, authentication support, retry logic
### Endpoint Count ### Endpoint Count
- Total endpoints: 80+ - Total endpoints: 65+
- Authenticated endpoints: 75+ - Authenticated endpoints: 60+
- Public endpoints: 5 (login, setup, health checks) - Public endpoints: 5 (login, setup, health checks for scans/schedules/settings/alerts/webhooks)
--- ---