diff --git a/webui/assets/script.js b/webui/assets/script.js new file mode 100644 index 0000000..2b6ea22 --- /dev/null +++ b/webui/assets/script.js @@ -0,0 +1,307 @@ +// UltyScan Web Interface - Frontend Logic + +document.addEventListener("DOMContentLoaded", function () { + initTabs(); + initModeHandler(); + loadWorkspaces(); + checkScanStatus(); +}); + +// Tab Navigation +function initTabs() { + const tabs = document.querySelectorAll(".tab"); + tabs.forEach((tab) => { + tab.addEventListener("click", () => { + tabs.forEach((t) => t.classList.remove("active")); + document + .querySelectorAll(".tab-content") + .forEach((c) => c.classList.remove("active")); + + tab.classList.add("active"); + document.getElementById(tab.dataset.tab).classList.add("active"); + }); + }); +} + +// Dynamic form fields based on mode +function initModeHandler() { + const modeSelect = document.getElementById("mode"); + const portGroup = document.getElementById("port-group"); + const targetFileGroup = document.getElementById("target-file-group"); + const singleTargetGroup = document.getElementById("single-target-group"); + + if (!modeSelect) return; + + modeSelect.addEventListener("change", function () { + const mode = this.value; + + // Modes that require a file + const fileModes = [ + "airstrike", + "nuke", + "massportscan", + "massweb", + "masswebscan", + "massvulnscan", + "flyover", + ]; + // Modes that require a port + const portModes = ["port", "webporthttp", "webporthttps"]; + + if (fileModes.includes(mode)) { + targetFileGroup.style.display = "block"; + singleTargetGroup.querySelector("input").required = false; + } else { + targetFileGroup.style.display = "none"; + singleTargetGroup.querySelector("input").required = true; + } + + if (portModes.includes(mode)) { + portGroup.style.display = "block"; + portGroup.querySelector("input").required = true; + } else { + portGroup.style.display = "none"; + portGroup.querySelector("input").required = false; + } + }); +} + +// Submit Scan Form +async function submitScan(event) { + event.preventDefault(); + + const form = event.target; + const formData = new FormData(form); + const submitBtn = form.querySelector('button[type="submit"]'); + const originalText = submitBtn.innerHTML; + + submitBtn.innerHTML = ' Starting...'; + submitBtn.disabled = true; + + try { + const response = await fetch("execute.php", { + method: "POST", + body: formData, + }); + + const result = await response.json(); + + if (result.success) { + showNotification("Scan started successfully!", "success"); + updateConsole(result.command); + startStatusPolling(); + } else { + showNotification("Error: " + result.error, "error"); + } + } catch (error) { + showNotification("Failed to start scan: " + error.message, "error"); + } finally { + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + } +} + +// Load Workspaces +async function loadWorkspaces() { + const container = document.getElementById("workspace-list"); + if (!container) return; + + container.innerHTML = + '

Loading workspaces...

