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:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
+2140
-864
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user