From 25782ea1737976bd75e2c296de8bdcc205fe0c37 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Fri, 27 Mar 2026 17:25:08 -0700 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Add=20macOS=20FileVault,=20Gate?= =?UTF-8?q?keeper,=20and=20SIP=20resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three new macOS security resources to the OS provider for compliance checking of core macOS security controls: - macos.filevault: FileVault full-disk encryption status via fdesetup - macos.gatekeeper: Gatekeeper app execution policy via spctl - macos.sip: System Integrity Protection status via csrutil Co-Authored-By: Claude Opus 4.6 (1M context) --- providers/os/resources/macos_filevault.go | 47 +++++ providers/os/resources/macos_gatekeeper.go | 39 ++++ providers/os/resources/macos_sip.go | 44 ++++ providers/os/resources/os.lr | 24 +++ providers/os/resources/os.lr.go | 228 +++++++++++++++++++++ providers/os/resources/os.lr.versions | 9 + 6 files changed, 391 insertions(+) create mode 100644 providers/os/resources/macos_filevault.go create mode 100644 providers/os/resources/macos_gatekeeper.go create mode 100644 providers/os/resources/macos_sip.go diff --git a/providers/os/resources/macos_filevault.go b/providers/os/resources/macos_filevault.go new file mode 100644 index 0000000000..c370c1fc09 --- /dev/null +++ b/providers/os/resources/macos_filevault.go @@ -0,0 +1,47 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package resources + +import ( + "io" + "strings" + + "go.mondoo.com/mql/v13/providers/os/connection/shared" +) + +func (m *mqlMacosFilevault) status() (string, error) { + conn := m.MqlRuntime.Connection.(shared.Connection) + + cmd, err := conn.RunCommand("fdesetup status") + if err != nil { + return "", err + } + + data, err := io.ReadAll(cmd.Stdout) + if err != nil { + return "", err + } + + // 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(string(data)) + + // Return the first line which contains the primary status + lines := strings.SplitN(output, "\n", 2) + return strings.TrimSpace(lines[0]), nil +} + +func (m *mqlMacosFilevault) enabled() (bool, error) { + status, err := m.status() + if err != nil { + return false, err + } + + // FileVault is considered enabled if it's on or encryption is in progress + return strings.Contains(status, "FileVault is On") || + strings.Contains(status, "Encryption in progress"), nil +} diff --git a/providers/os/resources/macos_gatekeeper.go b/providers/os/resources/macos_gatekeeper.go new file mode 100644 index 0000000000..3e257ae0ae --- /dev/null +++ b/providers/os/resources/macos_gatekeeper.go @@ -0,0 +1,39 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package resources + +import ( + "io" + "strings" + + "go.mondoo.com/mql/v13/providers/os/connection/shared" +) + +func (m *mqlMacosGatekeeper) status() (string, error) { + conn := m.MqlRuntime.Connection.(shared.Connection) + + cmd, err := conn.RunCommand("spctl --status") + if err != nil { + return "", err + } + + data, err := io.ReadAll(cmd.Stdout) + if err != nil { + return "", err + } + + // spctl --status outputs: + // "assessments enabled" (Gatekeeper on) + // "assessments disabled" (Gatekeeper off) + return strings.TrimSpace(string(data)), nil +} + +func (m *mqlMacosGatekeeper) enabled() (bool, error) { + status, err := m.status() + if err != nil { + return false, err + } + + return strings.Contains(status, "assessments enabled"), nil +} diff --git a/providers/os/resources/macos_sip.go b/providers/os/resources/macos_sip.go new file mode 100644 index 0000000000..9d2543e538 --- /dev/null +++ b/providers/os/resources/macos_sip.go @@ -0,0 +1,44 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package resources + +import ( + "io" + "strings" + + "go.mondoo.com/mql/v13/providers/os/connection/shared" +) + +func (m *mqlMacosSip) status() (string, error) { + conn := m.MqlRuntime.Connection.(shared.Connection) + + cmd, err := conn.RunCommand("csrutil status") + if err != nil { + return "", err + } + + data, err := io.ReadAll(cmd.Stdout) + if err != nil { + return "", err + } + + // 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(string(data)) + + // Return the first line which contains the primary status + lines := strings.SplitN(output, "\n", 2) + return strings.TrimSpace(lines[0]), nil +} + +func (m *mqlMacosSip) enabled() (bool, error) { + status, err := m.status() + if err != nil { + return false, err + } + + return strings.Contains(status, "enabled"), nil +} diff --git a/providers/os/resources/os.lr b/providers/os/resources/os.lr index c93a3fee99..2c86681743 100644 --- a/providers/os/resources/os.lr +++ b/providers/os/resources/os.lr @@ -2263,6 +2263,30 @@ 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 +} + +// macOS Gatekeeper application execution policy +macos.gatekeeper @defaults("enabled") { + // 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") { + // Whether System Integrity Protection is enabled + enabled() bool + // SIP status message + status() string +} + // macOS Time Machine macos.timemachine { // macOS Time Machine preferences diff --git a/providers/os/resources/os.lr.go b/providers/os/resources/os.lr.go index ed5271be87..e34fe81c1d 100644 --- a/providers/os/resources/os.lr.go +++ b/providers/os/resources/os.lr.go @@ -179,6 +179,9 @@ const ( ResourceMacosAlf string = "macos.alf" ResourceMacosFirewall string = "macos.firewall" ResourceMacosFirewallApp string = "macos.firewall.app" + ResourceMacosFilevault string = "macos.filevault" + ResourceMacosGatekeeper string = "macos.gatekeeper" + ResourceMacosSip string = "macos.sip" ResourceMacosTimemachine string = "macos.timemachine" ResourceMacosSystemsetup string = "macos.systemsetup" ResourceOpenBSMAudit string = "openBSMAudit" @@ -879,6 +882,18 @@ func init() { // to override args, implement: initMacosFirewallApp(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) Create: createMacosFirewallApp, }, + "macos.filevault": { + // to override args, implement: initMacosFilevault(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) + Create: createMacosFilevault, + }, + "macos.gatekeeper": { + // to override args, implement: initMacosGatekeeper(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) + Create: createMacosGatekeeper, + }, + "macos.sip": { + // to override args, implement: initMacosSip(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) + Create: createMacosSip, + }, "macos.timemachine": { // to override args, implement: initMacosTimemachine(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) Create: createMacosTimemachine, @@ -3498,6 +3513,24 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "macos.firewall.app.state": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMacosFirewallApp).GetState()).ToDataRes(types.Int) }, + "macos.filevault.enabled": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMacosFilevault).GetEnabled()).ToDataRes(types.Bool) + }, + "macos.filevault.status": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMacosFilevault).GetStatus()).ToDataRes(types.String) + }, + "macos.gatekeeper.enabled": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMacosGatekeeper).GetEnabled()).ToDataRes(types.Bool) + }, + "macos.gatekeeper.status": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMacosGatekeeper).GetStatus()).ToDataRes(types.String) + }, + "macos.sip.enabled": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMacosSip).GetEnabled()).ToDataRes(types.Bool) + }, + "macos.sip.status": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMacosSip).GetStatus()).ToDataRes(types.String) + }, "macos.timemachine.preferences": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMacosTimemachine).GetPreferences()).ToDataRes(types.Dict) }, @@ -8261,6 +8294,42 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool{ r.(*mqlMacosFirewallApp).State, ok = plugin.RawToTValue[int64](v.Value, v.Error) return }, + "macos.filevault.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosFilevault).__id, ok = v.Value.(string) + return + }, + "macos.filevault.enabled": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosFilevault).Enabled, ok = plugin.RawToTValue[bool](v.Value, v.Error) + return + }, + "macos.filevault.status": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosFilevault).Status, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "macos.gatekeeper.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosGatekeeper).__id, ok = v.Value.(string) + return + }, + "macos.gatekeeper.enabled": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosGatekeeper).Enabled, ok = plugin.RawToTValue[bool](v.Value, v.Error) + return + }, + "macos.gatekeeper.status": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosGatekeeper).Status, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "macos.sip.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosSip).__id, ok = v.Value.(string) + return + }, + "macos.sip.enabled": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosSip).Enabled, ok = plugin.RawToTValue[bool](v.Value, v.Error) + return + }, + "macos.sip.status": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosSip).Status, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, "macos.timemachine.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlMacosTimemachine).__id, ok = v.Value.(string) return @@ -22834,6 +22903,165 @@ func (c *mqlMacosFirewallApp) GetState() *plugin.TValue[int64] { return &c.State } +// mqlMacosFilevault for the macos.filevault resource +type mqlMacosFilevault struct { + MqlRuntime *plugin.Runtime + __id string + // optional: if you define mqlMacosFilevaultInternal it will be used here + Enabled plugin.TValue[bool] + Status plugin.TValue[string] +} + +// createMacosFilevault creates a new instance of this resource +func createMacosFilevault(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { + res := &mqlMacosFilevault{ + MqlRuntime: runtime, + } + + err := SetAllData(res, args) + if err != nil { + return res, err + } + + // to override __id implement: id() (string, error) + + if runtime.HasRecording { + args, err = runtime.ResourceFromRecording("macos.filevault", res.__id) + if err != nil || args == nil { + return res, err + } + return res, SetAllData(res, args) + } + + return res, nil +} + +func (c *mqlMacosFilevault) MqlName() string { + return "macos.filevault" +} + +func (c *mqlMacosFilevault) MqlID() string { + return c.__id +} + +func (c *mqlMacosFilevault) GetEnabled() *plugin.TValue[bool] { + return plugin.GetOrCompute[bool](&c.Enabled, func() (bool, error) { + return c.enabled() + }) +} + +func (c *mqlMacosFilevault) GetStatus() *plugin.TValue[string] { + return plugin.GetOrCompute[string](&c.Status, func() (string, error) { + return c.status() + }) +} + +// mqlMacosGatekeeper for the macos.gatekeeper resource +type mqlMacosGatekeeper struct { + MqlRuntime *plugin.Runtime + __id string + // optional: if you define mqlMacosGatekeeperInternal it will be used here + Enabled plugin.TValue[bool] + Status plugin.TValue[string] +} + +// createMacosGatekeeper creates a new instance of this resource +func createMacosGatekeeper(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { + res := &mqlMacosGatekeeper{ + MqlRuntime: runtime, + } + + err := SetAllData(res, args) + if err != nil { + return res, err + } + + // to override __id implement: id() (string, error) + + if runtime.HasRecording { + args, err = runtime.ResourceFromRecording("macos.gatekeeper", res.__id) + if err != nil || args == nil { + return res, err + } + return res, SetAllData(res, args) + } + + return res, nil +} + +func (c *mqlMacosGatekeeper) MqlName() string { + return "macos.gatekeeper" +} + +func (c *mqlMacosGatekeeper) MqlID() string { + return c.__id +} + +func (c *mqlMacosGatekeeper) GetEnabled() *plugin.TValue[bool] { + return plugin.GetOrCompute[bool](&c.Enabled, func() (bool, error) { + return c.enabled() + }) +} + +func (c *mqlMacosGatekeeper) GetStatus() *plugin.TValue[string] { + return plugin.GetOrCompute[string](&c.Status, func() (string, error) { + return c.status() + }) +} + +// mqlMacosSip for the macos.sip resource +type mqlMacosSip struct { + MqlRuntime *plugin.Runtime + __id string + // optional: if you define mqlMacosSipInternal it will be used here + Enabled plugin.TValue[bool] + Status plugin.TValue[string] +} + +// createMacosSip creates a new instance of this resource +func createMacosSip(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { + res := &mqlMacosSip{ + MqlRuntime: runtime, + } + + err := SetAllData(res, args) + if err != nil { + return res, err + } + + // to override __id implement: id() (string, error) + + if runtime.HasRecording { + args, err = runtime.ResourceFromRecording("macos.sip", res.__id) + if err != nil || args == nil { + return res, err + } + return res, SetAllData(res, args) + } + + return res, nil +} + +func (c *mqlMacosSip) MqlName() string { + return "macos.sip" +} + +func (c *mqlMacosSip) MqlID() string { + return c.__id +} + +func (c *mqlMacosSip) GetEnabled() *plugin.TValue[bool] { + return plugin.GetOrCompute[bool](&c.Enabled, func() (bool, error) { + return c.enabled() + }) +} + +func (c *mqlMacosSip) GetStatus() *plugin.TValue[string] { + return plugin.GetOrCompute[string](&c.Status, func() (string, error) { + return c.status() + }) +} + // mqlMacosTimemachine for the macos.timemachine resource type mqlMacosTimemachine struct { MqlRuntime *plugin.Runtime diff --git a/providers/os/resources/os.lr.versions b/providers/os/resources/os.lr.versions index b601b73c77..a8ddd51d2f 100644 --- a/providers/os/resources/os.lr.versions +++ b/providers/os/resources/os.lr.versions @@ -524,6 +524,9 @@ macos.alf.loggingOption 9.0.1 macos.alf.stealthEnabled 9.0.1 macos.alf.version 9.0.1 macos.computerName 11.4.68 +macos.filevault 13.5.1 +macos.filevault.enabled 13.5.1 +macos.filevault.status 13.5.1 macos.firewall 13.3.1 macos.firewall.allowDownloadSignedApps 13.3.1 macos.firewall.allowSignedApps 13.3.1 @@ -540,6 +543,9 @@ macos.firewall.loggingDetail 13.3.1 macos.firewall.loggingEnabled 13.3.1 macos.firewall.stealthEnabled 13.3.1 macos.firewall.version 13.3.1 +macos.gatekeeper 13.5.1 +macos.gatekeeper.enabled 13.5.1 +macos.gatekeeper.status 13.5.1 macos.globalAccountPolicies 9.0.1 macos.hardware 11.4.68 macos.hardware.activationLockStatus 11.4.68 @@ -554,6 +560,9 @@ macos.hardware.physicalMemory 11.4.68 macos.hardware.platformUUID 11.4.68 macos.hardware.provisioningUDID 11.4.68 macos.hardware.serialNumber 11.4.68 +macos.sip 13.5.1 +macos.sip.enabled 13.5.1 +macos.sip.status 13.5.1 macos.systemExtension 11.2.20 macos.systemExtension.active 11.2.20 macos.systemExtension.bundlePath 11.2.20 From e41ffed737952f2639e76c379e990a5017e47ad1 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Fri, 27 Mar 2026 17:26:27 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A7=B9=20Add=20status=20to=20default?= =?UTF-8?q?=20fields=20for=20gatekeeper=20and=20sip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use Internal struct caching to avoid double command execution - Use command resource pattern instead of conn.RunCommand - Use more specific SIP enabled check (status: enabled) - Add filevault to spelling expect.txt - Add hasPersonalRecoveryKey, hasInstitutionalRecoveryKey, users fields to filevault Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/actions/spelling/expect.txt | 1 + providers/os/resources/macos_filevault.go | 127 ++++++++++++++++++--- providers/os/resources/macos_gatekeeper.go | 41 +++++-- providers/os/resources/macos_sip.go | 48 +++++--- providers/os/resources/os.lr | 10 +- providers/os/resources/os.lr.go | 52 ++++++++- providers/os/resources/os.lr.versions | 3 + 7 files changed, 237 insertions(+), 45 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 60d56422c1..655d945959 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -81,6 +81,7 @@ failback fargate filestore filesz +filevault firefox firestore FLEXGROUP diff --git a/providers/os/resources/macos_filevault.go b/providers/os/resources/macos_filevault.go index c370c1fc09..9f019088e3 100644 --- a/providers/os/resources/macos_filevault.go +++ b/providers/os/resources/macos_filevault.go @@ -4,44 +4,143 @@ package resources import ( - "io" + "errors" "strings" + "sync" - "go.mondoo.com/mql/v13/providers/os/connection/shared" + "go.mondoo.com/mql/v13/llx" ) -func (m *mqlMacosFilevault) status() (string, error) { - conn := m.MqlRuntime.Connection.(shared.Connection) +type mqlMacosFilevaultInternal struct { + lock sync.Mutex + fetched bool + output string +} - cmd, err := conn.RunCommand("fdesetup status") - if err != nil { - return "", err +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 } - data, err := io.ReadAll(cmd.Stdout) + 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(string(data)) - - // Return the first line which contains the primary status + output := strings.TrimSpace(cmd.GetStdout().Data) lines := strings.SplitN(output, "\n", 2) - return strings.TrimSpace(lines[0]), nil + + 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.status() + status, err := m.fetchStatus() if err != nil { return false, err } - // FileVault is considered enabled if it's on or encryption is in progress 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 +} diff --git a/providers/os/resources/macos_gatekeeper.go b/providers/os/resources/macos_gatekeeper.go index 3e257ae0ae..abe71e79fd 100644 --- a/providers/os/resources/macos_gatekeeper.go +++ b/providers/os/resources/macos_gatekeeper.go @@ -4,33 +4,54 @@ package resources import ( - "io" + "errors" "strings" + "sync" - "go.mondoo.com/mql/v13/providers/os/connection/shared" + "go.mondoo.com/mql/v13/llx" ) -func (m *mqlMacosGatekeeper) status() (string, error) { - conn := m.MqlRuntime.Connection.(shared.Connection) +type mqlMacosGatekeeperInternal struct { + lock sync.Mutex + fetched bool + output string +} - cmd, err := conn.RunCommand("spctl --status") - if err != nil { - return "", err +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 } - data, err := io.ReadAll(cmd.Stdout) + 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) - return strings.TrimSpace(string(data)), nil + 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.status() + status, err := m.fetchStatus() if err != nil { return false, err } diff --git a/providers/os/resources/macos_sip.go b/providers/os/resources/macos_sip.go index 9d2543e538..6b49e9a75d 100644 --- a/providers/os/resources/macos_sip.go +++ b/providers/os/resources/macos_sip.go @@ -4,41 +4,61 @@ package resources import ( - "io" + "errors" "strings" + "sync" - "go.mondoo.com/mql/v13/providers/os/connection/shared" + "go.mondoo.com/mql/v13/llx" ) -func (m *mqlMacosSip) status() (string, error) { - conn := m.MqlRuntime.Connection.(shared.Connection) +type mqlMacosSipInternal struct { + lock sync.Mutex + fetched bool + output string +} - cmd, err := conn.RunCommand("csrutil status") - if err != nil { - return "", err +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 } - data, err := io.ReadAll(cmd.Stdout) + 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(string(data)) - - // Return the first line which contains the primary status + output := strings.TrimSpace(cmd.GetStdout().Data) lines := strings.SplitN(output, "\n", 2) - return strings.TrimSpace(lines[0]), nil + + 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.status() + status, err := m.fetchStatus() if err != nil { return false, err } - return strings.Contains(status, "enabled"), nil + return strings.Contains(status, "status: enabled"), nil } diff --git a/providers/os/resources/os.lr b/providers/os/resources/os.lr index 2c86681743..859bc02df7 100644 --- a/providers/os/resources/os.lr +++ b/providers/os/resources/os.lr @@ -2269,10 +2269,16 @@ macos.filevault @defaults("enabled status") { 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") { +macos.gatekeeper @defaults("enabled status") { // Whether Gatekeeper assessments are enabled enabled() bool // Gatekeeper assessment status (assessments enabled, assessments disabled) @@ -2280,7 +2286,7 @@ macos.gatekeeper @defaults("enabled") { } // macOS System Integrity Protection (SIP) -macos.sip @defaults("enabled") { +macos.sip @defaults("enabled status") { // Whether System Integrity Protection is enabled enabled() bool // SIP status message diff --git a/providers/os/resources/os.lr.go b/providers/os/resources/os.lr.go index e34fe81c1d..2f94041723 100644 --- a/providers/os/resources/os.lr.go +++ b/providers/os/resources/os.lr.go @@ -3519,6 +3519,15 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "macos.filevault.status": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMacosFilevault).GetStatus()).ToDataRes(types.String) }, + "macos.filevault.hasPersonalRecoveryKey": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMacosFilevault).GetHasPersonalRecoveryKey()).ToDataRes(types.Bool) + }, + "macos.filevault.hasInstitutionalRecoveryKey": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMacosFilevault).GetHasInstitutionalRecoveryKey()).ToDataRes(types.Bool) + }, + "macos.filevault.users": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlMacosFilevault).GetUsers()).ToDataRes(types.Array(types.String)) + }, "macos.gatekeeper.enabled": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlMacosGatekeeper).GetEnabled()).ToDataRes(types.Bool) }, @@ -8306,6 +8315,18 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool{ r.(*mqlMacosFilevault).Status, ok = plugin.RawToTValue[string](v.Value, v.Error) return }, + "macos.filevault.hasPersonalRecoveryKey": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosFilevault).HasPersonalRecoveryKey, ok = plugin.RawToTValue[bool](v.Value, v.Error) + return + }, + "macos.filevault.hasInstitutionalRecoveryKey": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosFilevault).HasInstitutionalRecoveryKey, ok = plugin.RawToTValue[bool](v.Value, v.Error) + return + }, + "macos.filevault.users": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlMacosFilevault).Users, ok = plugin.RawToTValue[[]any](v.Value, v.Error) + return + }, "macos.gatekeeper.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlMacosGatekeeper).__id, ok = v.Value.(string) return @@ -22907,9 +22928,12 @@ func (c *mqlMacosFirewallApp) GetState() *plugin.TValue[int64] { type mqlMacosFilevault struct { MqlRuntime *plugin.Runtime __id string - // optional: if you define mqlMacosFilevaultInternal it will be used here - Enabled plugin.TValue[bool] - Status plugin.TValue[string] + mqlMacosFilevaultInternal + Enabled plugin.TValue[bool] + Status plugin.TValue[string] + HasPersonalRecoveryKey plugin.TValue[bool] + HasInstitutionalRecoveryKey plugin.TValue[bool] + Users plugin.TValue[[]any] } // createMacosFilevault creates a new instance of this resource @@ -22956,11 +22980,29 @@ func (c *mqlMacosFilevault) GetStatus() *plugin.TValue[string] { }) } +func (c *mqlMacosFilevault) GetHasPersonalRecoveryKey() *plugin.TValue[bool] { + return plugin.GetOrCompute[bool](&c.HasPersonalRecoveryKey, func() (bool, error) { + return c.hasPersonalRecoveryKey() + }) +} + +func (c *mqlMacosFilevault) GetHasInstitutionalRecoveryKey() *plugin.TValue[bool] { + return plugin.GetOrCompute[bool](&c.HasInstitutionalRecoveryKey, func() (bool, error) { + return c.hasInstitutionalRecoveryKey() + }) +} + +func (c *mqlMacosFilevault) GetUsers() *plugin.TValue[[]any] { + return plugin.GetOrCompute[[]any](&c.Users, func() ([]any, error) { + return c.users() + }) +} + // mqlMacosGatekeeper for the macos.gatekeeper resource type mqlMacosGatekeeper struct { MqlRuntime *plugin.Runtime __id string - // optional: if you define mqlMacosGatekeeperInternal it will be used here + mqlMacosGatekeeperInternal Enabled plugin.TValue[bool] Status plugin.TValue[string] } @@ -23013,7 +23055,7 @@ func (c *mqlMacosGatekeeper) GetStatus() *plugin.TValue[string] { type mqlMacosSip struct { MqlRuntime *plugin.Runtime __id string - // optional: if you define mqlMacosSipInternal it will be used here + mqlMacosSipInternal Enabled plugin.TValue[bool] Status plugin.TValue[string] } diff --git a/providers/os/resources/os.lr.versions b/providers/os/resources/os.lr.versions index a8ddd51d2f..1299e7a285 100644 --- a/providers/os/resources/os.lr.versions +++ b/providers/os/resources/os.lr.versions @@ -526,7 +526,10 @@ macos.alf.version 9.0.1 macos.computerName 11.4.68 macos.filevault 13.5.1 macos.filevault.enabled 13.5.1 +macos.filevault.hasInstitutionalRecoveryKey 13.5.1 +macos.filevault.hasPersonalRecoveryKey 13.5.1 macos.filevault.status 13.5.1 +macos.filevault.users 13.5.1 macos.firewall 13.3.1 macos.firewall.allowDownloadSignedApps 13.3.1 macos.firewall.allowSignedApps 13.3.1