Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: better filtering to handle de-scoping after uninstall edge case #24963

Merged
merged 2 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion server/datastore/mysql/software.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +2299 to +2300
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can keep an eye on performance and maybe consider adding some index coverage on some of these columns in the future.

WHERE
-- use the latest VPP install attempt only
( hvsi.id IS NULL OR hvsi.id = (
Expand All @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down
113 changes: 113 additions & 0 deletions server/service/integration_enterprise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading