import os
import subprocess
import uuid
import datetime
import errno
import platform
from typing import List, Any, Union, Tuple
try:
import psutil
except ImportError:
psutil = None
def run_nmap(target: str, args: str) -> Tuple[str, int]:
"""
Launches nmap against `target` with optional `args`,
writing normal output to a timestamped file, and returns
the file path plus the scan process PID.
"""
executable = "nmap.exe" if os.name == 'nt' else "nmap"
output_dir = os.path.join(os.getcwd(), "nmap_output")
os.makedirs(output_dir, exist_ok=True)
ts = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
uid = uuid.uuid4().hex
filename = f"{ts}_{uid}"
output_path = os.path.join(output_dir, filename)
args_list = args.split() if args else []
cmd = [executable, "-oN", output_path] + args_list + [target]
if os.name == 'nt':
proc = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
)
else:
proc = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
pid = proc.pid
if psutil and platform.system() == "Windows":
try:
parent = psutil.Process(proc.pid)
for child in parent.children(recursive=True):
if child.name().lower().startswith("nmap"):
pid = child.pid
break
except Exception:
pass
return output_path, pid
def _is_process_running(pid: int) -> bool:
if psutil and os.name != 'nt':
try:
p = psutil.Process(pid)
if p.status() == psutil.STATUS_ZOMBIE:
try:
os.waitpid(pid, os.WNOHANG)
except (ChildProcessError, OSError):
pass
return False
except psutil.NoSuchProcess:
return False
except Exception:
pass
try:
os.kill(pid, 0)
except OSError as e:
if getattr(e, 'errno', None) == errno.ESRCH:
return False
if getattr(e, 'errno', None) == errno.EPERM:
return True
return False
except ValueError:
return True
return True
def get_nmap_output(file_path: str, pid: int) -> Union[str, List[str]]:
"""
If the scan is still running (process exists or file incomplete),
returns a “still running” message. Once finished, reads the file
and returns a list of lines.
"""
running = _is_process_running(pid)
if not os.path.isfile(file_path):
if running:
return f"Scan running with PID {pid}"
raise FileNotFoundError(f"Output file not found (scan PID {pid} has exited): {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
tail = lines[-10:] if len(lines) >= 10 else lines
if not any("Nmap done" in line for line in tail):
return f"Scan still in progress (waiting for Nmap to finish)... PID {pid}"
return lines