Skip to content

Commit

Permalink
add database reset history to uninstall and add uninstall history che…
Browse files Browse the repository at this point in the history
…ckup (#1639)

Co-authored-by: seph <seph@kolide.co>
  • Loading branch information
zackattack01 and directionless authored Mar 7, 2024
1 parent 8f63010 commit eb6e451
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 62 deletions.
90 changes: 58 additions & 32 deletions ee/agent/reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,19 @@ var (
hostDataKeySerial = []byte("serial")
hostDataKeyHardwareUuid = []byte("hardware_uuid")
hostDataKeyMunemo = []byte("munemo")

hostDataKeyResetRecords = []byte("reset_records")
)

const (
resetReasonNewHardwareOrEnrollmentDetected = "launcher detected new hardware or enrollment"
)

type UninitializedStorageError struct{}

func (use UninitializedStorageError) Error() string {
return "storage is uninitialized in knapsack"
}

// DetectAndRemediateHardwareChange checks to see if the hardware this installation is running on
// has changed, by checking current hardware- and enrollment- identifying information against
// stored data in the HostDataStore. If the hardware- or enrollment-identifying information
Expand Down Expand Up @@ -79,24 +84,12 @@ func DetectAndRemediateHardwareChange(ctx context.Context, k types.Knapsack) {
)

// In the future, we can proceed with backing up and resetting the database.
// For now, we are only logging that we detected the change.
// For now, we are only logging that we detected the change until we have a dependable
// hardware change detection method - see issue here https://github.com/kolide/launcher/issues/1346
/*
backup, err := prepareDatabaseResetRecords(ctx, k, resetReasonNewHardwareOrEnrollmentDetected)
if err != nil {
k.Slogger().Log(ctx, slog.LevelWarn, "could not prepare db reset records", "err", err)
}
if err := wipeDatabase(ctx, k); err != nil {
k.Slogger().Log(ctx, slog.LevelError, "could not wipe database", "err", err)
return
}
// Store the backup data
if err := k.PersistentHostDataStore().Set(hostDataKeyResetRecords, backup); err != nil {
k.Slogger().Log(ctx, slog.LevelWarn, "could not store db reset records", "err", err)
if err := ResetDatabase(ctx, k, resetReasonNewHardwareOrEnrollmentDetected); err != nil {
k.Slogger().Log(ctx, slog.LevelError, "failed to reset database", "err", err)
}
*/

// Cache hardware and rollout data for future checks
Expand All @@ -112,6 +105,49 @@ func DetectAndRemediateHardwareChange(ctx context.Context, k types.Knapsack) {
}
}

func GetResetRecords(ctx context.Context, k types.Knapsack) ([]dbResetRecord, error) {
resetRecords := make([]dbResetRecord, 0)
if k.PersistentHostDataStore() == nil {
return resetRecords, UninitializedStorageError{}
}

resetRecordsRaw, err := k.PersistentHostDataStore().Get(hostDataKeyResetRecords)
if err != nil {
return resetRecords, err
}

if len(resetRecordsRaw) == 0 {
return resetRecords, nil
}

if err := json.Unmarshal(resetRecordsRaw, &resetRecords); err != nil {
return resetRecords, err
}

return resetRecords, nil
}

func ResetDatabase(ctx context.Context, k types.Knapsack, resetReason string) error {
backup, err := prepareDatabaseResetRecords(ctx, k, resetReason)
if err != nil {
k.Slogger().Log(ctx, slog.LevelError, "could not prepare db reset records", "err", err)
return err
}

if err := wipeDatabase(ctx, k); err != nil {
k.Slogger().Log(ctx, slog.LevelError, "could not wipe database", "err", err)
return err
}

// Store the backup data
if err := k.PersistentHostDataStore().Set(hostDataKeyResetRecords, backup); err != nil {
k.Slogger().Log(ctx, slog.LevelWarn, "could not store db reset records", "err", err)
return err
}

return nil
}

