mirror of
https://github.com/mblanke/StrikePackageGPT.git
synced 2026-03-01 14:20:21 -05:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user