-
Notifications
You must be signed in to change notification settings - Fork 843
Compute, ingest, persist, and serve .app bundle executable hashes and paths #38118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
60 commits
Select commit
Hold shift + click to select a range
d7cc8fb
update osquery schema
jacobshandling 8224965
add table output parsing
jacobshandling 162b819
back out table changes, leave placeholder note
jacobshandling 2c29e88
Go entities: `host_software_installed_paths.executable_sha256` –> `.c…
jacobshandling ce3dd3c
migration - update col name, add new col
jacobshandling 1820741
make dump-test-schema
jacobshandling 9b66a89
website schema
jacobshandling 8538de5
fix insert host software installed paths statement
jacobshandling 19e1e70
update db test
jacobshandling ef207af
restore original API fieldname for backwards compatibility
jacobshandling 508f2f7
add new column to API responses
jacobshandling fa45003
update tests
jacobshandling cdcf21a
add missing column when inserting host sw installed paths
jacobshandling 6a1472d
cleanup, fix
jacobshandling 43b5743
fix integration test
jacobshandling 89860f9
test fix
jacobshandling d6baa54
fix list host sw test
jacobshandling ff47c80
wip - ingestion
jacobshandling 792e334
bump migration, dump test schema
jacobshandling 40646a4
add new migration files
jacobshandling c2a53c6
implement `fileutil` fleetd table
jacobshandling 135648a
test new table
jacobshandling ebf14e3
add new sw override query
jacobshandling 7f969d3
update
jacobshandling 3b759b1
fix
jacobshandling 9810b58
wip
jacobshandling a7e6e3e
wip
jacobshandling 49c9a93
working
jacobshandling 2b3d2f3
cleanup
jacobshandling e3abbc2
bump migration
jacobshandling 17b1655
remove column from codesign table
jacobshandling 3ed7c31
test preprocessing
jacobshandling 02cfd49
test-wip
jacobshandling 7fdb508
fix test
jacobshandling dabc950
bump n dump
jacobshandling 492e40a
lint
jacobshandling 12abda2
fix test
jacobshandling 136e899
fix
jacobshandling ce500cb
make generate-doc
jacobshandling 7135b35
update software override description
jacobshandling 89f54a9
add non-null path constraint to override query
jacobshandling 26a70a7
disambiguate 'path' returned from table
jacobshandling 7579a8d
remove reference to null err
jacobshandling f2d9054
assert nil before dereference
jacobshandling c9af06f
fix error message
jacobshandling ef0ce1c
make generate-doc
jacobshandling 52c4330
filepath.Join over string concat
jacobshandling e94a60e
wip 1 - update table name, column names, api fields, include exec path
jacobshandling 37e02c8
wip 2
jacobshandling e97ad9d
update migration
jacobshandling 88ebf72
make dump-test-schema
jacobshandling 84605cb
cleanup
jacobshandling f37185a
bump n dump
jacobshandling 52d3216
make generate-doc
jacobshandling 48950a1
fix list
jacobshandling 7348b20
fix test
jacobshandling 614a0c5
fix ingestion
jacobshandling 50c1edd
fix test
jacobshandling 008ea5f
gen doc
jacobshandling e675f8d
change files
jacobshandling File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| * Implemented ingesting, persisting, and serving the sha256 hash and path for the CFBundleExecutable binaries of .app bundles on macOS. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| - Implemented the `executable_hashes` `fleetd` table, providing easy access to the sha256 hashes of a binary, either passed directly via a PATH clause, or discovered within a .app bundle when the path to the bundle is provided in the WHERE clause. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| //go:build darwin | ||
|
|
||
| // Package executable_hashes implements an extension osquery table to get information about a macOS file | ||
| package executable_hashes | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/sha256" | ||
| "encoding/hex" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| "github.com/osquery/osquery-go/plugin/table" | ||
| "github.com/rs/zerolog/log" | ||
| ) | ||
|
|
||
| const ( | ||
| colPath = "path" | ||
| colExecPath = "executable_path" | ||
| colExecHash = "executable_sha256" | ||
| ) | ||
|
|
||
| // Columns is the schema of the table. | ||
| func Columns() []table.ColumnDefinition { | ||
| return []table.ColumnDefinition{ | ||
| table.TextColumn(colPath), | ||
| table.TextColumn(colExecPath), | ||
| table.TextColumn(colExecHash), | ||
| } | ||
| } | ||
|
|
||
| // Generate is called to return the results for the table at query time. | ||
| func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { | ||
| path := "" | ||
| wildcard := false | ||
|
|
||
| var results []map[string]string | ||
|
|
||
| if constraintList, present := queryContext.Constraints[colPath]; present { | ||
| // 'path' is in the where clause | ||
| for _, constraint := range constraintList.Constraints { | ||
| path = constraint.Expression | ||
|
|
||
| switch constraint.Operator { | ||
| case table.OperatorLike: | ||
| path = constraint.Expression | ||
| wildcard = true | ||
| case table.OperatorEquals: | ||
| path = constraint.Expression | ||
| wildcard = false | ||
| } | ||
| } | ||
| } else { | ||
| return results, errors.New("missing `path` constraint: provide a `path` in the query's `WHERE` clause") | ||
| } | ||
|
|
||
| processed, err := processFile(path, wildcard) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| for _, res := range processed { | ||
| results = append(results, map[string]string{ | ||
| colPath: res.Path, | ||
| colExecPath: res.ExecPath, | ||
| colExecHash: res.ExecSha256, | ||
| }) | ||
| } | ||
|
|
||
| return results, nil | ||
| } | ||
|
|
||
| type fileInfo struct { | ||
| Path string | ||
| ExecPath string | ||
| ExecSha256 string | ||
| } | ||
|
|
||
| func processFile(path string, wildcard bool) ([]fileInfo, error) { | ||
| var output []fileInfo | ||
|
|
||
| if wildcard { | ||
| replacedPath := strings.ReplaceAll(path, "%", "*") | ||
|
|
||
| resolvedPaths, err := filepath.Glob(replacedPath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to resolve filepaths for incoming path: %w", err) | ||
| } | ||
| for _, p := range resolvedPaths { | ||
| execPath := getExecutablePath(context.Background(), p) | ||
|
|
||
| hash, err := computeFileSHA256(execPath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("computing executable sha256 from wildcard path: %w", err) | ||
| } | ||
|
|
||
| output = append(output, fileInfo{Path: p, ExecPath: execPath, ExecSha256: hash}) | ||
| } | ||
| } else { | ||
| execPath := getExecutablePath(context.Background(), path) | ||
|
|
||
| hash, err := computeFileSHA256(execPath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("computing executable sha256 from specific path: %w", err) | ||
| } | ||
| output = append(output, fileInfo{Path: path, ExecPath: execPath, ExecSha256: hash}) | ||
| } | ||
| return output, nil | ||
| } | ||
|
jacobshandling marked this conversation as resolved.
|
||
|
|
||
| func computeFileSHA256(filePath string) (string, error) { | ||
| if filePath == "" { | ||
| log.Warn().Msg("empty path provided, returning empty hash") | ||
| return "", nil | ||
| } | ||
| f, err := os.Open(filePath) | ||
| if err != nil { | ||
| return "", fmt.Errorf("couldn't open filepath: %w", err) | ||
| } | ||
| defer f.Close() | ||
|
|
||
| h := sha256.New() | ||
| if _, err := io.Copy(h, f); err != nil { | ||
| return "", fmt.Errorf("computing hash: %w", err) | ||
| } | ||
|
|
||
| return hex.EncodeToString(h.Sum(nil)), nil | ||
| } | ||
|
|
||
| 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) | ||
| if err != nil { | ||
| log.Warn().Err(err).Str("path", path).Msg("couldn't get FileInfo") | ||
| return "" | ||
| } | ||
|
|
||
| // Only return the path if it's a regular file (not a directory) | ||
| if info.Mode().IsRegular() { | ||
| return path | ||
| } | ||
|
|
||
| log.Warn().Str("path", path).Msg("path is not a regular file nor a .app bundle") | ||
| return "" | ||
| } | ||
|
jacobshandling marked this conversation as resolved.
|
||
97 changes: 97 additions & 0 deletions
97
orbit/pkg/table/executable_hashes/executable_hashes_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| //go:build darwin | ||
|
|
||
| package executable_hashes | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/sha256" | ||
| "encoding/hex" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/osquery/osquery-go/plugin/table" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestGenerateWithExactPath(t *testing.T) { | ||
| dir := t.TempDir() | ||
| defer os.RemoveAll(dir) | ||
|
|
||
| path := filepath.Join(dir, "example.bin") | ||
| execPath := path | ||
| content := []byte("test file content for hashing") | ||
| require.NoError(t, os.WriteFile(path, content, 0o600)) | ||
|
|
||
| h := sha256.New() | ||
| h.Write(content) | ||
| expectedHash := hex.EncodeToString(h.Sum(nil)) | ||
|
|
||
| rows, err := Generate(context.Background(), table.QueryContext{ | ||
| Constraints: map[string]table.ConstraintList{ | ||
| colPath: { | ||
| Constraints: []table.Constraint{{ | ||
| Expression: path, | ||
| Operator: table.OperatorEquals, | ||
| }}, | ||
| }, | ||
| }, | ||
| }) | ||
| require.NoError(t, err) | ||
| require.Len(t, rows, 1) | ||
| require.Equal(t, path, rows[0][colPath]) | ||
| require.Equal(t, execPath, rows[0][colExecPath]) | ||
| require.Equal(t, expectedHash, rows[0][colExecHash]) | ||
| } | ||
|
|
||
| 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"), | ||
| } | ||
|
|
||
| expectedHashByBundlePath := make(map[string]string) | ||
|
|
||
| for filename, content := range testFiles { | ||
| path := filepath.Join(dir, filename) | ||
| require.NoError(t, os.WriteFile(path, content, 0o600)) | ||
|
|
||
| h := sha256.New() | ||
| h.Write(content) | ||
| expectedHashByBundlePath[path] = hex.EncodeToString(h.Sum(nil)) | ||
| } | ||
|
|
||
| rows, err := Generate(context.Background(), table.QueryContext{ | ||
| Constraints: map[string]table.ConstraintList{ | ||
| colPath: { | ||
| Constraints: []table.Constraint{{ | ||
| Expression: filepath.Join(dir, "%.bin"), | ||
| Operator: table.OperatorLike, | ||
| }}, | ||
| }, | ||
| }, | ||
| }) | ||
| require.NoError(t, err) | ||
| require.Len(t, rows, len(testFiles)) | ||
|
|
||
| got := make(map[string]fileInfo, len(rows)) | ||
| for _, row := range rows { | ||
| got[row[colPath]] = fileInfo{ | ||
| Path: row[colPath], | ||
| ExecPath: row[colExecPath], | ||
| ExecSha256: row[colExecHash], | ||
| } | ||
| } | ||
|
|
||
| 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) | ||
| require.Equal(t, expectedHash, info.ExecSha256) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
...tastore/mysql/migrations/tables/20260113012054_AddAndUpdateSwInstalledPathsBinHashCols.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package tables | ||
|
|
||
| import ( | ||
| "database/sql" | ||
| "fmt" | ||
| ) | ||
|
|
||
| func init() { | ||
| MigrationClient.AddMigration(Up_20260113012054, Down_20260113012054) | ||
| } | ||
|
|
||
| func Up_20260113012054(tx *sql.Tx) error { | ||
| _, err := tx.Exec(` | ||
| ALTER TABLE host_software_installed_paths | ||
| CHANGE executable_sha256 cdhash_sha256 CHAR(64) COLLATE utf8mb4_unicode_ci NULL | ||
| `) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to update name of 'host_software_installed_paths' column 'executable_sha256' to `cdhash_sha256`: %w", err) | ||
| } | ||
|
|
||
| _, err = tx.Exec(` | ||
| ALTER TABLE host_software_installed_paths | ||
| ADD COLUMN executable_sha256 CHAR(64) COLLATE utf8mb4_unicode_ci NULL, | ||
| ADD COLUMN executable_path TEXT COLLATE utf8mb4_unicode_ci NULL | ||
| `) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to add columns 'executable_sha256' and 'executable_path' to 'host_software_installed_paths' table: %w", err) | ||
| } | ||
|
jacobshandling marked this conversation as resolved.
|
||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func Down_20260113012054(tx *sql.Tx) error { | ||
| return nil | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.