Find every dev server on your LAN — starting with your own.
Most scanners point outward. This one starts with you.
devscan --me # what is my laptop exposing to this wifi right now?
devscan # ...and what is the rest of this network exposing?ENDPOINT HTTP STACK TITLE SERVER
--------------------------------------------------------------------------------------
10.0.4.138:8000 200 Python http.server internal dashboard SimpleHTTP/3.12
10.0.5.123:8501 200 Streamlit Streamlit uvicorn
10.0.6.58:3000 200 Next.js admin console Next.js
10.0.9.26:8000 200 FastAPI/uvicorn - uvicorn
10.0.11.234:9000 403 MinIO - MinIO
10.0.7.207:11434 200 Ollama - -
128 dev endpoints on 90 hosts | 2250 reachable | 624 AirPlay (hidden)
Identifies the stack behind each open port — Next.js, Express, FastAPI, Vite, Streamlit, Ollama, MinIO, nginx, Jupyter, Gradio, Flask, Grafana, Elasticsearch. No root. No telemetry. One bash file.
Built after a scan where 86% of the "open ports" turned out to be macOS AirPlay. It knows the difference — and it knows three other ways a port scan lies to you silently. See Four things that make the output trustworthy.
git clone <repo> devscan && cd devscan && ./install.shor just drop the single file anywhere on your PATH:
curl -o /usr/local/bin/devscan <raw-url> && chmod +x /usr/local/bin/devscanDependencies: nmap (brew install nmap) and curl. Falls back to netcat if
nmap is missing. No root required.
devscan # scan the local subnet
devscan --me # what am I exposing to this network?
devscan --fast # top 14 ports, more workers (~3x faster)
devscan --thorough # retry harder; use if you suspect missed ports
devscan -s 192.168.1.0/24 # a specific subnet
devscan -p 3000,8000,8080 # specific ports
devscan -j | jq '.services[]' # JSON out
devscan -o ~/scan # write txt/csv/json
devscan -a # ARP cache only, skip the sweep (fast)
devscan --me -all # every one of the 65535 ports on MY machine (~5s)
devscan -s 192.168.1.0/24 -all # ...on a whole subnet (slow; watch the ETA)
Four stages, each feeding the next so the expensive work only runs on live hosts:
- Host discovery — reads the ARP cache and expands the subnet CIDR.
- Liveness sweep — 250-way parallel ping. Cuts an 8190-address /19 down to the hosts that actually answer, in ~35 seconds. ARP-known hosts that stay silent get a cheap TCP liveness probe, so ping-blocking machines aren't lost — without dragging every stale ARP entry into the scan.
- Port scan — parallel
nmapTCP connect scan across ~41 dev ports, with a live progress meter (hosts done, ports found, ETA). - Identification — one
GET /per endpoint, classified into Next.js, Express, FastAPI/uvicorn, Streamlit, Vite, MinIO, nginx, Jupyter, Gradio, Ollama, Flask, Grafana, Elasticsearch, or a router admin panel.
These are the failure modes that made earlier hand-rolled versions of this scan quietly wrong. They're worth knowing because they'll bite any reimplementation.
macOS AirPlay masquerades as a dev server. Ports 5000 and 7000 are
ControlCenter (AirPlay Receiver) on every modern Mac. On a network of 546
Macs that's 318 of 369 open ports — 86% of your results are noise, and they
look exactly like a Flask app on 5000. devscan scans them, then reports them
separately. --show-airplay if you want them.
--host-timeout silently zeroes your scan. When most ports are filtered
rather than closed, each probe waits out its full timeout, so a host easily
exceeds a 40s budget and nmap abandons it — recording Status: Timeout and
emitting no open ports at all. The scan completes, exits 0, and reports
nothing wrong. devscan drops --host-timeout entirely and bounds the individual probes
instead (--max-rtt-timeout 900ms), so slow hosts finish rather than get
abandoned. It then counts any residual timeouts and warns.
--max-retries 0 silently abandons ports. The obvious cure for #2 is to stop
nmap retrying. Do that on lossy wifi and it starts printing
giving up on port because retransmission cap hit (0) — once per abandoned port,
to stdout, which most wrappers send to /dev/null. Measured on 200 hosts:
--max-retries 0 produced dozens of drops, --max-retries 1 produced one.
devscan defaults to 1, exposes 0 via --fast, and counts the warnings and tells
you rather than under-reporting in silence.
sort -u with restricted keys eats your findings. This looks reasonable:
sort -u -t. -k1,1n -k2,2n -k3,3n -k4,4n endpoints.txtWith -t., the fourth field of 10.7.11.170:8000 is 170:8000, and -k4,4n
parses that as 170. Every port on a host therefore shares one sort key, and
-u deduplicates on the key, not the line — so a host with four open ports
collapses to one. devscan builds an explicit zero-padded key and dedupes on the
full line (sort_eps).
A dead worker looks exactly like an empty network. devscan splits the host
list across parallel nmap processes. An early version of -all used
--min-parallelism 512 across 8 workers — nmap itself warns
"can hurt reliability" — and a worker scanning a remote host simply never
finished. Its chunk contributed zero open ports and no error, so a subnet
containing a machine with 16 open ports reported open: 0. devscan now counts
how many hosts actually returned results and, if any are missing, says the
output is a floor, not a total. All-ports scans use depth (4 workers,
parallelism 128) rather than a stampede.
And one more, non-obvious: in zsh, for p in $PORTS does not word-split an
unquoted scalar the way bash does — you get one iteration with the whole string.
devscan is #!/usr/bin/env bash for exactly this reason.
- Dev servers bound to
127.0.0.1are invisible from the network by design. Vite, Next.js and Rails all default to localhost, so absence here is not evidence nothing is running — it usually means it's bound correctly. - ICMP-filtered hosts are only probed if they appear in the ARP cache.
- IPv6-only listeners are invisible. This is an IPv4 connect scan, so a
service bound to
::without dual-stack will not appear. Verified on macOS:rapportdlisteners that bind IPv6-only never show up. lsofwithoutsudoshows only your processes, so it is not a complete ground truth. In testing,devscan --me -allcorrectly found open445(SMB) and3306(MySQL) that unprivilegedlsofcould not see.- Identification is a single unauthenticated
GET /. It reads banners; it does not probe paths, guess credentials, or test for vulnerabilities.
This is an inventory tool. It performs host discovery, a TCP connect scan, and one HTTP GET per open port — the same traffic as opening the page in a browser. It contains no exploitation, credential, or vulnerability-probing behaviour, and none will be added.
Only scan networks you own or have written authorization to assess. Port scanning third-party hosts without permission is unlawful in many jurisdictions, and shared conference, campus, or coworking Wi-Fi is not your network.
MIT