diff --git a/backend/app/scanner/nmap_scan.py b/backend/app/scanner/nmap_scan.py
index 59e220b..f0ec71d 100644
--- a/backend/app/scanner/nmap_scan.py
+++ b/backend/app/scanner/nmap_scan.py
@@ -56,6 +56,7 @@ def run_nmap_scan(
return []
iface = interface or os.environ.get("NETWORK_INTERFACE", "eth0")
+ host_timeout = os.environ.get("NMAP_HOST_TIMEOUT", "30s")
with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tmp:
xml_path = tmp.name
@@ -67,6 +68,8 @@ def run_nmap_scan(
# -O (OS detection) intentionally omitted: nmap hard-codes geteuid()==0
# for OS fingerprinting regardless of file capabilities, so it always
# quits when running as non-root uid 1000. os_guess will be empty.
+ "--host-timeout",
+ host_timeout,
"--top-ports",
"1000",
"-e",
diff --git a/backend/tests/test_scanner.py b/backend/tests/test_scanner.py
index 6fe0a23..cb5f78b 100644
--- a/backend/tests/test_scanner.py
+++ b/backend/tests/test_scanner.py
@@ -454,3 +454,37 @@ def test_uses_env_vars_for_interface_and_subnet(self, monkeypatch):
orchestrate_scan()
mock_arp.assert_called_once_with(interface="bond0", subnet="172.16.0.0/12")
+
+ @pytest.mark.unit
+ def test_host_timeout_default_in_command(self):
+ """--host-timeout 30s must appear in the nmap command by default."""
+ fixture_xml = _fixture("nmap_two_hosts.xml")
+
+ def fake_run(cmd, **kwargs):
+ idx = cmd.index("-oX")
+ Path(cmd[idx + 1]).write_text(fixture_xml)
+ return MagicMock(returncode=0, stdout="", stderr="")
+
+ with patch("app.scanner.nmap_scan.subprocess.run", side_effect=fake_run) as mock_run:
+ run_nmap_scan(hosts=["192.168.1.1"], interface="eth0")
+
+ cmd = mock_run.call_args[0][0]
+ assert "--host-timeout" in cmd
+ assert cmd[cmd.index("--host-timeout") + 1] == "30s"
+
+ @pytest.mark.unit
+ def test_host_timeout_env_override(self, monkeypatch):
+ """NMAP_HOST_TIMEOUT env var must override the default 30s value."""
+ monkeypatch.setenv("NMAP_HOST_TIMEOUT", "60s")
+ fixture_xml = _fixture("nmap_two_hosts.xml")
+
+ def fake_run(cmd, **kwargs):
+ idx = cmd.index("-oX")
+ Path(cmd[idx + 1]).write_text(fixture_xml)
+ return MagicMock(returncode=0, stdout="", stderr="")
+
+ with patch("app.scanner.nmap_scan.subprocess.run", side_effect=fake_run) as mock_run:
+ run_nmap_scan(hosts=["192.168.1.1"], interface="eth0")
+
+ cmd = mock_run.call_args[0][0]
+ assert cmd[cmd.index("--host-timeout") + 1] == "60s"
diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx
index 905abca..9acafcb 100644
--- a/frontend/src/pages/DashboardPage.tsx
+++ b/frontend/src/pages/DashboardPage.tsx
@@ -159,6 +159,7 @@ export function DashboardPage() {
const lastScan = scans[0] ?? null;
const loading = devLoading || scanLoading || summaryLoading;
+ const noScansYet = !loading && scans.length === 0;
const handleTrigger = async () => {
const result = await trigger();
@@ -199,6 +200,27 @@ export function DashboardPage() {
+ Discover every device on your LAN, identify misconfigurations, and
+ get actionable hardening advice — all locally, with no data leaving
+ your network.
+
+ Welcome to NetworkCrawler
+
+