Add macOS CIS 5.3.1 (#10397)

This adds a new check about whether all APFS volumes are encrypted. I
needed to add a new table, and I took that opportunity to add another so
that osquery has all information from `diskutil apfs list -plist`.

Note that it is somewhat unclear whether to use the `encryption` or
`filevault` field in the query. FileVault is about whether the volume is
encrypted with a password and Encryption is about whether it is
encrypted at all, since all modern macs have hardware-backed disk
encryption.
This commit is contained in:
Artemis Tosini
2023-03-10 12:29:14 -05:00
committed by GitHub
parent b3cd710286
commit af4c3f7061
7 changed files with 742 additions and 0 deletions
+34
View File
@@ -2006,6 +2006,40 @@ spec:
---
apiVersion: v1
kind: policy
spec:
name: CIS - Ensure all user storage APFS volumes are encrypted (Fleetd Required)
platforms: macOS
platform: darwin
description: |
Apple developed a new file system which was first made available in 10.12 and then became the
default in 10.13. The file system is optimized for Flash and Solid-State storage and encryption.
https://en.wikipedia.org/wiki/Apple_File_System macOS computers generally have several volumes
created as part of APFS formatting, including Preboot, Recovery and Virtual Memory (VM), as well
as traditional user disks.
All APFS volumes that do not have specific roles and do not require encryption should be
encrypted. "Role" disks include Preboot, Recovery and VM. User disks are labelled with "(No
specific role)" by default.
resolution: |
Manual method:
Use Disk Utility to erase a user disk and format as APFS (Encrypted).
Note: APFS Encrypted disks will be described as "FileVault" whether they are the boot volume or not in the ap list.
query: |
SELECT 1 WHERE NOT EXISTS (
SELECT 1 FROM apfs_volumes WHERE
role != "VM" AND
role != "Update" AND
role != "Recovery" AND
role != "Preboot" AND
role != "xART" AND
role != "Hardware" AND
filevault != 1
);
purpose: Informational
tags: compliance, CIS, CIS_Level1, CIS-macos-13-5.3.1
contributors: artemist-work
---
apiVersion: v1
kind: policy
spec:
name: CIS - Ensure the Sudo Timeout Period Is Set to Zero (Fleetd Required)
platforms: macOS
@@ -0,0 +1,189 @@
package apfs
import (
"context"
"fmt"
"os/exec"
"strconv"
"github.com/osquery/osquery-go/plugin/table"
"howett.net/plist"
)
type CmdResult struct {
Containers []Container
}
type Container struct {
APFSContainerUUID string
CapacityCeiling int64
CapacityFree int64
ContainerReference string
DesignatedPhysicalStore string
Fusion bool
PhysicalStores []PhysicalStore
Volumes []Volume
}
type PhysicalStore struct {
DeviceIdentifier string
DiskUUID string
Size int64
}
type Volume struct {
APFSVolumeUUID string
CapacityInUse int64
CapacityQuota int64
CapacityReserve int64
CryptoMigrationOn bool
DeviceIdentifier string
Encryption bool
FileVault bool
Locked bool
Name string
Roles []string
}
// Columns is the schema of the table.
func VolumesColumns() []table.ColumnDefinition {
return []table.ColumnDefinition{
table.TextColumn("container_uuid"),
table.TextColumn("container_designated_physical_store"),
table.TextColumn("container_reference"),
table.IntegerColumn("container_fusion"),
table.BigIntColumn("container_capacity_ceiling"),
table.BigIntColumn("container_capacity_free"),
table.TextColumn("uuid"),
table.TextColumn("device_identifier"),
table.TextColumn("name"),
table.TextColumn("role"),
table.BigIntColumn("capacity_in_use"),
table.BigIntColumn("capacity_quota"),
table.BigIntColumn("capacity_reserve"),
table.BigIntColumn("crypto_migration_on"),
table.BigIntColumn("encryption"),
table.IntegerColumn("filevault"),
table.IntegerColumn("locked"),
}
}
// Generate is called to return the results for the table at query time.
// Constraints for generating can be retrieved from the queryContext.
func VolumesGenerate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
cmd := exec.Command("/usr/sbin/diskutil", "apfs", "list", "-plist")
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("generate failed: %w", err)
}
rows, err := parseDiskutilVolumes(out)
if err != nil {
return nil, err
}
return rows, nil
}
func parseDiskutilVolumes(out []byte) ([]map[string]string, error) {
var m CmdResult
if _, err := plist.Unmarshal(out, &m); err != nil {
return nil, fmt.Errorf("parse diskutil apfs list -plist output: %w", err)
}
rows := make([]map[string]string, 0)
for _, container := range m.Containers {
for _, volume := range container.Volumes {
role := ""
if len(volume.Roles) > 0 {
role = volume.Roles[0]
}
rows = append(rows, map[string]string{
"container_uuid": container.APFSContainerUUID,
"container_designated_physical_store": container.DesignatedPhysicalStore,
"container_reference": container.ContainerReference,
"container_fusion": convertBool(container.Fusion),
"container_capacity_ceiling": strconv.FormatInt(container.CapacityCeiling, 10),
"container_capacity_free": strconv.FormatInt(container.CapacityFree, 10),
"uuid": volume.APFSVolumeUUID,
"device_identifier": volume.DeviceIdentifier,
"name": volume.Name,
"role": role,
"capacity_in_use": strconv.FormatInt(volume.CapacityInUse, 10),
"capacity_quota": strconv.FormatInt(volume.CapacityQuota, 10),
"capacity_reserve": strconv.FormatInt(volume.CapacityReserve, 10),
"crypto_migration_on": convertBool(volume.CryptoMigrationOn),
"encryption": convertBool(volume.Encryption),
"filevault": convertBool(volume.FileVault),
"locked": convertBool(volume.Locked),
})
}
}
return rows, nil
}
// Columns is the schema of the table.
func PhysicalStoresColumns() []table.ColumnDefinition {
return []table.ColumnDefinition{
table.TextColumn("container_uuid"),
table.TextColumn("container_designated_physical_store"),
table.TextColumn("container_reference"),
table.IntegerColumn("container_fusion"),
table.BigIntColumn("container_capacity_ceiling"),
table.BigIntColumn("container_capacity_free"),
table.TextColumn("uuid"),
table.TextColumn("identifier"),
table.BigIntColumn("size"),
}
}
// Generate is called to return the results for the table at query time.
// Constraints for generating can be retrieved from the queryContext.
func PhysicalStoresGenerate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
cmd := exec.Command("/usr/sbin/diskutil", "apfs", "list", "-plist")
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("generate failed: %w", err)
}
rows, err := parseDiskutilPhysicalStores(out)
if err != nil {
return nil, err
}
return rows, nil
}
func parseDiskutilPhysicalStores(out []byte) ([]map[string]string, error) {
var m CmdResult
if _, err := plist.Unmarshal(out, &m); err != nil {
return nil, err
}
rows := make([]map[string]string, 0)
for _, container := range m.Containers {
for _, physicalStore := range container.PhysicalStores {
rows = append(rows, map[string]string{
"container_uuid": container.APFSContainerUUID,
"container_designated_physical_store": container.DesignatedPhysicalStore,
"container_reference": container.ContainerReference,
"container_fusion": convertBool(container.Fusion),
"container_capacity_ceiling": strconv.FormatInt(container.CapacityCeiling, 10),
"container_capacity_free": strconv.FormatInt(container.CapacityFree, 10),
"uuid": physicalStore.DiskUUID,
"identifier": physicalStore.DeviceIdentifier,
"size": strconv.FormatInt(physicalStore.Size, 10),
})
}
}
return rows, nil
}
func convertBool(b bool) string {
if b {
return "1"
}
return "0"
}
@@ -0,0 +1,210 @@
//go:build darwin
// +build darwin
package apfs
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestParseDiskutilVolumes(t *testing.T) {
const sampleOutput = `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Containers</key>
<array>
<dict>
<key>APFSContainerUUID</key>
<string>57770A0F-0637-44E3-94BE-52D37EAFB88E</string>
<key>CapacityCeiling</key>
<integer>524288000</integer>
<key>CapacityFree</key>
<integer>505962496</integer>
<key>ContainerReference</key>
<string>disk1</string>
<key>DesignatedPhysicalStore</key>
<string>disk0s1</string>
<key>Fusion</key>
<false/>
<key>PhysicalStores</key>
<array>
</array>
<key>Volumes</key>
<array>
<dict>
<key>APFSVolumeUUID</key>
<string>10DC02F1-71D9-4D8C-A666-899BFDFE2058</string>
<key>CapacityInUse</key>
<integer>6475776</integer>
<key>CapacityQuota</key>
<integer>0</integer>
<key>CapacityReserve</key>
<integer>0</integer>
<key>CryptoMigrationOn</key>
<false/>
<key>DeviceIdentifier</key>
<string>disk1s1</string>
<key>Encryption</key>
<false/>
<key>FileVault</key>
<false/>
<key>Locked</key>
<false/>
<key>Name</key>
<string>iSCPreboot</string>
<key>Roles</key>
<array>
<string>Preboot</string>
</array>
</dict>
<dict>
<key>APFSVolumeUUID</key>
<string>AD45A111-EF76-4A09-9D8D-CFB0162952F8</string>
<key>CapacityInUse</key>
<integer>6311936</integer>
<key>CapacityQuota</key>
<integer>0</integer>
<key>CapacityReserve</key>
<integer>0</integer>
<key>CryptoMigrationOn</key>
<false/>
<key>DeviceIdentifier</key>
<string>disk1s2</string>
<key>Encryption</key>
<true/>
<key>FileVault</key>
<true/>
<key>Locked</key>
<false/>
<key>Name</key>
<string>xART</string>
<key>Roles</key>
<array>
<string>xART</string>
</array>
</dict>
</array>
</dict>
</array>
</dict>`
parseResult, err := parseDiskutilVolumes([]byte(sampleOutput))
require.NoError(t, err)
require.Equal(t, []map[string]string{
{
"container_uuid": "57770A0F-0637-44E3-94BE-52D37EAFB88E",
"container_designated_physical_store": "disk0s1",
"container_reference": "disk1",
"container_fusion": "0",
"container_capacity_ceiling": "524288000",
"container_capacity_free": "505962496",
"uuid": "10DC02F1-71D9-4D8C-A666-899BFDFE2058",
"device_identifier": "disk1s1",
"name": "iSCPreboot",
"role": "Preboot",
"capacity_in_use": "6475776",
"capacity_quota": "0",
"capacity_reserve": "0",
"crypto_migration_on": "0",
"encryption": "0",
"filevault": "0",
"locked": "0",
},
{
"container_uuid": "57770A0F-0637-44E3-94BE-52D37EAFB88E",
"container_designated_physical_store": "disk0s1",
"container_reference": "disk1",
"container_fusion": "0",
"container_capacity_ceiling": "524288000",
"container_capacity_free": "505962496",
"uuid": "AD45A111-EF76-4A09-9D8D-CFB0162952F8",
"device_identifier": "disk1s2",
"name": "xART",
"role": "xART",
"capacity_in_use": "6311936",
"capacity_quota": "0",
"capacity_reserve": "0",
"crypto_migration_on": "0",
"encryption": "1",
"filevault": "1",
"locked": "0",
},
}, parseResult)
}
func TestParseDiskutilPhysicalStores(t *testing.T) {
const sampleOutput = `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Containers</key>
<array>
<dict>
<key>APFSContainerUUID</key>
<string>57770A0F-0637-44E3-94BE-52D37EAFB88E</string>
<key>CapacityCeiling</key>
<integer>524288000</integer>
<key>CapacityFree</key>
<integer>505962496</integer>
<key>ContainerReference</key>
<string>disk1</string>
<key>DesignatedPhysicalStore</key>
<string>disk0s1</string>
<key>Fusion</key>
<false/>
<key>PhysicalStores</key>
<array>
<dict>
<key>DeviceIdentifier</key>
<string>disk0s1</string>
<key>DiskUUID</key>
<string>01A35F52-1070-47F4-9F11-ACA37BA87A61</string>
<key>Size</key>
<integer>524288000</integer>
</dict>
<dict>
<key>DeviceIdentifier</key>
<string>disk1s2</string>
<key>DiskUUID</key>
<string>4483B6EA-22CD-448B-B5AB-5D937CD19CB3</string>
<key>Size</key>
<integer>1048576000</integer>
</dict>
</array>
<key>Volumes</key>
<array>
</array>
</dict>
</array>
</dict>`
parseResult, err := parseDiskutilPhysicalStores([]byte(sampleOutput))
require.NoError(t, err)
require.Equal(t, []map[string]string{
{
"container_uuid": "57770A0F-0637-44E3-94BE-52D37EAFB88E",
"container_designated_physical_store": "disk0s1",
"container_reference": "disk1",
"container_fusion": "0",
"container_capacity_ceiling": "524288000",
"container_capacity_free": "505962496",
"uuid": "01A35F52-1070-47F4-9F11-ACA37BA87A61",
"identifier": "disk0s1",
"size": "524288000",
},
{
"container_uuid": "57770A0F-0637-44E3-94BE-52D37EAFB88E",
"container_designated_physical_store": "disk0s1",
"container_reference": "disk1",
"container_fusion": "0",
"container_capacity_ceiling": "524288000",
"container_capacity_free": "505962496",
"uuid": "4483B6EA-22CD-448B-B5AB-5D937CD19CB3",
"identifier": "disk1s2",
"size": "1048576000",
},
}, parseResult)
}
+3
View File
@@ -5,6 +5,7 @@ package table
import (
"github.com/fleetdm/fleet/v4/orbit/pkg/table/authdb"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/csrutil_info"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/diskutil/apfs"
"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"
@@ -35,6 +36,8 @@ func platformTables() []osquery.OsqueryPlugin {
table.NewPlugin("sudo_info", sudo_info.Columns(), sudo_info.Generate),
table.NewPlugin("firmware_eficheck_integrity_check", firmware_eficheck_integrity_check.Columns(), firmware_eficheck_integrity_check.Generate),
table.NewPlugin("dscl", dscl.Columns(), dscl.Generate),
table.NewPlugin("apfs_volumes", apfs.VolumesColumns(), apfs.VolumesGenerate),
table.NewPlugin("apfs_physical_stores", apfs.PhysicalStoresColumns(), apfs.PhysicalStoresGenerate),
// Macadmins extension tables
table.NewPlugin("filevault_users", filevaultusers.FileVaultUsersColumns(), filevaultusers.FileVaultUsersGenerate),
+182
View File
@@ -26635,6 +26635,188 @@
],
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/yum_sources.yml"
},
{
"name": "apfs_physical_stores",
"platforms": [
"darwin"
],
"description": "Information about APFS physical stores from the `diskutil apfs list -plist` command.",
"columns": [
{
"name": "container_uuid",
"type": "text",
"required": false,
"description": "The UUID of the APFS Contianer"
},
{
"name": "container_designated_physical_store",
"type": "text",
"required": false,
"description": "The disk displayed as the backing store of the container. There may be multiple,\nuse `apfs_physical_stores` to see all actual physical stores\n"
},
{
"name": "container_reference",
"type": "text",
"required": false,
"description": "The current reference for the APFS container, e.g. \"disk3\""
},
{
"name": "container_fusion",
"type": "text",
"required": false,
"description": "Whether this container is on a \"fusion drive\" (i.e. SSHD)"
},
{
"name": "container_capacity_ceiling",
"type": "bigint",
"required": false,
"description": "The total amount of space in the container"
},
{
"name": "container_capacity_free",
"type": "bigint",
"required": false,
"description": "The amount of remaining free space in the container"
},
{
"name": "uuid",
"type": "text",
"required": false,
"description": "The UUID of the physical store"
},
{
"name": "identifier",
"type": "text",
"required": false,
"description": "The current identifier of the physical store (e.g. disk1s2)"
},
{
"name": "size",
"type": "bigint",
"required": false,
"description": "The size of the physical store in byptes"
}
],
"notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
"evented": false,
"url": "https://fleetdm.com/tables/apfs_physical_stores",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/apfs_physical_stores.yml"
},
{
"name": "apfs_volumes",
"platforms": [
"darwin"
],
"description": "Information about APFS volumes from the `diskutil apfs list -plist` command.",
"columns": [
{
"name": "container_uuid",
"type": "text",
"required": false,
"description": "The UUID of the APFS Contianer"
},
{
"name": "container_designated_physical_store",
"type": "text",
"required": false,
"description": "The disk displayed as the backing store of the container. There may be multiple,\nuse `apfs_physical_stores` to see all actual physical stores\n"
},
{
"name": "container_reference",
"type": "text",
"required": false,
"description": "The current reference for the APFS container, e.g. \"disk3\""
},
{
"name": "container_fusion",
"type": "text",
"required": false,
"description": "Whether this container is on a \"fusion drive\" (i.e. SSHD)"
},
{
"name": "container_capacity_ceiling",
"type": "bigint",
"required": false,
"description": "The total amount of space in the container"
},
{
"name": "container_capacity_free",
"type": "bigint",
"required": false,
"description": "The amount of remaining free space in the container"
},
{
"name": "uuid",
"type": "text",
"required": false,
"description": "The UUID of the volume"
},
{
"name": "device_identifier",
"type": "text",
"required": false,
"description": "The current identifier of the volume (e.g. disk3s2)"
},
{
"name": "name",
"type": "text",
"required": false,
"description": "The user-selected name of the volume (e.g. \"Macintosh HD\")"
},
{
"name": "role",
"type": "text",
"required": false,
"description": "The first role of the volume. User-created volumes will have no role (this will be empty).\nSystem volumes might have roles like \"Data\", \"Hardware\", etc.\n"
},
{
"name": "capacity_in_use",
"type": "bigint",
"required": false,
"description": "Storage space used by the volume"
},
{
"name": "capacity_quota",
"type": "bigint",
"required": false,
"description": "Storage quota for the volume, or 0 if disabled"
},
{
"name": "capacity_reserve",
"type": "bigint",
"required": false,
"description": "Storage reserved for this volume even if contianer is otherwise full, or 0 if disabled"
},
{
"name": "crypto_migration_on",
"type": "integer",
"required": false,
"description": "Whether the volume is in the process of being encrypted"
},
{
"name": "encryption",
"type": "integer",
"required": false,
"description": "Whether the volume is encrypted, including without requiring a password"
},
{
"name": "filevault",
"type": "integer",
"required": false,
"description": "Whether the volume requires a password to decrypt"
},
{
"name": "locked",
"type": "integer",
"required": false,
"description": "Whether the volume is unreadable because it does not have a key entered"
}
],
"notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).",
"evented": false,
"url": "https://fleetdm.com/tables/apfs_volumes",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/apfs_volumes.yml"
},
{
"name": "authdb",
"platforms": [
+45
View File
@@ -0,0 +1,45 @@
name: apfs_physical_stores
platforms:
- darwin
description: Information about APFS physical stores from the `diskutil apfs list -plist` command.
columns:
- name: container_uuid
type: text
required: false
description: The UUID of the APFS Contianer
- name: container_designated_physical_store
type: text
required: false
description: |
The disk displayed as the backing store of the container. There may be multiple,
use `apfs_physical_stores` to see all actual physical stores
- name: container_reference
type: text
required: false
description: The current reference for the APFS container, e.g. "disk3"
- name: container_fusion
type: text
required: false
description: Whether this container is on a "fusion drive" (i.e. SSHD)
- name: container_capacity_ceiling
type: bigint
required: false
description: The total amount of space in the container
- name: container_capacity_free
type: bigint
required: false
description: The amount of remaining free space in the container
- name: uuid
type: text
required: false
description: The UUID of the physical store
- name: identifier
type: text
required: false
description: The current identifier of the physical store (e.g. disk1s2)
- name: size
type: bigint
required: false
description: The size of the physical store in byptes
notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
evented: false
+79
View File
@@ -0,0 +1,79 @@
name: apfs_volumes
platforms:
- darwin
description: Information about APFS volumes from the `diskutil apfs list -plist` command.
columns:
- name: container_uuid
type: text
required: false
description: The UUID of the APFS Contianer
- name: container_designated_physical_store
type: text
required: false
description: |
The disk displayed as the backing store of the container. There may be multiple,
use `apfs_physical_stores` to see all actual physical stores
- name: container_reference
type: text
required: false
description: The current reference for the APFS container, e.g. "disk3"
- name: container_fusion
type: text
required: false
description: Whether this container is on a "fusion drive" (i.e. SSHD)
- name: container_capacity_ceiling
type: bigint
required: false
description: The total amount of space in the container
- name: container_capacity_free
type: bigint
required: false
description: The amount of remaining free space in the container
- name: uuid
type: text
required: false
description: The UUID of the volume
- name: device_identifier
type: text
required: false
description: The current identifier of the volume (e.g. disk3s2)
- name: name
type: text
required: false
description: The user-selected name of the volume (e.g. "Macintosh HD")
- name: role
type: text
required: false
description: |
The first role of the volume. User-created volumes will have no role (this will be empty).
System volumes might have roles like "Data", "Hardware", etc.
- name: capacity_in_use
type: bigint
required: false
description: Storage space used by the volume
- name: capacity_quota
type: bigint
required: false
description: Storage quota for the volume, or 0 if disabled
- name: capacity_reserve
type: bigint
required: false
description: Storage reserved for this volume even if contianer is otherwise full, or 0 if disabled
- name: crypto_migration_on
type: integer
required: false
description: Whether the volume is in the process of being encrypted
- name: encryption
type: integer
required: false
description: Whether the volume is encrypted, including without requiring a password
- name: filevault
type: integer
required: false
description: Whether the volume requires a password to decrypt
- name: locked
type: integer
required: false
description: Whether the volume is unreadable because it does not have a key entered
notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
evented: false