fix: better filtering to handle de-scoping after uninstall edge case (#24963)
> Issue found during testing # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
@@ -2296,6 +2296,8 @@ INNER JOIN software_cve scve ON scve.software_id = s.id
|
||||
host_vpp_software_installs hvsi ON vat.adam_id = hvsi.adam_id AND hvsi.host_id = :host_id AND hvsi.removed = 0
|
||||
LEFT OUTER JOIN
|
||||
nano_command_results ncr ON ncr.command_uuid = hvsi.command_uuid
|
||||
LEFT OUTER JOIN
|
||||
host_script_results hsr ON hsr.host_id = :host_id AND hsr.execution_id = hsi.last_uninstall_execution_id
|
||||
WHERE
|
||||
-- use the latest VPP install attempt only
|
||||
( hvsi.id IS NULL OR hvsi.id = (
|
||||
@@ -2309,6 +2311,67 @@ INNER JOIN software_cve scve ON scve.software_id = s.id
|
||||
-- on host (via installer or VPP app). If only available for install is
|
||||
-- requested, then the software installed on host clause is empty.
|
||||
( %s hsi.host_id IS NOT NULL OR hvsi.host_id IS NOT NULL )
|
||||
AND
|
||||
-- label membership check
|
||||
(
|
||||
-- do the label membership check only for software installers
|
||||
CASE WHEN (si.ID IS NOT NULL AND hsi.last_uninstalled_at IS NOT NULL AND hsr.exit_code = 0) THEN
|
||||
(
|
||||
EXISTS (
|
||||
|
||||
SELECT 1 FROM (
|
||||
|
||||
-- no labels
|
||||
SELECT 0 AS count_installer_labels, 0 AS count_host_labels, 0 as count_host_updated_after_labels
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM software_installer_labels sil WHERE sil.software_installer_id = si.id
|
||||
)
|
||||
|
||||
UNION
|
||||
|
||||
-- include any
|
||||
SELECT
|
||||
COUNT(*) AS count_installer_labels,
|
||||
COUNT(lm.label_id) AS count_host_labels,
|
||||
0 as count_host_updated_after_labels
|
||||
FROM
|
||||
software_installer_labels sil
|
||||
LEFT OUTER JOIN label_membership lm ON lm.label_id = sil.label_id
|
||||
AND lm.host_id = :host_id
|
||||
WHERE
|
||||
sil.software_installer_id = si.id
|
||||
AND sil.exclude = 0
|
||||
HAVING
|
||||
count_installer_labels > 0 AND count_host_labels > 0
|
||||
|
||||
UNION
|
||||
|
||||
-- exclude any, ignore software that depends on labels created
|
||||
-- _after_ the label_updated_at timestamp of the host (because
|
||||
-- we don't have results for that label yet, the host may or may
|
||||
-- not be a member).
|
||||
SELECT
|
||||
COUNT(*) AS count_installer_labels,
|
||||
COUNT(lm.label_id) AS count_host_labels,
|
||||
SUM(CASE WHEN lbl.created_at IS NOT NULL AND :host_label_updated_at >= lbl.created_at THEN 1 ELSE 0 END) as count_host_updated_after_labels
|
||||
FROM
|
||||
software_installer_labels sil
|
||||
LEFT OUTER JOIN labels lbl
|
||||
ON lbl.id = sil.label_id
|
||||
LEFT OUTER JOIN label_membership lm
|
||||
ON lm.label_id = sil.label_id AND lm.host_id = :host_id
|
||||
WHERE
|
||||
sil.software_installer_id = si.id
|
||||
AND sil.exclude = 1
|
||||
HAVING
|
||||
count_installer_labels > 0 AND count_installer_labels = count_host_updated_after_labels AND count_host_labels = 0
|
||||
) t
|
||||
)
|
||||
)
|
||||
-- it's some other type of software that has been checked above
|
||||
ELSE true END
|
||||
)
|
||||
|
||||
%s
|
||||
`, status, softwareIsInstalledOnHostClause, onlySelfServiceClause)
|
||||
|
||||
@@ -2474,12 +2537,12 @@ INNER JOIN software_cve scve ON scve.software_id = s.id
|
||||
"mdm_status_format_error": fleet.MDMAppleStatusCommandFormatError,
|
||||
"global_or_team_id": globalOrTeamID,
|
||||
"is_mdm_enrolled": opts.IsMDMEnrolled,
|
||||
"host_label_updated_at": host.LabelUpdatedAt,
|
||||
}
|
||||
|
||||
stmt := stmtInstalled
|
||||
if opts.OnlyAvailableForInstall || (opts.IncludeAvailableForInstall && !opts.VulnerableOnly) {
|
||||
namedArgs["vpp_apps_platforms"] = fleet.VPPAppsPlatforms
|
||||
namedArgs["host_label_updated_at"] = host.LabelUpdatedAt
|
||||
if fleet.IsLinux(host.Platform) {
|
||||
namedArgs["host_compatible_platforms"] = fleet.HostLinuxOSs
|
||||
} else {
|
||||
|
||||
@@ -16179,3 +16179,116 @@ func (s *integrationEnterpriseTestSuite) TestDeleteLabels() {
|
||||
// delete the unused label2
|
||||
s.DoJSON("DELETE", "/api/v1/fleet/labels/id/"+fmt.Sprint(lbl2), nil, http.StatusOK, &delLabelResp)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestListHostSoftwareWithLabelScoping() {
|
||||
ctx := context.Background()
|
||||
t := s.T()
|
||||
|
||||
host := createOrbitEnrolledHost(t, "linux", "", s.ds)
|
||||
|
||||
// Create software installers and corresponding host install requests.
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install script",
|
||||
PreInstallQuery: "pre install query",
|
||||
PostInstallScript: "post install script",
|
||||
Filename: "ruby.deb",
|
||||
Title: "ruby",
|
||||
}
|
||||
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
|
||||
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "deb_packages")
|
||||
|
||||
latestInstallUUID := func() string {
|
||||
var id string
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &id, `SELECT execution_id FROM host_software_installs ORDER BY id DESC LIMIT 1`)
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
// create install request for the software and record a successful result
|
||||
resp := installSoftwareResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/%d/install", host.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
installUUID := latestInstallUUID()
|
||||
|
||||
s.Do("POST", "/api/fleet/orbit/software_install/result",
|
||||
json.RawMessage(fmt.Sprintf(`{
|
||||
"orbit_node_key": %q,
|
||||
"install_uuid": %q,
|
||||
"pre_install_condition_output": "",
|
||||
"install_script_exit_code": 0,
|
||||
"install_script_output": "success"
|
||||
}`, *host.OrbitNodeKey, installUUID)),
|
||||
http.StatusNoContent)
|
||||
|
||||
// Software is now installed on the host. We should see it in the host software list
|
||||
getHostSw := getHostSoftwareResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw)
|
||||
require.Len(t, getHostSw.Software, 1)
|
||||
require.Equal(t, getHostSw.Software[0].Name, "ruby")
|
||||
|
||||
// De-scope the software by adding an exclude any label that the host has.
|
||||
// TODO(JVE): remove/update this once the API is in place
|
||||
updateInstallerLabel := func(siID, labelID uint, exclude bool) {
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO software_installer_labels (software_installer_id, label_id, exclude) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE exclude = VALUES(exclude)`,
|
||||
siID, labelID, exclude,
|
||||
)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
var installerID uint
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &installerID, "SELECT id FROM software_installers WHERE title_id = ?", titleID)
|
||||
})
|
||||
require.NotEmpty(t, installerID)
|
||||
|
||||
// create some labels and assign them to the host
|
||||
var labelResp createLabelResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/labels", &createLabelRequest{fleet.LabelPayload{
|
||||
Name: "label1",
|
||||
Hosts: []string{host.Hostname},
|
||||
}}, http.StatusOK, &labelResp)
|
||||
require.NotZero(t, labelResp.Label.ID)
|
||||
lbl1 := labelResp.Label
|
||||
|
||||
s.DoJSON("POST", "/api/latest/fleet/labels", &createLabelRequest{fleet.LabelPayload{
|
||||
Name: "label2",
|
||||
Query: "SELECT 1",
|
||||
}}, http.StatusOK, &labelResp)
|
||||
require.NotZero(t, labelResp.Label.ID)
|
||||
lbl2 := labelResp.Label
|
||||
err := s.ds.RecordLabelQueryExecutions(context.Background(), host, map[uint]*bool{lbl2.ID: ptr.Bool(true)}, time.Now(), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
updateInstallerLabel(installerID, lbl1.ID, true)
|
||||
updateInstallerLabel(installerID, lbl2.ID, true)
|
||||
|
||||
// We should still see the software at this point, because we haven't uninstalled it yet
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw)
|
||||
require.Len(t, getHostSw.Software, 1)
|
||||
|
||||
// uninstall the software
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/uninstall", host.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw)
|
||||
require.Len(t, getHostSw.Software, 1)
|
||||
assert.NotNil(t, getHostSw.Software[0].SoftwarePackage.LastInstall)
|
||||
assert.Equal(t, fleet.SoftwareUninstallPending, *getHostSw.Software[0].Status)
|
||||
require.NotNil(t, getHostSw.Software[0].SoftwarePackage.LastUninstall)
|
||||
uninstallExecutionID := getHostSw.Software[0].SoftwarePackage.LastUninstall.ExecutionID
|
||||
|
||||
// Host sends failed uninstall result
|
||||
var orbitPostScriptResp orbitPostScriptResultResponse
|
||||
s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
|
||||
json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "uninstall"}`, *host.OrbitNodeKey,
|
||||
uninstallExecutionID)),
|
||||
http.StatusOK, &orbitPostScriptResp)
|
||||
|
||||
// Now that the software is uninstalled, we should no longer see it in the host software list,
|
||||
// because it is de-scoped via labels.
|
||||
getHostSw = getHostSoftwareResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw)
|
||||
require.Empty(t, getHostSw.Software)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user