diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 1064188..e565273 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -26,6 +26,7 @@ const navLinks = [ { to: "/risks", label: "Risks", end: false }, { to: "/history", label: "History", end: false }, { to: "/recommendations", label: "Recommendations", end: false }, + { to: "/docs", label: "Docs", end: false }, { to: "/settings", label: "Settings", end: false }, ]; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 3961372..be094d4 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -7,6 +7,7 @@ import { DashboardPage, DevicesPage, DeviceDetailPage, + DocsPage, HistoryPage, RisksPage, RecommendationsPage, @@ -30,6 +31,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render( element={} /> } /> + } /> } /> diff --git a/frontend/src/pages/DocsPage.tsx b/frontend/src/pages/DocsPage.tsx new file mode 100644 index 0000000..e6f73e5 --- /dev/null +++ b/frontend/src/pages/DocsPage.tsx @@ -0,0 +1,455 @@ +/** + * DocsPage — in-app documentation: overview, how scans work, risk levels, + * configuration reference, and practical how-to guides. + * Route: /docs + */ +import type { ReactNode } from "react"; +import { Card, PageHeader } from "../components"; + +// ── Shared primitives ──────────────────────────────────────────────────────── + +function SectionHeading({ id, children }: { id: string; children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +function H3({ children }: { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +function P({ children }: { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +function UL({ children }: { children: ReactNode }) { + return ( +
    + {children} +
