Defensive, read-only Python utility for passive network-device inventory, classification, and reporting.
Synthetic data only in examples. All sample inputs, demo policies, and screenshots use documentation IP ranges (
192.0.2.0/24,198.51.100.0/24,203.0.113.0/24,2001:db8::/32) and locally administered MAC addresses. No real network identifiers are included in the repository.
network-device-watch turns passive observation snapshots into a structured, queryable inventory with advisory classification labels — without scanning, probing, or enforcing anything on your network. It is designed for home-lab operators, sysadmins, and security-minded users who want visibility and audit trails from data they already have (router leases, ARP tables, neighbor caches) rather than active discovery.
Local networks grow quietly. Devices appear on DHCP leases, ARP caches, and neighbor tables long before anyone documents them. Without a lightweight inventory:
- Unknown endpoints go unnoticed until something goes wrong.
- Guest, IoT, and administrative devices are hard to distinguish from policy alone.
- Comparing "what changed since yesterday" requires manual diffing of raw logs.
- Sharing inventory data risks leaking sensitive MAC and IP identifiers.
Most network tools either actively probe hosts (which many environments forbid) or enforce access control (which this project explicitly does not do).
- Passive visibility — ingest read-only snapshots; never ping, scan, or connect to remote hosts.
- Advisory classification — label devices as trusted, guest, unknown, quarantine, or ignored based on operator-defined policies.
- Durable inventory — persist devices, observations, and events in SQLite with idempotent ingestion.
- Privacy-aware reporting — export masked, hashed, or full identifiers according to context.
- Automation-friendly — predictable exit codes for CI, cron, and monitoring hooks.
- Zero runtime dependencies — stdlib-only Python 3.11+ on Windows and Linux.
| Area | Capability |
|---|---|
| Ingestion | CSV, JSON, dnsmasq leases, Windows arp -a, Linux ip neigh |
| Local collection | Read OS neighbor cache only (collect-local) |
| Classification | Policy rules: exact MAC, prefix, hostname regex, IP network, source type, vendor label |
| Comparison | Diff two snapshots without touching the database |
| Reports | JSON and HTML inventory reports with privacy modes |
| Events | First seen, IP/hostname changes, classification changes, parser warnings |
| Notifications | Optional Telegram alerts (disabled by default) |
| CLI | ingest, collect-local, report, compare, list-devices, list-events, classify, validate-config, generate-demo |
network-device-watch is read-only and non-enforcing.
- It does not ping, port-scan, ARP-scan, or connect to discovered hosts.
- It does not block, quarantine, firewall, or modify network traffic.
- Classification labels (
trusted,guest,unknown,quarantine,ignored) are inventory advisory metadata only — they exist to help operators prioritize review, not to take automated action. - Collectors read the local OS neighbor cache (
Get-NetNeighbor/arp -aon Windows;ip neighon Linux) and nothing more. - The SQLite database stores what you ingest; privacy transforms apply to reports and CLI output, not to the underlying enforcement layer (because there is none).
Supported passive observation formats:
| Format | Typical origin | CLI --source-format |
|---|---|---|
| CSV | Exported spreadsheets, custom scripts | csv |
| JSON | API exports, structured logs | json |
| dnsmasq | Router/DHCP lease file | dnsmasq |
| Windows ARP | arp -a output |
windows-arp |
| Linux neighbor | ip neigh output |
linux-neigh |
See docs/data-sources.md for field mappings and parser behaviour.
Five advisory labels, evaluated by priority-ordered policy rules:
| Label | Meaning |
|---|---|
trusted |
Operator-confirmed expected device |
guest |
Guest or limited-trust device |
unknown |
Default when no rule matches — review recommended |
quarantine |
Flagged for immediate operator review (advisory only) |
ignored |
Excluded from operational concern |
See docs/classification-model.md for rule types and evaluation order.
Ingestion and reclassification emit structured events (device_first_seen, ip_address_changed, unknown_device_detected, etc.) with severities (info, warning, critical, error). Events are stored in SQLite and surfaced in reports and optional Telegram notifications.
git clone https://github.com/salvomazzaglia/network-device-watch.git
cd network-device-watch
python -m pip install -e .
# Generate synthetic demo inputs (no network access)
network-device-watch generate-demo
# Validate configuration
network-device-watch validate-config --config config/demo.config.json
# Ingest a synthetic snapshot
network-device-watch ingest \
--source examples/input/snapshot-01.csv \
--source-format csv \
--config config/demo.config.json
# List devices (masked privacy)
network-device-watch list-devices --database temp/demo-inventory.db
# Generate HTML/JSON report
network-device-watch report \
--database temp/demo-inventory.db \
--output-directory temp/inventory-reportRequirements: Python 3.11 or 3.12, Windows or Linux.
# Editable install (development)
python -m pip install -e ".[dev]"
# Runtime only
python -m pip install .The console script network-device-watch is registered automatically. You can also run:
python -m network_device_watch --versionThe repository ships a complete synthetic demo that never touches a real network:
# 1. Generate deterministic synthetic observation files
network-device-watch generate-demo --output examples/input
# 2. Ingest first snapshot
network-device-watch ingest \
--source examples/input/snapshot-01.csv \
--source-format csv \
--config config/demo.config.json
# 3. Ingest second snapshot (simulates a later observation)
network-device-watch ingest \
--source examples/input/snapshot-02.csv \
--source-format csv \
--config config/demo.config.json
# 4. Compare snapshots without database writes
network-device-watch compare \
--before examples/input/snapshot-01.csv \
--after examples/input/snapshot-02.csv \
--source-format csv \
--policies config/demo-policies.json \
--output-directory temp/comparison-report
# 5. Generate inventory report
network-device-watch report \
--database temp/demo-inventory.db \
--output-directory temp/inventory-report \
--privacy-mode masked| File | Purpose |
|---|---|
config/demo-policies.json |
Used only with synthetic demo data and sample reports |
config/policies.example.json |
Neutral template — copy this for local/production policies |
config/local-policies.json |
Your local policies (gitignored) |
Copy-Item config/policies.example.json config/local-policies.jsoncp config/policies.example.json config/local-policies.jsonPoint classification.policy_file at config/local-policies.json, or pass --policies config/local-policies.json.
network-device-watch ingest \
--source /path/to/observations.csv \
--source-format csv \
--config config/default.config.json \
[--policies config/policies.json] \
[--database data/inventory.db] \
[--observed-at 2026-01-15T10:00:00Z] \
[--dry-run] \
[--notify] \
[--verbose] \
[--fail-on-warning]network-device-watch collect-local \
--config config/default.config.json \
[--dry-run] [--notify] [--verbose] [--fail-on-warning]network-device-watch report \
--database data/inventory.db \
[--output-directory reports] \
[--format all|json|html] \
[--since 2026-01-01T00:00:00Z] \
[--classification unknown] \
[--privacy-mode masked|hashed|full] \
[--no-include-inactive]network-device-watch compare \
--before earlier.csv \
--after later.csv \
--source-format csv \
--policies config/policies.json \
[--output-directory reports] \
[--privacy-mode masked|full]network-device-watch list-devices --database data/inventory.db [--active-only] [--limit 50]
network-device-watch list-events --database data/inventory.db [--severity warning] [--limit 50]
network-device-watch classify --database data/inventory.db --policies config/policies.json [--dry-run]
network-device-watch validate-config --config config/default.config.json
network-device-watch versionFull CLI reference: run network-device-watch --help or network-device-watch <command> --help.
Configuration is a single JSON file with required sections: database, inventory, classification, privacy, notifications, reporting, collectors, logging.
Example (config/default.config.json):
{
"database": { "path": "data/inventory.db" },
"inventory": {
"active_window_hours": 24,
"preserve_first_seen": true,
"ignore_incomplete_neighbors": true
},
"classification": {
"policy_file": "config/policies.json",
"default": "unknown"
},
"privacy": {
"default_report_mode": "masked",
"include_hostnames": true,
"include_vendor_labels": true
},
"notifications": {
"enabled": false,
"provider": "telegram",
"notify_on": [
"unknown_device_detected",
"quarantine_device_detected",
"classification_changed"
]
},
"reporting": {
"formats": ["json", "html"],
"output_directory": "reports"
},
"collectors": { "command_timeout_seconds": 10 },
"logging": { "level": "INFO" }
}Policies are defined in a separate JSON file referenced by classification.policy_file. Each rule has an id, priority, match block, classification, and reason. Rules are evaluated in ascending priority order; the first enabled match wins.
Example (synthetic MAC):
{
"id": "trusted-admin-workstation",
"enabled": true,
"priority": 10,
"match": { "type": "exact_mac", "value": "02:00:00:00:00:01" },
"classification": "trusted",
"reason": "Exact MAC match for synthetic admin workstation"
}See docs/classification-model.md, config/policies.example.json (template), and config/demo-policies.json (synthetic demo only).
Inventory is stored in SQLite (schema version 1) with three main tables:
devices— one row per normalized MAC addressobservations— individual sighting records (idempotent viauniqueness_key)events— audit trail of inventory changes
The database always stores full identifiers regardless of report privacy mode.
Three report privacy modes:
| Mode | MAC example | IP example |
|---|---|---|
masked (default) |
02:00:00:**:**:** |
192.0.2.xxx |
hashed |
SHA-256 digest | SHA-256 digest |
full |
Complete MAC | Complete IP |
Set NETWORK_DEVICE_WATCH_HASH_SALT for production hashed mode. Telegram notifications always use masked identifiers.
See docs/privacy-model.md and docs/security-and-privacy.md.
Optional Telegram notifications are disabled by default. When enabled, credentials come from environment variables only:
export NETWORK_DEVICE_WATCH_TELEGRAM_BOT_TOKEN="your-bot-token"
export NETWORK_DEVICE_WATCH_TELEGRAM_CHAT_ID="your-chat-id"Pass --notify on ingest, collect-local, or classify to trigger notifications for events listed in notifications.notify_on.
See docs/telegram-notifications.md.
The report command generates:
inventory-report.json— structured device and event datainventory-report.html— human-readable summary with classification breakdown
Reports include privacy notices, classification counts, recent events, and operator recommendations. Use --synthetic (internal/demo flag) to mark demonstration reports.
See docs/results-explained.md.
| Code | Meaning | Typical cause |
|---|---|---|
0 |
OK | Healthy run, or collector unavailable (non-fatal) |
1 |
Warning | Unknown devices, parser warnings, non-critical events |
2 |
Critical | Quarantine events; or Warning with --fail-on-warning |
3 |
Error | Config/parse/DB failure, unexpected exception |
compare returns 0 on successful comparison regardless of diff content.
flowchart TB
subgraph inputs [Passive Inputs]
CSV[CSV snapshot]
JSON[JSON snapshot]
DNS[dnsmasq leases]
WIN[Windows arp -a]
LIN[Linux ip neigh]
LOC[collect-local OS cache]
end
subgraph core [network-device-watch Core]
Parsers[Parsers]
Classifier[Policy Classifier]
Inventory[Inventory Engine]
Events[Event Generator]
DB[(SQLite)]
Reports[JSON / HTML Reports]
Notify[Telegram optional]
end
CSV --> Parsers
JSON --> Parsers
DNS --> Parsers
WIN --> Parsers
LIN --> Parsers
LOC --> Parsers
Parsers --> Inventory
Inventory --> Classifier
Classifier --> Inventory
Inventory --> Events
Inventory --> DB
Events --> DB
DB --> Reports
Events --> Notify
Compare[compare command] --> Parsers
Parsers --> Compare
Compare --> Reports
See docs/architecture.md for module-level detail.
python -m pip install -e ".[dev]"
python -m pytest
python scripts/run_ci.py # lint + format + mypy + pytestCI runs on Ubuntu and Windows with Python 3.11 and 3.12.
See docs/testing.md.
- Read-only design: no network enforcement, no active probing.
- Subprocess collectors use fixed argv with
shell=False. - Database path validation rejects executable extensions.
- Telegram tokens are read from environment variables and redacted in error messages.
- Full identifiers in SQLite require filesystem access controls.
See SECURITY.md and docs/security-and-privacy.md.
- Windows ARP parser expects English
arp -alayout; localized output may fail. - dnsmasq lease expiry timestamps are not used as observation time.
- MAC OUI prefixes are not reliable device-type signals; vendor labels are static policy fields only.
comparewith--privacy-mode hashedapplies masked transforms (not SHA-256 hashes)."Not observed in later snapshot"does not mean a device is offline.- Only Telegram is supported as a notification provider.
refresh_active_flags()logic exists internally but has no dedicated CLI command.
Planned directions (not committed to v1.x):
- Additional notification providers (webhook, email)
- Scheduled ingestion helpers
- Policy rule templates and validation UI
- Improved localized ARP/neighbor parser support
- Optional OUI lookup as an explicit, opt-in enrichment step
Contributions welcome — see CONTRIBUTING.md.
This project demonstrates:
- Defensive security tooling design (passive-only, advisory classification)
- Structured CLI design with automation-friendly exit codes
- SQLite schema design with idempotent ingestion
- Privacy-preserving report generation (masked / hashed / full)
- Cross-platform read-only collectors (Windows PowerShell + Linux
ip neigh) - Parser architecture for heterogeneous network observation formats
- Stdlib-only Python packaging with typed, tested, CI-gated code
See CONTRIBUTING.md for development setup, coding standards, and pull request guidelines.
MIT License — see LICENSE.
Salvatore Mazzaglia
- GitHub: @salvomazzaglia
- Repository: github.com/salvomazzaglia/network-device-watch
