Add Web Interface for UltyScan

This commit is contained in:
2026-01-01 17:16:12 +11:00
parent f046dee832
commit 7e2fc7edce
6 changed files with 1203 additions and 0 deletions

307
webui/assets/script.js Normal file
View File

@@ -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 = '<span class="spinner"></span> 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 =
'<p style="color: var(--text-secondary);">Loading workspaces...</p>';
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) => `
<div class="workspace-item">
<span class="workspace-name">${escapeHtml(ws)}</span>
<div class="workspace-actions">
<button class="btn btn-secondary" onclick="viewWorkspace('${escapeHtml(
ws
)}')">View</button>
<button class="btn btn-secondary" onclick="exportWorkspace('${escapeHtml(
ws
)}')">Export</button>
<button class="btn btn-danger" onclick="deleteWorkspace('${escapeHtml(
ws
)}')">Delete</button>
</div>
</div>
`
)
.join("");
} else {
container.innerHTML =
'<p style="color: var(--text-secondary);">No workspaces found. Run a scan to create one.</p>';
}
} catch (error) {
container.innerHTML =
'<p style="color: var(--accent-danger);">Failed to load workspaces.</p>';
}
}
// 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 = '<span class="spinner"></span> 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);

382
webui/assets/style.css Normal file
View File

@@ -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;
}

151
webui/execute.php Normal file
View File

@@ -0,0 +1,151 @@
<?php
/**
* UltyScan Web Interface - Execute Scan
* Handles form submissions and runs the sniper command
*/
header('Content-Type: application/json');
// Security: Only allow POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => 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'
]);

239
webui/index.php Normal file
View File

