Fix macOS AUTO_UPDATE when updates are disabled
The check read only AutomaticCheckEnabled from /Library/Preferences, falling back to softwareupdate --schedule when that key was absent. Both describe automatic checking alone, so a Mac with downloads or installs turned off still reported PASS. The key is also absent when a configuration profile manages it, since the value then lives in /Library/Managed Preferences, a layer never consulted. Read the five Software Update preferences backing the System Settings toggles, resolving each from the managed layer before the system one. macOS treats them as enabled when unset, so only an explicit disabled value fails. The now-unused softwareupdate binary leaves the command allowlist. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user