Skip to content

emrecdr/netlens

Repository files navigation

NetLens

Version Python Changelog

A Python CLI tool that deeply scans local networks to discover devices, classify them by type, assess their security posture, analyze traffic patterns, detect firmware vulnerabilities, audit WiFi security, track changes over time, and map network topology.

Built for personal/home network security auditing.

Use Cases

  • Home network audit — Find every device on your network, see what ports they expose, and get actionable security fixes
  • IoT device inventory — Identify smart speakers, cameras, hubs, printers, and other IoT devices by type with confidence scoring
  • Security assessment — Test for default credentials, weak TLS, outdated firmware, open admin panels, and known CVEs
  • Traffic monitoring — Capture and analyze per-device traffic patterns, DNS queries, and beaconing behavior
  • Change detection — Track devices appearing/disappearing over time, compare scans, get alerts on changes (terminal or webhook)
  • Network segmentation planning — Get zone recommendations (trusted/IoT/quarantine) and firewall rules
  • WiFi security — Detect rogue APs, hidden SSIDs, WEP / Open networks, WPS PIN brute-force exposure (Reaver / Pixie-Dust), and PMKID-capturable WPA2/WPA3 networks (Steube 2018 / hashcat -m 22000)
  • Cross-radio device tracking — Auto-join WiFi/IP devices with their BLE adverts ("your iPhone is at 192.168.1.42 and broadcasting BLE AA:BB:…") — a view no free competitor produces automatically
  • Baseline drift alerts — A device opening a new port, changing OS, or picking up a firmware update surfaces as a scan finding under netlens risks
  • Bluetooth audit — Inventory BLE devices in range, map them to known products (smart locks, trackers, wearables, smart TVs, soundbars, Chromecasts, fitness trackers), map their chipset firmware to published CVEs (SweynTooth / BrakTooth / BleedingTooth / KNOB), detect privacy leaks from Apple Continuity and Microsoft Swift Pair beacons, and surface anti-stalking warnings from persistent AirTag / Tile presence across scans
  • Interactive BLE probenetlens probe <address> enumerates a single target device's GATT services, marks writable characteristics, surfaces the negotiated MTU, and drops into a REPL for one-shot writes — the manual hands-on counterpart to the passive netlens bluetooth audit. Paged reads defeat the 512-byte ATT-MTU ceiling so gzipped config blobs come out whole.

Requirements

  • Python 3.13+
  • nmap system binary
# macOS
brew install nmap

# Debian/Ubuntu
sudo apt-get install nmap

Installation

# Clone and install with uv
git clone <repo-url> && cd netlens
uv sync

# Optional: macOS WiFi scanning support
uv sync --extra macos

# Optional: Bluetooth (BLE) audit support — adds bleak + pyyaml
uv sync --extra bluetooth

Quick Start

# Full scan with root (best results — enables ARP, traffic capture, WiFi, OS detection)
sudo uv run netlens scan

# Scan without root (reduced capabilities, no traffic/WiFi)
uv run netlens scan

# Scan a specific subnet
sudo uv run netlens scan 192.168.1.0/24

# View security findings
uv run netlens risks

# Generate an HTML report
uv run netlens report

# Bluetooth audit (no sudo needed on macOS; requires [bluetooth] extra)
uv run netlens bluetooth --duration 30

All flags: see Scan Options for the netlens scan reference, Bluetooth Options for the netlens bluetooth reference, or Global Flags for flags that work on any command. You can also run netlens --help and netlens <command> --help.

Commands

Scanning

Command Description
netlens scan [NETWORK] Full network discovery and security audit
netlens watch [NETWORK] Continuous monitoring — alerts on device changes
netlens bluetooth BLE discovery + security audit (firmware CVEs, beacons, anti-stalking)
netlens probe [ADDRESS] Interactive single-target BLE GATT probe — enumerate writable characteristics, send targeted writes, observe notifications

Analysis

Command Description
netlens risks Security findings sorted by severity with remediation
netlens traffic Per-device traffic patterns, DNS queries, anomalies
netlens segment Network zone recommendations and firewall rules
netlens topology ASCII network topology map (gateway → switches → devices)
netlens correlate Join WiFi/IP devices with BLE devices from the same audit window

History & Reporting

Command Description
netlens history List past scans with device/finding counts
netlens diff <SCAN_ID> Compare two scans — new, removed, changed devices
netlens report Generate a self-contained HTML security report
netlens devices View, label, and annotate device profiles

Setup

Command Description
netlens setup-geoip Download MaxMind GeoIP databases for traffic geolocation

Global Flags

Flag Description
--version, -V Print netlens <version> and exit
--quiet, -q Suppress pre-flight and next-steps panels
--no-color Disable ANSI colors (useful for CI logs)
--no-progress Disable the live scan dashboard
--theme {auto,dark,light} Color theme variant

Scan Options

