diff --git a/changes/47388-getclientconfig-nil-map-panic b/changes/47388-getclientconfig-nil-map-panic new file mode 100644 index 00000000000..6562294f464 --- /dev/null +++ b/changes/47388-getclientconfig-nil-map-panic @@ -0,0 +1 @@ +- Fixed a server panic ("assignment to entry in nil map") when a host checked in for its osquery config while its agent options had a null `config`. diff --git a/server/service/osquery.go b/server/service/osquery.go index 267430054d8..f63be74a2c5 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -415,6 +415,13 @@ func (svc *Service) GetClientConfig(ctx context.Context) (map[string]interface{} if err != nil { return nil, newOsqueryError("internal error: parse base configuration: " + err.Error()) } + if config == nil { + // Unmarshaling the JSON literal `null` (e.g. agent options with + // "config": null) sets the map to nil rather than leaving it empty. + // Re-initialize so later assignments (e.g. config["packs"]) don't + // panic with "assignment to entry in nil map". + config = make(map[string]any) + } } packConfig := fleet.Packs{} diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 8785c1b2f59..3469f56f720 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -215,6 +215,52 @@ func TestGetClientConfig(t *testing.T) { ) } +// TestGetClientConfigNullConfig is a regression test for a panic +// ("assignment to entry in nil map") that occurred when a host's agent options +// had a null "config" and the host also had packs/scheduled queries. See +// https://github.com/fleetdm/fleet/issues/47388. +func TestGetClientConfigNullConfig(t *testing.T) { + ds := new(mock.Store) + + ds.TeamAgentOptionsFunc = func(ctx context.Context, teamID uint) (*json.RawMessage, error) { + return nil, nil + } + ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) { + return []*fleet.Pack{{ID: 1, Name: "pack_by_label"}}, nil + } + ds.ListScheduledQueriesInPackFunc = func(ctx context.Context, pid uint) (fleet.ScheduledQueryList, error) { + return []*fleet.ScheduledQuery{ + {Name: "time", Query: "select * from time", Interval: 30, Removed: new(false)}, + }, nil + } + ds.ListScheduledQueriesForAgentsFunc = func(ctx context.Context, teamID *uint, hostID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) { + return nil, nil + } + // Global agent options with a null config. This unmarshals into a nil map, + // which previously caused a panic once packs were added to the config. + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{AgentOptions: new(json.RawMessage(`{"config":null}`))}, nil + } + ds.UpdateHostFunc = func(ctx context.Context, host *fleet.Host) error { + return nil + } + + svc, ctx := newTestService(t, ds, nil, nil) + ctx = hostctx.NewContext(ctx, &fleet.Host{ID: 1}) + + conf, err := svc.GetClientConfig(ctx) + require.NoError(t, err) + assert.JSONEq(t, `{ + "pack_by_label": { + "queries":{ + "time":{"query":"select * from time","interval":30,"removed":false} + } + } + }`, + string(conf["packs"].(json.RawMessage)), + ) +} + func TestAgentOptionsForHost(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil)