diff --git a/ee/cis/macos-13/cis-policy-queries.yml b/ee/cis/macos-13/cis-policy-queries.yml
index 8792e1b1fe..4a24070cb6 100644
--- a/ee/cis/macos-13/cis-policy-queries.yml
+++ b/ee/cis/macos-13/cis-policy-queries.yml
@@ -1735,6 +1735,7 @@ spec:
);
purpose: Informational
tags: compliance, CIS, CIS_Level1, CIS5.4
+ contributors: lucasmrod
---
apiVersion: v1
kind: policy
@@ -1765,6 +1766,28 @@ spec:
---
apiVersion: v1
kind: policy
+spec:
+ name: CIS - Ensure the "root" Account Is Disabled (Fleetd Required)
+ platforms: macOS
+ platform: darwin
+ description: |
+ Enabling and using the root account puts the system at risk since any successful exploit or mistake
+ while the root account is in use could have unlimited access privileges within the system.
+ Using the sudo command allows users to perform functions as a root user while limiting and password
+ protecting the access privileges. By default the root account is not enabled on a macOS computer.
+ An administrator can escalate privileges using the sudo command (use -s or -i to get a root shell).
+ resolution: |
+ Automated method:
+ Ask your system administrator to deploy the following script:
+ /usr/bin/sudo /usr/sbin/dsenableroot -d
+ query: |
+ SELECT 1 from dscl WHERE command = 'read' AND path = '/Users/root' AND key = 'AuthenticationAuthority' AND value = '';
+ purpose: Informational
+ tags: compliance, CIS, CIS_Level1, CIS5.6
+ contributors: lucasmrod
+---
+apiVersion: v1
+kind: policy
spec:
name: CIS - Ensure an Administrator Account Cannot Login to Another User's Active and Locked Session (Fleetd Required)
platforms: macOS
@@ -1827,7 +1850,7 @@ spec:
If EFI does not pass the integrity check, you may send a report to Apple. Backing up files and clean installing a
known good Operating System and Firmware is recommended.
query: |
- SELECT 1 FROM firmware_eficheck_integity_check
+ SELECT 1 FROM firmware_eficheck_integrity_check
WHERE chip != 'intel-t1' OR (
chip = 'intel-t1' AND
output LIKE '%Primary allowlist version match found. No changes detected in primary hashes%' AND
diff --git a/ee/cis/macos-13/test/scripts/CIS_5.6.sh b/ee/cis/macos-13/test/scripts/CIS_5.6.sh
new file mode 100644
index 0000000000..1597cee0df
--- /dev/null
+++ b/ee/cis/macos-13/test/scripts/CIS_5.6.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+/usr/bin/sudo /usr/sbin/dsenableroot -d
diff --git a/orbit/changes/9260-add-dscl-table-cis-5.6 b/orbit/changes/9260-add-dscl-table-cis-5.6
new file mode 100644
index 0000000000..6289c7cfcd
--- /dev/null
+++ b/orbit/changes/9260-add-dscl-table-cis-5.6
@@ -0,0 +1 @@
+* Add `dscl` table to Orbit for CIS check 5.6 on macOS.
diff --git a/orbit/changes/9260-add-firmware_eficheck_integity_check-table b/orbit/changes/9260-add-firmware_eficheck_integity_check-table
index 02f9ba4746..7fc8f09a42 100644
--- a/orbit/changes/9260-add-firmware_eficheck_integity_check-table
+++ b/orbit/changes/9260-add-firmware_eficheck_integity_check-table
@@ -1 +1 @@
-* Add `firmware_eficheck_integity_check` table for macOS CIS 5.9.
+* Add `firmware_eficheck_integrity_check` table for macOS CIS 5.9.
diff --git a/orbit/pkg/table/dscl/dscl_darwin.go b/orbit/pkg/table/dscl/dscl_darwin.go
new file mode 100644
index 0000000000..629cc83572
--- /dev/null
+++ b/orbit/pkg/table/dscl/dscl_darwin.go
@@ -0,0 +1,113 @@
+//go:build darwin
+// +build darwin
+
+// Package dscl allows querying dscl read commands on the local domain.
+package dscl
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os/exec"
+ "regexp"
+ "strings"
+
+ "github.com/osquery/osquery-go/plugin/table"
+)
+
+// Columns is the schema of the table.
+func Columns() []table.ColumnDefinition {
+ return []table.ColumnDefinition{
+ table.TextColumn("command"), // required (currently only read is supported)
+ table.TextColumn("path"), // required
+ table.TextColumn("key"), // required (could be relaxed in the future)
+ table.TextColumn("value"),
+ }
+}
+
+// Generate is called to return the results for the table at query time.
+//
+// Constraints for generating can be retrieved from the queryContext.
+func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
+ supportedCommands := []string{"read"}
+
+ getArgumentOpEqual := func(argName string) string {
+ argValue := ""
+ if constraints, ok := queryContext.Constraints[argName]; ok {
+ for _, constraint := range constraints.Constraints {
+ if constraint.Operator == table.OperatorEquals {
+ argValue = constraint.Expression
+ }
+ }
+ }
+ return argValue
+ }
+
+ command := getArgumentOpEqual("command")
+ if command == "" {
+ return nil, fmt.Errorf("missing command argument, supported commands: %+v", supportedCommands)
+ }
+ supported := false
+ for _, supportedCommand := range supportedCommands {
+ if supportedCommand == command {
+ supported = true
+ break
+ }
+ }
+ if !supported {
+ return nil, fmt.Errorf("unsupported command: %s, supported commands: %+v", command, supportedCommands)
+ }
+
+ path := getArgumentOpEqual("path")
+ if path == "" {
+ return nil, errors.New("missing path argument")
+ }
+
+ key := getArgumentOpEqual("key")
+ if key == "" {
+ // In the future we can allow this to be empty and return all key/values of a path.
+ return nil, errors.New("missing key argument")
+ }
+
+ cmd := exec.Command("dscl", ".", "-"+command, path, key)
+ out, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("command failed: %w", err)
+ }
+
+ value, err := parseDSCLReadOutput(out)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse dscl value: %w", err)
+ }
+
+ m := []map[string]string{{
+ "command": command,
+ "path": path,
+ "key": key,
+ "value": "",
+ }}
+
+ if value != nil {
+ m[0]["value"] = *value
+ }
+
+ return m, nil
+}
+
+func parseDSCLReadOutput(out []byte) (*string, error) {
+ regex := regexp.MustCompile(`(\S):[ \n]([\S\t\f\r\n ]+)`)
+
+ outs := string(out)
+ if strings.TrimSpace(outs) == "" || strings.HasPrefix(outs, "No such key: ") {
+ return nil, nil
+ }
+ matches := regex.FindSubmatch(out)
+ if matches == nil {
+ return nil, fmt.Errorf("unexpected entry: %q", outs)
+ }
+ value := string(matches[2])
+ if value[0] == ' ' {
+ value = value[1:]
+ }
+ return &value, nil
+}
diff --git a/orbit/pkg/table/dscl/dscl_darwin_test.go b/orbit/pkg/table/dscl/dscl_darwin_test.go
new file mode 100644
index 0000000000..11b8ba9581
--- /dev/null
+++ b/orbit/pkg/table/dscl/dscl_darwin_test.go
@@ -0,0 +1,97 @@
+//go:build darwin
+// +build darwin
+
+package dscl
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseDSCLOutput(t *testing.T) {
+ // NOTE(lucas): I've seen the following behavior when running the command as non-root.
+ const noKeySample = `No such key: Foobar`
+ value, err := parseDSCLReadOutput([]byte(noKeySample))
+ require.NoError(t, err)
+ require.Nil(t, value)
+
+ // NOTE(lucas): I've seen the following behavior when running the command as root.
+ const noKeySample2 = ``
+ value, err = parseDSCLReadOutput([]byte(noKeySample2))
+ require.NoError(t, err)
+ require.Nil(t, value)
+
+ const keySample0 = `PrimaryGroupID: 20`
+ value, err = parseDSCLReadOutput([]byte(keySample0))
+ require.NoError(t, err)
+ require.NotNil(t, value)
+ require.Equal(t, "20", *value)
+
+ const keySample1 = `Picture:
+ /Library/User Pictures/Animals/Penguin.tif`
+ value, err = parseDSCLReadOutput([]byte(keySample1))
+ require.NoError(t, err)
+ require.NotNil(t, value)
+ require.Equal(t, "/Library/User Pictures/Animals/Penguin.tif", *value)
+
+ const keySample2 = `RecordName: foo com.apple.idms.appleid.prd.0A771AC1-B614-4A18-9FA5-0ADFA8EED4BC`
+ value, err = parseDSCLReadOutput([]byte(keySample2))
+ require.NoError(t, err)
+ require.NotNil(t, value)
+ require.Equal(t, "foo com.apple.idms.appleid.prd.0A771AC1-B614-4A18-9FA5-0ADFA8EED4BC", *value)
+
+ const keySample3 = `dsAttrTypeNative:accountPolicyData:
+
+
+
+
+ creationTime
+ 1634811726.5620289
+ failedLoginCount
+ 0
+ failedLoginTimestamp
+ 0
+ passwordLastSetTime
+ 1636975330.6275649
+
+`
+ value, err = parseDSCLReadOutput([]byte(keySample3))
+ require.NoError(t, err)
+ require.NotNil(t, value)
+ require.Equal(t, `
+
+
+
+ creationTime
+ 1634811726.5620289
+ failedLoginCount
+ 0
+ failedLoginTimestamp
+ 0
+ passwordLastSetTime
+ 1636975330.6275649
+
+`, *value)
+
+ const keySample4 = `RecordType: dsRecTypeStandard:Users`
+ value, err = parseDSCLReadOutput([]byte(keySample4))
+ require.NoError(t, err)
+ require.NotNil(t, value)
+ require.Equal(t, "dsRecTypeStandard:Users", *value)
+
+ const keySample5 = `RecordName:
+ root
+ BUILTIN\Local System`
+ value, err = parseDSCLReadOutput([]byte(keySample5))
+ require.NoError(t, err)
+ require.NotNil(t, value)
+ require.Equal(t, `root
+ BUILTIN\Local System`, *value)
+
+ const keySample6 = `NFSHomeDirectory: /var/root /private/var/root`
+ value, err = parseDSCLReadOutput([]byte(keySample6))
+ require.NoError(t, err)
+ require.NotNil(t, value)
+ require.Equal(t, `/var/root /private/var/root`, *value)
+}
diff --git a/orbit/pkg/table/extension_darwin.go b/orbit/pkg/table/extension_darwin.go
index 6d019876ff..78bed5b3d7 100644
--- a/orbit/pkg/table/extension_darwin.go
+++ b/orbit/pkg/table/extension_darwin.go
@@ -5,7 +5,8 @@ package table
import (
"github.com/fleetdm/fleet/v4/orbit/pkg/table/authdb"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/csrutil_info"
- firmware_eficheck_integity_check "github.com/fleetdm/fleet/v4/orbit/pkg/table/firmware_eficheck_integrity_check"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/table/dscl"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/table/firmware_eficheck_integrity_check"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/nvram_info"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/pmset"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/privaterelay"
@@ -32,7 +33,8 @@ func platformTables() []osquery.OsqueryPlugin {
table.NewPlugin("authdb", authdb.Columns(), authdb.Generate),
table.NewPlugin("pmset", pmset.Columns(), pmset.Generate),
table.NewPlugin("sudo_info", sudo_info.Columns(), sudo_info.Generate),
- table.NewPlugin("firmware_eficheck_integity_check", firmware_eficheck_integity_check.Columns(), firmware_eficheck_integity_check.Generate),
+ table.NewPlugin("firmware_eficheck_integrity_check", firmware_eficheck_integrity_check.Columns(), firmware_eficheck_integrity_check.Generate),
+ table.NewPlugin("dscl", dscl.Columns(), dscl.Generate),
// Macadmins extension tables
table.NewPlugin("filevault_users", filevaultusers.FileVaultUsersColumns(), filevaultusers.FileVaultUsersGenerate),
diff --git a/orbit/pkg/table/firmware_eficheck_integrity_check/firmware_eficheck_integrity_check_darwin.go b/orbit/pkg/table/firmware_eficheck_integrity_check/firmware_eficheck_integrity_check_darwin.go
index 8be8a72ae5..60d5703eea 100644
--- a/orbit/pkg/table/firmware_eficheck_integrity_check/firmware_eficheck_integrity_check_darwin.go
+++ b/orbit/pkg/table/firmware_eficheck_integrity_check/firmware_eficheck_integrity_check_darwin.go
@@ -3,7 +3,7 @@
// Package firmware_integrity_check implements a table
// to perform an integrity check for Legacy EFI.
-package firmware_eficheck_integity_check
+package firmware_eficheck_integrity_check
import (
"context"
diff --git a/schema/tables/dscl.yml b/schema/tables/dscl.yml
new file mode 100644
index 0000000000..6d378466af
--- /dev/null
+++ b/schema/tables/dscl.yml
@@ -0,0 +1,24 @@
+name: dscl
+platforms:
+ - darwin
+description: Returns the output of the `dscl . -read` command (local domain).
+columns:
+ - name: command
+ type: text
+ required: true
+ description: The dscl command to execute, only "read" is currently supported.
+ - name: path
+ type: text
+ required: true
+ description: The path to use in the read command.
+ - name: key
+ type: text
+ required: true
+ description: The key to query on the read command and path.
+ - name: value
+ type: text
+ required: false
+ description: The value of the read path and key. The value is the empty string if the key doesn't exist.
+notes: >-
+ - This table is not a core osquery table. It is included as part of Fleetd, the osquery manager from Fleet.
+evented: false
diff --git a/schema/tables/firmware_eficheck_integity_check.yml b/schema/tables/firmware_eficheck_integrity_check.yml
similarity index 94%
rename from schema/tables/firmware_eficheck_integity_check.yml
rename to schema/tables/firmware_eficheck_integrity_check.yml
index 8c1a1dcdd6..b98c4a7813 100644
--- a/schema/tables/firmware_eficheck_integity_check.yml
+++ b/schema/tables/firmware_eficheck_integrity_check.yml
@@ -1,4 +1,4 @@
-name: firmware_eficheck_integity_check
+name: firmware_eficheck_integrity_check
platforms:
- darwin
description: Performs eficheck's integrity check on macOS Intel T1 chips (CIS 5.9).