From 9e6ccc48034c0d16f3a872748cbef1a25c9ec6a8 Mon Sep 17 00:00:00 2001 From: Zach Wasserman Date: Mon, 21 Nov 2022 12:56:15 -0600 Subject: [PATCH] Add `icloud_private_relay` table (#8655) --- orbit/changes/icloud-private-relay | 1 + orbit/pkg/table/extension_darwin.go | 5 ++ orbit/pkg/table/privaterelay/privaterelay.go | 80 ++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 orbit/changes/icloud-private-relay create mode 100644 orbit/pkg/table/privaterelay/privaterelay.go diff --git a/orbit/changes/icloud-private-relay b/orbit/changes/icloud-private-relay new file mode 100644 index 0000000000..03a9da4914 --- /dev/null +++ b/orbit/changes/icloud-private-relay @@ -0,0 +1 @@ +- Implement `icloud_private_relay` table to get iCloud Private Relay status. diff --git a/orbit/pkg/table/extension_darwin.go b/orbit/pkg/table/extension_darwin.go index 00919ef013..4fb0def52f 100644 --- a/orbit/pkg/table/extension_darwin.go +++ b/orbit/pkg/table/extension_darwin.go @@ -3,6 +3,7 @@ package table import ( + "github.com/fleetdm/fleet/v4/orbit/pkg/table/privaterelay" "github.com/osquery/osquery-go" "github.com/osquery/osquery-go/plugin/table" @@ -15,6 +16,10 @@ import ( func platformTables() []osquery.OsqueryPlugin { return []osquery.OsqueryPlugin{ + // Fleet tables + table.NewPlugin("icloud_private_relay", privaterelay.Columns(), privaterelay.Generate), + + // Macadmins extension tables table.NewPlugin("filevault_users", filevaultusers.FileVaultUsersColumns(), filevaultusers.FileVaultUsersGenerate), table.NewPlugin("macos_profiles", macos_profiles.MacOSProfilesColumns(), macos_profiles.MacOSProfilesGenerate), table.NewPlugin("mdm", mdm.MDMInfoColumns(), mdm.MDMInfoGenerate), diff --git a/orbit/pkg/table/privaterelay/privaterelay.go b/orbit/pkg/table/privaterelay/privaterelay.go new file mode 100644 index 0000000000..04a6a0c6f0 --- /dev/null +++ b/orbit/pkg/table/privaterelay/privaterelay.go @@ -0,0 +1,80 @@ +//go:build darwin +// +build darwin + +package privaterelay + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + "syscall" + "time" + + "github.com/osquery/osquery-go/plugin/table" +) + +// Columns is the schema of the table. +func Columns() []table.ColumnDefinition { + return []table.ColumnDefinition{ + table.IntegerColumn("status"), + } +} + +// Generate is called to return the results for the table at query time. +// +// Constraints for generating can be retrieved from the queryContext. +func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { + uid, gid, err := getConsoleUidGid() + if err != nil { + return nil, fmt.Errorf("failed to get console user: %w", err) + } + + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + cmd := exec.CommandContext( + ctx, + "bash", "-c", + // This seems a bit brittle, but it works as of now. When it breaks we might want to look + // into finding a more resilient mechanism for getting the status. This method discovered by + // @sharvilshah. + `defaults export com.apple.networkserviceproxy - | plutil -extract NSPServiceStatusManagerInfo raw - -o - | base64 -D | plutil -convert xml1 - -o - | plutil -p - | grep '"PrivacyProxyServiceStatus" =>' | head -1`, + ) + // Run as the current console user (otherwise we get empty results for the root user) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{Uid: uid, Gid: gid}, + } + + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("generate failed: %w", err) + } + + switch s := strings.TrimSpace(string(out)); s { + case `"PrivacyProxyServiceStatus" => 0`: + return []map[string]string{{"status": "0"}}, nil + + case `"PrivacyProxyServiceStatus" => 1`: + return []map[string]string{{"status": "1"}}, nil + + default: + return nil, fmt.Errorf("failed to parse: '%s'", s) + } +} + +// getActiveUserGroup gets the uid and gid of the current (or more accurately, most recently logged +// in) *console* user. In most scenarios this should be the currently logged in user on the system. +// Note that getting the current user of the Orbit process is typically going to return root and we +// need the underlying user. +func getConsoleUidGid() (uid uint32, gid uint32, err error) { + info, err := os.Stat("/dev/console") + if err != nil { + return 0, 0, err + } + stat, ok := info.Sys().(*syscall.Stat_t) + if !ok { + return 0, 0, fmt.Errorf("unexpected type %T", info.Sys()) + } + return stat.Uid, stat.Gid, nil +}