Every netlens scan flag, what it does, and which stage it affects. Defaults shown for value-taking flags.

Flag Default What it does Stage
--full off (top ~1000 ports) Scan all 65535 TCP ports 2 (Fingerprint)
--deep off Enable NSE vulnerability scripts, DHCP rogue detection, SNMP community probing, and ARP tracking during traffic capture 1, 3, 6
--ports TEXT Custom port list (e.g. 22,80,443 or 1-1024) — overrides --full 2
--timeout INT 10 Per-host scan timeout in seconds 2
--capture-time INT 60 Traffic capture duration in seconds 6
--skip-creds off Skip default-password testing (SSH/Telnet/HTTP) 5
--skip-traffic off Skip packet capture (faster, but loses beaconing detection) 6
--skip-wifi off Skip WiFi environment scan 7
--skip-topology off Skip network topology mapping post-scan
--wifi-interface TEXT auto-detect WiFi interface for monitor mode (e.g. en0) 7
--json off Emit results as JSON to stdout (machine-readable) output
--verbose, -v off Show scan duration and extra per-stage detail output

Common combinations

# Heaviest scan — all ports + deep checks (slow but thorough)
sudo uv run netlens scan --full --deep

# Fast triage — discovery + classification only, no creds/traffic/wifi
uv run netlens scan --skip-creds --skip-traffic --skip-wifi

# Custom port set (overrides --full)
uv run netlens scan --ports 22,80,443,8080

# Longer traffic capture for catching low-rate beaconing
sudo uv run netlens scan --capture-time 300

# CI / log-friendly output
uv run netlens --no-progress --no-color scan --json

Global flags like --quiet, --no-progress, --no-color, --theme apply to every command and must precede the subcommand (netlens --no-progress scan, not netlens scan --no-progress). See Global Flags.

Scan Pipeline

NetLens runs 7 stages sequentially. Each stage can fail independently without blocking others.

# Stage What it does Needs root?
1 Discovery ARP + mDNS + SSDP + DHCP rogue detection No (fallback to arp -a)
2 Fingerprinting Nmap ports/services, MAC vendor, SNMP probe, banner grab, favicon hash No (connect scan only)
3 SNMP Deep Interface tables, ARP/routing tables, community string audit No
4 Classification Device type assignment (12 categories), firmware extraction, confidence scoring No
5 Security Audit 12 parallel security checks (see below) No
6 Traffic Analysis Packet capture, DNS extraction, anomaly detection Yes
7 WiFi Security Nearby networks, rogue AP detection, encryption + WPS + PMKID + PMF audit Yes

Security Checks (Stage 5)

Check Severity What it detects
Default credentials CRITICAL SSH/Telnet/HTTP login with common defaults (root/root, admin/admin)
CVE lookup CRITICAL–LOW Known vulnerabilities via NVD API by CPE/product name
Firmware outdated HIGH Device firmware behind manufacturer's latest release
TLS/SSL analysis HIGH–MEDIUM Expired certs, weak ciphers, self-signed certificates
Admin panels MEDIUM Accessible /admin, /login, /console without authentication
UPnP control surface HIGH Controllable UPnP services accepting SOAP commands
MQTT injection HIGH Unauthenticated MQTT broker accepting publish commands
REST API exposure MEDIUM Unauthenticated API endpoints on common ports
DNS rebinding MEDIUM Devices vulnerable to DNS rebinding attacks
Protocol security MEDIUM Telnet, FTP, unencrypted HTTP, weak SSH ciphers
HTTP methods MEDIUM Dangerous methods enabled (PUT, DELETE, TRACE)
Printer vulnerabilities MEDIUM SNMP community strings, default management passwords

Device Types (Stage 4)

Router, Gateway, Managed Switch, Smart Home Hub, Smart Speaker, Smart TV, Media Player, Media Server, Printer, IP Camera, IoT Sensor, Mobile Device, Single Board Computer.

WiFi Findings (Stage 7)

Beacon-parsed findings that ride the same severity / netlens risks / JSON / HTML report pipeline as Stage-5 device findings. Each finding is attached to its source network by BSSID.

Finding Severity What it detects
WEP encryption HIGH Broken since 2001 (Fluhrer/Mantin/Shamir); aircrack-ng recovers the key in seconds from a few captured IVs
Open (unencrypted) WiFi HIGH All traffic broadcast in the clear; flagged for any visible (non-hidden) network
WPS PIN brute-force possible MEDIUM WPS enabled + AP-Setup-Locked clear (or unknown): Reaver (4-10h for the full 10^4+10^3 PIN space)
WPS PIN attack (Pixie-Dust likely) HIGH Same as above, but the AP's BSSID OUI matches a curated Pixie-Dust-vulnerable chipset (Realtek RTL81xx / Ralink RT3xxx / Broadcom BCM63xx). Extend the list via ~/.netlens/wifi_chipsets.yaml.
WPS enabled but currently locked INFO AP rate-limited PIN entry; lock typically expires and re-enables the attack surface
PMKID capture possible MEDIUM WPA/WPA2/WPA3-PSK without PMF required: attacker initiates association without a real client, harvests PMKID from EAPOL M1, cracks offline with hashcat -m 22000 (Steube, 2018)

