macOS CIS: Use find command (exposed as fleetd table) instead of relying on the osquery core file table (#12560)

#10292, #12554

When scanning tens of thousands of files for permissions, using the
`find` command exposed as a fleetd table is more performant than trying
to use the `file` table. This change caused the watchdog to *stop*
killing osquery because of exceeding memory or CPU limit.

- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- ~[ ] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)~
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
  - For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
This commit is contained in:
Lucas Manuel Rodriguez
2023-06-29 16:22:41 -03:00
committed by GitHub
parent 9f3331ef94
commit 810eb58b95
8 changed files with 2522 additions and 879 deletions
@@ -0,0 +1 @@
* For performance reasons, update macOS CIS policies 5.1.6 and 5.1.7 to use a new fleetd table `find_cmd` instead of relying on the osquery `file` table.
+13 -12
View File
@@ -2621,10 +2621,11 @@ spec:
SELECT 1 WHERE NOT EXISTS (
SELECT apps.path FROM apps
LEFT JOIN file on file.path = apps.path
WHERE apps.path LIKE '/Applications/%' AND
-- file.mode's last character are the permissions for 'other',
-- bitwise && with '0x2' selects the write permission,
-- which we do not want here.
WHERE CAST(SUBSTRING(file.mode, -1) AS INTEGER) & 0x2 != 0
CAST(SUBSTRING(file.mode, -1) AS INTEGER) & 0x2 != 0
);
purpose: Informational
tags: compliance, CIS, CIS_Level1, CIS-macos-13-5.1.5
@@ -2650,11 +2651,11 @@ spec:
done
query: |
SELECT 1 WHERE NOT EXISTS (
SELECT 1 FROM file WHERE
path LIKE '/System/Volumes/Data/System/%%'
AND type = 'directory'
AND directory NOT LIKE '%Drop Box%'
AND CAST( SUBSTRING( mode ,-1) AS INTEGER) & 0x2 !=0 -- mode last char is others' permissions. bitwise with 0x2 means write permissions. (which we do not want here)
SELECT 1 FROM find_cmd WHERE
directory = '/System/Volumes/Data/System'
AND type = 'd'
AND perm = '-2'
AND path NOT LIKE '%Drop Box%'
);
purpose: Informational
tags: compliance, CIS, CIS_Level1, CIS-macos-13-5.1.6
@@ -2678,12 +2679,12 @@ spec:
done
query: |
SELECT 1 WHERE NOT EXISTS (
SELECT 1 FROM file WHERE
path LIKE '/System/Volumes/Data/Library/%%'
AND type = 'directory'
AND directory NOT LIKE '%Caches%'
AND directory NOT LIKE '%/Preferences/Audio/Data%'
AND CAST( SUBSTRING( mode ,-1) AS INTEGER) & 0x2 !=0 -- mode last char is others' permissions. bitwise with 0x2 means write permissions. (which we do not want here)
SELECT 1 FROM find_cmd WHERE
directory = '/System/Volumes/Data/Library'
AND type = 'd'
AND perm = '-2'
AND path NOT LIKE '%Caches%'
AND path NOT LIKE '%/Preferences/Audio/Data%'
);
purpose: Informational
tags: compliance, CIS, CIS_Level2, CIS-macos-13-5.1.7
+2
View File
@@ -9,6 +9,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/table/diskutil/corestorage"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/dscl"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/filevault_prk"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/find_cmd"
"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"
@@ -46,6 +47,7 @@ func PlatformTables() []osquery.OsqueryPlugin {
table.NewPlugin("corestorage_logical_volumes", corestorage.LogicalVolumesColumns(), corestorage.LogicalVolumesGenerate),
table.NewPlugin("corestorage_logical_volume_families", corestorage.LogicalVolumeFamiliesColumns(), corestorage.LogicalVolumeFamiliesGenerate),
table.NewPlugin("filevault_prk", filevault_prk.Columns(), filevault_prk.Generate),
table.NewPlugin("find_cmd", find_cmd.Columns(), find_cmd.Generate),
// Macadmins extension tables
table.NewPlugin("filevault_users", filevaultusers.FileVaultUsersColumns(), filevaultusers.FileVaultUsersGenerate),
+146
View File
@@ -0,0 +1,146 @@
//go:build darwin
// +build darwin
// Package find_cmd implements a table that executes the /usr/bin/find command.
// This table provides only a subset of the find functionality. Currently only
// allows setting the -perm and -type arguments.
//
// NOTE(lucas): Why does this table exist?
// Initially we implemented queries that used the osquery core `file` table,
// but when processing a high number (10k+) of files it exceeded osquery
// default CPU and memory limits.
package find_cmd
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/osquery/osquery-go/plugin/table"
"github.com/rs/zerolog/log"
)
// Columns is the schema of the table.
func Columns() []table.ColumnDefinition {
return []table.ColumnDefinition{
// directory is the first argument of the find command (basically
// where to search for files).
table.TextColumn("directory"),
// type allows setting find's '-type' argument.
table.TextColumn("type"),
// perm allows setting find's '-perm' argument.
table.TextColumn("perm"),
// path are the found directories.
table.TextColumn("path"),
}
}
var permRegexp = regexp.MustCompile("[-+]*\\d+")
// 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) {
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
}
directory := getArgumentOpEqual("directory")
if directory == "" {
return nil, errors.New("missing directory argument")
}
if !filepath.IsAbs(directory) {
return nil, errors.New("directory must be an absolute path")
}
findType := getArgumentOpEqual("type")
if findType != "" {
switch findType {
case "b", "c", "d", "f", "l", "p", "s":
// OK
default:
return nil, errors.New("type must be one of: 'b', 'c', 'd', 'f', 'l', 'p' or 's'")
}
}
perm := getArgumentOpEqual("perm")
if perm != "" {
if !permRegexp.Match([]byte(perm)) {
return nil, fmt.Errorf("perm must be of the form: %s", permRegexp)
}
}
args := []string{directory}
if findType != "" {
args = append(args, "-type", findType)
}
if perm != "" {
args = append(args, "-perm", perm)
}
cmd := exec.Command("/usr/bin/find", args...)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("create stdout pipe: %w", err)
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
return nil, fmt.Errorf("create stderr pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("command start failed: %w", err)
}
var outDirs []string
reader := bufio.NewReader(stdoutPipe)
line, err := reader.ReadString('\n')
for err == nil {
line = strings.TrimSuffix(line, "\n")
if line == "" {
continue
}
outDirs = append(outDirs, line)
line, err = reader.ReadString('\n')
}
if err != io.EOF {
return nil, fmt.Errorf("unexpected error: %w", err)
}
stderr, err := io.ReadAll(stderrPipe)
if err != nil {
log.Debug().Err(err).Msg("failed to read find stderr")
}
if err := cmd.Wait(); err != nil {
// We ignore error as these could be of the form:
// 'find: /System/Volumes/Data/Library/Caches/com.apple.aned: Operation not permitted'
// which are files unaccessible even for root.
log.Debug().Err(err).Bytes("stderr", stderr).Msg("find failed")
}
rows := make([]map[string]string, 0, len(outDirs))
for _, outDir := range outDirs {
rows = append(rows, map[string]string{
"directory": directory,
"perm": perm,
"type": findType,
"path": outDir,
})
}
return rows, nil
}
@@ -0,0 +1,188 @@
//go:build darwin
// +build darwin
package find_cmd
import (
"os"
"path/filepath"
"testing"
"github.com/osquery/osquery-go/plugin/table"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
// TestGenerate tests the find_cmd table generation.
func TestGenerate(t *testing.T) {
// Test not setting required column directory.
_, err := Generate(context.Background(), table.QueryContext{})
require.Error(t, err)
testDir := t.TempDir()
// Test with an empty directory.
rows, err := Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"directory": {
Affinity: table.ColumnTypeText,
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: testDir,
},
},
},
},
})
require.NoError(t, err)
require.Len(t, rows, 1)
require.Equal(t, rows[0]["path"], testDir)
// Test with invalid type argument.
_, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"directory": {
Affinity: table.ColumnTypeText,
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: testDir,
},
},
},
"type": {
Affinity: table.ColumnTypeText,
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "z",
},
},
},
},
})
require.Error(t, err)
// Test with invalid perm argument.
_, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"directory": {
Affinity: table.ColumnTypeText,
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: testDir,
},
},
},
"perm": {
Affinity: table.ColumnTypeText,
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "foobar",
},
},
},
},
})
require.Error(t, err)
// Populate the directory.
f, err := os.Create(filepath.Join(testDir, "foo.txt"))
require.NoError(t, err)
err = f.Close()
require.NoError(t, err)
err = os.Chmod(filepath.Join(testDir, "foo.txt"), os.ModePerm)
require.NoError(t, err)
require.NoError(t, err)
err = os.Mkdir(filepath.Join(testDir, "zoo"), os.ModePerm)
require.NoError(t, err)
err = os.Chmod(filepath.Join(testDir, "zoo"), os.ModePerm)
require.NoError(t, err)
// Test directory with a few entries.
rows, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"directory": {
Affinity: table.ColumnTypeText,
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: testDir,
},
},
},
},
})
require.NoError(t, err)
require.Len(t, rows, 3)
require.Equal(t, rows[0]["path"], testDir)
require.Equal(t, rows[1]["path"], filepath.Join(testDir, "zoo"))
require.Equal(t, rows[2]["path"], filepath.Join(testDir, "foo.txt"))
// Test directory with a few entries and setting the perm column.
rows, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"directory": {
Affinity: table.ColumnTypeText,
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: testDir,
},
},
},
"perm": {
Affinity: table.ColumnTypeText,
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "-2",
},
},
},
},
})
require.NoError(t, err)
require.Len(t, rows, 2)
require.Equal(t, rows[0]["path"], filepath.Join(testDir, "zoo"))
require.Equal(t, rows[1]["path"], filepath.Join(testDir, "foo.txt"))
// Test directory with a few entries and setting the perm and type column.
rows, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"directory": {
Affinity: table.ColumnTypeText,
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: testDir,
},
},
},
"perm": {
Affinity: table.ColumnTypeText,
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "-2",
},
},
},
"type": {
Affinity: table.ColumnTypeText,
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "d",
},
},
},
},
})
require.NoError(t, err)
require.Len(t, rows, 1)
require.Equal(t, rows[0]["path"], filepath.Join(testDir, "zoo"))
}
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
name: find_cmd
platforms:
- darwin
description: Uses the /usr/bin/find command to list files and directories.
columns:
- name: directory
type: text
required: true
description: |
The directory passed to find as first argument.
- name: type
type: text
required: false
description: |
Sets the value of the `-type` flag.
- name: perm
type: text
required: false
description: |
Sets the value of the `-perm` flag.
- name: path
type: text
required: false
description: |
Contains the found paths.
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 installers can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
evented: false
@@ -23,7 +23,7 @@ set terminal jpeg
set title 'Memory (MB)'
set output 'osquery_worker_memory.jpg'
plot '/tmp/osqueryd.dat' using 1:3 with linespoints linetype 7 linewidth 2 title 'Memory (MB)'
plot '/tmp/osqueryd.dat' using 1:3 with linespoints linetype -1 linewidth 1 title 'Memory (MB)'
set title 'CPU'
set output 'osquery_worker_cpu.jpg'
@@ -34,10 +34,10 @@ set yrange [0:24000]
# where default values are: check_interval=3000ms, percent_cpu_limit=10%.
# On my Macbook with 4 physical core this gives 1200ms.
#
plot '/tmp/osqueryd.dat' using 1:2 with linespoints linetype 6 linewidth 2 title 'CPU', 1200 linecolor 1
plot '/tmp/osqueryd.dat' using 1:2 with linespoints linetype -1 linewidth 1 title 'CPU', 1200 linecolor 1
EOF
gnuplot < gnuplot_commands.txt
rm gnuplot_commands.txt
open osquery_worker_cpu.jpg osquery_worker_memory.jpg
open osquery_worker_cpu.jpg osquery_worker_memory.jpg