{"id":50792854,"url":"https://github.com/cryptojones/networkinventoryagent","last_synced_at":"2026-06-12T12:02:24.188Z","repository":{"id":360557031,"uuid":"1238050415","full_name":"CryptoJones/NetworkInventoryAgent","owner":"CryptoJones","description":"Network Inventory Agent","archived":false,"fork":false,"pushed_at":"2026-06-05T13:12:50.000Z","size":500,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T15:09:40.752Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/CryptoJones.png","metadata":{"files":{"readme":"README.md","changelog":"ChangeLog.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-13T19:06:27.000Z","updated_at":"2026-06-05T13:12:54.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/CryptoJones/NetworkInventoryAgent","commit_stats":null,"previous_names":["cryptojones/networkinventoryagent"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/CryptoJones/NetworkInventoryAgent","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CryptoJones%2FNetworkInventoryAgent","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CryptoJones%2FNetworkInventoryAgent/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CryptoJones%2FNetworkInventoryAgent/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CryptoJones%2FNetworkInventoryAgent/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CryptoJones","download_url":"https://codeload.github.com/CryptoJones/NetworkInventoryAgent/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CryptoJones%2FNetworkInventoryAgent/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34243053,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-12T02:00:06.859Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-12T12:01:45.882Z","updated_at":"2026-06-12T12:02:24.060Z","avatar_url":"https://github.com/CryptoJones.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NetworkInventoryAgent\n\nA lightweight, autonomous network inventory agent that discovers, catalogs, and reports on devices and assets across your network infrastructure.\n\n## Overview\n\nNetworkInventoryAgent continuously scans your network to build and maintain an up-to-date inventory of all connected devices. It identifies hosts, open ports, running services, operating systems, and hardware details — giving you a living map of your network without requiring manual audits.\n\nThe system is designed to run as **two cooperating agent instances** — named **Wintermute** and **Neuromancer** — that scan the same subnets independently and continuously sanity-check each other. If either agent crashes, stalls, or starts reporting wildly different data, the other detects it and logs a clear warning. This mutual watchdog architecture means the inventory is never silently wrong.\n\n## Features\n\n- **Active discovery** — concurrent TCP-probe scanning across configurable CIDR ranges to find live hosts. Optional deep TCP and UDP probe passes per profile.\n- **Asset fingerprinting** — banner-grab on SSH, FTP, SMTP, POP3, IMAP, HTTP, HTTPS (with TLS cert peek), MySQL handshake, PostgreSQL (SSLRequest probe), Redis (`INFO`), Memcached (`version`), VNC (RFB greeting), RDP (X.224), MSSQL (TDS pre-login), MongoDB (wire `isMaster`), Telnet, plus UDP DNS and NTP (stratum) probes. Stored per-port in `Port.Service`.\n- **Device-type classifier** — heuristic rules over (vendor, OS banner, open ports) tag hosts as printer / router / hypervisor / windows-host / windows-server / windows-dc / nas / database (mysql|postgres|…) / mail-server / dns-server / kubernetes-node / container-host / camera / linux-host / appliance / iot-broker / embedded.\n- **MAC + vendor enrichment** — neighbour-cache lookup on Linux (`/proc/net/arp`), macOS (routing socket) and Windows (`GetIpNetTable`), all shell-free + embedded OUI prefix table for ~90 common vendors, including major IP-camera (Hikvision/Dahua/Axis), NAS (Synology/QNAP/WD), networking (Ubiquiti), and IoT (Espressif) brands that also drive device classification.\n- **Per-subnet scan profiles** — aggressive hourly deep scans on critical infra, lazy daily liveness on guest networks, all in one config.\n- **Change detection + alerts** — diffs host inventory each cycle; fires `host.discovered` / `host.vanished` events to HTTP webhook and/or RFC 5424 syslog.\n- **JSON query API** — `/api/v1/hosts` with filters (vendor, device type, hostname, subnet, port) and pagination; `/api/v1/hosts/{ip}` with nested ports; `/api/v1/scans` paginated scan history (optional `subnet` filter).\n- **Continuous monitoring** — periodic re-scans detect new devices, removed devices, and configuration changes over time.\n- **Mutual watchdog** — two agent instances cross-check each other for liveness, scan freshness, and inventory consistency. Optional mTLS between peers.\n- **Web admin console** — dark-themed browser UI with dashboard, host inventory, per-host port detail, scan history, watchdog peer status; auto-starts alongside each agent.\n- **Terminal UI console** — full-featured Bubbletea TUI (`cmd/console`) providing the same views as the web console; connects directly to any agent's SQLite database.\n- **Prometheus `/metrics`** — counters for scans, probes, DB errors, watchdog events, alerts; gauges for host count and peer-up state. Dependency-free exposer.\n- **OpenTelemetry tracing** — OTLP/HTTP exporter, W3C TraceContext propagation across the watchdog peer hop.\n- **Structured logging** — human-readable text or machine-readable JSON log output via `log/slog`.\n- **Graceful shutdown** — SIGINT / SIGTERM cancel in-flight scans cleanly before exit.\n- **Multi-platform releases** — signed binaries (cosign keyless OIDC) for linux/darwin/windows × amd64/arm64, plus a multi-arch Docker image on `ghcr.io`. CycloneDX SBOMs per archive.\n- **Low footprint** — no external server process; the database is a single SQLite file.\n\n## Requirements\n\n- Go 1.25+\n- Network access to the target subnets\n\nNo C toolchain is required. The SQLite driver (`modernc.org/sqlite`) is pure Go.\n\n## Installation\n\n### Docker (multi-arch, signed)\n\n```bash\ndocker pull ghcr.io/cryptojones/networkinventoryagent:latest\ndocker run --rm ghcr.io/cryptojones/networkinventoryagent:latest -version\n```\n\nThe `:latest` and `:\u003cversion\u003e` tags both point at multi-arch manifests\n(linux/amd64 + linux/arm64); your Docker client picks the right one for\nthe host. The manifests are signed with `cosign` keyless OIDC — verify with:\n\n```bash\ncosign verify \\\n  --certificate-identity-regexp 'https://github.com/CryptoJones/NetworkInventoryAgent/' \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  ghcr.io/cryptojones/networkinventoryagent:\u003cversion\u003e\n```\n\nThe image's default entrypoint is `agent` (standalone). To run the paired\nWintermute/Neuromancer mode, override the entrypoint:\n\n```bash\ndocker run --rm \\\n  --entrypoint /usr/local/bin/wintermute \\\n  ghcr.io/cryptojones/networkinventoryagent:latest -version\n```\n\n### Pre-built binaries (signed)\n\nTagged releases at \u003chttps://github.com/CryptoJones/NetworkInventoryAgent/releases\u003e\nship binaries for linux/darwin/windows × amd64/arm64. Every archive contains\na CycloneDX SBOM and every artefact is signed with `cosign` keyless OIDC\n(via GitHub Actions). Verify before running:\n\n```bash\ncosign verify-blob \\\n  --certificate-identity-regexp 'https://github.com/CryptoJones/NetworkInventoryAgent/' \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate networkinventoryagent_\u003cver\u003e_linux_amd64.tar.gz.pem \\\n  --signature   networkinventoryagent_\u003cver\u003e_linux_amd64.tar.gz.sig \\\n                networkinventoryagent_\u003cver\u003e_linux_amd64.tar.gz\n```\n\n### Build from source\n\n```bash\ngit clone https://codeberg.org/Ronin48/NetworkInventoryAgent.git\ncd NetworkInventoryAgent\ngo build -o wintermute  ./cmd/wintermute\ngo build -o neuromancer ./cmd/neuromancer\ngo build -o console     ./cmd/console\n```\n\n### Windows Installation\n\nThe project builds natively on Windows using the standard Go toolchain. You can either:\n- Use Git Bash/MSYS2/WSL to run the provided shell scripts\n- Or execute the equivalent commands manually in Command Prompt/PowerShell\n\nTo build natively on Windows:\n```cmd\ngo build -o wintermute.exe  ./cmd/wintermute\ngo build -o neuromancer.exe ./cmd/neuromancer\ngo build -o console.exe     ./cmd/console\n```\n\nFor cross-compilation from Linux/macOS to Windows:\n```bash\nGOOS=windows GOARCH=amd64 go build -o wintermute.exe  ./cmd/wintermute\nGOOS=windows GOARCH=amd64 go build -o neuromancer.exe ./cmd/neuromancer\n```\n\nOr use `make`:\n\n```bash\nmake build   # compiles all binaries\nmake test    # runs the full test suite with the race detector\nmake lint    # gofmt + go vet\n```\n\n## Docker\n\nThe repository ships a multi-stage `Dockerfile` and a `docker-compose.yml` that runs the Wintermute/Neuromancer pair.\n\n### Quick start\n\n```bash\ndocker compose up --build -d\n```\n\nThis compiles both agent binaries in a `golang:1.25-bookworm` build stage and runs them in a minimal `alpine:3.20` image as a non-root user. Two containers start:\n\n| Container | Health port | Admin console | Watchdog peer |\n|-----------|------------|---------------|---------------|\n| `wintermute` | `8080` | `9090` | `http://neuromancer:8081` |\n| `neuromancer` | `8081` | `9091` | `http://wintermute:8080` |\n\nDatabases are written to named Docker volumes (`wintermute-db`, `neuromancer-db`) and persist across restarts.\n\n### Running a single agent\n\n```bash\ndocker run -d \\\n  -v \"$PWD/configs/wintermute.docker.json:/etc/inventory/config.json:ro\" \\\n  -v inventorydata:/data \\\n  -p 8080:8080 \\\n  -p 9090:9090 \\\n  --entrypoint /usr/local/bin/wintermute \\\n  networkinventoryagent -config /etc/inventory/config.json\n```\n\n### Make targets\n\n| Target | Description |\n|--------|-------------|\n| `make docker-build` | Build the image locally |\n| `make docker-up` | Start the Wintermute/Neuromancer pair in the background |\n| `make docker-down` | Stop and remove containers |\n| `make docker-logs` | Tail combined logs from both agents |\n\n### Docker-specific config\n\nThe configs in `configs/*.docker.json` differ from the local configs in four ways:\n\n1. `health.addr` binds to `0.0.0.0:\u003cport\u003e` so Docker's network stack can route traffic into the container.\n2. `admin.addr` binds to `0.0.0.0:9090` so the admin console is reachable from the host.\n3. `watchdog.peer_addr` uses the Compose service name (`http://neuromancer:8081`) instead of `localhost`.\n4. `database.path` writes to `/data/\u003cname\u003e.db` inside the mounted volume.\n\nEdit the `subnets` list in these files before deploying.\n\n## Running the agents locally\n\n### Quick start with the startup script\n\nThe easiest way to run the agents locally is `start.sh`. It builds the binaries, optionally updates the subnet list in your config files, then starts the agents and prints the console URLs. Press `Ctrl+C` to stop everything cleanly.\n\n**Prerequisites:** Go 1.25+ and `jq` must be on your `PATH`.\n\n```bash\n# Interactive — prompts for mode and subnets\n./start.sh\n\n# Non-interactive examples\n./start.sh --mode paired     --subnet 192.168.1.0/24\n./start.sh --mode standalone --subnet 10.0.0.0/24 --subnet 10.1.0.0/24\n\n# Build binaries only, do not start agents\n./start.sh --build-only\n```\n\n#### Startup script options\n\n| Flag | Values | Description |\n|------|--------|-------------|\n| `-m`, `--mode` | `paired` \\| `standalone` | Agent mode (default: interactive prompt) |\n| `-s`, `--subnet` | CIDR, e.g. `10.0.0.0/24` | Subnet to scan — repeat for multiple subnets |\n| `-b`, `--build-only` | — | Build binaries and exit without starting |\n| `-h`, `--help` | — | Show usage |\n\n**Paired mode** starts Wintermute and Neuromancer as a mutual-watchdog pair (recommended). **Standalone mode** starts a single agent with no watchdog peer.\n\n### Manual startup\n\nIf you prefer to start the agents yourself, build and run them directly.\n\n**Requirements:** Go 1.25+. No C toolchain needed.\n\nEdit the `subnets` list in the relevant config file first, then:\n\n```bash\n# Build\ngo build -o wintermute  ./cmd/wintermute\ngo build -o neuromancer ./cmd/neuromancer\ngo build -o agent       ./cmd/agent\ngo build -o console     ./cmd/console\n\n# Paired mode (two terminals)\n./wintermute  -config configs/wintermute.json   # Terminal 1\n./neuromancer -config configs/neuromancer.json  # Terminal 2\n\n# Standalone mode\n./agent -config configs/agent.json\n```\n\nEach agent:\n\n1. Opens its own SQLite database\n2. Starts an HTTP health server (Wintermute on `127.0.0.1:8080`, Neuromancer on `127.0.0.1:8081`)\n3. Starts the web admin console (Wintermute on `127.0.0.1:9090`, Neuromancer on `127.0.0.1:9091`)\n4. Launches a watchdog goroutine pointed at its partner's health server\n5. Runs the scan loop in the foreground until it receives a signal\n\nReady-to-use configs are in `configs/`. Press `Ctrl+C` to stop an agent cleanly.\n\n## Admin console\n\nEach agent automatically starts a browser-based admin console alongside the scan loop. The console does not require any additional setup — open the address logged at startup to explore the current inventory.\n\n### Web console\n\n| Page | URL | Description |\n|------|-----|-------------|\n| Dashboard | `/` | Summary cards and latest 10 scans and hosts; auto-refreshes every 30 s |\n| Host inventory | `/hosts` | List of discovered hosts with metadata; paginated (`?limit=`, `?offset=`, default 100) |\n| Host detail | `/hosts/{ip}` | Per-host metadata and open port table |\n| Scan history | `/scans` | Subnet sweeps with duration and status; paginated (`?limit=`, `?offset=`, default 100) |\n\n### Terminal UI console\n\nThe `console` binary connects directly to any agent's SQLite database and provides the same views in a Bubbletea TUI. It opens the database read-only so it is safe to run against a live agent's database file.\n\n```bash\n./console -db wintermute.db\n```\n\n| Key | Action |\n|-----|--------|\n| `1` | Dashboard |\n| `2` | Host inventory |\n| `3` | Scan history |\n| `Enter` | Drill into host detail (ports) |\n| `Esc` / `Backspace` | Back to host list |\n| `r` | Refresh current view |\n| `q` / `Ctrl+C` | Quit |\n\n## How the mutual watchdog works\n\nEvery `watchdog.interval` seconds, each agent performs three checks against its partner:\n\n### 1. Liveness\n\n```\nGET /health  →  200 OK (healthy) | 503 Service Unavailable (unhealthy)\n```\n\nIf the peer fails to respond or returns a non-200 status, the failure is logged as a warning. After `max_failures` consecutive failures the peer is declared **DOWN** and an error is logged. The watchdog never kills or restarts the peer — that is left to an external supervisor (systemd, Docker, Kubernetes).\n\n### 2. Freshness\n\n```\nGET /status  →  JSON { last_scan_at, scan_count, host_count, ... }\n```\n\nIf the peer's `last_scan_at` timestamp is older than `2 × scanner.scan_interval`, the peer is considered stale and a warning is logged. This catches a peer that is alive and responding to pings but whose scan loop has silently stopped making progress.\n\n### 3. Consistency\n\nIf both agents have completed at least one scan, their `host_count` values are compared. If the percentage difference exceeds `max_host_drift_pct`, a warning is logged:\n\n```\ndrift_pct = |local_hosts - peer_hosts| / max(local_hosts, peer_hosts) × 100\n```\n\nThis catches split-brain scenarios where both agents are running but scanning different effective subsets of the network (e.g., due to a routing change or misconfiguration).\n\n## Configuration\n\nEach agent reads a JSON config file and then applies environment variable overrides on top. Environment variables always win, which makes the agents suitable for Docker and Kubernetes deployments.\n\n### Full config reference\n\n```json\n{\n  \"database\": {\n    \"path\": \"wintermute.db\"\n  },\n  \"scanner\": {\n    \"subnets\": [\"192.168.1.0/24\", \"10.0.0.0/24\"],\n    \"scan_interval\": \"5m\",\n    \"timeout\": \"2s\",\n    \"workers\": 50,\n    \"max_hosts\": 65535\n  },\n  \"log\": {\n    \"level\": \"info\",\n    \"format\": \"text\"\n  },\n  \"health\": {\n    \"addr\": \"127.0.0.1:8080\"\n  },\n  \"admin\": {\n    \"addr\": \"127.0.0.1:9090\"\n  },\n  \"watchdog\": {\n    \"peer_addr\": \"http://localhost:8081\",\n    \"interval\": \"30s\",\n    \"max_host_drift_pct\": 50.0,\n    \"max_failures\": 3\n  }\n}\n```\n\n| Key | Default | Description |\n|-----|---------|-------------|\n| `database.path` | `inventory.db` | SQLite database file. Use `:memory:` for tests. |\n| **Scanner — global defaults** | | |\n| `scanner.subnets` | `[]` | Legacy flat CIDR list. Mutually exclusive with `scanner.profiles`. |\n| `scanner.profiles` | `[]` | Per-subnet override list (see below). |\n| `scanner.scan_interval` | `5m` | How often to re-scan; default for any profile that doesn't set its own. |\n| `scanner.timeout` | `2s` | Per-host TCP probe timeout; also bounds reverse-DNS (PTR) lookups. |\n| `scanner.workers` | `50` | GLOBAL concurrent probe cap across every subnet (not per-subnet). |\n| `scanner.max_hosts` | `65535` | Maximum usable addresses per subnet; larger subnets are rejected. |\n| `scanner.probe_ports` | `[22, 80, 443, 8080]` | TCP liveness ports — host alive if any answer. |\n| `scanner.deep_probe` | `false` | Second-pass scan of `deep_probe_ports` on every live host. |\n| `scanner.deep_probe_ports` | `top-services list` | TCP ports for the deep pass when `deep_probe` is on. |\n| `scanner.udp_ports` | `[]` | UDP ports to probe per live host. Empty disables UDP probing. |\n| `scanner.enrich_arp` | `false` | Populate Host.MACAddress + Vendor from the OS neighbour cache (Linux `/proc/net/arp`, macOS routing socket, Windows `GetIpNetTable`). No-op on other platforms. |\n| `scanner.host_ttl` | `0` (disabled) | Hosts not seen within this duration are deleted at the end of each cycle. |\n| `scanner.scan_history_ttl` | `0` (disabled) | Scan-history rows older than this duration are deleted at the end of each cycle, bounding the `scans` table and `/scans` view. |\n| **Scanner — per-subnet profile (each item in `scanner.profiles`)** | | |\n| `subnet` | required | CIDR for this profile. Must be unique. |\n| `scan_interval` | inherits global | Per-profile scan cadence. |\n| `timeout` | inherits global | Per-profile dial budget. |\n| `probe_ports` | inherits global | Per-profile liveness ports. |\n| `deep_probe` | inherits global | Per-profile deep probing (bool). |\n| `deep_probe_ports` | inherits global | Per-profile deep ports. |\n| `udp_ports` | inherits global | Per-profile UDP ports. |\n| `enrich_arp` | inherits global | Per-profile ARP enrichment (bool). |\n| **Log** | | |\n| `log.level` | `info` | Log verbosity: `debug`, `info`, `warn`, `error`. |\n| `log.format` | `text` | Log format: `text` (human) or `json` (machine). |\n| **Health server** | | |\n| `health.addr` | `127.0.0.1:8080` | Listen address for `/health`, `/status`, `/metrics`. |\n| `health.auth_token` | — | Bearer token; required when `health.addr` is off-loopback. |\n| `health.tls_cert_path` | — | When set with `tls_key_path`, serves HTTPS. |\n| `health.tls_key_path` | — | Private key matching `tls_cert_path`. |\n| `health.client_ca_path` | — | When set, requires mTLS (clients must present a cert signed by this CA). |\n| **Admin console** | | |\n| `admin.addr` | `127.0.0.1:9090` | Listen address for the admin console + `/api/v1/*`. |\n| `admin.auth_token` | — | Shared secret gating the whole console. Required when `admin.addr` is off-loopback. Clients send `Authorization: Bearer \u003ctoken\u003e` or HTTP Basic with the token as the password. |\n| **Watchdog** | | |\n| `watchdog.peer_addr` | — | Base URL of the partner agent's health server. |\n| `watchdog.peer_token` | — | Bearer token sent to the peer. Must match peer's `health.auth_token`. |\n| `watchdog.interval` | `30s` | How often the watchdog checks the partner. |\n| `watchdog.max_host_drift_pct` | `50.0` | Max % host-count difference before a warning. |\n| `watchdog.max_failures` | `3` | Consecutive liveness failures before declaring peer DOWN. |\n| `watchdog.tls.ca_cert_path` | — | Project CA the peer's cert must chain to. |\n| `watchdog.tls.client_cert_path` | — | Client cert for mTLS to the peer. |\n| `watchdog.tls.client_key_path` | — | Client key matching `client_cert_path`. |\n| `watchdog.tls.server_name` | — | SNI / cert-verification hostname override. |\n| **Tracing** | | |\n| `tracing.endpoint` | — | OTLP/HTTP collector URL. Empty = no-op exporter (instrumentation active, spans discarded). |\n| **Alerts** | | |\n| `alerts.webhook.url` | — | HTTP POST target for host.discovered / host.vanished events. Must be `http`/`https`; scheme-validated at startup. |\n| `alerts.webhook.auth_header` | — | Verbatim `Authorization` header (e.g. `Bearer abc123`). |\n| `alerts.syslog.addr` | — | `udp://host:514` or `tcp://host:514`. RFC 5424. Scheme-validated at startup. |\n| `alerts.syslog.tag` | `network-inventory` | APP-NAME field. |\n| `alerts.syslog.facility` | `16` (local0) | RFC 5424 facility number 0..23. |\n\nDuration values in the JSON config accept human-readable strings (`\"5m\"`, `\"30s\"`, `\"2h\"`) in addition to raw nanosecond integers.\n\n#### Per-subnet profile example\n\nAggressive hourly deep scans on critical infrastructure, lazy daily liveness on guest network:\n\n```json\n{\n  \"scanner\": {\n    \"profiles\": [\n      { \"subnet\": \"10.0.0.0/24\", \"scan_interval\": \"1h\", \"deep_probe\": true, \"enrich_arp\": true },\n      { \"subnet\": \"192.168.99.0/24\", \"scan_interval\": \"24h\" }\n    ],\n    \"scan_interval\": \"5m\",\n    \"timeout\": \"2s\",\n    \"workers\": 50,\n    \"host_ttl\": \"168h\"\n  }\n}\n```\n\nProfiles inherit any field they don't set from the `scanner.*` globals.\n`scanner.subnets` and `scanner.profiles` are mutually exclusive — boot\nfails fast if both are set.\n\n### Environment variable overrides\n\n| Variable | Overrides |\n|----------|-----------|\n| `INVENTORY_DB_PATH` | `database.path` |\n| `INVENTORY_LOG_LEVEL` | `log.level` |\n| `INVENTORY_LOG_FORMAT` | `log.format` |\n| `INVENTORY_HEALTH_ADDR` | `health.addr` |\n| `INVENTORY_ADMIN_ADDR` | `admin.addr` |\n| `INVENTORY_AUTH_TOKEN` | `health.auth_token` |\n| `INVENTORY_PEER_TOKEN` | `watchdog.peer_token` |\n| `INVENTORY_ADMIN_TOKEN` | `admin.auth_token` |\n\n## Health endpoints\n\nBoth agents expose two HTTP endpoints used by the watchdog and for external monitoring:\n\n**Health server** (default `127.0.0.1:8080`, bearer-gated when off-loopback):\n\n| Endpoint | Method | Response |\n|----------|--------|----------|\n| `/health` | GET | `200 OK` if healthy and last scan is fresh; `503 Service Unavailable` otherwise |\n| `/status` | GET | JSON-encoded status snapshot (see below) |\n| `/metrics` | GET | Prometheus text exposition format — counters for scans, probes, DB, watchdog, alerts; gauges for host count + peer-up state |\n\n**Admin console** (default `127.0.0.1:9090`). Unauthenticated on the loopback default; set `admin.auth_token` (or `INVENTORY_ADMIN_TOKEN`) to gate every route below. A token is **required** when binding off-loopback — the agent refuses to start otherwise. Authenticate with `Authorization: Bearer \u003ctoken\u003e` or HTTP Basic auth using the token as the password (browsers get a native login prompt):\n\n| Endpoint | Method | Response |\n|----------|--------|----------|\n| `/` | GET | HTML dashboard |\n| `/hosts` | GET | HTML host inventory (paginated: `?limit=`, `?offset=`) |\n| `/hosts/{ip}` | GET | HTML host detail (with ports) |\n| `/scans` | GET | HTML scan history (paginated: `?limit=`, `?offset=`) |\n| `/watchdog` | GET | HTML watchdog peer-status panel |\n| `/export.json` | GET | Full inventory snapshot as JSON |\n| `/export.csv` | GET | Full inventory snapshot as CSV |\n| `/api/v1/hosts` | GET | Filterable JSON list — `?vendor=`, `?device_type=`, `?hostname=`, `?subnet=`, `?port=`, `?limit=`, `?offset=` |\n| `/api/v1/hosts/{ip}` | GET | Single-host JSON with nested ports |\n| `/api/v1/scans` | GET | Paginated JSON scan history — `?subnet=`, `?limit=`, `?offset=` |\n| `/scan` | POST | Trigger an out-of-cycle scan (CSRF-gated) |\n\n### `/status` response\n\n```json\n{\n  \"name\":         \"wintermute\",\n  \"healthy\":      true,\n  \"started_at\":   \"2024-01-15T10:00:00Z\",\n  \"last_scan_at\": \"2024-01-15T10:05:00Z\",\n  \"host_count\":   42,\n  \"scan_count\":   3\n}\n```\n\n## Project layout\n\n```\ncmd/\n  agent/          Generic single-agent binary (no watchdog peer required).\n  wintermute/     Wintermute entry point. Watchdog pointed at Neuromancer.\n  neuromancer/    Neuromancer entry point. Watchdog pointed at Wintermute.\n  console/        Interactive Bubbletea TUI console. Opens the SQLite\n                  database directly (read-only); no agent required.\n    tui/          TUI model, views, and lipgloss styles.\n\nconfigs/\n  wintermute.json         Local config for Wintermute.\n  neuromancer.json        Local config for Neuromancer.\n  wintermute.docker.json  Docker config for Wintermute (0.0.0.0 binding,\n                          service-name peer address, /data volume path).\n  neuromancer.docker.json Docker config for Neuromancer.\n\nmodels/           Pure domain types (Host, Port, Scan). No database\n                  imports, no business logic — just structs.\n\ninternal/\n  store/          Persistence interfaces (HostStore, PortStore, ScanStore)\n                  and the ErrNotFound sentinel. The rest of the application\n                  depends only on these interfaces, never on a concrete DB.\n\n  sqlite/         SQLite implementations of the store interfaces.\n    migrations/   Versioned SQL files embedded into the binary at\n                  compile time. The runner records each applied\n                  migration in schema_migrations and wraps each one\n                  in a transaction, so a failed migration never\n                  leaves the schema in a partial state.\n\n  config/         Config loading: JSON file merged with environment\n                  variable overrides. Custom Duration type supports\n                  human-readable strings (\"5m\") in JSON and marshals\n                  back to the same format.\n\n  health/         Status type, concurrency-safe Tracker, HTTP server\n                  (/health and /status endpoints), and HTTP client\n                  used by the watchdog to poll its partner.\n\n  admin/          Web admin console HTTP server. Parses embedded HTML\n                  templates at startup. Serves dashboard, host inventory,\n                  per-host port detail, and scan history pages.\n    templates/    Embedded HTML templates (Go text/template, GitHub-dark\n                  colour scheme). base.html defines the shared head and\n                  nav partials used by the four page templates.\n\n  watchdog/       Watchdog loop: runs three checks (liveness,\n                  freshness, consistency) against the partner agent\n                  on every tick. Logs warnings and errors; never\n                  kills or restarts the peer process.\n\n  scanner/        Concurrent TCP-probe network scanner. Skips IPv4\n                  network and broadcast addresses. Enforces a\n                  configurable per-subnet host limit. Uses a worker\n                  pool (semaphore) to bound parallelism. Banner-grabs\n                  open ports (banner.go) and tags hosts with a\n                  device type (classify.go). ARP enrichment via\n                  arp.go on Linux.\n\n  agent/          Periodic scan loop. Resolves per-subnet profiles,\n                  drives the scanner across due profiles, runs the\n                  host TTL prune, diffs the inventory and emits\n                  change events, updates the health Tracker.\n\n  alerts/         host.discovered / host.vanished event subsystem.\n                  Multiplexer fans out to WebhookSink (HTTP POST\n                  JSON) and SyslogSink (RFC 5424 over UDP/TCP).\n\n  metrics/        Dependency-free Prometheus text-format exposer.\n                  Counters and gauges incremented as side effects\n                  of the agent's normal work.\n\n  tracing/        OpenTelemetry wiring. OTLP/HTTP exporter,\n                  HTTPMiddleware for incoming requests, HTTPClient\n                  for outgoing requests.\n\n  tlsutil/        Shared *tls.Config builder. Used by both the\n                  health server (inbound TLS / optional mTLS) and\n                  the watchdog client (CA pinning to a project CA).\n\n  logging/        Shared slog initialisation helper used by all\n                  agent binaries.\n\nstart.sh          Local startup script. Builds binaries, optionally\n                  updates subnet config, then starts the selected mode\n                  (paired or standalone). Ctrl+C stops all agents.\n\nDockerfile        Multi-stage build: golang:1.25-bookworm → alpine:3.20.\n                  Compiles all four binaries; runs as non-root user.\ndocker-compose.yml Runs the Wintermute/Neuromancer pair with named\n                  volumes and Docker health checks. Exposes admin\n                  console on ports 9090 (wintermute) and 9091 (neuromancer).\n```\n\n## Architecture decisions\n\nThese decisions were made at project start to keep the codebase maintainable as it grows. Future contributors should understand the reasoning before changing them.\n\n---\n\n### Mutual watchdog — two named agents (Wintermute and Neuromancer)\n\nThe system is intentionally designed to run as a pair. Running a single agent means a silent crash or stalled scan loop goes undetected until someone notices the inventory is stale. Running two independent agents that continuously cross-check each other eliminates that blind spot.\n\nThree checks run on every watchdog tick:\n\n- **Liveness** — is the peer reachable and reporting healthy?\n- **Freshness** — has the peer completed a scan recently (within 2× the configured scan interval)?\n- **Consistency** — do the two agents agree on how many hosts are on the network (within the drift threshold)?\n\nThe watchdog never takes corrective action itself. It only logs. Actual recovery (restart, alert, failover) is the responsibility of an external supervisor. This keeps the watchdog simple, testable, and free of side effects.\n\nThe names Wintermute and Neuromancer are a reference to William Gibson's *Neuromancer* (1984), in which two AIs monitor and interact with each other.\n\n---\n\n### Repository interfaces (`internal/store`)\n\nAll database access goes through the `HostStore`, `PortStore`, and `ScanStore` interfaces defined in `internal/store`. No package outside `internal/sqlite` ever imports `internal/sqlite` directly.\n\n**Why:** When the project outgrows SQLite — whether because of write volume, multi-node requirements, or team preference — the new backend is written as a new package that satisfies the same interfaces. Business logic, tests, and the rest of the codebase are untouched.\n\n---\n\n### Compile-time interface checks\n\nEvery repository type carries a blank-identifier assignment:\n\n```go\nvar _ store.HostStore = (*HostRepo)(nil)\n```\n\n**Why:** If `store.HostStore` gains a new method and `HostRepo` is not updated, the build fails immediately with a clear error pointing at this line. Without this, the mismatch is only caught at runtime (or not at all, if the missing method is never called in tests). In a large legacy codebase this saves hours of debugging.\n\n---\n\n### Versioned, embedded SQL migrations (`internal/sqlite/migrations`)\n\nSchema changes live in numbered SQL files (`001_initial.sql`, `002_add_tags.sql`, etc.) embedded into the binary at compile time using `//go:embed`. A lightweight runner applies any unapplied migrations in order and records each one in a `schema_migrations` table. Each migration runs inside its own transaction.\n\n**Why:** Keeping migrations in separate files means every schema change is reviewable in git history. Embedding them in the binary means deployments are self-contained — no external migration tool or file to distribute. Transactional application means a failed migration never leaves the schema half-applied.\n\nTo add a new migration, create the next numbered file: `internal/sqlite/migrations/002_\u003cdescription\u003e.sql`. The runner picks it up automatically on next startup.\n\n---\n\n### `context.Context` on every store method\n\nAll store methods accept a `context.Context` as their first argument.\n\n**Why:** Context is the Go-idiomatic way to propagate deadlines, cancellation signals, and request-scoped values (such as trace IDs). Adding it later requires changing every call site. Adding it now costs nothing and means the codebase is ready for per-request database timeouts, graceful shutdown, and distributed tracing.\n\n---\n\n### SQLite WAL mode and `busy_timeout`\n\nThe database is opened with:\n\n```sql\nPRAGMA journal_mode = WAL;\nPRAGMA busy_timeout = 5000;\n```\n\n`SetMaxOpenConns(1)` is also set so the driver never opens a second connection.\n\n**Why:** WAL allows concurrent reads during a write, which matters once the agent is also serving health checks while scanning. `busy_timeout` tells SQLite to wait up to 5 seconds for a lock rather than returning `SQLITE_BUSY` immediately. Serialising connections at the driver level is simpler than handling `SQLITE_BUSY` in application code.\n\n---\n\n### Foreign key enforcement\n\n```sql\nPRAGMA foreign_keys = ON;\n```\n\n**Why:** SQLite does not enforce foreign key constraints by default. Without this pragma, deleting a host would leave orphaned rows in the `ports` table indefinitely. Enabling it ensures referential integrity is maintained at the database level — a safety net that works even when application-level delete logic has bugs.\n\n---\n\n### Human-readable durations in JSON config\n\nThe custom `config.Duration` type unmarshals both string values (`\"5m\"`, `\"30s\"`) and raw nanosecond integers from JSON, and marshals back to the string form.\n\n**Why:** Raw nanosecond integers (`300000000000`) are unreadable in config files. String durations (`\"5m\"`) are immediately obvious. This wrapper keeps the rest of the codebase using `time.Duration` natively while making configs human-friendly.\n\n---\n\n### Concurrent scanning with a worker pool\n\nThe scanner uses a buffered channel as a semaphore to bound the number of concurrent TCP probe goroutines. The `workers` and `max_hosts` fields in `ScannerConfig` give operators control over resource consumption.\n\n**Why:** A naive sequential scanner is too slow on large subnets (/16 or larger). Unbounded goroutine creation risks exhausting file descriptors. A semaphore provides throughput without runaway resource use.\n\nIPv4 network and broadcast addresses (first and last in subnets with a prefix length of /30 or shorter) are skipped, matching RFC behaviour. /31 and /32 ranges are not skipped (RFC 3021).\n\n---\n\n### `cmd/` entry point structure\n\nAgent binaries live under `cmd/\u003cname\u003e/main.go` rather than a root `main.go`.\n\n**Why:** A root `main.go` implies the repository is a single binary forever. `cmd/\u003cname\u003e/` is the idiomatic Go layout for projects that may grow multiple binaries. Adding a new binary requires no restructuring.\n\n---\n\n### Structured logging (`log/slog`)\n\nAll log output goes through `log/slog` from the Go standard library (Go 1.21+). The format is selectable between `text` (human-readable) and `json` (machine-readable) at runtime.\n\n**Why:** Unstructured log strings are difficult to query, alert on, or ingest into log aggregation systems. Structured logging with consistent field names means logs are queryable from day one. Using the stdlib package avoids a dependency and ensures any logging framework added later can wrap or replace it cleanly.\n\n---\n\n### Graceful shutdown via `signal.NotifyContext`\n\n`main` creates a context that is cancelled on `SIGINT` or `SIGTERM` and passes it to all long-running operations.\n\n**Why:** A scanner loop killed mid-write can corrupt state or leave partial scan records. A context-aware shutdown gives in-flight operations the opportunity to finish cleanly before the process exits. This is essential for any agent running under systemd, Kubernetes, or Docker with proper lifecycle management.\n\n---\n\n### SQLite as the database\n\nSQLite was chosen as the initial backing store.\n\n**Why:** For a local network inventory agent, SQLite is the correct default. It requires no server process, no connection string management, no separate installation, and no configuration. The database is a single file that can be copied, backed up, and inspected with standard tooling. It is fully ACID compliant and handles the read/write patterns of a periodic scanner with ease. The repository interface design means that if the project later needs multi-node storage or higher write throughput, the backing store can be replaced without touching any code outside `internal/sqlite`.\n\n---\n\n## Security\n\nSee [SECURITY.md](SECURITY.md) for the full OWASP Top 10 compliance table, operator hardening guidance, and how to report vulnerabilities.\n\nSummary of design decisions made for security:\n\n| OWASP | Mitigation |\n|-------|-----------|\n| A03 Injection | All SQL uses parameterized queries; scanner uses `net.Dialer`, never shell invocation; admin console uses `html/template` (auto-escaped) |\n| A04 Insecure Design | `peer_addr` validated to `http`/`https` schemes only at config load |\n| A05 Misconfiguration | Default `health.addr` and `admin.addr` bind to `127.0.0.1` (loopback), not all interfaces |\n| A06 Vulnerable Components | Pure-Go dependencies; `go.sum` enforced; `govulncheck` required on dep PRs |\n| A08 Data Integrity | `go.sum` verifies all module downloads; config validated at startup |\n| A09 Logging | All three watchdog failure modes logged at WARN/ERROR with structured fields |\n| A10 SSRF | `peer_addr` scheme validated; peer HTTP responses capped at 1 MiB |\n\nThe OWASP AI Top 10 is not applicable — this project contains no AI or ML components.\n\n## Contributing\n\nPull requests are welcome. Please open an issue first to discuss any significant changes. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow.\n\n## License\n\n[MIT](LICENSE)\n\nProudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcryptojones%2Fnetworkinventoryagent","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcryptojones%2Fnetworkinventoryagent","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcryptojones%2Fnetworkinventoryagent/lists"}