Files
UltyScan/webui/assets/script.js

345 lines
10 KiB
JavaScript

// UltyScan Web Interface - Frontend Logic
document.addEventListener("DOMContentLoaded", function () {
initTabs();
initModeHandler();
loadWorkspaces();
checkScanStatus();
loadVersion();
});
// Load Git Version Info
async function loadVersion() {
const versionDisplay = document.getElementById("version-display");
if (!versionDisplay) return;
try {
const response = await fetch("version.php");
const data = await response.json();
if (data.commit) {
versionDisplay.innerHTML = `
<span style="background: var(--bg-secondary); padding: 0.25rem 0.5rem; border-radius: 4px; margin-right: 0.5rem;">
<span style="color: var(--accent-primary);">⎇</span> ${data.branch || 'main'}
</span>
<span style="background: var(--bg-secondary); padding: 0.25rem 0.5rem; border-radius: 4px;">
<span style="color: var(--accent-success);">#</span>${data.commit}
</span>
<span style="margin-left: 0.5rem; color: var(--text-secondary);">${data.age}</span>
`;
}
} catch (error) {
console.error("Failed to load version:", error);
}
}
// 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) => {
const name = ws.name || ws;
const size = ws.size || '';
const modified = ws.modified || '';
return `
<div class="workspace-item">
<div style="flex: 1;">
<span class="workspace-name">${escapeHtml(name)}</span>
${size ? `<span style="color: var(--text-secondary); font-size: 0.8rem; margin-left: 1rem;">${size}</span>` : ''}
${modified ? `<span style="color: var(--text-secondary); font-size: 0.8rem; margin-left: 0.5rem;">| ${modified}</span>` : ''}
</div>
<div class="workspace-actions">
<button class="btn btn-secondary" onclick="viewWorkspace('${escapeHtml(name)}')">View</button>
<button class="btn btn-danger" onclick="deleteWorkspace('${escapeHtml(name)}')">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.reportUrl) {
window.open(result.reportUrl, "_blank");
} else if (result.error) {
showNotification(result.error, "error");
} else {
showNotification("No report found for this workspace.", "warning");
}
} catch (error) {
showNotification("Failed to open workspace.", "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");
const stopBtn = document.getElementById("stop-scan-btn");
const consoleOutput = document.getElementById("console-output");
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';
if (stopBtn) stopBtn.style.display = "inline-flex";
startStatusPolling();
} else {
statusBadge.className = "status-badge status-idle";
statusBadge.textContent = "Idle";
if (stopBtn) stopBtn.style.display = "none";
stopStatusPolling();
}
// Update console with log content - only if there's actual content
if (consoleOutput) {
if (result.logContent || result.workspaceOutput) {
let output = "UltyScan Console\n";
output += "================\n";
if (result.latestLogFile) {
output += `Log: ${result.latestLogFile}\n`;
}
output += `Time: ${result.timestamp}\n`;
output += "----------------\n\n";
if (result.workspaceOutput) {
output += result.workspaceOutput;
} else if (result.logContent) {
output += result.logContent;
}
consoleOutput.textContent = output;
consoleOutput.scrollTop = consoleOutput.scrollHeight;
}
// Don't clear console if there's no log content - keep existing content
}
} 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);