A terminal dashboard that scans your home network, SSH-spiders into reachable hosts, and gives you a live view of security risks, severity scores, remediation steps, and a full Software Bill of Materials (SBOM) for every host.
┌─ Oh-Shit Network Security Dashboard ──────────── High risk (score 22) ─┐
│ Hosts │ Host Details │ Findings │ Remediation │ SBOM │ Vulns│
│ │ │
│ CRIT router │ 192.168.1.1 (router) — Critical risk (score 32) │
│ HIGH myserver │ OS: OpenWrt 23.05 │
│ LOW laptop │ Kernel: 5.15.134 │
│ LOW pi-hole │ MAC: aa:bb:cc:dd:ee:ff │
│ │ │
│ │ Open Ports: │
│ │ 22 tcp ssh OpenSSH 9.1 │
│ │ 80 tcp http lighttpd 1.4.73 │
│ │ 443 tcp https lighttpd 1.4.73 │
│────────────────────────────────────────────────────────────────────────│
│ [SSH: myserver] [████ 80%]│
│ 14:32:07 Discovery done: 8 hosts found │
└────────── r:Re-scan s:Re-scan host b:SBOM B:SBOM All v:Vulns e:Export q:Quit ┘
git clone <this-repo>
cd oh-shit
make setup
make runThat's it. make setup installs uv if it isn't
already on your system, then creates a virtual environment and installs all
Python dependencies.
| Requirement | Notes |
|---|---|
| Python 3.12+ | Managed automatically by uv |
ping |
Present on all Linux/macOS systems |
ip / arp |
Standard Linux networking tools |
nmap (optional) |
Enables port scanning and OS detection. sudo apt install nmap |
| SSH key or agent | Used to log into discovered hosts |
Root / sudo: Not required to run. Some data (OS fingerprinting via
nmap -O, reading remoteiptablesrules) requires root on the target host. The tool degrades gracefully when privileges are unavailable.
make setup Install uv (if needed) and all Python dependencies
make run Launch the full TUI — discovers hosts then SSH-collects data
make run-no-ssh Discovery only, no SSH login (faster, less detail)
make run-dev Textual hot-reload mode for UI development
make test Run the test suite
make lint Syntax-check all source files
make clean Remove .venv and cache directories
| Method | What it finds |
|---|---|
Local ARP table (/proc/net/arp) |
Hosts your machine already knows about |
| ICMP ping sweep | All live hosts on the detected subnet (up to /24) |
| Gateway ARP table | SSH into your router and read its neighbour table |
nmap -sV |
Open ports, service names, and version banners |
The subnet and gateway are detected automatically from ip route. Override
with --subnet 192.168.1.0/24 if needed.
The tool detects which IP addresses belong to the machine it is running on by
inspecting ip addr. For the local machine it runs the same commands directly
via shell subprocesses instead of SSH — so it works even when sshd is not
installed or not running. The host is labelled Local in the host list.
For all other reachable hosts, the tool connects with your existing SSH key or
agent (SSH_AUTH_SOCK) and runs read-only commands concurrently:
- OS and kernel version
- Running systemd services
- Listening ports (
ss -tlnp) - UFW / iptables firewall status
/etc/ssh/sshd_configaudit- Available package upgrades (
apt) - Docker containers and images
- User accounts and sudo membership
- Shadow file readability
- Package inventory for SBOM (see below)
Up to 5 remote hosts are collected in parallel. Hosts that are unreachable or reject the key are marked SSH Failed and shown without findings.
Note: SSH host-key verification is disabled by default so newly re-imaged LAN devices don't block scans. Enable strict checking with
ohshit --strict-host-keys.
Before per-host SSH collection, the tool listens passively on the LAN for device announcements:
- mDNS / Bonjour — captures device names and service types advertised on
224.0.0.251:5353 - SSDP / UPnP — sends an M-SEARCH multicast and parses SSDP responses for friendly names and model information
- MAC OUI lookup — maps the first 24 bits of each MAC address to a vendor name (IEEE registry, cached locally)
- Banner grabbing — connects to common IoT ports and reads the first bytes to identify device type
- ESPHome native API — probes port 6053 for ESPHome firmware metadata (device name, version, board, project)
- Home Assistant — optionally queries the HA REST API (pass
--ha-token) to correlate devices with HA entity IDs
Each collected data point is evaluated against security rules. Vulnerability data (when available) also contributes to the score:
| Rule | Severity | Score |
|---|---|---|
| No active firewall | Critical | 10 |
/etc/shadow world-readable |
Critical | 10 |
| Accounts with empty passwords | Critical | 10 |
| Telnet (port 23) open | Critical | 10 |
| Docker socket / daemon port exposed | Critical | 10 |
| CVE in CISA KEV catalog (actively exploited) | Critical | 10 |
| 50+ CVEs in installed packages | Critical | 10 |
| OS is end-of-life | Critical | 10 |
| OS end-of-life within 90 days | Critical | 10 |
SSH PasswordAuthentication yes |
High | 5 |
SSH PermitRootLogin yes |
High | 5 |
| Kernel update available | High | 5 |
| Privileged Docker containers | High | 5 |
| FTP (port 21) open | High | 5 |
| 21–49 CVEs in installed packages | High | 5 |
| High/Critical severity CVEs present | High | 5 |
| OS end-of-life within 6 months | High | 5 |
SSH X11Forwarding yes |
Medium | 2 |
| >10 outdated packages | Medium | 2 |
| HTTP without HTTPS | Medium | 2 |
Unexpected service on 0.0.0.0 |
Medium | 2 |
| 6–20 CVEs in installed packages | Medium | 2 |
| OS end-of-life within a year | Medium | 2 |
| 1–10 outdated packages | Low | 1 |
| 1–5 CVEs in installed packages | Low | 1 |
Per-host score = sum of finding scores.
Risk label: Critical ≥ 30 · High ≥ 15 · Medium ≥ 8 · Low > 0 · Info = 0
Network score = max(host scores) + mean(host scores)
Vulnerability findings are added automatically when vuln data is available
(loaded from cache on startup, or freshly queried with v).
After a successful SSH session, the tool collects a full Software Bill of Materials for the host by querying the package managers available on the remote system:
| Tool / command | Package type | What is collected |
|---|---|---|
dpkg-query -W |
deb |
All installed Debian/Ubuntu packages — name, version, architecture |
rpm -qa |
rpm |
All installed RPM packages (RHEL, Fedora, openSUSE) — name, version-release, architecture |
snap list |
snap |
Installed Snap packages — name, version |
flatpak list |
flatpak |
Installed Flatpak apps — application ID, version, origin |
pip3 list --format=freeze |
python |
System Python packages (both system-wide and user --user installs) |
docker images |
container |
All container images present on the host — repository, tag |
Only the tools present on the target host produce output; the rest return nothing and are silently skipped.
Each SBOM collection is stored as its own DuckDB database:
sbom/
<host-id>/ # MAC address preferred, IP fallback
20260412T143200.duckdb
20260411T091500.duckdb
...
sbom_catalog.ducklake # DuckLake catalog index
catalog_data/ # DuckLake Parquet data files
The host ID uses the MAC address when known so SBOM history follows the hardware, not the IP — useful when DHCP leases change.
Under normal operation only the latest SBOM for each host is shown in the UI. All historical databases are kept on disk for manual comparison or archaeology — they are never deleted and never overwritten. A new SBOM is only collected if none exists for the host, or if the most recent one is more than 24 hours old.
The DuckLake extension provides a unified catalog
view across all per-host databases. It is installed automatically by DuckDB on
first run (INSTALL ducklake).
After SBOM collection, vulnerability data is fetched automatically in the
background on startup and kept fresh with a daily refresh cycle. Press v
to force an immediate re-query for the selected host.
| Source | What it covers | Auth required |
|---|---|---|
| OSV (Google) | Debian, Ubuntu, PyPI, Red Hat, openSUSE, Alpine, Rocky Linux, AlmaLinux, Fedora, npm, Cargo, Go, RubyGems, NuGet, and 30+ more ecosystems | None |
| CISA KEV (CISA) | ~1,500 CVEs with confirmed active exploitation in the wild; includes ransomware campaign indicator | None |
| EPSS (FIRST.org) | Exploit Prediction Scoring System — probability (0–1) and percentile that a CVE will be exploited in the wild within 30 days | None |
Snap, Flatpak, and container image tags are not indexed by OSV and produce no results.
| SBOM source | OSV ecosystems queried |
|---|---|
dpkg |
Debian, Ubuntu |
rpm |
Red Hat, openSUSE, AlmaLinux, Rocky Linux, Fedora |
pip |
PyPI |
snap |
— (not indexed) |
flatpak |
— (not indexed) |
docker |
— (image tags not indexed) |
Per-host vulnerability matches are persisted in ohshit.db (vuln_cache
table) so they survive restarts without re-querying the network.
On startup the tool:
- Loads previously cached matches from DB and applies them immediately
- Identifies any host whose cache is missing or older than 24 hours
- Quietly refreshes those hosts in a background thread, logging each result to the log panel
The raw advisory data (advisory IDs, CVSS scores, EPSS scores, summaries) is cached
separately in ~/.cache/ohshit/vuln.duckdb, shared across all projects.
- SBOM tab —
CVEscolumn (first column, shown in red when non-zero) shows the count of matching advisories per package. Packages are sorted by: KEV hits first → worst CVE severity → CVE count → newest release date. - Vulnerabilities tab — full advisory table sorted by exploitation risk:
actively exploited (KEV) first, then known ransomware campaigns (
R), then highest EPSS probability, then severity, then CVSS score.Exploitcolumn:★= in CISA KEV catalog (confirmed active exploitation);R= associated with known ransomware campaigns;★ R= both.EPSS%column: probability of exploitation in the wild within 30 days (highlighted in yellow when ≥10%).CVSScolumn: numeric severity score from the advisory.
The Textual TUI refreshes as data arrives:
- Left panel — host list sorted by risk score, with coloured severity badges.
The machine running oh-shit is labelled Local instead of SSH OK/Failed.
Hosts with an OS approaching or past end-of-life show a coloured EOL badge:
red
EOL/EOL<90d(past or imminent), yellowEOL<6m, orangeEOL<1y. - Right panel (tabbed)
- Host Details — IP, MAC, OS, kernel, vendor, IoT identifiers, CVE summary (total count, severity breakdown, KEV/ransomware indicators), and open ports. If the OS version is known, an EOL status line is shown below the OS name: support type, end-of-life date, days remaining, and recommended upgrade target. Colour-coded red (EOL/imminent), yellow (<6 months), orange (<1 year), dim (in support).
- Findings — table of all findings sorted by severity
- Remediation — fix instructions for each finding. Shell commands are shown
with a
$prefix in green; explanatory notes are shown in muted text without a prefix. All commands are tailored to the detected distribution and package manager (apt/dnf/yum/apk/zypper). OS end-of-life findings include step-by-step major-version upgrade instructions specific to the installed distro. - SBOM — package inventory for the selected host. First two columns are
Risk(coloured badge:★KEV/CRIT/HIGH/MED/LOW) andCVEs(count, red if non-zero). Sorted by: KEV hits → worst CVE severity → CVE count → newest release date. Header shows collection timestamp, package count, and total CVE count. - Vulnerabilities — CVE advisory list sorted by exploitation risk: KEV
(confirmed active exploitation,
★) first, then ransomware-linked (R), then highest EPSS probability, then severity, then CVSS score. Columns: advisory ID,Exploitbadge, Severity, CVSS, EPSS%, affected package, summary. EPSS% is highlighted yellow when ≥ 10%.
- Bottom — scan progress bar (shows vuln fetch progress too) and live log
| Key | Action |
|---|---|
r |
Re-scan all hosts |
s |
Re-scan the selected host |
b |
Collect SBOM for the selected host only (local shell or SSH; bypasses 24 h age check) |
B |
Collect SBOM for all SSH-reachable hosts |
v |
Show vulnerability data for the selected host — reads from local cache instantly; only fetches from OSV if no cache exists yet |
V |
Force-download fresh KEV + OSV data for all hosts with an SBOM, then update every host's risk score |
e |
Export Markdown report to ~/network-security-report-<timestamp>.md |
q |
Quit |
ohshit [--no-ssh] [--subnet CIDR] [--strict-host-keys] [--ha-token TOKEN] [--db PATH]
--no-ssh Discovery only — skip SSH login and data collection
--subnet CIDR Override auto-detected subnet, e.g. 192.168.1.0/24
--strict-host-keys Verify SSH host keys via ~/.ssh/known_hosts
--ha-token TOKEN Home Assistant long-lived access token for IoT correlation
--db PATH Path to the main DuckDB database (default: ohshit.db)
All scan data is stored in a DuckDB database (ohshit.db by default) and
persists across runs. The tool loads existing host data on startup and only
auto-scans if no hosts are known yet — otherwise it waits for r.
Hosts are never deleted — offline hosts are marked Unreachable and remain in the list. Previously seen MAC addresses are tracked in a separate append-only history table to detect hardware repurposing.
SBOM databases accumulate in the sbom/ directory next to the main database
and are never automatically removed.
Per-host vulnerability matches are stored in ohshit.db (vuln_cache table)
and loaded on every startup. Stale entries (>24 h) are refreshed automatically
in the background. Pressing v reads from this cache instantly with no network
access; pressing V forces a full re-download for all hosts.
Raw advisory data (CVE details, CVSS scores) is cached in
~/.cache/ohshit/vuln.duckdb, shared across all projects and runs.
src/ohshit/
models.py Dataclasses: Host, Finding, PortInfo, IotInfo, ScanResult, enums
discovery.py ARP / ping sweep / nmap / router ARP
ssh_collector.py SSH + local-shell collection; auto-detects local machine IPs
distro_eol.py OS end-of-life database; distro-specific upgrade steps and package manager helpers
risk_engine.py Scoring rules → Finding objects; all remediation tailored to detected distro
sbom.py SBOM collection, parsing, per-host DuckDB storage, DuckLake catalog
vuln_db.py Vulnerability cache — OSV + CISA KEV, local DuckDB at ~/.cache/ohshit/vuln.duckdb
port_probe.py Deep port banner / TLS / ESPHome probing
iot.py mDNS sniff, UPnP/SSDP, Home Assistant, IoT device detection
oui_db.py MAC OUI vendor lookup and MAC permanence classification
report.py Markdown report generator
main.py CLI entry point
tui/
app.py Textual App, scanner pipeline, DB polling, SBOM/vuln wiring
widgets.py Custom widgets: host list, findings table, SBOM tab, vuln tab, etc.
app.tcss Textual CSS layout and theming
| Library | Purpose |
|---|---|
| Textual | Terminal UI framework |
| asyncssh | Async SSH client for remote collection |
| DuckDB | Embedded analytics database (hosts, findings, SBOM, vuln cache) |
| DuckLake | Catalog layer unifying per-host SBOM databases (DuckDB extension, auto-installed) |
| aiohttp | Async HTTP for Home Assistant, UPnP probing, and OSV API queries |
| aiofiles | Async file I/O for report export |
| cryptography | TLS certificate inspection |
| rich | Terminal formatting (used by Textual) |
make setup # first-time setup
make run-dev # hot-reload TUI (changes to .py/.tcss reload instantly)
make test # pytest
make lint # syntax checkThe TUI is built with Textual. Network
discovery and SSH collection are fully async (asyncio + asyncssh) and run
in a background thread with its own event loop, writing to DuckDB via WAL
which the UI thread reads without locking.
- The tool only reads data from remote hosts; it does not modify anything.
- All SSH connections use your existing keys/agent — no passwords are stored.
known_hostschecking is off by default for LAN convenience; use--strict-host-keysin higher-trust environments.- The exported Markdown report may contain sensitive system information (open ports, package versions, account names). Treat it accordingly.
- SBOM databases stored in
sbom/contain full package inventories for each host. Restrict access to this directory appropriately. - Vulnerability queries are sent to
api.osv.dev(Google),www.cisa.gov, andapi.first.org(EPSS) — the package names and versions of every installed package on your hosts are transmitted. This happens automatically in the background for any host with a stale or missing vuln cache, and on demand whenVis pressed. Do not run on air-gapped networks or where package inventory must remain confidential.