Bugfix: Fix query to ignore host_software_installs rows where host is deleted (#38250)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Fixed a bug where installed software would not show up in the software inventory of an ADE-enrolled macOS host after a wipe and a re-enrollment.
|
||||
@@ -3170,13 +3170,13 @@ func hostSoftwareInstalls(ds *Datastore, ctx context.Context, hostID uint) ([]*h
|
||||
hsi.software_installer_id = hsi2.software_installer_id AND
|
||||
hsi.uninstall = hsi2.uninstall AND
|
||||
hsi2.removed = 0 AND
|
||||
hsi2.canceled = 0 AND
|
||||
hsi2.canceled = 0 AND
|
||||
hsi2.host_deleted_at IS NULL AND
|
||||
(hsi.created_at < hsi2.created_at OR (hsi.created_at = hsi2.created_at AND hsi.id < hsi2.id))
|
||||
WHERE
|
||||
hsi.host_id = ? AND
|
||||
hsi.removed = 0 AND
|
||||
hsi.canceled = 0 AND
|
||||
hsi.canceled = 0 AND
|
||||
hsi.uninstall = 0 AND
|
||||
hsi.host_deleted_at IS NULL AND
|
||||
hsi2.id IS NULL AND
|
||||
@@ -3250,14 +3250,14 @@ func hostSoftwareUninstalls(ds *Datastore, ctx context.Context, hostID uint) ([]
|
||||
hsi.software_installer_id = hsi2.software_installer_id AND
|
||||
hsi.uninstall = hsi2.uninstall AND
|
||||
hsi2.removed = 0 AND
|
||||
hsi2.canceled = 0 AND
|
||||
hsi2.canceled = 0 AND
|
||||
hsi2.host_deleted_at IS NULL AND
|
||||
(hsi.created_at < hsi2.created_at OR (hsi.created_at = hsi2.created_at AND hsi.id < hsi2.id))
|
||||
WHERE
|
||||
hsi.host_id = ? AND
|
||||
hsi.removed = 0 AND
|
||||
hsi.uninstall = 1 AND
|
||||
hsi.canceled = 0 AND
|
||||
hsi.canceled = 0 AND
|
||||
hsi.host_deleted_at IS NULL AND
|
||||
hsi2.id IS NULL AND
|
||||
NOT EXISTS (
|
||||
@@ -4511,7 +4511,8 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt
|
||||
hsi.host_id = :host_id AND
|
||||
hsi.software_installer_id = si.id AND
|
||||
hsi.removed = 0 AND
|
||||
hsi.canceled = 0
|
||||
hsi.canceled = 0 AND
|
||||
hsi.host_deleted_at IS NULL
|
||||
) AND
|
||||
-- sofware install/uninstall is not upcoming on host
|
||||
NOT EXISTS (
|
||||
@@ -5933,6 +5934,7 @@ func (ds *Datastore) CountHostSoftwareInstallAttempts(ctx context.Context, hostI
|
||||
AND policy_id = ?
|
||||
AND removed = 0
|
||||
AND canceled = 0
|
||||
AND host_deleted_at IS NULL
|
||||
AND (attempt_number > 0 OR attempt_number IS NULL)
|
||||
`, hostID, softwareInstallerID, policyID)
|
||||
if err != nil {
|
||||
@@ -6061,7 +6063,7 @@ SELECT
|
||||
FROM software_titles st
|
||||
INNER JOIN software_installers si ON si.title_id = st.id
|
||||
INNER JOIN host_software_installs hsi ON hsi.host_id = :host_id AND hsi.software_installer_id = si.id
|
||||
WHERE hsi.removed = 0 AND hsi.canceled = 0 AND hsi.status = :software_status_installed
|
||||
WHERE hsi.removed = 0 AND hsi.canceled = 0 AND hsi.host_deleted_at IS NULL AND hsi.status = :software_status_installed
|
||||
|
||||
UNION
|
||||
|
||||
@@ -6077,9 +6079,9 @@ FROM software_titles st
|
||||
INNER JOIN vpp_apps vap ON vap.title_id = st.id
|
||||
INNER JOIN host_vpp_software_installs hvsi ON hvsi.host_id = :host_id AND hvsi.adam_id = vap.adam_id AND hvsi.platform = vap.platform
|
||||
LEFT JOIN nano_command_results ncr ON ncr.command_uuid = hvsi.command_uuid
|
||||
WHERE
|
||||
hvsi.removed = 0 AND
|
||||
hvsi.canceled = 0 AND
|
||||
WHERE
|
||||
hvsi.removed = 0 AND
|
||||
hvsi.canceled = 0 AND
|
||||
(ncr.status = :mdm_status_acknowledged OR hvsi.verification_at IS NOT NULL)
|
||||
`
|
||||
selectStmt, args, err := sqlx.Named(stmt, map[string]interface{}{
|
||||
|
||||
@@ -106,6 +106,7 @@ func TestSoftware(t *testing.T) {
|
||||
{"ListHostSoftwareAndroidVPPAppMatching", testListHostSoftwareAndroidVPPAppMatching},
|
||||
{"CountHostSoftwareInstallAttempts", testCountHostSoftwareInstallAttempts},
|
||||
{"ListSoftwareVersionsSearchByTitleName", testListSoftwareVersionsSearchByTitleName},
|
||||
{"ListSoftwareInventoryDeletedHost", testListSoftwareInventoryDeletedHost},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
@@ -10672,3 +10673,63 @@ func testListSoftwareVersionsSearchByTitleName(t *testing.T, ds *Datastore) {
|
||||
require.Len(t, software, 1, "Search by software name should still work")
|
||||
assert.Equal(t, "Office Runtime Libraries", software[0].Name)
|
||||
}
|
||||
|
||||
// This test verifies the fix for https://github.com/fleetdm/fleet/issues/33815
|
||||
func testListSoftwareInventoryDeletedHost(t *testing.T, ds *Datastore) {
|
||||
ctx := t.Context()
|
||||
|
||||
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
||||
user := test.NewUser(t, ds, "User", "test@example.com", true)
|
||||
|
||||
tfr, err := fleet.NewTempFileReader(strings.NewReader("content"), t.TempDir)
|
||||
require.NoError(t, err)
|
||||
installerID, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "echo 'installing'",
|
||||
InstallerFile: tfr,
|
||||
StorageID: "storage1",
|
||||
Filename: "installer.pkg",
|
||||
Title: "Software",
|
||||
Version: "1.0",
|
||||
Source: "apps",
|
||||
UserID: user.ID,
|
||||
Platform: host.Platform,
|
||||
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// install the software on the host
|
||||
installUUID, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerID, fleet.HostSoftwareInstallOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
|
||||
HostID: host.ID,
|
||||
InstallUUID: installUUID,
|
||||
PreInstallConditionOutput: ptr.String("ok"),
|
||||
InstallScriptExitCode: ptr.Int(0),
|
||||
PostInstallScriptExitCode: ptr.Int(0),
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := fleet.HostSoftwareTitleListOptions{
|
||||
ListOptions: fleet.ListOptions{
|
||||
OrderKey: "name",
|
||||
},
|
||||
OnlyAvailableForInstall: true,
|
||||
}
|
||||
software, _, err := ds.ListHostSoftware(ctx, host, opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, software, 1)
|
||||
require.Equal(t, "Software", software[0].Name)
|
||||
require.Equal(t, titleID, software[0].ID)
|
||||
|
||||
err = ds.DeleteHost(ctx, host.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// it should still show up as available for install (still part of the inventory
|
||||
// and the datastore layer does not check if host exists)
|
||||
software, _, err = ds.ListHostSoftware(ctx, host, opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, software, 1)
|
||||
require.Equal(t, "Software", software[0].Name)
|
||||
require.Equal(t, titleID, software[0].ID)
|
||||
}
|
||||
|
||||
@@ -2911,3 +2911,220 @@ func (s *integrationMDMTestSuite) TestStickyMDMTeamEnrollment() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This test verifies the fix for https://github.com/fleetdm/fleet/issues/33815
|
||||
func (s *integrationMDMTestSuite) TestSoftwareInventoryForADEMacOSAfterWipeAndReenroll() {
|
||||
t := s.T()
|
||||
s.enableABM(t.Name())
|
||||
s.setSkipWorkerJobs(t)
|
||||
ctx := t.Context()
|
||||
|
||||
user, err := s.ds.UserByEmail(context.Background(), "admin1@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
devices := []godep.Device{
|
||||
{SerialNumber: uuid.New().String(), Model: "MacBook Pro", OS: "osx", OpType: "added"},
|
||||
}
|
||||
profileAssignmentReqs := []profileAssignmentReq{}
|
||||
|
||||
s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
encoder := json.NewEncoder(w)
|
||||
switch r.URL.Path {
|
||||
case "/session":
|
||||
err := encoder.Encode(map[string]string{"auth_session_token": "xyz"})
|
||||
require.NoError(t, err)
|
||||
case "/profile":
|
||||
err := encoder.Encode(godep.ProfileResponse{ProfileUUID: uuid.New().String()})
|
||||
require.NoError(t, err)
|
||||
case "/server/devices":
|
||||
err := encoder.Encode(godep.DeviceResponse{Devices: devices[:1]})
|
||||
require.NoError(t, err)
|
||||
case "/devices/sync":
|
||||
err := encoder.Encode(godep.DeviceResponse{Devices: devices, Cursor: "foo"})
|
||||
require.NoError(t, err)
|
||||
case "/profile/devices":
|
||||
b, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
var prof profileAssignmentReq
|
||||
require.NoError(t, json.Unmarshal(b, &prof))
|
||||
profileAssignmentReqs = append(profileAssignmentReqs, prof)
|
||||
var resp godep.ProfileResponse
|
||||
resp.ProfileUUID = prof.ProfileUUID
|
||||
resp.Devices = make(map[string]string, len(prof.Devices))
|
||||
for _, device := range prof.Devices {
|
||||
resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess)
|
||||
}
|
||||
err = encoder.Encode(resp)
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
_, _ = w.Write([]byte(`{}`))
|
||||
}
|
||||
}))
|
||||
|
||||
s.pushProvider.PushFunc = func(_ context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
|
||||
return map[string]*push.Response{}, nil
|
||||
}
|
||||
|
||||
performHostEnroll := func() *mdmtest.TestAppleMDMClient {
|
||||
// Enroll the host via ADE
|
||||
depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
|
||||
mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
|
||||
mdmDevice.SerialNumber = devices[0].SerialNumber
|
||||
err = mdmDevice.Enroll()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simulate an osquery enrollment too
|
||||
// set an enroll secret
|
||||
var applyResp applyEnrollSecretSpecResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
|
||||
Spec: &fleet.EnrollSecretSpec{
|
||||
Secrets: []*fleet.EnrollSecret{{Secret: t.Name()}},
|
||||
},
|
||||
}, http.StatusOK, &applyResp)
|
||||
|
||||
// simulate a matching host enrolling via osquery
|
||||
j, err := json.Marshal(&contract.EnrollOsqueryAgentRequest{
|
||||
EnrollSecret: t.Name(),
|
||||
HostIdentifier: mdmDevice.UUID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var enrollResp contract.EnrollOsqueryAgentResponse
|
||||
hres := s.DoRawNoAuth("POST", "/api/osquery/enroll", j, http.StatusOK)
|
||||
require.NoError(t, json.NewDecoder(hres.Body).Decode(&enrollResp))
|
||||
require.NotEmpty(t, enrollResp.NodeKey)
|
||||
|
||||
return mdmDevice
|
||||
}
|
||||
|
||||
mdmDevice := performHostEnroll()
|
||||
|
||||
listHostsRes := listHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
||||
require.Len(t, listHostsRes.Hosts, 1)
|
||||
h := listHostsRes.Hosts[0]
|
||||
|
||||
// ensure the host has an orbit key (so it doesn't fail with "does not have fleetd")
|
||||
h.OrbitNodeKey = ptr.String("some-orbit-key")
|
||||
err = s.ds.UpdateHost(ctx, h.Host)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.runDEPSchedule()
|
||||
|
||||
// run the worker to process the DEP enroll request
|
||||
s.runWorker()
|
||||
// run the cron to assign configuration profiles
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
cmd, err := mdmDevice.Idle()
|
||||
require.NoError(t, err)
|
||||
for cmd != nil {
|
||||
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// add a couple software installers to "no team"
|
||||
tfr1, err := fleet.NewTempFileReader(strings.NewReader("installer1"), t.TempDir)
|
||||
require.NoError(t, err)
|
||||
installerPayload1 := fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "installer1",
|
||||
PreInstallQuery: "SELECT 1",
|
||||
InstallerFile: tfr1,
|
||||
StorageID: "installer1",
|
||||
Filename: "installer1.pkg",
|
||||
Title: "installer1",
|
||||
Version: "1.0",
|
||||
Source: "apps",
|
||||
UserID: user.ID,
|
||||
TeamID: nil,
|
||||
Platform: string(fleet.MacOSPlatform),
|
||||
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
||||
}
|
||||
_, titleID1, err := s.ds.MatchOrCreateSoftwareInstaller(ctx, &installerPayload1)
|
||||
require.NoError(t, err)
|
||||
|
||||
tfr2, err := fleet.NewTempFileReader(strings.NewReader("installer2"), t.TempDir)
|
||||
require.NoError(t, err)
|
||||
installerPayload2 := fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "installer2",
|
||||
PreInstallQuery: "SELECT 1",
|
||||
InstallerFile: tfr2,
|
||||
StorageID: "installer2",
|
||||
Title: "installer2",
|
||||
Version: "2.0",
|
||||
Source: "apps",
|
||||
UserID: user.ID,
|
||||
TeamID: nil,
|
||||
Platform: string(fleet.MacOSPlatform),
|
||||
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
||||
}
|
||||
_, titleID2, err := s.ds.MatchOrCreateSoftwareInstaller(ctx, &installerPayload2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// list host software inventory, both installers are listed
|
||||
getHostSw := getHostSoftwareResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h.ID), nil, http.StatusOK, &getHostSw, "available_for_install", "true")
|
||||
require.Len(t, getHostSw.Software, 2)
|
||||
require.Equal(t, titleID1, getHostSw.Software[0].ID)
|
||||
require.Equal(t, installerPayload1.Title, getHostSw.Software[0].Name)
|
||||
require.Equal(t, titleID2, getHostSw.Software[1].ID)
|
||||
require.Equal(t, installerPayload2.Title, getHostSw.Software[1].Name)
|
||||
|
||||
// install the first installer on the host
|
||||
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", h.ID, titleID1), installSoftwareRequest{}, http.StatusAccepted)
|
||||
installUUID := getLatestSoftwareInstallExecID(t, s.ds, h.ID)
|
||||
|
||||
// process installation successfully
|
||||
s.Do("POST", "/api/fleet/orbit/software_install/result", orbitPostSoftwareInstallResultRequest{
|
||||
OrbitNodeKey: *h.OrbitNodeKey,
|
||||
HostSoftwareInstallResultPayload: &fleet.HostSoftwareInstallResultPayload{
|
||||
HostID: h.ID,
|
||||
InstallUUID: installUUID,
|
||||
InstallScriptExitCode: ptr.Int(0),
|
||||
InstallScriptOutput: ptr.String("done"),
|
||||
},
|
||||
}, http.StatusNoContent)
|
||||
|
||||
// wipe the host
|
||||
var wipeResp wipeHostResponse
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", h.ID), nil, http.StatusOK, &wipeResp)
|
||||
require.Equal(t, fleet.PendingActionWipe, wipeResp.PendingAction)
|
||||
|
||||
// simulate a successful MDM result for the wipe command
|
||||
cmd, err = mdmDevice.Idle()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cmd)
|
||||
require.Equal(t, "EraseDevice", cmd.Command.RequestType)
|
||||
_, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// refresh the host's status, it is wiped
|
||||
var getHostResp getHostResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &getHostResp)
|
||||
require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
|
||||
require.Equal(t, "wiped", *getHostResp.Host.MDM.DeviceStatus)
|
||||
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
|
||||
require.Equal(t, "", *getHostResp.Host.MDM.PendingAction)
|
||||
|
||||
// delete the host record (will not really delete it as it is in ABM)
|
||||
var delResp deleteHostResponse
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &delResp)
|
||||
|
||||
listHostsRes = listHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
|
||||
require.Len(t, listHostsRes.Hosts, 1)
|
||||
require.Equal(t, h.ID, listHostsRes.Hosts[0].ID)
|
||||
|
||||
// re-enroll the host
|
||||
performHostEnroll()
|
||||
|
||||
// Sofware inventory should list both installers
|
||||
getHostSw = getHostSoftwareResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h.ID), nil, http.StatusOK, &getHostSw, "available_for_install", "true")
|
||||
require.Len(t, getHostSw.Software, 2)
|
||||
require.Equal(t, titleID1, getHostSw.Software[0].ID)
|
||||
require.Equal(t, installerPayload1.Title, getHostSw.Software[0].Name)
|
||||
require.Equal(t, titleID2, getHostSw.Software[1].ID)
|
||||
require.Equal(t, installerPayload2.Title, getHostSw.Software[1].Name)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user