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:
199
CLAUDE.md
199
CLAUDE.md
@@ -61,11 +61,21 @@ python3 -c "import yaml; yaml.safe_load(open('configs/example-site.yaml'))"
|
||||
- `_get_screenshot_dir()`: Creates screenshots subdirectory
|
||||
- `_generate_filename()`: Generates filename for screenshot (IP_PORT.png)
|
||||
|
||||
3. **configs/** - YAML configuration files
|
||||
3. **src/report_generator.py** - HTML report generator
|
||||
- `HTMLReportGenerator` class: Generates comprehensive HTML reports from JSON scan data
|
||||
- `generate_report()`: Main method to generate HTML report with summary dashboard and service details
|
||||
- `_load_json_report()`: Loads and parses JSON scan report
|
||||
- `_calculate_summary_stats()`: Calculates scan statistics for dashboard (IPs, ports, services, screenshots)
|
||||
- `_identify_drift_alerts()`: Identifies unexpected/missing ports and services
|
||||
- `_identify_security_warnings()`: Identifies security issues (expiring certs, weak TLS, self-signed certs)
|
||||
- `_format_date()`: Helper to format ISO date strings
|
||||
- `_format_duration()`: Helper to format scan duration
|
||||
|
||||
4. **configs/** - YAML configuration files
|
||||
- Define scan title, sites, IPs, and expected network behavior
|
||||
- Each IP includes expected ping response and TCP/UDP ports
|
||||
|
||||
4. **output/** - JSON scan reports and screenshots
|
||||
5. **output/** - JSON scan reports and screenshots
|
||||
- Timestamped JSON files: `scan_report_YYYYMMDD_HHMMSS.json`
|
||||
- Screenshot directory: `scan_report_YYYYMMDD_HHMMSS_screenshots/`
|
||||
- Contains actual vs. expected comparison for each IP
|
||||
@@ -266,6 +276,41 @@ JSON structure defined in src/scanner.py:365+. To modify:
|
||||
3. Update README.md output format documentation
|
||||
4. Update example output in both README.md and CLAUDE.md
|
||||
|
||||
### Generating HTML Reports
|
||||
|
||||
**Basic usage:**
|
||||
```bash
|
||||
# Generate HTML report from most recent JSON scan
|
||||
python3 src/report_generator.py output/scan_report_20251113_175235.json
|
||||
```
|
||||
|
||||
**Custom output location:**
|
||||
```bash
|
||||
# Specify output filename
|
||||
python3 src/report_generator.py output/scan.json reports/my_report.html
|
||||
```
|
||||
|
||||
**Programmatic usage:**
|
||||
```python
|
||||
from src.report_generator import HTMLReportGenerator
|
||||
|
||||
generator = HTMLReportGenerator('output/scan_report.json')
|
||||
html_path = generator.generate_report('custom_output.html')
|
||||
print(f"Report generated: {html_path}")
|
||||
```
|
||||
|
||||
**Customizing the HTML template:**
|
||||
- Edit `templates/report_template.html` to modify layout, colors, or content
|
||||
- Template uses Jinja2 syntax with variables like `{{ title }}`, `{% for site in sites %}`
|
||||
- CSS is embedded in the template for portability (single file output)
|
||||
- Test design changes with `templates/report_mockup.html` first
|
||||
|
||||
**Output:**
|
||||
- Standalone HTML file (no external dependencies)
|
||||
- Can be opened directly in any browser
|
||||
- Dark theme optimized for readability
|
||||
- Screenshot links are relative paths (keep output directory structure intact)
|
||||
|
||||
### Customizing Screenshot Capture
|
||||
|
||||
**Change viewport size** (src/screenshot_capture.py:35):
|
||||
@@ -339,53 +384,69 @@ Optimization strategies:
|
||||
- Adjust screenshot timeout (currently 15 seconds in src/screenshot_capture.py:34)
|
||||
- Disable screenshot capture for faster scans (set screenshot_capture to None)
|
||||
|
||||
## HTML Report Generation (✅ Implemented)
|
||||
|
||||
SneakyScanner now includes comprehensive HTML report generation from JSON scan data.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Generate HTML report from JSON scan output
|
||||
python3 src/report_generator.py output/scan_report_20251113_175235.json
|
||||
|
||||
# Specify custom output path
|
||||
python3 src/report_generator.py output/scan.json custom_report.html
|
||||
```
|
||||
|
||||
**Implemented Features:**
|
||||
- ✅ Dark theme with slate/grey color scheme for easy reading
|
||||
- ✅ Summary dashboard with scan statistics, drift alerts, and security warnings
|
||||
- ✅ Site-by-site breakdown with IP grouping
|
||||
- ✅ Service details with expandable cards (click to expand)
|
||||
- ✅ Visual badges for expected vs. unexpected services (green/red/yellow)
|
||||
- ✅ SSL/TLS certificate details with expandable sections
|
||||
- ✅ TLS version support display with cipher suites
|
||||
- ✅ Certificate expiration warnings (highlighted for <30 days)
|
||||
- ✅ Weak TLS version detection (TLS 1.0/1.1)
|
||||
- ✅ Self-signed certificate identification
|
||||
- ✅ Screenshot links (referenced, not embedded)
|
||||
- ✅ Missing expected services clearly marked
|
||||
- ✅ UDP port handling:
|
||||
- Expected UDP ports shown with green "Expected" badge
|
||||
- Unexpected UDP ports shown with red "Unexpected" badge
|
||||
- Missing expected UDP ports shown with yellow "Missing" badge
|
||||
- Note displayed that service detection not available for UDP
|
||||
- ✅ Responsive layout for different screen sizes
|
||||
- ✅ No JavaScript dependencies - pure HTML/CSS with minimal vanilla JS
|
||||
- ✅ Optimized hover effects for table rows (lighter background + blue left border)
|
||||
|
||||
**Template Architecture:**
|
||||
- `templates/report_template.html` - Jinja2 template for dynamic report generation
|
||||
- `templates/report_mockup.html` - Static mockup for design testing
|
||||
- Custom CSS with dark slate theme (#0f172a background, #60a5fa accents)
|
||||
- Expandable service details and SSL/TLS sections
|
||||
- Color-coded status badges (expected=green, unexpected=red, missing=yellow)
|
||||
|
||||
**HTMLReportGenerator Class** (`src/report_generator.py`):
|
||||
- Loads JSON scan reports
|
||||
- Calculates summary statistics (IPs, ports, services, screenshots)
|
||||
- Identifies drift alerts (unexpected/missing ports and services)
|
||||
- Identifies security warnings (expiring certs, weak TLS, self-signed certs, high ports)
|
||||
- Renders Jinja2 template with context data
|
||||
- Outputs standalone HTML file (no server required)
|
||||
|
||||
**Future Enhancements for HTML Reports:**
|
||||
- Sortable/filterable tables with JavaScript
|
||||
- Timeline view of scan history
|
||||
- Scan comparison (diff between two reports)
|
||||
- Export to PDF capability
|
||||
- Interactive charts/graphs for trends
|
||||
- Embedded screenshot thumbnails (currently links only)
|
||||
|
||||
## Planned Features (Future Development)
|
||||
|
||||
The following features are planned for future implementation:
|
||||
|
||||
### 1. HTML Report Generation
|
||||
Build comprehensive HTML reports from JSON scan data with interactive visualizations.
|
||||
|
||||
**Report Features:**
|
||||
- Service details and SSL/TLS information tables
|
||||
- Visual comparison of expected vs. actual results (red/green highlighting)
|
||||
- Certificate expiration warnings with countdown timers
|
||||
- TLS version compliance reports (highlight weak configurations)
|
||||
- Embedded webpage screenshots
|
||||
- Sortable/filterable tables
|
||||
- Timeline view of scan history
|
||||
- Export to PDF capability
|
||||
|
||||
**Implementation Considerations:**
|
||||
- Template engine: Jinja2 or similar
|
||||
- CSS framework: Bootstrap or Tailwind for responsive design
|
||||
- Charts/graphs: Chart.js or Plotly for visualizations
|
||||
- Store templates in `templates/` directory
|
||||
- Generate static HTML that can be opened without server
|
||||
|
||||
**Architecture:**
|
||||
```python
|
||||
class HTMLReportGenerator:
|
||||
def __init__(self, json_report_path, template_dir='templates'):
|
||||
pass
|
||||
|
||||
def generate_report(self, output_path):
|
||||
# Parse JSON
|
||||
# Render template with data
|
||||
# Include screenshots
|
||||
# Write HTML file
|
||||
pass
|
||||
|
||||
def _compare_expected_actual(self, expected, actual):
|
||||
# Generate diff/comparison data
|
||||
pass
|
||||
|
||||
def _generate_cert_warnings(self, services):
|
||||
# Identify expiring certs, weak TLS, etc.
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. Comparison Reports (Scan Diffs)
|
||||
### 1. Comparison Reports (Scan Diffs)
|
||||
Generate reports showing changes between scans over time.
|
||||
|
||||
**Features:**
|
||||
@@ -409,18 +470,21 @@ Generate reports showing changes between scans over time.
|
||||
- python-libnmap==0.7.3 (nmap XML parsing)
|
||||
- sslyze==6.0.0 (SSL/TLS analysis)
|
||||
- playwright==1.40.0 (webpage screenshot capture)
|
||||
- Built-in: socket, ssl, subprocess, xml.etree.ElementTree, logging
|
||||
- Jinja2==3.1.2 (HTML report template engine)
|
||||
- Built-in: socket, ssl, subprocess, xml.etree.ElementTree, logging, json, pathlib, datetime
|
||||
- System: chromium, chromium-driver (installed via Dockerfile)
|
||||
|
||||
### For HTML Reports, Will Need:
|
||||
- Jinja2 (template engine)
|
||||
- Optional: weasyprint or pdfkit for PDF export
|
||||
### For Future Enhancements, May Need:
|
||||
- weasyprint or pdfkit for PDF export
|
||||
- Chart.js or Plotly for interactive visualizations
|
||||
|
||||
### Key Files to Modify for New Features:
|
||||
1. **src/scanner.py** - Core scanning logic (add new phases/methods)
|
||||
2. **src/screenshot_capture.py** - ✅ Implemented: Webpage screenshot capture module
|
||||
3. **src/report_generator.py** - New file for HTML report generation (planned)
|
||||
4. **templates/** - New directory for HTML templates (planned)
|
||||
3. **src/report_generator.py** - ✅ Implemented: HTML report generation with Jinja2 templates
|
||||
4. **templates/** - ✅ Implemented: Jinja2 templates for HTML reports
|
||||
- `report_template.html` - Main report template with dark theme
|
||||
- `report_mockup.html` - Static mockup for design testing
|
||||
5. **requirements.txt** - Add new dependencies
|
||||
6. **Dockerfile** - Install additional system dependencies (browsers, etc.)
|
||||
|
||||
@@ -475,6 +539,39 @@ Generate reports showing changes between scans over time.
|
||||
- **Check**: System certificates up to date in container
|
||||
- **Solution**: This should not happen; file an issue if it does
|
||||
|
||||
### HTML Report Generation Issues
|
||||
|
||||
**Problem**: "Template not found" error
|
||||
- **Check**: Verify `templates/report_template.html` exists
|
||||
- **Check**: Running script from correct directory (project root)
|
||||
- **Solution**: Ensure template directory structure is intact: `templates/report_template.html`
|
||||
|
||||
**Problem**: Report generated but looks broken/unstyled
|
||||
- **Check**: Browser opens HTML file correctly (not viewing source)
|
||||
- **Check**: CSS is embedded in template (should be in `<style>` tags)
|
||||
- **Solution**: Try different browser (Chrome, Firefox, Edge)
|
||||
|
||||
**Problem**: UDP ports not showing in report
|
||||
- **Check**: JSON report contains UDP port data in `actual.udp_ports`
|
||||
- **Solution**: Regenerate report with updated template (template was fixed to show UDP ports)
|
||||
- **Note**: Expected behavior - UDP ports show with note that service detection unavailable
|
||||
|
||||
**Problem**: Screenshot links broken in HTML report
|
||||
- **Check**: Screenshot directory exists in same parent directory as HTML file
|
||||
- **Check**: Screenshot paths in JSON are relative (not absolute)
|
||||
- **Solution**: Keep HTML report and screenshot directory together (don't move HTML file alone)
|
||||
- **Example**: If HTML is at `output/scan_report.html`, screenshots should be at `output/scan_report_screenshots/`
|
||||
|
||||
**Problem**: "Invalid JSON" error when generating report
|
||||
- **Check**: JSON report file is valid: `python3 -m json.tool output/scan_report.json`
|
||||
- **Check**: JSON file not truncated or corrupted
|
||||
- **Solution**: Re-run scan to generate fresh JSON report
|
||||
|
||||
**Problem**: Expandable sections not working (click doesn't expand)
|
||||
- **Check**: JavaScript is enabled in browser
|
||||
- **Check**: Browser console for JavaScript errors (F12 → Console)
|
||||
- **Solution**: This indicates template corruption; re-generate from `templates/report_template.html`
|
||||
|
||||
### Nmap/Masscan Issues
|
||||
|
||||
**Problem**: No ports discovered
|
||||
|
||||
91
README.md
91
README.md
@@ -41,6 +41,13 @@ A dockerized network scanning tool that uses masscan for fast port discovery, nm
|
||||
|
||||
### Reporting & Output
|
||||
- **Machine-readable JSON output** format for easy post-processing
|
||||
- **HTML report generation**:
|
||||
- Comprehensive HTML reports with dark theme for easy reading
|
||||
- Summary dashboard with scan statistics, drift alerts, and security warnings
|
||||
- Site-by-site breakdown with expandable service details
|
||||
- Visual badges for expected vs. unexpected services
|
||||
- SSL/TLS certificate details with expiration warnings
|
||||
- One-click generation from JSON scan data
|
||||
- **Dockerized** for consistent execution environment and root privilege isolation
|
||||
- **Expected vs. Actual comparison** to identify infrastructure drift
|
||||
- Timestamped reports with complete scan duration metrics
|
||||
@@ -269,17 +276,87 @@ Screenshots are captured on a best-effort basis:
|
||||
- Failed screenshots are logged but don't stop the scan
|
||||
- Services without screenshots simply omit the `screenshot` field in JSON output
|
||||
|
||||
## HTML Report Generation
|
||||
|
||||
SneakyScanner can generate comprehensive HTML reports from JSON scan data, providing an easy-to-read visual interface for analyzing scan results.
|
||||
|
||||
### Generating Reports
|
||||
|
||||
After completing a scan, generate an HTML report from the JSON output:
|
||||
|
||||
```bash
|
||||
# Generate HTML report (creates report in same directory as JSON)
|
||||
python3 src/report_generator.py output/scan_report_20251113_175235.json
|
||||
|
||||
# Specify custom output path
|
||||
python3 src/report_generator.py output/scan_report.json /path/to/custom_report.html
|
||||
```
|
||||
|
||||
### Report Features
|
||||
|
||||
The generated HTML report includes:
|
||||
|
||||
**Summary Dashboard**:
|
||||
- **Scan Statistics**: Total IPs scanned, TCP/UDP ports found, services identified, web services, screenshots captured
|
||||
- **Drift Alerts**: Unexpected TCP/UDP ports, missing expected services, new services detected
|
||||
- **Security Warnings**: Expiring certificates (<30 days), weak TLS versions (1.0/1.1), self-signed certificates, high port services (>10000)
|
||||
|
||||
**Site-by-Site Breakdown**:
|
||||
- Organized by logical site grouping from configuration
|
||||
- Per-IP sections with status badges (ping, port drift summary)
|
||||
- Service tables with expandable details (click any row to expand)
|
||||
- Visual badges: green (expected), red (unexpected), yellow (missing/warning)
|
||||
|
||||
**Service Details** (click to expand):
|
||||
- Product name, version, extra information, OS type
|
||||
- HTTP/HTTPS protocol detection
|
||||
- Screenshot links for web services
|
||||
- SSL/TLS certificate details (expandable):
|
||||
- Subject, issuer, validity dates, serial number
|
||||
- Days until expiration with color-coded warnings
|
||||
- Subject Alternative Names (SANs)
|
||||
- TLS version support (1.0, 1.1, 1.2, 1.3) with cipher suites
|
||||
- Weak TLS and self-signed certificate warnings
|
||||
|
||||
**UDP Port Handling**:
|
||||
- Expected UDP ports shown with green "Expected" badge
|
||||
- Unexpected UDP ports shown with red "Unexpected" badge
|
||||
- Missing expected UDP ports shown with yellow "Missing" badge
|
||||
- Note: Service detection not available for UDP (nmap limitation)
|
||||
|
||||
**Design**:
|
||||
- Dark theme with slate/grey color scheme for comfortable reading
|
||||
- Responsive layout works on different screen sizes
|
||||
- No external dependencies - single HTML file
|
||||
- Minimal JavaScript for expand/collapse functionality
|
||||
- Optimized hover effects for table rows
|
||||
|
||||
### Report Output
|
||||
|
||||
The HTML report is a standalone file that can be:
|
||||
- Opened directly in any web browser (Chrome, Firefox, Safari, Edge)
|
||||
- Shared via email or file transfer
|
||||
- Archived for compliance or historical comparison
|
||||
- Viewed without an internet connection or web server
|
||||
|
||||
Screenshot links in the report are relative paths, so keep the report and screenshot directory together.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
SneakyScanner/
|
||||
├── src/
|
||||
│ ├── scanner.py # Main scanner application
|
||||
│ └── screenshot_capture.py # Webpage screenshot capture module
|
||||
│ ├── screenshot_capture.py # Webpage screenshot capture module
|
||||
│ └── report_generator.py # HTML report generation module
|
||||
├── templates/
|
||||
│ ├── report_template.html # Jinja2 template for HTML reports
|
||||
│ └── report_mockup.html # Static mockup for design testing
|
||||
├── configs/
|
||||
│ └── example-site.yaml # Example configuration
|
||||
├── output/ # Scan results
|
||||
│ ├── scan_report_*.json # JSON reports with timestamps
|
||||
│ ├── scan_report_*.html # HTML reports (generated from JSON)
|
||||
│ └── scan_report_*_screenshots/ # Screenshot directories
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
@@ -298,12 +375,12 @@ Only use this tool on networks you own or have explicit authorization to scan. U
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **HTML Report Generation**: Build comprehensive HTML reports from JSON output with:
|
||||
- Service details and SSL/TLS information
|
||||
- Visual comparison of expected vs. actual results
|
||||
- Certificate expiration warnings
|
||||
- TLS version compliance reports
|
||||
- Embedded webpage screenshots
|
||||
- **Enhanced HTML Reports**:
|
||||
- Sortable/filterable service tables with JavaScript
|
||||
- Interactive charts and graphs for trends
|
||||
- Timeline view of scan history
|
||||
- Embedded screenshot thumbnails (currently links only)
|
||||
- Export to PDF capability
|
||||
- **Comparison Reports**: Generate diff reports showing changes between scans
|
||||
- **Email Notifications**: Alert on unexpected changes or certificate expirations
|
||||
- **Scheduled Scanning**: Automated periodic scans with cron integration
|
||||
|
||||
@@ -2,3 +2,4 @@ PyYAML==6.0.1
|
||||
python-libnmap==0.7.3
|
||||
sslyze==6.0.0
|
||||
playwright==1.40.0
|
||||
Jinja2==3.1.2
|
||||
|
||||
327
src/report_generator.py
Executable file
327
src/report_generator.py
Executable file
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTML Report Generator for SneakyScanner
|
||||
|
||||
Generates comprehensive HTML reports from JSON scan results with:
|
||||
- Summary dashboard (statistics, drift alerts, security warnings)
|
||||
- Site-by-site breakdown with service details
|
||||
- SSL/TLS certificate and cipher suite information
|
||||
- Visual badges for expected vs. unexpected services
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HTMLReportGenerator:
|
||||
"""Generates HTML reports from SneakyScanner JSON output."""
|
||||
|
||||
def __init__(self, json_report_path: str, template_dir: str = 'templates'):
|
||||
"""
|
||||
Initialize the HTML report generator.
|
||||
|
||||
Args:
|
||||
json_report_path: Path to the JSON scan report
|
||||
template_dir: Directory containing Jinja2 templates
|
||||
"""
|
||||
self.json_report_path = Path(json_report_path)
|
||||
self.template_dir = Path(template_dir)
|
||||
self.report_data = None
|
||||
|
||||
# Initialize Jinja2 environment
|
||||
self.jinja_env = Environment(
|
||||
loader=FileSystemLoader(self.template_dir),
|
||||
autoescape=select_autoescape(['html', 'xml'])
|
||||
)
|
||||
|
||||
# Register custom filters
|
||||
self.jinja_env.filters['format_date'] = self._format_date
|
||||
self.jinja_env.filters['format_duration'] = self._format_duration
|
||||
|
||||
def generate_report(self, output_path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Generate HTML report from JSON scan data.
|
||||
|
||||
Args:
|
||||
output_path: Path for output HTML file. If None, derives from JSON filename.
|
||||
|
||||
Returns:
|
||||
Path to generated HTML report
|
||||
"""
|
||||
logger.info(f"Loading JSON report from {self.json_report_path}")
|
||||
self._load_json_report()
|
||||
|
||||
logger.info("Calculating summary statistics")
|
||||
summary_stats = self._calculate_summary_stats()
|
||||
|
||||
logger.info("Identifying drift alerts")
|
||||
drift_alerts = self._identify_drift_alerts()
|
||||
|
||||
logger.info("Identifying security warnings")
|
||||
security_warnings = self._identify_security_warnings()
|
||||
|
||||
# Prepare template context
|
||||
context = {
|
||||
'title': self.report_data.get('title', 'SneakyScanner Report'),
|
||||
'scan_time': self.report_data.get('scan_time'),
|
||||
'scan_duration': self.report_data.get('scan_duration'),
|
||||
'config_file': self.report_data.get('config_file'),
|
||||
'sites': self.report_data.get('sites', []),
|
||||
'summary_stats': summary_stats,
|
||||
'drift_alerts': drift_alerts,
|
||||
'security_warnings': security_warnings,
|
||||
}
|
||||
|
||||
# Determine output path
|
||||
if output_path is None:
|
||||
output_path = self.json_report_path.with_suffix('.html')
|
||||
else:
|
||||
output_path = Path(output_path)
|
||||
|
||||
logger.info("Rendering HTML template")
|
||||
template = self.jinja_env.get_template('report_template.html')
|
||||
html_content = template.render(**context)
|
||||
|
||||
logger.info(f"Writing HTML report to {output_path}")
|
||||
output_path.write_text(html_content, encoding='utf-8')
|
||||
|
||||
logger.info(f"Successfully generated HTML report: {output_path}")
|
||||
return str(output_path)
|
||||
|
||||
def _load_json_report(self) -> None:
|
||||
"""Load and parse JSON scan report."""
|
||||
if not self.json_report_path.exists():
|
||||
raise FileNotFoundError(f"JSON report not found: {self.json_report_path}")
|
||||
|
||||
with open(self.json_report_path, 'r') as f:
|
||||
self.report_data = json.load(f)
|
||||
|
||||
def _calculate_summary_stats(self) -> Dict[str, int]:
|
||||
"""
|
||||
Calculate summary statistics for the dashboard.
|
||||
|
||||
Returns:
|
||||
Dictionary with stat counts
|
||||
"""
|
||||
stats = {
|
||||
'total_ips': 0,
|
||||
'tcp_ports': 0,
|
||||
'udp_ports': 0,
|
||||
'services': 0,
|
||||
'web_services': 0,
|
||||
'screenshots': 0,
|
||||
}
|
||||
|
||||
for site in self.report_data.get('sites', []):
|
||||
for ip_data in site.get('ips', []):
|
||||
stats['total_ips'] += 1
|
||||
|
||||
actual = ip_data.get('actual', {})
|
||||
stats['tcp_ports'] += len(actual.get('tcp_ports', []))
|
||||
stats['udp_ports'] += len(actual.get('udp_ports', []))
|
||||
|
||||
services = actual.get('services', [])
|
||||
stats['services'] += len(services)
|
||||
|
||||
# Count web services (HTTP/HTTPS)
|
||||
for service in services:
|
||||
if service.get('http_info'):
|
||||
stats['web_services'] += 1
|
||||
if service['http_info'].get('screenshot'):
|
||||
stats['screenshots'] += 1
|
||||
|
||||
return stats
|
||||
|
||||
def _identify_drift_alerts(self) -> Dict[str, int]:
|
||||
"""
|
||||
Identify infrastructure drift (unexpected/missing items).
|
||||
|
||||
Returns:
|
||||
Dictionary with drift alert counts
|
||||
"""
|
||||
alerts = {
|
||||
'unexpected_tcp': 0,
|
||||
'unexpected_udp': 0,
|
||||
'missing_tcp': 0,
|
||||
'missing_udp': 0,
|
||||
'new_services': 0,
|
||||
}
|
||||
|
||||
for site in self.report_data.get('sites', []):
|
||||
for ip_data in site.get('ips', []):
|
||||
expected = ip_data.get('expected', {})
|
||||
actual = ip_data.get('actual', {})
|
||||
|
||||
expected_tcp = set(expected.get('tcp_ports', []))
|
||||
actual_tcp = set(actual.get('tcp_ports', []))
|
||||
expected_udp = set(expected.get('udp_ports', []))
|
||||
actual_udp = set(actual.get('udp_ports', []))
|
||||
|
||||
# Count unexpected ports
|
||||
alerts['unexpected_tcp'] += len(actual_tcp - expected_tcp)
|
||||
alerts['unexpected_udp'] += len(actual_udp - expected_udp)
|
||||
|
||||
# Count missing ports
|
||||
alerts['missing_tcp'] += len(expected_tcp - actual_tcp)
|
||||
alerts['missing_udp'] += len(expected_udp - actual_udp)
|
||||
|
||||
# Count new services (any service on unexpected port)
|
||||
unexpected_ports = (actual_tcp - expected_tcp) | (actual_udp - expected_udp)
|
||||
for service in actual.get('services', []):
|
||||
if service.get('port') in unexpected_ports:
|
||||
alerts['new_services'] += 1
|
||||
|
||||
return alerts
|
||||
|
||||
def _identify_security_warnings(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Identify security issues (cert expiry, weak TLS, etc.).
|
||||
|
||||
Returns:
|
||||
Dictionary with security warning counts and details
|
||||
"""
|
||||
warnings = {
|
||||
'expiring_certs': 0,
|
||||
'weak_tls': 0,
|
||||
'self_signed': 0,
|
||||
'high_ports': 0,
|
||||
'expiring_cert_details': [], # List of IPs with expiring certs
|
||||
}
|
||||
|
||||
for site in self.report_data.get('sites', []):
|
||||
for ip_data in site.get('ips', []):
|
||||
actual = ip_data.get('actual', {})
|
||||
|
||||
for service in actual.get('services', []):
|
||||
port = service.get('port')
|
||||
|
||||
# Check for high ports (>10000)
|
||||
if port and port > 10000:
|
||||
warnings['high_ports'] += 1
|
||||
|
||||
# Check SSL/TLS if present
|
||||
http_info = service.get('http_info', {})
|
||||
ssl_tls = http_info.get('ssl_tls', {})
|
||||
|
||||
if ssl_tls:
|
||||
# Check certificate expiry
|
||||
cert = ssl_tls.get('certificate', {})
|
||||
days_until_expiry = cert.get('days_until_expiry')
|
||||
|
||||
if days_until_expiry is not None and days_until_expiry < 30:
|
||||
warnings['expiring_certs'] += 1
|
||||
warnings['expiring_cert_details'].append({
|
||||
'ip': ip_data.get('address'),
|
||||
'port': port,
|
||||
'days': days_until_expiry,
|
||||
'subject': cert.get('subject'),
|
||||
})
|
||||
|
||||
# Check for self-signed
|
||||
issuer = cert.get('issuer', '')
|
||||
subject = cert.get('subject', '')
|
||||
if issuer and subject and issuer == subject:
|
||||
warnings['self_signed'] += 1
|
||||
|
||||
# Check for weak TLS versions
|
||||
tls_versions = ssl_tls.get('tls_versions', {})
|
||||
if tls_versions.get('TLS 1.0', {}).get('supported'):
|
||||
warnings['weak_tls'] += 1
|
||||
elif tls_versions.get('TLS 1.1', {}).get('supported'):
|
||||
warnings['weak_tls'] += 1
|
||||
|
||||
return warnings
|
||||
|
||||
@staticmethod
|
||||
def _format_date(date_str: Optional[str]) -> str:
|
||||
"""
|
||||
Format ISO date string for display.
|
||||
|
||||
Args:
|
||||
date_str: ISO format date string
|
||||
|
||||
Returns:
|
||||
Formatted date string
|
||||
"""
|
||||
if not date_str:
|
||||
return 'N/A'
|
||||
|
||||
try:
|
||||
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
return dt.strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||
except (ValueError, AttributeError):
|
||||
return str(date_str)
|
||||
|
||||
@staticmethod
|
||||
def _format_duration(duration: Optional[float]) -> str:
|
||||
"""
|
||||
Format scan duration for display.
|
||||
|
||||
Args:
|
||||
duration: Duration in seconds
|
||||
|
||||
Returns:
|
||||
Formatted duration string
|
||||
"""
|
||||
if duration is None:
|
||||
return 'N/A'
|
||||
|
||||
if duration < 60:
|
||||
return f"{duration:.1f} seconds"
|
||||
elif duration < 3600:
|
||||
minutes = duration / 60
|
||||
return f"{minutes:.1f} minutes"
|
||||
else:
|
||||
hours = duration / 3600
|
||||
return f"{hours:.2f} hours"
|
||||
|
||||
|
||||
def main():
|
||||
"""Command-line entry point for standalone usage."""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python report_generator.py <json_report_path> [output_html_path]")
|
||||
print("\nExample:")
|
||||
print(" python report_generator.py output/scan_report_20251114_103000.json")
|
||||
print(" python report_generator.py output/scan_report.json custom_report.html")
|
||||
sys.exit(1)
|
||||
|
||||
json_path = sys.argv[1]
|
||||
output_path = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
try:
|
||||
# Determine template directory relative to script location
|
||||
script_dir = Path(__file__).parent.parent
|
||||
template_dir = script_dir / 'templates'
|
||||
|
||||
generator = HTMLReportGenerator(json_path, template_dir=str(template_dir))
|
||||
result_path = generator.generate_report(output_path)
|
||||
|
||||
print(f"\n✓ Successfully generated HTML report:")
|
||||
print(f" {result_path}")
|
||||
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"File not found: {e}")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Invalid JSON in report file: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating report: {e}", exc_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1424
templates/report_mockup.html
Normal file
1424
templates/report_mockup.html
Normal file
File diff suppressed because it is too large
Load Diff
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