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 new file mode 100644 index 0000000000..9f019088e3 --- /dev/null +++ b/providers/os/resources/macos_filevault.go @@ -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 +} diff --git a/providers/os/resources/macos_gatekeeper.go b/providers/os/resources/macos_gatekeeper.go new file mode 100644 index 0000000000..abe71e79fd --- /dev/null +++ b/providers/os/resources/macos_gatekeeper.go @@ -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 +} diff --git a/providers/os/resources/macos_sip.go b/providers/os/resources/macos_sip.go new file mode 100644 index 0000000000..6b49e9a75d --- /dev/null +++ b/providers/os/resources/macos_sip.go @@ -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 +} diff --git a/providers/os/resources/os.lr b/providers/os/resources/os.lr index c93a3fee99..859bc02df7 100644 --- a/providers/os/resources/os.lr +++ b/providers/os/resources/os.lr @@ -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 diff --git a/providers/os/resources/os.lr.go b/providers/os/resources/os.lr.go index ed5271be87..2f94041723 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,33 @@ 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.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) + }, + "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 +8303,54 @@ 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.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 + }, + "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 +22924,186 @@ func (c *mqlMacosFirewallApp) GetState() *plugin.TValue[int64] { return &c.State } +// mqlMacosFilevault for the macos.filevault resource +type mqlMacosFilevault struct { + MqlRuntime *plugin.Runtime + __id 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 +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() + }) +} + +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 + mqlMacosGatekeeperInternal + 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 + mqlMacosSipInternal + 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..1299e7a285 100644 --- a/providers/os/resources/os.lr.versions +++ b/providers/os/resources/os.lr.versions @@ -524,6 +524,12 @@ 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.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 @@ -540,6 +546,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 +563,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