From 5d9b102a1beef8b1409b71459ae27b508de47a2c Mon Sep 17 00:00:00 2001 From: Jonathan Katz <44128041+jkatz01@users.noreply.github.com> Date: Fri, 22 May 2026 15:11:23 -0400 Subject: [PATCH] 45190 patch policy wrong installer (#46087) **Related issue:** Resolves # Adds a missing `is_active=1` check to get the actual active installer for the software automation's installer. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually - I tested by pinning the version of the app to the older one, then unpinning and back and checked that the policy is associated to the correct installer with the query below. - I did not manually test updating to a new FMA version. It's possible to do that easily by creating a new branch with a newer version and referring FLEET_DEV_MAINTAINED_APPS_BASE_URL to it. ``` SELECT si.id, si.team_id, si.title_id, si.filename, si.version, si.storage_id, si.uploaded_at, si.updated_at, si.url, si.fleet_maintained_app_id, si.install_during_setup, si.is_active, si.patch_query, si.http_etag, p.patch_software_title_id, p.software_installer_id, p.query FROM software_installers si LEFT JOIN policies p on si.id = p.software_installer_id WHERE filename LIKE "%%" AND platform = "darwin" AND global_or_team_id = ; ``` ## Summary by CodeRabbit * **Bug Fixes** * Patch policies using software install automations now correctly prioritize active installers, ensuring deployment uses the latest appropriate version rather than inactive or outdated alternatives. * **Tests** * Added test scenarios to validate policy installer version selection and active status during automation evaluation. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/46087?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- ...90-patch-policy-wrong-installer-automation | 1 + server/datastore/mysql/policies.go | 3 +- server/service/integration_enterprise_test.go | 51 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 changes/45190-patch-policy-wrong-installer-automation diff --git a/changes/45190-patch-policy-wrong-installer-automation b/changes/45190-patch-policy-wrong-installer-automation new file mode 100644 index 0000000000..0f14943ecb --- /dev/null +++ b/changes/45190-patch-policy-wrong-installer-automation @@ -0,0 +1 @@ +- Fixed a bug where patch policies with software install automations used an inactive, older installer and not the latest. diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go index d0578c5010..d7f31f9b7a 100644 --- a/server/datastore/mysql/policies.go +++ b/server/datastore/mysql/policies.go @@ -1373,7 +1373,8 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs VPPAppsTeamsID *uint `db:"vat_id"` } err := sqlx.GetContext(ctx, queryerContext, &ids, - `SELECT id si_id, NULL vat_id FROM software_installers WHERE global_or_team_id = ? AND title_id = ? + `SELECT id si_id, NULL vat_id FROM software_installers + WHERE global_or_team_id = ? AND title_id = ? AND is_active = 1 UNION SELECT NULL si_id, vat.id vat_id FROM vpp_apps_teams vat JOIN vpp_apps va ON va.adam_id = vat.adam_id AND va.platform = vat.platform diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 0f252d8eb8..de0df59d4e 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -27924,6 +27924,24 @@ func (s *integrationEnterpriseTestSuite) TestPatchPolicies() { require.Equal(t, name, policy.Name) } + checkPolicyInstaller := func(policyID uint, version string) { + var row struct { + Version string `db:"version"` + IsActive bool `db:"is_active"` + } + mysqltest.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { + return sqlx.GetContext(ctx, q, &row, + `SELECT si.version, si.is_active + FROM policies p + JOIN software_installers si ON si.id = p.software_installer_id + WHERE p.id = ?`, + policyID, + ) + }) + require.Equal(t, version, row.Version) + require.True(t, row.IsActive) + } + createHostPolicyResults := func(host *fleet.Host, policy *fleet.Policy) { distributedResp := submitDistributedQueryResultsResponse{} s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults( @@ -28247,6 +28265,7 @@ func (s *integrationEnterpriseTestSuite) TestPatchPolicies() { checkPolicy(listPolResp.Policies[0], spec.Name, "1.0", title.ID) // This is only set if the automation is enable require.Equal(t, title.ID, listPolResp.Policies[0].InstallSoftware.SoftwareTitleID) + checkPolicyInstaller(listPolResp.Policies[0].ID, "1.0") // Now disable the automation spec = &fleet.PolicySpec{ @@ -28325,6 +28344,38 @@ func (s *integrationEnterpriseTestSuite) TestPatchPolicies() { s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team.ID), fleet.ListTeamPoliciesRequest{}, http.StatusOK, &listPolResp, "page", "0") require.Len(t, listPolResp.Policies, 1) checkPolicy(listPolResp.Policies[0], spec.Name, "1.0", title.ID) + + // Test 4: FMA upgraded again with two versions already cached. + spec = &fleet.PolicySpec{ + Name: "team patch policy", + Query: "SELECT 1", + Team: team.Name, + Type: fleet.PolicyTypePatch, + FleetMaintainedAppSlug: "zoom/windows", + SoftwareTitleID: new(title.ID), + } + + resetFMAState(states["/zoom/windows.json"], "1.3", []byte("ghi"), "") + + s.DoJSON("POST", "/api/latest/fleet/software/batch", + batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{Slug: ptr.String("zoom/windows")}}, TeamName: team.Name}, + http.StatusAccepted, &resp, + "team_name", team.Name, "team_id", fmt.Sprint(team.ID), + ) + waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, team.Name, resp.RequestUUID) + + applyResp = fleet.ApplyPolicySpecsResponse{} + s.DoJSON("POST", "/api/latest/fleet/spec/policies", + fleet.ApplyPolicySpecsRequest{Specs: []*fleet.PolicySpec{spec}}, + http.StatusOK, &applyResp, + ) + title = getActiveTitleForTeam(team.ID, "zoom") + + listPolResp = fleet.ListTeamPoliciesResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/fleets/%d/policies", team.ID), fleet.ListTeamPoliciesRequest{}, http.StatusOK, &listPolResp, "page", "0") + require.Len(t, listPolResp.Policies, 1) + checkPolicy(listPolResp.Policies[0], spec.Name, "1.3", title.ID) + checkPolicyInstaller(listPolResp.Policies[0].ID, "1.3") }) t.Run("override and empty queries behave the same", func(t *testing.T) {