mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #41328 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet (see [Must rule] - [x] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes - [x] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed an issue where executable hashes failed to compute for macOS app bundles with emoji or other Unicode characters in executable names, improving bundle detection and integrity checks. * **Tests** * Added comprehensive tests to ensure correct handling of Unicode escape sequences and emoji in bundle names and executables. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
155 lines
4.1 KiB
Go
155 lines
4.1 KiB
Go
//go:build darwin
|
|
|
|
// Package executable_hashes implements an extension osquery table to get information about a macOS bundle
|
|
package executable_hashes
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"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
|
|
}
|
|
|
|
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 {
|
|
infoPlistPath := filepath.Join(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 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 ""
|
|
}
|
|
|
|
executableName := strings.TrimSpace(string(output))
|
|
if executableName == "" {
|
|
return ""
|
|
}
|
|
|
|
// The macOS `defaults read` command encodes supplementary Unicode characters (such as emoji) as
|
|
// \uXXXX escape sequences using UTF-16 surrogate pairs.
|
|
executableName = fleet.DecodeUnicodeEscapes(executableName)
|
|
|
|
return filepath.Join(path, "/Contents/MacOS/", executableName)
|
|
}
|