mirror of
https://github.com/P-Aimon-Pen/ROFMAD
synced 2026-06-21 13:45:02 +00:00
1134 lines
41 KiB
HTML
Executable File
1134 lines
41 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
/* Dashboard */
|
|
.dashboard-header {
|
|
padding: 1rem 0 1.25rem;
|
|
margin-bottom: 1.25rem;
|
|
border-bottom: 1px solid var(--gb-bg1);
|
|
}
|
|
|
|
.dashboard-title {
|
|
font-size: 1.4rem;
|
|
font-weight: 700;
|
|
color: var(--gb-blue-b);
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
background: none;
|
|
-webkit-text-fill-color: unset;
|
|
-webkit-background-clip: unset;
|
|
background-clip: unset;
|
|
}
|
|
|
|
.dashboard-subtitle {
|
|
color: var(--text-dim);
|
|
font-size: 0.78rem;
|
|
}
|
|
|
|
.stat-icon {
|
|
display: none;
|
|
}
|
|
|
|
.stat-description {
|
|
color: var(--text-dim);
|
|
font-size: 0.72rem;
|
|
display: block;
|
|
margin-top: 0.2rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* Discovery cards */
|
|
.discovery-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.discovery-card {
|
|
background-color: var(--gb-bg);
|
|
padding: 1.25rem;
|
|
border-left: 2px solid var(--gb-blue);
|
|
position: relative;
|
|
transition: border-left-color 0.1s, background-color 0.1s;
|
|
}
|
|
|
|
.discovery-card::before {
|
|
display: none;
|
|
}
|
|
|
|
.discovery-card:hover {
|
|
background-color: var(--gb-bg-s);
|
|
border-left-color: var(--gb-purple);
|
|
}
|
|
|
|
.discovery-card h4 {
|
|
color: var(--text-muted);
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.discovery-value {
|
|
font-size: 1.8rem;
|
|
font-weight: 700;
|
|
color: var(--gb-blue-b);
|
|
display: block;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
.discovery-detail {
|
|
color: var(--text-dim);
|
|
font-size: 0.72rem;
|
|
margin-top: 0.2rem;
|
|
}
|
|
|
|
/* Ready analysis section */
|
|
.ready-analysis-section {
|
|
border-left: 3px solid var(--gb-blue);
|
|
}
|
|
|
|
.ready-analysis-section::before {
|
|
display: none;
|
|
}
|
|
|
|
.ready-hosts-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.ready-host-card {
|
|
background-color: var(--gb-bg1);
|
|
border: 1px solid var(--gb-bg2);
|
|
border-left: 3px solid var(--gb-blue);
|
|
padding: 1rem;
|
|
cursor: pointer;
|
|
transition: background-color 0.1s, border-color 0.1s;
|
|
position: relative;
|
|
}
|
|
|
|
.ready-host-card::before {
|
|
display: none;
|
|
}
|
|
|
|
.ready-host-card:hover {
|
|
background-color: var(--gb-bg-s);
|
|
border-left-color: var(--gb-purple);
|
|
border-color: var(--gb-blue);
|
|
}
|
|
|
|
.ready-host-header h4 {
|
|
color: var(--gb-blue-b);
|
|
margin: 0;
|
|
font-size: 0.95rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.ready-host-hostname {
|
|
color: var(--text-dim);
|
|
font-size: 0.75rem;
|
|
font-style: italic;
|
|
}
|
|
|
|
.ready-host-stats {
|
|
display: flex;
|
|
gap: 0;
|
|
margin: 0.75rem 0;
|
|
border: 1px solid var(--gb-bg1);
|
|
}
|
|
|
|
.ready-stat {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
flex: 1;
|
|
padding: 0.5rem;
|
|
border-right: 1px solid var(--gb-bg1);
|
|
}
|
|
|
|
.ready-stat:last-child {
|
|
border-right: none;
|
|
}
|
|
|
|
.ready-stat-number {
|
|
color: var(--text-primary);
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
.ready-stat-label {
|
|
color: var(--text-dim);
|
|
font-size: 0.65rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
margin-top: 0.15rem;
|
|
}
|
|
|
|
.nuclei-severity-stats .ready-stat.severity-critical .ready-stat-number { color: var(--gb-red); }
|
|
.nuclei-severity-stats .ready-stat.severity-high .ready-stat-number { color: var(--gb-orange); }
|
|
.nuclei-severity-stats .ready-stat.severity-medium .ready-stat-number { color: var(--gb-yellow); }
|
|
.nuclei-severity-stats .ready-stat.severity-low .ready-stat-number { color: var(--gb-blue); }
|
|
.nuclei-severity-stats .ready-stat.severity-info .ready-stat-number,
|
|
.nuclei-severity-stats .ready-stat.severity-unknown .ready-stat-number { color: var(--gb-fg2); }
|
|
|
|
.ready-host-tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.25rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.ready-tag {
|
|
background: var(--gb-blue);
|
|
color: var(--gb-fg);
|
|
padding: 0.1rem 0.4rem;
|
|
font-size: 0.65rem;
|
|
font-weight: 600;
|
|
border-radius: 0;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
.ready-tag-more {
|
|
background: var(--gb-bg2);
|
|
color: var(--text-muted);
|
|
padding: 0.1rem 0.4rem;
|
|
font-size: 0.65rem;
|
|
font-style: italic;
|
|
border-radius: 0;
|
|
}
|
|
|
|
.ready-host-action {
|
|
padding-top: 0.25rem;
|
|
}
|
|
|
|
.ready-action-text {
|
|
color: var(--text-muted);
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
}
|
|
|
|
.empty-ready-hosts {
|
|
min-height: 180px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 1px solid var(--gb-bg1);
|
|
background-color: var(--gb-bg);
|
|
}
|
|
|
|
.empty-icon {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 0.75rem;
|
|
display: block;
|
|
opacity: 0.35;
|
|
}
|
|
|
|
/* Performance */
|
|
.performance-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.performance-card {
|
|
background-color: var(--gb-bg);
|
|
padding: 1.25rem;
|
|
border-left: 2px solid var(--gb-purple);
|
|
}
|
|
|
|
.performance-card h4 {
|
|
color: var(--text-muted);
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.performance-value {
|
|
color: var(--gb-blue-b);
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
display: block;
|
|
}
|
|
|
|
.performance-label {
|
|
color: var(--text-dim);
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* Concurrency */
|
|
.concurrency-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.concurrency-card {
|
|
background-color: var(--gb-bg);
|
|
padding: 1rem;
|
|
text-align: left;
|
|
border-left: 2px solid var(--gb-bg3);
|
|
}
|
|
|
|
.concurrency-card h4 {
|
|
color: var(--text-muted);
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.usage-fraction {
|
|
color: var(--gb-blue-b);
|
|
font-weight: 700;
|
|
font-size: 1rem;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
.usage-bar {
|
|
background-color: var(--gb-bg1);
|
|
height: 8px;
|
|
margin: 0.35rem 0;
|
|
border: 1px solid var(--gb-bg2);
|
|
}
|
|
|
|
.usage-fill {
|
|
background-color: var(--gb-blue);
|
|
height: 100%;
|
|
}
|
|
|
|
/* No targets guidance */
|
|
.no-targets-guidance {
|
|
padding: 2rem;
|
|
background-color: var(--gb-bg);
|
|
border: 1px solid var(--gb-bg1);
|
|
border-left: 3px solid var(--gb-blue);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.guidance-icon {
|
|
display: none;
|
|
}
|
|
|
|
.guidance-content h2,
|
|
.guidance-content h3 {
|
|
color: var(--gb-blue-b);
|
|
margin-bottom: 0.5rem;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.guidance-content p {
|
|
color: var(--text-secondary);
|
|
margin-bottom: 1rem;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.guidance-actions {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.guidance-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
.tasks-table-container {
|
|
border: 1px solid var(--gb-bg1);
|
|
overflow: hidden;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
.discovery-grid,
|
|
.performance-grid,
|
|
.concurrency-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.ready-hosts-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.guidance-actions {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
{% block title %}Dashboard{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="dashboard">
|
|
<!-- Enhanced Dashboard Header -->
|
|
<div class="dashboard-header">
|
|
<h1 class="dashboard-title">ROFMAD Dashboard</h1>
|
|
<p class="dashboard-subtitle">Real-time reconnaissance and vulnerability assessment overview</p>
|
|
</div>
|
|
|
|
<!-- Main Statistics Grid -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<h3>Total Hosts</h3>
|
|
<span class="stat-number">{{ stats.total_hosts }}</span>
|
|
<span class="stat-description">Discovered targets</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Total Subnets</h3>
|
|
<span class="stat-number">{{ stats.total_subnets }}</span>
|
|
<span class="stat-description">Network segments</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Active Tasks</h3>
|
|
<span class="stat-number" id="active-count">{{ stats.active_tasks.len() }}</span>
|
|
<span class="stat-description">Currently running</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Queued Tasks</h3>
|
|
<span class="stat-number" id="queued-count">{{ stats.queued_tasks_count }}</span>
|
|
<span class="stat-description">Pending execution</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No Targets Guidance Box (Only shown when no targets exist) -->
|
|
{% if stats.total_hosts == 0 %}
|
|
<div class="no-targets-guidance">
|
|
<div class="guidance-content">
|
|
<h2>Welcome to ROFMAD</h2>
|
|
<p>No targets have been discovered yet. Get started by adding targets or configuring network scanning.</p>
|
|
<div class="guidance-actions">
|
|
<a href="/scanners" class="btn btn-primary guidance-btn">Go to Scanners</a>
|
|
<a href="/settings" class="btn btn-secondary guidance-btn">Configure Settings</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Discovery Statistics Section -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<h2 class="section-title">
|
|
Discovery Statistics
|
|
</h2>
|
|
<p class="section-subtitle">Overview of reconnaissance progress and findings</p>
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="discovery-grid">
|
|
<div class="discovery-card">
|
|
<h4>Hosts with Open Ports</h4>
|
|
<div class="discovery-info">
|
|
<span class="discovery-value">{{ stats.hosts_with_open_ports }}</span>
|
|
<span class="discovery-detail">{{ stats.total_open_ports }} total ports discovered</span>
|
|
</div>
|
|
</div>
|
|
<div class="discovery-card">
|
|
<h4>Hosts with HTTP Services Found</h4>
|
|
<div class="discovery-info">
|
|
<span class="discovery-value">{{ stats.hosts_with_http_services }}</span>
|
|
<span class="discovery-detail">Http services found by probbing</span>
|
|
</div>
|
|
</div>
|
|
<div class="discovery-card">
|
|
<h4>Screenshots Captured</h4>
|
|
<div class="discovery-info">
|
|
<span class="discovery-value">{{ stats.total_screenshots }}</span>
|
|
<span class="discovery-detail">web interface captures</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hosts Ready for Analysis Section -->
|
|
<div class="section-card ready-analysis-section">
|
|
<div class="section-header">
|
|
<h2 class="section-title">
|
|
Hosts Ready for Analysis
|
|
</h2>
|
|
<p class="section-subtitle">Targets that have completed all reconnaissance phases and are ready for manual
|
|
security assessment</p>
|
|
</div>
|
|
<div class="section-content">
|
|
{% if stats.hosts_ready_for_analysis.len() > 0 %}
|
|
<div class="ready-hosts-grid hosts-ready-analysis">
|
|
{% for host in stats.hosts_ready_for_analysis %}
|
|
<div class="ready-host-card" data-host-ip="{{ host.ip }}"
|
|
onclick="openHostForAnalysis('{{ host.ip }}')">
|
|
<div class="ready-host-header">
|
|
<h4>{{ host.ip }}</h4>
|
|
{% if host.hostname != "" %}
|
|
<span class="ready-host-hostname">{{ host.hostname }}</span>
|
|
{% endif %}
|
|
</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">
|
|
{% for tag in host.tags_with_source %}
|
|
{% if loop.index <= 6 %} <span class="ready-tag tag-{{ tag.source }}">{{ tag.name }}</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% if host.tags_with_source.len() > 6 %}
|
|
<span class="ready-tag-more">+{{ host.tags_with_source.len() - 6 }} more</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="ready-host-action">
|
|
<span class="ready-action-text">Analyze</span>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-ready-hosts hosts-ready-analysis">
|
|
<div class="empty-placeholder">
|
|
<h3>Scanning in progress</h3>
|
|
<p>Hosts will appear here once all scanning phases and workflow actions are complete.</p>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nuclei Findings Section — always visible, updated by JS polling -->
|
|
<div class="section-card nuclei-findings-section">
|
|
<div class="section-header">
|
|
<h2 class="section-title">Nuclei Findings</h2>
|
|
<p class="section-subtitle">Hosts with nuclei vulnerability scan results ready for review</p>
|
|
</div>
|
|
<div class="section-content">
|
|
{% if stats.hosts_with_nuclei_findings.len() > 0 %}
|
|
<div class="ready-hosts-grid hosts-nuclei-findings">
|
|
{% for host in stats.hosts_with_nuclei_findings %}
|
|
<div class="ready-host-card" data-host-ip="{{ host.ip }}"
|
|
onclick="openNucleiHost('{{ host.ip }}')">
|
|
<div class="ready-host-header">
|
|
<h4>{{ host.ip }}</h4>
|
|
{% if host.hostname != "" %}
|
|
<span class="ready-host-hostname">{{ host.hostname }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="ready-host-stats nuclei-severity-stats">
|
|
{% if host.nuclei_severity_counts.critical > 0 %}
|
|
<div class="ready-stat severity-critical">
|
|
<span class="ready-stat-number">{{ host.nuclei_severity_counts.critical }}</span>
|
|
<span class="ready-stat-label">Critical</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if host.nuclei_severity_counts.high > 0 %}
|
|
<div class="ready-stat severity-high">
|
|
<span class="ready-stat-number">{{ host.nuclei_severity_counts.high }}</span>
|
|
<span class="ready-stat-label">High</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if host.nuclei_severity_counts.medium > 0 %}
|
|
<div class="ready-stat severity-medium">
|
|
<span class="ready-stat-number">{{ host.nuclei_severity_counts.medium }}</span>
|
|
<span class="ready-stat-label">Medium</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if host.nuclei_severity_counts.low > 0 %}
|
|
<div class="ready-stat severity-low">
|
|
<span class="ready-stat-number">{{ host.nuclei_severity_counts.low }}</span>
|
|
<span class="ready-stat-label">Low</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if host.nuclei_severity_counts.info > 0 %}
|
|
<div class="ready-stat severity-info">
|
|
<span class="ready-stat-number">{{ host.nuclei_severity_counts.info }}</span>
|
|
<span class="ready-stat-label">Info</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if host.nuclei_severity_counts.unknown > 0 %}
|
|
<div class="ready-stat severity-unknown">
|
|
<span class="ready-stat-number">{{ host.nuclei_severity_counts.unknown }}</span>
|
|
<span class="ready-stat-label">Unknown</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="ready-host-action">
|
|
<span class="ready-action-text">View Findings</span>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-ready-hosts hosts-nuclei-findings">
|
|
<div class="empty-placeholder">
|
|
<h3>No Nuclei Findings</h3>
|
|
<p>Use the "Run Nuclei" button on any HTTP service to scan for vulnerabilities.</p>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active Tasks Section -->
|
|
{% if stats.active_tasks.len() > 0 %}
|
|
<div class="section-card" id="active-tasks-section">
|
|
<div class="section-header">
|
|
<h2 class="section-title">
|
|
Active Tasks
|
|
</h2>
|
|
<p class="section-subtitle">Real-time monitoring of running reconnaissance tasks and their progress</p>
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="tasks-table-container">
|
|
<table class="tasks-table" id="active-tasks-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Task</th>
|
|
<th>Target</th>
|
|
<th>Duration</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for task in stats.active_tasks %}
|
|
<tr>
|
|
<td class="task-name">{{ task.name }}</td>
|
|
<td class="task-target">{{ task.target }}</td>
|
|
<td class="task-duration">{{ task.duration }}</td>
|
|
<td class="task-actions">
|
|
<a href="/task/{{ task.id }}" class="btn btn-detail btn-small">Detail</a>
|
|
<button class="btn btn-danger btn-small"
|
|
onclick="killTask('{{ task.id }}', '{{ task.name }}')">Kill</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- System Performance Section -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<h2 class="section-title">
|
|
System Performance
|
|
</h2>
|
|
<p class="section-subtitle">Monitor system resources and task execution efficiency</p>
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="performance-grid">
|
|
<div class="performance-card">
|
|
<h4>Task Queue</h4>
|
|
<div class="performance-info">
|
|
<span class="performance-value">{{ stats.active_tasks.len() }}/{{ stats.queued_tasks_count
|
|
}}</span>
|
|
<span class="performance-label">Active/Queued</span>
|
|
</div>
|
|
</div>
|
|
<div class="performance-card">
|
|
<h4>Discovery Progress</h4>
|
|
<div class="performance-info">
|
|
<span class="performance-value">{{ stats.hosts_with_open_ports }}/{{ stats.total_hosts }}</span>
|
|
<span class="performance-label">Hosts Scanned</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Concurrency Management Section -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<h2 class="section-title">
|
|
Concurrency Status
|
|
</h2>
|
|
<p class="section-subtitle">Real-time semaphore usage and resource allocation</p>
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="concurrency-grid">
|
|
{% for group in stats.concurrency_stats.display_groups %}
|
|
<div class="concurrency-card">
|
|
<h4>{{ group.name }}</h4>
|
|
<div class="concurrency-info">
|
|
<div class="concurrency-usage">
|
|
<span class="usage-fraction">{{ group.in_use }}/{{ group.total_limit }}</span>
|
|
<div class="usage-bar">
|
|
<div class="usage-fill"
|
|
style="width: {% if group.total_limit > 0 %}{{ (group.in_use * 100 / group.total_limit) }}{% else %}0{% endif %}%">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Kill task function
|
|
async function killTask(taskId, taskName) {
|
|
if (!confirm(`Kill task "${taskName}"?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/kill-task/${taskId}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
if (window.ROFMAD?.showNotification) {
|
|
window.ROFMAD.showNotification(data.message, 'success');
|
|
}
|
|
// Task will disappear on next refresh cycle
|
|
} else {
|
|
alert('Failed to kill task: ' + (data.message || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error killing task:', error);
|
|
alert('Failed to kill task: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Dashboard refresh functionality
|
|
async function updateDashboardContent() {
|
|
if (document.hidden || window.location.pathname !== '/') return;
|
|
|
|
try {
|
|
// Fetch all dashboard data
|
|
const [taskResponse, analysisResponse, concurrencyResponse, activeTasksResponse, discoveryResponse, nucleiResponse] = await Promise.all([
|
|
fetch('/api/task-status'),
|
|
fetch('/api/hosts-ready-analysis'),
|
|
fetch('/api/concurrency-status'),
|
|
fetch('/api/active-tasks'),
|
|
fetch('/api/discovery-stats'),
|
|
fetch('/api/nuclei-findings')
|
|
]);
|
|
|
|
if (taskResponse.ok) {
|
|
const taskData = await taskResponse.json();
|
|
updateTaskCounters(taskData);
|
|
}
|
|
|
|
if (analysisResponse.ok) {
|
|
const analysisData = await analysisResponse.json();
|
|
updateReadyForAnalysisSection(analysisData.hosts);
|
|
}
|
|
|
|
if (concurrencyResponse.ok) {
|
|
const concurrencyData = await concurrencyResponse.json();
|
|
updateConcurrencyDisplay(concurrencyData);
|
|
}
|
|
|
|
if (activeTasksResponse.ok) {
|
|
const activeTasksData = await activeTasksResponse.json();
|
|
updateActiveTasksTable(activeTasksData.tasks);
|
|
}
|
|
|
|
if (discoveryResponse.ok) {
|
|
const discoveryData = await discoveryResponse.json();
|
|
updateDiscoveryStatistics(discoveryData);
|
|
}
|
|
|
|
if (nucleiResponse.ok) {
|
|
const nucleiData = await nucleiResponse.json();
|
|
updateNucleiFindingsSection(nucleiData.hosts);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Dashboard update failed:', error);
|
|
}
|
|
}
|
|
|
|
function updateTaskCounters(data) {
|
|
const activeCountEl = document.getElementById('active-count');
|
|
const queuedCountEl = document.getElementById('queued-count');
|
|
|
|
if (activeCountEl) activeCountEl.textContent = data.active_count;
|
|
if (queuedCountEl) queuedCountEl.textContent = data.queued_count;
|
|
}
|
|
|
|
function updateReadyForAnalysisSection(hosts) {
|
|
const container = document.querySelector('.hosts-ready-analysis');
|
|
if (!container) return;
|
|
|
|
if (hosts.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-placeholder">
|
|
<h3>Scanning in Progress</h3>
|
|
<p>Hosts will appear here once all scanning phases and workflow actions are complete.</p>
|
|
</div>
|
|
`;
|
|
container.className = 'empty-ready-hosts hosts-ready-analysis';
|
|
return;
|
|
}
|
|
|
|
container.className = 'ready-hosts-grid hosts-ready-analysis';
|
|
|
|
// Skip full re-render if the same hosts are already displayed — prevents
|
|
// the hover transition from replaying every 2s due to innerHTML replacement.
|
|
const existingCards = container.querySelectorAll('.ready-host-card');
|
|
const existingIPs = new Set([...existingCards].map(c => c.dataset.hostIp));
|
|
const newIPs = new Set(hosts.map(h => h.ip));
|
|
if (existingIPs.size === newIPs.size && [...newIPs].every(ip => existingIPs.has(ip))) {
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = 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('');
|
|
}
|
|
|
|
function updateNucleiFindingsSection(hosts) {
|
|
const container = document.querySelector('.hosts-nuclei-findings');
|
|
if (!container) return;
|
|
|
|
if (hosts.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-placeholder">
|
|
<h3>No Nuclei Findings</h3>
|
|
<p>Use the "Run Nuclei" button on any HTTP service to scan for vulnerabilities.</p>
|
|
</div>`;
|
|
container.className = 'empty-ready-hosts hosts-nuclei-findings';
|
|
return;
|
|
}
|
|
|
|
container.className = 'ready-hosts-grid hosts-nuclei-findings';
|
|
|
|
// Skip re-render if same hosts already displayed — prevents hover transition flicker
|
|
const existingCards = container.querySelectorAll('.ready-host-card');
|
|
const existingIPs = new Set([...existingCards].map(c => c.dataset.hostIp));
|
|
const newIPs = new Set(hosts.map(h => h.ip));
|
|
if (existingIPs.size === newIPs.size && [...newIPs].every(ip => existingIPs.has(ip))) return;
|
|
|
|
container.innerHTML = hosts.map(host => `
|
|
<div class="ready-host-card" data-host-ip="${host.ip}"
|
|
onclick="openNucleiHost('${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 nuclei-severity-stats">
|
|
${['critical', 'high', 'medium', 'low', 'info', 'unknown']
|
|
.filter(sev => (host.nuclei_severity_counts?.[sev] || 0) > 0)
|
|
.map(sev => `
|
|
<div class="ready-stat severity-${sev}">
|
|
<span class="ready-stat-number">${host.nuclei_severity_counts[sev]}</span>
|
|
<span class="ready-stat-label">${sev.charAt(0).toUpperCase() + sev.slice(1)}</span>
|
|
</div>`).join('')}
|
|
</div>
|
|
<div class="ready-host-action"><span class="ready-action-text">View Findings</span></div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function updateConcurrencyDisplay(data) {
|
|
// Update using existing DOM structure from dashboard.html
|
|
(data.display_groups || []).forEach(group => {
|
|
const cards = document.querySelectorAll('.concurrency-card');
|
|
cards.forEach(card => {
|
|
const h4 = card.querySelector('h4');
|
|
if (h4 && h4.textContent.trim() === group.name) {
|
|
const fraction = card.querySelector('.usage-fraction');
|
|
const fill = card.querySelector('.usage-fill');
|
|
|
|
if (fraction) fraction.textContent = `${group.in_use}/${group.total_limit}`;
|
|
if (fill) {
|
|
const percentage = group.total_limit > 0 ? (group.in_use * 100 / group.total_limit) : 0;
|
|
fill.style.width = `${percentage}%`;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function updateActiveTasksTable(tasks) {
|
|
const activeSection = document.getElementById('active-tasks-section');
|
|
if (!activeSection) return;
|
|
|
|
const tbody = activeSection.querySelector('tbody');
|
|
if (!tbody) return;
|
|
|
|
if (tasks.length > 0) {
|
|
// Show the section and update table
|
|
activeSection.style.display = 'block';
|
|
tbody.innerHTML = tasks.map(task => `
|
|
<tr>
|
|
<td class="task-name">${task.name}</td>
|
|
<td class="task-target">${task.target}</td>
|
|
<td class="task-duration">${task.duration}</td>
|
|
<td class="task-actions">
|
|
<a href="/task/${task.id}" class="btn btn-detail btn-small">Detail</a>
|
|
<button class="btn btn-danger btn-small" onclick="killTask('${task.id}', '${task.name}')">Kill</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} else {
|
|
// Hide section when no active tasks (dashboard conditionally shows this section)
|
|
activeSection.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function updateDiscoveryStatistics(data) {
|
|
// Update Discovery Statistics section
|
|
const hostsWithPortsValue = document.querySelector('.discovery-card:nth-child(1) .discovery-value');
|
|
const hostsWithPortsDetail = document.querySelector('.discovery-card:nth-child(1) .discovery-detail');
|
|
const httpServicesValue = document.querySelector('.discovery-card:nth-child(2) .discovery-value');
|
|
const screenshotsValue = document.querySelector('.discovery-card:nth-child(3) .discovery-value');
|
|
|
|
if (hostsWithPortsValue) hostsWithPortsValue.textContent = data.hosts_with_open_ports;
|
|
if (hostsWithPortsDetail) hostsWithPortsDetail.textContent = `${data.total_open_ports} total ports discovered`;
|
|
if (httpServicesValue) httpServicesValue.textContent = data.hosts_with_http_services;
|
|
if (screenshotsValue) screenshotsValue.textContent = data.total_screenshots;
|
|
|
|
// Update placeholder stats if empty state is showing
|
|
const totalHostsStat = document.getElementById('total-hosts-stat');
|
|
const activeTasksStat = document.getElementById('active-tasks-stat');
|
|
|
|
if (totalHostsStat) totalHostsStat.textContent = data.total_hosts;
|
|
// Note: active-tasks-stat is updated by updateTaskCounters from /api/task-status
|
|
}
|
|
|
|
// Enhanced JavaScript functionality
|
|
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';
|
|
|
|
// Open host detail in new tab
|
|
window.open(`/hosts/${ip}`, '_blank');
|
|
|
|
// Mark this host as analyzed (remove from ready list)
|
|
fetch('/mark-analyzed', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `ip=${encodeURIComponent(ip)}&analyzed=true`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Remove the card with animation
|
|
hostCard.style.transition = 'all 0.5s ease';
|
|
hostCard.style.transform = 'translateX(100%)';
|
|
hostCard.style.opacity = '0';
|
|
|
|
setTimeout(() => {
|
|
hostCard.remove();
|
|
}, 500);
|
|
|
|
if (window.ROFMAD?.showNotification) {
|
|
window.ROFMAD.showNotification(`Host ${ip} marked as analyzed`, 'success');
|
|
}
|
|
} else {
|
|
// Restore on failure
|
|
hostCard.disabled = false;
|
|
hostCard.style.opacity = '1';
|
|
hostCard.style.pointerEvents = 'auto';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error marking host as analyzed:', error);
|
|
// Restore on error
|
|
hostCard.disabled = false;
|
|
hostCard.style.opacity = '1';
|
|
hostCard.style.pointerEvents = 'auto';
|
|
|
|
if (window.ROFMAD?.showNotification) {
|
|
window.ROFMAD.showNotification('Failed to mark host as analyzed', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
function openNucleiHost(ip) {
|
|
const card = event.currentTarget;
|
|
if (card.disabled) return;
|
|
card.disabled = true;
|
|
card.style.opacity = '0.7';
|
|
card.style.pointerEvents = 'none';
|
|
|
|
fetch('/mark-nuclei-reviewed', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: `ip=${encodeURIComponent(ip)}&nuclei_reviewed=true`
|
|
}).finally(() => {
|
|
window.open(`/hosts/${ip}`, '_blank');
|
|
});
|
|
}
|
|
|
|
let dashboardRefreshInterval;
|
|
|
|
function startDashboardRefresh() {
|
|
// Initial update
|
|
updateDashboardContent();
|
|
|
|
// Set up 2-second interval
|
|
dashboardRefreshInterval = setInterval(updateDashboardContent, 2000);
|
|
}
|
|
|
|
function stopDashboardRefresh() {
|
|
if (dashboardRefreshInterval) {
|
|
clearInterval(dashboardRefreshInterval);
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Enhanced statistics animation
|
|
const statNumbers = document.querySelectorAll('.stat-number');
|
|
statNumbers.forEach((stat, index) => {
|
|
const finalValue = parseInt(stat.textContent);
|
|
if (finalValue > 0) {
|
|
stat.textContent = '0';
|
|
|
|
setTimeout(() => {
|
|
let current = 0;
|
|
const increment = Math.max(1, finalValue / 20);
|
|
const timer = setInterval(() => {
|
|
current += increment;
|
|
if (current >= finalValue) {
|
|
stat.textContent = finalValue;
|
|
clearInterval(timer);
|
|
} else {
|
|
stat.textContent = Math.floor(current);
|
|
}
|
|
}, 50);
|
|
}, index * 200);
|
|
}
|
|
});
|
|
|
|
// Enhanced hover effects for discovery cards
|
|
// Discovery card hover is handled by CSS
|
|
|
|
// Add keyboard shortcuts
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.altKey) {
|
|
switch (e.key) {
|
|
case 'd':
|
|
e.preventDefault();
|
|
window.location.href = '/';
|
|
break;
|
|
case 'h':
|
|
e.preventDefault();
|
|
window.location.href = '/hosts';
|
|
break;
|
|
case 's':
|
|
e.preventDefault();
|
|
window.location.href = '/search';
|
|
break;
|
|
case 't':
|
|
e.preventDefault();
|
|
window.location.href = '/tags';
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add tooltips for stats
|
|
const statCards = document.querySelectorAll('.stat-card');
|
|
statCards.forEach(card => {
|
|
card.addEventListener('mouseenter', function () {
|
|
const tooltip = document.createElement('div');
|
|
tooltip.className = 'tooltip';
|
|
tooltip.style.cssText = `
|
|
position: absolute;
|
|
background: var(--gb-bg1);
|
|
color: var(--gb-fg);
|
|
padding: 0.4rem 0.75rem;
|
|
border-radius: 0;
|
|
font-size: 0.78rem;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
z-index: 1000;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
border: 1px solid var(--gb-bg2);
|
|
`;
|
|
|
|
const title = this.querySelector('h3').textContent;
|
|
const value = this.querySelector('.stat-number').textContent;
|
|
const description = this.querySelector('.stat-description').textContent;
|
|
|
|
tooltip.innerHTML = `<strong>${title}:</strong> ${value}<br><em>${description}</em>`;
|
|
|
|
document.body.appendChild(tooltip);
|
|
|
|
const rect = this.getBoundingClientRect();
|
|
tooltip.style.left = (rect.left + rect.width / 2 - tooltip.offsetWidth / 2) + 'px';
|
|
tooltip.style.top = (rect.top - tooltip.offsetHeight - 10) + 'px';
|
|
|
|
setTimeout(() => tooltip.style.opacity = '1', 10);
|
|
|
|
this.addEventListener('mouseleave', () => {
|
|
tooltip.style.opacity = '0';
|
|
setTimeout(() => tooltip.remove(), 300);
|
|
}, { once: true });
|
|
});
|
|
});
|
|
|
|
// Ready host card hover is handled by CSS
|
|
|
|
// Start real-time updates
|
|
startDashboardRefresh();
|
|
});
|
|
|
|
// Stop refresh when page unloads
|
|
window.addEventListener('beforeunload', stopDashboardRefresh);
|
|
</script>
|
|
{% endblock %} |