'; + + try { + const response = await fetch("workspaces.php?action=list"); + const result = await response.json(); + + if (result.workspaces && result.workspaces.length > 0) { + container.innerHTML = result.workspaces + .map( + (ws) => ` +
+ ${escapeHtml(ws)} +
+ + + +
+
+ ` + ) + .join(""); + } else { + container.innerHTML = + '

No workspaces found. Run a scan to create one.

'; + } + } catch (error) { + container.innerHTML = + '

Failed to load workspaces.

'; + } +} + +// Workspace Actions +async function viewWorkspace(name) { + try { + const response = await fetch( + `workspaces.php?action=view&name=${encodeURIComponent(name)}` + ); + const result = await response.json(); + + if (result.reportPath) { + window.open(result.reportPath, "_blank"); + } else { + showNotification("No report found for this workspace.", "warning"); + } + } catch (error) { + showNotification("Failed to open workspace.", "error"); + } +} + +async function exportWorkspace(name) { + showNotification("Exporting workspace: " + name, "info"); + try { + const response = await fetch("workspaces.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "export", name: name }), + }); + const result = await response.json(); + + if (result.success) { + showNotification("Workspace exported: " + result.path, "success"); + } else { + showNotification("Export failed: " + result.error, "error"); + } + } catch (error) { + showNotification("Export failed.", "error"); + } +} + +async function deleteWorkspace(name) { + if (!confirm(`Are you sure you want to delete workspace "${name}"?`)) return; + + try { + const response = await fetch("workspaces.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "delete", name: name }), + }); + const result = await response.json(); + + if (result.success) { + showNotification("Workspace deleted.", "success"); + loadWorkspaces(); + } else { + showNotification("Delete failed: " + result.error, "error"); + } + } catch (error) { + showNotification("Delete failed.", "error"); + } +} + +// Check Scan Status +let statusInterval = null; + +async function checkScanStatus() { + const statusBadge = document.getElementById("scan-status"); + if (!statusBadge) return; + + try { + const response = await fetch("status.php"); + const result = await response.json(); + + if (result.running) { + statusBadge.className = "status-badge status-running"; + statusBadge.innerHTML = ' Scan Running'; + startStatusPolling(); + } else { + statusBadge.className = "status-badge status-idle"; + statusBadge.textContent = "Idle"; + stopStatusPolling(); + } + } catch (error) { + console.error("Status check failed:", error); + } +} + +function startStatusPolling() { + if (statusInterval) return; + statusInterval = setInterval(checkScanStatus, 5000); +} + +function stopStatusPolling() { + if (statusInterval) { + clearInterval(statusInterval); + statusInterval = null; + } +} + +// Console Output +function updateConsole(text) { + const console = document.getElementById("console-output"); + if (console) { + console.textContent += "\n$ " + text; + console.scrollTop = console.scrollHeight; + } +} + +// Notifications +function showNotification(message, type = "info") { + // Create notification element + const notification = document.createElement("div"); + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 1rem 1.5rem; + border-radius: 8px; + color: white; + font-weight: 500; + z-index: 9999; + animation: slideIn 0.3s ease; + max-width: 400px; + `; + + const colors = { + success: "var(--accent-success)", + error: "var(--accent-danger)", + warning: "var(--accent-warning)", + info: "var(--accent-primary)", + }; + + notification.style.background = colors[type] || colors.info; + notification.textContent = message; + + document.body.appendChild(notification); + + setTimeout(() => { + notification.style.animation = "slideOut 0.3s ease forwards"; + setTimeout(() => notification.remove(), 300); + }, 4000); +} + +// Utility +function escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} + +// Add animation keyframes +const style = document.createElement("style"); +style.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + @keyframes slideOut { + from { transform: translateX(0); opacity: 1; } + to { transform: translateX(100%); opacity: 0; } + } +`; +document.head.appendChild(style); diff --git a/webui/assets/style.css b/webui/assets/style.css new file mode 100644 index 0000000..0b9c932 --- /dev/null +++ b/webui/assets/style.css @@ -0,0 +1,382 @@ +/* UltyScan Web Interface - Modern Dark Theme */ + +:root { + --bg-primary: #0a0e17; + --bg-secondary: #111827; + --bg-card: rgba(17, 24, 39, 0.8); + --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); + --glass-bg: rgba(17, 24, 39, 0.7); + --glass-border: rgba(255, 255, 255, 0.1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Inter", "Segoe UI", sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + background-image: radial-gradient( + ellipse at 20% 50%, + rgba(59, 130, 246, 0.15) 0%, + transparent 50% + ), + radial-gradient( + ellipse at 80% 50%, + rgba(139, 92, 246, 0.1) 0%, + transparent 50% + ); +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +/* Header */ +.header { + text-align: center; + margin-bottom: 3rem; + padding: 2rem; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: 16px; + backdrop-filter: blur(10px); +} + +.header h1 { + font-size: 2.5rem; + font-weight: 700; + background: linear-gradient( + 135deg, + var(--accent-primary), + var(--accent-secondary) + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.header .subtitle { + color: var(--text-secondary); + margin-top: 0.5rem; +} + +/* Cards */ +.card { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: 16px; + padding: 1.5rem; + margin-bottom: 1.5rem; + backdrop-filter: blur(10px); +} + +.card-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.card-header h2 { + font-size: 1.25rem; + font-weight: 600; +} + +.card-header .icon { + width: 24px; + height: 24px; + color: var(--accent-primary); +} + +/* Form Elements */ +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.25rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-group.full-width { + grid-column: 1 / -1; +} + +label { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); +} + +input[type="text"], +input[type="number"], +select, +textarea { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 0.75rem 1rem; + color: var(--text-primary); + font-size: 0.95rem; + transition: all 0.2s ease; +} + +input[type="text"]:focus, +input[type="number"]:focus, +select:focus, +textarea:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); +} + +select { + cursor: pointer; +} + +/* Checkboxes */ +.checkbox-grid { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.checkbox-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.checkbox-item:hover { + border-color: var(--accent-primary); +} + +.checkbox-item input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--accent-primary); +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 8px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-primary { + background: linear-gradient( + 135deg, + var(--accent-primary), + var(--accent-secondary) + ); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); +} + +.btn-secondary { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover { + border-color: var(--accent-primary); +} + +.btn-danger { + background: var(--accent-danger); + color: white; +} + +.btn-success { + background: var(--accent-success); + color: white; +} + +.btn-group { + display: flex; + gap: 1rem; + flex-wrap: wrap; + margin-top: 1.5rem; +} + +/* Status Badge */ +.status-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.875rem; + font-weight: 500; +} + +.status-idle { + background: rgba(107, 114, 128, 0.2); + color: #9ca3af; +} + +.status-running { + background: rgba(59, 130, 246, 0.2); + color: #60a5fa; +} + +.status-complete { + background: rgba(16, 185, 129, 0.2); + color: #34d399; +} + +/* Output Console */ +.console { + background: #000; + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1rem; + font-family: "Fira Code", "Consolas", monospace; + font-size: 0.85rem; + color: #10b981; + max-height: 400px; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-all; +} + +/* Workspace List */ +.workspace-list { + display: grid; + gap: 0.75rem; +} + +.workspace-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; +} + +.workspace-item:hover { + border-color: var(--accent-primary); +} + +.workspace-name { + font-weight: 500; +} + +.workspace-actions { + display: flex; + gap: 0.5rem; +} + +.workspace-actions .btn { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .header h1 { + font-size: 1.75rem; + } + + .form-grid { + grid-template-columns: 1fr; + } +} + +/* Loading Spinner */ +.spinner { + width: 20px; + height: 20px; + border: 2px solid transparent; + border-top-color: currentColor; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Tabs */ +.tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 1.5rem; + border-bottom: 1px solid var(--border-color); + padding-bottom: 1rem; +} + +.tab { + padding: 0.75rem 1.5rem; + background: transparent; + border: 1px solid transparent; + border-radius: 8px; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s ease; +} + +.tab:hover { + color: var(--text-primary); +} + +.tab.active { + background: var(--bg-secondary); + border-color: var(--accent-primary); + color: var(--accent-primary); +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} diff --git a/webui/execute.php b/webui/execute.php new file mode 100644 index 0000000..22dd6cf --- /dev/null +++ b/webui/execute.php @@ -0,0 +1,151 @@ + false, 'error' => 'Method not allowed']); + exit; +} + +// Configuration +define('SNIPER_PATH', '/usr/share/sniper/sniper'); +define('LOG_DIR', '/var/log/ultyscan'); + +// Ensure log directory exists +if (!is_dir(LOG_DIR)) { + mkdir(LOG_DIR, 0755, true); +} + +// Handle special actions +$action = $_POST['action'] ?? ''; + +if ($action === 'update') { + $cmd = SNIPER_PATH . ' -u 2>&1'; + $logFile = LOG_DIR . '/update_' . date('Ymd_His') . '.log'; + exec("nohup $cmd > $logFile 2>&1 &"); + echo json_encode(['success' => true, 'message' => 'Update started', 'log' => $logFile]); + exit; +} + +if ($action === 'stop') { + exec('pkill -f "sniper"'); + echo json_encode(['success' => true, 'message' => 'Stop signal sent']); + exit; +} + +// Build the sniper command +$command = SNIPER_PATH; +$errors = []; + +// Target (required unless using file) +$target = trim($_POST['target'] ?? ''); +$targetFile = trim($_POST['target_file'] ?? ''); +$mode = trim($_POST['mode'] ?? 'normal'); + +// Validate mode against allowed list +$allowedModes = [ + 'normal', + 'stealth', + 'web', + 'webscan', + 'webporthttp', + 'webporthttps', + 'port', + 'fullportonly', + 'discover', + 'flyover', + 'airstrike', + 'nuke', + 'massportscan', + 'massweb', + 'masswebscan', + 'massvulnscan' +]; + +if (!in_array($mode, $allowedModes)) { + echo json_encode(['success' => false, 'error' => 'Invalid scan mode']); + exit; +} + +// Modes that require a file instead of single target +$fileModes = ['airstrike', 'nuke', 'massportscan', 'massweb', 'masswebscan', 'massvulnscan', 'flyover']; + +if (in_array($mode, $fileModes)) { + if (empty($targetFile)) { + echo json_encode(['success' => false, 'error' => 'This mode requires a target file']); + exit; + } + // Write targets to temp file + $tmpFile = '/tmp/ultyscan_targets_' . uniqid() . '.txt'; + file_put_contents($tmpFile, $targetFile); + $command .= ' -f ' . escapeshellarg($tmpFile); +} else { + if (empty($target)) { + echo json_encode(['success' => false, 'error' => 'Target is required']); + exit; + } + // Sanitize target + $target = preg_replace('/[^a-zA-Z0-9\.\-\_\/\:]/', '', $target); + $command .= ' -t ' . escapeshellarg($target); +} + +// Mode +$command .= ' -m ' . escapeshellarg($mode); + +// Workspace +$workspace = trim($_POST['workspace'] ?? ''); +if (!empty($workspace)) { + $workspace = preg_replace('/[^a-zA-Z0-9\-\_]/', '', $workspace); + $command .= ' -w ' . escapeshellarg($workspace); +} + +// Port (for port modes) +$port = intval($_POST['port'] ?? 0); +if ($port > 0 && $port <= 65535) { + $command .= ' -p ' . $port; +} + +// Options +if (!empty($_POST['osint'])) { + $command .= ' -o'; +} + +if (!empty($_POST['recon'])) { + $command .= ' -re'; +} + +if (!empty($_POST['bruteforce'])) { + $command .= ' -b'; +} + +if (!empty($_POST['fullportscan'])) { + $command .= ' -fp'; +} + +// Create log file for this scan +$scanId = date('Ymd_His') . '_' . substr(md5(uniqid()), 0, 6); +$logFile = LOG_DIR . '/scan_' . $scanId . '.log'; + +// Run the command in background +$fullCommand = "nohup $command > $logFile 2>&1 &"; + +// Log the command (for debugging) +file_put_contents(LOG_DIR . '/commands.log', date('Y-m-d H:i:s') . " - $command\n", FILE_APPEND); + +// Execute +exec($fullCommand); + +echo json_encode([ + 'success' => true, + 'scanId' => $scanId, + 'command' => $command, + 'logFile' => $logFile, + 'message' => 'Scan started' +]); diff --git a/webui/index.php b/webui/index.php new file mode 100644 index 0000000..fd195fb --- /dev/null +++ b/webui/index.php @@ -0,0 +1,239 @@ + + + + + + UltyScan - Web Interface + + + + +
+ +
+

