diff --git a/ee/agent/flags/flag_controller.go b/ee/agent/flags/flag_controller.go index 8c4a7f13b..96776209f 100644 --- a/ee/agent/flags/flag_controller.go +++ b/ee/agent/flags/flag_controller.go @@ -10,6 +10,7 @@ import ( "github.com/kolide/launcher/ee/agent/flags/keys" "github.com/kolide/launcher/ee/agent/types" + "github.com/kolide/launcher/ee/tuf" "github.com/kolide/launcher/pkg/autoupdate" "github.com/kolide/launcher/pkg/launcher" "golang.org/x/exp/maps" @@ -486,6 +487,38 @@ func (fc *FlagController) UpdateDirectory() string { ).get(fc.getControlServerValue(keys.UpdateDirectory)) } +func (fc *FlagController) SetPinnedLauncherVersion(version string) error { + return fc.setControlServerValue(keys.PinnedLauncherVersion, []byte(version)) +} +func (fc *FlagController) PinnedLauncherVersion() string { + fc.overrideMutex.RLock() + defer fc.overrideMutex.RUnlock() + + return NewStringFlagValue( + WithOverrideString(fc.overrides[keys.PinnedLauncherVersion]), + WithDefaultString(""), + WithSanitizer(func(version string) string { + return tuf.SanitizePinnedVersion("launcher", version) + }), + ).get(fc.getControlServerValue(keys.PinnedLauncherVersion)) +} + +func (fc *FlagController) SetPinnedOsquerydVersion(version string) error { + return fc.setControlServerValue(keys.PinnedOsquerydVersion, []byte(version)) +} +func (fc *FlagController) PinnedOsquerydVersion() string { + fc.overrideMutex.RLock() + defer fc.overrideMutex.RUnlock() + + return NewStringFlagValue( + WithOverrideString(fc.overrides[keys.PinnedOsquerydVersion]), + WithDefaultString(""), + WithSanitizer(func(version string) string { + return tuf.SanitizePinnedVersion("osqueryd", version) + }), + ).get(fc.getControlServerValue(keys.PinnedOsquerydVersion)) +} + func (fc *FlagController) SetExportTraces(enabled bool) error { return fc.setControlServerValue(keys.ExportTraces, boolToBytes(enabled)) } diff --git a/ee/agent/flags/keys/keys.go b/ee/agent/flags/keys/keys.go index 839cd8d86..4291c1abb 100644 --- a/ee/agent/flags/keys/keys.go +++ b/ee/agent/flags/keys/keys.go @@ -44,6 +44,8 @@ const ( UpdateChannel FlagKey = "update_channel" AutoupdateInitialDelay FlagKey = "autoupdater_initial_delay" UpdateDirectory FlagKey = "update_directory" + PinnedLauncherVersion FlagKey = "pinned_launcher_version" + PinnedOsquerydVersion FlagKey = "pinned_osqueryd_version" ExportTraces FlagKey = "export_traces" TraceSamplingRate FlagKey = "trace_sampling_rate" TraceBatchTimeout FlagKey = "trace_batch_timeout" diff --git a/ee/agent/knapsack/knapsack.go b/ee/agent/knapsack/knapsack.go index f5cd274ff..31d0163a7 100644 --- a/ee/agent/knapsack/knapsack.go +++ b/ee/agent/knapsack/knapsack.go @@ -147,7 +147,7 @@ func (k *knapsack) getKVStore(storeType storage.Store) types.KVStore { } func (k *knapsack) LatestOsquerydPath(ctx context.Context) string { - latestBin, err := tuf.CheckOutLatest(ctx, "osqueryd", k.RootDirectory(), k.UpdateDirectory(), k.UpdateChannel(), k.Slogger()) + latestBin, err := tuf.CheckOutLatest(ctx, "osqueryd", k.RootDirectory(), k.UpdateDirectory(), k.PinnedOsquerydVersion(), k.UpdateChannel(), k.Slogger()) if err != nil { return autoupdate.FindNewest(ctx, k.OsquerydPath()) } diff --git a/ee/agent/startupsettings/writer.go b/ee/agent/startupsettings/writer.go index 3b96af25b..ccf4fb4e6 100644 --- a/ee/agent/startupsettings/writer.go +++ b/ee/agent/startupsettings/writer.go @@ -37,12 +37,14 @@ func OpenWriter(ctx context.Context, knapsack types.Knapsack) (*startupSettingsW kvStore: store, knapsack: knapsack, storedFlags: map[keys.FlagKey]func() string{ - keys.UpdateChannel: func() string { return knapsack.UpdateChannel() }, + keys.UpdateChannel: func() string { return knapsack.UpdateChannel() }, + keys.PinnedLauncherVersion: func() string { return knapsack.PinnedLauncherVersion() }, + keys.PinnedOsquerydVersion: func() string { return knapsack.PinnedOsquerydVersion() }, }, } // Attempt to ensure flags are up-to-date - if err := s.setFlags(ctx); err != nil { + if err := s.setFlags(); err != nil { s.knapsack.Slogger().Log(ctx, slog.LevelWarn, "could not set flags", "err", err) } @@ -54,7 +56,7 @@ func OpenWriter(ctx context.Context, knapsack types.Knapsack) (*startupSettingsW } // setFlags updates the flags with their values from the agent flag data store. -func (s *startupSettingsWriter) setFlags(ctx context.Context) error { +func (s *startupSettingsWriter) setFlags() error { updatedFlags := make(map[string]string) for flag, getter := range s.storedFlags { updatedFlags[flag.String()] = getter() @@ -71,7 +73,7 @@ func (s *startupSettingsWriter) setFlags(ctx context.Context) 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(context.Background()); err != nil { + if err := s.setFlags(); err != nil { s.knapsack.Slogger().Log(context.Background(), slog.LevelError, "could not set flags after change", "err", err, diff --git a/ee/agent/startupsettings/writer_test.go b/ee/agent/startupsettings/writer_test.go index 4d87a8a9c..039ac075b 100644 --- a/ee/agent/startupsettings/writer_test.go +++ b/ee/agent/startupsettings/writer_test.go @@ -21,8 +21,12 @@ func TestOpenWriter_NewDatabase(t *testing.T) { k := typesmocks.NewKnapsack(t) k.On("RootDirectory").Return(testRootDir) k.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel) + k.On("RegisterChangeObserver", mock.Anything, keys.PinnedLauncherVersion) + k.On("RegisterChangeObserver", mock.Anything, keys.PinnedOsquerydVersion) updateChannelVal := "stable" k.On("UpdateChannel").Return(updateChannelVal) + k.On("PinnedLauncherVersion").Return("") + k.On("PinnedOsquerydVersion").Return("5.11.0") // Set up storage db, which should create the database and set all flags s, err := OpenWriter(context.TODO(), k) @@ -44,22 +48,38 @@ func TestOpenWriter_DatabaseAlreadyExists(t *testing.T) { store, err := agentsqlite.OpenRW(context.TODO(), testRootDir, agentsqlite.StartupSettingsStore) require.NoError(t, err, "getting connection to test db") require.NoError(t, store.Set([]byte(keys.UpdateChannel.String()), []byte("some_old_value")), "setting key") + require.NoError(t, store.Set([]byte(keys.PinnedLauncherVersion.String()), []byte("")), "setting key") + require.NoError(t, store.Set([]byte(keys.PinnedOsquerydVersion.String()), []byte("")), "setting key") // Confirm flags were set v1, err := store.Get([]byte(keys.UpdateChannel.String())) require.NoError(t, err, "getting startup value") require.Equal(t, "some_old_value", string(v1), "incorrect flag value") + v2, err := store.Get([]byte(keys.PinnedLauncherVersion.String())) + require.NoError(t, err, "getting startup value") + require.Equal(t, "", string(v2), "incorrect flag value") + + v3, err := store.Get([]byte(keys.PinnedOsquerydVersion.String())) + require.NoError(t, err, "getting startup value") + require.Equal(t, "", string(v3), "incorrect flag value") + require.NoError(t, store.Close(), "closing setup connection") // Set up dependencies k := typesmocks.NewKnapsack(t) k.On("RootDirectory").Return(testRootDir) k.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel) + k.On("RegisterChangeObserver", mock.Anything, keys.PinnedLauncherVersion) + k.On("RegisterChangeObserver", mock.Anything, keys.PinnedOsquerydVersion) // Set up flag updateChannelVal := "alpha" + pinnedLauncherVersion := "1.6.0" + pinnedOsquerydVersion := "5.11.0" k.On("UpdateChannel").Return(updateChannelVal) + k.On("PinnedLauncherVersion").Return(pinnedLauncherVersion) + k.On("PinnedOsquerydVersion").Return(pinnedOsquerydVersion) // Set up storage db, which should create the database and set all flags s, err := OpenWriter(context.TODO(), k) @@ -70,6 +90,14 @@ func TestOpenWriter_DatabaseAlreadyExists(t *testing.T) { require.NoError(t, err, "getting startup value") require.Equal(t, updateChannelVal, string(v1), "incorrect flag value") + v2, err = s.kvStore.Get([]byte(keys.PinnedLauncherVersion.String())) + require.NoError(t, err, "getting startup value") + require.Equal(t, pinnedLauncherVersion, string(v2), "incorrect flag value") + + v3, err = s.kvStore.Get([]byte(keys.PinnedOsquerydVersion.String())) + require.NoError(t, err, "getting startup value") + require.Equal(t, pinnedOsquerydVersion, string(v3), "incorrect flag value") + require.NoError(t, s.Close(), "closing startup db") } @@ -81,8 +109,14 @@ func TestFlagsChanged(t *testing.T) { k := typesmocks.NewKnapsack(t) k.On("RootDirectory").Return(testRootDir) k.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel) + k.On("RegisterChangeObserver", mock.Anything, keys.PinnedLauncherVersion) + k.On("RegisterChangeObserver", mock.Anything, keys.PinnedOsquerydVersion) updateChannelVal := "beta" k.On("UpdateChannel").Return(updateChannelVal).Once() + pinnedLauncherVersion := "1.2.3" + k.On("PinnedLauncherVersion").Return(pinnedLauncherVersion).Once() + pinnedOsquerydVersion := "5.3.2" + k.On("PinnedOsquerydVersion").Return(pinnedOsquerydVersion).Once() // Set up storage db, which should create the database and set all flags s, err := OpenWriter(context.TODO(), k) @@ -93,9 +127,21 @@ func TestFlagsChanged(t *testing.T) { require.NoError(t, err, "getting startup value") require.Equal(t, updateChannelVal, string(v1), "incorrect flag value") + v2, err := s.kvStore.Get([]byte(keys.PinnedLauncherVersion.String())) + require.NoError(t, err, "getting startup value") + require.Equal(t, pinnedLauncherVersion, string(v2), "incorrect flag value") + + v3, err := s.kvStore.Get([]byte(keys.PinnedOsquerydVersion.String())) + require.NoError(t, err, "getting startup value") + require.Equal(t, pinnedOsquerydVersion, string(v3), "incorrect flag value") + // Now, prepare for flag changes newFlagValue := "alpha" k.On("UpdateChannel").Return(newFlagValue).Once() + newPinnedLauncherVersion := "" + k.On("PinnedLauncherVersion").Return(newPinnedLauncherVersion).Once() + newPinnedOsquerydVersion := "5.4.3" + k.On("PinnedOsquerydVersion").Return(newPinnedOsquerydVersion).Once() // Call FlagsChanged and expect that all flag values are updated s.FlagsChanged(keys.UpdateChannel) @@ -103,6 +149,14 @@ func TestFlagsChanged(t *testing.T) { require.NoError(t, err, "getting startup value") require.Equal(t, newFlagValue, string(v1), "incorrect flag value") + v2, err = s.kvStore.Get([]byte(keys.PinnedLauncherVersion.String())) + require.NoError(t, err, "getting startup value") + require.Equal(t, newPinnedLauncherVersion, string(v2), "incorrect flag value") + + v3, err = s.kvStore.Get([]byte(keys.PinnedOsquerydVersion.String())) + require.NoError(t, err, "getting startup value") + require.Equal(t, newPinnedOsquerydVersion, string(v3), "incorrect flag value") + require.NoError(t, s.Close(), "closing startup db") } diff --git a/ee/agent/types/flags.go b/ee/agent/types/flags.go index 8b93657a2..cbccb1899 100644 --- a/ee/agent/types/flags.go +++ b/ee/agent/types/flags.go @@ -173,6 +173,14 @@ type Flags interface { SetUpdateDirectory(directory string) error UpdateDirectory() string + // PinnedLauncherVersion is the launcher version to lock the autoupdater to, rather than autoupdating via the update channel. + SetPinnedLauncherVersion(version string) error + PinnedLauncherVersion() string + + // PinnedOsquerydVersion is the osqueryd version to lock the autoupdater to, rather than autoupdating via the update channel. + SetPinnedOsquerydVersion(version string) error + PinnedOsquerydVersion() string + // ExportTraces enables exporting our traces SetExportTraces(enabled bool) error SetExportTracesOverride(value bool, duration time.Duration) diff --git a/ee/agent/types/mocks/flags.go b/ee/agent/types/mocks/flags.go index 8255d52c1..dfad789b6 100644 --- a/ee/agent/types/mocks/flags.go +++ b/ee/agent/types/mocks/flags.go @@ -594,6 +594,34 @@ func (_m *Flags) OsquerydPath() string { return r0 } +// PinnedLauncherVersion provides a mock function with given fields: +func (_m *Flags) PinnedLauncherVersion() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// PinnedOsquerydVersion provides a mock function with given fields: +func (_m *Flags) PinnedOsquerydVersion() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // RegisterChangeObserver provides a mock function with given fields: observer, flagKeys func (_m *Flags) RegisterChangeObserver(observer types.FlagsChangeObserver, flagKeys ...keys.FlagKey) { _va := make([]interface{}, len(flagKeys)) @@ -999,6 +1027,34 @@ func (_m *Flags) SetOsqueryVerbose(verbose bool) error { return r0 } +// SetPinnedLauncherVersion provides a mock function with given fields: version +func (_m *Flags) SetPinnedLauncherVersion(version string) error { + ret := _m.Called(version) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(version) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetPinnedOsquerydVersion provides a mock function with given fields: version +func (_m *Flags) SetPinnedOsquerydVersion(version string) error { + ret := _m.Called(version) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(version) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // SetTraceBatchTimeout provides a mock function with given fields: duration func (_m *Flags) SetTraceBatchTimeout(duration time.Duration) error { ret := _m.Called(duration) diff --git a/ee/agent/types/mocks/knapsack.go b/ee/agent/types/mocks/knapsack.go index 8c0ac3e7c..8aa88efd2 100644 --- a/ee/agent/types/mocks/knapsack.go +++ b/ee/agent/types/mocks/knapsack.go @@ -756,6 +756,34 @@ func (_m *Knapsack) PersistentHostDataStore() types.GetterSetterDeleterIteratorU return r0 } +// PinnedLauncherVersion provides a mock function with given fields: +func (_m *Knapsack) PinnedLauncherVersion() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// PinnedOsquerydVersion provides a mock function with given fields: +func (_m *Knapsack) PinnedOsquerydVersion() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // ReadEnrollSecret provides a mock function with given fields: func (_m *Knapsack) ReadEnrollSecret() (string, error) { ret := _m.Called() @@ -1233,6 +1261,34 @@ func (_m *Knapsack) SetOsqueryVerbose(verbose bool) error { return r0 } +// SetPinnedLauncherVersion provides a mock function with given fields: version +func (_m *Knapsack) SetPinnedLauncherVersion(version string) error { + ret := _m.Called(version) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(version) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetPinnedOsquerydVersion provides a mock function with given fields: version +func (_m *Knapsack) SetPinnedOsquerydVersion(version string) error { + ret := _m.Called(version) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(version) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // SetTraceBatchTimeout provides a mock function with given fields: duration func (_m *Knapsack) SetTraceBatchTimeout(duration time.Duration) error { ret := _m.Called(duration) diff --git a/ee/tuf/autoupdate.go b/ee/tuf/autoupdate.go index d25f6815b..87a0b6bdc 100644 --- a/ee/tuf/autoupdate.go +++ b/ee/tuf/autoupdate.go @@ -16,6 +16,7 @@ import ( "path" "path/filepath" "runtime" + "slices" "strconv" "sync" "time" @@ -88,6 +89,9 @@ type TufAutoupdater struct { knapsack types.Knapsack store types.KVStore // stores autoupdater errors for kolide_tuf_autoupdater_errors table updateChannel string + pinnedVersions map[autoupdatableBinary]string // maps the binaries to their pinned versions + pinnedVersionGetters map[autoupdatableBinary]func() string // maps the binaries to the knapsack function to retrieve updated pinned versions + initialDelayLock *sync.Mutex updateLock *sync.Mutex interrupt chan struct{} interrupted bool @@ -113,11 +117,20 @@ func NewTufAutoupdater(ctx context.Context, k types.Knapsack, metadataHttpClient defer span.End() ta := &TufAutoupdater{ - knapsack: k, - interrupt: make(chan struct{}, 1), - signalRestart: make(chan error, 1), - store: k.AutoupdateErrorsStore(), - updateChannel: k.UpdateChannel(), + knapsack: k, + interrupt: make(chan struct{}, 1), + signalRestart: make(chan error, 1), + store: k.AutoupdateErrorsStore(), + updateChannel: k.UpdateChannel(), + pinnedVersions: map[autoupdatableBinary]string{ + binaryLauncher: k.PinnedLauncherVersion(), // empty string if not pinned + binaryOsqueryd: k.PinnedOsquerydVersion(), // ditto + }, + pinnedVersionGetters: map[autoupdatableBinary]func() string{ + binaryLauncher: func() string { return k.PinnedLauncherVersion() }, + binaryOsqueryd: func() string { return k.PinnedOsquerydVersion() }, + }, + initialDelayLock: &sync.Mutex{}, updateLock: &sync.Mutex{}, osquerier: osquerier, osquerierRetryInterval: 30 * time.Second, @@ -146,7 +159,7 @@ func NewTufAutoupdater(ctx context.Context, k types.Knapsack, metadataHttpClient } // Subscribe to changes in update-related flags - ta.knapsack.RegisterChangeObserver(ta, keys.UpdateChannel) + ta.knapsack.RegisterChangeObserver(ta, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion) return ta, nil } @@ -205,15 +218,18 @@ func DefaultLibraryDirectory(rootDirectory string) string { // we store them in. func (ta *TufAutoupdater) Execute() (err error) { // Delay startup, if initial delay is set + ta.initialDelayLock.Lock() // prevent updates during delay select { case <-ta.interrupt: ta.slogger.Log(context.TODO(), slog.LevelDebug, "received external interrupt during initial delay, stopping", ) + ta.initialDelayLock.Unlock() return nil case <-time.After(ta.knapsack.AutoupdateInitialDelay()): break } + ta.initialDelayLock.Unlock() // For now, tidy the library on startup. In the future, we will tidy the library // earlier, after version selection. @@ -265,6 +281,13 @@ func (ta *TufAutoupdater) Interrupt(_ error) { // Do satisfies the actionqueue.actor interface; it allows the control server to send // requests down to autoupdate immediately. func (ta *TufAutoupdater) Do(data io.Reader) error { + if !ta.initialDelayLock.TryLock() { + ta.slogger.Log(context.TODO(), slog.LevelWarn, + "received update request during initial delay", + ) + return errors.New("cannot perform update during initial delay") + } + var updateRequest controlServerAutoupdateRequest if err := json.NewDecoder(data).Decode(&updateRequest); err != nil { ta.slogger.Log(context.TODO(), slog.LevelWarn, @@ -321,24 +344,49 @@ func (ta *TufAutoupdater) Do(data io.Reader) error { // FlagsChanged satisfies the FlagsChangeObserver interface, allowing the autoupdater // to respond to changes to autoupdate-related settings. func (ta *TufAutoupdater) FlagsChanged(flagKeys ...keys.FlagKey) { - // No change -- this is the only setting we currently care about. - if ta.updateChannel == ta.knapsack.UpdateChannel() { + binariesToCheckForUpdate := make([]autoupdatableBinary, 0) + + // Check to see if update channel has changed + if ta.updateChannel != ta.knapsack.UpdateChannel() { + ta.slogger.Log(context.TODO(), slog.LevelInfo, + "control server sent down new update channel value", + "new_channel", ta.knapsack.UpdateChannel(), + "old_channel", ta.updateChannel, + ) + ta.updateChannel = ta.knapsack.UpdateChannel() + binariesToCheckForUpdate = append(binariesToCheckForUpdate, binaryLauncher, binaryOsqueryd) + } + + // Check to see if pinned versions have changed + for binary, currentPinnedVersion := range ta.pinnedVersions { + newPinnedVersion := ta.pinnedVersionGetters[binary]() + if currentPinnedVersion != newPinnedVersion { + ta.slogger.Log(context.TODO(), slog.LevelInfo, + "control server sent down new pinned version for binary", + "binary", binary, + "new_pinned_version", newPinnedVersion, + "old_pinned_version", currentPinnedVersion, + ) + ta.pinnedVersions[binary] = newPinnedVersion + if !slices.Contains(binariesToCheckForUpdate, binary) { + binariesToCheckForUpdate = append(binariesToCheckForUpdate, binary) + } + } + } + + // No updates, or we're in the initial delay + if len(binariesToCheckForUpdate) == 0 || !ta.initialDelayLock.TryLock() { return } - // Update channel has changed -- update it, then check to see if we - // need to switch versions - ta.slogger.Log(context.TODO(), slog.LevelInfo, - "control server sent down new update channel value", - "new_channel", ta.knapsack.UpdateChannel(), - "old_channel", ta.updateChannel, - ) - ta.updateChannel = ta.knapsack.UpdateChannel() - if err := ta.checkForUpdate(binaries); err != nil { + // At least one binary requires a recheck -- perform that now + if err := ta.checkForUpdate(binariesToCheckForUpdate); err != nil { ta.storeError(err) ta.slogger.Log(context.TODO(), slog.LevelError, - "error checking for update after switching update channels", - "new_channel", ta.updateChannel, + "error checking for update after autoupdate setting changed", + "update_channel", ta.updateChannel, + "pinned_launcher_version", ta.knapsack.PinnedLauncherVersion(), + "pinned_osqueryd_version", ta.knapsack.PinnedOsquerydVersion(), "err", err, ) } @@ -494,19 +542,19 @@ func (ta *TufAutoupdater) checkForUpdate(binariesToCheck []autoupdatableBinary) // downloadUpdate will download a new release for the given binary, if available from TUF // and not already downloaded. func (ta *TufAutoupdater) downloadUpdate(binary autoupdatableBinary, targets data.TargetFiles) (string, error) { - release, releaseMetadata, err := findRelease(context.Background(), binary, targets, ta.updateChannel) + target, targetMetadata, err := findTarget(context.Background(), binary, targets, ta.pinnedVersions[binary], ta.updateChannel, ta.slogger) if err != nil { - return "", fmt.Errorf("could not find release: %w", err) + return "", fmt.Errorf("could not find appropriate target: %w", err) } // Ensure we don't download duplicate versions var currentVersion string currentVersion, _ = ta.currentRunningVersion(binary) - if currentVersion == versionFromTarget(binary, release) { + if currentVersion == versionFromTarget(binary, target) { return "", nil } - if ta.libraryManager.Available(binary, release) { + if ta.libraryManager.Available(binary, target) { // The release is already available in the library but we don't know if we're running it -- // err on the side of not restarting. if currentVersion == "" { @@ -520,15 +568,57 @@ func (ta *TufAutoupdater) downloadUpdate(binary autoupdatableBinary, targets dat // The release is already available in the library and it's not our current running version -- // return the version to signal for a restart. - return release, nil + return target, nil } // We haven't yet downloaded this release -- download it - if err := ta.libraryManager.AddToLibrary(binary, currentVersion, release, releaseMetadata); err != nil { - return "", fmt.Errorf("could not add release %s for binary %s to library: %w", release, binary, err) + if err := ta.libraryManager.AddToLibrary(binary, currentVersion, target, targetMetadata); err != nil { + return "", fmt.Errorf("could not add target %s for binary %s to library: %w", target, binary, err) + } + + return target, nil +} + +// findTarget selects the appropriate target from `targets` for the given binary, using the pinned version (if set) +// and otherwise selecting the correct release for the given channel. +func findTarget(ctx context.Context, binary autoupdatableBinary, targets data.TargetFiles, pinnedVersion string, channel string, slogger *slog.Logger) (string, data.TargetFileMeta, error) { + ctx, span := traces.StartSpan(ctx) + defer span.End() + + if pinnedVersion != "" { + target, targetMetadata, err := findTargetByVersion(ctx, binary, targets, pinnedVersion) + if err == nil { + // Binary version found + return target, targetMetadata, nil + } + slogger.Log(ctx, slog.LevelWarn, + "could not find target for version, falling back to release version", + "pinned_version", pinnedVersion, + "binary", binary, + "err", err, + ) } - return release, nil + // Either there isn't a pinned version, or the pinned version couldn't be found -- + // find the release target for the given channel instead. + return findRelease(ctx, binary, targets, channel) +} + +// findTargetByVersion selects the appropriate target from `targets` for the given binary and version. +func findTargetByVersion(ctx context.Context, binary autoupdatableBinary, targets data.TargetFiles, binaryVersion string) (string, data.TargetFileMeta, error) { + _, span := traces.StartSpan(ctx) + defer span.End() + + targetNameForVersion := path.Join(string(binary), runtime.GOOS, PlatformArch(), fmt.Sprintf("%s-%s.tar.gz", binary, binaryVersion)) + + for targetName, target := range targets { + if targetName != targetNameForVersion { + continue + } + + return filepath.Base(targetName), target, nil + } + return "", data.TargetFileMeta{}, fmt.Errorf("could not find metadata for binary %s and version %s", binary, binaryVersion) } // findRelease checks the latest data from TUF (in `targets`) to see whether a new release diff --git a/ee/tuf/autoupdate_test.go b/ee/tuf/autoupdate_test.go index 3b17e7916..d75863d63 100644 --- a/ee/tuf/autoupdate_test.go +++ b/ee/tuf/autoupdate_test.go @@ -43,7 +43,9 @@ func TestNewTufAutoupdater(t *testing.T) { mockKnapsack.On("MirrorServerURL").Return("https://example.com") mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) mockKnapsack.On("UpdateChannel").Return("nightly") - mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("PinnedOsquerydVersion").Return("") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() _, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, newMockQuerier(t)) require.NoError(t, err, "could not initialize new TUF autoupdater") @@ -75,6 +77,8 @@ func TestExecute_launcherUpdate(t *testing.T) { mockKnapsack := typesmocks.NewKnapsack(t) mockKnapsack.On("RootDirectory").Return(testRootDir) mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("PinnedOsquerydVersion").Return("") mockKnapsack.On("AutoupdateInterval").Return(500 * time.Millisecond) mockKnapsack.On("AutoupdateInitialDelay").Return(0 * time.Second) mockKnapsack.On("AutoupdateErrorsStore").Return(s) @@ -82,7 +86,7 @@ func TestExecute_launcherUpdate(t *testing.T) { mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") mockKnapsack.On("LocalDevelopmentPath").Return("") - mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() mockQuerier := newMockQuerier(t) // Set logger so that we can capture output @@ -167,13 +171,15 @@ func TestExecute_osquerydUpdate(t *testing.T) { mockKnapsack := typesmocks.NewKnapsack(t) mockKnapsack.On("RootDirectory").Return(testRootDir) mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("PinnedOsquerydVersion").Return("") mockKnapsack.On("AutoupdateInterval").Return(100 * time.Millisecond) // Set the check interval to something short so we can make a couple requests to our test metadata server mockKnapsack.On("AutoupdateInitialDelay").Return(0 * time.Second) mockKnapsack.On("AutoupdateErrorsStore").Return(s) mockKnapsack.On("TufServerURL").Return(tufServerUrl) mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") - mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() mockQuerier := newMockQuerier(t) // Set logger so that we can capture output @@ -242,13 +248,15 @@ func TestExecute_downgrade(t *testing.T) { mockKnapsack := typesmocks.NewKnapsack(t) mockKnapsack.On("RootDirectory").Return(testRootDir) mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("PinnedOsquerydVersion").Return("") mockKnapsack.On("AutoupdateInterval").Return(100 * time.Millisecond) // Set the check interval to something short so we can make a couple requests to our test metadata server mockKnapsack.On("AutoupdateInitialDelay").Return(0 * time.Second) mockKnapsack.On("AutoupdateErrorsStore").Return(s) mockKnapsack.On("TufServerURL").Return(tufServerUrl) mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") - mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() mockQuerier := newMockQuerier(t) // Set logger so that we can capture output @@ -335,7 +343,9 @@ func TestExecute_withInitialDelay(t *testing.T) { mockKnapsack.On("UpdateDirectory").Return("") mockKnapsack.On("MirrorServerURL").Return("https://example.com") mockKnapsack.On("UpdateChannel").Return("nightly") - mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("PinnedOsquerydVersion").Return("") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() mockQuerier := newMockQuerier(t) // Set logger so that we can capture output @@ -402,7 +412,9 @@ func TestInterrupt_Multiple(t *testing.T) { mockKnapsack.On("MirrorServerURL").Return("https://example.com") mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) mockKnapsack.On("UpdateChannel").Return("nightly") - mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("PinnedOsquerydVersion").Return("") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() mockQuerier := newMockQuerier(t) // Set up autoupdater @@ -522,6 +534,8 @@ func TestDo(t *testing.T) { mockKnapsack := typesmocks.NewKnapsack(t) mockKnapsack.On("RootDirectory").Return(testRootDir) mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("PinnedOsquerydVersion").Return("") mockKnapsack.On("AutoupdateErrorsStore").Return(s) mockKnapsack.On("TufServerURL").Return(tufServerUrl) mockKnapsack.On("UpdateDirectory").Return("") @@ -529,7 +543,7 @@ func TestDo(t *testing.T) { mockKnapsack.On("LocalDevelopmentPath").Return("").Maybe() mockQuerier := newMockQuerier(t) mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) - mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() // Set up autoupdater autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier, WithOsqueryRestart(func() error { return nil })) @@ -585,10 +599,11 @@ func TestDo_HandlesSimultaneousUpdates(t *testing.T) { mockKnapsack := typesmocks.NewKnapsack(t) mockKnapsack.On("RootDirectory").Return(testRootDir) mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("PinnedOsquerydVersion").Return("") interval := 500 * time.Millisecond mockKnapsack.On("AutoupdateInterval").Return(interval) - initialDelay := 100 * time.Millisecond - mockKnapsack.On("AutoupdateInitialDelay").Return(initialDelay) + mockKnapsack.On("AutoupdateInitialDelay").Return(0 * time.Millisecond) mockKnapsack.On("AutoupdateErrorsStore").Return(s) mockKnapsack.On("TufServerURL").Return(tufServerUrl) mockKnapsack.On("UpdateDirectory").Return("") @@ -596,7 +611,7 @@ func TestDo_HandlesSimultaneousUpdates(t *testing.T) { mockKnapsack.On("LocalDevelopmentPath").Return("") mockQuerier := newMockQuerier(t) mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) - mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() // Set up autoupdater autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier, WithOsqueryRestart(func() error { return nil })) @@ -639,7 +654,7 @@ func TestDo_HandlesSimultaneousUpdates(t *testing.T) { // Start the autoupdater, then make the control server request go autoupdater.Execute() - time.Sleep(initialDelay) + time.Sleep(100 * time.Millisecond) require.NoError(t, autoupdater.Do(data), "expected no error making update request") // Give autoupdater a chance to run @@ -652,7 +667,82 @@ func TestDo_HandlesSimultaneousUpdates(t *testing.T) { mockKnapsack.AssertExpectations(t) } -func TestFlagsChanged(t *testing.T) { +func TestDo_WillNotExecuteDuringInitialDelay(t *testing.T) { + t.Parallel() + + testRootDir := t.TempDir() + testReleaseVersion := "1.5.0" + tufServerUrl, rootJson := tufci.InitRemoteTufServer(t, testReleaseVersion) + s := setupStorage(t) + mockKnapsack := typesmocks.NewKnapsack(t) + mockKnapsack.On("RootDirectory").Return(testRootDir) + mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("PinnedOsquerydVersion").Return("") + interval := 100 * time.Millisecond + mockKnapsack.On("AutoupdateInterval").Return(interval) + initialDelay := 1 * time.Second + mockKnapsack.On("AutoupdateInitialDelay").Return(initialDelay) + mockKnapsack.On("AutoupdateErrorsStore").Return(s) + mockKnapsack.On("TufServerURL").Return(tufServerUrl) + mockKnapsack.On("UpdateDirectory").Return("") + mockKnapsack.On("MirrorServerURL").Return("https://example.com") + mockQuerier := newMockQuerier(t) + mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() + + // Set up autoupdater + autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier, WithOsqueryRestart(func() error { return nil })) + require.NoError(t, err, "could not initialize new TUF autoupdater") + + // Update the metadata client with our test root JSON + require.NoError(t, autoupdater.metadataClient.Init(rootJson), "could not initialize metadata client with test root JSON") + + // Get metadata for each release + _, err = autoupdater.metadataClient.Update() + require.NoError(t, err, "could not update metadata client to fetch target metadata") + + // Set up expectations for Execute function: tidying library, checking for updates + mockLibraryManager := NewMocklibrarian(t) + autoupdater.libraryManager = mockLibraryManager + currentOsqueryVersion := "1.1.1" + mockQuerier.On("Query", mock.Anything).Return([]map[string]string{{"version": currentOsqueryVersion}}, nil) + mockLibraryManager.On("TidyLibrary", binaryOsqueryd, mock.Anything).Return().Once() + autoupdater.libraryManager = mockLibraryManager + for _, b := range binaries { + mockLibraryManager.On("Available", b, fmt.Sprintf("%s-%s.tar.gz", string(b), testReleaseVersion)).Return(true) + } + + // Prepare control server request + rawRequest, err := json.Marshal(controlServerAutoupdateRequest{ + BinariesToUpdate: []binaryToUpdate{ + { + Name: "launcher", + }, + { + Name: "osqueryd", + }, + }, + }) + require.NoError(t, err, "marshalling update request") + data := bytes.NewReader(rawRequest) + + // Start the autoupdater, then make the control server request right away, during the initial delay + go autoupdater.Execute() + time.Sleep(100 * time.Millisecond) + require.Error(t, autoupdater.Do(data), "expected error making request during initial delay") + + // Give autoupdater a chance to run + time.Sleep(initialDelay + interval) + + // Assert expectation that we added the expected `testReleaseVersion` to the updates library + mockLibraryManager.AssertExpectations(t) + + // Confirm we pulled all config items as expected + mockKnapsack.AssertExpectations(t) +} + +func TestFlagsChanged_UpdateChannelChanged(t *testing.T) { t.Parallel() testRootDir := t.TempDir() @@ -668,7 +758,9 @@ func TestFlagsChanged(t *testing.T) { mockKnapsack.On("LocalDevelopmentPath").Return("").Maybe() mockQuerier := newMockQuerier(t) mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) - mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("PinnedOsquerydVersion").Return("") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() // Start out on beta channel, then swap to nightly mockKnapsack.On("UpdateChannel").Return("beta").Once() @@ -709,6 +801,130 @@ func TestFlagsChanged(t *testing.T) { require.Equal(t, "nightly", autoupdater.updateChannel) } +func TestFlagsChanged_PinnedVersionChanged(t *testing.T) { + t.Parallel() + + testRootDir := t.TempDir() + pinnedOsquerydVersion := "5.11.0" + tufServerUrl, rootJson := tufci.InitRemoteTufServer(t, pinnedOsquerydVersion) + s := setupStorage(t) + mockKnapsack := typesmocks.NewKnapsack(t) + mockKnapsack.On("RootDirectory").Return(testRootDir) + mockKnapsack.On("AutoupdateErrorsStore").Return(s) + mockKnapsack.On("TufServerURL").Return(tufServerUrl) + mockKnapsack.On("UpdateDirectory").Return("") + mockKnapsack.On("MirrorServerURL").Return("https://example.com") + mockKnapsack.On("LocalDevelopmentPath").Return("").Maybe() + mockQuerier := newMockQuerier(t) + mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) + mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() + + // Start out with no pinned version, then set a pinned version + mockKnapsack.On("PinnedOsquerydVersion").Return("").Once() + mockKnapsack.On("PinnedOsquerydVersion").Return(pinnedOsquerydVersion) + + // Set up autoupdater + autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier, WithOsqueryRestart(func() error { return nil })) + require.NoError(t, err, "could not initialize new TUF autoupdater") + require.Equal(t, "", autoupdater.pinnedVersions[binaryOsqueryd]) + + // Update the metadata client with our test root JSON + require.NoError(t, autoupdater.metadataClient.Init(rootJson), "could not initialize metadata client with test root JSON") + + // Get metadata for each release + _, err = autoupdater.metadataClient.Update() + require.NoError(t, err, "could not update metadata client to fetch target metadata") + + // Expect that we attempt to update the library + mockLibraryManager := NewMocklibrarian(t) + autoupdater.libraryManager = mockLibraryManager + currentOsqueryVersion := "1.1.1" + mockQuerier.On("Query", mock.Anything).Return([]map[string]string{{"version": currentOsqueryVersion}}, nil) + mockLibraryManager.On("Available", binaryOsqueryd, fmt.Sprintf("%s-%s.tar.gz", binaryOsqueryd, pinnedOsquerydVersion)).Return(false) + mockLibraryManager.On("AddToLibrary", binaryOsqueryd, mock.Anything, mock.Anything, mock.Anything).Return(nil) + + // Notify that flags changed + autoupdater.FlagsChanged(keys.PinnedOsquerydVersion) + + // Assert expectation that we added the expected `testReleaseVersion` to the updates library + mockLibraryManager.AssertExpectations(t) + + // Confirm we pulled all config items as expected + mockKnapsack.AssertExpectations(t) + + // Confirm we have the current osqueryd version set + require.Equal(t, pinnedOsquerydVersion, autoupdater.pinnedVersions[binaryOsqueryd]) +} + +func TestFlagsChanged_DuringInitialDelay(t *testing.T) { + t.Parallel() + + testRootDir := t.TempDir() + testReleaseVersion := "1.5.0" + tufServerUrl, rootJson := tufci.InitRemoteTufServer(t, testReleaseVersion) + s := setupStorage(t) + mockKnapsack := typesmocks.NewKnapsack(t) + mockKnapsack.On("RootDirectory").Return(testRootDir) + mockKnapsack.On("UpdateChannel").Return("nightly") + mockKnapsack.On("PinnedOsquerydVersion").Return("") + interval := 100 * time.Millisecond + mockKnapsack.On("AutoupdateInterval").Return(interval) + initialDelay := 1 * time.Second + mockKnapsack.On("AutoupdateInitialDelay").Return(initialDelay) + mockKnapsack.On("AutoupdateErrorsStore").Return(s) + mockKnapsack.On("TufServerURL").Return(tufServerUrl) + mockKnapsack.On("UpdateDirectory").Return("") + mockKnapsack.On("MirrorServerURL").Return("https://example.com") + mockQuerier := newMockQuerier(t) + mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() + + // Start out with a pinned version, then unset the pinned version + pinnedLauncherVersion := "1.7.3" + mockKnapsack.On("PinnedLauncherVersion").Return(pinnedLauncherVersion).Once() + mockKnapsack.On("PinnedLauncherVersion").Return("") + + // Set up autoupdater + autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier, WithOsqueryRestart(func() error { return nil })) + require.NoError(t, err, "could not initialize new TUF autoupdater") + require.Equal(t, pinnedLauncherVersion, autoupdater.pinnedVersions[binaryLauncher]) + + // Update the metadata client with our test root JSON + require.NoError(t, autoupdater.metadataClient.Init(rootJson), "could not initialize metadata client with test root JSON") + + // Get metadata for each release + _, err = autoupdater.metadataClient.Update() + require.NoError(t, err, "could not update metadata client to fetch target metadata") + + // Set up expectations for Execute function: tidying library, checking for updates + mockLibraryManager := NewMocklibrarian(t) + autoupdater.libraryManager = mockLibraryManager + currentOsqueryVersion := "1.1.1" + mockQuerier.On("Query", mock.Anything).Return([]map[string]string{{"version": currentOsqueryVersion}}, nil) + mockLibraryManager.On("TidyLibrary", binaryOsqueryd, mock.Anything).Return().Once() + autoupdater.libraryManager = mockLibraryManager + for _, b := range binaries { + mockLibraryManager.On("Available", b, fmt.Sprintf("%s-%s.tar.gz", string(b), testReleaseVersion)).Return(true) + } + + // Start the autoupdater, then notify flag change right away, during the initial delay + go autoupdater.Execute() + time.Sleep(100 * time.Millisecond) + autoupdater.FlagsChanged(keys.PinnedLauncherVersion) + + // Confirm we unset the pinned launcher version + require.Equal(t, "", autoupdater.pinnedVersions[binaryLauncher]) + + // Give autoupdater a chance to run + time.Sleep(initialDelay + interval) + + // Confirm we didn't add anything to the library, and pulled all config items as expected + mockLibraryManager.AssertExpectations(t) + mockKnapsack.AssertExpectations(t) +} + func Test_currentRunningVersion_launcher_errorWhenVersionIsNotSet(t *testing.T) { t.Parallel() @@ -781,7 +997,9 @@ func Test_storeError(t *testing.T) { mockKnapsack.On("MirrorServerURL").Return("https://example.com") mockKnapsack.On("Slogger").Return(multislogger.NewNopLogger()) mockKnapsack.On("UpdateChannel").Return("nightly") - mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel).Return() + mockKnapsack.On("PinnedLauncherVersion").Return("") + mockKnapsack.On("PinnedOsquerydVersion").Return("") + mockKnapsack.On("RegisterChangeObserver", mock.Anything, keys.UpdateChannel, keys.PinnedLauncherVersion, keys.PinnedOsquerydVersion).Return() mockQuerier := newMockQuerier(t) autoupdater, err := NewTufAutoupdater(context.TODO(), mockKnapsack, http.DefaultClient, http.DefaultClient, mockQuerier) diff --git a/ee/tuf/ci/tuf_server.go b/ee/tuf/ci/tuf_server.go index 37ab962f8..35399aeed 100644 --- a/ee/tuf/ci/tuf_server.go +++ b/ee/tuf/ci/tuf_server.go @@ -18,6 +18,8 @@ import ( //go:embed testdata/*.tar.gz var testTarballs embed.FS +const NonReleaseVersion = "0.1.1" + func getTarballContents(t *testing.T, binary string) []byte { tarballName := fmt.Sprintf("testdata/%s_%s.tar.gz", runtime.GOOS, binary) @@ -58,7 +60,7 @@ func InitRemoteTufServer(t *testing.T, testReleaseVersion string) (tufServerURL // Create test binaries and release files per binary and per release channel for _, b := range []string{"osqueryd", "launcher"} { - for _, v := range []string{"0.1.1", "0.12.3-deadbeef", testReleaseVersion} { + for _, v := range []string{NonReleaseVersion, "0.12.3-deadbeef", testReleaseVersion} { binaryFileName := fmt.Sprintf("%s-%s.tar.gz", b, v) // Create a valid test binary -- an archive of an executable with the proper directory structure diff --git a/ee/tuf/library_lookup.go b/ee/tuf/library_lookup.go index d70403a2e..14e87d617 100644 --- a/ee/tuf/library_lookup.go +++ b/ee/tuf/library_lookup.go @@ -9,7 +9,6 @@ import ( "path/filepath" "strings" - "github.com/Masterminds/semver" "github.com/kolide/launcher/ee/agent/flags/keys" "github.com/kolide/launcher/ee/agent/startupsettings" "github.com/kolide/launcher/pkg/autoupdate" @@ -49,38 +48,45 @@ func CheckOutLatestWithoutConfig(binary autoupdatableBinary, slogger *slog.Logge } // Get update channel from startup settings - updateChannel, err := getUpdateChannelFromStartupSettings(ctx, cfg.rootDirectory) + pinnedVersion, updateChannel, err := getUpdateSettingsFromStartupSettings(ctx, binary, cfg.rootDirectory) if err != nil { slogger.Log(ctx, slog.LevelWarn, - "could not get update channel from startup settings, falling back to config value instead", - "config_update_channel", cfg.channel, + "could not get startup settings", "err", err, ) + } + // Default to config update channel if not set in startup settings + if updateChannel == "" { updateChannel = cfg.channel } - return CheckOutLatest(ctx, binary, cfg.rootDirectory, cfg.updateDirectory, updateChannel, slogger) + return CheckOutLatest(ctx, binary, cfg.rootDirectory, cfg.updateDirectory, pinnedVersion, updateChannel, slogger) } -// getUpdateChannelFromStartupSettings queries the startup settings database to fetch the desired -// update channel. This accounts for e.g. the control server sending down a particular value for -// the update channel, overriding the config file. -func getUpdateChannelFromStartupSettings(ctx context.Context, rootDirectory string) (string, error) { +// getUpdateSettingsFromStartupSettings queries the startup settings database to fetch the +// pinned version and update channel. This accounts for e.g. the control server sending down +// a particular value for the update channel, overriding the config file. +func getUpdateSettingsFromStartupSettings(ctx context.Context, binary autoupdatableBinary, rootDirectory string) (string, string, error) { ctx, span := traces.StartSpan(ctx) defer span.End() r, err := startupsettings.OpenReader(ctx, rootDirectory) if err != nil { - return "", fmt.Errorf("opening startupsettings reader: %w", err) + return "", "", fmt.Errorf("opening startupsettings reader: %w", err) } defer r.Close() - updateChannel, err := r.Get(keys.UpdateChannel.String()) - if err != nil { - return "", fmt.Errorf("getting update channel from startupsettings: %w", err) + var pinnedVersion string + switch binary { + case binaryLauncher: + pinnedVersion, _ = r.Get(keys.PinnedLauncherVersion.String()) + case binaryOsqueryd: + pinnedVersion, _ = r.Get(keys.PinnedOsquerydVersion.String()) } - return updateChannel, nil + updateChannel, _ := r.Get(keys.UpdateChannel.String()) + + return pinnedVersion, updateChannel, nil } // getAutoupdateConfig pulls the configuration values necessary to work with the autoupdate library @@ -177,19 +183,21 @@ func getAutoupdateConfigFromFile(configFilePath string) (*autoupdateConfig, erro // CheckOutLatest returns the path to the latest downloaded executable for our binary, as well // as its version. -func CheckOutLatest(ctx context.Context, binary autoupdatableBinary, rootDirectory string, updateDirectory string, channel string, slogger *slog.Logger) (*BinaryUpdateInfo, error) { +func CheckOutLatest(ctx context.Context, binary autoupdatableBinary, rootDirectory string, + updateDirectory string, pinnedVersion string, channel string, slogger *slog.Logger) (*BinaryUpdateInfo, error) { ctx, span := traces.StartSpan(ctx, "binary", string(binary)) defer span.End() + slogger = slogger.With("binary", string(binary), "update_channel", channel, "pinned_version", pinnedVersion) if updateDirectory == "" { updateDirectory = DefaultLibraryDirectory(rootDirectory) } - update, err := findExecutableFromRelease(ctx, binary, LocalTufDirectory(rootDirectory), channel, updateDirectory) + update, err := findExecutable(ctx, binary, LocalTufDirectory(rootDirectory), pinnedVersion, channel, updateDirectory, slogger) if err == nil { - span.AddEvent("found_latest_from_release") + span.AddEvent("found_latest") slogger.Log(ctx, slog.LevelInfo, - "found executable matching current release", + "found executable matching current release or pinned version", "executable_path", update.Path, "executable_version", update.Version, ) @@ -206,9 +214,9 @@ func CheckOutLatest(ctx context.Context, binary autoupdatableBinary, rootDirecto return mostRecentVersion(ctx, binary, updateDirectory, channel) } -// findExecutableFromRelease looks at our local TUF repository to find the release for our +// findExecutable looks at our local TUF repository to find the release for our // given channel. If it's already downloaded, then we return its path and version. -func findExecutableFromRelease(ctx context.Context, binary autoupdatableBinary, tufRepositoryLocation string, channel string, baseUpdateDirectory string) (*BinaryUpdateInfo, error) { +func findExecutable(ctx context.Context, binary autoupdatableBinary, tufRepositoryLocation string, pinnedVersion string, channel string, baseUpdateDirectory string, slogger *slog.Logger) (*BinaryUpdateInfo, error) { ctx, span := traces.StartSpan(ctx) defer span.End() @@ -224,7 +232,7 @@ func findExecutableFromRelease(ctx context.Context, binary autoupdatableBinary, return nil, fmt.Errorf("could not get target: %w", err) } - targetName, _, err := findRelease(ctx, binary, targets, channel) + targetName, _, err := findTarget(ctx, binary, targets, pinnedVersion, channel, slogger) if err != nil { return nil, fmt.Errorf("could not find release: %w", err) } @@ -265,16 +273,12 @@ func mostRecentVersion(ctx context.Context, binary autoupdatableBinary, baseUpda // We rolled out TUF more broadly beginning in v1.4.1. Don't select versions earlier than that. if binary == binaryLauncher && channel == "stable" { - recentVersion, err := semver.NewVersion(mostRecentVersionInLibraryRaw) - if err != nil { - return nil, fmt.Errorf("could not parse most recent version %s in launcher library: %w", recentVersion, err) - } - startingVersion, err := semver.NewVersion("1.4.1") + supportsTuf, err := launcherVersionSupportsTuf(mostRecentVersionInLibraryRaw) if err != nil { - return nil, fmt.Errorf("could not parse required starting version for launcher binary: %w", err) + return nil, fmt.Errorf("could not determine if launcher version %s supports TUF: %w", mostRecentVersionInLibraryRaw, err) } - if recentVersion.LessThan(startingVersion) { - return nil, fmt.Errorf("most recent version %s for binary launcher is not newer than required v1.4.1", recentVersion) + if !supportsTuf { + return nil, fmt.Errorf("most recent version %s for binary launcher is not newer than required %s", mostRecentVersionInLibraryRaw, tufVersionMinimum.String()) } } diff --git a/ee/tuf/library_lookup_test.go b/ee/tuf/library_lookup_test.go index 4274ae887..130af5fd9 100644 --- a/ee/tuf/library_lookup_test.go +++ b/ee/tuf/library_lookup_test.go @@ -14,36 +14,27 @@ import ( "github.com/stretchr/testify/require" ) -func Test_getUpdateChannelFromStartupSettings(t *testing.T) { +func Test_getUpdateSettingsFromStartupSettings(t *testing.T) { t.Parallel() expectedChannel := "beta" + expectedPinnedVersion := "1.5.5" // Set up an override for the channel in the startupsettings db rootDir := t.TempDir() store, err := agentsqlite.OpenRW(context.TODO(), rootDir, agentsqlite.StartupSettingsStore) require.NoError(t, err, "setting up db connection") require.NoError(t, store.Set([]byte(keys.UpdateChannel.String()), []byte(expectedChannel)), "setting key") + require.NoError(t, store.Set([]byte(keys.PinnedLauncherVersion.String()), []byte(expectedPinnedVersion)), "setting key") + require.NoError(t, store.Set([]byte(keys.PinnedOsquerydVersion.String()), []byte("5.5.5")), "setting key") require.NoError(t, store.Close(), "closing test db") - actualChannel, err := getUpdateChannelFromStartupSettings(context.TODO(), rootDir) - require.NoError(t, err, "did not expect error getting update channel from startup settings") + actualVersion, actualChannel, err := getUpdateSettingsFromStartupSettings(context.TODO(), "launcher", rootDir) + require.NoError(t, err, "did not expect error getting update settings from startup settings") + require.Equal(t, expectedPinnedVersion, actualVersion, "did not get expected version") require.Equal(t, expectedChannel, actualChannel, "did not get expected channel") } -func Test_getUpdateChannelFromStartupSettings_NotFound(t *testing.T) { - t.Parallel() - - // Create a startupsettings db but don't set anything in it - rootDir := t.TempDir() - store, err := agentsqlite.OpenRW(context.TODO(), rootDir, agentsqlite.StartupSettingsStore) - require.NoError(t, err, "setting up db connection") - require.NoError(t, store.Close(), "closing test db") - - _, err = getUpdateChannelFromStartupSettings(context.TODO(), rootDir) - require.Error(t, err, "should not have been able to get update channel when it is not set") -} - func TestCheckOutLatest_withTufRepository(t *testing.T) { t.Parallel() @@ -77,7 +68,49 @@ func TestCheckOutLatest_withTufRepository(t *testing.T) { require.NoError(t, os.Chmod(tooRecentPath, 0755)) // Check it - latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "nightly", multislogger.NewNopLogger()) + latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "", "nightly", multislogger.NewNopLogger()) + require.NoError(t, err, "unexpected error on checking out latest") + require.Equal(t, executablePath, latest.Path) + require.Equal(t, executableVersion, latest.Version) + }) + } +} + +func TestCheckOutLatest_withTufRepository_withPinnedVersion(t *testing.T) { + t.Parallel() + + for _, binary := range binaries { + binary := binary + t.Run(string(binary), func(t *testing.T) { + t.Parallel() + + // Set up an update library + rootDir := t.TempDir() + updateDir := DefaultLibraryDirectory(rootDir) + + // Set up a local TUF repo + tufDir := LocalTufDirectory(rootDir) + require.NoError(t, os.MkdirAll(tufDir, 488)) + pinnedVersion := tufci.NonReleaseVersion + expectedTargetName := fmt.Sprintf("%s-%s.tar.gz", binary, pinnedVersion) + testReleaseVersion := "2.3.3" + tufci.SeedLocalTufRepo(t, testReleaseVersion, rootDir) + + // Create a corresponding downloaded target for the pinned version + executablePath, executableVersion := pathToTargetVersionExecutable(binary, expectedTargetName, updateDir) + require.NoError(t, os.MkdirAll(filepath.Dir(executablePath), 0755)) + tufci.CopyBinary(t, executablePath) + require.NoError(t, os.Chmod(executablePath, 0755)) + + // Make a more recent version that we should ignore since it isn't the pinned version + releaseTarget := fmt.Sprintf("%s-%s.tar.gz", binary, testReleaseVersion) + releaseTargetPath, _ := pathToTargetVersionExecutable(binary, releaseTarget, updateDir) + require.NoError(t, os.MkdirAll(filepath.Dir(releaseTargetPath), 0755)) + tufci.CopyBinary(t, releaseTargetPath) + require.NoError(t, os.Chmod(releaseTargetPath, 0755)) + + // Check it + latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", pinnedVersion, "nightly", multislogger.NewNopLogger()) require.NoError(t, err, "unexpected error on checking out latest") require.Equal(t, executablePath, latest.Path) require.Equal(t, executableVersion, latest.Version) @@ -104,7 +137,7 @@ func TestCheckOutLatest_withoutTufRepository(t *testing.T) { require.NoError(t, err, "did not make test binary") // Check it - latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "nightly", multislogger.NewNopLogger()) + latest, err := CheckOutLatest(context.TODO(), binary, rootDir, "", "", "nightly", multislogger.NewNopLogger()) require.NoError(t, err, "unexpected error on checking out latest") require.Equal(t, executablePath, latest.Path) require.Equal(t, executableVersion, latest.Version) diff --git a/ee/tuf/library_manager.go b/ee/tuf/library_manager.go index 870813028..3c5b94ff3 100644 --- a/ee/tuf/library_manager.go +++ b/ee/tuf/library_manager.go @@ -380,11 +380,3 @@ func sortedVersionsInLibrary(ctx context.Context, binary autoupdatableBinary, ba return versionsInLibraryStr, invalidVersions, nil } - -// versionFromTarget extracts the semantic version for an update from its filename. -func versionFromTarget(binary autoupdatableBinary, targetFilename string) string { - // The target is in the form `launcher-0.13.6.tar.gz` -- trim the prefix and the file extension to return the version - prefixToTrim := fmt.Sprintf("%s-", binary) - - return strings.TrimSuffix(strings.TrimPrefix(targetFilename, prefixToTrim), ".tar.gz") -} diff --git a/ee/tuf/library_manager_test.go b/ee/tuf/library_manager_test.go index f25ba9678..6f6ee1c5f 100644 --- a/ee/tuf/library_manager_test.go +++ b/ee/tuf/library_manager_test.go @@ -594,55 +594,3 @@ func Test_sortedVersionsInLibrary_devBuilds(t *testing.T) { require.Equal(t, middleVersion, validVersions[1], "not sorted") require.Equal(t, newerVersion, validVersions[2], "not sorted") } - -func Test_versionFromTarget(t *testing.T) { - t.Parallel() - - testVersions := []struct { - target string - binary autoupdatableBinary - operatingSystem string - version string - }{ - { - target: "launcher/darwin/launcher-0.10.1.tar.gz", - binary: binaryLauncher, - operatingSystem: "darwin", - version: "0.10.1", - }, - { - target: "launcher/windows/launcher-1.13.5.tar.gz", - binary: binaryLauncher, - operatingSystem: "windows", - version: "1.13.5", - }, - { - target: "launcher/linux/launcher-0.13.5-40-gefdc582.tar.gz", - binary: binaryLauncher, - operatingSystem: "linux", - version: "0.13.5-40-gefdc582", - }, - { - target: "osqueryd/darwin/osqueryd-5.8.1.tar.gz", - binary: binaryOsqueryd, - operatingSystem: "darwin", - version: "5.8.1", - }, - { - target: "osqueryd/windows/osqueryd-0.8.1.tar.gz", - binary: binaryOsqueryd, - operatingSystem: "windows", - version: "0.8.1", - }, - { - target: "osqueryd/linux/osqueryd-5.8.2.tar.gz", - binary: binaryOsqueryd, - operatingSystem: "linux", - version: "5.8.2", - }, - } - - for _, testVersion := range testVersions { - require.Equal(t, testVersion.version, versionFromTarget(testVersion.binary, filepath.Base(testVersion.target))) - } -} diff --git a/ee/tuf/version.go b/ee/tuf/version.go new file mode 100644 index 000000000..04940aa14 --- /dev/null +++ b/ee/tuf/version.go @@ -0,0 +1,55 @@ +package tuf + +import ( + "fmt" + "strings" + + "github.com/Masterminds/semver" +) + +var ( + tufVersionMinimum = semver.MustParse("1.4.1") + pinnedLauncherVersionMinimum = semver.MustParse("1.6.1") +) + +// SanitizePinnedVersion ensures that the given version is a valid semantic version, +// and that it meets the requirements for pinning. +func SanitizePinnedVersion(binary autoupdatableBinary, rawVersion string) string { + parsedVersion, err := semver.NewVersion(rawVersion) + if err != nil { + // Invalid semver + return "" + } + + // For osqueryd, we will accept any valid semver -- the autoupdater will validate + // that the version exists at update time. + if binary != binaryLauncher { + return rawVersion + } + + // For launcher, we require that the version is at least greater than v1.6.1, the + // first version to support pinning versions. + if parsedVersion.LessThan(pinnedLauncherVersionMinimum) { + return "" + } + return rawVersion +} + +// launcherVersionSupportsTuf determines if the given version is greater than the minimum +// required to run the new autoupdater. +func launcherVersionSupportsTuf(rawVersion string) (bool, error) { + versionParsed, err := semver.NewVersion(rawVersion) + if err != nil { + return false, fmt.Errorf("could not parse launcher version %s: %w", rawVersion, err) + } + + return versionParsed.GreaterThan(tufVersionMinimum) || versionParsed.Equal(tufVersionMinimum), nil +} + +// versionFromTarget extracts the semantic version for an update from its filename. +func versionFromTarget(binary autoupdatableBinary, targetFilename string) string { + // The target is in the form `launcher-0.13.6.tar.gz` -- trim the prefix and the file extension to return the version + prefixToTrim := fmt.Sprintf("%s-", binary) + + return strings.TrimSuffix(strings.TrimPrefix(targetFilename, prefixToTrim), ".tar.gz") +} diff --git a/ee/tuf/version_test.go b/ee/tuf/version_test.go new file mode 100644 index 000000000..b3f50357e --- /dev/null +++ b/ee/tuf/version_test.go @@ -0,0 +1,166 @@ +package tuf + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSanitizePinnedVersion(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + name string + pinnedVersion string + binary autoupdatableBinary + expectedSanitizedVersion string + }{ + { + name: "osqueryd, valid version", + pinnedVersion: "5.10.0", + binary: binaryOsqueryd, + expectedSanitizedVersion: "5.10.0", + }, + { + name: "osqueryd, invalid version", + pinnedVersion: "version five point ten point zero, please", + binary: binaryOsqueryd, + expectedSanitizedVersion: "", + }, + { + name: "osqueryd, valid early version", + pinnedVersion: "1.0.0", + binary: binaryOsqueryd, + expectedSanitizedVersion: "1.0.0", + }, + { + name: "launcher, valid version", + pinnedVersion: "1.6.3", + binary: binaryLauncher, + expectedSanitizedVersion: "1.6.3", + }, + { + name: "launcher, invalid version", + pinnedVersion: "alpha", + binary: binaryLauncher, + expectedSanitizedVersion: "", + }, + { + name: "launcher, version too early", + pinnedVersion: "1.5.3", + binary: binaryLauncher, + expectedSanitizedVersion: "", + }, + { + name: "launcher, valid version at minimum", + pinnedVersion: pinnedLauncherVersionMinimum.Original(), + binary: binaryLauncher, + expectedSanitizedVersion: pinnedLauncherVersionMinimum.Original(), + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.expectedSanitizedVersion, SanitizePinnedVersion(tt.binary, tt.pinnedVersion)) + }) + } +} + +func Test_launcherVersionSupportsTuf(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + name string + launcherVersion string + expectedError bool + expectedSupportsTuf bool + }{ + { + name: "version supports TUF", + launcherVersion: "1.6.0", + expectedError: false, + expectedSupportsTuf: true, + }, + { + name: "version is TUF minimum, supports TUF", + launcherVersion: tufVersionMinimum.Original(), + expectedError: false, + expectedSupportsTuf: true, + }, + { + name: "version is too early to support TUF", + launcherVersion: "1.0.0", + expectedError: false, + expectedSupportsTuf: false, + }, + { + name: "version is invalid", + launcherVersion: "not a semver", + expectedError: true, + expectedSupportsTuf: false, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + supportsTuf, err := launcherVersionSupportsTuf(tt.launcherVersion) + if tt.expectedError { + require.Error(t, err, "expected error checking if version supports TUF") + } else { + require.NoError(t, err, "expected no error checking if version supports TUF") + } + require.Equal(t, tt.expectedSupportsTuf, supportsTuf) + }) + } +} + +func Test_versionFromTarget(t *testing.T) { + t.Parallel() + + for _, testVersion := range []struct { + target string + binary autoupdatableBinary + operatingSystem string + version string + }{ + { + target: "launcher/darwin/launcher-0.10.1.tar.gz", + binary: binaryLauncher, + operatingSystem: "darwin", + version: "0.10.1", + }, + { + target: "launcher/windows/launcher-1.13.5.tar.gz", + binary: binaryLauncher, + operatingSystem: "windows", + version: "1.13.5", + }, + { + target: "launcher/linux/launcher-0.13.5-40-gefdc582.tar.gz", + binary: binaryLauncher, + operatingSystem: "linux", + version: "0.13.5-40-gefdc582", + }, + { + target: "osqueryd/darwin/osqueryd-5.8.1.tar.gz", + binary: binaryOsqueryd, + operatingSystem: "darwin", + version: "5.8.1", + }, + { + target: "osqueryd/windows/osqueryd-0.8.1.tar.gz", + binary: binaryOsqueryd, + operatingSystem: "windows", + version: "0.8.1", + }, + { + target: "osqueryd/linux/osqueryd-5.8.2.tar.gz", + binary: binaryOsqueryd, + operatingSystem: "linux", + version: "5.8.2", + }, + } { + require.Equal(t, testVersion.version, versionFromTarget(testVersion.binary, filepath.Base(testVersion.target))) + } +} diff --git a/pkg/osquery/runtime/runner.go b/pkg/osquery/runtime/runner.go index 9f6120876..039d0374d 100644 --- a/pkg/osquery/runtime/runner.go +++ b/pkg/osquery/runtime/runner.go @@ -315,7 +315,7 @@ func (r *Runner) launchOsqueryInstance() error { // FindNewest uses context as a way to get a logger, so we need to // create and pass a ctxlog in. var currentOsquerydBinaryPath string - currentOsquerydBinary, err := tuf.CheckOutLatest(ctx, "osqueryd", o.opts.rootDirectory, o.opts.updateDirectory, o.opts.updateChannel, r.slogger) + currentOsquerydBinary, err := tuf.CheckOutLatest(ctx, "osqueryd", o.opts.rootDirectory, o.opts.updateDirectory, o.knapsack.PinnedOsquerydVersion(), o.opts.updateChannel, r.slogger) if err != nil { r.slogger.Log(ctx, slog.LevelDebug, "could not get latest version of osqueryd from new autoupdate library, falling back", diff --git a/pkg/osquery/runtime/runtime_test.go b/pkg/osquery/runtime/runtime_test.go index 5d67ef8fb..c6d7c52fb 100644 --- a/pkg/osquery/runtime/runtime_test.go +++ b/pkg/osquery/runtime/runtime_test.go @@ -300,6 +300,7 @@ func TestBadBinaryPath(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("Slogger").Return(multislogger.NewNopLogger()) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) + k.On("PinnedOsquerydVersion").Return("") runner := New( k, @@ -323,6 +324,7 @@ func TestWithOsqueryFlags(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) + k.On("PinnedOsquerydVersion").Return("") runner := New( k, @@ -355,6 +357,7 @@ func TestFlagsChanged(t *testing.T) { k.On("WatchdogDelaySec").Return(120) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) + k.On("PinnedOsquerydVersion").Return("") // Start the runner runner := New( @@ -447,6 +450,7 @@ func TestSimplePath(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) + k.On("PinnedOsquerydVersion").Return("") runner := New( k, @@ -475,6 +479,7 @@ func TestMultipleShutdowns(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) + k.On("PinnedOsquerydVersion").Return("") runner := New( k, @@ -530,6 +535,7 @@ func TestOsqueryDies(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) + k.On("PinnedOsquerydVersion").Return("") runner := New( k, @@ -621,6 +627,7 @@ func TestExtensionSocketPath(t *testing.T) { k.On("WatchdogEnabled").Return(false) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) k.On("Slogger").Return(multislogger.NewNopLogger()) + k.On("PinnedOsquerydVersion").Return("") extensionSocketPath := filepath.Join(rootDirectory, "sock") runner := New( @@ -663,6 +670,7 @@ func TestOsquerySlowStart(t *testing.T) { k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) slogger := multislogger.New(slog.NewJSONHandler(&logBytes, &slog.HandlerOptions{Level: slog.LevelDebug})) k.On("Slogger").Return(slogger.Logger) + k.On("PinnedOsquerydVersion").Return("") runner := New( k, @@ -715,6 +723,7 @@ func setupOsqueryInstanceForTests(t *testing.T) (runner *Runner, teardown func() k.On("WatchdogDelaySec").Return(120) k.On("RegisterChangeObserver", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe() k.On("Slogger").Return(multislogger.NewNopLogger()) + k.On("PinnedOsquerydVersion").Return("") runner = New( k,