Add device agent library
Provide enrollment, elevated install, posture checks, keystore, and system-tray helpers shared by the probo-agent binary. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
261
pkg/deviceagent/tray/autostart_darwin.go
Normal file
261
pkg/deviceagent/tray/autostart_darwin.go
Normal file
@@ -0,0 +1,261 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const (
|
||||
trayLabel = "com.probo.agent.tray"
|
||||
trayPlistPath = "/Library/LaunchAgents/com.probo.agent.tray.plist"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed launchagent.plist.tmpl
|
||||
launchAgentPlistTmpl string
|
||||
|
||||
launchAgentPlist = template.Must(
|
||||
template.New("launchagent").Funcs(template.FuncMap{"xml": xmlEscape}).Parse(launchAgentPlistTmpl),
|
||||
)
|
||||
)
|
||||
|
||||
func xmlEscape(v string) (string, error) {
|
||||
var sb strings.Builder
|
||||
if err := xml.EscapeText(&sb, []byte(v)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
type launchAgentData struct {
|
||||
Label string
|
||||
ExePath string
|
||||
RunDir string
|
||||
}
|
||||
|
||||
func RegisterAutoStart(exePath string, runDir string) error {
|
||||
if exePath == "" {
|
||||
return fmt.Errorf("executable path is required")
|
||||
}
|
||||
|
||||
if runDir == "" {
|
||||
return fmt.Errorf("enrollment run directory is required")
|
||||
}
|
||||
|
||||
if err := writeTrayLaunchAgentPlist(exePath, runDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uids := activeGUIUserUIDs()
|
||||
if len(uids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return bootstrapTrayForUIDs(uids)
|
||||
}
|
||||
|
||||
func writeTrayLaunchAgentPlist(exePath string, runDir string) error {
|
||||
agentsDir := filepath.Dir(trayPlistPath)
|
||||
if err := os.MkdirAll(agentsDir, 0o755); err != nil {
|
||||
return fmt.Errorf("cannot ensure launch agents directory: %w", err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(trayPlistPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write plist (need root?): %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
if err := launchAgentPlist.Execute(
|
||||
f,
|
||||
launchAgentData{
|
||||
Label: trayLabel,
|
||||
ExePath: exePath,
|
||||
RunDir: runDir,
|
||||
},
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot render LaunchAgent plist: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnregisterAutoStart stops the tray LaunchAgent and removes its plist.
|
||||
func UnregisterAutoStart() error {
|
||||
bootoutTrayForUIDs(activeGUIUserUIDs())
|
||||
|
||||
if err := os.Remove(trayPlistPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("cannot remove plist: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bootstrapTrayForUIDs(uids []int) error {
|
||||
for _, uid := range uids {
|
||||
target := fmt.Sprintf("gui/%d/%s", uid, trayLabel)
|
||||
|
||||
_ = exec.Command("launchctl", "bootout", target).Run()
|
||||
|
||||
if out, err := exec.Command(
|
||||
"launchctl",
|
||||
"bootstrap",
|
||||
fmt.Sprintf("gui/%d", uid),
|
||||
trayPlistPath,
|
||||
).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"cannot run launchctl bootstrap for uid %d: %w: %s",
|
||||
uid,
|
||||
err,
|
||||
strings.TrimSpace(string(out)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bootoutTrayForUIDs(uids []int) {
|
||||
for _, uid := range uids {
|
||||
target := fmt.Sprintf("gui/%d/%s", uid, trayLabel)
|
||||
_ = exec.Command("launchctl", "bootout", target).Run()
|
||||
}
|
||||
}
|
||||
|
||||
func activeGUIUserUIDs() []int {
|
||||
names := loggedInUsernames()
|
||||
if len(names) == 0 {
|
||||
if uid, err := currentGUIUserUID(); err == nil {
|
||||
return []int{uid}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[int]struct{}, len(names))
|
||||
uids := make([]int, 0, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
uid, err := uidForUsername(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := seen[uid]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[uid] = struct{}{}
|
||||
uids = append(uids, uid)
|
||||
}
|
||||
|
||||
if len(uids) == 0 {
|
||||
if uid, err := currentGUIUserUID(); err == nil {
|
||||
return []int{uid}
|
||||
}
|
||||
}
|
||||
|
||||
return uids
|
||||
}
|
||||
|
||||
func loggedInUsernames() []string {
|
||||
out, err := exec.Command("users").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return parseLoggedInUsernames(string(out))
|
||||
}
|
||||
|
||||
func parseLoggedInUsernames(usersOutput string) []string {
|
||||
tokens := strings.Fields(usersOutput)
|
||||
|
||||
seen := make(map[string]struct{}, len(tokens))
|
||||
names := make([]string, 0, len(tokens))
|
||||
|
||||
for _, name := range tokens {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || name == "root" || name == "loginwindow" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := seen[name]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[name] = struct{}{}
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func currentGUIUserUID() (int, error) {
|
||||
name := strings.TrimSpace(consoleUserName())
|
||||
if name == "" || name == "root" || name == "loginwindow" {
|
||||
name = strings.TrimSpace(os.Getenv("USER"))
|
||||
}
|
||||
|
||||
if name == "" || name == "root" || name == "loginwindow" {
|
||||
return 0, fmt.Errorf("cannot resolve active GUI user")
|
||||
}
|
||||
|
||||
return uidForUsername(name)
|
||||
}
|
||||
|
||||
func uidForUsername(name string) (int, error) {
|
||||
uidOut, err := exec.Command("id", "-u", name).Output()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot resolve uid for %s: %w", name, err)
|
||||
}
|
||||
|
||||
uidRaw := strings.TrimSpace(string(uidOut))
|
||||
|
||||
uid, err := strconv.Atoi(uidRaw)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid uid %q", uidRaw)
|
||||
}
|
||||
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
func consoleUserName() string {
|
||||
out, err := exec.Command("stat", "-f", "%Su", "/dev/console").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
87
pkg/deviceagent/tray/autostart_darwin_test.go
Normal file
87
pkg/deviceagent/tray/autostart_darwin_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseLoggedInUsernames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
output string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "empty output",
|
||||
output: "",
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "single user",
|
||||
output: "alice\n",
|
||||
want: []string{"alice"},
|
||||
},
|
||||
{
|
||||
name: "multiple users",
|
||||
output: "alice bob\n",
|
||||
want: []string{"alice", "bob"},
|
||||
},
|
||||
{
|
||||
name: "duplicate users",
|
||||
output: "alice alice bob\n",
|
||||
want: []string{"alice", "bob"},
|
||||
},
|
||||
{
|
||||
name: "skips root and loginwindow",
|
||||
output: "root loginwindow alice\n",
|
||||
want: []string{"alice"},
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
output: " \n",
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "extra whitespace between names",
|
||||
output: " alice bob \n",
|
||||
want: []string{"alice", "bob"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(
|
||||
tt.name,
|
||||
func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := parseLoggedInUsernames(tt.output)
|
||||
assert.Equal(t, tt.want, got)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
94
pkg/deviceagent/tray/autostart_windows.go
Normal file
94
pkg/deviceagent/tray/autostart_windows.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
const runKeyPath = `Software\Microsoft\Windows\CurrentVersion\Run`
|
||||
|
||||
const runValueName = "ProboAgentTray"
|
||||
|
||||
func RegisterAutoStart(exePath string, runDir string) error {
|
||||
if exePath == "" {
|
||||
return fmt.Errorf("executable path is required")
|
||||
}
|
||||
if runDir == "" {
|
||||
return fmt.Errorf("enrollment run directory is required")
|
||||
}
|
||||
|
||||
sid, err := currentInteractiveUserSID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot resolve interactive user for tray auto-start: %w", err)
|
||||
}
|
||||
|
||||
keyPath := sid + `\` + runKeyPath
|
||||
|
||||
key, _, err := registry.CreateKey(registry.USERS, keyPath, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open or create Run registry key for interactive user: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = key.Close() }()
|
||||
|
||||
command := fmt.Sprintf(`"%s" tray --run-dir "%s"`, exePath, runDir)
|
||||
if err := key.SetStringValue(runValueName, command); err != nil {
|
||||
return fmt.Errorf("cannot set Run registry value: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnregisterAutoStart() error {
|
||||
sid, err := currentInteractiveUserSID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot resolve interactive user for tray auto-start: %w", err)
|
||||
}
|
||||
|
||||
keyPath := sid + `\` + runKeyPath
|
||||
|
||||
key, err := registry.OpenKey(registry.USERS, keyPath, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
if errors.Is(err, registry.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot open Run registry key for interactive user: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = key.Close() }()
|
||||
|
||||
if err := key.DeleteValue(runValueName); err != nil {
|
||||
if errors.Is(err, registry.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot delete Run registry value: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
213
pkg/deviceagent/tray/console_user_windows.go
Normal file
213
pkg/deviceagent/tray/console_user_windows.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const invalidSessionID = 0xFFFFFFFF
|
||||
|
||||
func currentInteractiveUserSID() (string, error) {
|
||||
var lastErr error
|
||||
|
||||
for _, sessionID := range interactiveSessionCandidates() {
|
||||
sid, err := sidFromSessionID(sessionID)
|
||||
if err == nil {
|
||||
return sid, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return "", fmt.Errorf("no interactive user session available: %w", lastErr)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no interactive user session available")
|
||||
}
|
||||
|
||||
func interactiveSessionCandidates() []uint32 {
|
||||
candidates := make([]uint32, 0, 4)
|
||||
|
||||
var sessionID uint32
|
||||
|
||||
if err := windows.ProcessIdToSessionId(windows.GetCurrentProcessId(), &sessionID); err == nil && sessionID != 0 {
|
||||
candidates = appendUniqueSessionID(candidates, sessionID)
|
||||
}
|
||||
|
||||
consoleSessionID := windows.WTSGetActiveConsoleSessionId()
|
||||
if consoleSessionID != invalidSessionID {
|
||||
candidates = appendUniqueSessionID(candidates, consoleSessionID)
|
||||
}
|
||||
|
||||
for _, id := range activeWTSSessionIDs() {
|
||||
candidates = appendUniqueSessionID(candidates, id)
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
func activeWTSSessionIDs() []uint32 {
|
||||
var (
|
||||
sessions *windows.WTS_SESSION_INFO
|
||||
count uint32
|
||||
)
|
||||
|
||||
if err := windows.WTSEnumerateSessions(0, 0, 1, &sessions, &count); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(sessions)))
|
||||
|
||||
sessionSlice := unsafe.Slice(sessions, count)
|
||||
|
||||
ids := make([]uint32, 0, len(sessionSlice))
|
||||
for _, session := range sessionSlice {
|
||||
if session.SessionID == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if session.State != windows.WTSActive && session.State != windows.WTSConnected {
|
||||
continue
|
||||
}
|
||||
|
||||
ids = append(ids, session.SessionID)
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
func appendUniqueSessionID(ids []uint32, sessionID uint32) []uint32 {
|
||||
for _, id := range ids {
|
||||
if id == sessionID {
|
||||
return ids
|
||||
}
|
||||
}
|
||||
|
||||
return append(ids, sessionID)
|
||||
}
|
||||
|
||||
func sidFromSessionID(sessionID uint32) (string, error) {
|
||||
sid, err := sidFromSessionInformation(sessionID)
|
||||
if err == nil {
|
||||
return sid, nil
|
||||
}
|
||||
|
||||
if isCurrentProcessLocalSystem() {
|
||||
if sid, tokenErr := sidFromSessionUserToken(sessionID); tokenErr == nil {
|
||||
return sid, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
func sidFromSessionInformation(sessionID uint32) (string, error) {
|
||||
user, domain, err := sessionUserAndDomain(sessionID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
account := user
|
||||
system := ""
|
||||
if domain != "" {
|
||||
account = domain + `\` + user
|
||||
}
|
||||
|
||||
sid, _, _, err := windows.LookupSID(system, account)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot lookup session %d user SID: %w", sessionID, err)
|
||||
}
|
||||
|
||||
sidStr := sid.String()
|
||||
if sidStr == "" {
|
||||
return "", fmt.Errorf("session %d user SID is empty", sessionID)
|
||||
}
|
||||
|
||||
if !isInteractiveUserSID(sidStr) {
|
||||
return "", fmt.Errorf("session %d has no interactive user", sessionID)
|
||||
}
|
||||
|
||||
return sidStr, nil
|
||||
}
|
||||
|
||||
func sidFromSessionUserToken(sessionID uint32) (string, error) {
|
||||
var token windows.Token
|
||||
|
||||
if err := windows.WTSQueryUserToken(sessionID, &token); err != nil {
|
||||
return "", fmt.Errorf("cannot query session %d user token: %w", sessionID, err)
|
||||
}
|
||||
|
||||
defer func() { _ = token.Close() }()
|
||||
|
||||
tu, err := token.GetTokenUser()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot read session %d user: %w", sessionID, err)
|
||||
}
|
||||
|
||||
if tu.User.Sid == nil {
|
||||
return "", fmt.Errorf("session %d user SID is empty", sessionID)
|
||||
}
|
||||
|
||||
sid := tu.User.Sid.String()
|
||||
if sid == "" {
|
||||
return "", fmt.Errorf("session %d user SID is empty", sessionID)
|
||||
}
|
||||
|
||||
if !isInteractiveUserSID(sid) {
|
||||
return "", fmt.Errorf("session %d has no interactive user", sessionID)
|
||||
}
|
||||
|
||||
return sid, nil
|
||||
}
|
||||
|
||||
func isCurrentProcessLocalSystem() bool {
|
||||
token, err := windows.OpenCurrentProcessToken()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
defer func() { _ = token.Close() }()
|
||||
|
||||
tu, err := token.GetTokenUser()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
systemSID, err := windows.CreateWellKnownSid(windows.WinLocalSystemSid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return tu.User.Sid.Equals(systemSID)
|
||||
}
|
||||
|
||||
func isInteractiveUserSID(sid string) bool {
|
||||
return strings.HasPrefix(sid, "S-1-5-21-") ||
|
||||
strings.HasPrefix(sid, "S-1-12-1-")
|
||||
}
|
||||
128
pkg/deviceagent/tray/console_user_windows_test.go
Normal file
128
pkg/deviceagent/tray/console_user_windows_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsInteractiveUserSID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sid string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "domain user sid",
|
||||
sid: "S-1-5-21-1234567890-123456789-123456789-1001",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "entra user sid",
|
||||
sid: "S-1-12-1-3603547745-1252762009-756918658-301435180",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "entra authority without user subauthority",
|
||||
sid: "S-1-12-2-3603547745-1252762009-756918658-301435180",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "bare entra authority prefix",
|
||||
sid: "S-1-12-",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "local system sid",
|
||||
sid: "S-1-5-18",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "builtin administrators sid",
|
||||
sid: "S-1-5-32-544",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty sid",
|
||||
sid: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(
|
||||
tt.name,
|
||||
func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, tt.want, isInteractiveUserSID(tt.sid))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendUniqueSessionID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ids []uint32
|
||||
sessionID uint32
|
||||
want []uint32
|
||||
}{
|
||||
{
|
||||
name: "append to empty slice",
|
||||
ids: nil,
|
||||
sessionID: 2,
|
||||
want: []uint32{2},
|
||||
},
|
||||
{
|
||||
name: "append new session",
|
||||
ids: []uint32{1},
|
||||
sessionID: 2,
|
||||
want: []uint32{1, 2},
|
||||
},
|
||||
{
|
||||
name: "skip duplicate session",
|
||||
ids: []uint32{1, 2},
|
||||
sessionID: 2,
|
||||
want: []uint32{1, 2},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(
|
||||
tt.name,
|
||||
func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := appendUniqueSessionID(tt.ids, tt.sessionID)
|
||||
assert.Equal(t, tt.want, got)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
41
pkg/deviceagent/tray/dialog_darwin.go
Normal file
41
pkg/deviceagent/tray/dialog_darwin.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func showAbout(version string) {
|
||||
path, ok := osascriptPath()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
script := fmt.Sprintf(
|
||||
`display alert "Probo Device Posture Agent" message %q buttons {"OK"} default button "OK"`,
|
||||
fmt.Sprintf("Version %s\n\nReports device posture to your Probo workspace.", version),
|
||||
)
|
||||
_ = exec.Command(path, "-e", script).Run()
|
||||
}
|
||||
35
pkg/deviceagent/tray/dialog_windows.go
Normal file
35
pkg/deviceagent/tray/dialog_windows.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func showAbout(version string) {
|
||||
message := fmt.Sprintf(
|
||||
"Version %s\r\n\r\nReports device posture to your Probo workspace.",
|
||||
version,
|
||||
)
|
||||
nativeMessageBox("Probo Device Posture Agent", message, mbOK|mbIconInformation)
|
||||
}
|
||||
BIN
pkg/deviceagent/tray/icon.png
Normal file
BIN
pkg/deviceagent/tray/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 B |
BIN
pkg/deviceagent/tray/iconTemplate.png
Normal file
BIN
pkg/deviceagent/tray/iconTemplate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 B |
31
pkg/deviceagent/tray/icons.go
Normal file
31
pkg/deviceagent/tray/icons.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package tray
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// Icons are embedded PNGs; systray does not accept SVG.
|
||||
|
||||
//go:embed icon.png
|
||||
var iconData []byte
|
||||
|
||||
//go:embed iconTemplate.png
|
||||
var iconTemplateData []byte
|
||||
29
pkg/deviceagent/tray/icons_darwin.go
Normal file
29
pkg/deviceagent/tray/icons_darwin.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build darwin && cgo
|
||||
|
||||
package tray
|
||||
|
||||
import "fyne.io/systray"
|
||||
|
||||
func setTrayIcons() {
|
||||
systray.SetTemplateIcon(iconTemplateData, iconData)
|
||||
}
|
||||
29
pkg/deviceagent/tray/icons_windows.go
Normal file
29
pkg/deviceagent/tray/icons_windows.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package tray
|
||||
|
||||
import "fyne.io/systray"
|
||||
|
||||
func setTrayIcons() {
|
||||
systray.SetIcon(iconData)
|
||||
}
|
||||
20
pkg/deviceagent/tray/launchagent.plist.tmpl
Normal file
20
pkg/deviceagent/tray/launchagent.plist.tmpl
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>{{xml .Label}}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{xml .ExePath}}</string>
|
||||
<string>tray</string>
|
||||
<string>--run-dir</string>
|
||||
<string>{{xml .RunDir}}</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
58
pkg/deviceagent/tray/messagebox_windows.go
Normal file
58
pkg/deviceagent/tray/messagebox_windows.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
mbOK = 0x00000000
|
||||
mbIconInformation = 0x00000040
|
||||
)
|
||||
|
||||
var (
|
||||
modUser32 = windows.NewLazySystemDLL("user32.dll")
|
||||
procMessageBoxW = modUser32.NewProc("MessageBoxW")
|
||||
)
|
||||
|
||||
func nativeMessageBox(title, message string, flags uint32) {
|
||||
titleUTF16, err := windows.UTF16PtrFromString(title)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
messageUTF16, err := windows.UTF16PtrFromString(message)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _, _ = procMessageBoxW.Call(
|
||||
0,
|
||||
uintptr(unsafe.Pointer(messageUTF16)),
|
||||
uintptr(unsafe.Pointer(titleUTF16)),
|
||||
uintptr(flags),
|
||||
)
|
||||
}
|
||||
34
pkg/deviceagent/tray/osascript_darwin.go
Normal file
34
pkg/deviceagent/tray/osascript_darwin.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package tray
|
||||
|
||||
import "go.probo.inc/probo/pkg/deviceagent/checks"
|
||||
|
||||
func osascriptPath() (string, bool) {
|
||||
candidates := checks.CommandCandidates("osascript")
|
||||
if len(candidates) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return candidates[0], true
|
||||
}
|
||||
28
pkg/deviceagent/tray/tray.go
Normal file
28
pkg/deviceagent/tray/tray.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package tray
|
||||
|
||||
type Options struct {
|
||||
RunDir string
|
||||
ExePath string
|
||||
ServerURL string
|
||||
Version string
|
||||
}
|
||||
29
pkg/deviceagent/tray/tray_nocgo.go
Normal file
29
pkg/deviceagent/tray/tray_nocgo.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build darwin && !cgo
|
||||
|
||||
package tray
|
||||
|
||||
import "errors"
|
||||
|
||||
func Run(_ Options) error {
|
||||
return errors.New("tray helper requires a CGO-enabled build (set CGO_ENABLED=1)")
|
||||
}
|
||||
37
pkg/deviceagent/tray/tray_stub.go
Normal file
37
pkg/deviceagent/tray/tray_stub.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build !darwin && !windows
|
||||
|
||||
package tray
|
||||
|
||||
import "errors"
|
||||
|
||||
func Run(_ Options) error {
|
||||
return errors.New("tray helper is only supported on macOS and Windows")
|
||||
}
|
||||
|
||||
func RegisterAutoStart(_ string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnregisterAutoStart() error {
|
||||
return nil
|
||||
}
|
||||
151
pkg/deviceagent/tray/tray_supported.go
Normal file
151
pkg/deviceagent/tray/tray_supported.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build (darwin && cgo) || windows
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"fyne.io/systray"
|
||||
"go.probo.inc/probo/pkg/deviceagent"
|
||||
)
|
||||
|
||||
func Run(opts Options) error {
|
||||
if opts.ServerURL == "" {
|
||||
opts.ServerURL = deviceagent.DefaultServerURL
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
var shutdown sync.Once
|
||||
|
||||
stop := func() {
|
||||
shutdown.Do(func() {
|
||||
close(done)
|
||||
})
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(sigCh)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-sigCh:
|
||||
stop()
|
||||
systray.Quit()
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
systray.Run(
|
||||
func() {
|
||||
onReady(opts, done)
|
||||
},
|
||||
stop,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func onReady(opts Options, done <-chan struct{}) {
|
||||
setTrayIcons()
|
||||
|
||||
systray.SetTitle("Probo")
|
||||
|
||||
enrollmentRequiredItem := systray.AddMenuItem("Enrollment required", "Enroll from the Probo console")
|
||||
enrollmentRequiredItem.Disable()
|
||||
|
||||
connectedItem := systray.AddMenuItem("Connected", "Device is enrolled and reporting")
|
||||
connectedItem.Disable()
|
||||
|
||||
statusUnavailableItem := systray.AddMenuItem(
|
||||
"Status unavailable",
|
||||
"Cannot read enrollment status",
|
||||
)
|
||||
statusUnavailableItem.Disable()
|
||||
statusUnavailableItem.Hide()
|
||||
|
||||
systray.AddSeparator()
|
||||
|
||||
aboutItem := systray.AddMenuItem(
|
||||
fmt.Sprintf("About probo-agent %s", opts.Version),
|
||||
"Probo device posture agent",
|
||||
)
|
||||
|
||||
updateMenu := func() {
|
||||
enrolled, err := deviceagent.IsEnrolled(opts.RunDir)
|
||||
if err != nil {
|
||||
enrollmentRequiredItem.Hide()
|
||||
connectedItem.Hide()
|
||||
statusUnavailableItem.Show()
|
||||
systray.SetTooltip("Probo Device Posture Agent — Status unavailable")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
statusUnavailableItem.Hide()
|
||||
|
||||
if enrolled {
|
||||
enrollmentRequiredItem.Hide()
|
||||
connectedItem.Show()
|
||||
systray.SetTooltip("Probo Device Posture Agent — Connected")
|
||||
} else {
|
||||
enrollmentRequiredItem.Show()
|
||||
connectedItem.Hide()
|
||||
systray.SetTooltip("Probo Device Posture Agent — Enrollment required")
|
||||
}
|
||||
}
|
||||
|
||||
updateMenu()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
updateMenu()
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-aboutItem.ClickedCh:
|
||||
showAbout(opts.Version)
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
87
pkg/deviceagent/tray/wts_session_windows.go
Normal file
87
pkg/deviceagent/tray/wts_session_windows.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package tray
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
wtsUserName = 5
|
||||
wtsDomainName = 7
|
||||
)
|
||||
|
||||
var (
|
||||
modWtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll")
|
||||
procWTSQuerySessionInformationW = modWtsapi32.NewProc("WTSQuerySessionInformationW")
|
||||
)
|
||||
|
||||
func wtsQuerySessionString(sessionID uint32, infoClass uint32) (string, error) {
|
||||
var (
|
||||
buffer *uint16
|
||||
bytesReturned uint32
|
||||
)
|
||||
|
||||
r0, _, err := procWTSQuerySessionInformationW.Call(
|
||||
0,
|
||||
uintptr(sessionID),
|
||||
uintptr(infoClass),
|
||||
uintptr(unsafe.Pointer(&buffer)),
|
||||
uintptr(unsafe.Pointer(&bytesReturned)),
|
||||
)
|
||||
if r0 == 0 {
|
||||
if err != syscall.Errno(0) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "", syscall.EINVAL
|
||||
}
|
||||
|
||||
defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(buffer)))
|
||||
|
||||
return windows.UTF16PtrToString(buffer), nil
|
||||
}
|
||||
|
||||
func sessionUserAndDomain(sessionID uint32) (string, string, error) {
|
||||
user, err := wtsQuerySessionString(sessionID, wtsUserName)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("cannot query session %d username: %w", sessionID, err)
|
||||
}
|
||||
|
||||
user = strings.TrimSpace(user)
|
||||
if user == "" {
|
||||
return "", "", fmt.Errorf("session %d username is empty", sessionID)
|
||||
}
|
||||
|
||||
domain, err := wtsQuerySessionString(sessionID, wtsDomainName)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("cannot query session %d domain: %w", sessionID, err)
|
||||
}
|
||||
|
||||
return user, strings.TrimSpace(domain), nil
|
||||
}
|
||||
Reference in New Issue
Block a user