diff --git a/pkg/osquery/table/chrome_login_data_emails.go b/pkg/osquery/table/chrome_login_data_emails.go deleted file mode 100644 index 4e799dc0ac..0000000000 --- a/pkg/osquery/table/chrome_login_data_emails.go +++ /dev/null @@ -1,141 +0,0 @@ -package table - -import ( - "context" - "database/sql" - "fmt" - "log/slog" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/kolide/kit/fsutil" - "github.com/kolide/launcher/v2/ee/agent" - "github.com/kolide/launcher/v2/ee/agent/types" - "github.com/kolide/launcher/v2/ee/observability" - "github.com/kolide/launcher/v2/ee/tables/tablewrapper" - "github.com/osquery/osquery-go/plugin/table" - _ "modernc.org/sqlite" -) - -var profileDirs = map[string][]string{ - "windows": {"Appdata/Local/Google/Chrome/User Data"}, - "darwin": {"Library/Application Support/Google/Chrome"}, -} -var profileDirsDefault = []string{".config/google-chrome", ".config/chromium", "snap/chromium/current/.config/chromium"} - -func ChromeLoginDataEmails(flags types.Flags, slogger *slog.Logger) *table.Plugin { - c := &ChromeLoginDataEmailsTable{ - slogger: slogger.With("table", "kolide_chrome_login_data_emails"), - } - columns := []table.ColumnDefinition{ - table.TextColumn("username"), - table.TextColumn("email"), - table.BigIntColumn("count"), - } - return tablewrapper.New(flags, slogger, "kolide_chrome_login_data_emails", columns, c.generate, - tablewrapper.WithDescription("Email addresses found in Chrome's saved login data, grouped by OS user and email with a login count. Useful for identifying which accounts are saved in Chrome profiles."), - ) -} - -type ChromeLoginDataEmailsTable struct { - slogger *slog.Logger -} - -func (c *ChromeLoginDataEmailsTable) generateForPath(ctx context.Context, file userFileInfo) ([]map[string]string, error) { - dir, err := agent.MkdirTemp("kolide_chrome_login_data_emails") - if err != nil { - return nil, fmt.Errorf("creating kolide_chrome_login_data_emails tmp dir: %w", err) - } - defer os.RemoveAll(dir) // clean up - - dst := filepath.Join(dir, "tmpfile") - if err := fsutil.CopyFile(file.path, dst); err != nil { - return nil, fmt.Errorf("copying sqlite file to tmp dir: %w", err) - } - - db, err := sql.Open("sqlite", dst) - if err != nil { - return nil, fmt.Errorf("connecting to sqlite db: %w", err) - } - defer db.Close() - - rows, err := db.QueryContext(ctx, "SELECT username_value, count(*) AS count FROM logins GROUP BY lower(username_value)") - if err != nil { - return nil, fmt.Errorf("query rows from chrome login keychain db: %w", err) - } - defer func() { - if err := rows.Close(); err != nil { - c.slogger.Log(ctx, slog.LevelWarn, - "closing rows after scanning results", - "err", err, - ) - } - if err := rows.Err(); err != nil { - c.slogger.Log(ctx, slog.LevelWarn, - "encountered iteration error", - "err", err, - ) - } - }() - - var results []map[string]string - - // loop through all the sqlite rows and add them as osquery rows in the results map - for rows.Next() { // we initialize these variables for every row, that way we don't have data from the previous iteration - var usernameValue string - var usernameCount string - if err := rows.Scan(&usernameValue, &usernameCount); err != nil { - return nil, fmt.Errorf("scanning chrome login keychain db row: %w", err) - } - // append anything that could be an email - if !strings.Contains(usernameValue, "@") { - continue - } - results = append(results, map[string]string{ - "username": file.user, - "email": usernameValue, - "count": usernameCount, - }) - } - return results, nil -} - -func (c *ChromeLoginDataEmailsTable) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { - ctx, span := observability.StartSpan(ctx, "table_name", "kolide_chrome_login_data_emails") - defer span.End() - - var results []map[string]string - osProfileDirs, ok := profileDirs[runtime.GOOS] - if !ok { - osProfileDirs = profileDirsDefault - } - - for _, profileDir := range osProfileDirs { - files, err := findFileInUserDirs(filepath.Join(profileDir, "*/Login Data"), c.slogger) - if err != nil { - c.slogger.Log(ctx, slog.LevelInfo, - "finding chrome login data sqlite DBs", - "path", profileDir, - "err", err, - ) - continue - } - - for _, file := range files { - res, err := c.generateForPath(ctx, file) - if err != nil { - c.slogger.Log(ctx, slog.LevelInfo, - "generating chrome keychain result", - "path", file.path, - "err", err, - ) - continue - } - results = append(results, res...) - } - } - - return results, nil -} diff --git a/pkg/osquery/table/chrome_login_data_emails_test.go b/pkg/osquery/table/chrome_login_data_emails_test.go deleted file mode 100644 index b159ce64ab..0000000000 --- a/pkg/osquery/table/chrome_login_data_emails_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package table - -import ( - "database/sql" - "fmt" - "os" - "path/filepath" - "runtime" - "testing" - "time" - - typesmocks "github.com/kolide/launcher/v2/ee/agent/types/mocks" - "github.com/kolide/launcher/v2/pkg/log/multislogger" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "go.uber.org/goleak" -) - -func TestMain(m *testing.M) { - goleak.VerifyTestMain(m) -} - -func TestChromeLoginDataEmails(t *testing.T) { //nolint:paralleltest // We need to update package-level vars in this test - // Set up table dependencies - mockFlags := typesmocks.NewFlags(t) - mockFlags.On("TableGenerateTimeout").Return(1 * time.Minute) - mockFlags.On("RegisterChangeObserver", mock.Anything, mock.Anything).Return() - slogger := multislogger.NewNopLogger() - - // Set up a sqlite database for querying - // It must live in ///*/Login Data - testUsername := "testusername" - tempHomeDir := t.TempDir() - tempUserHomeDir := filepath.Join(tempHomeDir, testUsername) - require.NoError(t, os.Mkdir(tempUserHomeDir, 0755)) - appDir := "chrome" - require.NoError(t, os.Mkdir(filepath.Join(tempUserHomeDir, appDir), 0755)) - profileDir := filepath.Join(tempUserHomeDir, appDir, "testprofile") - require.NoError(t, os.Mkdir(profileDir, 0755)) - tempSqliteFilepath := filepath.Join(profileDir, "Login Data") - f, err := os.Create(tempSqliteFilepath) - require.NoError(t, err) - f.Close() - db, err := sql.Open("sqlite", tempSqliteFilepath) - require.NoError(t, err) - _, err = db.ExecContext(t.Context(), `CREATE TABLE IF NOT EXISTS logins (username_value TEXT);`) - require.NoError(t, err) - _, err = db.ExecContext(t.Context(), `INSERT INTO logins (username_value) VALUES ("testusername@example.com");`) - require.NoError(t, err) - require.NoError(t, db.Close()) - - // Point the table to this new db by modifying package vars - homeDirLocations[runtime.GOOS] = append(homeDirLocations[runtime.GOOS], tempHomeDir) - profileDirs[runtime.GOOS] = append(profileDirs[runtime.GOOS], appDir) - - // Create table and verify the name is what we expect - chromeLoginDataEmailsTable := ChromeLoginDataEmails(mockFlags, slogger) - require.Equal(t, "kolide_chrome_login_data_emails", chromeLoginDataEmailsTable.Name()) - - // Confirm we can call the table successfully - response := chromeLoginDataEmailsTable.Call(t.Context(), map[string]string{ - "action": "generate", - "context": "{}", - }) - require.Equal(t, int32(0), response.Status.Code, response.Status.Message) // 0 means success - testUserFound := false - for _, row := range response.Response { - if username, ok := row["username"]; ok { - if username == testUsername { - testUserFound = true - break - } - } - } - require.True(t, testUserFound, fmt.Sprintf("response did not include testusername: %+v", response)) -} diff --git a/pkg/osquery/table/chrome_login_keychain.go b/pkg/osquery/table/chrome_login_keychain.go index a4f86e247a..c9ce099cab 100644 --- a/pkg/osquery/table/chrome_login_keychain.go +++ b/pkg/osquery/table/chrome_login_keychain.go @@ -17,7 +17,7 @@ import ( _ "modernc.org/sqlite" ) -// DEPRECATED use kolide_chrome_login_data_emails +// DEPRECATED use katc_chrome_login_data_emails func ChromeLoginKeychainInfo(flags types.Flags, slogger *slog.Logger) *table.Plugin { c := &ChromeLoginKeychain{ slogger: slogger.With("table", "kolide_chrome_login_keychain"), @@ -28,7 +28,7 @@ func ChromeLoginKeychainInfo(flags types.Flags, slogger *slog.Logger) *table.Plu table.TextColumn("username_value"), } return tablewrapper.New(flags, slogger, "kolide_chrome_login_keychain", columns, c.generate, - tablewrapper.WithDescription("Saved login URLs and usernames from Chrome's Login Data database on macOS. Deprecated in favor of kolide_chrome_login_data_emails."), + tablewrapper.WithDescription("Saved login URLs and usernames from Chrome's Login Data database on macOS. Deprecated in favor of katc_chrome_login_data_emails."), ) } diff --git a/pkg/osquery/table/gdrive_sync.go b/pkg/osquery/table/gdrive_sync.go deleted file mode 100644 index e155bb43fc..0000000000 --- a/pkg/osquery/table/gdrive_sync.go +++ /dev/null @@ -1,133 +0,0 @@ -package table - -import ( - "context" - "database/sql" - "fmt" - "log/slog" - "os" - "path/filepath" - - "github.com/kolide/kit/fsutil" - "github.com/kolide/launcher/v2/ee/agent" - "github.com/kolide/launcher/v2/ee/agent/types" - "github.com/kolide/launcher/v2/ee/observability" - "github.com/kolide/launcher/v2/ee/tables/tablewrapper" - "github.com/osquery/osquery-go/plugin/table" - _ "modernc.org/sqlite" -) - -func GDriveSyncConfig(flags types.Flags, slogger *slog.Logger) *table.Plugin { - g := &gdrive{ - slogger: slogger.With("table", "kolide_gdrive_sync_config"), - } - - columns := []table.ColumnDefinition{ - table.TextColumn("user_email"), - table.TextColumn("local_sync_root_path"), - } - return tablewrapper.New(flags, slogger, "kolide_gdrive_sync_config", columns, g.generate, - tablewrapper.WithDescription("Google Drive sync configuration on macOS, including the syncing user's email and local sync root path. Useful for identifying Google Drive accounts and sync locations."), - ) -} - -type gdrive struct { - slogger *slog.Logger -} - -func (g *gdrive) generateForPath(ctx context.Context, path string) ([]map[string]string, error) { - dir, err := agent.MkdirTemp("kolide_gdrive_sync_config") - if err != nil { - return nil, fmt.Errorf("creating kolide_gdrive_sync_config tmp dir: %w", err) - } - defer os.RemoveAll(dir) // clean up - - dst := filepath.Join(dir, "tmpfile") - if err := fsutil.CopyFile(path, dst); err != nil { - return nil, fmt.Errorf("copying sqlite db to tmp dir: %w", err) - } - - db, err := sql.Open("sqlite", dst) - if err != nil { - return nil, fmt.Errorf("connecting to sqlite db: %w", err) - } - defer db.Close() - - db.ExecContext(ctx, "PRAGMA journal_mode=WAL;") - - rows, err := db.QueryContext(ctx, - `SELECT entry_key, data_value - FROM data - WHERE entry_key = 'user_email' OR entry_key='local_sync_root_path' - AND data_value IS NOT NULL`) - if err != nil { - return nil, fmt.Errorf("query rows from gdrive sync config db: %w", err) - } - defer func() { - if err := rows.Close(); err != nil { - g.slogger.Log(ctx, slog.LevelWarn, - "closing rows after scanning results", - "err", err, - ) - } - if err := rows.Err(); err != nil { - g.slogger.Log(ctx, slog.LevelWarn, - "encountered iteration error", - "err", err, - ) - } - }() - - var email string - var localsyncpath string - for rows.Next() { - var ( - entryKey string - dataValue string - ) - if err := rows.Scan(&entryKey, &dataValue); err != nil { - return nil, fmt.Errorf("scanning gdrive sync config db row: %w", err) - } - - switch entryKey { - case "user_email": - email = dataValue - case "local_sync_root_path": - localsyncpath = dataValue - default: - continue - } - } - return []map[string]string{ - { - "user_email": email, - "local_sync_root_path": localsyncpath, - }, - }, nil -} - -func (g *gdrive) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { - ctx, span := observability.StartSpan(ctx, "table_name", "kolide_gdrive_sync_config") - defer span.End() - - files, err := findFileInUserDirs("/Library/Application Support/Google/Drive/user_default/sync_config.db", g.slogger) - if err != nil { - return nil, fmt.Errorf("find gdrive sync config sqlite DBs: %w", err) - } - - var results []map[string]string - for _, file := range files { - res, err := g.generateForPath(ctx, file.path) - if err != nil { - g.slogger.Log(ctx, slog.LevelInfo, - "generating gdrive sync result", - "path", file.path, - "err", err, - ) - continue - } - results = append(results, res...) - } - - return results, nil -} diff --git a/pkg/osquery/table/gdrive_sync_history.go b/pkg/osquery/table/gdrive_sync_history.go deleted file mode 100644 index a014b4f60a..0000000000 --- a/pkg/osquery/table/gdrive_sync_history.go +++ /dev/null @@ -1,128 +0,0 @@ -package table - -import ( - "context" - "database/sql" - "fmt" - "log/slog" - "os" - "path/filepath" - - "github.com/kolide/kit/fsutil" - "github.com/kolide/launcher/v2/ee/agent" - "github.com/kolide/launcher/v2/ee/agent/types" - "github.com/kolide/launcher/v2/ee/observability" - "github.com/kolide/launcher/v2/ee/tables/tablewrapper" - "github.com/osquery/osquery-go/plugin/table" - _ "modernc.org/sqlite" -) - -func GDriveSyncHistoryInfo(flags types.Flags, slogger *slog.Logger) *table.Plugin { - g := &GDriveSyncHistory{ - slogger: slogger.With("table", "kolide_gdrive_sync_history"), - } - columns := []table.ColumnDefinition{ - table.TextColumn("inode"), - table.TextColumn("filename"), - table.TextColumn("mtime"), - table.TextColumn("size"), - } - return tablewrapper.New(flags, slogger, "kolide_gdrive_sync_history", columns, g.generate, - tablewrapper.WithDescription("Google Drive file sync history on macOS, including filenames, inodes, modification times, and sizes. Useful for auditing which files have been synced via Google Drive."), - ) -} - -type GDriveSyncHistory struct { - slogger *slog.Logger -} - -// GDriveSyncHistoryGenerate will be called whenever the table is queried. It should return -// a full table scan. -func (g *GDriveSyncHistory) generateForPath(ctx context.Context, path string) ([]map[string]string, error) { - dir, err := agent.MkdirTemp("kolide_gdrive_sync_history") - if err != nil { - return nil, fmt.Errorf("creating kolide_gdrive_sync_history tmp dir: %w", err) - } - defer os.RemoveAll(dir) // clean up - - dst := filepath.Join(dir, "tmpfile") - if err := fsutil.CopyFile(path, dst); err != nil { - return nil, fmt.Errorf("copying sqlite db to tmp dir: %w", err) - } - - db, err := sql.Open("sqlite", dst) - if err != nil { - return nil, fmt.Errorf("connecting to sqlite db: %w", err) - } - defer db.Close() - - db.ExecContext(ctx, "PRAGMA journal_mode=WAL;") - - rows, err := db.QueryContext(ctx, "select distinct le.inode, le.filename, le.modified AS mtime, le.size from local_entry le, cloud_entry ce using (checksum) order by le.modified desc;") - if err != nil { - return nil, fmt.Errorf("query rows from gdrive sync history db: %w", err) - } - defer func() { - if err := rows.Close(); err != nil { - g.slogger.Log(ctx, slog.LevelWarn, - "closing rows after scanning results", - "err", err, - ) - } - if err := rows.Err(); err != nil { - g.slogger.Log(ctx, slog.LevelWarn, - "encountered iteration error", - "err", err, - ) - } - }() - - var results []map[string]string - - // loop through all the sqlite rows and add them as osquery rows in the results map - for rows.Next() { // we initialize these variables for every row, that way we don't have data from the previous iteration - var inode string - var filename string - var mtime string - var size string - if err := rows.Scan(&inode, &filename, &mtime, &size); err != nil { - return nil, fmt.Errorf("scanning gdrive sync history db row: %w", err) - } - - results = append(results, map[string]string{ - "inode": inode, - "filename": filename, - "mtime": mtime, - "size": size, - }) - } - return results, nil -} - -// GDriveSyncHistoryGenerate will be called whenever the table is queried. It should return -// a full table scan. -func (g *GDriveSyncHistory) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { - ctx, span := observability.StartSpan(ctx, "table_name", "kolide_gdrive_sync_history") - defer span.End() - - files, err := findFileInUserDirs("Library/Application Support/Google/Drive/user_default/snapshot.db", g.slogger) - if err != nil { - return nil, fmt.Errorf("find gdrive sync history sqlite DBs: %w", err) - } - - var results []map[string]string - for _, file := range files { - res, err := g.generateForPath(ctx, file.path) - if err != nil { - g.slogger.Log(ctx, slog.LevelInfo, - "generating gdrive history result", - "path", file.path, - "err", err, - ) - continue - } - results = append(results, res...) - } - - return results, nil -} diff --git a/pkg/osquery/table/gdrive_sync_history_test.go b/pkg/osquery/table/gdrive_sync_history_test.go deleted file mode 100644 index a06e7a6fb2..0000000000 --- a/pkg/osquery/table/gdrive_sync_history_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package table - -import ( - "database/sql" - "fmt" - "os" - "path/filepath" - "runtime" - "testing" - "time" - - typesmocks "github.com/kolide/launcher/v2/ee/agent/types/mocks" - "github.com/kolide/launcher/v2/pkg/log/multislogger" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestGDriveSyncHistoryInfo(t *testing.T) { //nolint:paralleltest // We need to update package-level vars in this test - // Set up table dependencies - mockFlags := typesmocks.NewFlags(t) - mockFlags.On("TableGenerateTimeout").Return(1 * time.Minute) - mockFlags.On("RegisterChangeObserver", mock.Anything, mock.Anything).Return() - slogger := multislogger.NewNopLogger() - - // Set up a sqlite database for querying - testUsername := "testusername" - tempHomeDir := t.TempDir() - tempUserHomeDir := filepath.Join(tempHomeDir, testUsername) - require.NoError(t, os.Mkdir(tempUserHomeDir, 0755)) - tempSqliteFilepath := filepath.Join(tempUserHomeDir, "Library/Application Support/Google/Drive/user_default/snapshot.db") - require.NoError(t, os.MkdirAll(filepath.Dir(tempSqliteFilepath), 0755)) - f, err := os.Create(tempSqliteFilepath) - require.NoError(t, err) - f.Close() - db, err := sql.Open("sqlite", tempSqliteFilepath) - require.NoError(t, err) - _, err = db.ExecContext(t.Context(), `CREATE TABLE IF NOT EXISTS local_entry (inode TEXT, filename TEXT, modified TEXT, size TEXT, checksum TEXT);`) - require.NoError(t, err) - _, err = db.ExecContext(t.Context(), `INSERT INTO local_entry (inode, filename, modified, size, checksum) VALUES ("", "testfile", "", "", "");`) - require.NoError(t, err) - _, err = db.ExecContext(t.Context(), `CREATE TABLE IF NOT EXISTS cloud_entry (inode TEXT, filename TEXT, modified TEXT, size TEXT, checksum TEXT);`) - require.NoError(t, err) - _, err = db.ExecContext(t.Context(), `INSERT INTO cloud_entry (inode, filename, modified, size, checksum) VALUES ("", "testfile", "", "", "");`) - require.NoError(t, err) - require.NoError(t, db.Close()) - - // Point the table to this new db by modifying package vars - homeDirLocations[runtime.GOOS] = append(homeDirLocations[runtime.GOOS], tempHomeDir) - - // Create table and verify the name is what we expect - gdriveHistoryTable := GDriveSyncHistoryInfo(mockFlags, slogger) - require.Equal(t, "kolide_gdrive_sync_history", gdriveHistoryTable.Name()) - - // Confirm we can call the table successfully - response := gdriveHistoryTable.Call(t.Context(), map[string]string{ - "action": "generate", - "context": "{}", - }) - require.Equal(t, int32(0), response.Status.Code, response.Status.Message) // 0 means success - testFileFound := false - for _, row := range response.Response { - if fileName, ok := row["filename"]; ok { - if fileName == "testfile" { - testFileFound = true - break - } - } - } - require.True(t, testFileFound, fmt.Sprintf("response did not include testfile: %+v", response)) -} diff --git a/pkg/osquery/table/gdrive_sync_test.go b/pkg/osquery/table/gdrive_sync_test.go deleted file mode 100644 index 270f33faee..0000000000 --- a/pkg/osquery/table/gdrive_sync_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package table - -import ( - "database/sql" - "fmt" - "os" - "path/filepath" - "runtime" - "testing" - "time" - - typesmocks "github.com/kolide/launcher/v2/ee/agent/types/mocks" - "github.com/kolide/launcher/v2/pkg/log/multislogger" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestGDriveSyncConfig(t *testing.T) { //nolint:paralleltest // We need to update package-level vars in this test - // Set up table dependencies - mockFlags := typesmocks.NewFlags(t) - mockFlags.On("TableGenerateTimeout").Return(1 * time.Minute) - mockFlags.On("RegisterChangeObserver", mock.Anything, mock.Anything).Return() - slogger := multislogger.NewNopLogger() - - // Set up a sqlite database for querying - testUsername := "testusername" - tempHomeDir := t.TempDir() - tempUserHomeDir := filepath.Join(tempHomeDir, testUsername) - require.NoError(t, os.Mkdir(tempUserHomeDir, 0755)) - tempSqliteFilepath := filepath.Join(tempUserHomeDir, "/Library/Application Support/Google/Drive/user_default/sync_config.db") - require.NoError(t, os.MkdirAll(filepath.Dir(tempSqliteFilepath), 0755)) - f, err := os.Create(tempSqliteFilepath) - require.NoError(t, err) - f.Close() - db, err := sql.Open("sqlite", tempSqliteFilepath) - require.NoError(t, err) - _, err = db.ExecContext(t.Context(), `CREATE TABLE IF NOT EXISTS data (entry_key TEXT, data_value TEXT);`) - require.NoError(t, err) - _, err = db.ExecContext(t.Context(), `INSERT INTO data (entry_key, data_value) VALUES ("user_email", "testusername@example.com"), ("local_sync_root_path", "test");`) - require.NoError(t, err) - require.NoError(t, db.Close()) - - // Point the table to this new db by modifying package vars - homeDirLocations[runtime.GOOS] = append(homeDirLocations[runtime.GOOS], tempHomeDir) - - // Create table and verify the name is what we expect - gdriveTable := GDriveSyncConfig(mockFlags, slogger) - require.Equal(t, "kolide_gdrive_sync_config", gdriveTable.Name()) - - // Confirm we can call the table successfully - response := gdriveTable.Call(t.Context(), map[string]string{ - "action": "generate", - "context": "{}", - }) - require.Equal(t, int32(0), response.Status.Code, response.Status.Message) // 0 means success - testUserFound := false - for _, row := range response.Response { - if userEmail, ok := row["user_email"]; ok { - if userEmail == "testusername@example.com" { - testUserFound = true - break - } - } - } - require.True(t, testUserFound, fmt.Sprintf("response did not include testusername@example.com: %+v", response)) -} diff --git a/pkg/osquery/table/macho.go b/pkg/osquery/table/macho.go index 46d6965f0a..a331072bce 100644 --- a/pkg/osquery/table/macho.go +++ b/pkg/osquery/table/macho.go @@ -4,7 +4,9 @@ import ( "context" "debug/macho" "errors" + "fmt" "log/slog" + "path/filepath" "strings" "github.com/kolide/launcher/v2/ee/agent/types" @@ -34,23 +36,40 @@ func generateMacho(ctx context.Context, queryContext table.QueryContext) ([]map[ return nil, errors.New("The kolide_macho_info table requires that you specify a constraint WHERE path =") } path := q.Constraints[0].Expression - f, err := macho.Open(path) + + f, thinErr := macho.Open(path) + // macho.Open only handles thin binaries. When it succeeds, return that + // single architecture before falling back to macho.OpenFat. + if thinErr == nil { + defer f.Close() + + return []map[string]string{machoResult(path, f.Cpu.String())}, nil + } + + fatFile, err := macho.OpenFat(path) if err != nil { - return nil, err + return nil, fmt.Errorf("opening Mach-O binary: %w", thinErr) + } + defer fatFile.Close() + + results := make([]map[string]string, 0, len(fatFile.Arches)) + for _, arch := range fatFile.Arches { + results = append(results, machoResult(path, arch.File.Cpu.String())) } - defer f.Close() - var results []map[string]string - results = append(results, map[string]string{ + return results, nil +} + +func machoResult(path, cpu string) map[string]string { + return map[string]string{ "path": path, "name": appFromPath(path), - "cpu": f.Cpu.String(), - }) - return results, nil + "cpu": cpu, + } } func appFromPath(path string) string { - parts := strings.SplitSeq(path, "/") + parts := strings.SplitSeq(filepath.ToSlash(path), "/") for part := range parts { if strings.HasSuffix(part, ".app") { return part diff --git a/pkg/osquery/table/macho_test.go b/pkg/osquery/table/macho_test.go new file mode 100644 index 0000000000..0664a1097f --- /dev/null +++ b/pkg/osquery/table/macho_test.go @@ -0,0 +1,92 @@ +package table + +import ( + "debug/macho" + "encoding/binary" + "os" + "path/filepath" + "testing" + + "github.com/kolide/launcher/v2/ee/tables/tablehelpers" + "github.com/stretchr/testify/require" +) + +func TestGenerateMacho(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + contents []byte + wantCpus []string + }{ + { + name: "thin binary", + contents: thinMachoHeader(macho.CpuArm64), + wantCpus: []string{"CpuArm64"}, + }, + { + name: "universal binary", + contents: fatMachoFile(macho.CpuAmd64, macho.CpuArm64), + wantCpus: []string{"CpuAmd64", "CpuArm64"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "Example.app", "Contents", "MacOS", "example") + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0755)) + require.NoError(t, os.WriteFile(path, tt.contents, 0600)) + + results, err := generateMacho(t.Context(), tablehelpers.MockQueryContext(map[string][]string{ + "path": {path}, + })) + require.NoError(t, err) + require.Len(t, results, len(tt.wantCpus)) + + cpus := make([]string, 0, len(results)) + for _, result := range results { + require.Equal(t, path, result["path"]) + require.Equal(t, "Example.app", result["name"]) + cpus = append(cpus, result["cpu"]) + } + require.ElementsMatch(t, tt.wantCpus, cpus) + }) + } +} + +func thinMachoHeader(cpu macho.Cpu) []byte { + const macho64HeaderSize = 32 + + header := make([]byte, macho64HeaderSize) + binary.LittleEndian.PutUint32(header[0:4], macho.Magic64) + binary.LittleEndian.PutUint32(header[4:8], uint32(cpu)) + binary.LittleEndian.PutUint32(header[12:16], uint32(macho.TypeExec)) + + return header +} + +func fatMachoFile(cpus ...macho.Cpu) []byte { + const ( + fatHeaderSize = 8 + fatArchSize = 20 + machoHeaderLen = 32 + ) + + firstMachoOffset := fatHeaderSize + len(cpus)*fatArchSize + contents := make([]byte, firstMachoOffset+len(cpus)*machoHeaderLen) + binary.BigEndian.PutUint32(contents[0:4], macho.MagicFat) + binary.BigEndian.PutUint32(contents[4:8], uint32(len(cpus))) + + for i, cpu := range cpus { + archOffset := fatHeaderSize + i*fatArchSize + machoOffset := firstMachoOffset + i*machoHeaderLen + binary.BigEndian.PutUint32(contents[archOffset:archOffset+4], uint32(cpu)) + binary.BigEndian.PutUint32(contents[archOffset+8:archOffset+12], uint32(machoOffset)) + binary.BigEndian.PutUint32(contents[archOffset+12:archOffset+16], machoHeaderLen) + copy(contents[machoOffset:], thinMachoHeader(cpu)) + } + + return contents +} diff --git a/pkg/osquery/table/platform_tables_darwin.go b/pkg/osquery/table/platform_tables_darwin.go index 752ce070ba..6ef92e0580 100644 --- a/pkg/osquery/table/platform_tables_darwin.go +++ b/pkg/osquery/table/platform_tables_darwin.go @@ -98,8 +98,6 @@ func platformSpecificTables(k types.Knapsack, slogger *slog.Logger, currentOsque brew_upgradeable.TablePlugin(k, slogger), ChromeLoginKeychainInfo(k, slogger), firmwarepasswd.TablePlugin(k, slogger), - GDriveSyncConfig(k, slogger), - GDriveSyncHistoryInfo(k, slogger), MDMInfo(k, slogger), macos_software_update.MacOSUpdate(k, slogger), macos_software_update.RecommendedUpdates(k, slogger), diff --git a/pkg/osquery/table/table.go b/pkg/osquery/table/table.go index 16e28e78a1..10f56a9dc5 100644 --- a/pkg/osquery/table/table.go +++ b/pkg/osquery/table/table.go @@ -57,7 +57,6 @@ func LauncherTables(k types.Knapsack, slogger *slog.Logger) []osquery.OsqueryPlu func PlatformTables(k types.Knapsack, enrollmentId string, slogger *slog.Logger, currentOsquerydBinaryPath string) []osquery.OsqueryPlugin { // Common tables to all platforms tables := []osquery.OsqueryPlugin{ - ChromeLoginDataEmails(k, slogger), ChromeUserProfiles(k, slogger), KeyInfo(k, slogger), OnePasswordAccounts(k, slogger), diff --git a/pkg/osquery/table/table_test.go b/pkg/osquery/table/table_test.go new file mode 100644 index 0000000000..972730426d --- /dev/null +++ b/pkg/osquery/table/table_test.go @@ -0,0 +1,11 @@ +package table + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +}