Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ failback
fargate
filestore
filesz
filevault
firefox
firestore
FLEXGROUP
Expand Down
146 changes: 146 additions & 0 deletions providers/os/resources/macos_filevault.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package resources

import (
"errors"
"strings"
"sync"

"go.mondoo.com/mql/v13/llx"
)

type mqlMacosFilevaultInternal struct {
lock sync.Mutex
fetched bool
output string
}

func (m *mqlMacosFilevault) fetchStatus() (string, error) {
if m.fetched {
return m.output, nil
}
m.lock.Lock()
defer m.lock.Unlock()
if m.fetched {
return m.output, nil
}

res, err := NewResource(m.MqlRuntime, "command", map[string]*llx.RawData{
"command": llx.StringData("fdesetup status"),
})
if err != nil {
return "", err
}
cmd := res.(*mqlCommand)
if exit := cmd.GetExitcode(); exit.Data != 0 {
return "", errors.New("fdesetup status failed: " + cmd.GetStderr().Data)
}

// fdesetup status outputs lines like:
// "FileVault is On."
// "FileVault is Off."
// "Encryption in progress: Percent completed = 50.0"
// "Decryption in progress: Percent completed = 50.0"
output := strings.TrimSpace(cmd.GetStdout().Data)
lines := strings.SplitN(output, "\n", 2)

m.output = strings.TrimSpace(lines[0])
m.fetched = true
return m.output, nil
}

func (m *mqlMacosFilevault) status() (string, error) {
return m.fetchStatus()
}

func (m *mqlMacosFilevault) enabled() (bool, error) {
status, err := m.fetchStatus()
if err != nil {
return false, err
}

return strings.Contains(status, "FileVault is On") ||
strings.Contains(status, "Encryption in progress"), nil
}

func (m *mqlMacosFilevault) runFdesetup(subcmd string) (string, error) {
res, err := NewResource(m.MqlRuntime, "command", map[string]*llx.RawData{
"command": llx.StringData("fdesetup " + subcmd),
})
if err != nil {
return "", err
}
cmd := res.(*mqlCommand)
if exit := cmd.GetExitcode(); exit.Data != 0 {
return "", errors.New("fdesetup " + subcmd + " failed: " + cmd.GetStderr().Data)
}
return strings.TrimSpace(cmd.GetStdout().Data), nil
}

func (m *mqlMacosFilevault) hasPersonalRecoveryKey() (bool, error) {
enabled, err := m.enabled()
if err != nil {
return false, err
}
if !enabled {
return false, nil
}
// fdesetup haspersonalrecoverykey outputs "true" or "false"
output, err := m.runFdesetup("haspersonalrecoverykey")
if err != nil {
return false, err
}
return output == "true", nil
}

func (m *mqlMacosFilevault) hasInstitutionalRecoveryKey() (bool, error) {
enabled, err := m.enabled()
if err != nil {
return false, err
}
if !enabled {
return false, nil
}
// fdesetup hasinstitutionalrecoverykey outputs "true" or "false"
output, err := m.runFdesetup("hasinstitutionalrecoverykey")
if err != nil {
return false, err
}
return output == "true", nil
}

func (m *mqlMacosFilevault) users() ([]any, error) {
enabled, err := m.enabled()
if err != nil {
return nil, err
}
if !enabled {
return []any{}, nil
}
// fdesetup list outputs lines like:
// "user1,85632A00-1234-5678-ABCD-123456789ABC"
// "user2,95632A00-1234-5678-ABCD-123456789ABC"
output, err := m.runFdesetup("list")
if err != nil {
return nil, err
}

if output == "" {
return []any{}, nil
}

var users []any
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Each line is "username,UUID" — extract the username
parts := strings.SplitN(line, ",", 2)
users = append(users, parts[0])
}

return users, nil
}
60 changes: 60 additions & 0 deletions providers/os/resources/macos_gatekeeper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package resources

import (
"errors"
"strings"
"sync"

"go.mondoo.com/mql/v13/llx"
)

