Add HTML report generation with dark theme
Implements comprehensive HTML report generation from JSON scan data with Jinja2 templates. Reports feature a dark slate theme with summary dashboard, drift alerts, security warnings, and expandable service details. Features: - Dark theme HTML reports with slate/grey color scheme - Summary dashboard: scan statistics, drift alerts, security warnings - Site-by-site breakdown with IP grouping and status badges - Expandable service details and SSL/TLS certificate information - Visual badges: green (expected), red (unexpected), yellow (missing) - UDP port handling: shows expected, unexpected, and missing UDP ports - Screenshot links with relative paths for portability - Optimized hover effects for table rows - Standalone HTML output (no external dependencies) Technical changes: - Added src/report_generator.py: HTMLReportGenerator class with summary calculations - Added templates/report_template.html: Jinja2 template for dynamic reports - Added templates/report_mockup.html: Static mockup for design testing - Updated requirements.txt: Added Jinja2==3.1.2 - Updated README.md: Added HTML report generation section with usage and features - Updated CLAUDE.md: Added implementation details, usage guide, and troubleshooting Usage: python3 src/report_generator.py output/scan_report.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
949
templates/report_template.html
Normal file
949
templates/report_template.html
Normal file
@@ -0,0 +1,949 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SneakyScanner Report - {{ title }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #0f172a;
|
||||
color: #e2e8f0;
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #475569;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.header-meta {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
color: #94a3b8;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.header-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Summary Dashboard */
|
||||
.dashboard {
|
||||
background-color: #1e293b;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.dashboard h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 20px;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background-color: #0f172a;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.dashboard-card h3 {
|
||||
font-size: 1rem;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 15px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #e2e8f0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.alert-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
background-color: #1e293b;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid;
|
||||
}
|
||||
|
||||
.alert-item.critical {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.alert-item.warning {
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.alert-item.info {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.badge.expected {
|
||||
background-color: #065f46;
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
.badge.unexpected {
|
||||
background-color: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.badge.missing {
|
||||
background-color: #78350f;
|
||||
color: #fcd34d;
|
||||
}
|
||||
|
||||
.badge.critical {
|
||||
background-color: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.badge.warning {
|
||||
background-color: #78350f;
|
||||
color: #fcd34d;
|
||||
}
|
||||
|
||||
.badge.good {
|
||||
background-color: #065f46;
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
.badge.info {
|
||||
background-color: #1e3a8a;
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.badge-count {
|
||||
background-color: #334155;
|
||||
color: #e2e8f0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Site Section */
|
||||
.site-section {
|
||||
background-color: #1e293b;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 25px;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #334155;
|
||||
}
|
||||
|
||||
.site-header h2 {
|
||||
font-size: 1.75rem;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.site-stats {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* IP Section */
|
||||
.ip-section {
|
||||
background-color: #0f172a;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.ip-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.ip-header h3 {
|
||||
font-size: 1.25rem;
|
||||
color: #e2e8f0;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.ip-badges {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Service Table */
|
||||
.service-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 15px;
|
||||
background-color: #1e293b;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.service-table thead {
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
.service-table th {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.service-table td {
|
||||
padding: 12px;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.service-table tbody tr {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.service-table tbody tr:hover {
|
||||
background-color: #334155;
|
||||
border-left-color: #60a5fa;
|
||||
}
|
||||
|
||||
.service-row-clickable {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.service-row-clickable::after {
|
||||
content: '▼';
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
color: #64748b;
|
||||
font-size: 0.7rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.service-row-clickable.expanded::after {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
/* Service Details Card */
|
||||
.service-details {
|
||||
display: none;
|
||||
background-color: #0f172a;
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #334155;
|
||||
border-left: 3px solid #60a5fa;
|
||||
}
|
||||
|
||||
.service-details.show {
|
||||
display: block;
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #94a3b8;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #e2e8f0;
|
||||
font-weight: 500;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* SSL/TLS Section */
|
||||
.ssl-section {
|
||||
background-color: #1e293b;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-top: 15px;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.ssl-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.ssl-header h4 {
|
||||
color: #60a5fa;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.ssl-toggle {
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.ssl-content {
|
||||
display: none;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.ssl-content.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cert-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tls-versions {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.tls-version-item {
|
||||
background-color: #0f172a;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 3px solid;
|
||||
}
|
||||
|
||||
.tls-version-item.supported {
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.tls-version-item.unsupported {
|
||||
border-color: #64748b;
|
||||
}
|
||||
|
||||
.tls-version-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cipher-list {
|
||||
color: #94a3b8;
|
||||
font-size: 0.85rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.cipher-list li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* Screenshot Link */
|
||||
.screenshot-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #60a5fa;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 10px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.screenshot-link:hover {
|
||||
color: #93c5fd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<h1>{{ title }}</h1>
|
||||
<div class="header-meta">
|
||||
<span>📅 <strong>Scan Time:</strong> {{ scan_time | format_date }}</span>
|
||||
<span>⏱️ <strong>Duration:</strong> {{ scan_duration | format_duration }}</span>
|
||||
{% if config_file %}
|
||||
<span>📄 <strong>Config:</strong> {{ config_file }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Dashboard -->
|
||||
<div class="dashboard">
|
||||
<h2>Scan Summary</h2>
|
||||
<div class="dashboard-grid">
|
||||
<!-- Statistics Card -->
|
||||
<div class="dashboard-card">
|
||||
<h3>Scan Statistics</h3>
|
||||
<div class="stat-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Total IPs Scanned</span>
|
||||
<span class="stat-value">{{ summary_stats.total_ips }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">TCP Ports Found</span>
|
||||
<span class="stat-value">{{ summary_stats.tcp_ports }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">UDP Ports Found</span>
|
||||
<span class="stat-value">{{ summary_stats.udp_ports }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Services Identified</span>
|
||||
<span class="stat-value">{{ summary_stats.services }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Web Services</span>
|
||||
<span class="stat-value">{{ summary_stats.web_services }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Screenshots Captured</span>
|
||||
<span class="stat-value">{{ summary_stats.screenshots }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drift Alerts Card -->
|
||||
<div class="dashboard-card">
|
||||
<h3>Drift Alerts</h3>
|
||||
<div class="alert-grid">
|
||||
{% if drift_alerts.unexpected_tcp > 0 %}
|
||||
<div class="alert-item warning">
|
||||
<span>Unexpected TCP Ports</span>
|
||||
<span class="badge-count">{{ drift_alerts.unexpected_tcp }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if drift_alerts.unexpected_udp > 0 %}
|
||||
<div class="alert-item warning">
|
||||
<span>Unexpected UDP Ports</span>
|
||||
<span class="badge-count">{{ drift_alerts.unexpected_udp }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if drift_alerts.missing_tcp > 0 or drift_alerts.missing_udp > 0 %}
|
||||
<div class="alert-item critical">
|
||||
<span>Missing Expected Services</span>
|
||||
<span class="badge-count">{{ drift_alerts.missing_tcp + drift_alerts.missing_udp }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if drift_alerts.new_services > 0 %}
|
||||
<div class="alert-item info">
|
||||
<span>New Services Detected</span>
|
||||
<span class="badge-count">{{ drift_alerts.new_services }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if drift_alerts.unexpected_tcp == 0 and drift_alerts.unexpected_udp == 0 and drift_alerts.missing_tcp == 0 and drift_alerts.missing_udp == 0 %}
|
||||
<div class="alert-item info">
|
||||
<span>No drift detected - all services match expectations</span>
|
||||
<span class="badge good">✓</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Warnings Card -->
|
||||
<div class="dashboard-card">
|
||||
<h3>Security Warnings</h3>
|
||||
<div class="alert-grid">
|
||||
{% if security_warnings.expiring_certs > 0 %}
|
||||
<div class="alert-item critical">
|
||||
<span>Certificates Expiring Soon (<30 days)</span>
|
||||
<span class="badge-count">{{ security_warnings.expiring_certs }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if security_warnings.weak_tls > 0 %}
|
||||
<div class="alert-item warning">
|
||||
<span>Weak TLS Versions (1.0/1.1)</span>
|
||||
<span class="badge-count">{{ security_warnings.weak_tls }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if security_warnings.self_signed > 0 %}
|
||||
<div class="alert-item warning">
|
||||
<span>Self-Signed Certificates</span>
|
||||
<span class="badge-count">{{ security_warnings.self_signed }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if security_warnings.high_ports > 0 %}
|
||||
<div class="alert-item info">
|
||||
<span>High Port Services (>10000)</span>
|
||||
<span class="badge-count">{{ security_warnings.high_ports }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if security_warnings.expiring_certs == 0 and security_warnings.weak_tls == 0 and security_warnings.self_signed == 0 %}
|
||||
<div class="alert-item info">
|
||||
<span>No critical security warnings detected</span>
|
||||
<span class="badge good">✓</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sites -->
|
||||
{% for site in sites %}
|
||||
<div class="site-section">
|
||||
<div class="site-header">
|
||||
<h2>{{ site.name }}</h2>
|
||||
<div class="site-stats">
|
||||
<span>{{ site.ips | length }} IP{{ 's' if site.ips | length != 1 else '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IPs -->
|
||||
{% for ip_data in site.ips %}
|
||||
{% set expected = ip_data.expected %}
|
||||
{% set actual = ip_data.actual %}
|
||||
{% set expected_tcp = expected.tcp_ports | default([]) | list %}
|
||||
{% set actual_tcp = actual.tcp_ports | default([]) | list %}
|
||||
{% set expected_udp = expected.udp_ports | default([]) | list %}
|
||||
{% set actual_udp = actual.udp_ports | default([]) | list %}
|
||||
{% set unexpected_tcp = actual_tcp | reject('in', expected_tcp) | list %}
|
||||
{% set unexpected_udp = actual_udp | reject('in', expected_udp) | list %}
|
||||
{% set missing_tcp = expected_tcp | reject('in', actual_tcp) | list %}
|
||||
{% set missing_udp = expected_udp | reject('in', actual_udp) | list %}
|
||||
|
||||
<div class="ip-section">
|
||||
<div class="ip-header">
|
||||
<h3>{{ ip_data.address }}</h3>
|
||||
<div class="ip-badges">
|
||||
{% if expected.ping %}
|
||||
{% if actual.ping %}
|
||||
<span class="badge expected">Ping: Expected</span>
|
||||
{% else %}
|
||||
<span class="badge missing">Ping: Missing</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if (unexpected_tcp | length) > 0 or (unexpected_udp | length) > 0 %}
|
||||
<span class="badge warning">{{ (unexpected_tcp | length) + (unexpected_udp | length) }} Unexpected Port{{ 's' if ((unexpected_tcp | length) + (unexpected_udp | length)) > 1 else '' }}</span>
|
||||
{% elif (missing_tcp | length) > 0 or (missing_udp | length) > 0 %}
|
||||
<span class="badge critical">{{ (missing_tcp | length) + (missing_udp | length) }} Missing Service{{ 's' if ((missing_tcp | length) + (missing_udp | length)) > 1 else '' }}</span>
|
||||
{% else %}
|
||||
<span class="badge good">All Ports Expected</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="service-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Port</th>
|
||||
<th>Protocol</th>
|
||||
<th>Service</th>
|
||||
<th>Product</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for service in actual.services | default([]) %}
|
||||
{% set service_id = 'service_' ~ loop.index ~ '_' ~ ip_data.address | replace('.', '_') %}
|
||||
{% set is_expected = service.port in expected_tcp or service.port in expected_udp %}
|
||||
<tr class="service-row-clickable" onclick="toggleDetails('{{ service_id }}')">
|
||||
<td class="mono">{{ service.port }}</td>
|
||||
<td>{{ service.protocol | upper }}</td>
|
||||
<td>{{ service.service | default('unknown') }}</td>
|
||||
<td>{{ service.product | default('') }} {% if service.version %}{{ service.version }}{% endif %}</td>
|
||||
<td>
|
||||
{% if is_expected %}
|
||||
<span class="badge expected">Expected</span>
|
||||
{% else %}
|
||||
<span class="badge unexpected">Unexpected</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div id="{{ service_id }}" class="service-details">
|
||||
<div class="details-grid">
|
||||
{% if service.product %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Product</span>
|
||||
<span class="detail-value">{{ service.product }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if service.version %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Version</span>
|
||||
<span class="detail-value">{{ service.version }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if service.extrainfo %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Extra Info</span>
|
||||
<span class="detail-value">{{ service.extrainfo }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if service.ostype %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">OS Type</span>
|
||||
<span class="detail-value">{{ service.ostype }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if service.http_info %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Protocol</span>
|
||||
<span class="detail-value">{{ service.http_info.protocol | upper }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not is_expected %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">⚠️ Status</span>
|
||||
<span class="detail-value text-warning">Not in expected ports list</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if service.http_info and service.http_info.screenshot %}
|
||||
<a href="{{ service.http_info.screenshot }}" class="screenshot-link" target="_blank">
|
||||
🖼️ View Screenshot
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if service.http_info and service.http_info.ssl_tls %}
|
||||
{% set ssl_id = 'ssl_' ~ loop.index ~ '_' ~ ip_data.address | replace('.', '_') %}
|
||||
{% set ssl = service.http_info.ssl_tls %}
|
||||
{% set cert = ssl.certificate %}
|
||||
<div class="ssl-section">
|
||||
<div class="ssl-header" onclick="toggleSSL('{{ ssl_id }}')">
|
||||
<h4>🔒 SSL/TLS Details
|
||||
{% if cert.days_until_expiry is defined and cert.days_until_expiry < 30 %}
|
||||
<span class="badge critical" style="margin-left: 10px;">Certificate Expiring Soon</span>
|
||||
{% elif cert.issuer == cert.subject %}
|
||||
<span class="badge warning" style="margin-left: 10px;">Self-Signed Certificate</span>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<span class="ssl-toggle">Click to expand ▼</span>
|
||||
</div>
|
||||
<div id="{{ ssl_id }}" class="ssl-content">
|
||||
<h5 style="color: #94a3b8; margin-bottom: 10px;">Certificate Information</h5>
|
||||
<div class="cert-grid">
|
||||
{% if cert.subject %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Subject</span>
|
||||
<span class="detail-value">{{ cert.subject }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if cert.issuer %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Issuer</span>
|
||||
<span class="detail-value {% if cert.issuer == cert.subject %}text-warning{% endif %}">
|
||||
{{ cert.issuer }}{% if cert.issuer == cert.subject %} (Self-Signed){% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if cert.not_valid_before %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Valid From</span>
|
||||
<span class="detail-value">{{ cert.not_valid_before | format_date }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if cert.not_valid_after %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Valid Until</span>
|
||||
<span class="detail-value {% if cert.days_until_expiry is defined and cert.days_until_expiry < 30 %}text-danger{% else %}text-success{% endif %}">
|
||||
{{ cert.not_valid_after | format_date }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if cert.days_until_expiry is defined %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Days Until Expiry</span>
|
||||
<span class="detail-value {% if cert.days_until_expiry < 30 %}text-danger{% else %}text-success{% endif %}">
|
||||
{{ cert.days_until_expiry }} days{% if cert.days_until_expiry < 30 %} ⚠️{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if cert.serial_number %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Serial Number</span>
|
||||
<span class="detail-value">{{ cert.serial_number }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if cert.sans %}
|
||||
<div class="detail-item" style="margin-bottom: 15px;">
|
||||
<span class="detail-label">Subject Alternative Names (SANs)</span>
|
||||
<span class="detail-value">{{ cert.sans | join(', ') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ssl.tls_versions %}
|
||||
<div class="tls-versions">
|
||||
<h5 style="color: #94a3b8; margin-bottom: 10px;">TLS Version Support</h5>
|
||||
|
||||
{% for version_name in ['TLS 1.0', 'TLS 1.1', 'TLS 1.2', 'TLS 1.3'] %}
|
||||
{% set tls_version = ssl.tls_versions.get(version_name, {}) %}
|
||||
{% if tls_version.supported %}
|
||||
<div class="tls-version-item supported">
|
||||
<div class="tls-version-header">
|
||||
<strong>{{ version_name }}</strong>
|
||||
{% if version_name in ['TLS 1.0', 'TLS 1.1'] %}
|
||||
<span class="badge warning">Supported (Weak) ⚠️</span>
|
||||
{% else %}
|
||||
<span class="badge good">Supported</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if tls_version.cipher_suites %}
|
||||
<ul class="cipher-list">
|
||||
{% for cipher in tls_version.cipher_suites %}
|
||||
<li>{{ cipher }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif tls_version.supported is defined %}
|
||||
<div class="tls-version-item unsupported">
|
||||
<div class="tls-version-header">
|
||||
<strong>{{ version_name }}</strong>
|
||||
<span class="badge info">Not Supported</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{# Show expected UDP ports (UDP ports found and expected, with no service details) #}
|
||||
{% for port in actual_udp %}
|
||||
{% if port in expected_udp %}
|
||||
<tr class="service-row-clickable" onclick="toggleDetails('udp_{{ port }}_{{ ip_data.address | replace('.', '_') }}')">
|
||||
<td class="mono">{{ port }}</td>
|
||||
<td>UDP</td>
|
||||
<td colspan="2" class="text-muted">No service detection available</td>
|
||||
<td><span class="badge expected">Expected</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div id="udp_{{ port }}_{{ ip_data.address | replace('.', '_') }}" class="service-details">
|
||||
<div class="details-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Protocol</span>
|
||||
<span class="detail-value">UDP</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Note</span>
|
||||
<span class="detail-value text-muted">Service detection not available for UDP ports</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{# Show unexpected UDP ports (UDP ports found but not expected, with no service details) #}
|
||||
{% for port in unexpected_udp %}
|
||||
<tr class="service-row-clickable" onclick="toggleDetails('udp_{{ port }}_{{ ip_data.address | replace('.', '_') }}')">
|
||||
<td class="mono">{{ port }}</td>
|
||||
<td>UDP</td>
|
||||
<td colspan="2" class="text-muted">No service detection available</td>
|
||||
<td><span class="badge unexpected">Unexpected</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div id="udp_{{ port }}_{{ ip_data.address | replace('.', '_') }}" class="service-details">
|
||||
<div class="details-grid">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">⚠️ Status</span>
|
||||
<span class="detail-value text-warning">UDP port discovered but not in expected ports list. Service detection not available for UDP.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{# Show missing expected services #}
|
||||
{% for port in missing_tcp %}
|
||||
<tr style="background-color: rgba(127, 29, 29, 0.2);">
|
||||
<td class="mono">{{ port }}</td>
|
||||
<td>TCP</td>
|
||||
<td colspan="2" class="text-danger">❌ Expected but not found</td>
|
||||
<td><span class="badge missing">Missing</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for port in missing_udp %}
|
||||
<tr style="background-color: rgba(127, 29, 29, 0.2);">
|
||||
<td class="mono">{{ port }}</td>
|
||||
<td>UDP</td>
|
||||
<td colspan="2" class="text-danger">❌ Expected but not found</td>
|
||||
<td><span class="badge missing">Missing</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleDetails(id) {
|
||||
const details = document.getElementById(id);
|
||||
const row = event.currentTarget;
|
||||
|
||||
if (details.classList.contains('show')) {
|
||||
details.classList.remove('show');
|
||||
row.classList.remove('expanded');
|
||||
} else {
|
||||
details.classList.add('show');
|
||||
row.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSSL(id) {
|
||||
const sslContent = document.getElementById(id);
|
||||
const header = event.currentTarget;
|
||||
const toggle = header.querySelector('.ssl-toggle');
|
||||
|
||||
if (sslContent.classList.contains('show')) {
|
||||
sslContent.classList.remove('show');
|
||||
toggle.textContent = 'Click to expand ▼';
|
||||
} else {
|
||||
sslContent.classList.add('show');
|
||||
toggle.textContent = 'Click to collapse ▲';
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user