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)
|
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 {
|
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,
|
ctx,
|
||||||
"defaults",
|
"defaults",
|
||||||
"read",
|
"read",
|
||||||
"/Library/Preferences/com.apple.SoftwareUpdate",
|
darwinSoftwareUpdateManagedDomain,
|
||||||
"AutomaticCheckEnabled",
|
key,
|
||||||
)
|
)
|
||||||
if primary.Err == nil {
|
if managed.Err == nil {
|
||||||
ev := map[string]any{
|
return darwinSoftwareUpdatePref{
|
||||||
"backend": "defaults",
|
value: strings.TrimSpace(managed.Stdout),
|
||||||
"automatic_check_enabled": primary.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)
|
return fail(ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
fallback := RunCommand(ctx, "softwareupdate", "--schedule")
|
if len(indeterminate) > 0 {
|
||||||
|
ev["indeterminate_keys"] = indeterminate
|
||||||
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
|
|
||||||
|
|
||||||
return unknown(ev)
|
return unknown(ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
lower := strings.ToLower(fallback.Stdout)
|
return pass(ev)
|
||||||
switch {
|
}
|
||||||
case strings.Contains(lower, "is turned on"),
|
|
||||||
strings.Contains(lower, "automatic check is on"):
|
func darwinPrefIndicatesEnabled(value string) bool {
|
||||||
return pass(ev)
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
case strings.Contains(lower, "is turned off"),
|
case "1", "true", "yes":
|
||||||
strings.Contains(lower, "automatic check is off"):
|
return true
|
||||||
return fail(ev)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return unknown(ev)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func darwinPasswordPolicy(ctx context.Context) Result {
|
func darwinPasswordPolicy(ctx context.Context) Result {
|
||||||
|
|||||||
@@ -21,16 +21,15 @@
|
|||||||
package checks
|
package checks
|
||||||
|
|
||||||
var darwinCommandPaths = map[string][]string{
|
var darwinCommandPaths = map[string][]string{
|
||||||
"defaults": {"/usr/bin/defaults"},
|
"defaults": {"/usr/bin/defaults"},
|
||||||
"fdesetup": {"/usr/bin/fdesetup"},
|
"fdesetup": {"/usr/bin/fdesetup"},
|
||||||
"osascript": {"/usr/bin/osascript"},
|
"osascript": {"/usr/bin/osascript"},
|
||||||
"pwpolicy": {"/usr/bin/pwpolicy"},
|
"pwpolicy": {"/usr/bin/pwpolicy"},
|
||||||
"softwareupdate": {"/usr/sbin/softwareupdate"},
|
"stat": {"/usr/bin/stat"},
|
||||||
"stat": {"/usr/bin/stat"},
|
"sudo": {"/usr/bin/sudo"},
|
||||||
"sudo": {"/usr/bin/sudo"},
|
"sw_vers": {"/usr/bin/sw_vers"},
|
||||||
"sw_vers": {"/usr/bin/sw_vers"},
|
"sysadminctl": {"/usr/sbin/sysadminctl"},
|
||||||
"sysadminctl": {"/usr/sbin/sysadminctl"},
|
"systemsetup": {"/usr/sbin/systemsetup"},
|
||||||
"systemsetup": {"/usr/sbin/systemsetup"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func commandCandidates(cmd string) []string {
|
func commandCandidates(cmd string) []string {
|
||||||
|
|||||||
Reference in New Issue
Block a user