Repository: kolide/launcher

Issue:
kolide_dnf_upgradeable sometimes panics
```
runtime error: slice bounds out of range [:-1]
github.com/kolide/launcher/ee/gowrapper.GoWithRecoveryAction.func1.1
	/__w/launcher/launcher/ee/gowrapper/goroutine.go:31
runtime.gopanic
	/__t/go/1.23.0/x64/src/runtime/panic.go:785
runtime.goPanicSliceAlen
	/__t/go/1.23.0/x64/src/runtime/panic.go:129
github.com/kolide/launcher/ee/tables/execparsers/dnf.dnfParse
	/__w/launcher/launcher/ee/tables/execparsers/dnf/parser.go:27
github.com/kolide/launcher/ee/tables/execparsers/dnf.parser.Parse
	/__w/launcher/launcher/ee/tables/execparsers/dnf/dnf.go:16
github.com/kolide/launcher/ee/tables/dataflattentable.parserFlattener.FlattenBytes
	/__w/launcher/launcher/ee/tables/dataflattentable/flattener_from_parser.go:26
github.com/kolide/launcher/ee/tables/dataflattentable.(*execTableV2).generate
	/__w/launcher/launcher/ee/tables/dataflattentable/exec_and_parse.go:108
github.com/kolide/launcher/ee/tables/tablewrapper.(*wrappedTable).generate.func1
	/__w/launcher/launcher/ee/tables/tablewrapper/tablewrapper.go:130
github.com/kolide/launcher/ee/gowrapper.GoWithRecoveryAction.func1
	/__w/launcher/launcher/ee/gowrapper/goroutine.go:39
runtime.goexit
	/__t/go/1.23.0/x64/src/runtime/asm_amd64.s:1700
```

Discussion:

Solution patch:
diff --git a/ee/control/consumers/uninstallconsumer/uninstallconsumer.go b/ee/control/consumers/uninstallconsumer/uninstallconsumer.go
index a3c4cca7..40da236a 100644
--- a/ee/control/consumers/uninstallconsumer/uninstallconsumer.go
+++ b/ee/control/consumers/uninstallconsumer/uninstallconsumer.go
@@ -3,6 +3,7 @@ package uninstallconsumer
 import (
 	"context"
 	"io"
+	"log/slog"
 
 	"github.com/kolide/launcher/ee/agent/types"
 	"github.com/kolide/launcher/ee/uninstall"
@@ -14,16 +15,21 @@ const (
 )
 
 type UninstallConsumer struct {
+	slogger  *slog.Logger
 	knapsack types.Knapsack
 }
 
 func New(knapsack types.Knapsack) *UninstallConsumer {
 	return &UninstallConsumer{
+		slogger:  knapsack.Slogger().With("component", "uninstall_consumer"),
 		knapsack: knapsack,
 	}
 }
 
 func (c *UninstallConsumer) Do(data io.Reader) error {
+	c.slogger.Log(context.TODO(), slog.LevelInfo,
+		"received request to uninstall",
+	)
 	uninstall.Uninstall(context.TODO(), c.knapsack, true)
 	return nil
 }
diff --git a/ee/tables/execparsers/dnf/parser.go b/ee/tables/execparsers/dnf/parser.go
index 7477adde..547dff3e 100755
--- a/ee/tables/execparsers/dnf/parser.go
+++ b/ee/tables/execparsers/dnf/parser.go
@@ -22,6 +22,10 @@ func dnfParse(reader io.Reader) (any, error) {
 		}
 
 		splitIndex := strings.LastIndex(fields[0], ".")
+		if splitIndex == -1 {
+			// We assume this means the line is malformed or doesn't contain a valid update.
+			continue
+		}
 
 		row := make(map[string]string)
 		row["package"] = strings.TrimSpace(fields[0][:splitIndex])
diff --git a/pkg/osquery/extension.go b/pkg/osquery/extension.go
index d34c4c24..fbd2ca26 100644
--- a/pkg/osquery/extension.go
+++ b/pkg/osquery/extension.go
@@ -500,6 +500,9 @@ func (e *Extension) Enroll(ctx context.Context) (string, bool, error) {
 
 	switch {
 	case errors.Is(err, service.ErrDeviceDisabled{}):
+		e.slogger.Log(ctx, slog.LevelInfo,
+			"received device disabled error during enrollment, uninstalling",
+		)
 		uninstall.Uninstall(ctx, e.knapsack, true)
 		// the uninstall call above will cause launcher to uninstall and exit
 		// so we are returning the err here just incase something somehow
@@ -621,6 +624,9 @@ func (e *Extension) generateConfigsWithReenroll(ctx context.Context, reenroll bo
 	config, invalid, err := e.serviceClient.RequestConfig(ctx, nodeKey)
 	switch {
 	case errors.Is(err, service.ErrDeviceDisabled{}):
+		e.slogger.Log(ctx, slog.LevelInfo,
+			"received device disabled error during config request, uninstalling",
+		)
 		uninstall.Uninstall(ctx, e.knapsack, true)
 		// the uninstall call above will cause launcher to uninstall and exit
 		// so we are returning the err here just incase something somehow
@@ -891,6 +897,9 @@ func (e *Extension) writeLogsWithReenroll(ctx context.Context, typ logger.LogTyp
 	_, _, invalid, err := e.serviceClient.PublishLogs(ctx, nodeKey, typ, logs)
 
 	if errors.Is(err, service.ErrDeviceDisabled{}) {
+		e.slogger.Log(ctx, slog.LevelInfo,
+			"received device disabled error during log publish, uninstalling",
+		)
 		uninstall.Uninstall(ctx, e.knapsack, true)
 		// the uninstall call above will cause launcher to uninstall and exit
 		// so we are returning the err here just incase something somehow
@@ -1039,6 +1048,9 @@ func (e *Extension) getQueriesWithReenroll(ctx context.Context, reenroll bool) (
 
 	switch {
 	case errors.Is(err, service.ErrDeviceDisabled{}):
+		e.slogger.Log(ctx, slog.LevelInfo,
+			"received device disabled error during queries request, uninstalling",
+		)
 		uninstall.Uninstall(ctx, e.knapsack, true)
 		// the uninstall call above will cause launcher to uninstall and exit
 		// so we are returning the err here just incase something somehow
@@ -1101,6 +1113,9 @@ func (e *Extension) writeResultsWithReenroll(ctx context.Context, results []dist
 	_, _, invalid, err := e.serviceClient.PublishResults(ctx, nodeKey, results)
 	switch {
 	case errors.Is(err, service.ErrDeviceDisabled{}):
+		e.slogger.Log(ctx, slog.LevelInfo,
+			"received device disabled error during results publish, uninstalling",
+		)
 		uninstall.Uninstall(ctx, e.knapsack, true)
 		// the uninstall call above will cause launcher to uninstall and exit
 		// so we are returning the err here just incase something somehow



Test patch:
diff --git a/ee/tables/execparsers/dnf/parser_test.go b/ee/tables/execparsers/dnf/parser_test.go
index b8fab23b..890d7d21 100644
--- a/ee/tables/execparsers/dnf/parser_test.go
+++ b/ee/tables/execparsers/dnf/parser_test.go
@@ -34,6 +34,11 @@ func TestParse(t *testing.T) {
 				},
 			},
 		},
+		{
+			name:     "more malformed input",
+			input:    []byte("three words here"),
+			expected: []map[string]string{},
+		},
 		{
 			name:  "dnf_upgradeable",
 			input: dnf_upgradeable,