From afe51ab3fd79404adf40049d7a84065470dc5b5f Mon Sep 17 00:00:00 2001 From: James Pickett Date: Mon, 22 Apr 2024 11:42:07 -0700 Subject: [PATCH 1/9] write atc config to DB, auto load in interactive --- ee/agent/startupsettings/writer.go | 42 +++++++++++++ pkg/launcher/paths.go | 10 +++- pkg/osquery/extension.go | 8 +-- pkg/osquery/interactive/interactive.go | 66 ++++++++++++++++++--- pkg/osquery/interactive/interactive_test.go | 8 +++ 5 files changed, 122 insertions(+), 12 deletions(-) diff --git a/ee/agent/startupsettings/writer.go b/ee/agent/startupsettings/writer.go index a9f2116a2..154d0d6b2 100644 --- a/ee/agent/startupsettings/writer.go +++ b/ee/agent/startupsettings/writer.go @@ -5,12 +5,14 @@ package startupsettings import ( "context" + "encoding/json" "fmt" "log/slog" "github.com/kolide/launcher/ee/agent/flags/keys" agentsqlite "github.com/kolide/launcher/ee/agent/storage/sqlite" "github.com/kolide/launcher/ee/agent/types" + "github.com/kolide/launcher/pkg/osquery" "github.com/kolide/launcher/pkg/traces" ) @@ -63,6 +65,18 @@ func (s *startupSettingsWriter) setFlags() error { } updatedFlags["use_tuf_autoupdater"] = "enabled" // Hardcode for backwards compatibility circa v1.5.3 + // Set flags will only be called when a flag value changes. The osquery config that contains the atc config + // comes in via osquery extension. So a new config will not trigger a re-write. + atcConfig, err := s.extractAutoTableConstructionConfig() + if err != nil { + s.knapsack.Slogger().Log(context.TODO(), slog.LevelDebug, + "could not extract auto_table_construction config", + "err", err, + ) + } else { + updatedFlags["auto_table_construction"] = atcConfig + } + if _, err := s.kvStore.Update(updatedFlags); err != nil { return fmt.Errorf("updating flags: %w", err) } @@ -85,3 +99,31 @@ func (s *startupSettingsWriter) FlagsChanged(flagKeys ...keys.FlagKey) { func (s *startupSettingsWriter) Close() error { return s.kvStore.Close() } + +func (s *startupSettingsWriter) extractAutoTableConstructionConfig() (string, error) { + osqConfig, err := s.knapsack.ConfigStore().Get([]byte(osquery.ConfigKey)) + if err != nil { + return "", fmt.Errorf("could not get osquery config from store: %w", err) + } + + // convert osquery config to map + var configUnmarshalled map[string]json.RawMessage + if err := json.Unmarshal(osqConfig, &configUnmarshalled); err != nil { + return "", fmt.Errorf("could not unmarshal osquery config: %w", err) + } + + // delete what we don't want + for k := range configUnmarshalled { + if k == "auto_table_construction" { + continue + } + delete(configUnmarshalled, k) + } + + atcJson, err := json.Marshal(configUnmarshalled) + if err != nil { + return "", fmt.Errorf("could not marshal auto_table_construction: %w", err) + } + + return string(atcJson), nil +} diff --git a/pkg/launcher/paths.go b/pkg/launcher/paths.go index 46b8a3229..6398b3d01 100644 --- a/pkg/launcher/paths.go +++ b/pkg/launcher/paths.go @@ -1,6 +1,7 @@ package launcher import ( + "os" "path/filepath" "runtime" ) @@ -56,7 +57,14 @@ func DefaultPath(path defaultPath) string { // not windows switch path { case RootDirectory: - return "/var/kolide-k2/k2device.kolide.com/" + const defaultRootDir = "/var/kolide-k2/k2device.kolide.com" + + // see if default root dir exists, if not assume it's a preprod install + if _, err := os.Stat(defaultRootDir); err != nil { + return "/var/kolide-k2/k2device-preprod.kolide.com" + } + + return defaultRootDir case EtcDirectory: return "/etc/kolide-k2/" case BinDirectory: diff --git a/pkg/osquery/extension.go b/pkg/osquery/extension.go index 10e722c3f..7e41d3143 100644 --- a/pkg/osquery/extension.go +++ b/pkg/osquery/extension.go @@ -51,7 +51,7 @@ const ( // DB key for node key nodeKeyKey = "nodeKey" // DB key for last retrieved config - configKey = "config" + ConfigKey = "config" // DB keys for the rsa keys privateKeyKey = "privateKey" @@ -332,7 +332,7 @@ func NodeKey(getter types.Getter) (string, error) { // Config returns the device config from the storage layer func Config(getter types.Getter) (string, error) { - key, err := getter.Get([]byte(configKey)) + key, err := getter.Get([]byte(ConfigKey)) if err != nil { return "", fmt.Errorf("error getting config key: %w", err) } @@ -504,7 +504,7 @@ func (e *Extension) GenerateConfigs(ctx context.Context) (map[string]string, err ) // Try to use cached config var confBytes []byte - confBytes, _ = e.knapsack.ConfigStore().Get([]byte(configKey)) + confBytes, _ = e.knapsack.ConfigStore().Get([]byte(ConfigKey)) if len(confBytes) == 0 { if !e.enrolled() { @@ -516,7 +516,7 @@ func (e *Extension) GenerateConfigs(ctx context.Context) (map[string]string, err config = string(confBytes) } else { // Store good config - e.knapsack.ConfigStore().Set([]byte(configKey), []byte(config)) + e.knapsack.ConfigStore().Set([]byte(ConfigKey), []byte(config)) // TODO log or record metrics when caching config fails? We // would probably like to return the config and not an error in // this case. diff --git a/pkg/osquery/interactive/interactive.go b/pkg/osquery/interactive/interactive.go index 874743a8d..ae2257ec7 100644 --- a/pkg/osquery/interactive/interactive.go +++ b/pkg/osquery/interactive/interactive.go @@ -1,24 +1,31 @@ package interactive import ( + "context" "fmt" "log/slog" "os" "path/filepath" "runtime" + "strings" "time" "github.com/kolide/kit/fsutil" + "github.com/kolide/launcher/ee/agent/startupsettings" "github.com/kolide/launcher/pkg/augeas" + "github.com/kolide/launcher/pkg/launcher" osqueryRuntime "github.com/kolide/launcher/pkg/osquery/runtime" "github.com/kolide/launcher/pkg/osquery/table" osquery "github.com/osquery/osquery-go" + "github.com/osquery/osquery-go/plugin/config" ) -const extensionName = "com.kolide.launcher_interactive" +const ( + extensionName = "com.kolide.launcher_interactive" + defaultConfigPluginName = "interactive_config" +) func StartProcess(slogger *slog.Logger, rootDir, osquerydPath string, osqueryFlags []string) (*os.Process, *osquery.ExtensionManagerServer, error) { - if err := os.MkdirAll(rootDir, fsutil.DirMode); err != nil { return nil, nil, fmt.Errorf("creating root dir for interactive mode: %w", err) } @@ -37,6 +44,35 @@ func StartProcess(slogger *slog.Logger, rootDir, osquerydPath string, osqueryFla } } + // check to see if a config flag path was given, + // we need to check this before loading the default config plugin, + // passing 2 configs to osquery will result in an error + haveConfigPathOsqFlag := false + for _, flag := range osqueryFlags { + if strings.HasPrefix(flag, "config_path") { + haveConfigPathOsqFlag = true + break + } + } + + // start building list of osq plugins with the kolide tables + osqPlugins := table.PlatformTables(slogger, osquerydPath) + + // if we were not provided a config path flag, try to add default config + if !haveConfigPathOsqFlag { + // check to see if we can actually get a config plugin + configPlugin, err := configPlugin() + if err != nil { + slogger.Log(context.TODO(), slog.LevelDebug, + "error creating config plugin", + "err", err, + ) + } else { + osqPlugins = append(osqPlugins, configPlugin) + osqueryFlags = append(osqueryFlags, fmt.Sprintf("config_plugin=%s", defaultConfigPluginName)) + } + } + proc, err := os.StartProcess(osquerydPath, buildOsqueryFlags(socketPath, augeasLensesPath, osqueryFlags), &os.ProcAttr{ // Transfer stdin, stdout, and stderr to the new process Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, @@ -46,7 +82,7 @@ func StartProcess(slogger *slog.Logger, rootDir, osquerydPath string, osqueryFla return nil, nil, fmt.Errorf("error starting osqueryd in interactive mode: %w", err) } - // while developing for windows it was found that it will sometimes take osquey a while + // while developing for windows it was found that it will sometimes take osquery a while // to create the socket, so we wait for it to exist before continuing if err := waitForFile(socketPath, time.Second/4, time.Second*10); err != nil { @@ -58,7 +94,7 @@ func StartProcess(slogger *slog.Logger, rootDir, osquerydPath string, osqueryFla return nil, nil, fmt.Errorf("error waiting for osquery to create socket: %w", err) } - extensionServer, err := loadExtensions(slogger, socketPath, osquerydPath) + extensionServer, err := loadExtensions(socketPath, osqPlugins...) if err != nil { err = fmt.Errorf("error loading extensions: %w", err) @@ -74,7 +110,6 @@ func StartProcess(slogger *slog.Logger, rootDir, osquerydPath string, osqueryFla } func buildOsqueryFlags(socketPath, augeasLensesPath string, osqueryFlags []string) []string { - // putting "-S" (the interactive flag) first because the behavior is inconsistent // when it's in the middle, found this during development on M1 macOS monterey 12.4 // ~James Pickett 07/05/2022 @@ -100,7 +135,7 @@ func buildOsqueryFlags(socketPath, augeasLensesPath string, osqueryFlags []strin return flags } -func loadExtensions(slogger *slog.Logger, socketPath string, osquerydPath string) (*osquery.ExtensionManagerServer, error) { +func loadExtensions(socketPath string, plugins ...osquery.OsqueryPlugin) (*osquery.ExtensionManagerServer, error) { client, err := osquery.NewClient(socketPath, 10*time.Second, osquery.MaxWaitTime(10*time.Second)) if err != nil { return nil, fmt.Errorf("error creating osquery client: %w", err) @@ -117,7 +152,7 @@ func loadExtensions(slogger *slog.Logger, socketPath string, osquerydPath string return extensionManagerServer, fmt.Errorf("error creating extension manager server: %w", err) } - extensionManagerServer.RegisterPlugin(table.PlatformTables(slogger, osquerydPath)...) + extensionManagerServer.RegisterPlugin(plugins...) if err := extensionManagerServer.Start(); err != nil { return nil, fmt.Errorf("error starting extension manager server: %w", err) @@ -153,3 +188,20 @@ func waitForFile(path string, interval, timeout time.Duration) error { } } } + +func configPlugin() (*config.Plugin, error) { + r, err := startupsettings.OpenReader(context.TODO(), launcher.DefaultPath(launcher.RootDirectory)) + if err != nil { + return nil, fmt.Errorf("error opening startup settings reader: %w", err) + } + defer r.Close() + + atcConfig, err := r.Get("auto_table_construction") + if err != nil { + return nil, fmt.Errorf("error getting auto_table_construction from startup settings: %w", err) + } + + return config.NewPlugin(defaultConfigPluginName, func(ctx context.Context) (map[string]string, error) { + return map[string]string{defaultConfigPluginName: atcConfig}, nil + }), nil +} diff --git a/pkg/osquery/interactive/interactive_test.go b/pkg/osquery/interactive/interactive_test.go index 4022251d2..495c61440 100644 --- a/pkg/osquery/interactive/interactive_test.go +++ b/pkg/osquery/interactive/interactive_test.go @@ -15,6 +15,7 @@ import ( "testing" "github.com/kolide/kit/fsutil" + "github.com/kolide/kit/ulid" "github.com/kolide/launcher/pkg/log/multislogger" "github.com/kolide/launcher/pkg/packaging" "github.com/stretchr/testify/require" @@ -69,6 +70,13 @@ func TestProc(t *testing.T) { }, wantProc: true, }, + { + name: "config path", + osqueryFlags: []string{ + fmt.Sprintf("config_path=%s", ulid.New()), + }, + wantProc: true, + }, { name: "socket path too long, the name of the test causes the socket path to be to long to be created, resulting in timeout waiting for the socket", wantProc: false, From cbf8db7ecb482ad861371bacf964d294ab7df609 Mon Sep 17 00:00:00 2001 From: James Pickett Date: Mon, 22 Apr 2024 13:01:34 -0700 Subject: [PATCH 2/9] update writer tests to account for atc gathering --- ee/agent/startupsettings/writer_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ee/agent/startupsettings/writer_test.go b/ee/agent/startupsettings/writer_test.go index 3cb55d74c..6fa7fe21a 100644 --- a/ee/agent/startupsettings/writer_test.go +++ b/ee/agent/startupsettings/writer_test.go @@ -2,12 +2,17 @@ package startupsettings import ( "context" + "encoding/json" "testing" _ "github.com/golang-migrate/migrate/v4/database/sqlite" + "github.com/kolide/kit/ulid" "github.com/kolide/launcher/ee/agent/flags/keys" + "github.com/kolide/launcher/ee/agent/storage/inmemory" agentsqlite "github.com/kolide/launcher/ee/agent/storage/sqlite" typesmocks "github.com/kolide/launcher/ee/agent/types/mocks" + "github.com/kolide/launcher/pkg/log/multislogger" + "github.com/kolide/launcher/pkg/osquery" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" _ "modernc.org/sqlite" @@ -27,6 +32,8 @@ func TestOpenWriter_NewDatabase(t *testing.T) { k.On("UpdateChannel").Return(updateChannelVal) k.On("PinnedLauncherVersion").Return("") k.On("PinnedOsquerydVersion").Return("5.11.0") + k.On("ConfigStore").Return(inmemory.NewStore()) + k.On("Slogger").Return(multislogger.NewNopLogger()) // Set up storage db, which should create the database and set all flags s, err := OpenWriter(context.TODO(), k) @@ -85,6 +92,9 @@ func TestOpenWriter_DatabaseAlreadyExists(t *testing.T) { k.On("PinnedLauncherVersion").Return(pinnedLauncherVersion) k.On("PinnedOsquerydVersion").Return(pinnedOsquerydVersion) + k.On("ConfigStore").Return(inmemory.NewStore()) + k.On("Slogger").Return(multislogger.NewNopLogger()) + // Set up storage db, which should create the database and set all flags s, err := OpenWriter(context.TODO(), k) require.NoError(t, err, "expected no error setting up storage db") @@ -122,6 +132,17 @@ func TestFlagsChanged(t *testing.T) { pinnedOsquerydVersion := "5.3.2" k.On("PinnedOsquerydVersion").Return(pinnedOsquerydVersion).Once() + configStore := inmemory.NewStore() + configMap := map[string]any{ + "auto_table_construction": ulid.New(), + "something_else_not_important": ulid.New(), + } + configJson, err := json.Marshal(configMap) + require.NoError(t, err, "marshalling config map") + + configStore.Set([]byte(osquery.ConfigKey), configJson) + k.On("ConfigStore").Return(configStore) + // Set up storage db, which should create the database and set all flags s, err := OpenWriter(context.TODO(), k) require.NoError(t, err, "expected no error setting up storage db") From 487a7728b92a51ce2746a0677c7855e31d494fe3 Mon Sep 17 00:00:00 2001 From: James Pickett Date: Wed, 24 Apr 2024 09:10:26 -0700 Subject: [PATCH 3/9] extension opens writer to write new config, feedback --- ee/agent/startupsettings/writer.go | 3 +-- ee/agent/startupsettings/writer_test.go | 3 +-- pkg/osquery/extension.go | 21 +++++++++++++++++---- pkg/osquery/interactive/interactive.go | 4 ++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/ee/agent/startupsettings/writer.go b/ee/agent/startupsettings/writer.go index 154d0d6b2..4c6105346 100644 --- a/ee/agent/startupsettings/writer.go +++ b/ee/agent/startupsettings/writer.go @@ -12,7 +12,6 @@ import ( "github.com/kolide/launcher/ee/agent/flags/keys" agentsqlite "github.com/kolide/launcher/ee/agent/storage/sqlite" "github.com/kolide/launcher/ee/agent/types" - "github.com/kolide/launcher/pkg/osquery" "github.com/kolide/launcher/pkg/traces" ) @@ -101,7 +100,7 @@ func (s *startupSettingsWriter) Close() error { } func (s *startupSettingsWriter) extractAutoTableConstructionConfig() (string, error) { - osqConfig, err := s.knapsack.ConfigStore().Get([]byte(osquery.ConfigKey)) + osqConfig, err := s.knapsack.ConfigStore().Get([]byte("config")) if err != nil { return "", fmt.Errorf("could not get osquery config from store: %w", err) } diff --git a/ee/agent/startupsettings/writer_test.go b/ee/agent/startupsettings/writer_test.go index 6fa7fe21a..cf3167a42 100644 --- a/ee/agent/startupsettings/writer_test.go +++ b/ee/agent/startupsettings/writer_test.go @@ -12,7 +12,6 @@ import ( agentsqlite "github.com/kolide/launcher/ee/agent/storage/sqlite" typesmocks "github.com/kolide/launcher/ee/agent/types/mocks" "github.com/kolide/launcher/pkg/log/multislogger" - "github.com/kolide/launcher/pkg/osquery" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" _ "modernc.org/sqlite" @@ -140,7 +139,7 @@ func TestFlagsChanged(t *testing.T) { configJson, err := json.Marshal(configMap) require.NoError(t, err, "marshalling config map") - configStore.Set([]byte(osquery.ConfigKey), configJson) + configStore.Set([]byte("config"), configJson) k.On("ConfigStore").Return(configStore) // Set up storage db, which should create the database and set all flags diff --git a/pkg/osquery/extension.go b/pkg/osquery/extension.go index 7e41d3143..631937a3c 100644 --- a/pkg/osquery/extension.go +++ b/pkg/osquery/extension.go @@ -15,6 +15,7 @@ import ( "github.com/google/uuid" "github.com/kolide/launcher/ee/agent" + "github.com/kolide/launcher/ee/agent/startupsettings" "github.com/kolide/launcher/ee/agent/types" "github.com/kolide/launcher/pkg/backoff" "github.com/kolide/launcher/pkg/osquery/runtime/history" @@ -51,7 +52,7 @@ const ( // DB key for node key nodeKeyKey = "nodeKey" // DB key for last retrieved config - ConfigKey = "config" + configKey = "config" // DB keys for the rsa keys privateKeyKey = "privateKey" @@ -332,7 +333,7 @@ func NodeKey(getter types.Getter) (string, error) { // Config returns the device config from the storage layer func Config(getter types.Getter) (string, error) { - key, err := getter.Get([]byte(ConfigKey)) + key, err := getter.Get([]byte(configKey)) if err != nil { return "", fmt.Errorf("error getting config key: %w", err) } @@ -504,7 +505,7 @@ func (e *Extension) GenerateConfigs(ctx context.Context) (map[string]string, err ) // Try to use cached config var confBytes []byte - confBytes, _ = e.knapsack.ConfigStore().Get([]byte(ConfigKey)) + confBytes, _ = e.knapsack.ConfigStore().Get([]byte(configKey)) if len(confBytes) == 0 { if !e.enrolled() { @@ -516,7 +517,19 @@ func (e *Extension) GenerateConfigs(ctx context.Context) (map[string]string, err config = string(confBytes) } else { // Store good config - e.knapsack.ConfigStore().Set([]byte(ConfigKey), []byte(config)) + e.knapsack.ConfigStore().Set([]byte(configKey), []byte(config)) + + // open the start up settings writer just to trigger a write of the config, + // then we can immediately close it + startupSettingsWriter, err := startupsettings.OpenWriter(ctx, e.knapsack) + if err != nil { + e.slogger.Log(ctx, slog.LevelWarn, + "could not get startup settings writer", + "err", err, + ) + } else { + startupSettingsWriter.Close() + } // TODO log or record metrics when caching config fails? We // would probably like to return the config and not an error in // this case. diff --git a/pkg/osquery/interactive/interactive.go b/pkg/osquery/interactive/interactive.go index ae2257ec7..7506d26a8 100644 --- a/pkg/osquery/interactive/interactive.go +++ b/pkg/osquery/interactive/interactive.go @@ -61,7 +61,7 @@ func StartProcess(slogger *slog.Logger, rootDir, osquerydPath string, osqueryFla // if we were not provided a config path flag, try to add default config if !haveConfigPathOsqFlag { // check to see if we can actually get a config plugin - configPlugin, err := configPlugin() + configPlugin, err := generateConfigPlugin() if err != nil { slogger.Log(context.TODO(), slog.LevelDebug, "error creating config plugin", @@ -189,7 +189,7 @@ func waitForFile(path string, interval, timeout time.Duration) error { } } -func configPlugin() (*config.Plugin, error) { +func generateConfigPlugin() (*config.Plugin, error) { r, err := startupsettings.OpenReader(context.TODO(), launcher.DefaultPath(launcher.RootDirectory)) if err != nil { return nil, fmt.Errorf("error opening startup settings reader: %w", err) From 4824669dabfe87cc03e6c472b1436a8cf9c3198d Mon Sep 17 00:00:00 2001 From: James Pickett Date: Wed, 24 Apr 2024 09:34:02 -0700 Subject: [PATCH 4/9] fix extension config caching test --- pkg/osquery/extension_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/osquery/extension_test.go b/pkg/osquery/extension_test.go index d6bed35c8..65231c03e 100644 --- a/pkg/osquery/extension_test.go +++ b/pkg/osquery/extension_test.go @@ -58,6 +58,7 @@ func makeKnapsack(t *testing.T, db *bbolt.DB) types.Knapsack { m.On("ConfigStore").Return(storageci.NewStore(t, multislogger.NewNopLogger(), storage.ConfigStore.String())) m.On("Slogger").Return(multislogger.NewNopLogger()) m.On("ReadEnrollSecret").Maybe().Return("enroll_secret", nil) + m.On("RootDirectory").Maybe().Return("whatever") return m } From 6c966d08a66320bf6e67948b50388850a0f15957 Mon Sep 17 00:00:00 2001 From: James Pickett Date: Wed, 24 Apr 2024 14:24:47 -0700 Subject: [PATCH 5/9] update interactive to parse std launcher flags and use knapsack --- cmd/launcher/interactive.go | 51 +++++++++++---------- pkg/osquery/interactive/interactive.go | 26 +++++------ pkg/osquery/interactive/interactive_test.go | 10 +++- 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/cmd/launcher/interactive.go b/cmd/launcher/interactive.go index 337dd2a08..361f69954 100644 --- a/cmd/launcher/interactive.go +++ b/cmd/launcher/interactive.go @@ -3,7 +3,6 @@ package main import ( "context" "errors" - "flag" "fmt" "log/slog" "os" @@ -11,6 +10,9 @@ import ( "github.com/kolide/launcher/cmd/launcher/internal" "github.com/kolide/launcher/ee/agent" + "github.com/kolide/launcher/ee/agent/flags" + "github.com/kolide/launcher/ee/agent/knapsack" + "github.com/kolide/launcher/ee/agent/storage/inmemory" "github.com/kolide/launcher/ee/tuf" "github.com/kolide/launcher/pkg/autoupdate" "github.com/kolide/launcher/pkg/launcher" @@ -19,22 +21,17 @@ import ( ) func runInteractive(systemMultiSlogger *multislogger.MultiSlogger, args []string) error { - flagset := flag.NewFlagSet("interactive", flag.ExitOnError) - var ( - flDebug = flagset.Bool("debug", false, "enable debug logging") - flOsquerydPath = flagset.String("osqueryd_path", "", "The path to the oqueryd binary") - flOsqueryFlags launcher.ArrayFlags - ) - - flagset.Var(&flOsqueryFlags, "osquery_flag", "Flags to pass to osquery (possibly overriding Launcher defaults)") - - flagset.Usage = commandUsage(flagset, "launcher interactive") - if err := flagset.Parse(args); err != nil { + opts, err := launcher.ParseOptions("interactive", args) + if err != nil { return err } + if opts.RootDirectory == "" { + opts.RootDirectory = launcher.DefaultPath(launcher.RootDirectory) + } + slogLevel := slog.LevelInfo - if *flDebug { + if opts.Debug { slogLevel = slog.LevelDebug } @@ -44,36 +41,35 @@ func runInteractive(systemMultiSlogger *multislogger.MultiSlogger, args []string AddSource: true, })) - osquerydPath := *flOsquerydPath - if osquerydPath == "" { + if opts.OsquerydPath == "" { latestOsquerydBinary, err := tuf.CheckOutLatestWithoutConfig("osqueryd", systemMultiSlogger.Logger) if err != nil { - osquerydPath = launcher.FindOsquery() - if osquerydPath == "" { + opts.OsquerydPath = launcher.FindOsquery() + if opts.OsquerydPath == "" { return errors.New("could not find osqueryd binary") } // Fall back to old autoupdate library - osquerydPath = autoupdate.FindNewest(context.Background(), osquerydPath) + opts.OsquerydPath = autoupdate.FindNewest(context.Background(), opts.OsquerydPath) } else { - osquerydPath = latestOsquerydBinary.Path + opts.OsquerydPath = latestOsquerydBinary.Path } } // have to keep tempdir name short so we don't exceed socket length - rootDir, err := agent.MkdirTemp("launcher-interactive") + interactiveRootDir, err := agent.MkdirTemp("launcher-interactive") if err != nil { return fmt.Errorf("creating temp dir for interactive mode: %w", err) } defer func() { - if err := os.RemoveAll(rootDir); err != nil { + if err := os.RemoveAll(interactiveRootDir); err != nil { fmt.Printf("error removing launcher interactive temp dir: %s\n", err) } }() hasTlsServerCertsOsqueryFlag := false // check to see if we were passed a tls_server_certs flag - for _, v := range flOsqueryFlags { + for _, v := range opts.OsqueryFlags { if strings.HasPrefix(v, "tls_server_certs") { hasTlsServerCertsOsqueryFlag = true break @@ -82,15 +78,20 @@ func runInteractive(systemMultiSlogger *multislogger.MultiSlogger, args []string // if we were not passed a tls_server_certs flag, pass default to osquery if !hasTlsServerCertsOsqueryFlag { - certs, err := internal.InstallCaCerts(rootDir) + certs, err := internal.InstallCaCerts(interactiveRootDir) if err != nil { return fmt.Errorf("installing CA certs: %w", err) } - flOsqueryFlags = append(flOsqueryFlags, fmt.Sprintf("tls_server_certs=%s", certs)) + opts.OsqueryFlags = append(opts.OsqueryFlags, fmt.Sprintf("tls_server_certs=%s", certs)) } - osqueryProc, extensionsServer, err := interactive.StartProcess(systemMultiSlogger.Logger, rootDir, osquerydPath, flOsqueryFlags) + fcOpts := []flags.Option{flags.WithCmdLineOpts(opts)} + flagController := flags.NewFlagController(systemMultiSlogger.Logger, inmemory.NewStore(), fcOpts...) + + knapsack := knapsack.New(nil, flagController, nil, systemMultiSlogger, nil) + + osqueryProc, extensionsServer, err := interactive.StartProcess(knapsack, interactiveRootDir) if err != nil { return fmt.Errorf("error starting osqueryd: %s", err) } diff --git a/pkg/osquery/interactive/interactive.go b/pkg/osquery/interactive/interactive.go index 7506d26a8..d10262214 100644 --- a/pkg/osquery/interactive/interactive.go +++ b/pkg/osquery/interactive/interactive.go @@ -12,8 +12,8 @@ import ( "github.com/kolide/kit/fsutil" "github.com/kolide/launcher/ee/agent/startupsettings" + "github.com/kolide/launcher/ee/agent/types" "github.com/kolide/launcher/pkg/augeas" - "github.com/kolide/launcher/pkg/launcher" osqueryRuntime "github.com/kolide/launcher/pkg/osquery/runtime" "github.com/kolide/launcher/pkg/osquery/table" osquery "github.com/osquery/osquery-go" @@ -25,13 +25,13 @@ const ( defaultConfigPluginName = "interactive_config" ) -func StartProcess(slogger *slog.Logger, rootDir, osquerydPath string, osqueryFlags []string) (*os.Process, *osquery.ExtensionManagerServer, error) { - if err := os.MkdirAll(rootDir, fsutil.DirMode); err != nil { +func StartProcess(knapsack types.Knapsack, interactiveRootDir string) (*os.Process, *osquery.ExtensionManagerServer, error) { + if err := os.MkdirAll(interactiveRootDir, fsutil.DirMode); err != nil { return nil, nil, fmt.Errorf("creating root dir for interactive mode: %w", err) } - socketPath := osqueryRuntime.SocketPath(rootDir) - augeasLensesPath := filepath.Join(rootDir, "augeas-lenses") + socketPath := osqueryRuntime.SocketPath(interactiveRootDir) + augeasLensesPath := filepath.Join(interactiveRootDir, "augeas-lenses") // only install augeas lenses on non-windows platforms if runtime.GOOS != "windows" { @@ -48,7 +48,7 @@ func StartProcess(slogger *slog.Logger, rootDir, osquerydPath string, osqueryFla // we need to check this before loading the default config plugin, // passing 2 configs to osquery will result in an error haveConfigPathOsqFlag := false - for _, flag := range osqueryFlags { + for _, flag := range knapsack.OsqueryFlags() { if strings.HasPrefix(flag, "config_path") { haveConfigPathOsqFlag = true break @@ -56,14 +56,15 @@ func StartProcess(slogger *slog.Logger, rootDir, osquerydPath string, osqueryFla } // start building list of osq plugins with the kolide tables - osqPlugins := table.PlatformTables(slogger, osquerydPath) + osqPlugins := table.PlatformTables(knapsack.Slogger(), knapsack.OsquerydPath()) + osqueryFlags := knapsack.OsqueryFlags() // if we were not provided a config path flag, try to add default config if !haveConfigPathOsqFlag { // check to see if we can actually get a config plugin - configPlugin, err := generateConfigPlugin() + configPlugin, err := generateConfigPlugin(knapsack.RootDirectory()) if err != nil { - slogger.Log(context.TODO(), slog.LevelDebug, + knapsack.Slogger().Log(context.TODO(), slog.LevelDebug, "error creating config plugin", "err", err, ) @@ -73,7 +74,7 @@ func StartProcess(slogger *slog.Logger, rootDir, osquerydPath string, osqueryFla } } - proc, err := os.StartProcess(osquerydPath, buildOsqueryFlags(socketPath, augeasLensesPath, osqueryFlags), &os.ProcAttr{ + proc, err := os.StartProcess(knapsack.OsquerydPath(), buildOsqueryFlags(socketPath, augeasLensesPath, osqueryFlags), &os.ProcAttr{ // Transfer stdin, stdout, and stderr to the new process Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, }) @@ -85,7 +86,6 @@ func StartProcess(slogger *slog.Logger, rootDir, osquerydPath string, osqueryFla // while developing for windows it was found that it will sometimes take osquery a while // to create the socket, so we wait for it to exist before continuing if err := waitForFile(socketPath, time.Second/4, time.Second*10); err != nil { - procKillErr := proc.Kill() if procKillErr != nil { err = fmt.Errorf("error killing osqueryd interactive: %s: %w", procKillErr, err) @@ -189,8 +189,8 @@ func waitForFile(path string, interval, timeout time.Duration) error { } } -func generateConfigPlugin() (*config.Plugin, error) { - r, err := startupsettings.OpenReader(context.TODO(), launcher.DefaultPath(launcher.RootDirectory)) +func generateConfigPlugin(launcherDaemonRootDir string) (*config.Plugin, error) { + r, err := startupsettings.OpenReader(context.TODO(), launcherDaemonRootDir) if err != nil { return nil, fmt.Errorf("error opening startup settings reader: %w", err) } diff --git a/pkg/osquery/interactive/interactive_test.go b/pkg/osquery/interactive/interactive_test.go index 495c61440..d865aeb5e 100644 --- a/pkg/osquery/interactive/interactive_test.go +++ b/pkg/osquery/interactive/interactive_test.go @@ -16,6 +16,7 @@ import ( "github.com/kolide/kit/fsutil" "github.com/kolide/kit/ulid" + "github.com/kolide/launcher/ee/agent/types/mocks" "github.com/kolide/launcher/pkg/log/multislogger" "github.com/kolide/launcher/pkg/packaging" "github.com/stretchr/testify/require" @@ -90,9 +91,14 @@ func TestProc(t *testing.T) { rootDir := t.TempDir() require.NoError(t, downloadOsquery(rootDir)) - osquerydPath := filepath.Join(rootDir, "osqueryd") - proc, _, err := StartProcess(multislogger.NewNopLogger(), rootDir, osquerydPath, tt.osqueryFlags) + mockSack := mocks.NewKnapsack(t) + mockSack.On("OsquerydPath").Return(filepath.Join(rootDir, "osqueryd")) + mockSack.On("OsqueryFlags").Return(tt.osqueryFlags) + mockSack.On("Slogger").Return(multislogger.NewNopLogger()) + mockSack.On("RootDirectory").Maybe().Return("whatever_the_root_launcher_dir_is") + + proc, _, err := StartProcess(mockSack, rootDir) if tt.errContainsStr != "" { require.Error(t, err) From 3d5aa29f5c532dc6e61c973af1de5488361a9997 Mon Sep 17 00:00:00 2001 From: James Pickett Date: Thu, 25 Apr 2024 07:54:26 -0700 Subject: [PATCH 6/9] add comments to clarify the root dir vs the interactive root dir --- cmd/launcher/interactive.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/launcher/interactive.go b/cmd/launcher/interactive.go index 361f69954..0dff355a9 100644 --- a/cmd/launcher/interactive.go +++ b/cmd/launcher/interactive.go @@ -26,6 +26,8 @@ func runInteractive(systemMultiSlogger *multislogger.MultiSlogger, args []string return err } + // here we are looking for the launcher "proper" root directory so that we know where + // to find the kv.sqlite where we can pull the auto table construction config from if opts.RootDirectory == "" { opts.RootDirectory = launcher.DefaultPath(launcher.RootDirectory) } @@ -55,7 +57,8 @@ func runInteractive(systemMultiSlogger *multislogger.MultiSlogger, args []string } } - // have to keep tempdir name short so we don't exceed socket length + // this is a tmp root directory that launcher can use to store files it needs to run + // such as the osquery socket and augeas lense files interactiveRootDir, err := agent.MkdirTemp("launcher-interactive") if err != nil { return fmt.Errorf("creating temp dir for interactive mode: %w", err) From cdfb5530b87ce245bb01c7e4ec9e1addc820b65a Mon Sep 17 00:00:00 2001 From: James Pickett Date: Fri, 26 Apr 2024 07:43:06 -0700 Subject: [PATCH 7/9] makes writing startup settings explict call instead of happening in constructor --- cmd/launcher/launcher.go | 7 +++++++ ee/agent/startupsettings/writer.go | 17 ++++++----------- ee/agent/startupsettings/writer_test.go | 6 ++++++ pkg/osquery/extension.go | 11 +++++++++-- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/cmd/launcher/launcher.go b/cmd/launcher/launcher.go index 93d46309d..2302554d2 100644 --- a/cmd/launcher/launcher.go +++ b/cmd/launcher/launcher.go @@ -247,6 +247,13 @@ func runLauncher(ctx context.Context, cancel func(), multiSlogger, systemMultiSl } defer s.Close() + if err := s.WriteSettings(); err != nil { + slogger.Log(ctx, slog.LevelError, + "writing startup settings", + "err", err, + ) + } + // If we have successfully opened the DB, and written a pid, // we expect we're live. Record the version for osquery to // pickup diff --git a/ee/agent/startupsettings/writer.go b/ee/agent/startupsettings/writer.go index 4c6105346..836dcdca3 100644 --- a/ee/agent/startupsettings/writer.go +++ b/ee/agent/startupsettings/writer.go @@ -44,11 +44,6 @@ func OpenWriter(ctx context.Context, knapsack types.Knapsack) (*startupSettingsW }, } - // Attempt to ensure flags are up-to-date - if err := s.setFlags(); err != nil { - s.knapsack.Slogger().Log(ctx, slog.LevelWarn, "could not set flags", "err", err) - } - for k := range s.storedFlags { s.knapsack.RegisterChangeObserver(s, k) } @@ -56,8 +51,8 @@ func OpenWriter(ctx context.Context, knapsack types.Knapsack) (*startupSettingsW return s, nil } -// setFlags updates the flags with their values from the agent flag data store. -func (s *startupSettingsWriter) setFlags() error { +// WriteSettings updates the flags with their values from the agent flag data store. +func (s *startupSettingsWriter) WriteSettings() error { updatedFlags := make(map[string]string) for flag, getter := range s.storedFlags { updatedFlags[flag.String()] = getter() @@ -69,7 +64,7 @@ func (s *startupSettingsWriter) setFlags() error { atcConfig, err := s.extractAutoTableConstructionConfig() if err != nil { s.knapsack.Slogger().Log(context.TODO(), slog.LevelDebug, - "could not extract auto_table_construction config", + "extracting auto_table_construction config", "err", err, ) } else { @@ -77,7 +72,7 @@ func (s *startupSettingsWriter) setFlags() error { } if _, err := s.kvStore.Update(updatedFlags); err != nil { - return fmt.Errorf("updating flags: %w", err) + return fmt.Errorf("writing settings: %w", err) } return nil @@ -87,9 +82,9 @@ func (s *startupSettingsWriter) setFlags() error { // that the startup database is registered for has a new value, the startup database // stores that updated value. func (s *startupSettingsWriter) FlagsChanged(flagKeys ...keys.FlagKey) { - if err := s.setFlags(); err != nil { + if err := s.WriteSettings(); err != nil { s.knapsack.Slogger().Log(context.Background(), slog.LevelError, - "could not set flags after change", + "writing startup settings after flag change", "err", err, ) } diff --git a/ee/agent/startupsettings/writer_test.go b/ee/agent/startupsettings/writer_test.go index cf3167a42..23c4178ab 100644 --- a/ee/agent/startupsettings/writer_test.go +++ b/ee/agent/startupsettings/writer_test.go @@ -38,6 +38,8 @@ func TestOpenWriter_NewDatabase(t *testing.T) { s, err := OpenWriter(context.TODO(), k) require.NoError(t, err, "expected no error setting up storage db") + require.NoError(t, s.WriteSettings(), "should be able to writing settings") + // Check that all values were set v1, err := s.kvStore.Get([]byte(keys.UpdateChannel.String())) require.NoError(t, err, "getting startup value") @@ -98,6 +100,8 @@ func TestOpenWriter_DatabaseAlreadyExists(t *testing.T) { s, err := OpenWriter(context.TODO(), k) require.NoError(t, err, "expected no error setting up storage db") + require.NoError(t, s.WriteSettings(), "should be able to writing settings") + // Now check that all values were updated v1, err = s.kvStore.Get([]byte(keys.UpdateChannel.String())) require.NoError(t, err, "getting startup value") @@ -146,6 +150,8 @@ func TestFlagsChanged(t *testing.T) { s, err := OpenWriter(context.TODO(), k) require.NoError(t, err, "expected no error setting up storage db") + require.NoError(t, s.WriteSettings(), "should be able to writing settings") + // Check that all values were set v1, err := s.kvStore.Get([]byte(keys.UpdateChannel.String())) require.NoError(t, err, "getting startup value") diff --git a/pkg/osquery/extension.go b/pkg/osquery/extension.go index 631937a3c..34e35a57b 100644 --- a/pkg/osquery/extension.go +++ b/pkg/osquery/extension.go @@ -523,12 +523,19 @@ func (e *Extension) GenerateConfigs(ctx context.Context) (map[string]string, err // then we can immediately close it startupSettingsWriter, err := startupsettings.OpenWriter(ctx, e.knapsack) if err != nil { - e.slogger.Log(ctx, slog.LevelWarn, + e.slogger.Log(ctx, slog.LevelError, "could not get startup settings writer", "err", err, ) } else { - startupSettingsWriter.Close() + defer startupSettingsWriter.Close() + + if err := startupSettingsWriter.WriteSettings(); err != nil { + e.slogger.Log(ctx, slog.LevelError, + "writing startup settings", + "err", err, + ) + } } // TODO log or record metrics when caching config fails? We // would probably like to return the config and not an error in From 4378ee7333b5bb0448f8aeb7cc40892722ee0d54 Mon Sep 17 00:00:00 2001 From: James Pickett Date: Fri, 26 Apr 2024 12:11:52 -0700 Subject: [PATCH 8/9] improve write test --- ee/agent/startupsettings/writer_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ee/agent/startupsettings/writer_test.go b/ee/agent/startupsettings/writer_test.go index 23c4178ab..b813fd233 100644 --- a/ee/agent/startupsettings/writer_test.go +++ b/ee/agent/startupsettings/writer_test.go @@ -3,6 +3,7 @@ package startupsettings import ( "context" "encoding/json" + "fmt" "testing" _ "github.com/golang-migrate/migrate/v4/database/sqlite" @@ -135,9 +136,11 @@ func TestFlagsChanged(t *testing.T) { pinnedOsquerydVersion := "5.3.2" k.On("PinnedOsquerydVersion").Return(pinnedOsquerydVersion).Once() + autoTableConstructionValue := ulid.New() + configStore := inmemory.NewStore() configMap := map[string]any{ - "auto_table_construction": ulid.New(), + "auto_table_construction": autoTableConstructionValue, "something_else_not_important": ulid.New(), } configJson, err := json.Marshal(configMap) @@ -165,6 +168,10 @@ func TestFlagsChanged(t *testing.T) { require.NoError(t, err, "getting startup value") require.Equal(t, pinnedOsquerydVersion, string(v3), "incorrect flag value") + v4, err := s.kvStore.Get([]byte("auto_table_construction")) + require.NoError(t, err, "getting startup value") + require.Equal(t, fmt.Sprintf("{\"auto_table_construction\":\"%s\"}", autoTableConstructionValue), string(v4), "incorrect config value") + // Now, prepare for flag changes newFlagValue := "alpha" k.On("UpdateChannel").Return(newFlagValue).Once() From f7cece76bbdb4d81be587b9a781f098a5b699058 Mon Sep 17 00:00:00 2001 From: James Pickett Date: Fri, 26 Apr 2024 15:08:15 -0700 Subject: [PATCH 9/9] remove stale comment --- ee/agent/startupsettings/writer.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/ee/agent/startupsettings/writer.go b/ee/agent/startupsettings/writer.go index 836dcdca3..aeb3b04d3 100644 --- a/ee/agent/startupsettings/writer.go +++ b/ee/agent/startupsettings/writer.go @@ -59,8 +59,6 @@ func (s *startupSettingsWriter) WriteSettings() error { } updatedFlags["use_tuf_autoupdater"] = "enabled" // Hardcode for backwards compatibility circa v1.5.3 - // Set flags will only be called when a flag value changes. The osquery config that contains the atc config - // comes in via osquery extension. So a new config will not trigger a re-write. atcConfig, err := s.extractAutoTableConstructionConfig() if err != nil { s.knapsack.Slogger().Log(context.TODO(), slog.LevelDebug,