Files
guru-connect/docs/specs/SPEC-003-machine-inventory.md
Mike Swanson abf499cb23 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>
2026-05-30 15:48:09 -07:00

14 KiB
Raw Blame History

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.rscollect() -> 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 111 are taken; supports_h264 = 11):
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:30107) 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:779835), 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:385386) 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.