🔍 UltyScan

+

Attack Surface Management Platform

+
+ Idle +
+
+ + +
+ + + + +
+ + +
+
+
+ + + +

Configure Scan

+
+ +
+
+ +
+ + +
+ + + + + +
+ + +
+ + +
+ + +
+ + + +
+ + +
+ +
+ + + + +
+
+ +
+ + +
+
+
+
+ + +
+
+
+ + + +

Workspaces

+
+ +
+ +
+ +
+

Loading workspaces...

+
+
+
+ + +
+
+
+ + + +

Console Output

+
+ +
UltyScan Web Interface v1.0 +Ready to scan...
+ +
+ + +
+
+
+ + +
+
+
+ + + + +

System Actions

+
+ +
+ + +
+ +
+

Scanner Info

+

+ Install Directory: /usr/share/sniper
+ Loot Directory: /usr/share/sniper/loot/workspace +

+
+
+
+
+ + + + + diff --git a/webui/status.php b/webui/status.php new file mode 100644 index 0000000..c96b332 --- /dev/null +++ b/webui/status.php @@ -0,0 +1,45 @@ +/dev/null'); +$running = !empty(trim($output)); + +// Get list of running scans +$processes = []; +if ($running) { + $psOutput = shell_exec('ps aux | grep "sniper" | grep -v grep 2>/dev/null'); + if (!empty($psOutput)) { + $lines = explode("\n", trim($psOutput)); + foreach ($lines as $line) { + if (!empty($line)) { + $processes[] = $line; + } + } + } +} + +// Get recent log files +$logs = []; +$logDir = '/var/log/ultyscan'; +if (is_dir($logDir)) { + $files = glob($logDir . '/scan_*.log'); + usort($files, function ($a, $b) { + return filemtime($b) - filemtime($a); + }); + $logs = array_slice($files, 0, 5); // Last 5 logs +} + +echo json_encode([ + 'running' => $running, + 'processCount' => count($processes), + 'processes' => $processes, + 'recentLogs' => $logs, + 'timestamp' => date('Y-m-d H:i:s') +]); diff --git a/webui/workspaces.php b/webui/workspaces.php new file mode 100644 index 0000000..aa47d82 --- /dev/null +++ b/webui/workspaces.php @@ -0,0 +1,79 @@ + $workspaces]); + exit; + } + + if ($action === 'view') { + $name = preg_replace('/[^a-zA-Z0-9\-\_\.]/', '', $_GET['name'] ?? ''); + if (empty($name)) { + echo json_encode(['error' => 'Invalid workspace name']); + exit; + } + + $reportPath = WORKSPACE_DIR . '/' . $name . '/sniper-report.html'; + if (file_exists($reportPath)) { + // Return relative web path (assuming workspace is web-accessible) + echo json_encode(['reportPath' => '/loot/workspace/' . $name . '/sniper-report.html']); + } else { + echo json_encode(['reportPath' => null, 'message' => 'No report found']); + } + exit; + } +} + +// Handle POST requests (delete, export) +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $data = json_decode(file_get_contents('php://input'), true); + $action = $data['action'] ?? ''; + $name = preg_replace('/[^a-zA-Z0-9\-\_\.]/', '', $data['name'] ?? ''); + + if (empty($name)) { + echo json_encode(['success' => false, 'error' => 'Invalid workspace name']); + exit; + } + + if ($action === 'delete') { + $cmd = SNIPER_PATH . ' -w ' . escapeshellarg($name) . ' -d 2>&1'; + // Auto-confirm the deletion + $output = shell_exec("echo 'y' | $cmd"); + echo json_encode(['success' => true, 'output' => $output]); + exit; + } + + if ($action === 'export') { + $cmd = SNIPER_PATH . ' -w ' . escapeshellarg($name) . ' --export 2>&1'; + $output = shell_exec($cmd); + echo json_encode([ + 'success' => true, + 'path' => '/usr/share/sniper/loot/' . $name . '.tar', + 'output' => $output + ]); + exit; + } +} + +echo json_encode(['error' => 'Invalid request']);