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:
@@ -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),
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
Reference in New Issue
Block a user