diff --git a/changes/31581-output-from-packages-only b/changes/31581-output-from-packages-only new file mode 100644 index 0000000000..04a70c353b --- /dev/null +++ b/changes/31581-output-from-packages-only @@ -0,0 +1 @@ +* Output from packages_only=true returns software with available installers. diff --git a/server/datastore/mysql/software_titles.go b/server/datastore/mysql/software_titles.go index 01ff3a6158..57d458ec25 100644 --- a/server/datastore/mysql/software_titles.go +++ b/server/datastore/mysql/software_titles.go @@ -113,6 +113,10 @@ func (ds *Datastore) ListSoftwareTitles( return nil, 0, nil, fleet.NewInvalidArgumentError("query", "min_cvss_score, max_cvss_score, and exploit can only be provided with vulnerable=true") } + if opt.TeamID == nil && opt.PackagesOnly { + return nil, 0, nil, fleet.NewInvalidArgumentError("query", "packages_only can only be provided with team_id") + } + dbReader := ds.reader(ctx) getTitlesStmt, args, err := selectSoftwareTitlesSQL(opt) if err != nil { @@ -354,7 +358,8 @@ SELECT {{end}} FROM software_titles st {{if hasTeamID .}} - LEFT JOIN software_installers si ON si.title_id = st.id AND si.global_or_team_id = {{teamID .}} + {{$installerJoin := printf "%s JOIN software_installers si ON si.title_id = st.id AND si.global_or_team_id = %d" (yesNo .PackagesOnly "INNER" "LEFT") (teamID .)}} + {{$installerJoin}} LEFT JOIN vpp_apps vap ON vap.title_id = st.id AND {{yesNo .PackagesOnly "FALSE" "TRUE"}} LEFT JOIN vpp_apps_teams vat ON vat.adam_id = vap.adam_id AND vat.platform = vap.platform AND {{if .PackagesOnly}} FALSE {{else}} vat.global_or_team_id = {{teamID .}}{{end}} @@ -395,8 +400,11 @@ WHERE {{end}} {{$additionalWhere}} {{end}} - -- If teamID is set, defaults to "a software installer or VPP app exists", and see next condition. - {{with $defFilter := yesNo (hasTeamID .) "(si.id IS NOT NULL OR vat.adam_id IS NOT NULL)" "FALSE"}} + {{with $defFilter := "FALSE"}} + -- If teamID is set and PackagesOnly is false, defaults to "a software installer or VPP app exists", and see next condition. + {{if and (hasTeamID $) (not $.PackagesOnly) }} + {{$defFilter = "(si.id IS NOT NULL OR vat.adam_id IS NOT NULL)"}} + {{end}} -- add software installed for hosts if we're not filtering for "available for install" only {{if not $.AvailableForInstall}} {{$defFilter = $defFilter | printf " ( %s OR sthc.hosts_count > 0 ) "}} diff --git a/server/datastore/mysql/software_titles_test.go b/server/datastore/mysql/software_titles_test.go index 68e841e3aa..b649f72061 100644 --- a/server/datastore/mysql/software_titles_test.go +++ b/server/datastore/mysql/software_titles_test.go @@ -41,6 +41,7 @@ func TestSoftwareTitles(t *testing.T) { {"UpdateSoftwareTitleName", testUpdateSoftwareTitleName}, {"ListSoftwareTitlesDoesnotIncludeDuplicates", testListSoftwareTitlesDoesnotIncludeDuplicates}, {"ListSoftwareTitlesAllTeamsWithAutomaticInstallersInNoTeam", testListSoftwareTitlesAllTeamsWithAutomaticInstallersInNoTeam}, + {"ListSoftwareTitlesPackagesOnly", testSoftwareTitlesPackagesOnly}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -2073,3 +2074,87 @@ func testListSoftwareTitlesAllTeamsWithAutomaticInstallersInNoTeam(t *testing.T, require.Len(t, allTeamsTitles, 1) require.Nil(t, allTeamsTitles[0].SoftwarePackage) } + +func testSoftwareTitlesPackagesOnly(t *testing.T, ds *Datastore) { + ctx := context.Background() + + team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) + require.NoError(t, err) + + host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now()) + require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host.ID}))) + + user := test.NewUser(t, ds, "Alice", "alice@example.com", true) + + software := []fleet.Software{ + {Name: "foo", Version: "1.0.0", Source: "deb_packages"}, + {Name: "bar", Version: "2.0.0", Source: "apps"}, + {Name: "baz", Version: "3.0.0", Source: "rpm_packages"}, + } + _, err = ds.UpdateHostSoftware(ctx, host.ID, software) + require.NoError(t, err) + + _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "foo", + Source: "deb_packages", + InstallScript: "echo foo", + Filename: "foo.pkg", + UserID: user.ID, + TeamID: &team1.ID, + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + }) + require.NoError(t, err) + + _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ + Title: "bar", + Source: "apps", + InstallScript: "echo bar", + Filename: "bar.pkg", + UserID: user.ID, + TeamID: &team1.ID, + ValidatedLabels: &fleet.LabelIdentsWithScope{}, + }) + require.NoError(t, err) + + // Sync and reconcile + require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) + require.NoError(t, ds.ReconcileSoftwareTitles(ctx)) + require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) + + t.Run("packages_only=false no team_id", func(t *testing.T) { + titles, _, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ + PackagesOnly: false, + }, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) + require.NoError(t, err) + require.Len(t, titles, 3) + }) + + t.Run("packages_only=true no team_id", func(t *testing.T) { + _, _, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ + PackagesOnly: true, + }, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) + require.Error(t, err) + require.Contains(t, err.Error(), "packages_only can only be provided with team_id") + }) + + t.Run("packages_only=false with team_id", func(t *testing.T) { + titles, _, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ + PackagesOnly: false, + TeamID: &team1.ID, + }, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) + require.NoError(t, err) + require.Len(t, titles, 3) + }) + + t.Run("packages_only=true with team_id", func(t *testing.T) { + titles, _, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{ + PackagesOnly: true, + TeamID: &team1.ID, + }, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) + require.NoError(t, err) + require.Len(t, titles, 2) + for _, title := range titles { + require.NotNil(t, title.SoftwarePackage) + } + }) +} diff --git a/server/datastore/mysql/testdata/select_software_titles_sql_fixture.gz b/server/datastore/mysql/testdata/select_software_titles_sql_fixture.gz index aaad2d7228..0415aaa8a7 100644 Binary files a/server/datastore/mysql/testdata/select_software_titles_sql_fixture.gz and b/server/datastore/mysql/testdata/select_software_titles_sql_fixture.gz differ