Risks

netlens risks runs a full scan and lists every security finding, sorted by severity. Covers IP-device findings (Stage 5) and WiFi findings (Stage 7) in one unified pipeline — --severity filters either source.

Flag Default What it does
NETWORK (positional) auto-detect Target network CIDR
--severity TEXT Filter by severity (comma-separated: critical,high)
--verbose, -v off Show full description + remediation per finding
--json off JSON output; WiFi findings tagged "source": "wifi"
# All findings on the local network
sudo uv run netlens risks

# Only HIGH and CRITICAL
sudo uv run netlens risks --severity critical,high

# Verbose: full remediation guidance
sudo uv run netlens risks --severity high -v

# Pipe to jq (JSON output is `soft_wrap=True` so it survives long descriptions)
sudo uv run netlens risks --json | jq '.[] | select(.severity == "high")'

Traffic Analysis

netlens traffic runs a scan with packet capture enabled and renders per-device communication summaries (DNS queries, destination peers, detected anomalies). Requires root for packet capture; without it the table will be empty.

Flag Default What it does
NETWORK (positional) auto-detect Target network CIDR
--capture-time INT 60 Traffic capture duration in seconds
--verbose, -v off Show per-device traffic breakdown
--json off JSON output for piping into analytics
# Default 60-second capture
sudo uv run netlens traffic

# Longer capture for low-rate beaconing IoT devices
sudo uv run netlens traffic --capture-time 300

# Per-device DNS + peer breakdown
sudo uv run netlens traffic -v

# Pipe DNS queries into jq
sudo uv run netlens traffic --json | jq '.devices[].dns_queries'

Topology

netlens topology detects the gateway, passively sniffs LLDP/CDP frames for switch/AP discovery (needs root), and renders an ASCII tree.

Flag Default What it does
--sniff-time INT 60 Seconds to listen for LLDP/CDP frames
--json off Emit topology as JSON for piping
# Default 60-second sniff + ASCII tree
sudo uv run netlens topology

# Quick gateway-only view (10s sniff)
sudo uv run netlens topology --sniff-time 10

# JSON for piping into other tools
sudo uv run netlens topology --json > topology.json

Scan History

Two commands share the SQLite history DB at ~/.netlens/history.db: history lists past scans; diff compares two of them.

netlens history

Flag Default What it does
--limit, -n INT 20 Maximum past scans to show
--json off JSON output
# 20 most recent scans
uv run netlens history

# Just the last 5
uv run netlens history -n 5

# Full history as JSON
uv run netlens history -n 1000 --json

netlens diff <SCAN_ID> [<SCAN_ID_2>]

Arg / Flag Default What it does
SCAN_ID (required) Baseline scan ID (from netlens history)
SCAN_ID_2 (optional) latest scan Second scan to compare against
--json off JSON output
# Compare abc123 against the latest
uv run netlens diff abc123

# Compare two specific scans
uv run netlens diff abc123 def456

# JSON pipe
uv run netlens diff abc123 --json | jq '.new_devices'

Bluetooth Audit

netlens bluetooth is a separate subcommand that performs an industry-standard BLE security audit comparable to the workflow a security professional on Kali would run by hand. It's gated behind the [bluetooth] extra so the base install stays lean:

uv sync --extra bluetooth
uv run netlens bluetooth --duration 30

Platform support: macOS works out of the box (no sudo). Linux works as long as the user has CAP_NET_ADMIN or root. Windows is supported via WinRT.

What it does

The audit is an 8-layer pipeline; every layer is independently skippable:

# Layer Output
1 Passive discovery via bleak Address, name, RSSI, advertised services, manufacturer data, BLE address-type (Public / Random Static / RPA / Random Non-Resolvable)
2 GATT enumeration of the Device Information Service Firmware/hardware/software revision, model, serial, manufacturer string, PnP ID. Allowlist-gated by default.
3 Beacon parsing & device classification iBeacon, Eddystone (UID/URL/TLM/EID), AltBeacon, Bluetooth Mesh detection. Service-UUID device class (e.g. "Heart rate monitor", "Smart Lock").
4 Continuity-protocol parsers Apple Continuity (Handoff, AirDrop, Nearby Info, Wi-Fi Settings, AirPods, FindMy) and Microsoft Swift Pair
5 CVE pipeline (3-stage fallback) NVD CPE search → NVD keyword search → curated offline chipset DB covering SweynTooth / BrakTooth / BleedingTooth / KNOB
6 Curated misconfiguration checks Pairing-mode posture, writable-no-auth GATT, manufacturer/DIS spoofing, PII-in-name, non-randomizing address
7 Companion-app fingerprint DB Maps advertisements to ~15 consumer products (smart locks, trackers, wearables, bulbs) with product-specific CVEs
8 Cross-scan tracking detection Anti-stalking: flags devices appearing across N of last K scans + ≥2 location tags. Special handling for AirTag-class beacons that rotate identifiers.
+ macOS escalation (system_profiler) Enumerates paired Classic Bluetooth devices invisible to BLE-only scanning
+ Linux escalation (bettercap if installed) Deeper GATT enrichment via Kali's standard tool
+ Optional active write-probe Writes a benign 0x00 to confirm-or-mitigate writable-no-auth findings (allowlist-gated by default; --probe-writes-all for authorized testing)

