feat: Add real-time progress tracking for network scans

- Progress bar shows X/255 (or calculated range size)
- Percentage display during scan
- Currently scanning IP shown in real-time
- Live host count as hosts are discovered
- Remaining hosts counter
- Faster polling (2s instead of 5s) for smoother updates
- Backend calculates total hosts from CIDR or range notation
- Parse nmap --stats-every output for progress info
This commit is contained in:
2025-11-28 15:32:57 -05:00
parent 304d223a49
commit e51e52129f
2 changed files with 139 additions and 16 deletions

View File

@@ -395,12 +395,16 @@ async def start_network_scan(request: NetworkScanRequest):
import uuid import uuid
scan_id = str(uuid.uuid4())[:8] scan_id = str(uuid.uuid4())[:8]
# Calculate total hosts in target range
total_hosts = calculate_target_hosts(request.target)
# Build nmap command based on scan type # Build nmap command based on scan type
# Use --stats-every to get progress updates
scan_commands = { scan_commands = {
"ping": f"nmap -sn {request.target} -oX -", "ping": f"nmap -sn {request.target} -oX - --stats-every 2s",
"quick": f"nmap -T4 -F -O --osscan-limit {request.target} -oX -", "quick": f"nmap -T4 -F -O --osscan-limit {request.target} -oX - --stats-every 2s",
"os": f"nmap -O -sV --version-light {request.target} -oX -", "os": f"nmap -O -sV --version-light {request.target} -oX - --stats-every 2s",
"full": f"nmap -sS -sV -O -p- --version-all {request.target} -oX -" "full": f"nmap -sS -sV -O -p- --version-all {request.target} -oX - --stats-every 2s"
} }
command = scan_commands.get(request.scan_type, scan_commands["os"]) command = scan_commands.get(request.scan_type, scan_commands["os"])
@@ -411,21 +415,57 @@ async def start_network_scan(request: NetworkScanRequest):
"scan_type": request.scan_type, "scan_type": request.scan_type,
"status": "running", "status": "running",
"hosts": [], "hosts": [],
"command": command "command": command,
"progress": {
"total": total_hosts,
"scanned": 0,
"current_ip": "",
"hosts_found": 0,
"percent": 0
}
} }
# Execute scan asynchronously # Execute scan asynchronously with progress tracking
import asyncio import asyncio
asyncio.create_task(execute_network_scan(scan_id, command)) asyncio.create_task(execute_network_scan_with_progress(scan_id, command, request.target))
return {"scan_id": scan_id, "status": "running"} return {"scan_id": scan_id, "status": "running", "total_hosts": total_hosts}
async def execute_network_scan(scan_id: str, command: str): def calculate_target_hosts(target: str) -> int:
"""Execute network scan and parse results""" """Calculate the number of hosts in a target specification"""
import ipaddress
# Handle CIDR notation
if '/' in target:
try:
network = ipaddress.ip_network(target, strict=False)
return network.num_addresses - 2 # Subtract network and broadcast
except ValueError:
pass
# Handle range notation (e.g., 192.168.1.1-50)
if '-' in target:
try:
parts = target.rsplit('.', 1)
if len(parts) == 2:
range_part = parts[1]
if '-' in range_part:
start, end = range_part.split('-')
return int(end) - int(start) + 1
except (ValueError, IndexError):
pass
# Single host
return 1
async def execute_network_scan_with_progress(scan_id: str, command: str, target: str):
"""Execute network scan with progress tracking"""
global network_hosts global network_hosts
try: try:
# Use streaming execution if available, otherwise batch
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
response = await client.post( response = await client.post(
f"{HACKGPT_API_URL}/execute", f"{HACKGPT_API_URL}/execute",
@@ -437,11 +477,19 @@ async def execute_network_scan(scan_id: str, command: str):
result = response.json() result = response.json()
stdout = result.get("stdout", "") stdout = result.get("stdout", "")
# Parse nmap XML output # Parse progress from nmap stats output
progress_info = parse_nmap_progress(stdout)
if progress_info:
network_scans[scan_id]["progress"].update(progress_info)
# Parse nmap XML output for hosts
hosts = parse_nmap_xml(stdout) hosts = parse_nmap_xml(stdout)
network_scans[scan_id]["status"] = "completed" network_scans[scan_id]["status"] = "completed"
network_scans[scan_id]["hosts"] = hosts network_scans[scan_id]["hosts"] = hosts
network_scans[scan_id]["progress"]["scanned"] = network_scans[scan_id]["progress"]["total"]
network_scans[scan_id]["progress"]["hosts_found"] = len(hosts)
network_scans[scan_id]["progress"]["percent"] = 100
# Update global host list # Update global host list
for host in hosts: for host in hosts:
@@ -459,6 +507,34 @@ async def execute_network_scan(scan_id: str, command: str):
network_scans[scan_id]["error"] = str(e) network_scans[scan_id]["error"] = str(e)
def parse_nmap_progress(output: str) -> dict:
"""Parse nmap stats output for progress information"""
import re
progress = {}
# Look for stats lines like: "Stats: 0:00:45 elapsed; 50 hosts completed (10 up), 5 undergoing..."
stats_pattern = r'Stats:.*?(\d+)\s+hosts?\s+completed.*?(\d+)\s+up'
match = re.search(stats_pattern, output, re.IGNORECASE)
if match:
progress['scanned'] = int(match.group(1))
progress['hosts_found'] = int(match.group(2))
# Look for percentage: "About 45.00% done"
percent_pattern = r'About\s+([\d.]+)%\s+done'
match = re.search(percent_pattern, output, re.IGNORECASE)
if match:
progress['percent'] = float(match.group(1))
# Look for current scan target
current_pattern = r'Scanning\s+([^\s\[]+)'
match = re.search(current_pattern, output)
if match:
progress['current_ip'] = match.group(1)
return progress
def parse_nmap_xml(xml_output: str) -> List[Dict[str, Any]]: def parse_nmap_xml(xml_output: str) -> List[Dict[str, Any]]:
"""Parse nmap XML output to extract hosts with OS info""" """Parse nmap XML output to extract hosts with OS info"""
import re import re

View File

@@ -500,9 +500,39 @@
</div> </div>
<div x-show="networkMapLoading" class="flex-1 flex items-center justify-center"> <div x-show="networkMapLoading" class="flex-1 flex items-center justify-center">
<div class="text-center text-sp-white-muted"> <div class="text-center text-sp-white-muted w-96">
<div class="text-4xl mb-4 animate-spin">🔄</div> <div class="text-4xl mb-4 animate-spin">🔄</div>
<p>Scanning network...</p> <p class="text-lg mb-4">Scanning network...</p>
<!-- Progress Bar -->
<div x-show="networkScanProgress.total > 0" class="mb-4">
<div class="flex justify-between text-sm mb-1">
<span x-text="networkScanProgress.current + ' / ' + networkScanProgress.total"></span>
<span x-text="Math.round((networkScanProgress.current / networkScanProgress.total) * 100) + '%'"></span>
</div>
<div class="w-full bg-sp-grey rounded-full h-3 overflow-hidden">
<div class="bg-sp-red h-3 rounded-full transition-all duration-300"
:style="'width: ' + ((networkScanProgress.current / networkScanProgress.total) * 100) + '%'"></div>
</div>
</div>
<!-- Current IP being scanned -->
<div x-show="networkScanProgress.currentIp" class="text-sm">
<p class="text-sp-white-muted">Currently scanning:</p>
<p class="font-mono text-sp-red" x-text="networkScanProgress.currentIp"></p>
</div>
<!-- Live host count -->
<div class="mt-4 flex justify-center gap-6">
<div class="text-center">
<div class="text-2xl font-bold text-green-400" x-text="networkScanProgress.hostsFound || networkHosts.length"></div>
<div class="text-xs text-sp-white-muted">Hosts Found</div>
</div>
<div class="text-center" x-show="networkScanProgress.total > 0">
<div class="text-2xl font-bold text-sp-white" x-text="networkScanProgress.total - networkScanProgress.current"></div>
<div class="text-xs text-sp-white-muted">Remaining</div>
</div>
</div>
</div> </div>
</div> </div>
@@ -861,6 +891,7 @@
networkHosts: [], networkHosts: [],
selectedHost: null, selectedHost: null,
networkMapLoading: false, networkMapLoading: false,
networkScanProgress: { current: 0, total: 0, currentIp: '', hostsFound: 0 },
showRangeScanModal: false, showRangeScanModal: false,
rangeScanTarget: '', rangeScanTarget: '',
rangeScanType: 'os', rangeScanType: 'os',
@@ -1444,6 +1475,7 @@ Select a phase above to begin, or use the quick actions in the sidebar!`
if (!this.rangeScanTarget) return; if (!this.rangeScanTarget) return;
this.showRangeScanModal = false; this.showRangeScanModal = false;
this.networkMapLoading = true; this.networkMapLoading = true;
this.networkScanProgress = { current: 0, total: 0, currentIp: '', hostsFound: 0 };
this.activeTab = 'network-map'; this.activeTab = 'network-map';
try { try {
@@ -1458,6 +1490,10 @@ Select a phase above to begin, or use the quick actions in the sidebar!`
const data = await response.json(); const data = await response.json();
if (data.scan_id) { if (data.scan_id) {
// Set initial progress based on target range
if (data.total_hosts) {
this.networkScanProgress.total = data.total_hosts;
}
// Poll for results // Poll for results
await this.pollNetworkScan(data.scan_id); await this.pollNetworkScan(data.scan_id);
} }
@@ -1470,24 +1506,35 @@ Select a phase above to begin, or use the quick actions in the sidebar!`
}); });
} }
this.networkMapLoading = false; this.networkMapLoading = false;
this.networkScanProgress = { current: 0, total: 0, currentIp: '', hostsFound: 0 };
}, },
async pollNetworkScan(scanId) { async pollNetworkScan(scanId) {
const maxAttempts = 120; // 10 minutes max const maxAttempts = 120; // 10 minutes max
for (let i = 0; i < maxAttempts; i++) { for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 5000)); await new Promise(r => setTimeout(r, 2000)); // Poll every 2 seconds for better progress updates
try { try {
const response = await fetch(`/api/network/scan/${scanId}`); const response = await fetch(`/api/network/scan/${scanId}`);
const data = await response.json(); const data = await response.json();
// Update progress
if (data.progress) {
this.networkScanProgress = {
current: data.progress.scanned || 0,
total: data.progress.total || this.networkScanProgress.total,
currentIp: data.progress.current_ip || '',
hostsFound: data.progress.hosts_found || 0
};
}
if (data.status === 'completed') { if (data.status === 'completed') {
this.networkHosts = data.hosts || []; this.networkHosts = data.hosts || [];
this.$nextTick(() => this.renderNetworkMap()); this.$nextTick(() => this.renderNetworkMap());
return; return;
} else if (data.status === 'failed') { } else if (data.status === 'failed') {
throw new Error('Scan failed'); throw new Error(data.error || 'Scan failed');
} }
// Update partial results // Update partial results as hosts are discovered
if (data.hosts && data.hosts.length > 0) { if (data.hosts && data.hosts.length > 0) {
this.networkHosts = data.hosts; this.networkHosts = data.hosts;
this.$nextTick(() => this.renderNetworkMap()); this.$nextTick(() => this.renderNetworkMap());