mirror of
https://github.com/DeNNiiInc/UltyScan.git
synced 2026-04-17 18:26:00 +00:00
Create comprehensive report viewer with full file contents
This commit is contained in:
825
webui/report.php
825
webui/report.php
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* UltyScan Web Interface - Workspace Report Viewer
|
||||
* Generates a beautiful HTML report for a workspace
|
||||
* UltyScan Web Interface - Comprehensive Report Viewer
|
||||
* Compiles all workspace files into a single readable HTML report
|
||||
*/
|
||||
|
||||
$name = preg_replace('/[^a-zA-Z0-9\-\_\.]/', '', $_GET['name'] ?? '');
|
||||
@@ -15,105 +15,111 @@ if (!is_dir($workspaceDir)) {
|
||||
die('Workspace not found');
|
||||
}
|
||||
|
||||
// Check for existing sniper report
|
||||
$sniperReport = $workspaceDir . '/sniper-report.html';
|
||||
if (file_exists($sniperReport)) {
|
||||
// Redirect to the existing report
|
||||
header('Location: /loot/workspace/' . urlencode($name) . '/sniper-report.html');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Collect workspace data
|
||||
$data = [
|
||||
// Collect all data from workspace
|
||||
$report = [
|
||||
'name' => $name,
|
||||
'created' => date('Y-m-d H:i:s', filectime($workspaceDir)),
|
||||
'modified' => date('Y-m-d H:i:s', filemtime($workspaceDir)),
|
||||
'hosts' => [],
|
||||
'ports' => [],
|
||||
'vulnerabilities' => [],
|
||||
'screenshots' => [],
|
||||
'files' => []
|
||||
'sections' => []
|
||||
];
|
||||
|
||||
// Get all files in workspace
|
||||
function scanWorkspace($dir, $prefix = '')
|
||||
// Helper function to read file safely
|
||||
function readFileContent($path, $maxLines = 500)
|
||||
{
|
||||
if (!file_exists($path) || !is_readable($path)) return null;
|
||||
$content = file_get_contents($path);
|
||||
if ($content === false) return null;
|
||||
// Limit very long files
|
||||
$lines = explode("\n", $content);
|
||||
if (count($lines) > $maxLines) {
|
||||
$content = implode("\n", array_slice($lines, 0, $maxLines));
|
||||
$content .= "\n\n... [Truncated - " . (count($lines) - $maxLines) . " more lines]";
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Helper to determine file category
|
||||
function categorizeFile($filename)
|
||||
{
|
||||
$lower = strtolower($filename);
|
||||
if (strpos($lower, 'nmap') !== false) return 'Port Scans';
|
||||
if (strpos($lower, 'nuclei') !== false) return 'Vulnerability Findings';
|
||||
if (strpos($lower, 'nikto') !== false) return 'Web Server Analysis';
|
||||
if (strpos($lower, 'whatweb') !== false) return 'Technology Detection';
|
||||
if (strpos($lower, 'dns') !== false || strpos($lower, 'subdomain') !== false) return 'DNS & Subdomains';
|
||||
if (strpos($lower, 'whois') !== false) return 'WHOIS Information';
|
||||
if (strpos($lower, 'ssl') !== false || strpos($lower, 'cert') !== false) return 'SSL/TLS Analysis';
|
||||
if (strpos($lower, 'dir') !== false || strpos($lower, 'brute') !== false) return 'Directory Discovery';
|
||||
if (strpos($lower, 'host') !== false) return 'Host Information';
|
||||
if (strpos($lower, 'osint') !== false) return 'OSINT Data';
|
||||
if (strpos($lower, 'screenshot') !== false) return 'Screenshots';
|
||||
return 'Other Findings';
|
||||
}
|
||||
|
||||
// Recursively scan workspace
|
||||
function scanWorkspaceFiles($dir, $prefix = '')
|
||||
{
|
||||
$files = [];
|
||||
if (is_dir($dir)) {
|
||||
$items = scandir($dir);
|
||||
foreach ($items as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
$path = $dir . '/' . $item;
|
||||
if (is_dir($path)) {
|
||||
$files = array_merge($files, scanWorkspace($path, $prefix . $item . '/'));
|
||||
} else {
|
||||
$files[] = [
|
||||
'name' => $item,
|
||||
'path' => $prefix . $item,
|
||||
'size' => filesize($path),
|
||||
'modified' => filemtime($path)
|
||||
];
|
||||
}
|
||||
if (!is_dir($dir)) return $files;
|
||||
|
||||
$items = scandir($dir);
|
||||
foreach ($items as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
$path = $dir . '/' . $item;
|
||||
$relativePath = $prefix . $item;
|
||||
|
||||
if (is_dir($path)) {
|
||||
$files = array_merge($files, scanWorkspaceFiles($path, $relativePath . '/'));
|
||||
} else {
|
||||
$ext = strtolower(pathinfo($item, PATHINFO_EXTENSION));
|
||||
// Skip binary and image files for content reading
|
||||
$skipExtensions = ['png', 'jpg', 'jpeg', 'gif', 'pdf', 'zip', 'tar', 'gz', 'exe', 'bin'];
|
||||
|
||||
$files[] = [
|
||||
'path' => $relativePath,
|
||||
'fullPath' => $path,
|
||||
'name' => $item,
|
||||
'size' => filesize($path),
|
||||
'category' => categorizeFile($relativePath),
|
||||
'extension' => $ext,
|
||||
'isImage' => in_array($ext, ['png', 'jpg', 'jpeg', 'gif']),
|
||||
'isBinary' => in_array($ext, $skipExtensions)
|
||||
];
|
||||
}
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
$data['files'] = scanWorkspace($workspaceDir);
|
||||
$allFiles = scanWorkspaceFiles($workspaceDir);
|
||||
|
||||
// Parse hosts from nmap directory
|
||||
$nmapDir = $workspaceDir . '/nmap';
|
||||
if (is_dir($nmapDir)) {
|
||||
$nmapFiles = glob($nmapDir . '/*.nmap');
|
||||
foreach ($nmapFiles as $file) {
|
||||
$content = file_get_contents($file);
|
||||
// Extract host info
|
||||
if (preg_match('/Nmap scan report for (.+)/', $content, $matches)) {
|
||||
$host = trim($matches[1]);
|
||||
if (!in_array($host, $data['hosts'])) {
|
||||
$data['hosts'][] = $host;
|
||||
}
|
||||
}
|
||||
// Extract ports
|
||||
if (preg_match_all('/(\d+)\/(tcp|udp)\s+open\s+(\S+)/', $content, $matches, PREG_SET_ORDER)) {
|
||||
foreach ($matches as $match) {
|
||||
$data['ports'][] = [
|
||||
'port' => $match[1],
|
||||
'protocol' => $match[2],
|
||||
'service' => $match[3]
|
||||
];
|
||||
}
|
||||
}
|
||||
// Group files by category
|
||||
$categorized = [];
|
||||
foreach ($allFiles as $file) {
|
||||
$cat = $file['category'];
|
||||
if (!isset($categorized[$cat])) {
|
||||
$categorized[$cat] = [];
|
||||
}
|
||||
$categorized[$cat][] = $file;
|
||||
}
|
||||
|
||||
// Get screenshots
|
||||
$screenshotDir = $workspaceDir . '/screenshots';
|
||||
if (is_dir($screenshotDir)) {
|
||||
$screenshots = glob($screenshotDir . '/*.{png,jpg,jpeg,gif}', GLOB_BRACE);
|
||||
foreach ($screenshots as $ss) {
|
||||
$data['screenshots'][] = basename($ss);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse vulnerabilities from output files
|
||||
$outputDir = $workspaceDir . '/output';
|
||||
if (is_dir($outputDir)) {
|
||||
$vulnFiles = glob($outputDir . '/*nuclei*.txt');
|
||||
foreach ($vulnFiles as $file) {
|
||||
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos($line, '[') !== false && strpos($line, ']') !== false) {
|
||||
$data['vulnerabilities'][] = trim($line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unique ports
|
||||
$data['ports'] = array_unique($data['ports'], SORT_REGULAR);
|
||||
$data['vulnerabilities'] = array_unique($data['vulnerabilities']);
|
||||
// Priority order for sections
|
||||
$sectionOrder = [
|
||||
'Host Information',
|
||||
'Port Scans',
|
||||
'Vulnerability Findings',
|
||||
'Web Server Analysis',
|
||||
'Technology Detection',
|
||||
'SSL/TLS Analysis',
|
||||
'DNS & Subdomains',
|
||||
'Directory Discovery',
|
||||
'WHOIS Information',
|
||||
'OSINT Data',
|
||||
'Screenshots',
|
||||
'Other Findings'
|
||||
];
|
||||
|
||||
// Generate unique ID for TOC
|
||||
$sectionId = 0;
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -122,20 +128,24 @@ $data['vulnerabilities'] = array_unique($data['vulnerabilities']);
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UltyScan Report - <?php echo htmlspecialchars($name); ?></title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-primary: #0a0e17;
|
||||
--bg-secondary: #111827;
|
||||
--bg-card: rgba(17, 24, 39, 0.9);
|
||||
--bg-primary: #0f1419;
|
||||
--bg-secondary: #1a1f2e;
|
||||
--bg-tertiary: #242938;
|
||||
--bg-code: #0d1117;
|
||||
--accent-primary: #3b82f6;
|
||||
--accent-secondary: #8b5cf6;
|
||||
--accent-success: #10b981;
|
||||
--accent-warning: #f59e0b;
|
||||
--accent-danger: #ef4444;
|
||||
--text-primary: #f3f4f6;
|
||||
--text-secondary: #9ca3af;
|
||||
--border-color: rgba(75, 85, 99, 0.4);
|
||||
--accent-info: #06b6d4;
|
||||
--text-primary: #e6edf3;
|
||||
--text-secondary: #8b949e;
|
||||
--text-muted: #6e7681;
|
||||
--border-color: #30363d;
|
||||
--border-accent: #388bfd;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -145,345 +155,516 @@ $data['vulnerabilities'] = array_unique($data['vulnerabilities']);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
background-image:
|
||||
radial-gradient(ellipse at 20% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 50%, rgba(139, 92, 246, 0.08) 0%, transparent 50%);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding: 2rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
/* Sidebar / Table of Contents */
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
.sidebar-header {
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-header h1 {
|
||||
font-size: 1.25rem;
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.sidebar-header .meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.toc-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.toc-title {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toc-link {
|
||||
display: block;
|
||||
padding: 0.4rem 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.15s;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.toc-link:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toc-link .count {
|
||||
float: right;
|
||||
font-size: 0.75rem;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
padding: 2rem 3rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.report-header {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.report-header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.header .meta {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.stats {
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card .number {
|
||||
font-size: 2.5rem;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-success));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.stat-card .label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.section {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 2.5rem;
|
||||
scroll-margin-top: 1rem;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 1.25rem;
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.section h2::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--bg-secondary);
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-info {
|
||||
.section-icon.port {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.tag-warning {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.tag-danger {
|
||||
.section-icon.vuln {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.tag-success {
|
||||
.section-icon.web {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.vuln-item {
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
.section-icon.dns {
|
||||
background: rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
.section-icon.ssl {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
}
|
||||
|
||||
.section-icon.other {
|
||||
background: rgba(107, 114, 128, 0.2);
|
||||
}
|
||||
|
||||
/* File Cards */
|
||||
.file-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
word-break: break-all;
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
.file-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 1.5rem;
|
||||
.file-header:hover {
|
||||
background: #2d3548;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--accent-info);
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: var(--text-secondary);
|
||||
.file-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.file-content {
|
||||
padding: 1rem;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-content pre {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Syntax highlighting for common patterns */
|
||||
.highlight-ip {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.highlight-port {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.highlight-vuln {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
.highlight-critical {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.highlight-success {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.highlight-info {
|
||||
color: #a371f7;
|
||||
}
|
||||
|
||||
/* Image section */
|
||||
.screenshot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.screenshot-item {
|
||||
.screenshot-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.screenshot-item img {
|
||||
.screenshot-card img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.screenshot-item .caption {
|
||||
padding: 0.5rem;
|
||||
.screenshot-card img:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.screenshot-caption {
|
||||
padding: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
/* Empty state */
|
||||
.empty-section {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.section {
|
||||
border: 1px solid #ddd;
|
||||
.file-card {
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapsible */
|
||||
.collapsed .file-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.collapsed .toggle-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>🔍 <?php echo htmlspecialchars($name); ?></h1>
|
||||
<p class="meta">
|
||||
Created: <?php echo $data['created']; ?> |
|
||||
Modified: <?php echo $data['modified']; ?>
|
||||
</p>
|
||||
</header>
|
||||
<!-- Sidebar / Table of Contents -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>🔍 UltyScan Report</h1>
|
||||
<div class="meta"><?php echo htmlspecialchars($name); ?></div>
|
||||
<div class="meta"><?php echo $report['modified']; ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="number"><?php echo count($data['hosts']); ?></div>
|
||||
<div class="label">Hosts Discovered</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number"><?php echo count($data['ports']); ?></div>
|
||||
<div class="label">Open Ports</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number"><?php echo count($data['vulnerabilities']); ?></div>
|
||||
<div class="label">Findings</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number"><?php echo count($data['files']); ?></div>
|
||||
<div class="label">Files Generated</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($data['hosts'])): ?>
|
||||
<div class="section">
|
||||
<h2>Discovered Hosts</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Host</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($data['hosts'] as $host): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($host); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['ports'])): ?>
|
||||
<div class="section">
|
||||
<h2>Open Ports</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Port</th>
|
||||
<th>Protocol</th>
|
||||
<th>Service</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($data['ports'] as $port): ?>
|
||||
<tr>
|
||||
<td><span class="tag tag-info"><?php echo htmlspecialchars($port['port']); ?></span></td>
|
||||
<td><?php echo htmlspecialchars($port['protocol']); ?></td>
|
||||
<td><?php echo htmlspecialchars($port['service']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['vulnerabilities'])): ?>
|
||||
<div class="section">
|
||||
<h2>Vulnerability Findings</h2>
|
||||
<?php foreach ($data['vulnerabilities'] as $vuln): ?>
|
||||
<div class="vuln-item"><?php echo htmlspecialchars($vuln); ?></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($data['screenshots'])): ?>
|
||||
<div class="section">
|
||||
<h2>Screenshots</h2>
|
||||
<div class="screenshot-grid">
|
||||
<?php foreach ($data['screenshots'] as $ss): ?>
|
||||
<div class="screenshot-item">
|
||||
<img src="/loot/workspace/<?php echo urlencode($name); ?>/screenshots/<?php echo urlencode($ss); ?>" alt="<?php echo htmlspecialchars($ss); ?>">
|
||||
<div class="caption"><?php echo htmlspecialchars($ss); ?></div>
|
||||
</div>
|
||||
<nav class="toc">
|
||||
<div class="toc-section">
|
||||
<div class="toc-title">Sections</div>
|
||||
<?php foreach ($sectionOrder as $section): ?>
|
||||
<?php if (isset($categorized[$section])): ?>
|
||||
<a href="#section-<?php echo $sectionId++; ?>" class="toc-link">
|
||||
<?php echo $section; ?>
|
||||
<span class="count"><?php echo count($categorized[$section]); ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="section">
|
||||
<h2>Generated Files</h2>
|
||||
<?php if (empty($data['files'])): ?>
|
||||
<div class="empty-state">No files found in this workspace.</div>
|
||||
<?php else: ?>
|
||||
<div class="file-list">
|
||||
<?php foreach ($data['files'] as $file): ?>
|
||||
<div class="file-item">
|
||||
<span class="file-icon">📄</span>
|
||||
<span class="file-name"><?php echo htmlspecialchars($file['path']); ?></span>
|
||||
<span class="file-size"><?php echo number_format($file['size'] / 1024, 1); ?> KB</span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<header class="report-header">
|
||||
<h1><?php echo htmlspecialchars($name); ?></h1>
|
||||
<p style="color: var(--text-secondary);">
|
||||
Generated: <?php echo $report['created']; ?> |
|
||||
Last Modified: <?php echo $report['modified']; ?>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="number"><?php echo count($allFiles); ?></div>
|
||||
<div class="label">Total Files</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number"><?php echo count($categorized); ?></div>
|
||||
<div class="label">Categories</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="number"><?php
|
||||
$total = 0;
|
||||
foreach ($allFiles as $f) $total += $f['size'];
|
||||
echo number_format($total / 1024, 1);
|
||||
?> KB</div>
|
||||
<div class="label">Total Size</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" onclick="expandAll()">Expand All</button>
|
||||
<button class="btn btn-secondary" onclick="collapseAll()">Collapse All</button>
|
||||
<button class="btn btn-secondary" onclick="window.print()">Print Report</button>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$sectionId = 0;
|
||||
foreach ($sectionOrder as $section):
|
||||
if (!isset($categorized[$section])) continue;
|
||||
$files = $categorized[$section];
|
||||
$iconClass = 'other';
|
||||
if (strpos($section, 'Port') !== false) $iconClass = 'port';
|
||||
elseif (strpos($section, 'Vuln') !== false) $iconClass = 'vuln';
|
||||
elseif (strpos($section, 'Web') !== false) $iconClass = 'web';
|
||||
elseif (strpos($section, 'DNS') !== false) $iconClass = 'dns';
|
||||
elseif (strpos($section, 'SSL') !== false) $iconClass = 'ssl';
|
||||
?>
|
||||
<section class="section" id="section-<?php echo $sectionId++; ?>">
|
||||
<div class="section-header">
|
||||
<span class="section-icon <?php echo $iconClass; ?>">
|
||||
<?php
|
||||
echo match ($iconClass) {
|
||||
'port' => '🔌',
|
||||
'vuln' => '⚠️',
|
||||
'web' => '🌐',
|
||||
'dns' => '🔍',
|
||||
'ssl' => '🔒',
|
||||
default => '📄'
|
||||
};
|
||||
?>
|
||||
</span>
|
||||
<h2><?php echo $section; ?></h2>
|
||||
</div>
|
||||
|
||||
<?php if ($section === 'Screenshots'): ?>
|
||||
<div class="screenshot-grid">
|
||||
<?php foreach ($files as $file): ?>
|
||||
<?php if ($file['isImage']): ?>
|
||||
<div class="screenshot-card">
|
||||
<img src="/loot/workspace/<?php echo urlencode($name); ?>/<?php echo $file['path']; ?>"
|
||||
alt="<?php echo htmlspecialchars($file['name']); ?>"
|
||||
onclick="window.open(this.src, '_blank')">
|
||||
<div class="screenshot-caption"><?php echo htmlspecialchars($file['name']); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($files as $file): ?>
|
||||
<?php if ($file['isBinary']): continue;
|
||||
endif; ?>
|
||||
<?php $content = readFileContent($file['fullPath']); ?>
|
||||
<?php if ($content === null || trim($content) === ''): continue;
|
||||
endif; ?>
|
||||
|
||||
<div class="file-card">
|
||||
<div class="file-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
||||
<div>
|
||||
<span class="file-name"><?php echo htmlspecialchars($file['path']); ?></span>
|
||||
</div>
|
||||
<div class="file-meta">
|
||||
<?php echo number_format($file['size'] / 1024, 1); ?> KB
|
||||
<span class="toggle-icon">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-content">
|
||||
<pre><?php echo htmlspecialchars($content); ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function expandAll() {
|
||||
document.querySelectorAll('.file-card').forEach(card => {
|
||||
card.classList.remove('collapsed');
|
||||
});
|
||||
}
|
||||
|
||||
function collapseAll() {
|
||||
document.querySelectorAll('.file-card').forEach(card => {
|
||||
card.classList.add('collapsed');
|
||||
});
|
||||
}
|
||||
|
||||
// Start with files collapsed for faster loading
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
collapseAll();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user