mirror of
https://github.com/DeNNiiInc/UltyScan.git
synced 2026-04-17 18:26:00 +00:00
Add Web Interface for UltyScan
This commit is contained in:
307
webui/assets/script.js
Normal file
307
webui/assets/script.js
Normal 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
382
webui/assets/style.css
Normal 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
151
webui/execute.php
Normal 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
239
webui/index.php
Normal 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 example2.com 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
45
webui/status.php
Normal 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
79
webui/workspaces.php
Normal 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']);
|
||||||
Reference in New Issue
Block a user