spec: add SPEC-003 full machine inventory in connection DB
Persist a complete per-machine device inventory on connect_machines (OS+locale+install, CPU/RAM, mfr/model/serial, external WAN IP captured server-side via trusted-proxy client_ip + private LAN IP + MAC, logged-on user, idle, time zone, uptime, local-admin-present), refreshed each AgentStatus and surfaced in the dashboard machine detail — ScreenConnect "Guest Info" parity. Data layer for SPEC-002 Phase 2; closes the GC side of the agent-IP gap (coord todo 7459428e). Requested by Mike 2026-05-30. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@ Bringing GC to parity with GuruRMM's release engineering. Full plan: [SPEC-001](
|
||||
|
||||
- [x] JWT auth, Argon2id passwords, rate limiting, security headers
|
||||
- [x] Sessions / machines / support-codes / events
|
||||
- [ ] **Full machine inventory in the connection DB** — P2 — persist per-machine device inventory (OS+locale+install, CPU/RAM, mfr/model/serial, external WAN IP captured server-side + private LAN IP + MAC, logged-on user, idle, time zone, uptime, local-admin) on `connect_machines`, refreshed each `AgentStatus`, shown in the dashboard machine detail (ScreenConnect "Guest Info" parity). Data layer for SPEC-002 Phase 2; closes GC side of agent-IP gap (todo 7459428e). ([SPEC-003](specs/SPEC-003-machine-inventory.md))
|
||||
- [ ] Programmatic session pre-create + viewer-token (integration contract) — P2
|
||||
|
||||
## Security & Infrastructure
|
||||
|
||||
241
docs/specs/SPEC-003-machine-inventory.md
Normal file
241
docs/specs/SPEC-003-machine-inventory.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# SPEC-003: Full Machine Inventory in the Connection Database
|
||||
|
||||
**Status:** Proposed
|
||||
**Priority:** P2
|
||||
**Requested By:** Mike (2026-05-30)
|
||||
**Estimated Effort:** Large
|
||||
|
||||
## Overview
|
||||
|
||||
Persist a complete per-machine device inventory on `connect_machines` — refreshed
|
||||
from the agent on each status cycle and stamped server-side with the real external
|
||||
IP — and surface it in the dashboard machine-detail view, bringing GuruConnect to
|
||||
parity with ScreenConnect's "Guest Info" panel. Today a machine record holds only
|
||||
`hostname`, `os_version` (literally `std::env::consts::OS` → `"windows"`),
|
||||
`is_elevated`, `organization`, `site`, `tags`, and timestamps; a technician cannot
|
||||
answer "what is this box, who's on it, what's its WAN/LAN IP, what's its MAC" from
|
||||
GuruConnect at all. Success = every connected agent shows hostname, company, site,
|
||||
logged-on user, idle time, OS (full string + locale + install date), CPU, memory,
|
||||
manufacturer/model, serial, **external WAN IP**, **private LAN IP**, **MAC**, client
|
||||
version, time zone, uptime, and local-admin-present in the machine detail, kept
|
||||
current as the agent reconnects.
|
||||
|
||||
Reference example (ScreenConnect Guest Info — `DESKTOP-VRBQ6LM`, Pavon / Curves):
|
||||
Network Address `174.78.94.186`, Private Network Address `192.168.1.128`, MAC
|
||||
`04:42:1A:0C:8C:A6`, Client Version `26.1.24.9579`, TZ `(UTC-07:00) Arizona`, Uptime
|
||||
`33d 4h`, Local Admin Present `Yes`, Logged On User `DESKTOP-VRBQ6LM\GeoVision`, OS
|
||||
`Microsoft Windows 11 Pro (10.0.26200) en-US`, OS install `3/15/2025`, CPU `i7-11700
|
||||
(16 virtual) X64`, Mem `5260 / 16140 MB`, Mfr/Model `ASUS System Product Name`.
|
||||
|
||||
> This is the data-layer companion to **SPEC-002 Phase 2**, which builds the
|
||||
> "machines inventory/history/delete" dashboard surface (SPEC-002 §"Phase 2"). SPEC-003
|
||||
> defines *what data exists and how it is collected*; SPEC-002 renders it. It also
|
||||
> closes the GuruConnect side of the GuruRMM agent-IP gap (coord todo `7459428e`):
|
||||
> GuruRMM has **no** IP fields at all, whereas GC already computes the client IP at
|
||||
> connect time (`utils::ip_extract::client_ip`) and merely fails to store it.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included in v1
|
||||
|
||||
- New nullable inventory columns on `connect_machines` (migration 008).
|
||||
- A `DeviceInventory` protobuf message reported by the agent (Windows collection via
|
||||
WMI/Win32 APIs), carried on `AgentStatus` and written through to the machine row.
|
||||
- **Server-side** capture of the external IP from the already-extracted
|
||||
trusted-proxy `client_ip` (never trust an agent-reported WAN IP).
|
||||
- Extend `MachineInfo` API DTO + `GET /api/machines` / `GET /api/machines/:agent_id`
|
||||
to return the inventory.
|
||||
- Dashboard machine-detail rendering of the inventory (the Guest-Info-style panel).
|
||||
- Derived/display-only fields composed from existing tables: Hosts/Guests Connected
|
||||
and "Guest Last Connected" from `connect_sessions` history.
|
||||
|
||||
### Explicitly out of scope
|
||||
|
||||
- macOS / Linux inventory collection — the agent is Windows-only today (cross-platform
|
||||
agents are roadmap "Future Considerations" P3). The collector is structured so
|
||||
non-Windows builds report `None`/empty without breaking the wire or schema.
|
||||
- Editable/operator-authored fields (Department, Device Type, custom Attributes as
|
||||
free-text the tech sets). v1 stores `department`/`device_type` as **agent-reported
|
||||
or null**; operator-editable metadata is a follow-up (needs a dashboard write path
|
||||
+ audit).
|
||||
- Live polling/push of inventory between status cycles, scheduled re-inventory jobs,
|
||||
and historical inventory diffing — v1 is "latest snapshot, refreshed on agent status".
|
||||
- Pending Activity / screenshot thumbnail (separate concerns; screenshot is SPEC-002
|
||||
viewer work).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Agent (`agent/src/`)
|
||||
- New module `agent/src/inventory/mod.rs` — `collect() -> DeviceInventory`. Windows
|
||||
implementation gathers:
|
||||
- **OS**: `Win32_OperatingSystem` (Caption, Version, OSLanguage→locale, InstallDate,
|
||||
FreePhysicalMemory, TotalVisibleMemorySize).
|
||||
- **System**: `Win32_ComputerSystem` (Manufacturer, Model, Domain/Workgroup,
|
||||
UserName→logged-on user, TotalPhysicalMemory).
|
||||
- **CPU**: `Win32_Processor` (Name, NumberOfLogicalProcessors, Architecture).
|
||||
- **Serial**: `Win32_BIOS.SerialNumber` (fallback `Win32_SystemEnclosure`).
|
||||
- **Network**: `GetAdaptersAddresses` (iphlpapi) for primary LAN IPv4 + MAC of the
|
||||
active adapter (skip loopback/virtual).
|
||||
- **Idle time**: `GetLastInputInfo` (must run in the interactive session — see Open
|
||||
Questions; SYSTEM service sees its own idle, so query via the same session path
|
||||
the agent already uses for capture, or report `None` when unavailable).
|
||||
- **Local admin present**: enumerate the local `Administrators` group for any
|
||||
enabled non-builtin member (`NetLocalGroupGetMembers`).
|
||||
- **Time zone**: `GetDynamicTimeZoneInformation`.
|
||||
- Uptime already available (`AgentStatus.uptime_secs`).
|
||||
- Win7 SP1+ safe: all of the above are available on Win7/Server 2008 R2. Prefer the
|
||||
`wmi` crate (pure-Rust over COM, no runtime redistributable — satisfies the
|
||||
"statically linked, no .NET/VC++" constraint) or direct `windows`-crate calls.
|
||||
- Collection cadence: inventory is near-static, so collect once at session start and
|
||||
re-collect every ~15 min (or on a `display_count`/IP change), **not** every
|
||||
heartbeat. Carry it on `AgentStatus.inventory` only when present; the lightweight
|
||||
status tick stays cheap.
|
||||
- Replace the bogus `os_version: std::env::consts::OS` with the real OS caption (keep
|
||||
`os_version` for back-compat; populate the richer `os_name` from inventory).
|
||||
|
||||
### Protobuf (`proto/guruconnect.proto`)
|
||||
- Add a `DeviceInventory` message and attach it to `AgentStatus` at the next free
|
||||
field number **12** (fields 1–11 are taken; `supports_h264 = 11`):
|
||||
|
||||
```proto
|
||||
message DeviceInventory {
|
||||
string os_name = 1; // "Microsoft Windows 11 Pro (10.0.26200)"
|
||||
string os_locale = 2; // "en-US"
|
||||
int64 os_install_unix = 3; // OS install time, unix secs
|
||||
string machine_domain = 4; // "WORKGROUP" or AD domain
|
||||
string logged_on_user = 5; // "DESKTOP-VRBQ6LM\\GeoVision"
|
||||
int64 idle_secs = 6; // -1 = unknown
|
||||
string cpu_model = 7;
|
||||
int32 cpu_logical = 8;
|
||||
string cpu_arch = 9; // "X64"
|
||||
int64 mem_total_mb = 10;
|
||||
int64 mem_available_mb = 11;
|
||||
string manufacturer = 12;
|
||||
string model = 13;
|
||||
string serial_number = 14;
|
||||
string machine_description = 15;
|
||||
string private_ip = 16; // LAN IPv4 of the active adapter
|
||||
string mac_address = 17;
|
||||
string time_zone = 18; // "(UTC-07:00) Arizona"
|
||||
bool local_admin_present = 19;
|
||||
string department = 20; // agent-config-reported (optional)
|
||||
string device_type = 21; // agent-reported (optional)
|
||||
}
|
||||
|
||||
message AgentStatus {
|
||||
// ... existing fields 1-11 ...
|
||||
DeviceInventory inventory = 12; // present periodically, not every tick
|
||||
}
|
||||
```
|
||||
|
||||
- External IP is **not** an agent-reported field (deliberately) — see Security.
|
||||
|
||||
### DB schema (`server/migrations/008_machine_inventory.sql`)
|
||||
- `ALTER TABLE connect_machines ADD COLUMN IF NOT EXISTS ...` — all nullable, no
|
||||
`NOT NULL` (consistent with the table's existing NULL-tolerant reality and the
|
||||
manual `FromRow`):
|
||||
`os_name TEXT, os_locale TEXT, os_install_at TIMESTAMPTZ, machine_domain TEXT,
|
||||
logged_on_user TEXT, idle_secs BIGINT, cpu_model TEXT, cpu_logical INT,
|
||||
cpu_arch TEXT, mem_total_mb BIGINT, mem_available_mb BIGINT, manufacturer TEXT,
|
||||
model TEXT, serial_number TEXT, machine_description TEXT, external_ip INET,
|
||||
private_ip INET, mac_address TEXT, time_zone TEXT, uptime_secs BIGINT,
|
||||
local_admin_present BOOLEAN, department TEXT, device_type TEXT,
|
||||
inventory_updated_at TIMESTAMPTZ`.
|
||||
- Idempotent `ADD COLUMN IF NOT EXISTS`, applied by `sqlx::migrate!()` on startup —
|
||||
**never** pre-applied via psql (see `.claude/standards/gururmm/sqlx-migrations.md`,
|
||||
and the 005→007 lesson: do not rely on `IF NOT EXISTS` to add constraints later).
|
||||
- Client Version reuses the existing version column written by
|
||||
`db::releases::update_machine_version` — do not duplicate.
|
||||
|
||||
### Server (`server/src/`)
|
||||
- `db/machines.rs`:
|
||||
- Extend the `Machine` struct + manual `FromRow` (machines.rs:30–107) with the new
|
||||
columns, each `Option<T>` read NULL-tolerantly (same pattern as `organization`).
|
||||
- New `update_machine_inventory(pool, agent_id, &DeviceInventory)` (sibling of
|
||||
`update_machine_metadata`, machines.rs:213) — `COALESCE` each field so a partial
|
||||
snapshot never nulls a previously-known value; set `inventory_updated_at = NOW()`.
|
||||
- New `update_machine_external_ip(pool, agent_id, IpAddr)`.
|
||||
- `relay/mod.rs`:
|
||||
- At connect/upsert (mod.rs:591, where `client_ip` is in scope) call
|
||||
`update_machine_external_ip(agent_id, client_ip)` so the WAN IP is stamped from the
|
||||
trusted-proxy-derived address.
|
||||
- In the `AgentStatus` handler (mod.rs:779–835), when `status.inventory` is present,
|
||||
call `update_machine_inventory(...)` alongside the existing
|
||||
`update_machine_metadata(...)` write.
|
||||
- `api/mod.rs`: extend `MachineInfo` (mod.rs:117) + `From<Machine>` (mod.rs:130) with
|
||||
the inventory fields (ISO-8601 for timestamps, IPs as strings). `GET /api/machines`,
|
||||
`GET /api/machines/:agent_id` (main.rs:385–386) return them unchanged in shape
|
||||
beyond the added fields. Compose Hosts/Guests-connected + "Guest Last Connected"
|
||||
from `db::sessions` for the detail endpoint.
|
||||
|
||||
### Dashboard (`dashboard/src/`)
|
||||
- Extend the `Machine` type (`dashboard/src/api/types.ts`) and machine-detail view
|
||||
with a Guest-Info-style inventory panel (Session / Device / Network groupings).
|
||||
Coordinate with SPEC-002 Phase 2 so this is one panel, not two.
|
||||
|
||||
## Implementation details
|
||||
|
||||
- Files to touch: `proto/guruconnect.proto` (~line 303, `AgentStatus`);
|
||||
`agent/src/inventory/mod.rs` (new) + wire into `agent/src/session/mod.rs:236`
|
||||
(`send_status`); `server/migrations/008_machine_inventory.sql` (new);
|
||||
`server/src/db/machines.rs:30,101,213`; `server/src/relay/mod.rs:591,824`;
|
||||
`server/src/api/mod.rs:117,130`; `dashboard/src/api/types.ts`,
|
||||
`dashboard/src/api/machines.ts`, machine-detail component.
|
||||
- Key structs/messages: `DeviceInventory` (proto), `Machine` (db), `MachineInfo` (api).
|
||||
- `idle_secs = -1`, empty strings, and absent adapters all map to `NULL`/unknown in
|
||||
the DB — display as "—", never as `0`/`false` masquerading as real data.
|
||||
|
||||
## Security considerations
|
||||
|
||||
- **External IP is server-authoritative.** Capture it only from
|
||||
`utils::ip_extract::client_ip` (trusted-proxy-aware; ignores client-supplied
|
||||
`X-Forwarded-For`/`X-Real-IP` from untrusted peers — see the module doc). An
|
||||
agent-reported WAN IP would be spoofable and is intentionally not modeled.
|
||||
- Auth unchanged: inventory flows over the already-authenticated agent WS (support
|
||||
code or per-machine key) and is served only on JWT-authenticated machine endpoints.
|
||||
- `serial_number`, `logged_on_user`, `machine_domain`, and local-admin presence are
|
||||
mildly sensitive; they are admin-dashboard-only (no new unauthenticated surface).
|
||||
- Input validation: treat all agent-reported strings as untrusted — length-cap each
|
||||
field server-side before persisting (defense against a compromised/rogue agent
|
||||
bloating the row); `bytea`/oversized values rejected.
|
||||
- Audit: no new audit events required, but an inventory write is `debug!`-logged with
|
||||
agent_id (no PII beyond what the admin already sees).
|
||||
|
||||
## Testing strategy
|
||||
|
||||
- **Unit:** `inventory::collect()` on Windows returns populated fields on a real box;
|
||||
non-Windows returns all-`None`. `update_machine_inventory` COALESCE semantics
|
||||
(partial snapshot preserves prior values). `MachineInfo` serialization round-trip.
|
||||
- **Integration:** agent → relay → DB: connect an agent, assert `connect_machines`
|
||||
gains `external_ip` (matching the test's `client_ip`) at connect and the full
|
||||
inventory after the first `AgentStatus.inventory`; `GET /api/machines/:id` returns
|
||||
it. Verify a status tick *without* inventory does not null existing values.
|
||||
- **Manual:** enroll the Pavon/Raiders or a lab box, confirm the dashboard detail
|
||||
matches the real machine (cross-check WAN IP against the box's actual egress IP,
|
||||
LAN IP/MAC against `ipconfig /all`).
|
||||
|
||||
## Effort estimate & dependencies
|
||||
|
||||
- **Size: Large.** The bulk is the Windows inventory collector (WMI/Win32, Win7-safe)
|
||||
and the dashboard panel; the proto/DB/relay/API wiring is mechanical and follows
|
||||
the existing `organization`/`site`/`tags` precedent end-to-end.
|
||||
- **Depends on:** nothing blocking. Reuses existing `client_ip` extraction and the
|
||||
`AgentStatus` pipeline.
|
||||
- **Unblocks / aligns with:** SPEC-002 Phase 2 dashboard "machines inventory" surface;
|
||||
resolves the GC side of coord todo `7459428e` (agent IP tracking).
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Idle time as a SYSTEM service.** `GetLastInputInfo` returns input idle for the
|
||||
*calling* session; a SYSTEM-context agent may not see the interactive user's idle.
|
||||
Does the agent already have an interactive-session path (it injects input/captures
|
||||
the active console) we can reuse, or do we report `idle_secs = -1` when running
|
||||
headless? Resolve during planning.
|
||||
2. **Multiple NICs.** Store only the primary active adapter's IP/MAC (v1), or all
|
||||
adapters (`TEXT[]`)? ScreenConnect shows one; v1 proposes one. Revisit if multi-homed
|
||||
boxes matter.
|
||||
3. **Operator-editable Department / Device Type / Attributes.** v1 treats these as
|
||||
agent-reported/null. Confirm whether the dashboard should also let a tech set them
|
||||
(adds a write endpoint + audit) now or as a follow-up spec.
|
||||
4. **Re-inventory cadence.** 15 min fixed vs. change-triggered vs. configurable —
|
||||
pick a default; 15 min proposed.
|
||||
Reference in New Issue
Block a user