diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 726d1aaf49..d8ceb3c62c 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -970,7 +970,10 @@ const HostDetailsPage = ({ softwareUpdatedAt={host.software_updated_at} isSoftwareEnabled={featuresConfig?.enable_software_inventory} router={router} - queryParams={parseHostSoftwareQueryParams(location.query)} + queryParams={{ + ...parseHostSoftwareQueryParams(location.query), + include_available_for_install: false, + }} pathname={location.pathname} onShowSoftwareDetails={setSelectedSoftwareDetails} hostTeamId={host.team_id || 0} @@ -1013,7 +1016,10 @@ const HostDetailsPage = ({ softwareUpdatedAt={host.software_updated_at} isSoftwareEnabled={featuresConfig?.enable_software_inventory} router={router} - queryParams={parseHostSoftwareQueryParams(location.query)} + queryParams={{ + ...parseHostSoftwareQueryParams(location.query), + include_available_for_install: false, + }} pathname={location.pathname} onShowSoftwareDetails={setSelectedSoftwareDetails} hostTeamId={host.team_id || 0} diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx index 8abaa69969..a963cf0a83 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx @@ -42,13 +42,18 @@ export interface ITableSoftware extends Omit { vulnerabilities: string[]; // for client-side search purposes, we only want an array of cve strings } +interface HostSoftwareQueryParams + extends ReturnType { + include_available_for_install?: boolean; +} + interface IHostSoftwareProps { /** This is the host id or the device token */ id: number | string; platform: HostPlatform; softwareUpdatedAt?: string; router: InjectedRouter; - queryParams: ReturnType; + queryParams: HostSoftwareQueryParams; pathname: string; hostTeamId: number; onShowSoftwareDetails: (software: IHostSoftware) => void; @@ -289,7 +294,12 @@ const HostSoftware = ({ searchQuery={queryParams.query} page={queryParams.page} pagePath={pathname} - vulnFilters={getSoftwareVulnFiltersFromQueryParams(queryParams)} + vulnFilters={getSoftwareVulnFiltersFromQueryParams({ + vulnerable: queryParams.vulnerable, + exploit: queryParams.exploit, + min_cvss_score: queryParams.min_cvss_score, + max_cvss_score: queryParams.max_cvss_score, + })} onAddFiltersClick={toggleSoftwareFiltersModal} pathPrefix={pathname} // for my device software details modal toggling @@ -301,7 +311,12 @@ const HostSoftware = ({ )} diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts index 27ea3008dc..7911084318 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -193,6 +193,7 @@ export interface IHostSoftwareQueryParams extends QueryParams { order_key: string; order_direction: "asc" | "desc"; available_for_install?: boolean; + include_available_for_install?: boolean; vulnerable?: boolean; min_cvss_score?: number; max_cvss_score?: number; diff --git a/server/fleet/software.go b/server/fleet/software.go index d321f1f7a6..5db1166287 100644 --- a/server/fleet/software.go +++ b/server/fleet/software.go @@ -270,10 +270,12 @@ type HostSoftwareTitleListOptions struct { // AvailableForInstall. SelfServiceOnly bool `query:"self_service,optional"` - // IncludeAvailableForInstall is not a query argument, it is set in the - // service layer to indicate to the datastore if software available for - // install (but not currently installed on the host) should be returned. - IncludeAvailableForInstall bool + IncludeAvailableForInstall bool `query:"include_available_for_install,optional"` + // IncludeAvailableForInstall was exposed as a query string parameter + // In order not to introduce a breaking change we have to mark this parameter as optional. + // However, instead of using *bool and modifying a lot of downstream code and tests + // Use this indicator + IncludeAvailableForInstallExplicitlySet bool // OnlyAvailableForInstall is set via a query argument that limits the // returned software titles to only those that are available for install on diff --git a/server/service/hosts.go b/server/service/hosts.go index f63f81a2cc..53f471c7e6 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -2780,6 +2780,31 @@ type getHostSoftwareResponse struct { func (r getHostSoftwareResponse) Error() error { return r.Err } +func (r getHostSoftwareRequest) DecodeRequest(ctx context.Context, req *http.Request) (interface{}, error) { + type defaultDecodeRequest struct { + ID uint `url:"id"` + fleet.HostSoftwareTitleListOptions + } + + defaultDecoder := makeDecoder(defaultDecodeRequest{}) + decoded, err := defaultDecoder(ctx, req) + if err != nil { + return nil, err + } + + result := decoded.(*defaultDecodeRequest) + queryParams := req.URL.Query() + _, wasIncludeAvailableForInstallSet := queryParams["include_available_for_install"] + result.HostSoftwareTitleListOptions.IncludeAvailableForInstallExplicitlySet = wasIncludeAvailableForInstallSet + + finalResult := getHostSoftwareRequest{ + ID: result.ID, + HostSoftwareTitleListOptions: result.HostSoftwareTitleListOptions, + } + + return &finalResult, nil +} + func getHostSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getHostSoftwareRequest) res, meta, err := svc.ListHostSoftware(ctx, req.ID, req.HostSoftwareTitleListOptions) @@ -2793,10 +2818,12 @@ func getHostSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet } func (svc *Service) ListHostSoftware(ctx context.Context, hostID uint, opts fleet.HostSoftwareTitleListOptions) ([]*fleet.HostSoftwareWithInstaller, *fleet.PaginationMetadata, error) { - // if the request is token-authenticated ("My device" page), we don't include - // software that is not installed but for which there's an installer - // available for that host (unless the request filters for self-service - // software only). + // When accessed via "My device", we default to only showing inventory (excluding software available for install + // but not in inventory), unless we're asked to filter to self-service software only. + // + // Otherwise (e.g. host software UI within Fleet's admin interface), the default is to show both installed and + // available-for-install software, to maintain existing API behavior. This behavior can be explicitly overridden + // if needed (see opts.IncludeAvailableForInstallExplicitlySet). var includeAvailableForInstall bool var host *fleet.Host @@ -2830,6 +2857,10 @@ func (svc *Service) ListHostSoftware(ctx context.Context, hostID uint, opts flee return nil, nil, ctxerr.Wrap(ctx, err, "checking mdm enrollment status") } + if opts.IncludeAvailableForInstallExplicitlySet { + includeAvailableForInstall = opts.IncludeAvailableForInstall + } + // cursor-based pagination is not supported opts.ListOptions.After = "" // custom ordering is not supported, always by name (but asc/desc is configurable) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 79749235e0..3ce99e9d76 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -10615,6 +10615,13 @@ func (s *integrationEnterpriseTestSuite) TestListHostSoftware() { require.True(t, *getHostSw.Software[2].SoftwarePackage.SelfService) require.Nil(t, getHostSw.Software[2].Status) + // user authenticated endpoint, but explicitly request to not include available for install software + getHostSw = getHostSoftwareResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software?include_available_for_install=false", host.ID), nil, http.StatusOK, &getHostSw) + require.Len(t, getHostSw.Software, 2) // foo and bar + require.Equal(t, getHostSw.Software[0].Name, "bar") + require.Equal(t, getHostSw.Software[1].Name, "foo") + // only the installer is returned for self-service only getHostSw = getHostSoftwareResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw, "self_service", "true")