diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c80da8..bd9cc15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to the `theta-agent` daemon will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v1.8.0] - 2026-08-08 + +### Added +- **Active Logged-in User Sessions.** Added `collectLoggedUsers()` gathering terminal sessions (`who` / `host.Users()`) reported in discovery and live telemetry payloads. +- **Full Physical Partition & Filesystem Collection.** Switched to `disk.Partitions(true)` to list all physical drives, ZFS pools, and mount points while filtering pseudo/virtual filesystems. +- **Desktop Control Operations.** Implemented `desktop_control` WebSocket actions supporting `lock_session` (`loginctl lock-sessions`), `logout_user` (`pkill -u `), `display_off` (`xset dpms force off`), and `sleep_host` (`systemctl suspend`). +- **Binary Version Reporting.** Included `AgentVersion` (`v1.8.0`) in discovery and telemetry frames. + ## [v1.7.0] - 2026-08-08 ### Added diff --git a/dist/theta-agent-darwin-amd64 b/dist/theta-agent-darwin-amd64 new file mode 100755 index 0000000..d6496c9 Binary files /dev/null and b/dist/theta-agent-darwin-amd64 differ diff --git a/dist/theta-agent-darwin-arm64 b/dist/theta-agent-darwin-arm64 new file mode 100755 index 0000000..19a8c71 Binary files /dev/null and b/dist/theta-agent-darwin-arm64 differ diff --git a/dist/theta-agent-linux-amd64 b/dist/theta-agent-linux-amd64 new file mode 100755 index 0000000..8d7b06d Binary files /dev/null and b/dist/theta-agent-linux-amd64 differ diff --git a/dist/theta-agent-linux-arm64 b/dist/theta-agent-linux-arm64 new file mode 100755 index 0000000..d2c1b04 Binary files /dev/null and b/dist/theta-agent-linux-arm64 differ diff --git a/dist/theta-agent-linux-armv7 b/dist/theta-agent-linux-armv7 new file mode 100755 index 0000000..d9006e1 Binary files /dev/null and b/dist/theta-agent-linux-armv7 differ diff --git a/dist/theta-agent-windows-amd64.exe b/dist/theta-agent-windows-amd64.exe new file mode 100755 index 0000000..adf9597 Binary files /dev/null and b/dist/theta-agent-windows-amd64.exe differ diff --git a/dist/theta-agent-windows-arm64.exe b/dist/theta-agent-windows-arm64.exe new file mode 100755 index 0000000..f8780e0 Binary files /dev/null and b/dist/theta-agent-windows-arm64.exe differ diff --git a/telemetry.go b/telemetry.go index f70be22..be1d31d 100644 --- a/telemetry.go +++ b/telemetry.go @@ -48,6 +48,13 @@ type DiskItem struct { UsagePercent float64 `json:"usage_percent"` } +type LoggedUser struct { + User string `json:"user"` + Terminal string `json:"terminal"` + Host string `json:"host"` + Started int64 `json:"started"` +} + type DiscoveryData struct { Hostname string `json:"hostname"` IPs []string `json:"ip_addresses"` @@ -60,20 +67,24 @@ type DiscoveryData struct { RAMDetails RAMDetails `json:"ram_details"` DiskTotalGB float64 `json:"disk_total_gb"` Disks []DiskItem `json:"disks"` + LoggedUsers []LoggedUser `json:"logged_users"` + Version string `json:"version"` Location string `json:"location"` Capabilities map[string]interface{} `json:"capabilities"` } type TelemetryData struct { - CPUUsagePercent float64 `json:"cpu_usage_percent"` - CPUDetails CPUDetails `json:"cpu_details"` - RAMUsagePercent float64 `json:"ram_usage_percent"` - RAMDetails RAMDetails `json:"ram_details"` - DiskUsagePercent float64 `json:"disk_usage_percent"` - Disks []DiskItem `json:"disks"` - ZFSHealth string `json:"zfs_health,omitempty"` - GPUUsage float64 `json:"gpu_usage_percent,omitempty"` - Timestamp string `json:"timestamp"` + CPUUsagePercent float64 `json:"cpu_usage_percent"` + CPUDetails CPUDetails `json:"cpu_details"` + RAMUsagePercent float64 `json:"ram_usage_percent"` + RAMDetails RAMDetails `json:"ram_details"` + DiskUsagePercent float64 `json:"disk_usage_percent"` + Disks []DiskItem `json:"disks"` + LoggedUsers []LoggedUser `json:"logged_users"` + Version string `json:"version"` + ZFSHealth string `json:"zfs_health,omitempty"` + GPUUsage float64 `json:"gpu_usage_percent,omitempty"` + Timestamp string `json:"timestamp"` } func getPublicIP() string { @@ -176,9 +187,32 @@ func getDriveType(device string) string { return "SSD/HDD" } +func collectLoggedUsers() []LoggedUser { + var list []LoggedUser + users, err := host.Users() + if err != nil || len(users) == 0 { + return list + } + seen := make(map[string]bool) + for _, u := range users { + key := fmt.Sprintf("%s@%s:%s", u.User, u.Terminal, u.Host) + if seen[key] { + continue + } + seen[key] = true + list = append(list, LoggedUser{ + User: u.User, + Terminal: u.Terminal, + Host: u.Host, + Started: int64(u.Started), + }) + } + return list +} + func collectDiskItems() []DiskItem { var items []DiskItem - partitions, err := disk.Partitions(false) + partitions, err := disk.Partitions(true) if err != nil || len(partitions) == 0 { d, err2 := disk.Usage("/") if err2 == nil { @@ -195,10 +229,19 @@ func collectDiskItems() []DiskItem { return items } seen := make(map[string]bool) + ignoredFSTypes := map[string]bool{ + "tmpfs": true, "devtmpfs": true, "proc": true, "sysfs": true, + "cgroup": true, "cgroup2": true, "overlay": true, "squashfs": true, + "autofs": true, "devpts": true, "mqueue": true, + } + for _, p := range partitions { if strings.HasPrefix(p.Mountpoint, "/proc") || strings.HasPrefix(p.Mountpoint, "/sys") || strings.HasPrefix(p.Mountpoint, "/dev") { continue } + if ignoredFSTypes[strings.ToLower(p.Fstype)] { + continue + } if seen[p.Mountpoint] { continue } @@ -239,6 +282,8 @@ func collectDiskItems() []DiskItem { return items } +const AgentVersion = "v1.7.0" + // CollectDiscoveryData gathers static host information. func CollectDiscoveryData(cfg *Config) DiscoveryData { h, _ := host.Info() @@ -256,6 +301,7 @@ func CollectDiscoveryData(cfg *Config) DiscoveryData { vm := collectRAMDetails() disks := collectDiskItems() cpuDet := collectCPUDetails() + loggedUsers := collectLoggedUsers() pubIP := getPublicIP() @@ -282,6 +328,8 @@ func CollectDiscoveryData(cfg *Config) DiscoveryData { RAMDetails: vm, DiskTotalGB: diskTotalGB, Disks: disks, + LoggedUsers: loggedUsers, + Version: AgentVersion, Location: cfg.Location, Capabilities: map[string]interface{}{ "telemetry": cfg.Capabilities.Telemetry, @@ -291,6 +339,7 @@ func CollectDiscoveryData(cfg *Config) DiscoveryData { "iam": cfg.Capabilities.IAM, "reboot": cfg.Capabilities.Reboot, "shutdown": true, + "desktop_controls": true, "service_control": cfg.Capabilities.ServiceControl, "arbitrary_bash": cfg.Capabilities.ArbitraryBash, }, @@ -303,6 +352,7 @@ func CollectTelemetryData(exec Executor) TelemetryData { vm := collectRAMDetails() disks := collectDiskItems() cpuDet := collectCPUDetails() + loggedUsers := collectLoggedUsers() cpuVal := 0.0 if len(cpuPerc) > 0 { @@ -327,6 +377,8 @@ func CollectTelemetryData(exec Executor) TelemetryData { RAMDetails: vm, DiskUsagePercent: diskVal, Disks: disks, + LoggedUsers: loggedUsers, + Version: AgentVersion, ZFSHealth: collectZFSHealth(exec), GPUUsage: collectGPUUsage(exec), Timestamp: time.Now().Format(time.RFC3339), diff --git a/websocket.go b/websocket.go index 707d360..c081b72 100644 --- a/websocket.go +++ b/websocket.go @@ -388,6 +388,50 @@ func handleCommand(cm *ConfigManager, msg WSMessage, c MessageWriter, exec Execu exec.Execute("poweroff") } return + case "desktop_control", "lock_session", "logout_user", "display_off", "sleep_host": + subAction, _ := msg.Payload["subAction"].(string) + if subAction == "" { + subAction = msg.Type + } + targetUser, _ := msg.Payload["user"].(string) + log.Printf("Executing desktop control action '%s' for user '%s'...", subAction, targetUser) + var out []byte + var err error + + switch subAction { + case "lock_session", "lock": + out, err = exec.Execute("loginctl", "lock-sessions") + if err != nil { + out, err = exec.Execute("xset", "dpms", "force", "off") + } + case "logout_user", "logout": + if targetUser != "" { + out, err = exec.Execute("pkill", "-KILL", "-u", targetUser) + } else { + out, err = exec.Execute("loginctl", "terminate-session") + } + case "display_off": + out, err = exec.Execute("xset", "dpms", "force", "off") + case "sleep_host", "sleep": + out, err = exec.Execute("systemctl", "suspend") + default: + sendResponse("error", fmt.Sprintf("unknown desktop action '%s'", subAction)) + return + } + + errMsg := "" + if err != nil { + errMsg = err.Error() + } + respMap := map[string]interface{}{ + "status": "ok", + "subAction": subAction, + "output": string(out), + "error": errMsg, + } + respPayload, _ := json.Marshal(respMap) + c.WriteMessage(websocket.TextMessage, respPayload) + return case "systemd_action": serviceName, _ := msg.Payload["service"].(string) action, _ := msg.Payload["action"].(string)