Files
probo/pkg/deviceagent/checks/checks_darwin.go
Sacha Al Himdani 4c57d201a4 Make license declarations consistently MIT
The source headers, LICENSE files, and license metadata had drifted
apart. Align the entire project to MIT:

- Convert every source-file header to the MIT text across all comment
  styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including
  SPDX-License-Identifier tags
- Set the root and cookie-banner LICENSE files to the MIT text with a
  "MIT License" title line
- Switch the package.json license fields, Docker image label, and
  cookie-banner README to MIT
- Update docs and the genmodels header generator accordingly
- Normalize copyright lines to a single format
  (Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the
  hello@getprobo.com and hello@probo.inc emails to hello@probo.com and
  the comma-separated years to a hyphenated range

Genuine third-party references are intentionally left untouched: the
Lucide icon attributions (Lucide is ISC) and the trivy dependency
license allowlist.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-13 16:21:14 +02:00

509 lines
12 KiB
Go

// 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 checks
import (
"context"
"os"
"strconv"
"strings"
)
func init() {
Register(KeyDiskEncryption, darwinDiskEncryption)
Register(KeyScreenLock, darwinScreenLock)
Register(KeyFirewallEnabled, darwinFirewall)
Register(KeyTimeSync, darwinTimeSync)
Register(KeyOSVersion, darwinOSVersion)
Register(KeyAutoUpdate, darwinAutoUpdate)
Register(KeyPasswordPolicy, darwinPasswordPolicy)
Register(KeyRemoteLogin, darwinRemoteLogin)
Register(KeyMalwareProtection, darwinMalwareProtection)
}
func darwinDiskEncryption(ctx context.Context) Result {
out := RunCommand(ctx, "fdesetup", "status")
if out.Err != nil {
return unknown(
map[string]any{
"error": out.Err.Error(),
"stderr": out.Stderr,
},
)
}
on := strings.Contains(strings.ToLower(out.Stdout), "filevault is on")
ev := map[string]any{"raw": out.Stdout}
if on {
return pass(ev)
}
return fail(ev)
}
func darwinScreenLock(ctx context.Context) Result {
if CommandExists("sysadminctl") {
status := RunCommand(ctx, "sysadminctl", "-screenLock", "status", "-password", "-")
rawCombined := strings.TrimSpace(status.Stdout + "\n" + status.Stderr)
ev := map[string]any{
"backend": "sysadminctl",
"raw": rawCombined,
"raw_stdout": status.Stdout,
"raw_stderr": status.Stderr,
}
mode, seconds, ok := darwinScreenLockMode(rawCombined)
if ok {
ev["mode"] = mode
if mode == "seconds" && seconds >= 0 {
ev["seconds"] = seconds
}
if mode == "immediate" {
return pass(ev)
}
return fail(ev)
}
if status.Err != nil {
ev["error"] = status.Err.Error()
}
}
ask, askSource := darwinReadScreenSaverDefault(ctx, "askForPassword")
ev := map[string]any{}
if askSource != "" {
ev["source"] = askSource
}
if ask.Err != nil {
if darwinDefaultsMissing(ask) {
ev["ask_for_password"] = "0"
ev["note"] = "askForPassword is unset or unavailable"
return fail(ev)
}
ev["error"] = ask.Err.Error()
ev["stderr"] = ask.Stderr
return unknown(ev)
}
enabled := strings.TrimSpace(ask.Stdout) == "1"
delayCmd, delaySource := darwinReadScreenSaverDefault(ctx, "askForPasswordDelay")
ev["ask_for_password"] = ask.Stdout
if delayCmd.Err == nil {
ev["ask_for_password_delay"] = delayCmd.Stdout
if delaySource != "" && delaySource != askSource {
ev["delay_source"] = delaySource
}
}
if enabled {
return pass(ev)
}
return fail(ev)
}
func darwinScreenLockMode(raw string) (string, int, bool) {
lower := strings.ToLower(raw)
if strings.Contains(lower, "immediate") {
return "immediate", 0, true
}
if strings.Contains(lower, "off") {
return "off", -1, true
}
if before, _, ok := strings.Cut(lower, "seconds"); ok {
prefix := strings.Fields(before)
if len(prefix) == 0 {
return "seconds", -1, true
}
n, err := strconv.Atoi(prefix[len(prefix)-1])
if err != nil {
return "seconds", -1, true
}
return "seconds", n, true
}
return "", 0, false
}
func darwinFirewall(ctx context.Context) Result {
out := RunCommand(
ctx,
"defaults",
"read",
"/Library/Preferences/com.apple.alf",
"globalstate",
)
if out.Err == nil {
state := strings.TrimSpace(out.Stdout)
ev := map[string]any{"backend": "defaults", "global_state": state}
if state == "1" || state == "2" {
return pass(ev)
}
return fail(ev)
}
fallback := RunCommand(ctx, "/usr/libexec/ApplicationFirewall/socketfilterfw", "--getglobalstate")
ev := map[string]any{
"backend": "socketfilterfw",
"raw": fallback.Stdout,
"defaults_error": errString(out.Err),
"defaults_stderr": out.Stderr,
}
if fallback.Err != nil {
ev["error"] = fallback.Err.Error()
ev["stderr"] = fallback.Stderr
return unknown(ev)
}
if darwinStateIndicatesEnabled(fallback.Stdout) {
return pass(ev)
}
if darwinStateIndicatesDisabled(fallback.Stdout) {
return fail(ev)
}
return unknown(ev)
}
// darwinReadScreenSaverDefault prefers console-user settings when running as root.
func darwinReadScreenSaverDefault(ctx context.Context, key string) (CmdResult, string) {
consoleUser := darwinConsoleUser(ctx)
if os.Geteuid() == 0 && consoleUser != "" {
var consoleMissing CmdResult
consoleMissingSource := ""
if CommandExists("sudo") {
consoleUserCurrentHost := RunCommand(
ctx,
"sudo",
"-u",
consoleUser,
"defaults",
"-currentHost",
"read",
"com.apple.screensaver",
key,
)
if consoleUserCurrentHost.Err == nil {
return consoleUserCurrentHost, "console_user_current_host:" + consoleUser
}
if !darwinDefaultsMissing(consoleUserCurrentHost) {
return consoleUserCurrentHost, "console_user_current_host:" + consoleUser
}
if consoleMissingSource == "" {
consoleMissing = consoleUserCurrentHost
consoleMissingSource = "console_user_current_host:" + consoleUser
}
consoleUserDomain := RunCommand(
ctx,
"sudo",
"-u",
consoleUser,
"defaults",
"read",
"com.apple.screensaver",
key,
)
if consoleUserDomain.Err == nil {
return consoleUserDomain, "console_user:" + consoleUser
}
if !darwinDefaultsMissing(consoleUserDomain) {
return consoleUserDomain, "console_user:" + consoleUser
}
if consoleMissingSource == "" {
consoleMissing = consoleUserDomain
consoleMissingSource = "console_user:" + consoleUser
}
}
plistPath := "/Users/" + consoleUser + "/Library/Preferences/com.apple.screensaver.plist"
consoleUserOut := RunCommand(ctx, "defaults", "read", plistPath, key)
if consoleUserOut.Err == nil {
return consoleUserOut, "console_user_plist:" + consoleUser
}
if !darwinDefaultsMissing(consoleUserOut) {
return consoleUserOut, "console_user_plist:" + consoleUser
}
if consoleMissingSource == "" {
consoleMissing = consoleUserOut
consoleMissingSource = "console_user_plist:" + consoleUser
}
if consoleMissingSource != "" {
return consoleMissing, consoleMissingSource
}
}
currentHost := RunCommand(ctx, "defaults", "-currentHost", "read", "com.apple.screensaver", key)
if currentHost.Err == nil {
return currentHost, "current_user_current_host"
}
currentUser := RunCommand(ctx, "defaults", "read", "com.apple.screensaver", key)
if currentUser.Err == nil {
return currentUser, "current_user"
}
if !darwinDefaultsMissing(currentUser) {
return currentUser, "current_user"
}
if !darwinDefaultsMissing(currentHost) {
return currentHost, "current_user_current_host"
}
return currentUser, "current_user"
}
func darwinDefaultsMissing(out CmdResult) bool {
lower := strings.ToLower(out.Stderr + "\n" + out.Stdout)
return strings.Contains(lower, "does not exist") ||
strings.Contains(lower, "could not find") ||
strings.Contains(lower, "does not exist in domain")
}
func darwinConsoleUser(ctx context.Context) string {
if sudoUser := strings.TrimSpace(os.Getenv("SUDO_USER")); sudoUser != "" && sudoUser != "root" {
return sudoUser
}
out := RunCommand(ctx, "stat", "-f", "%Su", "/dev/console")
if out.Err != nil {
return ""
}
user := strings.TrimSpace(out.Stdout)
if user == "" || user == "root" || user == "loginwindow" {
return ""
}
return user
}
func darwinStateIndicatesEnabled(raw string) bool {
lower := strings.ToLower(raw)
return strings.Contains(lower, "enabled") ||
strings.Contains(lower, "state = 1") ||
strings.Contains(lower, "state = 2")
}
func darwinStateIndicatesDisabled(raw string) bool {
lower := strings.ToLower(raw)
return strings.Contains(lower, "disabled") || strings.Contains(lower, "state = 0")
}
func darwinTimeSync(ctx context.Context) Result {
out := RunCommand(ctx, "systemsetup", "-getusingnetworktime")
if out.Err != nil || needsAdmin(out.Stdout) {
return unknown(
map[string]any{
"raw": out.Stdout,
"error": errString(out.Err),
},
)
}
on := strings.Contains(strings.ToLower(out.Stdout), "on")
ev := map[string]any{"raw": out.Stdout}
if on {
return pass(ev)
}
return fail(ev)
}
func darwinOSVersion(ctx context.Context) Result {
out := RunCommand(ctx, "sw_vers", "-productVersion")
if out.Err != nil || out.Stdout == "" {
return unknown(map[string]any{"error": "sw_vers failed"})
}
build := RunCommand(ctx, "sw_vers", "-buildVersion")
ev := map[string]any{
"product_version": out.Stdout,
"build_version": build.Stdout,
}
return pass(ev)
}
func darwinAutoUpdate(ctx context.Context) Result {
primary := RunCommand(
ctx,
"defaults",
"read",
"/Library/Preferences/com.apple.SoftwareUpdate",
"AutomaticCheckEnabled",
)
if primary.Err == nil {
ev := map[string]any{
"backend": "defaults",
"automatic_check_enabled": primary.Stdout,
}
if strings.TrimSpace(primary.Stdout) == "1" {
return pass(ev)
}
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
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 unknown(ev)
}
func darwinPasswordPolicy(ctx context.Context) Result {
out := RunCommand(ctx, "pwpolicy", "-getaccountpolicies")
if out.Err != nil {
return unknown(
map[string]any{
"error": out.Err.Error(),
"stderr": out.Stderr,
},
)
}
lower := strings.ToLower(out.Stdout)
ev := map[string]any{"raw_truncated": truncate(out.Stdout, 400)}
if strings.Contains(lower, "no account policies") || lower == "" {
return fail(ev)
}
return pass(ev)
}
func darwinRemoteLogin(ctx context.Context) Result {
out := RunCommand(ctx, "systemsetup", "-getremotelogin")
if out.Err != nil || needsAdmin(out.Stdout) {
return unknown(
map[string]any{
"raw": out.Stdout,
"error": errString(out.Err),
},
)
}
off := strings.Contains(strings.ToLower(out.Stdout), "off")
ev := map[string]any{"raw": out.Stdout}
if off {
return pass(ev)
}
return fail(ev)
}
func darwinMalwareProtection(ctx context.Context) Result {
candidates := []string{
"/Library/Apple/System/Library/CoreServices/XProtect.bundle/Contents/Resources/XProtect.meta.plist",
"/System/Library/CoreServices/XProtect.bundle/Contents/Resources/XProtect.meta.plist",
}
for _, path := range candidates {
if _, err := os.Stat(path); err != nil {
continue
}
ev := map[string]any{"engine": "XProtect", "plist": path}
version := RunCommand(
ctx,
"defaults",
"read",
strings.TrimSuffix(path, ".plist"),
"Version",
)
if version.Err == nil {
ev["version"] = version.Stdout
}
return pass(ev)
}
return fail(
map[string]any{
"engine": "XProtect",
"note": "XProtect.meta.plist not found in expected locations",
},
)
}
// needsAdmin checks systemsetup's stdout for privilege errors.
func needsAdmin(stdout string) bool {
return strings.Contains(strings.ToLower(stdout), "administrator access")
}