Scanning · 7 min read

How the Noxen scan engine works

One scan per host runs six probes in parallel. Findings stream in as each probe finishes — you don't wait for the slowest. Total time: 10–60 seconds per host, depending on open-port count.

The probe pipeline

The ScanEngine actor coordinates everything. Each probe is an async function that owns its own cancellation, error handling, and finding emission. They run concurrently — not sequentially — so a slow TLS handshake doesn't block the SSH inventory.

  1. Port scan (Apple's Network.framework) — top 1000 TCP ports. No external nmap binary, no sandbox-incompatible raw socket trickery. Open ports feed the TLS, HTTP-header, and admin-surface probes downstream.
  2. SSH inventory — connects via SystemSSHHostProbe (shells out to /usr/bin/ssh for reliability), reads /etc/os-release, kernel version, dpkg/rpm package list, sshd_config, and ~/.ssh/authorized_keys. All read-only — Noxen never modifies remote state.
  3. CVE match — every installed package version is cross-referenced against the loaded CVE feed using a CPE index. Matches surface as findings with the matching CVE ID, CVSS score, and distro-specific remediation.
  4. TLS audit — for any TLS-capable open port (443, 465, 636, 993, 995, 8443, 9443), runs a handshake with every supported cipher suite, parses the certificate chain, and checks for weak ciphers, deprecated protocols (TLSv1.0, TLSv1.1, SSLv3), HSTS presence, OCSP stapling, and near-expiry certificates.
  5. HTTP security headers — for every web-capable open port, GETs / and inspects the response headers: Content-Security-Policy, X-Frame-Options, Referrer-Policy, Permissions-Policy, Strict-Transport-Security, X-Content-Type-Options. Missing headers become medium-severity findings.
  6. Exposed admin surfaces — fingerprints ~70 services across 10 categories: home automation (Home Assistant, Pi-hole), media (Plex, Jellyfin, *arr suite), infrastructure (Proxmox, TrueNAS, Synology, QNAP), network (pfSense, OPNsense, UniFi, Mikrotik), DevOps (Gitea, Jenkins, Grafana, Prometheus, Uptime Kuma, Netdata), containers (Portainer, Kubernetes dashboard, Docker daemon), service mesh (Consul, Vault, Traefik), data (Redis, Mongo, Elasticsearch, phpMyAdmin), source-code leaks (.git/config, .env). Why this is the #1 homelab compromise vector →
Noxen host detail view showing edge-nuc with critical CVE-2024-6387 (regreSSHion), high CVE-2024-3094 (xz backdoor), exposed Grafana admin surface, and missing HSTS header.
Host detail view post-scan. Findings group by category; each row shows severity, title, detail, and a remediation hint.

Cancellation

Click Cancel scan in the toolbar (or hit Esc with the host selected) — every in-flight probe observes Swift's Task cancellation and unwinds cleanly. Partial findings already emitted are saved to the scan record; the scan's status flips to Cancelled with a timestamp. No side effects on the remote host.

Failed probes

Probes degrade gracefully. A failure in one probe doesn't stop the others — instead, the failed probe emits a single .probeError finding with the underlying error, and the rest of the scan continues. Common failure modes:

SSH auth failed → SSH inventory + CVE match skip
Network probes (port scan, TLS, HTTP, admin surfaces) still run. You get exposure data without inventory data.
Network unreachable → entire scan flagged failed
If port 22 itself isn't reachable, none of the probes have anything to do. The scan record's status is .failed and the sidebar shows a red dot.
Custom check JSON malformed → that check skipped
Other custom checks and built-in probes continue. The malformed check is logged for debugging — see custom checks reference.

Persistence

Each scan creates one Scan record and many Finding records, all in SwiftData. Scans are retained indefinitely so you can diff month-over-month; individual findings reference back to their parent scan. CloudKit sync (when enabled) replicates host catalog and scan history; SSH keys never sync — they live in Keychain.

Diff-from-yesterday

The default UI view on a scanned host is "what's new", not "what's known". Each scan is compared to the previous; the detail view defaults to filtering out findings that already existed in the prior scan. Toggle Show all to see the full set. The diff is severity-aware — a finding that changed severity (e.g., a CVE that escalated from medium to high after a vector update) shows as updated, not unchanged.

What Noxen does not do