From b2a50402392c6b09f42cd5718ab0c47174c7710c Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 17 Dec 2024 14:32:54 -0500 Subject: [PATCH 1/4] Update software add/edit/delete activities --- docs/Contributing/Audit-logs.md | 42 +++++++++++++-- server/fleet/activities.go | 91 +++++++++++++++++++++++++-------- 2 files changed, 108 insertions(+), 25 deletions(-) diff --git a/docs/Contributing/Audit-logs.md b/docs/Contributing/Audit-logs.md index 6e0e4462aec2..fc393fe5c03a 100644 --- a/docs/Contributing/Audit-logs.md +++ b/docs/Contributing/Audit-logs.md @@ -1242,6 +1242,8 @@ This activity contains the following fields: - "team_id": The ID of the team to which this software was added. `null` if it was added to no team. - "self_service": Whether the software is available for installation by the end user. - "software_title_id": ID of the added software title. +- "labels_include_any": Target hosts that have any label in the array. +- "labels_exclude_any": Target hosts that don't have any label in the array. #### Example @@ -1252,7 +1254,17 @@ This activity contains the following fields: "team_name": "Workstations", "team_id": 123, "self_service": true, - "software_title_id": 2234 + "software_title_id": 2234, + "labels_include_any": [ + { + "name": "Engineering", + "id": 12 + }, + { + "name": "Product", + "id": 17 + } + ] } ``` @@ -1266,6 +1278,8 @@ This activity contains the following fields: - "team_name": Name of the team on which this software was updated. `null` if it was updated on no team. - "team_id": The ID of the team on which this software was updated. `null` if it was updated on no team. - "self_service": Whether the software is available for installation by the end user. +- "labels_include_any": Target hosts that have any label in the array. +- "labels_exclude_any": Target hosts that don't have any label in the array. #### Example @@ -1275,7 +1289,17 @@ This activity contains the following fields: "software_package": "FalconSensor-6.44.pkg", "team_name": "Workstations", "team_id": 123, - "self_service": true + "self_service": true, + "labels_include_any": [ + { + "name": "Engineering", + "id": 12 + }, + { + "name": "Product", + "id": 17 + } + ] } ``` @@ -1289,6 +1313,8 @@ This activity contains the following fields: - "team_name": Name of the team to which this software was added. `null` if it was added to no team. - "team_id": The ID of the team to which this software was added. `null` if it was added to no team. - "self_service": Whether the software was available for installation by the end user. +- "labels_include_any": Target hosts that have any label in the array. +- "labels_exclude_any": Target hosts that don't have any label in the array. #### Example @@ -1298,7 +1324,17 @@ This activity contains the following fields: "software_package": "FalconSensor-6.44.pkg", "team_name": "Workstations", "team_id": 123, - "self_service": true + "self_service": true, + "labels_include_any": [ + { + "name": "Engineering", + "id": 12 + }, + { + "name": "Product", + "id": 17 + } + ] } ``` diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 9450d03c6d52..531a598fcafe 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -1663,13 +1663,20 @@ func (a ActivityTypeUninstalledSoftware) Documentation() (activity, details, det }` } +type ActivitySoftwareLabel struct { + Name string `json:"name"` + ID uint `json:"id"` +} + type ActivityTypeAddedSoftware struct { - SoftwareTitle string `json:"software_title"` - SoftwarePackage string `json:"software_package"` - TeamName *string `json:"team_name"` - TeamID *uint `json:"team_id"` - SelfService bool `json:"self_service"` - SoftwareTitleID uint `json:"software_title_id"` + SoftwareTitle string `json:"software_title"` + SoftwarePackage string `json:"software_package"` + TeamName *string `json:"team_name"` + TeamID *uint `json:"team_id"` + SelfService bool `json:"self_service"` + SoftwareTitleID uint `json:"software_title_id"` + LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"` + LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"` } func (a ActivityTypeAddedSoftware) ActivityName() string { @@ -1683,22 +1690,36 @@ func (a ActivityTypeAddedSoftware) Documentation() (string, string, string) { - "team_name": Name of the team to which this software was added.` + " `null` " + `if it was added to no team." + - "team_id": The ID of the team to which this software was added.` + " `null` " + `if it was added to no team. - "self_service": Whether the software is available for installation by the end user. -- "software_title_id": ID of the added software title.`, `{ +- "software_title_id": ID of the added software title. +- "labels_include_any": Target hosts that have any label in the array. +- "labels_exclude_any": Target hosts that don't have any label in the array.`, `{ "software_title": "Falcon.app", "software_package": "FalconSensor-6.44.pkg", "team_name": "Workstations", "team_id": 123, "self_service": true, - "software_title_id": 2234 + "software_title_id": 2234, + "labels_include_any": [ + { + "name": "Engineering", + "id": 12 + }, + { + "name": "Product", + "id": 17 + } + ] }` } type ActivityTypeEditedSoftware struct { - SoftwareTitle string `json:"software_title"` - SoftwarePackage *string `json:"software_package"` - TeamName *string `json:"team_name"` - TeamID *uint `json:"team_id"` - SelfService bool `json:"self_service"` + SoftwareTitle string `json:"software_title"` + SoftwarePackage *string `json:"software_package"` + TeamName *string `json:"team_name"` + TeamID *uint `json:"team_id"` + SelfService bool `json:"self_service"` + LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"` + LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"` } func (a ActivityTypeEditedSoftware) ActivityName() string { @@ -1711,21 +1732,35 @@ func (a ActivityTypeEditedSoftware) Documentation() (string, string, string) { - "software_package": Filename of the installer as of this update (including if unchanged). - "team_name": Name of the team on which this software was updated.` + " `null` " + `if it was updated on no team. - "team_id": The ID of the team on which this software was updated.` + " `null` " + `if it was updated on no team. -- "self_service": Whether the software is available for installation by the end user.`, `{ +- "self_service": Whether the software is available for installation by the end user. +- "labels_include_any": Target hosts that have any label in the array. +- "labels_exclude_any": Target hosts that don't have any label in the array.`, `{ "software_title": "Falcon.app", "software_package": "FalconSensor-6.44.pkg", "team_name": "Workstations", "team_id": 123, - "self_service": true + "self_service": true, + "labels_include_any": [ + { + "name": "Engineering", + "id": 12 + }, + { + "name": "Product", + "id": 17 + } + ] }` } type ActivityTypeDeletedSoftware struct { - SoftwareTitle string `json:"software_title"` - SoftwarePackage string `json:"software_package"` - TeamName *string `json:"team_name"` - TeamID *uint `json:"team_id"` - SelfService bool `json:"self_service"` + SoftwareTitle string `json:"software_title"` + SoftwarePackage string `json:"software_package"` + TeamName *string `json:"team_name"` + TeamID *uint `json:"team_id"` + SelfService bool `json:"self_service"` + LabelsIncludeAny []ActivitySoftwareLabel `json:"labels_include_any,omitempty"` + LabelsExcludeAny []ActivitySoftwareLabel `json:"labels_exclude_any,omitempty"` } func (a ActivityTypeDeletedSoftware) ActivityName() string { @@ -1738,12 +1773,24 @@ func (a ActivityTypeDeletedSoftware) Documentation() (string, string, string) { - "software_package": Filename of the installer. - "team_name": Name of the team to which this software was added.` + " `null` " + `if it was added to no team. - "team_id": The ID of the team to which this software was added.` + " `null` " + `if it was added to no team. -- "self_service": Whether the software was available for installation by the end user.`, `{ +- "self_service": Whether the software was available for installation by the end user. +- "labels_include_any": Target hosts that have any label in the array. +- "labels_exclude_any": Target hosts that don't have any label in the array.`, `{ "software_title": "Falcon.app", "software_package": "FalconSensor-6.44.pkg", "team_name": "Workstations", "team_id": 123, - "self_service": true + "self_service": true, + "labels_include_any": [ + { + "name": "Engineering", + "id": 12 + }, + { + "name": "Product", + "id": 17 + } + ] }` } From 4281486929bf7d8e708d72075f963a3f33524982 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 17 Dec 2024 15:01:02 -0500 Subject: [PATCH 2/4] Assign label fields to software installer activities --- ee/server/service/maintained_apps.go | 15 +++-- ee/server/service/software_installers.go | 79 +++++++++++++++++++----- 2 files changed, 72 insertions(+), 22 deletions(-) diff --git a/ee/server/service/maintained_apps.go b/ee/server/service/maintained_apps.go index 4056ec065acd..c27c1d0d0cb5 100644 --- a/ee/server/service/maintained_apps.go +++ b/ee/server/service/maintained_apps.go @@ -146,13 +146,16 @@ func (svc *Service) AddFleetMaintainedApp( teamName = &t.Name } + actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeAddedSoftware{ - SoftwareTitle: payload.Title, - SoftwarePackage: payload.Filename, - TeamName: teamName, - TeamID: payload.TeamID, - SelfService: payload.SelfService, - SoftwareTitleID: titleID, + SoftwareTitle: payload.Title, + SoftwarePackage: payload.Filename, + TeamName: teamName, + TeamID: payload.TeamID, + SelfService: payload.SelfService, + SoftwareTitleID: titleID, + LabelsIncludeAny: actLabelsIncl, + LabelsExcludeAny: actLabelsExcl, }); err != nil { return 0, ctxerr.Wrap(ctx, err, "creating activity for added software") } diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index f6ed7b3aace7..23afcc9c6f2a 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -89,13 +89,16 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet. } // Create activity + actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromValidatedLabels(payload.ValidatedLabels) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeAddedSoftware{ - SoftwareTitle: payload.Title, - SoftwarePackage: payload.Filename, - TeamName: teamName, - TeamID: payload.TeamID, - SelfService: payload.SelfService, - SoftwareTitleID: titleID, + SoftwareTitle: payload.Title, + SoftwarePackage: payload.Filename, + TeamName: teamName, + TeamID: payload.TeamID, + SelfService: payload.SelfService, + SoftwareTitleID: titleID, + LabelsIncludeAny: actLabelsIncl, + LabelsExcludeAny: actLabelsExcl, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for added software") } @@ -214,12 +217,16 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet. dirty["SelfService"] = true } + actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels( + existingInstaller.LabelsIncludeAny, existingInstaller.LabelsExcludeAny) activity := fleet.ActivityTypeEditedSoftware{ - SoftwareTitle: existingInstaller.SoftwareTitle, - TeamName: teamName, - TeamID: payload.TeamID, - SelfService: existingInstaller.SelfService, - SoftwarePackage: &existingInstaller.Name, + SoftwareTitle: existingInstaller.SoftwareTitle, + TeamName: teamName, + TeamID: payload.TeamID, + SelfService: existingInstaller.SelfService, + SoftwarePackage: &existingInstaller.Name, + LabelsIncludeAny: actLabelsIncl, + LabelsExcludeAny: actLabelsExcl, } var payloadForNewInstallerFile *fleet.UploadSoftwareInstallerPayload @@ -466,12 +473,15 @@ func (svc *Service) deleteSoftwareInstaller(ctx context.Context, meta *fleet.Sof teamID = meta.TeamID } + actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels(meta.LabelsIncludeAny, meta.LabelsExcludeAny) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeDeletedSoftware{ - SoftwareTitle: meta.SoftwareTitle, - SoftwarePackage: meta.Name, - TeamName: teamName, - TeamID: teamID, - SelfService: meta.SelfService, + SoftwareTitle: meta.SoftwareTitle, + SoftwarePackage: meta.Name, + TeamName: teamName, + TeamID: teamID, + SelfService: meta.SelfService, + LabelsIncludeAny: actLabelsIncl, + LabelsExcludeAny: actLabelsExcl, }); err != nil { return ctxerr.Wrap(ctx, err, "creating activity for deleted software") } @@ -1660,3 +1670,40 @@ func UninstallSoftwareMigration( return nil } + +func activitySoftwareLabelsFromValidatedLabels(validatedLabels *fleet.LabelIdentsWithScope) (include, exclude []fleet.ActivitySoftwareLabel) { + if validatedLabels == nil || len(validatedLabels.ByName) == 0 { + return nil, nil + } + + excludeAny := validatedLabels.LabelScope == fleet.LabelScopeExcludeAny + labels := make([]fleet.ActivitySoftwareLabel, 0, len(validatedLabels.ByName)) + for _, lbl := range validatedLabels.ByName { + labels = append(labels, fleet.ActivitySoftwareLabel{ + ID: lbl.LabelID, + Name: lbl.LabelName, + }) + } + if excludeAny { + exclude = labels + } else { + include = labels + } + return include, exclude +} + +func activitySoftwareLabelsFromSoftwareScopeLabels(includeScopeLabels, excludeScopeLabels []fleet.SoftwareScopeLabel) (include, exclude []fleet.ActivitySoftwareLabel) { + for _, label := range includeScopeLabels { + include = append(include, fleet.ActivitySoftwareLabel{ + ID: label.LabelID, + Name: label.LabelName, + }) + } + for _, label := range excludeScopeLabels { + exclude = append(exclude, fleet.ActivitySoftwareLabel{ + ID: label.LabelID, + Name: label.LabelName, + }) + } + return include, exclude +} From 205544b9192581f26df2c30170c051bd0881757d Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 17 Dec 2024 16:19:46 -0500 Subject: [PATCH 3/4] Add tests for activities --- ee/server/service/software_installers.go | 39 ++++++++++--------- server/service/integration_enterprise_test.go | 22 +++++++++-- server/service/integration_mdm_test.go | 12 +++--- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index 23afcc9c6f2a..380dc14eab99 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -22,7 +22,6 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm/apple/vpp" - "github.com/fleetdm/fleet/v4/server/ptr" "github.com/go-kit/log" kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" @@ -217,16 +216,17 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet. dirty["SelfService"] = true } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels( - existingInstaller.LabelsIncludeAny, existingInstaller.LabelsExcludeAny) + // activity team ID must be null if no team, not zero + var actTeamID *uint + if payload.TeamID != nil && *payload.TeamID != 0 { + actTeamID = payload.TeamID + } activity := fleet.ActivityTypeEditedSoftware{ - SoftwareTitle: existingInstaller.SoftwareTitle, - TeamName: teamName, - TeamID: payload.TeamID, - SelfService: existingInstaller.SelfService, - SoftwarePackage: &existingInstaller.Name, - LabelsIncludeAny: actLabelsIncl, - LabelsExcludeAny: actLabelsExcl, + SoftwareTitle: existingInstaller.SoftwareTitle, + TeamName: teamName, + TeamID: actTeamID, + SelfService: existingInstaller.SelfService, + SoftwarePackage: &existingInstaller.Name, } var payloadForNewInstallerFile *fleet.UploadSoftwareInstallerPayload @@ -367,6 +367,15 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet. } } + // now that the payload has been updated with any patches, we can set the + // final fields of the activity + actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels( + existingInstaller.LabelsIncludeAny, existingInstaller.LabelsExcludeAny) + activity.LabelsIncludeAny = actLabelsIncl + activity.LabelsExcludeAny = actLabelsExcl + if payload.SelfService != nil { + activity.SelfService = *payload.SelfService + } if err := svc.NewActivity(ctx, vc.User, activity); err != nil { return nil, ctxerr.Wrap(ctx, err, "creating activity for edited software") } @@ -465,20 +474,12 @@ func (svc *Service) deleteSoftwareInstaller(ctx context.Context, meta *fleet.Sof teamName = &t.Name } - var teamID *uint - switch { - case meta.TeamID == nil: - teamID = ptr.Uint(0) - case meta.TeamID != nil: - teamID = meta.TeamID - } - actLabelsIncl, actLabelsExcl := activitySoftwareLabelsFromSoftwareScopeLabels(meta.LabelsIncludeAny, meta.LabelsExcludeAny) if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeDeletedSoftware{ SoftwareTitle: meta.SoftwareTitle, SoftwarePackage: meta.Name, TeamName: teamName, - TeamID: teamID, + TeamID: meta.TeamID, SelfService: meta.SelfService, LabelsIncludeAny: actLabelsIncl, LabelsExcludeAny: actLabelsExcl, diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 881fc9b28355..4a097f15abdf 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -10699,12 +10699,24 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD _, titleID := checkSoftwareInstaller(t, payload) // check activity - activityData := fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, "team_id": null, "self_service": false, "software_title_id": %d}`, titleID) - s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), activityData, 0) + activityData := fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, + "team_id": null, "self_service": false, "software_title_id": %d, "labels_include_any": [{"id": %d, "name": %q}]}`, + titleID, labelResp.Label.ID, t.Name()) + s.lastActivityMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), activityData, 0) // upload again fails s.uploadSoftwareInstaller(t, payload, http.StatusConflict, "already exists") + // update the installer succeeds + body, headers := generateMultipartRequest(t, "software", + "", []byte{}, s.token, map[string][]string{"self_service": {"true"}, "team_id": {"0"}}) + s.DoRawWithHeaders("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/package", titleID), body.Bytes(), http.StatusOK, headers) + + activityData = fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, + "team_id": null, "self_service": true, "labels_include_any": [{"id": %d, "name": %q}]}`, + labelResp.Label.ID, t.Name()) + s.lastActivityMatches(fleet.ActivityTypeEditedSoftware{}.ActivityName(), activityData, 0) + // orbit-downloading fails with invalid orbit node key s.Do("POST", "/api/fleet/orbit/software_install/package?alt=media", orbitDownloadSoftwareInstallerRequest{ InstallerID: 123, @@ -10719,6 +10731,10 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD // delete from team 0 succeeds s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", titleID), nil, http.StatusNoContent, "team_id", "0") + activityData = fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, + "team_id": null, "self_service": true, "labels_include_any": [{"id": %d, "name": %q}]}`, + labelResp.Label.ID, t.Name()) + s.lastActivityMatches(fleet.ActivityTypeDeletedSoftware{}.ActivityName(), activityData, 0) }) t.Run("create team software installer", func(t *testing.T) { @@ -10935,7 +10951,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndD // check activity s.lastActivityOfTypeMatches(fleet.ActivityTypeDeletedSoftware{}.ActivityName(), - `{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, "team_id": 0, "self_service": true}`, 0) + `{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, "team_id": null, "self_service": true}`, 0) // 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", 0)) diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index fa8f13911dc0..01633975826e 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -4753,10 +4753,12 @@ func generateMultipartRequest(t *testing.T, writer := multipart.NewWriter(&body) // add file content - ff, err := writer.CreateFormFile(uploadFileField, fileName) - require.NoError(t, err) - _, err = io.Copy(ff, bytes.NewReader(fileContent)) - require.NoError(t, err) + if fileName != "" || len(fileContent) > 0 { + ff, err := writer.CreateFormFile(uploadFileField, fileName) + require.NoError(t, err) + _, err = io.Copy(ff, bytes.NewReader(fileContent)) + require.NoError(t, err) + } // add extra fields for key, values := range extraFields { @@ -4766,7 +4768,7 @@ func generateMultipartRequest(t *testing.T, } } - err = writer.Close() + err := writer.Close() require.NoError(t, err) headers := map[string]string{ From c85adfe0401e7018dc3122c54b5b9051afe587eb Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 17 Dec 2024 16:21:02 -0500 Subject: [PATCH 4/4] Add changes file --- changes/24792-update-software-installer-activities | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/24792-update-software-installer-activities diff --git a/changes/24792-update-software-installer-activities b/changes/24792-update-software-installer-activities new file mode 100644 index 000000000000..206fa80dac8b --- /dev/null +++ b/changes/24792-update-software-installer-activities @@ -0,0 +1 @@ +* Added the `labels_include_any` and `labels_exclude_any` fields to the software installer activities.