Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
d7cc8fb
update osquery schema
jacobshandling Dec 25, 2025
8224965
add table output parsing
jacobshandling Dec 25, 2025
162b819
back out table changes, leave placeholder note
jacobshandling Dec 26, 2025
2c29e88
Go entities: `host_software_installed_paths.executable_sha256` –> `.c…
jacobshandling Dec 26, 2025
ce3dd3c
migration - update col name, add new col
jacobshandling Dec 26, 2025
1820741
make dump-test-schema
jacobshandling Dec 26, 2025
9b66a89
website schema
jacobshandling Dec 26, 2025
8538de5
fix insert host software installed paths statement
jacobshandling Dec 26, 2025
19e1e70
update db test
jacobshandling Dec 26, 2025
ef207af
restore original API fieldname for backwards compatibility
jacobshandling Dec 26, 2025
508f2f7
add new column to API responses
jacobshandling Dec 26, 2025
fa45003
update tests
jacobshandling Dec 26, 2025
cdcf21a
add missing column when inserting host sw installed paths
jacobshandling Dec 27, 2025
6a1472d
cleanup, fix
jacobshandling Dec 27, 2025
43b5743
fix integration test
jacobshandling Dec 27, 2025
89860f9
test fix
jacobshandling Jan 6, 2026
d6baa54
fix list host sw test
jacobshandling Jan 6, 2026
ff47c80
wip - ingestion
jacobshandling Jan 6, 2026
792e334
bump migration, dump test schema
jacobshandling Jan 6, 2026
40646a4
add new migration files
jacobshandling Jan 6, 2026
c2a53c6
implement `fileutil` fleetd table
jacobshandling Jan 7, 2026
135648a
test new table
jacobshandling Jan 7, 2026
ebf14e3
add new sw override query
jacobshandling Jan 7, 2026
7f969d3
update
jacobshandling Jan 7, 2026
3b759b1
fix
jacobshandling Jan 7, 2026
9810b58
wip
jacobshandling Jan 9, 2026
a7e6e3e
wip
jacobshandling Jan 9, 2026
49c9a93
working
jacobshandling Jan 9, 2026
2b3d2f3
cleanup
jacobshandling Jan 9, 2026
e3abbc2
bump migration
jacobshandling Jan 9, 2026
17b1655
remove column from codesign table
jacobshandling Jan 9, 2026
3ed7c31
test preprocessing
jacobshandling Jan 10, 2026
02cfd49
test-wip
jacobshandling Jan 10, 2026
7fdb508
fix test
jacobshandling Jan 10, 2026
dabc950
bump n dump
jacobshandling Jan 10, 2026
492e40a
lint
jacobshandling Jan 10, 2026
12abda2
fix test
jacobshandling Jan 10, 2026
136e899
fix
jacobshandling Jan 10, 2026
ce500cb
make generate-doc
jacobshandling Jan 10, 2026
7135b35
update software override description
jacobshandling Jan 11, 2026
89f54a9
add non-null path constraint to override query
jacobshandling Jan 11, 2026
26a70a7
disambiguate 'path' returned from table
jacobshandling Jan 11, 2026
7579a8d
remove reference to null err
jacobshandling Jan 11, 2026
f2d9054
assert nil before dereference
jacobshandling Jan 11, 2026
c9af06f
fix error message
jacobshandling Jan 11, 2026
ef0ce1c
make generate-doc
jacobshandling Jan 11, 2026
52c4330
filepath.Join over string concat
jacobshandling Jan 12, 2026
e94a60e
wip 1 - update table name, column names, api fields, include exec path
jacobshandling Jan 13, 2026
37e02c8
wip 2
jacobshandling Jan 13, 2026
e97ad9d
update migration
jacobshandling Jan 13, 2026
88ebf72
make dump-test-schema
jacobshandling Jan 13, 2026
84605cb
cleanup
jacobshandling Jan 13, 2026
f37185a
bump n dump
jacobshandling Jan 13, 2026
52d3216
make generate-doc
jacobshandling Jan 13, 2026
48950a1
fix list
jacobshandling Jan 13, 2026
7348b20
fix test
jacobshandling Jan 13, 2026
614a0c5
fix ingestion
jacobshandling Jan 13, 2026
50c1edd
fix test
jacobshandling Jan 13, 2026
008ea5f
gen doc
jacobshandling Jan 13, 2026
e675f8d
change files
jacobshandling Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/33522-executable-hashes
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.
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,24 @@ SELECT c.*
JOIN codesign c ON a.path = c.path
```

## software_macos_executable_sha256

- Description: A software override query[^1] to append the sha256 hash of app bundle executables to macOS software entries. Requires `fleetd`

- Platforms: darwin

- Discovery query:
```sql
SELECT 1 FROM osquery_registry WHERE active = true AND registry = 'table' AND name = 'executable_hashes'
```

- Query:
```sql
SELECT eh.*
FROM apps a
JOIN executable_hashes eh ON a.path = eh.path
```
Comment thread
jacobshandling marked this conversation as resolved.

## software_macos_firefox

- Description: A software override query[^1] to differentiate between Firefox and Firefox ESR on macOS. Requires `fleetd`
Expand Down
1 change: 1 addition & 0 deletions orbit/changes/33522-executable_hashes-table
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.
168 changes: 168 additions & 0 deletions orbit/pkg/table/executable_hashes/executable_hashes.go
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
}
Comment thread
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 ""
}
Comment thread
jacobshandling marked this conversation as resolved.
97 changes: 97 additions & 0 deletions orbit/pkg/table/executable_hashes/executable_hashes_test.go
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)
}
}
2 changes: 2 additions & 0 deletions orbit/pkg/table/extension_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/table/diskutil/apfs"
"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/executable_hashes"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/filevault_prk"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/filevault_status"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/find_cmd"
Expand Down Expand Up @@ -111,6 +112,7 @@ func PlatformTables(opts PluginOpts) ([]osquery.OsqueryPlugin, error) {
table.NewPlugin("santa_status", santa.StatusColumns(), santa.GenerateStatus),
table.NewPlugin("santa_allowed", santa.LogColumns(), santa.GenerateAllowed),
table.NewPlugin("santa_denied", santa.LogColumns(), santa.GenerateDenied),
table.NewPlugin("executable_hashes", executable_hashes.Columns(), executable_hashes.Generate),
}

// append platform specific tables
Expand Down
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)
}
Comment thread
jacobshandling marked this conversation as resolved.

return nil
}

func Down_20260113012054(tx *sql.Tx) error {
return nil
}
Loading
Loading