mirror of
https://github.com/P-Aimon-Pen/ROFMAD
synced 2026-06-21 13:45:02 +00:00
1265 lines
54 KiB
HTML
Executable File
1265 lines
54 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>ROFMAD - {% block title %}{% endblock %}</title>
|
|
|
|
<style>
|
|
{% include "styles.html" %}
|
|
</style>
|
|
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
<!-- Navigation Bar -->
|
|
<nav class="navbar">
|
|
<div class="nav-brand">
|
|
<h1>ROFMAD</h1>
|
|
</div>
|
|
<div class="nav-links">
|
|
<a href="/">Dashboard</a>
|
|
<a href="/scanners" data-admin-only="true">Scanners</a>
|
|
<a href="/hosts">Hosts</a>
|
|
<a href="/actions/builder" data-admin-only="true">Actions</a>
|
|
<a href="/tags">Tags</a>
|
|
<a href="/queue">Queue</a>
|
|
<a href="/logs">Logs</a>
|
|
<a href="/settings" data-admin-only="true">Settings</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main Content -->
|
|
<main class="container">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<script>
|
|
class TerminalModal {
|
|
constructor() {
|
|
this.modal = null;
|
|
this.isOpen = false;
|
|
this.taskId = null;
|
|
this.pollInterval = null;
|
|
}
|
|
|
|
open(title, taskId) {
|
|
if (this.isOpen) this.close();
|
|
|
|
this.taskId = taskId;
|
|
this.createModal(title);
|
|
this.startPolling();
|
|
}
|
|
|
|
createModal(title) {
|
|
this.modal = document.createElement('div');
|
|
this.modal.className = 'terminal-modal-overlay';
|
|
this.modal.innerHTML = `
|
|
<div class="terminal-modal">
|
|
<div class="terminal-header">
|
|
<h3 class="terminal-title">${title}</h3>
|
|
<div class="terminal-controls">
|
|
<button class="terminal-btn" onclick="terminalModal.copyAll()">Copy All</button>
|
|
<button class="terminal-btn terminal-close" onclick="terminalModal.close()">Close</button>
|
|
</div>
|
|
</div>
|
|
<div class="terminal-body">
|
|
<div class="terminal-output" id="terminalOutput">Loading...</div>
|
|
<div class="terminal-status" id="terminalStatus">Connecting...</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(this.modal);
|
|
this.modal.style.display = 'flex';
|
|
this.isOpen = true;
|
|
|
|
// Close handlers
|
|
document.addEventListener('keydown', this.handleKeydown.bind(this));
|
|
this.modal.addEventListener('click', (e) => {
|
|
if (e.target === this.modal) this.close();
|
|
});
|
|
}
|
|
|
|
startPolling() {
|
|
this.pollInterval = setInterval(async () => {
|
|
try {
|
|
const response = await fetch(`/api/task/${this.taskId}/output`);
|
|
const data = await response.json();
|
|
this.updateOutput(data.output);
|
|
|
|
if (data.output.includes('complete') || data.output.includes('stopped')) {
|
|
document.getElementById('terminalStatus').textContent = 'Complete';
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch output:', error);
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
updateOutput(output) {
|
|
const outputEl = document.getElementById('terminalOutput');
|
|
if (outputEl) {
|
|
outputEl.textContent = output;
|
|
outputEl.scrollTop = outputEl.scrollHeight;
|
|
}
|
|
}
|
|
|
|
copyAll() {
|
|
const output = document.getElementById('terminalOutput').textContent;
|
|
if (window.ROFMAD) {
|
|
window.ROFMAD.copyToClipboard(output);
|
|
}
|
|
}
|
|
|
|
close() {
|
|
if (this.pollInterval) {
|
|
clearInterval(this.pollInterval);
|
|
this.pollInterval = null;
|
|
}
|
|
|
|
if (this.modal) {
|
|
document.body.removeChild(this.modal);
|
|
this.modal = null;
|
|
}
|
|
|
|
this.isOpen = false;
|
|
this.taskId = null;
|
|
document.removeEventListener('keydown', this.handleKeydown.bind(this));
|
|
}
|
|
|
|
handleKeydown(e) {
|
|
if (e.key === 'Escape' && this.isOpen) {
|
|
this.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
class DiscoveryResultsModal {
|
|
constructor() {
|
|
this.modal = null;
|
|
this.isOpen = false;
|
|
}
|
|
|
|
showDiscoveryResults(results, toolType) {
|
|
if (this.isOpen) this.close();
|
|
|
|
const title = toolType === 'dns-search' ? 'DNS Discovery Results' : 'Interface Discovery Results';
|
|
this.createModal(title);
|
|
this.displayResults(results);
|
|
}
|
|
|
|
createModal(title) {
|
|
this.modal = document.createElement('div');
|
|
this.modal.className = 'discovery-modal-overlay';
|
|
this.modal.innerHTML = `
|
|
<div class="discovery-modal">
|
|
<div class="discovery-modal-header">
|
|
<h3 class="discovery-modal-title">${title}</h3>
|
|
<button class="discovery-modal-close" onclick="discoveryModal.close()">Close</button>
|
|
</div>
|
|
<div class="discovery-modal-body" id="discoveryModalBody">
|
|
<!-- Content will be populated -->
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(this.modal);
|
|
this.modal.style.display = 'flex';
|
|
this.isOpen = true;
|
|
|
|
// Close handlers
|
|
document.addEventListener('keydown', this.handleKeydown.bind(this));
|
|
this.modal.addEventListener('click', (e) => {
|
|
if (e.target === this.modal) this.close();
|
|
});
|
|
}
|
|
|
|
displayResults(results) {
|
|
const body = document.getElementById('discoveryModalBody');
|
|
let content = '';
|
|
|
|
// Only show Hosts table if we have IPs
|
|
if (results.ips && results.ips.length > 0) {
|
|
content += `
|
|
<div class="discovery-section">
|
|
<h3>Discovered Hosts (${results.ips.length})</h3>
|
|
<table class="discovery-table">
|
|
<thead>
|
|
<tr>
|
|
<th>IP Address</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${results.ips.map(ip => `
|
|
<tr data-target="${ip}">
|
|
<td><code>${ip}</code></td>
|
|
<td>
|
|
<div class="discovery-actions">
|
|
<button class="discovery-btn discovery-btn-primary" onclick="discoveryModal.addToTargets('${ip}')">
|
|
Add to Targets
|
|
</button>
|
|
<button class="discovery-btn discovery-btn-danger" onclick="discoveryModal.blacklist('${ip}')">
|
|
Blacklist
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Only show Subnets table if we have subnets
|
|
if (results.subnets && results.subnets.length > 0) {
|
|
content += `
|
|
<div class="discovery-section">
|
|
<h3>Discovered Subnets (${results.subnets.length})</h3>
|
|
<table class="discovery-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Subnet</th>
|
|
<th>Size</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${results.subnets.map(subnet => {
|
|
const prefix = parseInt(subnet.split('/')[1]);
|
|
const sizeInfo = this.getSubnetSizeInfo(prefix);
|
|
const actions = this.getSubnetActionButtons(subnet, prefix);
|
|
|
|
return `
|
|
<tr data-target="${subnet}">
|
|
<td><code>${subnet}</code></td>
|
|
<td>${sizeInfo}</td>
|
|
<td>
|
|
<div class="discovery-actions">
|
|
${actions}
|
|
<button class="discovery-btn discovery-btn-danger" onclick="discoveryModal.blacklist('${subnet}')">
|
|
Blacklist
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// If no results at all
|
|
if (!content) {
|
|
content = `
|
|
<div class="discovery-empty">
|
|
<p>No new targets discovered</p>
|
|
<p>All discovered items may already be known or blacklisted.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
body.innerHTML = content;
|
|
}
|
|
|
|
getSubnetSizeInfo(prefix) {
|
|
if (prefix === 24) {
|
|
return '/24 (256 hosts)';
|
|
} else if (prefix > 24) {
|
|
const hosts = Math.pow(2, 32 - prefix);
|
|
return `/${prefix} (${hosts} hosts - smaller)`;
|
|
} else {
|
|
const hosts = Math.pow(2, 32 - prefix);
|
|
return `/${prefix} (${hosts.toLocaleString()} hosts - larger)`;
|
|
}
|
|
}
|
|
|
|
getSubnetActionButtons(subnet, prefix) {
|
|
const baseIp = subnet.split('/')[0];
|
|
|
|
if (prefix === 24) {
|
|
// /24 networks: Add subnet | Blacklist
|
|
return `
|
|
<button class="discovery-btn discovery-btn-primary" onclick="discoveryModal.addSubnet('${subnet}')">
|
|
Add Subnet
|
|
</button>
|
|
`;
|
|
} else if (prefix > 24) {
|
|
// Smaller than /24: Add as /24 | Blacklist
|
|
return `
|
|
<button class="discovery-btn discovery-btn-secondary" onclick="discoveryModal.addSubnet('${baseIp}/24')">
|
|
Add as /24
|
|
</button>
|
|
`;
|
|
} else {
|
|
// Larger than /24: Launch Masscan | Add as /24 | Blacklist
|
|
return `
|
|
<button class="discovery-btn discovery-btn-warning" onclick="discoveryModal.launchMasscan('${subnet}')">
|
|
Launch Masscan
|
|
</button>
|
|
<button class="discovery-btn discovery-btn-secondary" onclick="discoveryModal.addSubnet('${baseIp}/24')">
|
|
Add as /24
|
|
</button>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Action handlers
|
|
async addToTargets(ip) {
|
|
await this.performAction('/add-target', `target=${encodeURIComponent(ip)}`, `Added ${ip} to targets`);
|
|
}
|
|
|
|
async addSubnet(subnet) {
|
|
await this.performAction('/add-subnet', `subnet=${encodeURIComponent(subnet)}`, `Added ${subnet} for scanning`);
|
|
}
|
|
|
|
async blacklist(target) {
|
|
await this.performAction('/add-blacklist', `target=${encodeURIComponent(target)}`, `Blacklisted ${target}`);
|
|
}
|
|
|
|
async launchMasscan(subnet) {
|
|
await this.performAction('/add-subnet', `subnet=${encodeURIComponent(subnet)}`, `Launched Masscan on ${subnet}`);
|
|
}
|
|
|
|
async performAction(endpoint, body, successMessage) {
|
|
try {
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: body
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
ROFMAD.showNotification(successMessage, 'success');
|
|
// Remove the row from display
|
|
const target = body.split('=')[1];
|
|
const decodedTarget = decodeURIComponent(target);
|
|
this.removeRowByTarget(decodedTarget);
|
|
} else {
|
|
ROFMAD.showNotification(`Action failed: ${data.message}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
ROFMAD.showNotification(`Action error: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
removeRowByTarget(target) {
|
|
const row = document.querySelector(`tr[data-target="${target}"]`);
|
|
if (row) {
|
|
row.style.transition = 'opacity 0.3s ease';
|
|
row.style.opacity = '0';
|
|
setTimeout(() => {
|
|
row.remove();
|
|
this.checkIfTablesEmpty();
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
checkIfTablesEmpty() {
|
|
const sections = document.querySelectorAll('.discovery-section');
|
|
sections.forEach(section => {
|
|
const rows = section.querySelectorAll('tbody tr');
|
|
if (rows.length === 0) {
|
|
section.style.transition = 'opacity 0.3s ease';
|
|
section.style.opacity = '0';
|
|
setTimeout(() => section.remove(), 300);
|
|
}
|
|
});
|
|
|
|
// If no sections left, show empty message
|
|
if (document.querySelectorAll('.discovery-section').length === 0) {
|
|
document.getElementById('discoveryModalBody').innerHTML = `
|
|
<div class="discovery-empty">
|
|
<p>All actions completed!</p>
|
|
<p>No more items to process.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
close() {
|
|
if (this.modal) {
|
|
document.body.removeChild(this.modal);
|
|
this.modal = null;
|
|
}
|
|
this.isOpen = false;
|
|
document.removeEventListener('keydown', this.handleKeydown.bind(this));
|
|
}
|
|
|
|
handleKeydown(e) {
|
|
if (e.key === 'Escape' && this.isOpen) {
|
|
this.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Global instances
|
|
window.terminalModal = new TerminalModal();
|
|
window.discoveryModal = new DiscoveryResultsModal();
|
|
|
|
// Selective refresh - only update counters and analysis list
|
|
const selectiveRefresh = {
|
|
intervals: {},
|
|
|
|
startCounterRefresh: () => {
|
|
selectiveRefresh.intervals.counters = setInterval(async () => {
|
|
try {
|
|
const response = await fetch('/api/task-status');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
selectiveRefresh.updateCounters(data);
|
|
}
|
|
} catch (error) {
|
|
// Silent fail
|
|
}
|
|
}, 3000);
|
|
},
|
|
|
|
startAnalysisRefresh: () => {
|
|
selectiveRefresh.intervals.analysis = setInterval(async () => {
|
|
try {
|
|
const response = await fetch('/api/hosts-ready-analysis');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
selectiveRefresh.updateAnalysisList(data);
|
|
}
|
|
} catch (error) {
|
|
// Silent fail
|
|
}
|
|
}, 15000);
|
|
},
|
|
|
|
updateCounters: (data) => {
|
|
const updates = {
|
|
'queued-count': data.queued_count,
|
|
'active-count': data.active_count
|
|
};
|
|
|
|
Object.entries(updates).forEach(([id, value]) => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.textContent = value;
|
|
}
|
|
});
|
|
},
|
|
|
|
updateAnalysisList: (data) => {
|
|
const container = document.querySelector('.hosts-ready-analysis');
|
|
if (!container) return;
|
|
|
|
const isEmptyState = container.querySelector('.empty-placeholder');
|
|
|
|
if (data.hosts.length === 0 && !isEmptyState) {
|
|
container.innerHTML = `
|
|
<div class="empty-placeholder">
|
|
|
|
<h3>All Hosts Analyzed</h3>
|
|
<p>No more hosts ready for analysis at this time.</p>
|
|
</div>
|
|
`;
|
|
container.className = 'empty-ready-hosts hosts-ready-analysis'; // FIX: Update class
|
|
return;
|
|
}
|
|
|
|
if (data.hosts.length > 0 && isEmptyState) {
|
|
container.innerHTML = '';
|
|
container.className = 'ready-hosts-grid hosts-ready-analysis';
|
|
}
|
|
|
|
if (data.hosts.length > 0) {
|
|
const currentIPs = new Set(
|
|
Array.from(container.querySelectorAll('[data-host-ip]'))
|
|
.map(el => el.getAttribute('data-host-ip'))
|
|
);
|
|
|
|
const newIPs = new Set(data.hosts.map(h => h.ip));
|
|
|
|
// FIX: Always update if different
|
|
if (currentIPs.size !== newIPs.size || ![...currentIPs].every(ip => newIPs.has(ip))) {
|
|
container.className = 'ready-hosts-grid hosts-ready-analysis';
|
|
container.innerHTML = data.hosts.map(host => `
|
|
<div class="ready-host-card" data-host-ip="${host.ip}" onclick="openHostForAnalysis('${host.ip}')">
|
|
<div class="ready-host-header">
|
|
<h4>${host.ip}</h4>
|
|
${host.hostname ? `<span class="ready-host-hostname">${host.hostname}</span>` : ''}
|
|
</div>
|
|
<div class="ready-host-stats">
|
|
<div class="ready-stat">
|
|
<span class="ready-stat-number">${host.ports_count}</span>
|
|
<span class="ready-stat-label">Ports</span>
|
|
</div>
|
|
<div class="ready-stat">
|
|
<span class="ready-stat-number">${host.http_services_count}</span>
|
|
<span class="ready-stat-label">HTTP</span>
|
|
</div>
|
|
<div class="ready-stat">
|
|
<span class="ready-stat-number">${host.action_outputs_count}</span>
|
|
<span class="ready-stat-label">Actions</span>
|
|
</div>
|
|
</div>
|
|
<div class="ready-host-tags">
|
|
${host.tags_with_source.slice(0, 6).map(tag =>
|
|
`<span class="ready-tag tag-${tag.source}">${tag.name}</span>`
|
|
).join('')}
|
|
${host.tags_with_source.length > 6 ?
|
|
`<span class="ready-tag-more">+${host.tags_with_source.length - 6} more</span>` : ''}
|
|
</div>
|
|
<div class="ready-host-action">
|
|
<span class="ready-action-text">Analyze</span>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
}
|
|
},
|
|
|
|
stop: () => {
|
|
Object.values(selectiveRefresh.intervals).forEach(clearInterval);
|
|
selectiveRefresh.intervals = {};
|
|
}
|
|
};
|
|
|
|
// Tab switching functionality
|
|
function initTabs() {
|
|
const tabButtons = document.querySelectorAll('.tab-btn');
|
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
|
|
tabButtons.forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
const targetTab = button.getAttribute('data-tab');
|
|
|
|
// Remove active classes
|
|
tabButtons.forEach(btn => btn.classList.remove('active'));
|
|
tabContents.forEach(content => content.classList.remove('active'));
|
|
|
|
// Add active classes
|
|
button.classList.add('active');
|
|
const targetContent = document.getElementById(targetTab);
|
|
if (targetContent) {
|
|
targetContent.classList.add('active');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Status indicator tooltips
|
|
function initTooltips() {
|
|
const statusIndicators = document.querySelectorAll('.status-indicator');
|
|
|
|
statusIndicators.forEach(indicator => {
|
|
const title = indicator.getAttribute('title');
|
|
if (title) {
|
|
indicator.addEventListener('mouseenter', (e) => {
|
|
const tooltip = document.createElement('div');
|
|
tooltip.className = 'tooltip';
|
|
tooltip.textContent = title;
|
|
tooltip.style.cssText = `
|
|
position: absolute;
|
|
background: #1f1f1f;
|
|
color: #f8fafc;
|
|
padding: 0.5rem;
|
|
border-radius: 4px;
|
|
font-size: 0.8rem;
|
|
border: 1px solid #374151;
|
|
z-index: 1000;
|
|
pointer-events: none;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
|
|
`;
|
|
|
|
const rect = e.target.getBoundingClientRect();
|
|
tooltip.style.left = (rect.left + rect.width / 2) + 'px';
|
|
tooltip.style.top = (rect.top - 40) + 'px';
|
|
tooltip.style.transform = 'translateX(-50%)';
|
|
|
|
document.body.appendChild(tooltip);
|
|
|
|
e.target.addEventListener('mouseleave', () => {
|
|
if (tooltip.parentNode) {
|
|
tooltip.parentNode.removeChild(tooltip);
|
|
}
|
|
}, { once: true });
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Progress bar animations
|
|
function animateProgressBars() {
|
|
const progressBars = document.querySelectorAll('.progress-fill');
|
|
|
|
progressBars.forEach(bar => {
|
|
const width = bar.style.width;
|
|
bar.style.width = '0%';
|
|
|
|
setTimeout(() => {
|
|
bar.style.width = width;
|
|
}, 100);
|
|
});
|
|
}
|
|
|
|
// Enhanced search functionality with tag preview
|
|
function initSearch() {
|
|
const searchInput = document.getElementById('search-input');
|
|
if (searchInput) {
|
|
searchInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
const value = searchInput.value.trim();
|
|
if (value) {
|
|
window.location.href = `/search?tag=${encodeURIComponent(value)}`;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add dynamic tag suggestions if available
|
|
searchInput.addEventListener('input', (e) => {
|
|
const value = e.target.value.trim();
|
|
if (value.length >= 2) {
|
|
// Could add dynamic tag suggestions here
|
|
// For now, just show a helpful tooltip
|
|
if (!document.getElementById('search-tooltip')) {
|
|
const tooltip = document.createElement('div');
|
|
tooltip.id = 'search-tooltip';
|
|
tooltip.style.cssText = `
|
|
position: absolute;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
padding: 0.5rem;
|
|
font-size: 0.8rem;
|
|
color: var(--text-muted);
|
|
z-index: 1000;
|
|
margin-top: 2px;
|
|
max-width: 300px;
|
|
`;
|
|
tooltip.textContent = 'Press Enter to search, or visit Tags page for all available tags';
|
|
searchInput.parentNode.appendChild(tooltip);
|
|
|
|
setTimeout(() => {
|
|
const existingTooltip = document.getElementById('search-tooltip');
|
|
if (existingTooltip) {
|
|
existingTooltip.remove();
|
|
}
|
|
}, 3000);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Table sorting (basic client-side)
|
|
function initTableSorting() {
|
|
const tables = document.querySelectorAll('.hosts-table, .tasks-table');
|
|
|
|
tables.forEach(table => {
|
|
const headers = table.querySelectorAll('th');
|
|
headers.forEach((header, index) => {
|
|
if (header.textContent.trim() && !header.classList.contains('no-sort')) {
|
|
header.style.cursor = 'pointer';
|
|
header.addEventListener('click', () => {
|
|
sortTable(table, index);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function sortTable(table, columnIndex) {
|
|
const tbody = table.querySelector('tbody');
|
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
|
|
const isNumeric = rows.length > 0 && !isNaN(rows[0].cells[columnIndex]?.textContent.trim());
|
|
|
|
rows.sort((a, b) => {
|
|
const aVal = a.cells[columnIndex]?.textContent.trim() || '';
|
|
const bVal = b.cells[columnIndex]?.textContent.trim() || '';
|
|
|
|
if (isNumeric) {
|
|
return parseFloat(aVal) - parseFloat(bVal);
|
|
} else {
|
|
return aVal.localeCompare(bVal);
|
|
}
|
|
});
|
|
|
|
rows.forEach(row => tbody.appendChild(row));
|
|
}
|
|
|
|
// Enhanced keyboard shortcuts
|
|
function initKeyboardShortcuts() {
|
|
document.addEventListener('keydown', (e) => {
|
|
// Alt + D = Dashboard
|
|
if (e.altKey && e.key === 'd') {
|
|
e.preventDefault();
|
|
window.location.href = '/';
|
|
}
|
|
|
|
// Alt + H = Hosts
|
|
if (e.altKey && e.key === 'h') {
|
|
e.preventDefault();
|
|
window.location.href = '/hosts';
|
|
}
|
|
|
|
// Alt + T = Tags
|
|
if (e.altKey && e.key === 't') {
|
|
e.preventDefault();
|
|
window.location.href = '/tags';
|
|
}
|
|
|
|
// Alt + L = Logs
|
|
if (e.altKey && e.key === 'l') {
|
|
e.preventDefault();
|
|
window.location.href = '/logs';
|
|
}
|
|
|
|
// Alt + A = Actions
|
|
if (e.altKey && e.key === 'a') {
|
|
e.preventDefault();
|
|
window.location.href = '/actions/builder';
|
|
}
|
|
|
|
// Escape = Close modals/go back
|
|
if (e.key === 'Escape') {
|
|
const activeTab = document.querySelector('.tab-content.active');
|
|
if (activeTab && window.history.length > 1) {
|
|
window.history.back();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Enhanced tag interaction functionality
|
|
function initTagInteractions() {
|
|
// Add click-to-search functionality for tag elements
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('tag') || e.target.classList.contains('tag-item')) {
|
|
e.preventDefault();
|
|
const tagText = e.target.textContent.trim();
|
|
if (tagText) {
|
|
window.location.href = `/tags?tag=${encodeURIComponent(tagText)}`;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Categorize and style tags based on likely source
|
|
document.querySelectorAll('.tag').forEach(tag => {
|
|
const tagText = tag.textContent.trim().toLowerCase();
|
|
|
|
// Action-based tags (from specific_actions) - typically more specific/complex
|
|
if (isLikelyActionTag(tagText)) {
|
|
tag.classList.add('action-tag');
|
|
}
|
|
|
|
tag.style.cursor = 'pointer';
|
|
tag.title = 'Click to search for this tag';
|
|
});
|
|
}
|
|
|
|
// Determine if a tag is likely from action output vs automatic detection
|
|
function isLikelyActionTag(tagText) {
|
|
// Action tags are typically more specific, longer, or contain certain patterns
|
|
const actionIndicators = [
|
|
// Complex/specific strings that unlikely come from nmap
|
|
'domain-controller', 'file-server', 'mail-server', 'web-app-',
|
|
'vulnerable-', 'exploit-', 'cve-', 'admin-panel', 'backup-',
|
|
'config-', 'default-creds', 'weak-auth', 'anonymous-',
|
|
// Length-based heuristic (action tags tend to be longer/more descriptive)
|
|
];
|
|
|
|
// Check for action-specific patterns
|
|
if (actionIndicators.some(indicator => tagText.includes(indicator))) {
|
|
return true;
|
|
}
|
|
|
|
// Length and complexity heuristic
|
|
if (tagText.length > 15 || tagText.includes('-') && tagText.split('-').length > 2) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Analysis workflow functions for pentester workflow
|
|
function toggleAnalysisStatus(ip, analyzed) {
|
|
const row = document.querySelector(`tr[data-ip="${ip}"]`);
|
|
const statusContainer = row?.querySelector('.analysis-status-container');
|
|
|
|
if (!statusContainer) return;
|
|
|
|
// Add loading animation
|
|
statusContainer.classList.add('analysis-updating');
|
|
|
|
// Make the API call
|
|
fetch('/mark-analyzed', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `ip=${encodeURIComponent(ip)}&analyzed=${analyzed}`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Update the row appearance
|
|
if (analyzed) {
|
|
row.classList.add('analyzed-host');
|
|
statusContainer.innerHTML = `
|
|
<div class="analysis-complete" onclick="toggleAnalysisStatus('${ip}', false)">
|
|
|
|
<span class="analysis-text">Analysis Complete</span>
|
|
</div>
|
|
`;
|
|
ROFMAD.showNotification(`Host ${ip} marked as analyzed`, 'success');
|
|
} else {
|
|
row.classList.remove('analyzed-host');
|
|
statusContainer.innerHTML = `
|
|
<div class="analysis-pending" onclick="toggleAnalysisStatus('${ip}', true)">
|
|
|
|
<span class="analysis-text">User Analysis Complete</span>
|
|
</div>
|
|
`;
|
|
ROFMAD.showNotification(`Host ${ip} marked as pending analysis`, 'info');
|
|
}
|
|
} else {
|
|
ROFMAD.showNotification('Failed to update analysis status', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error updating analysis status:', error);
|
|
ROFMAD.showNotification('Failed to update analysis status', 'error');
|
|
})
|
|
.finally(() => {
|
|
// Remove loading animation
|
|
statusContainer.classList.remove('analysis-updating');
|
|
});
|
|
}
|
|
|
|
function toggleInterestingStatus(ip, interesting) {
|
|
fetch('/mark-interesting', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `ip=${encodeURIComponent(ip)}&interesting=${interesting}`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
ROFMAD.showNotification(`Host ${ip} ${interesting ? 'bookmarked' : 'unbookmarked'}`, 'success');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
ROFMAD.showNotification('Failed to update bookmark status', 'error');
|
|
});
|
|
}
|
|
|
|
// Function for opening host for analysis from dashboard
|
|
function openHostForAnalysis(ip) {
|
|
const hostCard = event.currentTarget;
|
|
|
|
// Prevent double clicks
|
|
if (hostCard.disabled) return;
|
|
hostCard.disabled = true;
|
|
|
|
// Visual feedback
|
|
hostCard.style.opacity = '0.7';
|
|
hostCard.style.pointerEvents = 'none';
|
|
|
|
// Mark as analyzed with retry logic
|
|
const markAnalyzed = async (retryCount = 0) => {
|
|
try {
|
|
const response = await fetch('/mark-analyzed', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `ip=${encodeURIComponent(ip)}&analyzed=true`,
|
|
signal: AbortSignal.timeout(5000) // 5 second timeout
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
// Success - remove card
|
|
hostCard.style.transition = 'all 0.5s ease';
|
|
hostCard.style.transform = 'translateX(100%)';
|
|
hostCard.style.opacity = '0';
|
|
|
|
setTimeout(() => {
|
|
hostCard.remove();
|
|
|
|
// Check if this was the last host card
|
|
const remainingCards = document.querySelectorAll('.ready-host-card');
|
|
if (remainingCards.length === 0) {
|
|
const container = document.querySelector('.hosts-ready-analysis');
|
|
if (container) {
|
|
container.innerHTML = `
|
|
<div class="empty-placeholder">
|
|
|
|
<h3>All Hosts Analyzed</h3>
|
|
<p>No more hosts ready for analysis at this time.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
}, 500);
|
|
|
|
// Open host detail in new tab
|
|
window.open(`/hosts/${ip}`, '_blank');
|
|
|
|
if (window.ROFMAD?.showNotification) {
|
|
window.ROFMAD.showNotification(`Host ${ip} marked as analyzed`, 'success');
|
|
}
|
|
} else {
|
|
throw new Error('Server returned failure');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error marking host as analyzed:', error);
|
|
|
|
// Retry logic
|
|
if (retryCount < 2) {
|
|
setTimeout(() => markAnalyzed(retryCount + 1), 1000);
|
|
return;
|
|
}
|
|
|
|
// Final failure - restore card
|
|
hostCard.disabled = false;
|
|
hostCard.style.opacity = '1';
|
|
hostCard.style.pointerEvents = 'auto';
|
|
|
|
if (window.ROFMAD?.showNotification) {
|
|
window.ROFMAD.showNotification(`Failed to mark host ${ip} as analyzed. Please try again.`, 'error');
|
|
}
|
|
}
|
|
};
|
|
|
|
markAnalyzed();
|
|
}
|
|
|
|
// Enhanced image modal for screenshots
|
|
function openImageModal(filename, url) {
|
|
const modal = document.getElementById('imageModal');
|
|
const modalImg = document.getElementById('modalImage');
|
|
const captionText = document.getElementById('modalCaption');
|
|
|
|
if (modal && modalImg && captionText) {
|
|
modal.style.display = 'block';
|
|
modalImg.src = '/screenshots/' + filename;
|
|
captionText.innerHTML = url;
|
|
}
|
|
}
|
|
|
|
function closeImageModal() {
|
|
const modal = document.getElementById('imageModal');
|
|
if (modal) {
|
|
modal.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Search by tag functionality
|
|
function searchTag(tag) {
|
|
window.location.href = `/tags?tag=${encodeURIComponent(tag)}`;
|
|
}
|
|
|
|
// Role-based UI management - hide admin-only elements from player accounts
|
|
function initRoleBasedUI() {
|
|
// Test if user is admin by trying to access a known admin endpoint
|
|
fetch('/api/user-role-check', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: 'test=role'
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (!data.success || data.message === 'Admin privileges required') {
|
|
// User is not admin - hide admin-only elements
|
|
hideAdminOnlyElements();
|
|
}
|
|
})
|
|
.catch(() => {
|
|
// If request fails, assume not admin and hide elements
|
|
hideAdminOnlyElements();
|
|
});
|
|
}
|
|
|
|
function hideAdminOnlyElements() {
|
|
// Hide navigation links marked with data-admin-only
|
|
document.querySelectorAll('[data-admin-only="true"]').forEach(element => {
|
|
element.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
// Initialize all functionality when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initTabs();
|
|
initTooltips();
|
|
animateProgressBars();
|
|
initSearch();
|
|
initTableSorting();
|
|
initKeyboardShortcuts();
|
|
initTagInteractions();
|
|
initRoleBasedUI();
|
|
|
|
// Start selective refresh only on dashboard
|
|
if (window.location.pathname === '/') {
|
|
selectiveRefresh.startCounterRefresh();
|
|
selectiveRefresh.startAnalysisRefresh();
|
|
}
|
|
|
|
// Close modal with Escape key
|
|
document.addEventListener('keydown', function(event) {
|
|
if (event.key === 'Escape') {
|
|
closeImageModal();
|
|
}
|
|
});
|
|
|
|
// Add smooth scrolling for anchor links
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
anchor.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
const target = document.querySelector(this.getAttribute('href'));
|
|
if (target) {
|
|
target.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Add loading states for forms
|
|
document.querySelectorAll('form').forEach(form => {
|
|
form.addEventListener('submit', function() {
|
|
const submitBtn = form.querySelector('button[type="submit"], input[type="submit"]');
|
|
if (submitBtn) {
|
|
const originalText = submitBtn.textContent || submitBtn.value;
|
|
submitBtn.disabled = true;
|
|
if (submitBtn.textContent !== undefined) {
|
|
submitBtn.textContent = 'Loading...';
|
|
} else {
|
|
submitBtn.value = 'Loading...';
|
|
}
|
|
|
|
// Restore original state after 5 seconds (fallback)
|
|
setTimeout(() => {
|
|
submitBtn.disabled = false;
|
|
if (submitBtn.textContent !== undefined) {
|
|
submitBtn.textContent = originalText;
|
|
} else {
|
|
submitBtn.value = originalText;
|
|
}
|
|
}, 5000);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Highlight current page in navigation
|
|
const currentPath = window.location.pathname;
|
|
document.querySelectorAll('.nav-links a').forEach(link => {
|
|
if (link.getAttribute('href') === currentPath) {
|
|
link.style.borderBottomColor = 'var(--gb-blue)';
|
|
link.style.color = 'var(--gb-blue-b)';
|
|
}
|
|
});
|
|
|
|
// Initialize copy functionality for code blocks
|
|
document.querySelectorAll('pre code').forEach(block => {
|
|
block.addEventListener('dblclick', function() {
|
|
ROFMAD.copyToClipboard(this.textContent);
|
|
});
|
|
block.title = 'Double-click to copy';
|
|
block.style.cursor = 'pointer';
|
|
});
|
|
|
|
// Add expand/collapse for large action outputs
|
|
document.querySelectorAll('.action-content pre').forEach(pre => {
|
|
if (pre.textContent.length > 2000) {
|
|
const lines = pre.textContent.split('\n');
|
|
if (lines.length > 50) {
|
|
const shortContent = lines.slice(0, 50).join('\n') + '\n\n... (truncated, click to expand)';
|
|
const fullContent = pre.textContent;
|
|
|
|
pre.textContent = shortContent;
|
|
pre.style.cursor = 'pointer';
|
|
|
|
let expanded = false;
|
|
pre.addEventListener('click', function() {
|
|
if (!expanded) {
|
|
this.textContent = fullContent;
|
|
expanded = true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add service URL click functionality
|
|
document.querySelectorAll('.service-header h4').forEach(header => {
|
|
header.style.cursor = 'pointer';
|
|
header.addEventListener('click', function() {
|
|
const url = this.textContent.trim();
|
|
if (url.startsWith('http')) {
|
|
window.open(url, '_blank');
|
|
}
|
|
});
|
|
header.title = 'Click to open in new tab';
|
|
});
|
|
});
|
|
|
|
// Stop refresh when page unloads
|
|
window.addEventListener('beforeunload', () => {
|
|
selectiveRefresh.stop();
|
|
});
|
|
|
|
// Enhanced utility functions available globally
|
|
window.ROFMAD = {
|
|
// Format numbers with commas
|
|
formatNumber: (num) => {
|
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
},
|
|
|
|
// Format bytes to human readable
|
|
formatBytes: (bytes) => {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
},
|
|
|
|
// Copy text to clipboard with enhanced error handling
|
|
copyToClipboard: (text) => {
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
ROFMAD.showNotification('Copied to clipboard', 'success');
|
|
}).catch((err) => {
|
|
console.error('Clipboard API failed:', err);
|
|
ROFMAD.fallbackCopyToClipboard(text);
|
|
});
|
|
} else {
|
|
ROFMAD.fallbackCopyToClipboard(text);
|
|
}
|
|
},
|
|
|
|
// Fallback copy method for older browsers
|
|
fallbackCopyToClipboard: (text) => {
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = text;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.left = '-999999px';
|
|
textArea.style.top = '-999999px';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
|
|
try {
|
|
document.execCommand('copy');
|
|
ROFMAD.showNotification('Copied to clipboard', 'success');
|
|
} catch (err) {
|
|
console.error('Fallback copy failed:', err);
|
|
ROFMAD.showNotification('Failed to copy to clipboard', 'error');
|
|
}
|
|
|
|
document.body.removeChild(textArea);
|
|
},
|
|
|
|
// Enhanced notification system
|
|
showNotification: (message, type = 'info') => {
|
|
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: 10000;
|
|
animation: slideIn 0.3s ease;
|
|
max-width: 300px;
|
|
word-wrap: break-word;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
`;
|
|
|
|
const colors = {
|
|
info: '#3b82f6',
|
|
success: '#10b981',
|
|
warning: '#f59e0b',
|
|
error: '#ef4444'
|
|
};
|
|
|
|
notification.style.backgroundColor = colors[type] || colors.info;
|
|
notification.textContent = message;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.style.animation = 'slideOut 0.3s ease';
|
|
setTimeout(() => {
|
|
if (notification.parentNode) {
|
|
notification.parentNode.removeChild(notification);
|
|
}
|
|
}, 300);
|
|
}, 3000);
|
|
}
|
|
};
|
|
|
|
// Make analysis functions globally available
|
|
window.toggleAnalysisStatus = toggleAnalysisStatus;
|
|
window.toggleInterestingStatus = toggleInterestingStatus;
|
|
window.openHostForAnalysis = openHostForAnalysis;
|
|
window.openImageModal = openImageModal;
|
|
window.closeImageModal = closeImageModal;
|
|
window.searchTag = searchTag;
|
|
|
|
// Add CSS animations
|
|
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; }
|
|
}
|
|
|
|
.tooltip {
|
|
animation: fadeIn 0.2s ease;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateX(-50%) translateY(5px); }
|
|
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
}
|
|
|
|
/* Enhanced analysis status styling */
|
|
.analysis-updating {
|
|
animation: analysisUpdate 0.3s ease;
|
|
}
|
|
|
|
@keyframes analysisUpdate {
|
|
0% { transform: scale(1); }
|
|
50% { transform: scale(1.1); }
|
|
100% { transform: scale(1); }
|
|
}
|
|
|
|
/* Copy button hover effects */
|
|
.copy-btn {
|
|
transition: background-color 0.1s, color 0.1s;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
</script>
|
|
|
|
{% block extra_js %}{% endblock %}
|
|
</body>
|
|
</html> |