type mqlMacosGatekeeperInternal struct {
lock sync.Mutex
fetched bool
output string
}

func (m *mqlMacosGatekeeper) fetchStatus() (string, error) {
if m.fetched {
return m.output, nil
}
m.lock.Lock()
defer m.lock.Unlock()
if m.fetched {
return m.output, nil
}

res, err := NewResource(m.MqlRuntime, "command", map[string]*llx.RawData{
"command": llx.StringData("spctl --status"),
})
if err != nil {
return "", err
}
cmd := res.(*mqlCommand)
if exit := cmd.GetExitcode(); exit.Data != 0 {
return "", errors.New("spctl --status failed: " + cmd.GetStderr().Data)
}

// spctl --status outputs:
// "assessments enabled" (Gatekeeper on)
// "assessments disabled" (Gatekeeper off)
m.output = strings.TrimSpace(cmd.GetStdout().Data)
m.fetched = true
return m.output, nil
}

func (m *mqlMacosGatekeeper) status() (string, error) {
return m.fetchStatus()
}

func (m *mqlMacosGatekeeper) enabled() (bool, error) {
status, err := m.fetchStatus()
if err != nil {
return false, err
}

return strings.Contains(status, "assessments enabled"), nil
}
64 changes: 64 additions & 0 deletions providers/os/resources/macos_sip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package resources

import (
"errors"
"strings"
"sync"

"go.mondoo.com/mql/v13/llx"
)

type mqlMacosSipInternal struct {
lock sync.Mutex
fetched bool
output string
}

func (m *mqlMacosSip) fetchStatus() (string, error) {
if m.fetched {
return m.output, nil
}
m.lock.Lock()
defer m.lock.Unlock()
if m.fetched {
return m.output, nil
}

res, err := NewResource(m.MqlRuntime, "command", map[string]*llx.RawData{
"command": llx.StringData("csrutil status"),
})
if err != nil {
return "", err
}
cmd := res.(*mqlCommand)
if exit := cmd.GetExitcode(); exit.Data != 0 {
return "", errors.New("csrutil status failed: " + cmd.GetStderr().Data)
}

// csrutil status outputs:
// "System Integrity Protection status: enabled."
// "System Integrity Protection status: disabled."
// May also include individual configuration flags on separate lines
output := strings.TrimSpace(cmd.GetStdout().Data)
lines := strings.SplitN(output, "\n", 2)

m.output = strings.TrimSpace(lines[0])
m.fetched = true
return m.output, nil
}

func (m *mqlMacosSip) status() (string, error) {
return m.fetchStatus()
}

func (m *mqlMacosSip) enabled() (bool, error) {
status, err := m.fetchStatus()
if err != nil {
return false, err
}

return strings.Contains(status, "status: enabled"), nil
}
30 changes: 30 additions & 0 deletions providers/os/resources/os.lr
Original file line number Diff line number Diff line change
Expand Up @@ -2263,6 +2263,36 @@ private macos.firewall.app @defaults("name state") {
state int
}

// macOS FileVault full-disk encryption
macos.filevault @defaults("enabled status") {
// Whether FileVault is enabled
enabled() bool
// FileVault status (On, Off, Encryption in progress, Decryption in progress)
status() string
// Whether a personal recovery key exists
hasPersonalRecoveryKey() bool
// Whether an institutional recovery key exists
hasInstitutionalRecoveryKey() bool
// Users that can unlock the FileVault encrypted drive
users() []string
}

// macOS Gatekeeper application execution policy
macos.gatekeeper @defaults("enabled status") {
// Whether Gatekeeper assessments are enabled
enabled() bool
// Gatekeeper assessment status (assessments enabled, assessments disabled)
status() string
}

// macOS System Integrity Protection (SIP)
macos.sip @defaults("enabled status") {
// Whether System Integrity Protection is enabled
enabled() bool
// SIP status message
status() string
}

// macOS Time Machine
macos.timemachine {
// macOS Time Machine preferences
Expand Down
Loading
Loading