diff --git a/orbit/pkg/table/executable_hashes/executable_hashes.go b/orbit/pkg/table/executable_hashes/executable_hashes.go index c43eb6e7e3..b861195ce5 100644 --- a/orbit/pkg/table/executable_hashes/executable_hashes.go +++ b/orbit/pkg/table/executable_hashes/executable_hashes.go @@ -1,6 +1,6 @@ //go:build darwin -// Package executable_hashes implements an extension osquery table to get information about a macOS file +// Package executable_hashes implements an extension osquery table to get information about a macOS bundle package executable_hashes import ( @@ -133,36 +133,18 @@ func computeFileSHA256(filePath string) (string, error) { } func getExecutablePath(ctx context.Context, path string) string { - if strings.HasSuffix(path, ".app") { - // Use defaults to read CFBundleExecutable from Info.plist - infoPlistPath := path + "/Contents/Info.plist" - output, err := exec.CommandContext(ctx, "/usr/bin/defaults", "read", infoPlistPath, "CFBundleExecutable").Output() - if err != nil { - // lots of helper .app bundles nested within parent .apps seem to have invalid Info.plists - warn and continue - log.Warn().Err(err).Str("path", path).Msg("failed to read CFBundleExecutable from Info.plist, returning empty binary path") - return "" - } - - executableName := strings.TrimSpace(string(output)) - if executableName == "" { - return "" - } - - return filepath.Join(path, "/Contents/MacOS/", executableName) - } - - // For non-app paths, check if it's a regular file (binary) - info, err := os.Stat(path) + infoPlistPath := filepath.Join(path, "/Contents/Info.plist") + output, err := exec.CommandContext(ctx, "/usr/bin/defaults", "read", infoPlistPath, "CFBundleExecutable").Output() if err != nil { - log.Warn().Err(err).Str("path", path).Msg("couldn't get FileInfo") + // lots of helper app bundles nested within parent bundles seem to have invalid Info.plists - warn and continue + log.Warn().Err(err).Str("path", path).Msg("failed to read CFBundleExecutable from Info.plist, returning empty binary path") return "" } - // Only return the path if it's a regular file (not a directory) - if info.Mode().IsRegular() { - return path + executableName := strings.TrimSpace(string(output)) + if executableName == "" { + return "" } - log.Warn().Str("path", path).Msg("path is not a regular file nor a .app bundle") - return "" + return filepath.Join(path, "/Contents/MacOS/", executableName) } diff --git a/orbit/pkg/table/executable_hashes/executable_hashes_test.go b/orbit/pkg/table/executable_hashes/executable_hashes_test.go index 7d51210ad1..8234dafb0c 100644 --- a/orbit/pkg/table/executable_hashes/executable_hashes_test.go +++ b/orbit/pkg/table/executable_hashes/executable_hashes_test.go @@ -6,6 +6,7 @@ import ( "context" "crypto/sha256" "encoding/hex" + "fmt" "os" "path/filepath" "testing" @@ -18,10 +19,29 @@ func TestGenerateWithExactPath(t *testing.T) { dir := t.TempDir() defer os.RemoveAll(dir) - path := filepath.Join(dir, "example.bin") - execPath := path + // Create a macOS app bundle structure + bundlePath := filepath.Join(dir, "Test.app") + contentsDir := filepath.Join(bundlePath, "Contents") + macosDir := filepath.Join(contentsDir, "MacOS") + require.NoError(t, os.MkdirAll(macosDir, 0o755)) + + execName := "Test" + // Create Info.plist with CFBundleExecutable key + infoPlistPath := filepath.Join(contentsDir, "Info.plist") + infoPlistContent := fmt.Sprintf(` + + + + CFBundleExecutable + %s + +`, execName) + require.NoError(t, os.WriteFile(infoPlistPath, []byte(infoPlistContent), 0o644)) + + // Create the actual executable binary in Contents/MacOS/ + execPath := filepath.Join(macosDir, execName) content := []byte("test file content for hashing") - require.NoError(t, os.WriteFile(path, content, 0o600)) + require.NoError(t, os.WriteFile(execPath, content, 0o644)) h := sha256.New() h.Write(content) @@ -31,7 +51,7 @@ func TestGenerateWithExactPath(t *testing.T) { Constraints: map[string]table.ConstraintList{ colPath: { Constraints: []table.Constraint{{ - Expression: path, + Expression: bundlePath, Operator: table.OperatorEquals, }}, }, @@ -39,7 +59,7 @@ func TestGenerateWithExactPath(t *testing.T) { }) require.NoError(t, err) require.Len(t, rows, 1) - require.Equal(t, path, rows[0][colPath]) + require.Equal(t, bundlePath, rows[0][colPath]) require.Equal(t, execPath, rows[0][colExecPath]) require.Equal(t, expectedHash, rows[0][colExecHash]) } @@ -48,37 +68,76 @@ func TestGenerateWithWildcard(t *testing.T) { dir := t.TempDir() defer os.RemoveAll(dir) - testFiles := map[string][]byte{ - "foo.bin": []byte("content of foo"), - "bar.bin": []byte("content of bar"), - "baz.bin": []byte("content of baz"), + testBundles := map[string]struct { + executableName string + content []byte + }{ + "Foo.app": {"Foo", []byte("content of foo")}, + "Bar.app": {"Bar", []byte("content of bar")}, + "Baz.service": {"Baz", []byte("content of baz")}, + "Bonk.service": {"Bonk", []byte("content of bonk")}, } expectedHashByBundlePath := make(map[string]string) + expectedExecPathByBundlePath := make(map[string]string) - for filename, content := range testFiles { - path := filepath.Join(dir, filename) - require.NoError(t, os.WriteFile(path, content, 0o600)) + // Create macOS app bundle structures + for bundleName, bundleInfo := range testBundles { + bundlePath := filepath.Join(dir, bundleName) + contentsDir := filepath.Join(bundlePath, "Contents") + macosDir := filepath.Join(contentsDir, "MacOS") + require.NoError(t, os.MkdirAll(macosDir, 0o755)) + + // Create Info.plist with CFBundleExecutable key + infoPlistPath := filepath.Join(contentsDir, "Info.plist") + infoPlistContent := fmt.Sprintf(` + + + + CFBundleExecutable + %s + +`, bundleInfo.executableName) + require.NoError(t, os.WriteFile(infoPlistPath, []byte(infoPlistContent), 0o644)) + + // Create the actual executable in Contents/MacOS/ + execPath := filepath.Join(macosDir, bundleInfo.executableName) + require.NoError(t, os.WriteFile(execPath, bundleInfo.content, 0o644)) h := sha256.New() - h.Write(content) - expectedHashByBundlePath[path] = hex.EncodeToString(h.Sum(nil)) + h.Write(bundleInfo.content) + expectedHashByBundlePath[bundlePath] = hex.EncodeToString(h.Sum(nil)) + expectedExecPathByBundlePath[bundlePath] = execPath } rows, err := Generate(context.Background(), table.QueryContext{ Constraints: map[string]table.ConstraintList{ colPath: { Constraints: []table.Constraint{{ - Expression: filepath.Join(dir, "%.bin"), + Expression: filepath.Join(dir, "%.app"), Operator: table.OperatorLike, }}, }, }, }) require.NoError(t, err) - require.Len(t, rows, len(testFiles)) + require.Len(t, rows, 2) - got := make(map[string]fileInfo, len(rows)) + serviceRows, err := Generate(context.Background(), table.QueryContext{ + Constraints: map[string]table.ConstraintList{ + colPath: { + Constraints: []table.Constraint{{ + Expression: filepath.Join(dir, "%.service"), + Operator: table.OperatorLike, + }}, + }, + }, + }) + require.NoError(t, err) + require.Len(t, serviceRows, 2) + rows = append(rows, serviceRows...) + + got := make(map[string]fileInfo, 4) for _, row := range rows { got[row[colPath]] = fileInfo{ Path: row[colPath], @@ -87,11 +146,11 @@ func TestGenerateWithWildcard(t *testing.T) { } } - for path, expectedHash := range expectedHashByBundlePath { - require.Contains(t, got, path) - info := got[path] - require.Equal(t, path, info.Path) - require.Equal(t, path, info.ExecPath) + for bundlePath, expectedHash := range expectedHashByBundlePath { + require.Contains(t, got, bundlePath) + info := got[bundlePath] + require.Equal(t, bundlePath, info.Path) + require.Equal(t, expectedExecPathByBundlePath[bundlePath], info.ExecPath) require.Equal(t, expectedHash, info.ExecSha256) } }