Bluetooth Options

Flag Default What it does
--duration INT 30 Passive listen duration in seconds
--connect/--no-connect --connect Attempt GATT enum on allowlisted devices to read DIS (firmware/model)
--connect-all off Override the allowlist gate; GATT-enum every device. Prints a 5-second active-probing consent banner.
--gatt-timeout INT 5 Per-device GATT connect timeout in seconds
--gatt-concurrency INT 2 Max simultaneous GATT connects (2 = safe default for BlueZ)
--allowlist PATH ~/.netlens/bluetooth_allowlist.txt Devices approved for GATT enum (see Allowlist file format)
--skip-cve off Skip CVE lookup entirely
--offline-only off Don't query NVD; use only the curated offline chipset DB (air-gapped audits)
--probe-writes off Active write-probe: write benign 0x00 to writable handles to confirm-or-mitigate findings (allowlist-gated)
--probe-writes-all off Probe ALL devices' writable handles. Prints 5-second consent banner.
--tracking/--skip-tracking --tracking Cross-scan tracking detection (needs ≥3 prior scans in history)
--tracking-lookback INT 10 Past scans to correlate against
--tracking-threshold FLOAT 0.6 Minimum appearance ratio to flag as following
--owned-devices PATH ~/.netlens/bluetooth_owned.txt Fingerprints to suppress in tracking detection
--skip-escalation off Skip platform-specific escalation (system_profiler / bettercap)
--location TEXT untagged Tag this scan with a location label for cross-scan tracking
--report PATH Write a self-contained HTML audit report to PATH after the scan
--load-external-checks off Load community-authored BLE checks from ~/.netlens/checks/*.py. Executes arbitrary Python so prints a 5-second consent banner; built-in check names cannot be shadowed
--json off Output as JSON (suppresses panels and HTML report banner)
--verbose, -v off Show per-device GATT-enum outcomes and fingerprint signals (the manufacturer ID + service UUIDs + name that fed the cross-scan identity hash — answers "why are these two adverts collapsed to one device?")

Bluetooth Findings

15 distinct finding types ship with the v1.0 audit:

Finding Severity What it surfaces
NVD chipset CVEs CRITICAL–LOW Known firmware CVEs (SweynTooth, BrakTooth, BleedingTooth, KNOB) via chipset+firmware → NVD CPE/keyword/offline DB
Companion-app CVEs CRITICAL–LOW Product-specific CVEs (e.g. August Smart Lock Pro firmware <2.5.0 → CVE-2019-17098)
Detached AirTag in range HIGH Apple Continuity subtype 0x12 — a FindMy accessory separated from its owner
Persistent tracker across scans HIGH/MEDIUM A device fingerprint appearing in ≥60% of last 10 scans across ≥2 location tags
Writable GATT char without auth HIGH (confirmed) / MEDIUM (advertised) A characteristic accepts writes without encryption/auth — classic IoT lock attack surface
Manufacturer mismatch (spoofing) HIGH Advertised manufacturer ID doesn't match the GATT-read DIS string
LE Legacy pairing HIGH Device only accepts pre-4.2 pairing methods (broken key derivation)
Apple Wi-Fi join in progress MEDIUM Apple device is on the Wi-Fi settings screen — social-engineering window for rogue APs
Discoverable + connectable MEDIUM Pairing-mode advertisement on a device that may not have meant to be paired
Just Works pairing MEDIUM Pairable without MITM protection
Eddystone-URL (non-https / IP-literal) MEDIUM Beacon advertising a suspicious URL
iBeacon / Eddystone-UID disclosure INFO Deployment UUID / namespace / instance disclosed
Unprovisioned Bluetooth Mesh MEDIUM A mesh-provisioning beacon — anyone in range could provision the device
PII in advertised name LOW Names like Alice's iPhone, Bedroom-Lock, Printer-AC4F1B
Non-randomizing address LOW Public / Random Static addresses are trackable across scans
AirPods / earbuds in range INFO Battery state visible in cleartext (informational)
Swift Pair beacon LOW / MEDIUM (PII) Windows device in pairing mode; PII finding when the laptop name looks personal

Common Bluetooth combinations

# Standard audit — 30s passive scan, NVD CVE lookup, all checks
uv run netlens bluetooth

# Tag scans with locations to enable anti-stalking detection
uv run netlens bluetooth --location home
uv run netlens bluetooth --location office
uv run netlens bluetooth --location cafe        # ≥3 scans + ≥2 locations → tracking engine activates

# Air-gapped audit — no NVD calls, offline static DB only
uv run netlens bluetooth --offline-only

# Authorized active write-probe (your own devices only — populate allowlist first)
uv run netlens bluetooth --probe-writes

# Active probe on all devices (authorized testing only — prints consent banner)
uv run netlens bluetooth --probe-writes-all

# Standalone HTML audit report
uv run netlens bluetooth --report ~/Desktop/bt-audit.html

# JSON output for piping / CI
uv run netlens bluetooth --json --skip-tracking | jq '.devices[].findings'

# Bare-bones discovery, no CVE / escalation / tracking
uv run netlens bluetooth --skip-cve --skip-escalation --skip-tracking --no-connect

BLE Probe (Interactive)

netlens probe is the active, single-target counterpart to the passive netlens bluetooth audit. Use it when you already own a device (or have authorization) and want to enumerate its writable characteristics, send targeted writes, or watch its notification traffic.

# List every BLE device currently broadcasting (10s live scan, no connect)
uv run netlens probe --list

# Auto-pick the strongest-RSSI non-Apple device and probe it
uv run netlens probe --latest

# Connect to a specific peripheral and dump writable characteristics
uv run netlens probe D1B8EA1F-E5A6-FA50-7DEF-06B29F536D81

# One-shot targeted write to a known characteristic UUID
uv run netlens probe AA:BB:CC:DD:EE:FF \
    --write 0000180a-0000-1000-8000-00805f9b34fb --payload 00
Flag Default What it does
ADDRESS BLE MAC (Linux/Windows) or peripheral UUID (macOS). Optional when using --list or --latest.
--list off 10-second live scan; print every broadcasting device sorted by RSSI, then exit
--latest off Auto-pick the strongest-RSSI non-Apple device from a fresh 10-second scan
--write UUID Characteristic UUID to write to. Skips the interactive REPL.
--payload HEX 00 Hex bytes for --write (e.g. 5601020303f0aa)
--timeout SEC 15 Connect timeout (macOS often needs 10-20s)
--no-notify off Skip subscribing to notification/indication characteristics

The probe prints the negotiated MTU at connect time so you can interpret ⚠ truncated annotations on read-back. It uses paged reads internally (see netlens.bluetooth.gatt.read_full_value) to defeat the 512-byte ATT-MTU ceiling — gzipped config blobs from devices like Chromecast come back whole instead of truncated at the first PDU.

BLE toolkit scripts (survey / read / control)

Three single-purpose scripts in scripts/ for ad-hoc exploration outside the netlens CLI. Each is --help-discoverable, depends only on bleak, and prints worked examples in its docstring.

Script What it does Risk
scripts/bt_connect_demo.py Surveys BLE range, attempts a read-only connect to every non-Apple/Microsoft device, prints a per-device verdict (✓ connected / ✗ timeout / ✗ refused). Answers "who in my room accepts strangers?" None — connect + disconnect only, no reads or writes.
scripts/bt_read_all.py <address> Connects to one device and reads every readable characteristic, dumping hex + printable-ASCII so firmware versions, model strings, and JSON config blobs are immediately visible. None — read-only. Some auth-required chars will fail individually; the dump continues.
scripts/bt_control.py <address> Active counterpart. --list-writable (safe default) enumerates writable chars. --write UUID --payload HEX does a single targeted write. --probe-volume runs a reversible demo against the Bluetooth-standard Volume Control Service (0x2B7D/0x2B7E): captures current volume, issues a 1-step decrement, confirms, restores original. Low for --probe-volume (reversible). Custom --write writes are the user's responsibility — vendor-specific chars can trigger OTA/factory-reset on misuse.
# 1. survey: who's reachable from a stranger Mac?
uv run python scripts/bt_connect_demo.py --rssi-min -85

# 2. read: what does this device leak to an unpaired peer?
uv run python scripts/bt_read_all.py <BLE_ADDRESS>

# 3. control: does it honour standard Bluetooth volume writes?
uv run python scripts/bt_control.py <BLE_ADDRESS> --probe-volume

Allowlist file format

GATT enumeration and write-probing are gated by ~/.netlens/bluetooth_allowlist.txt (or --allowlist PATH). One entry per line, comments allowed:

# My devices
AA:BB:CC:DD:EE:FF                       # match by address
name:My AirPods Pro                     # match by exact advertised name
fp:6b7e2a91b4...                        # match by fingerprint hash
AABBCCDD-EEFF-1122-3344-556677889900    # macOS CoreBluetooth UUID also accepted

The owned-devices file (~/.netlens/bluetooth_owned.txt, suppresses tracking findings) uses the same format but only fp:HASH lines are recognised — get the fingerprint hash from netlens bluetooth --json and copy it in.

Ethical-by-default design

By default, NetLens never connects to a stranger's BLE device:

  • GATT enum runs only on devices in your allowlist
  • Write-probe runs only on devices in your allowlist
  • --connect-all and --probe-writes-all exist for authorized security testing but print a 5-second consent banner before bypassing the gate

This is the same posture industry pen-test frameworks default to. Connecting to or probing strangers' devices is legally grey in most jurisdictions — NetLens makes it explicit rather than implicit.

Platform-specific notes

macOS: BLEDevice.address is a CoreBluetooth peripheral UUID, not a MAC. Cross-scan dedupe relies on the advertisement fingerprint rather than the address. system_profiler escalation runs automatically (no sudo needed) to surface paired Classic Bluetooth devices invisible to BLE-only scanning.

macOS GATT enumeration depth varies. CoreBluetooth applies stricter privacy guards than Linux's BlueZ — for many devices (especially Apple peers) the OS hides the GATT service tree until the user explicitly pairs through System Settings → Bluetooth, in which case the writable-no-auth detection and --probe-writes confirmation will report svc_count=0. However, some non-Apple devices DO expose their full service tree to unpaired macOS peers — this was empirically observed during a Chromecast Ultra probe where 11 writable and 11 readable characteristics were enumerated with no pairing. So macOS isn't categorically blind, it's selectively so. NetLens emits an INFO finding (macOS GATT service enumeration limited) when zero services come back across all connected devices on macOS, but absence of that finding doesn't guarantee full visibility for every target. For consistent deep GATT audit across every device, Linux (BlueZ exposes the full tree without pairing) remains the recommended platform.

Linux: Real MACs are exposed for non-RPA devices. Active scanning needs root or CAP_NET_ADMIN — a warning panel surfaces when running unprivileged. If bettercap is on PATH, it's invoked automatically for deeper GATT enrichment. BlueZ exposes the full GATT service tree without pairing, so this is the recommended platform for deep audit (writable-no-auth detection, write-probe confirmation).

Windows: WinRT backend; real MACs exposed; no privilege requirement. GATT service enumeration depth varies by adapter driver; usually works without pairing on first-party Microsoft drivers but may require pairing on generic USB BLE dongles. No platform-specific escalation in v1.

Privilege Levels

NetLens auto-detects privileges and degrades gracefully:

Feature With sudo Without sudo
ARP scanning Raw socket (fast, complete) arp -a fallback (cached only)
Port scanning SYN scan + OS detection Connect scan only
Traffic capture Full packet sniffing Skipped
WiFi scanning Monitor mode / CoreWLAN Skipped
LLDP/CDP topology Passive frame sniffing Gateway-only
DHCP rogue detection Listens for rogue servers Skipped
Security checks All checks run All checks run
BLE audit (netlens bluetooth) Works on macOS/Windows without sudo; on Linux needs root or CAP_NET_ADMIN Same — privilege model is per-platform, not per-NetLens-feature

Output Formats

All commands support --json for machine-readable output. Terminal tables auto-adapt column visibility to your terminal width.

HTML Reports

netlens report produces a self-contained HTML file (CSS inline, no external resources). Auto-attaches a "Cross-Radio Correlations" section when a BLE scan exists within ±1 hour of the IP scan.

Flag Default What it does
--scan-id TEXT latest scan Scan ID to report on
--output, -o PATH docs/reports/ Output file path
--compare TEXT Second scan ID — generates a comparison report
--bluetooth-scan-id TEXT auto (±1h) Specific BLE scan to correlate against
--no-correlate off Skip Cross-Radio Correlations even if a BLE scan exists
# Latest scan, default path
uv run netlens report

# Specific scan (get IDs via `netlens history`)
uv run netlens report --scan-id abc123

# Comparison report (baseline vs current)
uv run netlens report --scan-id abc123 --compare def456

# Force a specific BLE scan to correlate
uv run netlens report --bluetooth-scan-id ble-987

# Skip Cross-Radio Correlations
uv run netlens report --no-correlate

# Custom output path
uv run netlens report -o my-report.html

Data Storage

What Location
Scan history + device profiles (IP & Bluetooth) ~/.netlens/history.db (SQLite, schema v4)
HTML reports docs/reports/ (relative to working directory)
GeoIP databases ~/.netlens/geoip/ (optional)
Bluetooth GATT allowlist ~/.netlens/bluetooth_allowlist.txt (optional)
Bluetooth owned-device list (tracking suppression) ~/.netlens/bluetooth_owned.txt (optional)
User-curated vulnerable chipset overlay ~/.netlens/bluetooth_vulnerable_chipsets_user.yaml (optional, merged on top of package-shipped DB)
User-curated companion-app overlay ~/.netlens/bluetooth_companion_apps_user.yaml (optional)

GeoIP Setup (Optional)

Traffic destination geolocation requires free MaxMind databases. Databases land in ~/.netlens/geoip/ and are read by the traffic-analysis stage.

  1. Create a free account at maxmind.com
  2. Generate a license key under My Account → Manage License Keys
  3. Run netlens setup-geoip with your credentials:
Flag Required What it does
--account-id TEXT Your MaxMind account ID
--license-key TEXT Your MaxMind license key
# One-time setup (or refresh — overwrites existing DBs)
uv run netlens setup-geoip --account-id 123456 --license-key XYZ

Default Port Set

NetLens scans 33 IoT-common ports by default:

21, 22, 23, 25, 53, 80, 81, 443, 554, 631, 1883, 1900,
3000, 3689, 5000, 5353, 5683, 6668, 8000, 8008, 8009,
8080, 8443, 8883, 9000, 9100, 49152, 49153, 49154

Use --full for all 65535 ports or --ports for a custom list.

Continuous Monitoring

netlens watch runs repeated scans at a fixed interval and alerts when devices appear, disappear, or change. Each cycle is saved to history. Ctrl+C to stop. Skips traffic + WiFi by default for faster cycles; opt back in with --with-* flags.

Flag Default What it does
NETWORK (positional) auto-detect Target network CIDR
--interval, -i INT 300 Seconds between scans
--skip-traffic / --with-traffic skip Toggle traffic capture per cycle
--skip-wifi / --with-wifi skip Toggle WiFi audit per cycle
--webhook-url URL POST a JSON diff payload on every change
NETLENS_WEBHOOK_URL (env) Same as --webhook-url; keeps URL out of shell history
# Default — scan every 5 minutes, alert in terminal
sudo uv run netlens watch

# Custom interval (10 minutes between cycles)
sudo uv run netlens watch -i 600

# Lightweight cycles — already the default; explicit for clarity
sudo uv run netlens watch --skip-traffic --skip-wifi

# Heaviest cycles — enable traffic + WiFi capture in each pass
sudo uv run netlens watch --with-traffic --with-wifi

# Push every diff to a webhook (ntfy / Pushover / Discord / Slack)
sudo uv run netlens watch --webhook-url https://ntfy.example.com/netlens

# Same, via env var
NETLENS_WEBHOOK_URL=https://ntfy.example.com/netlens sudo uv run netlens watch

The webhook POSTs JSON on each detected diff:

{
  "scan_id": "abc123",
  "timestamp": "2026-05-17T00:55:00Z",
  "new":     [{"ip": "...", "mac": "...", "vendor": "..."}],
  "removed": [{"ip": "...", "mac": "...", "vendor": "..."}],
  "changed": [{"ip": "...", "mac": "...", "changes": ["port 22 opened"]}],
  "truncated": false
}

TLS verification is on by default; redirects are disabled (follow_redirects=False) as defense in depth against attacker-controlled URL bouncing. Each category is capped at 100 entries — truncated: true is set if the diff overflowed. Failures log a warning and never crash the watch loop.

Cross-Radio Correlation

netlens correlate joins WiFi/IP devices with BLE devices from the same audit window — surfaces "your iPhone is at 192.168.1.42 on Wi-Fi and broadcasting BLE adverts as AA:BB:…" with no manual cross-referencing.

Flag Default What it does
--scan-id TEXT latest IP scan IP scan to correlate
--bluetooth-scan-id TEXT latest BLE scan BLE scan to correlate against
--threshold FLOAT 0.5 Minimum aggregate score to emit a correlation row
--window FLOAT 600.0 Time window (seconds) for temporal-proximity signal
# Run an IP scan and a BLE scan, then correlate
sudo uv run netlens scan
uv run netlens bluetooth --duration 30
uv run netlens correlate

# Lower threshold to see weaker matches (for forensic review)
uv run netlens correlate --threshold 0.3

# Larger time window when the two scans aren't back-to-back
uv run netlens correlate --window 1800

# Compare specific historical scans
uv run netlens correlate --scan-id ip-abc --bluetooth-scan-id ble-xyz

Heuristics with per-signal weights (capped at 1.0; emits any pair scoring ≥ --threshold):

Signal Weight Notes
Vendor match +0.40 IEEE OUI ↔ Bluetooth SIG company ID, normalised
Temporal proximity +0.30 → 0.0 Linear taper within --window seconds (default 600)
Advertised name ≈ hostname +0.30 Tolerant of .local and trailing dots
Microsoft Swift Pair friendly_name +0.20 bonus Stacks on top when present

BLE addresses are deliberately not used as join keys (they rotate every ~15 min on RPA, and macOS replaces them with a per-host UUID). The "Signals" column in the output shows which heuristics fired and by how much, so a correlation can be audited at a glance.

netlens report auto-attaches this section when a BLE scan exists within ±1 hour of the IP scan's start time — use --bluetooth-scan-id to force a specific one, or --no-correlate to skip entirely.

Extending the BLE Audit

netlens bluetooth --load-external-checks discovers community-authored checks in ~/.netlens/checks/*.py. Each module exposes a register() -> list[BluetoothCheck]:

# ~/.netlens/checks/my_check.py
from netlens.bluetooth.checks import BluetoothCheck, CheckContext
from netlens.models.bluetooth import BluetoothDevice
from netlens.models.finding import Finding, Severity

async def check_thing(device: BluetoothDevice, ctx: CheckContext) -> list[Finding]:
    if device.manufacturer_id == 0x1234:
        return [Finding(
            severity=Severity.HIGH,
            title="Custom Vendor X risk",
            description="…",
            evidence="…",
            remediation="…",
        )]
    return []

def register() -> list[BluetoothCheck]:
    return [BluetoothCheck(name="my_check", func=check_thing)]

The loader is opt-in (--load-external-checks flag with a 5-second consent banner) because it executes arbitrary Python from disk. Built-in check names cannot be shadowed; import errors, missing register(), and type mismatches are logged and skipped without crashing the scan.

Network Segmentation

netlens segment assigns devices to 4 zones — TRUSTED, SMART_HOME, IOT_ISOLATED, QUARANTINE — and emits firewall rules for inter-zone communication.

Flag Default What it does
--scan-id TEXT latest scan Scan ID to analyze
--verbose, -v off Show rationale for each zone assignment
--json off JSON output for piping into firewall config generators
# Recommendations from the latest scan
uv run netlens segment

# Detailed rationale for each device-to-zone assignment
uv run netlens segment -v

# Analyze a specific historical scan
uv run netlens segment --scan-id abc123

# JSON for piping into firewall config generators
uv run netlens segment --json > segment.json

Device Profiles

netlens devices manages persistent per-MAC device profiles (custom names, labels, notes, history). Four sub-subcommands; calling netlens devices with no subcommand is equivalent to list.

netlens devices list

Flag Default What it does
--limit, -n INT 100 Maximum profiles to show
--json off JSON output

netlens devices show <MAC>

Arg / Flag Default What it does
MAC (required) Device MAC address (e.g. AA:BB:CC:DD:EE:FF)
--history off Include per-scan timeline / baseline-drift view
--limit, -n INT 20 Max history entries to show
--json off JSON output

netlens devices edit <MAC>

Arg / Flag Default What it does
MAC (required) Device MAC address
--name TEXT Set custom display name
--label TEXT Add a label (repeatable)
--remove-label TEXT Remove a label (repeatable)
--notes TEXT Free-form notes for this device

netlens devices delete <MAC>

Arg / Flag Default What it does
MAC (required) Device MAC address
--yes, -y off Skip confirmation prompt
# List all known devices (also the default for `netlens devices`)
uv run netlens devices list

# Deep-dive into a specific device
uv run netlens devices show AA:BB:CC:DD:EE:FF

# Detailed view with per-scan timeline
uv run netlens devices show AA:BB:CC:DD:EE:FF --history

# Tag a device with a custom name + labels
uv run netlens devices edit AA:BB:CC:DD:EE:FF \
    --name "Living Room TV" --label smart-tv --label trusted

# Add a note
uv run netlens devices edit AA:BB:CC:DD:EE:FF \
    --notes "Powered off when guests leave"

# Remove a stale profile
uv run netlens devices delete AA:BB:CC:DD:EE:FF

Development

# Install dev dependencies
uv sync --group dev

# Run tests
uv run pytest -q

# Lint and format
uv run ruff check src/ tests/
uv run ruff format src/ tests/

Releasing

The repo-root VERSION file is the single source of truth. pyproject.toml, netlens.__version__, pip show netlens, and netlens --version all read from it. See docs/RELEASING.md for the cut-a-release runbook.

Technology Stack

Component Technology
Language Python 3.13+
Package manager uv
CLI framework Typer
Terminal UI Rich
Packet manipulation Scapy
Port scanning nmap via python-nmap
mDNS discovery zeroconf
UPnP discovery ssdp
SSH testing asyncssh
Telnet testing telnetlib3
SNMP queries pysnmp 7
BLE discovery + GATT (optional [bluetooth] extra) bleak
CVE lookup nvdlib (NVD API)
HTTP client httpx
Data models Pydantic
XML parsing defusedxml
MAC vendor mac-vendor-lookup
GeoIP geoip2 (MaxMind)
Reports Jinja2
Database SQLite (stdlib)
Linter/formatter ruff
Testing pytest

License

Private — personal/home network security auditing tool.

About

NetLens is a Python CLI tool that scans your local network to discover every device, fingerprint its type and firmware, assess its security posture with actionable remediation, analyze traffic patterns, and track changes over time.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors