Add hash_sha256 field to host installer result and install software activity (#49085)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves # - Adds hash_sha256 field to host installer result and install software activity so they can be used in combination to cover past activities with existing installers + all new activities even if their installer will get deleted # Checklist for submitter ## 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 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Software installation results now include the installer’s SHA-256 hash when available. * Installed-software activity records now include the corresponding `hash_sha256` value. * **Bug Fixes** * Hash values are retained in activity history after an installer is deleted, while live results correctly show the value as unavailable. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1999,6 +1999,7 @@ SELECT
|
||||
COALESCE(st.name, hsi.software_title_name) AS software_title,
|
||||
hsi.software_title_id,
|
||||
hsi.software_installer_id,
|
||||
si.storage_id AS hash_sha256,
|
||||
COALESCE(hsi.execution_status, '') AS status,
|
||||
hsi.installer_filename AS software_package,
|
||||
hsi.user_id AS user_id,
|
||||
@@ -2014,6 +2015,7 @@ SELECT
|
||||
FROM
|
||||
host_software_installs hsi
|
||||
LEFT JOIN software_titles st ON hsi.software_title_id = st.id
|
||||
LEFT JOIN software_installers si ON hsi.software_installer_id = si.id
|
||||
WHERE
|
||||
hsi.execution_id = :execution_id AND
|
||||
hsi.uninstall = 0 AND
|
||||
@@ -2030,6 +2032,7 @@ SELECT
|
||||
COALESCE(st.name, ua.payload->>'$.software_title_name') AS software_title,
|
||||
siua.software_title_id,
|
||||
siua.software_installer_id,
|
||||
si.storage_id AS hash_sha256,
|
||||
'pending_install' AS status,
|
||||
ua.payload->>'$.installer_filename' AS software_package,
|
||||
ua.user_id AS user_id,
|
||||
@@ -2048,6 +2051,8 @@ FROM
|
||||
ON ua.id = siua.upcoming_activity_id
|
||||
LEFT JOIN software_titles st
|
||||
ON siua.software_title_id = st.id
|
||||
LEFT JOIN software_installers si
|
||||
ON siua.software_installer_id = si.id
|
||||
WHERE
|
||||
ua.execution_id = :execution_id AND
|
||||
ua.activity_type = 'software_install' AND
|
||||
|
||||
@@ -922,6 +922,7 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// create a host and software installer
|
||||
swFilename := "file_" + tc.name + ".pkg"
|
||||
swStorageID := "hash_" + tc.name
|
||||
installerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
Title: "foo" + tc.name,
|
||||
Source: "bar" + tc.name,
|
||||
@@ -929,6 +930,7 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
||||
Version: "1.11",
|
||||
TeamID: &teamID,
|
||||
Filename: swFilename,
|
||||
StorageID: swStorageID,
|
||||
UserID: user1.ID,
|
||||
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
||||
})
|
||||
@@ -979,6 +981,9 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
||||
res, err = ds.GetSoftwareInstallResults(ctx, installUUID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, swFilename, res.SoftwarePackage)
|
||||
// hash comes from the installer, which still exists here
|
||||
require.NotNil(t, res.HashSHA256)
|
||||
require.Equal(t, swStorageID, *res.HashSHA256)
|
||||
|
||||
// delete installer to confirm that we can still access the install record (unless pending)
|
||||
err = ds.DeleteSoftwareInstaller(ctx, installerID)
|
||||
@@ -1006,6 +1011,8 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, installUUID, res.InstallUUID)
|
||||
require.Equal(t, tc.expectedStatus, res.Status)
|
||||
require.Equal(t, swFilename, res.SoftwarePackage)
|
||||
// installer was deleted, so its hash is no longer available
|
||||
require.Nil(t, res.HashSHA256)
|
||||
require.Equal(t, host.ID, res.HostID)
|
||||
require.Equal(t, tc.preInstallQueryOutput, res.PreInstallQueryOutput)
|
||||
require.Equal(t, tc.postInstallScriptOutput, res.PostInstallScriptOutput)
|
||||
|
||||
@@ -1116,6 +1116,7 @@ type ActivityTypeInstalledSoftware struct {
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
SoftwareTitle string `json:"software_title"`
|
||||
SoftwarePackage string `json:"software_package"`
|
||||
HashSHA256 *string `json:"hash_sha256,omitempty"`
|
||||
SelfService bool `json:"self_service"`
|
||||
InstallUUID string `json:"install_uuid"`
|
||||
Status string `json:"status"`
|
||||
|
||||
@@ -449,6 +449,9 @@ type HostSoftwareInstallerResult struct {
|
||||
SoftwareInstallerID *uint `json:"-" db:"software_installer_id"`
|
||||
// SoftwarePackage is the name of the software installer package.
|
||||
SoftwarePackage string `json:"software_package" db:"software_package"`
|
||||
// HashSHA256 is the SHA256 hash of the software installer package. It is
|
||||
// nil when the installer has been deleted from the server.
|
||||
HashSHA256 *string `json:"hash_sha256" db:"hash_sha256"`
|
||||
// Source is the osquery source for this software (e.g., "sh_packages", "ps1_packages").
|
||||
Source *string `json:"source" db:"source"`
|
||||
// HostID is the ID of the host.
|
||||
|
||||
@@ -13271,6 +13271,12 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
|
||||
},
|
||||
}, http.StatusNoContent)
|
||||
|
||||
// while the installer exists, the install results carry its hash
|
||||
beforeDelete, err := s.ds.GetSoftwareInstallResults(context.Background(), installUUID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, beforeDelete.HashSHA256)
|
||||
wantHash := *beforeDelete.HashSHA256
|
||||
|
||||
_ = s.Do("POST", "/api/fleet/orbit/software_install/package?alt=media", fleet.OrbitDownloadSoftwareInstallerRequest{
|
||||
InstallerID: installerID,
|
||||
OrbitNodeKey: *hostInTeam.OrbitNodeKey,
|
||||
@@ -13282,6 +13288,21 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD
|
||||
// check activity
|
||||
s.lastActivityOfTypeMatches(fleet.ActivityTypeDeletedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "software_icon_url": null, "team_name": "%s", "team_id": %d, "fleet_name": "%s", "fleet_id": %d, "self_service": true}`, createTeamResp.Team.Name, createTeamResp.Team.ID, createTeamResp.Team.Name, createTeamResp.Team.ID), 0)
|
||||
|
||||
// the installed_software activity keeps its hash after the installer is deleted
|
||||
var hostActivities listActivitiesResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities", hostInTeam.ID), nil, http.StatusOK, &hostActivities)
|
||||
require.Len(t, hostActivities.Activities, 1)
|
||||
require.Equal(t, fleet.ActivityTypeInstalledSoftware{}.ActivityName(), hostActivities.Activities[0].Type)
|
||||
var installedActivity fleet.ActivityTypeInstalledSoftware
|
||||
require.NoError(t, json.Unmarshal([]byte(*hostActivities.Activities[0].Details), &installedActivity))
|
||||
require.NotNil(t, installedActivity.HashSHA256)
|
||||
require.Equal(t, wantHash, *installedActivity.HashSHA256)
|
||||
|
||||
// but the live install results lose it
|
||||
afterDelete, err := s.ds.GetSoftwareInstallResults(context.Background(), installUUID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, afterDelete.HashSHA256)
|
||||
|
||||
// download the installer, not found anymore
|
||||
s.Do("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d/package?alt=media", titleID), nil, http.StatusNotFound, "team_id", fmt.Sprintf("%d", *payload.TeamID))
|
||||
})
|
||||
@@ -16239,6 +16260,12 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
||||
assert.Greater(t, time.Now(), resp.Results.CreatedAt)
|
||||
}
|
||||
|
||||
installHash := func(installUUID string) *string {
|
||||
res, err := s.ds.GetSoftwareInstallResults(ctx, installUUID)
|
||||
require.NoError(t, err)
|
||||
return res.HashSHA256
|
||||
}
|
||||
|
||||
s.Do("POST", "/api/fleet/orbit/software_install/result",
|
||||
json.RawMessage(fmt.Sprintf(`{
|
||||
"orbit_node_key": %q,
|
||||
@@ -16264,6 +16291,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
||||
InstallUUID: installUUIDs[0],
|
||||
Status: string(fleet.SoftwareInstallFailed),
|
||||
Source: new("deb_packages"),
|
||||
HashSHA256: installHash(installUUIDs[0]),
|
||||
}
|
||||
s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
|
||||
@@ -16289,6 +16317,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
||||
InstallUUID: installUUIDs[1],
|
||||
Status: string(fleet.SoftwareInstallFailed),
|
||||
Source: new("deb_packages"),
|
||||
HashSHA256: installHash(installUUIDs[1]),
|
||||
}
|
||||
s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
|
||||
@@ -16320,6 +16349,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
||||
InstallUUID: installUUIDs[2],
|
||||
Status: string(fleet.SoftwareInstalled),
|
||||
Source: new("deb_packages"),
|
||||
HashSHA256: installHash(installUUIDs[2]),
|
||||
}
|
||||
lastActID := s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
|
||||
@@ -16357,6 +16387,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
||||
InstallUUID: installUUIDs[2],
|
||||
Status: string(fleet.SoftwareInstallFailed),
|
||||
Source: new("deb_packages"),
|
||||
HashSHA256: installHash(installUUIDs[2]),
|
||||
}
|
||||
s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
|
||||
@@ -19043,11 +19074,18 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
|
||||
"install_script_exit_code": 0,
|
||||
"install_script_output": "ok"
|
||||
}`, *host1Team1.OrbitNodeKey, host1LastInstall.ExecutionID)), http.StatusNoContent)
|
||||
var host1InstallerHash string
|
||||
mysqltest.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &host1InstallerHash,
|
||||
`SELECT si.storage_id FROM host_software_installs hsi JOIN software_installers si ON hsi.software_installer_id = si.id WHERE hsi.execution_id = ?`,
|
||||
host1LastInstall.ExecutionID)
|
||||
})
|
||||
s.lastActivityMatches(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), fmt.Sprintf(`{
|
||||
"host_id": %d,
|
||||
"host_display_name": "%s",
|
||||
"software_title": "%s",
|
||||
"software_package": "%s",
|
||||
"hash_sha256": "%s",
|
||||
"self_service": false,
|
||||
"install_uuid": "%s",
|
||||
"status": "installed",
|
||||
@@ -19055,7 +19093,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
|
||||
"policy_id": %d,
|
||||
"policy_name": "%s",
|
||||
"from_setup_experience": false
|
||||
}`, host1Team1.ID, host1Team1.DisplayName(), "DummyApp", "dummy_installer.pkg", host1LastInstall.ExecutionID, policy1Team1.ID, policy1Team1.Name), 0)
|
||||
}`, host1Team1.ID, host1Team1.DisplayName(), "DummyApp", "dummy_installer.pkg", host1InstallerHash, host1LastInstall.ExecutionID, policy1Team1.ID, policy1Team1.Name), 0)
|
||||
|
||||
var activityCount int
|
||||
mysqltest.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
@@ -21189,6 +21227,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareUploadRPM() {
|
||||
InstallUUID: installUUID,
|
||||
Status: string(fleet.SoftwareInstallFailed),
|
||||
Source: new("rpm_packages"),
|
||||
HashSHA256: resp.Results.HashSHA256,
|
||||
}
|
||||
s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
}
|
||||
@@ -22396,6 +22435,8 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerOrbitDownloadFailu
|
||||
require.Equal(t, scriptExecID, listUpcomingAct.Activities[0].UUID)
|
||||
|
||||
// past activity is created for the software install
|
||||
installRes, err := s.ds.GetSoftwareInstallResults(context.Background(), swInstallExecID)
|
||||
require.NoError(t, err)
|
||||
wantAct := fleet.ActivityTypeInstalledSoftware{
|
||||
HostID: host.ID,
|
||||
HostDisplayName: host.DisplayName(),
|
||||
@@ -22404,6 +22445,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerOrbitDownloadFailu
|
||||
InstallUUID: swInstallExecID,
|
||||
Status: string(fleet.SoftwareInstalled),
|
||||
Source: new("deb_packages"),
|
||||
HashSHA256: installRes.HashSHA256,
|
||||
}
|
||||
s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
}
|
||||
|
||||
@@ -498,6 +498,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
|
||||
"host_display_name": "%s",
|
||||
"software_title": "%s",
|
||||
"software_package": "%s",
|
||||
"hash_sha256": "%s",
|
||||
"self_service": false,
|
||||
"install_uuid": "%s",
|
||||
"status": "installed",
|
||||
@@ -506,7 +507,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
|
||||
"policy_name": null,
|
||||
"from_setup_experience": true
|
||||
}
|
||||
`, enrolledHost.ID, getHostResp.Host.DisplayName, statusResp.Results.Software[0].Name, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage.Name, installUUID)
|
||||
`, enrolledHost.ID, getHostResp.Host.DisplayName, statusResp.Results.Software[0].Name, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage.Name, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage.StorageID, installUUID)
|
||||
|
||||
s.lastActivityMatchesExtended(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), expectedActivityDetail, 0, ptr.Bool(true))
|
||||
|
||||
@@ -931,6 +932,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithFMAAndVersionRollba
|
||||
"host_display_name": "%s",
|
||||
"software_title": "1Password",
|
||||
"software_package": "%s",
|
||||
"hash_sha256": "%s",
|
||||
"self_service": false,
|
||||
"install_uuid": "%s",
|
||||
"status": "installed",
|
||||
@@ -939,7 +941,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithFMAAndVersionRollba
|
||||
"policy_name": null,
|
||||
"from_setup_experience": true
|
||||
}
|
||||
`, enrolledHost.ID, getHostResp.Host.DisplayName, titleDetail.SoftwareTitle.SoftwarePackage.Name, installUUID)
|
||||
`, enrolledHost.ID, getHostResp.Host.DisplayName, titleDetail.SoftwareTitle.SoftwarePackage.Name, titleDetail.SoftwareTitle.SoftwarePackage.StorageID, installUUID)
|
||||
s.lastActivityMatchesExtended(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), expectedActivityDetail, 0, ptr.Bool(true))
|
||||
}
|
||||
|
||||
|
||||
@@ -1710,6 +1710,7 @@ func (svc *Service) SaveHostSoftwareInstallResult(ctx context.Context, result *f
|
||||
HostDisplayName: host.DisplayName(),
|
||||
SoftwareTitle: hsi.SoftwareTitle,
|
||||
SoftwarePackage: hsi.SoftwarePackage,
|
||||
HashSHA256: hsi.HashSHA256,
|
||||
InstallUUID: result.InstallUUID,
|
||||
Status: string(status),
|
||||
Source: hsi.Source,
|
||||
|
||||
Reference in New Issue
Block a user