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:
Jahziel Villasana-Espinoza
2024-12-20 16:56:51 -05:00
committed by GitHub
parent 8694e981d5
commit 4a4ebfb156
2 changed files with 177 additions and 1 deletions
+64 -1
View File
@@ -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)
}