// currentSerialAndHardwareUUID queries osquery for the required information.
func currentSerialAndHardwareUUID(ctx context.Context, k types.Knapsack) (string, string, error) {
osqPath := k.LatestOsquerydPath(ctx)
Expand Down Expand Up @@ -299,23 +335,13 @@ func prepareDatabaseResetRecords(ctx context.Context, k types.Knapsack, resetRea
ResetReason: resetReason,
}

previousHostData, err := k.PersistentHostDataStore().Get(hostDataKeyResetRecords)
previousHostData, err := GetResetRecords(ctx, k)
if err != nil {
return nil, fmt.Errorf("getting previous host data from store: %w", err)
}

var hostDataCollection []dbResetRecord
if len(previousHostData) == 0 {
// No previous database resets
hostDataCollection = []dbResetRecord{dataToStore}
} else {
if err := json.Unmarshal(previousHostData, &hostDataCollection); err != nil {
return nil, fmt.Errorf("unmarshalling previous host data: %w", err)
}
hostDataCollection = append(hostDataCollection, dataToStore)
}

hostDataCollectionRaw, err := json.Marshal(hostDataCollection)
previousHostData = append(previousHostData, dataToStore)
hostDataCollectionRaw, err := json.Marshal(previousHostData)
if err != nil {
return nil, fmt.Errorf("marshalling host data for storage: %w", err)
}
Expand Down Expand Up @@ -344,9 +370,9 @@ func getLocalPubKey(k types.Knapsack) ([]byte, error) { // nolint:unused
return pubKeyBytes, nil
}

// WipeDatabase iterates over all stores in the database, deleting all keys from
// wipeDatabase iterates over all stores in the database, deleting all keys from
// each one.
func WipeDatabase(ctx context.Context, k types.Knapsack) error {
func wipeDatabase(ctx context.Context, k types.Knapsack) error {
for storeName, store := range k.Stores() {
if err := store.DeleteAll(); err != nil {
return fmt.Errorf("deleting keys in store %s: %w", storeName, err)
Expand Down
1 change: 1 addition & 0 deletions ee/debug/checkups/checkups.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func checkupsFor(k types.Knapsack, target targetBits) []checkupInt {
{&serverDataCheckup{k: k}, doctorSupported | flareSupported | logSupported},
{&osqDataCollector{k: k}, doctorSupported | flareSupported},
{&osqRestartCheckup{k: k}, doctorSupported | flareSupported},
{&uninstallHistoryCheckup{k: k}, flareSupported},
}

checkupsToRun := make([]checkupInt, 0)
Expand Down
68 changes: 68 additions & 0 deletions ee/debug/checkups/uninstall_history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package checkups

import (
"context"
"errors"
"io"
"time"

"github.com/kolide/launcher/ee/agent"
"github.com/kolide/launcher/ee/agent/types"
)

type (
uninstallHistoryCheckup struct {
k types.Knapsack
status Status
summary string
data map[string]any
}
)

func (hc *uninstallHistoryCheckup) Data() any { return hc.data }
func (hc *uninstallHistoryCheckup) ExtraFileName() string { return "" }
func (hc *uninstallHistoryCheckup) Name() string { return "Uninstall History" }
func (hc *uninstallHistoryCheckup) Status() Status { return hc.status }
func (hc *uninstallHistoryCheckup) Summary() string { return hc.summary }

func (hc *uninstallHistoryCheckup) Run(ctx context.Context, extraFH io.Writer) error {
hc.data = make(map[string]any)
resetRecords, err := agent.GetResetRecords(ctx, hc.k)
if err != nil && errors.Is(err, agent.UninitializedStorageError{}) {
hc.status = Informational
hc.summary = "Unable to access uninstall history"
return nil
}

if err != nil {
hc.status = Erroring
hc.summary = "Unable to gather previous host data from store"
hc.data["error"] = err.Error()
return nil
}

if len(resetRecords) == 0 {
hc.status = Informational
hc.summary = "No installation history exists for this device"
return nil
}

for _, uninstallRecord := range resetRecords {
resetTimeKey := time.Unix(uninstallRecord.ResetTimestamp, 0)
hc.data[resetTimeKey.Format(time.RFC3339)] = map[string]any{
"serial": uninstallRecord.Serial,
"hardware_uuid": uninstallRecord.HardwareUUID,
"munemo": uninstallRecord.Munemo,
"device_id": uninstallRecord.DeviceID,
"remote_ip": uninstallRecord.RemoteIP,
"tombstone_id": uninstallRecord.TombstoneID,
"reset_timestamp": resetTimeKey,
"reset_reason": uninstallRecord.ResetReason,
}
}

hc.status = Informational
hc.summary = "Successfully collected uninstallation history"

return nil
}
8 changes: 6 additions & 2 deletions ee/uninstall/uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import (
"github.com/kolide/launcher/ee/agent/types"
)

const (
resetReasonUninstallRequested = "remote uninstall requested"
)

// Uninstall just removes the enroll secret file and wipes the database.
// Logs errors, but does not return them, because we want to try each step independently.
// If exitOnCompletion is true, it will also disable launcher autostart and exit.
Expand All @@ -23,9 +27,9 @@ func Uninstall(ctx context.Context, k types.Knapsack, exitOnCompletion bool) {
)
}

if err := agent.WipeDatabase(ctx, k); err != nil {
if err := agent.ResetDatabase(ctx, k, resetReasonUninstallRequested); err != nil {
slogger.Log(ctx, slog.LevelError,
"wiping database",
"resetting database",
"err", err,
)
}
Expand Down
68 changes: 40 additions & 28 deletions ee/uninstall/uninstall_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"path/filepath"
"testing"

"github.com/kolide/kit/ulid"
"github.com/kolide/launcher/ee/agent"
"github.com/kolide/launcher/ee/agent/storage"
storageci "github.com/kolide/launcher/ee/agent/storage/ci"
"github.com/kolide/launcher/ee/agent/types"
Expand Down Expand Up @@ -41,46 +41,47 @@ func TestUninstall(t *testing.T) {
_, err = os.Stat(enrollSecretPath)
require.NoError(t, err)

// create 3 stores with 3 items each
stores := map[storage.Store]types.KVStore{}
for i := 0; i < 3; i++ {
store, err := storageci.NewStore(t, multislogger.NewNopLogger(), ulid.New())
require.NoError(t, err)
k := mocks.NewKnapsack(t)
k.On("EnrollSecretPath").Return(enrollSecretPath)
k.On("Slogger").Return(multislogger.NewNopLogger())
testConfigStore, err := storageci.NewStore(t, multislogger.NewNopLogger(), storage.ConfigStore.String())
require.NoError(t, err, "could not create test config store")
k.On("ConfigStore").Return(testConfigStore)
testHostDataStore, err := storageci.NewStore(t, multislogger.NewNopLogger(), storage.PersistentHostDataStore.String())
require.NoError(t, err, "could not create test host data store")
k.On("PersistentHostDataStore").Return(testHostDataStore)
testServerProvidedDataStore, err := storageci.NewStore(t, multislogger.NewNopLogger(), storage.ServerProvidedDataStore.String())
require.NoError(t, err, "could not create test server provided data store")
k.On("ServerProvidedDataStore").Return(testServerProvidedDataStore)
stores := map[storage.Store]types.KVStore{
storage.PersistentHostDataStore: testHostDataStore,
storage.ConfigStore: testConfigStore,
storage.ServerProvidedDataStore: testServerProvidedDataStore,
}
k.On("Stores").Return(stores)
testSerial := []byte("C999999999")
testHardwareUUID := []byte("99999999-9999-9999-9999-999999999999")

// seed the test storage with known serial and hardware_uuids to test against the reset records later
require.NoError(t, testHostDataStore.Set([]byte("serial"), testSerial), "could not set serial in test store")
require.NoError(t, testHostDataStore.Set([]byte("hardware_uuid"), testHardwareUUID), "could not set hardware uuid in test store")
// additionally seed all stores with some data to ensure we are clearing all values later
for _, store := range stores {
for j := 0; j < 3; j++ {
require.NoError(t, store.Set([]byte(fmt.Sprint(j)), []byte(fmt.Sprint(j))))
}

require.NoError(t, err)
stores[storage.Store(fmt.Sprint(i))] = store
}

// sanity check that we have 3 stores with 3 items each
itemsExpected := 9
itemsFound := 0
for _, store := range stores {
store.ForEach(
func(k, v []byte) error {
itemsFound++
return nil
},
)
}
require.Equal(t, itemsExpected, itemsFound)

k := mocks.NewKnapsack(t)
k.On("Stores").Return(stores)
k.On("EnrollSecretPath").Return(enrollSecretPath)
k.On("Slogger").Return(multislogger.NewNopLogger())

Uninstall(context.TODO(), k, false)

// check that file was deleted
_, err = os.Stat(enrollSecretPath)
require.True(t, os.IsNotExist(err))

// check that all stores are empty
itemsFound = 0
// check that all stores are empty except for the uninstallation history
itemsFound := 0
for _, store := range stores {
store.ForEach(
func(k, v []byte) error {
Expand All @@ -89,7 +90,18 @@ func TestUninstall(t *testing.T) {
},
)
}
require.Equal(t, 0, itemsFound)

// the expectation of 1 here is coming from the single remaining reset_records key
// see agent.ResetDatabase for additional context
require.Equal(t, 1, itemsFound)
resetRecords, err := agent.GetResetRecords(context.TODO(), k)
require.NoError(t, err, "could not get reset records from test store")
require.Equal(t, 1, len(resetRecords), "expected reset records to contain exactly 1 uninstallation record")
// now check the individual bits we want to ensure are migrated to the reset record
resetRecord := resetRecords[0]
require.Equal(t, resetReasonUninstallRequested, resetRecord.ResetReason, "expected reset record to indicate the uninstall requested")
require.Equal(t, string(testSerial), resetRecord.Serial, "expected reset record to indicate the serial number from the original installation")
require.Equal(t, string(testHardwareUUID), resetRecord.HardwareUUID, "expected reset record to indicate the hardware UUID from the original installation")
})
}
}

0 comments on commit eb6e451

Please sign in to comment.