Files
UltyScan/webui/report.php

670 lines
20 KiB
PHP

<?php
/**
* 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'] ?? '');
if (empty($name)) {
die('Invalid workspace name');
}
$workspaceDir = '/usr/share/sniper/loot/workspace/' . $name;
if (!is_dir($workspaceDir)) {
die('Workspace not found');
}
// 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)),
'sections' => []
];
// 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)) 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;
}
$allFiles = scanWorkspaceFiles($workspaceDir);
// Group files by category
$categorized = [];
foreach ($allFiles as $file) {
$cat = $file['category'];
if (!isset($categorized[$cat])) {
$categorized[$cat] = [];
}
$categorized[$cat][] = $file;
}
// 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">
<head>
<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&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--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;
--accent-info: #06b6d4;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--border-color: #30363d;
--border-accent: #388bfd;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
}
.container {
max-width: 1600px;
margin: 0 auto;
display: grid;
grid-template-columns: 280px 1fr;
min-height: 100vh;
}
/* 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;
}
.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;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
text-align: center;
}
.stat-card .number {
font-size: 1.75rem;
font-weight: 700;
color: var(--accent-primary);
}
.stat-card .label {
font-size: 0.8rem;
color: var(--text-secondary);
}
/* Sections */
.section {
margin-bottom: 2.5rem;
scroll-margin-top: 1rem;
}
.section-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
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;
justify-content: center;
font-size: 0.9rem;
}
.section-icon.port {
background: rgba(59, 130, 246, 0.2);
}
.section-icon.vuln {
background: rgba(239, 68, 68, 0.2);
}
.section-icon.web {
background: rgba(16, 185, 129, 0.2);
}
.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;
margin-bottom: 1rem;
overflow: hidden;
}
.file-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
cursor: pointer;
}
.file-header:hover {
background: #2d3548;
}
.file-name {
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
color: var(--accent-info);
}
.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(300px, 1fr));
gap: 1rem;
}
.screenshot-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}
.screenshot-card img {
width: 100%;
height: 200px;
object-fit: cover;
cursor: pointer;
transition: transform 0.2s;
}
.screenshot-card img:hover {
transform: scale(1.02);
}
.screenshot-caption {
padding: 0.75rem;
font-size: 0.8rem;
color: var(--text-secondary);
background: var(--bg-tertiary);
}
/* Empty state */
.empty-section {
text-align: center;
padding: 2rem;
color: var(--text-muted);
font-style: italic;
}
/* Print styles */
@media print {
.sidebar {
display: none;
}
.container {
display: block;
}
body {
background: white;
color: black;
}
.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">
<!-- 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>
<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>
</nav>
</aside>
<!-- 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>
<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>