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)
}
}