+ ); +} + +function LI({ children }: { children: ReactNode }) { + return ( +
  • + + › + + {children} +
  • + ); +} + +function Code({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +function CodeBlock({ children }: { children: ReactNode }) { + return ( +
    +      {children}
    +    
    + ); +} + +function SeverityBadge({ + level, + description, +}: { + level: string; + description: string; +}) { + const colours: Record = { + Critical: "bg-[var(--color-accent-danger)] text-white", + High: "bg-[var(--color-accent-warning)] text-black", + Medium: "bg-[var(--color-accent-caution)] text-black", + Low: "bg-[var(--color-accent-info)] text-white", + Info: "bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--color-text-secondary)]", + }; + return ( +
    + + {level} + + + {description} + +
    + ); +} + +interface EnvVar { + name: string; + default_: string; + description: string; +} + +function EnvRow({ name, default_, description }: EnvVar) { + return ( +
    + {name} +
    + {description} + {default_ && ( + + (default: {default_}) + + )} +
    +
    + ); +} + +// ── Table of Contents ──────────────────────────────────────────────────────── + +const TOC = [ + { id: "overview", label: "Overview" }, + { id: "how-scans-work", label: "How scans work" }, + { id: "risk-levels", label: "Risk levels" }, + { id: "configuration", label: "Configuration" }, + { id: "guides", label: "How-to guides" }, +]; + +// ── Page ───────────────────────────────────────────────────────────────────── + +export function DocsPage() { + return ( +
    + + + {/* Table of contents */} + + +
    + {/* ── Overview ──────────────────────────────────────────────────── */} +
    + Overview + +
    +

    + NetworkCrawler is a passive home-lab security scanner. It + discovers every device on your local network, identifies open + services and their versions, infers the operating system, and + evaluates the result against a set of security checks — + producing a prioritised list of risks and hardening + recommendations. +

    +

    + All scanning is passive from an exploitation perspective: no + credentials are tested, no vulnerabilities are probed. The goal + is visibility and awareness, not penetration testing. +

    +
      +
    • + Runs entirely on your LAN — no data leaves your network. +
    • +
    • + Deployed as a single Docker container with{" "} + network_mode: host to reach all devices. +
    • +
    • + Scans run on a configurable schedule or can be triggered + manually from the Dashboard. +
    • +
    • + History, trends, and per-device detail are retained in a local + SQLite database. +
    • +
    +
    +
    +
    + + {/* ── How scans work ────────────────────────────────────────────── */} +
    + How scans work + +
    +
    +

    1 · ARP discovery

    +

    + A broadcast ARP request is sent across the configured subnet + (e.g. 192.168.1.0/24). Every active device + responds with its MAC address, giving a definitive list of + hosts that are online at that moment. ARP operates at layer 2 + and cannot be filtered by a host firewall. +

    +
    +
    +

    2 · Port scan

    +

    + Each discovered IP is scanned with nmap against the top 1 000 + most common TCP ports. Service banners and version strings are + captured where available. This step is the most time-consuming + part of the scan. +

    +
    +
    +

    3 · Hostname resolution

    +

    + A reverse DNS PTR lookup is attempted for each IP. If that + fails, a direct mDNS query is sent to the multicast address{" "} + 224.0.0.251:5353 — the same mechanism your + browser uses to resolve .local names. This works + for most modern devices including Apple, Linux, and Windows + machines. +

    +
    +
    +

    4 · OS inference

    +

    + The MAC vendor (derived from the OUI prefix), open ports, and + service banners are combined to produce a best-guess operating + system label. This is heuristic, not authoritative. +

    +
    +
    +

    5 · Risk analysis

    +

    + Each device is evaluated against all active security checks. + Checks look for things like unencrypted remote-access + protocols, default management ports exposed, outdated software + indicators, and unnecessary services. Each failing check + produces a Risk record with a severity level, and a linked + Recommendation with concrete steps to remediate. +

    +
    +
    +
    +
    + + {/* ── Risk levels ───────────────────────────────────────────────── */} +
    + Risk levels + +
    + + + + + +
    +
    +
    + + {/* ── Configuration ─────────────────────────────────────────────── */} +
    + Configuration + +

    + All configuration is done via environment variables in your{" "} + docker-compose.yml. No file editing inside the + container is required. +

    +
    + {( + [ + { + name: "SCAN_SUBNET", + default_: "192.168.1.0/24", + description: + "CIDR range to scan. Must match your LAN subnet.", + }, + { + name: "SCAN_INTERVAL_MINUTES", + default_: "60", + description: + "How often to run an automatic scan, in minutes. Set to 0 to disable automatic scanning.", + }, + { + name: "DATA_DIR", + default_: "/app/data", + description: + "Path inside the container where the SQLite database is stored. Mount a host volume here to persist data across container restarts.", + }, + ] as EnvVar[] + ).map((v) => ( + + ))} +
    +
    +

    Minimal docker-compose.yml

    + {`services: + networkcrawler: + image: ghcr.io/reloadfast/networkcrawler:latest + network_mode: host # required — allows ARP and mDNS + environment: + SCAN_SUBNET: "192.168.1.0/24" + SCAN_INTERVAL_MINUTES: "60" + volumes: + - networkcrawler_data:/app/data + restart: unless-stopped + +volumes: + networkcrawler_data:`} +
    +
    +
    + + {/* ── How-to guides ─────────────────────────────────────────────── */} +
    + How-to guides + +
    + {/* Hostnames */} + +

    Set a readable hostname so devices are easy to identify

    +

    + By default many devices advertise generic names like{" "} + android-a3f2 or show no hostname at all. Setting a + descriptive hostname makes your device list immediately + understandable and survives across re-scans. +

    + +
    +
    +

    + Linux (systemd) +

    + {`sudo hostnamectl set-hostname my-server +# Restart avahi-daemon if installed so the new name is advertised over mDNS: +sudo systemctl restart avahi-daemon`} +
    + +
    +

    + macOS +

    +

    + Go to System Settings → General → Sharing{" "} + and edit the Local Hostname field. The change takes + effect immediately and is broadcast via Bonjour (mDNS). +

    +
    + +
    +

    + Windows 10 / 11 +

    +

    + Go to Settings → System → About and click{" "} + Rename this PC. A reboot is required. Windows + advertises the new name via NetBIOS and mDNS (WSD). +

    +
    + +
    +

    + Raspberry Pi / Debian +

    + {`sudo raspi-config +# Navigate to: System Options → Hostname +# Or directly: +echo "my-pi" | sudo tee /etc/hostname +sudo sed -i 's/127\\.0\\.1\\.1.*/127.0.1.1\\tmy-pi/' /etc/hosts +sudo reboot`} +
    + +
    +

    + Router / DHCP static mapping +

    +

    + If you cannot change the hostname on a device (e.g. an IoT + appliance), most routers allow you to assign a static IP and + hostname to a MAC address via the DHCP reservation table. + Check your router admin panel under{" "} + DHCP → Static Leases or similar. NetworkCrawler + will pick up the PTR record if your router populates its + local DNS accordingly. +

    +
    +
    +
    + + {/* Trusted devices */} + +

    Mark devices as trusted

    +

    + Once you have identified all expected devices on your network, + mark them as Trusted using the toggle on the device + detail page. Trusted devices are visually distinguished in the + device list. Any new device that appears after that point and is + not trusted stands out immediately as something to investigate. +

    +
    + + {/* Scan tips */} + +

    Get the best scan results

    +
      +
    • + Run NetworkCrawler on the same physical network segment as + your devices, not across a routed boundary — ARP does not + cross routers. +
    • +
    • + Use network_mode: host in your compose file. + Bridge networking prevents ARP discovery and mDNS from working + correctly. +
    • +
    • + Schedule scans during a time when all your devices are likely + to be on (e.g. evening) for the most complete inventory. +
    • +
    • + Devices that are powered off during a scan will not appear in + that scan's results but will remain in the database from + previous scans. +
    • +
    +
    +
    +
    +
    +
    + ); +} diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index 842e610..966f512 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -3,6 +3,7 @@ export { HistoryPage } from "./HistoryPage"; export { DashboardPage } from "./DashboardPage"; export { DevicesPage } from "./DevicesPage"; export { DeviceDetailPage } from "./DeviceDetailPage"; +export { DocsPage } from "./DocsPage"; export { RisksPage } from "./RisksPage"; export { RecommendationsPage } from "./RecommendationsPage"; export { RecommendationDetailPage } from "./RecommendationDetailPage";