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).