@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UltyScan - Web Interface</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<h1>🔍 UltyScan</h1>
<p class="subtitle">Attack Surface Management Platform</p>
<div style="margin-top: 1rem;">
<span id="scan-status" class="status-badge status-idle">Idle</span>
</div>
</header>
<!-- Tabs -->
<div class="tabs">
<button class="tab active" data-tab="scan-tab">New Scan</button>
<button class="tab" data-tab="workspaces-tab">Workspaces</button>
<button class="tab" data-tab="console-tab">Console</button>
<button class="tab" data-tab="settings-tab">Settings</button>
</div>
<!-- Scan Tab -->
<div id="scan-tab" class="tab-content active">
<div class="card">
<div class="card-header">
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<h2>Configure Scan</h2>
</div>
<form id="scan-form" onsubmit="submitScan(event)">
<div class="form-grid">
<!-- Target -->
<div class="form-group" id="single-target-group">
<label for="target">Target (Domain or IP)</label>
<input type="text" id="target" name="target" placeholder="example.com or 192.168.1.1" required>
</div>
<!-- Target File (for mass scans) -->
<div class="form-group" id="target-file-group" style="display: none;">
<label for="target_file">Target File (one per line)</label>
<textarea id="target_file" name="target_file" rows="4" placeholder="example1.com&#10;example2.com&#10;192.168.1.0/24"></textarea>
</div>
<!-- Mode -->
<div class="form-group">
<label for="mode">Scan Mode</label>
<select id="mode" name="mode">
<option value="normal">Normal - Balanced scan</option>
<option value="stealth">Stealth - Low profile</option>
<option value="web">Web - Ports 80/443 only</option>
<option value="webscan">WebScan - Full web app scan</option>
<option value="webporthttp">WebPortHTTP - HTTP on custom port</option>
<option value="webporthttps">WebPortHTTPS - HTTPS on custom port</option>
<option value="port">Port - Specific port scan</option>
<option value="fullportonly">FullPortOnly - All 65535 ports</option>
<option value="discover">Discover - Network discovery</option>
<option value="flyover">Flyover - Quick multi-target</option>
<option value="airstrike">Airstrike - Fast enumeration</option>
<option value="nuke">Nuke - Full aggressive audit</option>
<option value="massportscan">MassPortScan - Multi-target ports</option>
<option value="massweb">MassWeb - Multi-target web</option>
<option value="masswebscan">MassWebScan - Multi-target webapp</option>
<option value="massvulnscan">MassVulnScan - Multi-target vulns</option>
</select>
</div>
<!-- Workspace -->
<div class="form-group">
<label for="workspace">Workspace Name</label>
<input type="text" id="workspace" name="workspace" placeholder="project-alpha">
</div>
<!-- Port (for port modes) -->
<div class="form-group" id="port-group" style="display: none;">
<label for="port">Port Number</label>
<input type="number" id="port" name="port" min="1" max="65535" placeholder="8080">
</div>
</div>
<!-- Options -->
<div class="form-group full-width" style="margin-top: 1.5rem;">
<label>Additional Options</label>
<div class="checkbox-grid">
<label class="checkbox-item">
<input type="checkbox" name="osint" value="1">
<span>Enable OSINT</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="recon" value="1">
<span>Enable Recon</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="bruteforce" value="1">
<span>Enable Bruteforce</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="fullportscan" value="1">
<span>Full Port Scan</span>
</label>
</div>
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Start Scan
</button>
<button type="reset" class="btn btn-secondary">Clear Form</button>
</div>
</form>
</div>
</div>
<!-- Workspaces Tab -->
<div id="workspaces-tab" class="tab-content">
<div class="card">
<div class="card-header">
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
</svg>
<h2>Workspaces</h2>
</div>
<div class="btn-group" style="margin-bottom: 1.5rem;">
<button class="btn btn-secondary" onclick="loadWorkspaces()">
Refresh List
</button>
</div>
<div id="workspace-list" class="workspace-list">
<p style="color: var(--text-secondary);">Loading workspaces...</p>
</div>
</div>
</div>
<!-- Console Tab -->
<div id="console-tab" class="tab-content">
<div class="card">
<div class="card-header">
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<h2>Console Output</h2>
</div>
<div id="console-output" class="console">UltyScan Web Interface v1.0
Ready to scan...</div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="document.getElementById('console-output').textContent = 'Console cleared.\n'">
Clear Console
</button>
<button class="btn btn-secondary" onclick="checkScanStatus()">
Refresh Status
</button>
</div>
</div>
</div>
<!-- Settings Tab -->
<div id="settings-tab" class="tab-content">
<div class="card">
<div class="card-header">
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<h2>System Actions</h2>
</div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="updateScanner()">
Update UltyScan
</button>
<button class="btn btn-danger" onclick="if(confirm('Stop all running scans?')) stopAllScans()">
Stop All Scans
</button>
</div>
<div style="margin-top: 2rem;">
<h3 style="margin-bottom: 1rem; font-size: 1rem;">Scanner Info</h3>
<p style="color: var(--text-secondary); font-size: 0.9rem;">
Install Directory: <code style="color: var(--accent-primary);">/usr/share/sniper</code><br>
Loot Directory: <code style="color: var(--accent-primary);">/usr/share/sniper/loot/workspace</code>
</p>
</div>
</div>
</div>
</div>
<script src="assets/script.js"></script>
<script>
// Additional inline functions
async function updateScanner() {
if (!confirm('Update UltyScan? This may take a while.')) return;
showNotification('Starting update...', 'info');
try {
const response = await fetch('execute.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=update'
});
const result = await response.json();
if (result.success) {
showNotification('Update started. Check console for progress.', 'success');
}
} catch (e) {
showNotification('Update failed.', 'error');
}
}
async function stopAllScans() {
try {
await fetch('execute.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=stop'
});
showNotification('Stop signal sent.', 'warning');
checkScanStatus();
} catch (e) {
showNotification('Failed to stop scans.', 'error');
}
}
</script>
</body>
</html>

45
webui/status.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
/**
* UltyScan Web Interface - Scan Status
* Returns current scan status as JSON
*/
header('Content-Type: application/json');
// Check if any sniper process is running
$output = shell_exec('pgrep -f "sniper" 2>/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')
]);

79
webui/workspaces.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
/**
* UltyScan Web Interface - Workspace Management
*/
header('Content-Type: application/json');
define('WORKSPACE_DIR', '/usr/share/sniper/loot/workspace');
define('SNIPER_PATH', '/usr/share/sniper/sniper');
// Handle GET requests (list, view)
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$action = $_GET['action'] ?? 'list';
if ($action === 'list') {
$workspaces = [];
if (is_dir(WORKSPACE_DIR)) {
$dirs = scandir(WORKSPACE_DIR);
foreach ($dirs as $dir) {
if ($dir !== '.' && $dir !== '..' && is_dir(WORKSPACE_DIR . '/' . $dir)) {
$workspaces[] = $dir;
}
}
}
echo json_encode(['workspaces' => $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']);