diff --git a/pkg/deviceagent/checks/checks_darwin.go b/pkg/deviceagent/checks/checks_darwin.go index 3c4d8c688..694804557 100644 --- a/pkg/deviceagent/checks/checks_darwin.go +++ b/pkg/deviceagent/checks/checks_darwin.go @@ -376,54 +376,154 @@ func darwinOSVersion(ctx context.Context) Result { return pass(ev) } +const ( + darwinSoftwareUpdateManagedDomain = "/Library/Managed Preferences/com.apple.SoftwareUpdate" + darwinSoftwareUpdateSystemDomain = "/Library/Preferences/com.apple.SoftwareUpdate" + + darwinPrefSourceManaged = "managed" + darwinPrefSourceSystem = "system" + darwinPrefSourceDefault = "default" +) + +// darwinSoftwareUpdateKeys must all stay enabled for AUTO_UPDATE to pass. They +// map to the System Settings toggles for checking, downloading and installing +// macOS updates, security responses and system data files. App Store app +// updates are out of scope, matching the Windows and Linux implementations. +var darwinSoftwareUpdateKeys = []string{ + "AutomaticCheckEnabled", + "AutomaticDownload", + "AutomaticallyInstallMacOSUpdates", + "CriticalUpdateInstall", + "ConfigDataInstall", +} + +// darwinSoftwareUpdatePref is one Software Update preference resolved across +// the CFPreferences layers. An empty value with source default means macOS +// falls back to its own default, which is enabled for every key we read. +type darwinSoftwareUpdatePref struct { + value string + source string + err string + stderr string +} + func darwinAutoUpdate(ctx context.Context) Result { - primary := RunCommand( + prefs := make(map[string]darwinSoftwareUpdatePref, len(darwinSoftwareUpdateKeys)) + for _, key := range darwinSoftwareUpdateKeys { + prefs[key] = darwinReadSoftwareUpdatePref(ctx, key) + } + + return darwinSoftwareUpdateResult(prefs) +} + +// darwinReadSoftwareUpdatePref resolves one key with MDM precedence: a value +// forced by a configuration profile wins over the local system preference +// because the user cannot override it. +func darwinReadSoftwareUpdatePref(ctx context.Context, key string) darwinSoftwareUpdatePref { + managed := RunCommand( ctx, "defaults", "read", - "/Library/Preferences/com.apple.SoftwareUpdate", - "AutomaticCheckEnabled", + darwinSoftwareUpdateManagedDomain, + key, ) - if primary.Err == nil { - ev := map[string]any{ - "backend": "defaults", - "automatic_check_enabled": primary.Stdout, + if managed.Err == nil { + return darwinSoftwareUpdatePref{ + value: strings.TrimSpace(managed.Stdout), + source: darwinPrefSourceManaged, } - if strings.TrimSpace(primary.Stdout) == "1" { - return pass(ev) + } + + if !darwinDefaultsMissing(managed) { + return darwinSoftwareUpdatePref{ + source: darwinPrefSourceManaged, + err: errString(managed.Err), + stderr: managed.Stderr, } + } + + system := RunCommand( + ctx, + "defaults", + "read", + darwinSoftwareUpdateSystemDomain, + key, + ) + if system.Err == nil { + return darwinSoftwareUpdatePref{ + value: strings.TrimSpace(system.Stdout), + source: darwinPrefSourceSystem, + } + } + + if darwinDefaultsMissing(system) { + return darwinSoftwareUpdatePref{source: darwinPrefSourceDefault} + } + + return darwinSoftwareUpdatePref{ + source: darwinPrefSourceSystem, + err: errString(system.Err), + stderr: system.Stderr, + } +} + +func darwinSoftwareUpdateResult(prefs map[string]darwinSoftwareUpdatePref) Result { + ev := map[string]any{"backend": "defaults"} + + var disabled, indeterminate []string + + for _, key := range darwinSoftwareUpdateKeys { + pref := prefs[key] + + entry := map[string]any{"source": pref.source} + if pref.value != "" { + entry["value"] = pref.value + } + + switch { + case pref.err != "": + entry["error"] = pref.err + if pref.stderr != "" { + entry["stderr"] = truncate(pref.stderr, 256) + } + + indeterminate = append(indeterminate, key) + case pref.source == darwinPrefSourceDefault: + entry["enabled"] = true + default: + enabled := darwinPrefIndicatesEnabled(pref.value) + entry["enabled"] = enabled + + if !enabled { + disabled = append(disabled, key) + } + } + + ev[key] = entry + } + + if len(disabled) > 0 { + ev["disabled_keys"] = disabled return fail(ev) } - fallback := RunCommand(ctx, "softwareupdate", "--schedule") - - ev := map[string]any{ - "backend": "softwareupdate", - "raw": fallback.Stdout, - "defaults_error": errString(primary.Err), - "defaults_stderr": primary.Stderr, - } - if fallback.Err != nil || - needsAdmin(fallback.Stdout) || - needsAdmin(fallback.Stderr) { - ev["error"] = errString(fallback.Err) - ev["stderr"] = fallback.Stderr + if len(indeterminate) > 0 { + ev["indeterminate_keys"] = indeterminate return unknown(ev) } - lower := strings.ToLower(fallback.Stdout) - switch { - case strings.Contains(lower, "is turned on"), - strings.Contains(lower, "automatic check is on"): - return pass(ev) - case strings.Contains(lower, "is turned off"), - strings.Contains(lower, "automatic check is off"): - return fail(ev) + return pass(ev) +} + +func darwinPrefIndicatesEnabled(value string) bool { + switch strings.ToLower(strings.TrimSpace(value)) { + case "1", "true", "yes": + return true } - return unknown(ev) + return false } func darwinPasswordPolicy(ctx context.Context) Result { diff --git a/pkg/deviceagent/checks/runcmd_paths_darwin.go b/pkg/deviceagent/checks/runcmd_paths_darwin.go index 0fec4cdeb..d44f25090 100644 --- a/pkg/deviceagent/checks/runcmd_paths_darwin.go +++ b/pkg/deviceagent/checks/runcmd_paths_darwin.go @@ -21,16 +21,15 @@ package checks var darwinCommandPaths = map[string][]string{ - "defaults": {"/usr/bin/defaults"}, - "fdesetup": {"/usr/bin/fdesetup"}, - "osascript": {"/usr/bin/osascript"}, - "pwpolicy": {"/usr/bin/pwpolicy"}, - "softwareupdate": {"/usr/sbin/softwareupdate"}, - "stat": {"/usr/bin/stat"}, - "sudo": {"/usr/bin/sudo"}, - "sw_vers": {"/usr/bin/sw_vers"}, - "sysadminctl": {"/usr/sbin/sysadminctl"}, - "systemsetup": {"/usr/sbin/systemsetup"}, + "defaults": {"/usr/bin/defaults"}, + "fdesetup": {"/usr/bin/fdesetup"}, + "osascript": {"/usr/bin/osascript"}, + "pwpolicy": {"/usr/bin/pwpolicy"}, + "stat": {"/usr/bin/stat"}, + "sudo": {"/usr/bin/sudo"}, + "sw_vers": {"/usr/bin/sw_vers"}, + "sysadminctl": {"/usr/sbin/sysadminctl"}, + "systemsetup": {"/usr/sbin/systemsetup"}, } func commandCandidates(cmd string) []string {