From 83671662786475b340771e3eaa2c6386e267fcc2 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Wed, 8 May 2024 12:41:57 -0400 Subject: [PATCH] Add uninstalled but available software installers to the "List software titles" API (#18842) --- ...ailable-installers-to-list-software-titles | 1 + orbit/pkg/scripts/exec_nonwindows_test.go | 7 + server/datastore/mysql/software_titles.go | 28 +- .../datastore/mysql/software_titles_test.go | 279 ++++++++++++++---- 4 files changed, 254 insertions(+), 61 deletions(-) create mode 100644 changes/18831-add-available-installers-to-list-software-titles diff --git a/changes/18831-add-available-installers-to-list-software-titles b/changes/18831-add-available-installers-to-list-software-titles new file mode 100644 index 0000000000..8b01544862 --- /dev/null +++ b/changes/18831-add-available-installers-to-list-software-titles @@ -0,0 +1 @@ +* Added the uninstalled but available software installers to the response payload of the "List software titles" endpoint (`GET /software/titles`). diff --git a/orbit/pkg/scripts/exec_nonwindows_test.go b/orbit/pkg/scripts/exec_nonwindows_test.go index 52a5c069c0..4965b08838 100644 --- a/orbit/pkg/scripts/exec_nonwindows_test.go +++ b/orbit/pkg/scripts/exec_nonwindows_test.go @@ -5,6 +5,7 @@ package scripts import ( "context" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -59,6 +60,12 @@ func TestExecCmdNonWindows(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + if strings.HasPrefix(tc.contents, "#!"+zshPath) { + // skip if zsh is not installed + if _, err := exec.LookPath(zshPath); err != nil { + t.Skipf("zsh not installed: %s", err) + } + } scriptPath := strings.ReplaceAll(tc.name, " ", "_") + ".sh" scriptPath = filepath.Join(tmpDir, scriptPath) err := os.WriteFile(scriptPath, []byte(tc.contents), os.ModePerm) diff --git a/server/datastore/mysql/software_titles.go b/server/datastore/mysql/software_titles.go index ea52931d7d..0140dc0ffc 100644 --- a/server/datastore/mysql/software_titles.go +++ b/server/datastore/mysql/software_titles.go @@ -191,15 +191,18 @@ SELECT st.name, st.source, st.browser, - MAX(sthc.hosts_count) as hosts_count, - MAX(sthc.updated_at) as counts_updated_at + MAX(COALESCE(sthc.hosts_count, 0)) as hosts_count, + MAX(COALESCE(sthc.updated_at, date('0001-01-01 00:00:00'))) as counts_updated_at FROM software_titles st -JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id +LEFT JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id AND sthc.team_id = ? -- placeholder for JOIN on software/software_cve %s -- placeholder for optional extra WHERE filter -WHERE sthc.team_id = ? %s -AND sthc.hosts_count > 0 +WHERE %s +AND ( + sthc.hosts_count > 0 OR + EXISTS (SELECT 1 FROM software_installers si WHERE si.title_id = st.id AND si.global_or_team_id = ?) +) GROUP BY st.id` cveJoinType := "LEFT" @@ -207,27 +210,34 @@ GROUP BY st.id` cveJoinType = "INNER" } + var globalOrTeamID uint args := []any{0} if opt.TeamID != nil { args[0] = *opt.TeamID + globalOrTeamID = *opt.TeamID } - additionalWhere := "" + additionalWhere := "TRUE" match := opt.ListOptions.MatchQuery softwareJoin := "" if match != "" || opt.VulnerableOnly { + // if we do a match but not vulnerable only, we want a LEFT JOIN on + // software because software installers may not have entries in software + // for their software title. If we do want vulnerable only, then we have to + // INNER JOIN because a CVE implies a specific software version. softwareJoin = fmt.Sprintf(` - JOIN software s ON s.title_id = st.id + %s JOIN software s ON s.title_id = st.id -- placeholder for changing the JOIN type to filter vulnerable software - %s JOIN software_cve scve ON s.id = scve.software_id + %[1]s JOIN software_cve scve ON s.id = scve.software_id `, cveJoinType) } if match != "" { - additionalWhere += " AND (st.name LIKE ? OR scve.cve LIKE ?)" + additionalWhere = " (st.name LIKE ? OR scve.cve LIKE ?)" match = likePattern(match) args = append(args, match, match) } + args = append(args, globalOrTeamID) stmt = fmt.Sprintf(stmt, softwareJoin, additionalWhere) return stmt, args diff --git a/server/datastore/mysql/software_titles_test.go b/server/datastore/mysql/software_titles_test.go index cf09e3326d..7fdd4cf5a5 100644 --- a/server/datastore/mysql/software_titles_test.go +++ b/server/datastore/mysql/software_titles_test.go @@ -3,11 +3,12 @@ package mysql import ( "context" "database/sql" - "github.com/stretchr/testify/assert" "sort" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" @@ -24,6 +25,7 @@ func TestSoftwareTitles(t *testing.T) { {"SyncHostsSoftwareTitles", testSoftwareSyncHostsSoftwareTitles}, {"OrderSoftwareTitles", testOrderSoftwareTitles}, {"TeamFilterSoftwareTitles", testTeamFilterSoftwareTitles}, + {"ListSoftwareTitlesInstallersOnly", testListSoftwareTitlesInstallersOnly}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -70,8 +72,8 @@ func testSoftwareSyncHostsSoftwareTitles(t *testing.T, ds *Datastore) { require.NoError(t, err) _, err = ds.UpdateHostSoftware(ctx, host2.ID, software2) require.NoError(t, err) - require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) + require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) globalOpts := fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{OrderKey: "hosts_count", OrderDirection: fleet.OrderDescending}} @@ -91,8 +93,8 @@ func testSoftwareSyncHostsSoftwareTitles(t *testing.T, ds *Datastore) { } _, err = ds.UpdateHostSoftware(ctx, host2.ID, software2) require.NoError(t, err) - require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) + require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) globalCounts = listSoftwareTitlesCheckCount(t, ds, 1, 1, globalOpts, false) @@ -157,8 +159,8 @@ func testSoftwareSyncHostsSoftwareTitles(t *testing.T, ds *Datastore) { checkTableTotalCount(1) // after a call to Calculate, the global counts are updated and the team counts appear - require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) + require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) globalCounts = listSoftwareTitlesCheckCount(t, ds, 2, 2, globalOpts, false) @@ -195,8 +197,8 @@ func testSoftwareSyncHostsSoftwareTitles(t *testing.T, ds *Datastore) { _, err = ds.UpdateHostSoftware(ctx, host4.ID, software4) require.NoError(t, err) - require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) + require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) globalCounts = listSoftwareTitlesCheckCount(t, ds, 1, 1, globalOpts, false) @@ -223,8 +225,8 @@ func testSoftwareSyncHostsSoftwareTitles(t *testing.T, ds *Datastore) { software4 = []fleet.Software{} _, err = ds.UpdateHostSoftware(ctx, host4.ID, software4) require.NoError(t, err) - require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) + require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) listSoftwareTitlesCheckCount(t, ds, 0, 0, team2Opts, false) @@ -232,8 +234,8 @@ func testSoftwareSyncHostsSoftwareTitles(t *testing.T, ds *Datastore) { require.NoError(t, ds.DeleteTeam(ctx, team2.ID)) // this call will remove team2 from the software host counts table - require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) + require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) globalCounts = listSoftwareTitlesCheckCount(t, ds, 1, 1, globalOpts, false) @@ -284,8 +286,29 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) { require.NoError(t, err) _, err = ds.UpdateHostSoftware(ctx, host3.ID, software3) require.NoError(t, err) - require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) + + // create a software installer not installed on any host + installer1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "installer1", + Source: "apps", + InstallScript: "echo", + Filename: "installer1.pkg", + }) + require.NoError(t, err) + require.NotZero(t, installer1) + // create a software installer with an install request on host1 + installer2, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "installer2", + Source: "apps", + InstallScript: "echo", + Filename: "installer2.pkg", + }) + require.NoError(t, err) + err = ds.InsertSoftwareInstallRequest(ctx, host1.ID, installer2) + require.NoError(t, err) + require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) + require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) // primary sort is "hosts_count DESC", followed by "name ASC, source ASC, browser ASC" @@ -294,7 +317,7 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) { OrderDirection: fleet.OrderDescending, }}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) require.NoError(t, err) - require.Len(t, titles, 7) + require.Len(t, titles, 9) require.Equal(t, "bar", titles[0].Name) require.Equal(t, "deb_packages", titles[0].Source) require.Equal(t, "foo", titles[1].Name) @@ -311,6 +334,10 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) { require.Equal(t, "edge", titles[5].Browser) require.Equal(t, "foo", titles[6].Name) require.Equal(t, "rpm_packages", titles[6].Source) + require.Equal(t, "installer1", titles[7].Name) + require.Equal(t, "apps", titles[7].Source) + require.Equal(t, "installer2", titles[8].Name) + require.Equal(t, "apps", titles[8].Source) // primary sort is "hosts_count ASC", followed by "name ASC, source ASC, browser ASC" titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{ @@ -318,23 +345,27 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) { OrderDirection: fleet.OrderAscending, }}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) require.NoError(t, err) - require.Len(t, titles, 7) - require.Equal(t, "bar", titles[0].Name) + require.Len(t, titles, 9) + require.Equal(t, "installer1", titles[0].Name) require.Equal(t, "apps", titles[0].Source) - require.Equal(t, "baz", titles[1].Name) - require.Equal(t, "chrome_extensions", titles[1].Source) - require.Equal(t, "chrome", titles[1].Browser) - require.Equal(t, "baz", titles[2].Name) - require.Equal(t, "chrome_extensions", titles[2].Source) - require.Equal(t, "edge", titles[2].Browser) - require.Equal(t, "foo", titles[3].Name) - require.Equal(t, "rpm_packages", titles[3].Source) - require.Equal(t, "bar", titles[4].Name) - require.Equal(t, "deb_packages", titles[4].Source) + require.Equal(t, "installer2", titles[1].Name) + require.Equal(t, "apps", titles[1].Source) + require.Equal(t, "bar", titles[2].Name) + require.Equal(t, "apps", titles[2].Source) + require.Equal(t, "baz", titles[3].Name) + require.Equal(t, "chrome_extensions", titles[3].Source) + require.Equal(t, "chrome", titles[3].Browser) + require.Equal(t, "baz", titles[4].Name) + require.Equal(t, "chrome_extensions", titles[4].Source) + require.Equal(t, "edge", titles[4].Browser) require.Equal(t, "foo", titles[5].Name) - require.Equal(t, "chrome_extensions", titles[5].Source) - require.Equal(t, "foo", titles[6].Name) + require.Equal(t, "rpm_packages", titles[5].Source) + require.Equal(t, "bar", titles[6].Name) require.Equal(t, "deb_packages", titles[6].Source) + require.Equal(t, "foo", titles[7].Name) + require.Equal(t, "chrome_extensions", titles[7].Source) + require.Equal(t, "foo", titles[8].Name) + require.Equal(t, "deb_packages", titles[8].Source) // primary sort is "name ASC", followed by "host_count DESC, source ASC, browser ASC" titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{ @@ -342,7 +373,7 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) { OrderDirection: fleet.OrderAscending, }}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) require.NoError(t, err) - require.Len(t, titles, 7) + require.Len(t, titles, 9) require.Equal(t, "bar", titles[0].Name) require.Equal(t, "deb_packages", titles[0].Source) require.Equal(t, "bar", titles[1].Name) @@ -359,6 +390,10 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) { require.Equal(t, "deb_packages", titles[5].Source) require.Equal(t, "foo", titles[6].Name) require.Equal(t, "rpm_packages", titles[6].Source) + require.Equal(t, "installer1", titles[7].Name) + require.Equal(t, "apps", titles[7].Source) + require.Equal(t, "installer2", titles[8].Name) + require.Equal(t, "apps", titles[8].Source) // primary sort is "name DESC", followed by "host_count DESC, source ASC, browser ASC" titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{ @@ -366,23 +401,59 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) { OrderDirection: fleet.OrderDescending, }}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) require.NoError(t, err) - require.Len(t, titles, 7) - require.Equal(t, "foo", titles[0].Name) - require.Equal(t, "chrome_extensions", titles[0].Source) - require.Equal(t, "foo", titles[1].Name) - require.Equal(t, "deb_packages", titles[1].Source) + require.Len(t, titles, 9) + require.Equal(t, "installer2", titles[0].Name) + require.Equal(t, "apps", titles[0].Source) + require.Equal(t, "installer1", titles[1].Name) + require.Equal(t, "apps", titles[1].Source) require.Equal(t, "foo", titles[2].Name) - require.Equal(t, "rpm_packages", titles[2].Source) - require.Equal(t, "baz", titles[3].Name) - require.Equal(t, "chrome_extensions", titles[3].Source) - require.Equal(t, "chrome", titles[3].Browser) - require.Equal(t, "baz", titles[4].Name) - require.Equal(t, "chrome_extensions", titles[4].Source) - require.Equal(t, "edge", titles[4].Browser) - require.Equal(t, "bar", titles[5].Name) - require.Equal(t, "deb_packages", titles[5].Source) - require.Equal(t, "bar", titles[6].Name) - require.Equal(t, "apps", titles[6].Source) + require.Equal(t, "chrome_extensions", titles[2].Source) + require.Equal(t, "foo", titles[3].Name) + require.Equal(t, "deb_packages", titles[3].Source) + require.Equal(t, "foo", titles[4].Name) + require.Equal(t, "rpm_packages", titles[4].Source) + require.Equal(t, "baz", titles[5].Name) + require.Equal(t, "chrome_extensions", titles[5].Source) + require.Equal(t, "chrome", titles[5].Browser) + require.Equal(t, "baz", titles[6].Name) + require.Equal(t, "chrome_extensions", titles[6].Source) + require.Equal(t, "edge", titles[6].Browser) + require.Equal(t, "bar", titles[7].Name) + require.Equal(t, "deb_packages", titles[7].Source) + require.Equal(t, "bar", titles[8].Name) + require.Equal(t, "apps", titles[8].Source) + + // using a match query + titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{ + OrderKey: "name", + OrderDirection: fleet.OrderDescending, + MatchQuery: "ba", + }}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) + require.NoError(t, err) + require.Len(t, titles, 4) + require.Equal(t, "baz", titles[0].Name) + require.Equal(t, "chrome_extensions", titles[0].Source) + require.Equal(t, "chrome", titles[0].Browser) + require.Equal(t, "baz", titles[1].Name) + require.Equal(t, "chrome_extensions", titles[1].Source) + require.Equal(t, "edge", titles[1].Browser) + require.Equal(t, "bar", titles[2].Name) + require.Equal(t, "deb_packages", titles[2].Source) + require.Equal(t, "bar", titles[3].Name) + require.Equal(t, "apps", titles[3].Source) + + // using another (installer-only) match query + titles, _, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{ + OrderKey: "name", + OrderDirection: fleet.OrderDescending, + MatchQuery: "insta", + }}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) + require.NoError(t, err) + require.Len(t, titles, 2) + require.Equal(t, "installer2", titles[0].Name) + require.Equal(t, "apps", titles[0].Source) + require.Equal(t, "installer1", titles[1].Name) + require.Equal(t, "apps", titles[1].Source) } func listSoftwareTitlesCheckCount(t *testing.T, ds *Datastore, expectedListCount int, expectedFullCount int, opts fleet.SoftwareTitleListOptions, returnSorted bool) []fleet.SoftwareTitle { @@ -407,11 +478,11 @@ func testTeamFilterSoftwareTitles(t *testing.T, ds *Datastore) { host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now()) require.NoError(t, ds.AddHostsToTeam(ctx, &team2.ID, []uint{host2.ID})) - user1, err := ds.NewUser(ctx, &fleet.User{Name: "user1", Password: []byte("test"), Email: "test1@email.com", GlobalRole: ptr.String(fleet.RoleAdmin)}) + userGlobalAdmin, err := ds.NewUser(ctx, &fleet.User{Name: "user1", Password: []byte("test"), Email: "test1@email.com", GlobalRole: ptr.String(fleet.RoleAdmin)}) require.NoError(t, err) - user2, err := ds.NewUser(ctx, &fleet.User{Name: "user2", Password: []byte("test"), Email: "test2@email.com", Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleAdmin}}}) + userTeam1Admin, err := ds.NewUser(ctx, &fleet.User{Name: "user2", Password: []byte("test"), Email: "test2@email.com", Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team1.ID}, Role: fleet.RoleAdmin}}}) require.NoError(t, err) - user3, err := ds.NewUser(ctx, &fleet.User{Name: "user3", Password: []byte("test"), Email: "test3@email.com", Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team2.ID}, Role: fleet.RoleAdmin}}}) + userTeam2Admin, err := ds.NewUser(ctx, &fleet.User{Name: "user3", Password: []byte("test"), Email: "test3@email.com", Teams: []fleet.UserTeam{{Team: fleet.Team{ID: team2.ID}, Role: fleet.RoleAdmin}}}) require.NoError(t, err) software1 := []fleet.Software{ @@ -427,19 +498,47 @@ func testTeamFilterSoftwareTitles(t *testing.T, ds *Datastore) { _, err = ds.UpdateHostSoftware(ctx, host2.ID, software2) require.NoError(t, err) - require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) + // create a software installer for team1 + installer1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "installer1", + Source: "apps", + InstallScript: "echo", + Filename: "installer1.pkg", + TeamID: &team1.ID, + }) + require.NoError(t, err) + require.NotZero(t, installer1) + // create a software installer for team2 + installer2, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "installer2", + Source: "apps", + InstallScript: "echo", + Filename: "installer2.pkg", + TeamID: &team2.ID, + }) + require.NoError(t, err) + require.NotZero(t, installer2) + require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) + require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) - // Testing the global user - globalTeamFilter := fleet.TeamFilter{User: user1, IncludeObserver: true} + // Testing the global user (for no team) + globalTeamFilter := fleet.TeamFilter{User: userGlobalAdmin, IncludeObserver: true} titles, count, _, err := ds.ListSoftwareTitles( context.Background(), fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{}}, globalTeamFilter, ) sortTitlesByName(titles) + // software installers are associated with a team, so they don't show up in + // this request for no team, but other titles do because software titles are + // not associated with a team. require.NoError(t, err) require.Len(t, titles, 2) require.Equal(t, 2, count) + require.Equal(t, "bar", titles[0].Name) + require.Equal(t, "deb_packages", titles[0].Source) + require.Equal(t, "foo", titles[1].Name) + require.Equal(t, "chrome_extensions", titles[1].Source) require.Equal(t, uint(1), titles[0].VersionsCount) assert.Equal(t, uint(1), titles[0].HostsCount) require.Equal(t, uint(2), titles[1].VersionsCount) @@ -465,14 +564,20 @@ func testTeamFilterSoftwareTitles(t *testing.T, ds *Datastore) { assert.Equal(t, "0.0.3", title.Versions[0].Version) // Testing the team 1 user - team1TeamFilter := fleet.TeamFilter{User: user2, IncludeObserver: true} + team1TeamFilter := fleet.TeamFilter{User: userTeam1Admin, IncludeObserver: true} titles, count, _, err = ds.ListSoftwareTitles( context.Background(), fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{}, TeamID: &team1.ID}, team1TeamFilter, ) + // installer1 is associated with team 1 require.NoError(t, err) - require.Len(t, titles, 1) - require.Equal(t, 1, count) + require.Len(t, titles, 2) + require.Equal(t, 2, count) + require.Equal(t, "foo", titles[0].Name) + require.Equal(t, "chrome_extensions", titles[0].Source) + require.Equal(t, "installer1", titles[1].Name) + require.Equal(t, "apps", titles[1].Source) require.Equal(t, uint(1), titles[0].VersionsCount) + require.Equal(t, uint(0), titles[1].VersionsCount) // Testing with team filter -- this team does contain this software title title, err = ds.SoftwareTitleByID(context.Background(), titles[0].ID, &team1.ID, team1TeamFilter) @@ -483,16 +588,86 @@ func testTeamFilterSoftwareTitles(t *testing.T, ds *Datastore) { // Testing the team 2 user titles, count, _, err = ds.ListSoftwareTitles(context.Background(), fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{}, TeamID: &team2.ID}, fleet.TeamFilter{ - User: user3, + User: userTeam2Admin, IncludeObserver: true, }) + // installer2 is associated with team 2 require.NoError(t, err) - require.Len(t, titles, 2) - require.Equal(t, 2, count) + require.Len(t, titles, 3) + require.Equal(t, 3, count) + require.Equal(t, "bar", titles[0].Name) + require.Equal(t, "deb_packages", titles[0].Source) + require.Equal(t, "foo", titles[1].Name) + require.Equal(t, "chrome_extensions", titles[1].Source) + require.Equal(t, "installer2", titles[2].Name) + require.Equal(t, "apps", titles[2].Source) require.Equal(t, uint(1), titles[0].VersionsCount) require.Equal(t, uint(1), titles[1].VersionsCount) + require.Equal(t, uint(0), titles[2].VersionsCount) } func sortTitlesByName(titles []fleet.SoftwareTitle) { sort.Slice(titles, func(i, j int) bool { return titles[i].Name < titles[j].Name }) } + +func testListSoftwareTitlesInstallersOnly(t *testing.T, ds *Datastore) { + ctx := context.Background() + + // create a couple software installers not installed on any host + installer1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "installer1", + Source: "apps", + InstallScript: "echo", + Filename: "installer1.pkg", + }) + require.NoError(t, err) + require.NotZero(t, installer1) + installer2, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "installer2", + Source: "apps", + InstallScript: "echo", + Filename: "installer2.pkg", + }) + require.NoError(t, err) + require.NotZero(t, installer2) + + titles, counts, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{ + OrderKey: "name", + OrderDirection: fleet.OrderAscending, + }}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) + require.NoError(t, err) + require.EqualValues(t, 2, counts) + require.Len(t, titles, 2) + require.Equal(t, "installer1", titles[0].Name) + require.Equal(t, "apps", titles[0].Source) + require.Equal(t, "installer2", titles[1].Name) + require.Equal(t, "apps", titles[1].Source) + require.True(t, titles[0].CountsUpdatedAt.IsZero()) + require.True(t, titles[1].CountsUpdatedAt.IsZero()) + + require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) + require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) + require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) + + titles, counts, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{ + OrderKey: "name", + OrderDirection: fleet.OrderAscending, + MatchQuery: "installer1", + }}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) + require.NoError(t, err) + require.EqualValues(t, 1, counts) + require.Len(t, titles, 1) + require.Equal(t, "installer1", titles[0].Name) + require.Equal(t, "apps", titles[0].Source) + require.True(t, titles[0].CountsUpdatedAt.IsZero()) + + // vulnerable only returns nothing + titles, counts, _, err = ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ListOptions: fleet.ListOptions{ + OrderKey: "name", + OrderDirection: fleet.OrderAscending, + MatchQuery: "installer1", + }, VulnerableOnly: true}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) + require.NoError(t, err) + require.EqualValues(t, 0, counts) + require.Len(t, titles, 0) +}