Skip to content

Commit

Permalink
Add clause to exclude VPP apps from host software list (#23207)
Browse files Browse the repository at this point in the history
  • Loading branch information
dantecatalfamo authored Oct 28, 2024
1 parent 7ec2184 commit 6e9955d
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 4 deletions.
1 change: 1 addition & 0 deletions changes/23207-filter-vpp-mdm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Filter out VPP apps on non-MDM enrolled devices
3 changes: 3 additions & 0 deletions cmd/fleet/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,9 @@ func TestScanVulnerabilities(t *testing.T) {
},
}, nil
}
ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
return true, nil
}

vulnPath := filepath.Join("..", "..", "server", "vulnerabilities", "testdata")

Expand Down
13 changes: 9 additions & 4 deletions server/datastore/mysql/software.go
Original file line number Diff line number Diff line change
Expand Up @@ -2162,6 +2162,11 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt
onlySelfServiceClause = ` AND ( si.self_service = 1 OR vat.self_service = 1 ) `
}

var excludeVPPAppsClause string
if opts.ExcludeVPPApps {
excludeVPPAppsClause = ` AND vat.id IS NULL `
}

var onlyVulnerableJoin string
if opts.VulnerableOnly {
onlyVulnerableJoin = `
Expand Down Expand Up @@ -2281,8 +2286,8 @@ 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 )
%s
`, status, softwareIsInstalledOnHostClause, onlySelfServiceClause)
%s %s
`, status, softwareIsInstalledOnHostClause, onlySelfServiceClause, excludeVPPAppsClause)

// this statement lists only the software that has never been installed nor
// attempted to be installed on the host, but that is available to be
Expand Down Expand Up @@ -2347,8 +2352,8 @@ INNER JOIN software_cve scve ON scve.software_id = s.id
) AND
-- either the software installer or the vpp app exists for the host's team
( si.id IS NOT NULL OR vat.platform = :host_platform )
%s
`, onlySelfServiceClause)
%s %s
`, onlySelfServiceClause, excludeVPPAppsClause)

// this is the top-level SELECT of fields from the UNION of the sub-selects
// (stmtAvailable and stmtInstalled).
Expand Down
35 changes: 35 additions & 0 deletions server/datastore/mysql/software_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"database/sql"
"encoding/hex"
"fmt"
"maps"
"math/rand"
"sort"
"strings"
Expand Down Expand Up @@ -3873,6 +3874,8 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
vpp1TmCmdUUID := createVPPAppInstallRequest(t, ds, tmHost, vpp1, user.ID)
require.NotEmpty(t, vpp1TmCmdUUID)

expectedWithoutVPP := maps.Clone(expected)

expected["vpp1apps"] = fleet.HostSoftwareWithInstaller{
Name: "vpp1",
Source: "apps",
Expand All @@ -3893,12 +3896,26 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 2}, meta)
compareResults(expected, sw, true, i3.Name+i3.Source, i2.Name+i2.Source) // i3 is for team, i2 is available (excluded)

// Exclude VPP apps from query
opts.ExcludeVPPApps = true
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 4}, meta)
compareResults(expectedWithoutVPP, sw, true, i3.Name+i3.Source, i2.Name+i2.Source) // i3 is for team, i2 is available (excluded)
opts.ExcludeVPPApps = false

expected["vpp3apps"] = fleet.HostSoftwareWithInstaller{
Name: "vpp3",
Source: "apps",
Status: nil,
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp3, SelfService: ptr.Bool(true)},
}

expectedAvailableOnlyExcludeVPP := maps.Clone(expectedAvailableOnly)
for _, app := range expectedAvailableOnlyExcludeVPP {
fmt.Printf(" app: %+v\n", app)
}

expectedAvailableOnly["vpp1apps"] = expected["vpp1apps"]
expectedAvailableOnly["vpp2apps"] = expected["vpp2apps"]
expectedAvailableOnly["vpp3apps"] = expected["vpp3apps"]
Expand All @@ -3909,6 +3926,14 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 1}, meta)
compareResults(expected, sw, true, i3.Name+i3.Source) // i3 is for team

// Exclude vpp apps from query
opts.ExcludeVPPApps = true
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 4}, meta)
compareResults(expectedWithoutVPP, sw, true, i3.Name+i3.Source) // i3 is for team
opts.ExcludeVPPApps = false

// Available for install only
opts.OnlyAvailableForInstall = true
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
Expand All @@ -3917,6 +3942,16 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
compareResults(expectedAvailableOnly, sw, true)
opts.OnlyAvailableForInstall = false

// Available for install only without vpp
opts.OnlyAvailableForInstall = true
opts.ExcludeVPPApps = true
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expectedAvailableOnlyExcludeVPP))}, meta)
compareResults(expectedAvailableOnlyExcludeVPP, sw, true)
opts.ExcludeVPPApps = false
opts.OnlyAvailableForInstall = false

// team host sees available i3 and pending vpp1
opts.IncludeAvailableForInstall = true
sw, meta, err = ds.ListHostSoftware(ctx, tmHost, opts)
Expand Down
3 changes: 3 additions & 0 deletions server/fleet/software.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ type HostSoftwareTitleListOptions struct {
OnlyAvailableForInstall bool `query:"available_for_install,optional"`

VulnerableOnly bool `query:"vulnerable,optional"`

// Non-MDM-enabled hosts cannot install VPP apps
ExcludeVPPApps bool
}

// AuthzSoftwareInventory is used for access controls on software inventory.
Expand Down
6 changes: 6 additions & 0 deletions server/service/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2627,13 +2627,19 @@ func (svc *Service) ListHostSoftware(ctx context.Context, hostID uint, opts flee
host = h
}

mdmEnrolled, err := svc.ds.IsHostConnectedToFleetMDM(ctx, host)
if err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "checking mdm enrollment status")
}

// cursor-based pagination is not supported
opts.ListOptions.After = ""
// custom ordering is not supported, always by name (but asc/desc is configurable)
opts.ListOptions.OrderKey = "name"
// always include metadata
opts.ListOptions.IncludeMetadata = true
opts.IncludeAvailableForInstall = includeAvailableForInstall || opts.SelfServiceOnly
opts.ExcludeVPPApps = !mdmEnrolled

software, meta, err := svc.ds.ListHostSoftware(ctx, host, opts)
return software, meta, ctxerr.Wrap(ctx, err, "list host software")
Expand Down
3 changes: 3 additions & 0 deletions server/service/hosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,9 @@ func TestHostAuth(t *testing.T) {
ds.ListHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, opts fleet.HostSoftwareTitleListOptions) ([]*fleet.HostSoftwareWithInstaller, *fleet.PaginationMetadata, error) {
return nil, nil, nil
}
ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
return true, nil
}

testCases := []struct {
name string
Expand Down
14 changes: 14 additions & 0 deletions server/service/integration_enterprise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13628,6 +13628,20 @@ func (s *integrationEnterpriseTestSuite) TestVPPAppsWithoutMDM() {
}, &team.ID)
require.NoError(t, err)

pkgPayload := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "some pkg install script",
Filename: "dummy_installer.pkg",
TeamID: &team.ID,
}
s.uploadSoftwareInstaller(t, pkgPayload, http.StatusOK, "")

// We don't see VPP, but we do still see the installers
resp := getHostSoftwareResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", orbitHost.ID), getHostSoftwareRequest{}, http.StatusOK, &resp)
assert.Len(t, resp.Software, 1)
assert.NotNil(t, resp.Software[0].SoftwarePackage)
assert.Nil(t, resp.Software[0].AppStoreApp)

r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", orbitHost.ID, app.TitleID), &installSoftwareRequest{},
http.StatusUnprocessableEntity)
require.Contains(t, extractServerErrorText(r.Body), "Couldn't install. MDM is turned off. Please make sure that MDM is turned on to install App Store apps.")
Expand Down
40 changes: 40 additions & 0 deletions server/service/integration_mdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11746,6 +11746,46 @@ func (d *noopCertDepot) Put(_ string, _ *x509.Certificate) error {
return nil
}

func (s *integrationMDMTestSuite) TestVPPAppsMDMFiltering() {
t := s.T()

ctx := context.Background()

// Create hosts
orbitHost := createOrbitEnrolledHost(t, "darwin", "nonmdm", s.ds)
mdmHost, mdmClient := createHostThenEnrollMDM(s.ds, s.server.URL, t)
_, _ = mdmHost, mdmClient

test.CreateInsertGlobalVPPToken(t, s.ds)

// Create team and add hosts to team
var newTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1")}}, http.StatusOK, &newTeamResp)
team := newTeamResp.Team

s.Do("POST", "/api/latest/fleet/hosts/transfer", &addHostsToTeamRequest{HostIDs: []uint{orbitHost.ID, mdmHost.ID}, TeamID: &team.ID}, http.StatusOK)

// Add an app so that we don't get a not found error
_, err := s.ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "App " + t.Name(),
BundleIdentifier: "bid_" + t.Name(),
VPPAppTeam: fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{
AdamID: "adam_" + t.Name(),
Platform: fleet.MacOSPlatform,
},
},
}, &team.ID)
require.NoError(t, err)

resp := getHostSoftwareResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", orbitHost.ID), getHostSoftwareRequest{}, http.StatusOK, &resp)
assert.Len(t, resp.Software, 0)

s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), getHostSoftwareRequest{}, http.StatusOK, &resp)
assert.Len(t, resp.Software, 1)
}

func (s *integrationMDMTestSuite) TestSetupExperience() {
t := s.T()
ds := s.ds
Expand Down

0 comments on commit 6e9955d

Please sign in to comment.