diff --git a/changes/21470-fix-policy-counts-on-hosts b/changes/21470-fix-policy-counts-on-hosts new file mode 100644 index 0000000000..997f249230 --- /dev/null +++ b/changes/21470-fix-policy-counts-on-hosts @@ -0,0 +1 @@ +* Fixed a bug where policy failures of a host were not being cleared in the host details page after configuring the host to not run any policies. diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go index 1f3f2492e3..20340f1683 100644 --- a/server/datastore/mysql/policies.go +++ b/server/datastore/mysql/policies.go @@ -26,8 +26,10 @@ const policyCols = ` p.calendar_events_enabled, p.software_installer_id, p.script_id ` -var errSoftwareTitleIDOnGlobalPolicy = errors.New("install software title id can be only be set on team policies") -var errScriptIDOnGlobalPolicy = errors.New("run script id can only be set on team or \"no team\" policies") +var ( + errSoftwareTitleIDOnGlobalPolicy = errors.New("install software title id can be only be set on team policies") + errScriptIDOnGlobalPolicy = errors.New("run script id can only be set on team or \"no team\" policies") +) var policySearchColumns = []string{"p.name"} @@ -177,8 +179,10 @@ func (ds *Datastore) SavePolicy(ctx context.Context, p *fleet.Policy, shouldRemo ) } -var errMismatchedInstallerTeam = &fleet.BadRequestError{Message: "software installer is associated with a different team"} -var errMismatchedScriptTeam = &fleet.BadRequestError{Message: "script is associated with a different team"} +var ( + errMismatchedInstallerTeam = &fleet.BadRequestError{Message: "software installer is associated with a different team"} + errMismatchedScriptTeam = &fleet.BadRequestError{Message: "script is associated with a different team"} +) func (ds *Datastore) assertTeamMatches(ctx context.Context, teamID uint, softwareInstallerID *uint, scriptID *uint) error { if softwareInstallerID != nil { @@ -382,10 +386,12 @@ func (ds *Datastore) RecordPolicyQueryExecutions(ctx context.Context, host *flee if err != nil { return err } - if len(results) > 0 { - if err := ds.UpdateHostIssuesFailingPolicies(ctx, []uint{host.ID}); err != nil { - return err - } + + // ds.UpdateHostIssuesFailingPolicies should be executed even if len(results) == 0 + // because this means the host is configured to run no policies and we would like + // to cleanup the counts (if any). + if err := ds.UpdateHostIssuesFailingPolicies(ctx, []uint{host.ID}); err != nil { + return err } if deferredSaveHost { diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 7fb0131c1b..23a775c6fc 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -12149,3 +12149,63 @@ func (s *integrationTestSuite) TestAutofillPolicies() { resp = s.Do("POST", "/api/latest/fleet/autofill/policy", req, http.StatusBadRequest) assertBodyContains(t, resp, "AI features are disabled") } + +func (s *integrationTestSuite) TestHostWithNoPoliciesClearsPolicyCounts() { + t := s.T() + ctx := context.Background() + + team, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "Zoobar"}) + require.NoError(t, err) + + host, err := s.ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String("foobar"), + UUID: "foobar", + Hostname: "com.foobar.local", + Platform: "linux", + TeamID: &team.ID, + }) + require.NoError(t, err) + + policy, err := s.ds.NewTeamPolicy(ctx, team.ID, nil, fleet.PolicyPayload{ + Name: "Barfoo", + Query: "SELECT 1;", + }) + require.NoError(t, err) + + distributedWriteResp := submitDistributedQueryResultsResponse{} + s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults( + host, + map[uint]*bool{ + policy.ID: ptr.Bool(false), + }, + ), http.StatusOK, &distributedWriteResp) + + listHostsResp := listHostsResponse{} + s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsResp) + require.Len(t, listHostsResp.Hosts, 1) + require.Equal(t, uint64(1), listHostsResp.Hosts[0].FailingPoliciesCount) + + _, err = s.ds.DeleteTeamPolicies(ctx, team.ID, []uint{policy.ID}) + require.NoError(t, err) + + distributedWriteResp = submitDistributedQueryResultsResponse{} + results := make(map[string]json.RawMessage) + results[hostNoPoliciesWildcard] = json.RawMessage("{\"1\": \"1\"}") + statuses := make(map[string]interface{}) + statuses[hostNoPoliciesWildcard] = 0 + s.DoJSON("POST", "/api/osquery/distributed/write", submitDistributedQueryResultsRequestShim{ + NodeKey: *host.NodeKey, + Results: results, + Statuses: statuses, + Stats: map[string]*fleet.Stats{}, + }, http.StatusOK, &distributedWriteResp) + + listHostsResp = listHostsResponse{} + s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsResp) + require.Len(t, listHostsResp.Hosts, 1) + require.Equal(t, uint64(0), listHostsResp.Hosts[0].FailingPoliciesCount) +}