mirror of
https://github.com/DeNNiiInc/UltyScan.git
synced 2026-04-17 16:16:00 +00:00
700 lines
21 KiB
PHP
700 lines
21 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">
|
|
<link rel="icon" type="image/svg+xml" href="assets/Logo.svg">
|
|
<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 style="display: flex; align-items: center; gap: 0.5rem;">
|
|
<img src="assets/Logo.svg" alt="UltyScan Logo" style="height: 1.25em;">
|
|
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>
|