remove default'd available_for_install (#30516)

This was added to support the "All Software" when listing software on
the host.

Fixes #30188

- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [x] Added/updated automated tests
- [x] Manual QA for all new/changed functionality
- [x] For unreleased bug fixes in a release candidate, confirmed that
the fix is not expected to adversely impact load test results or alerted
the release DRI if additional load testing is needed.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added an option to explicitly exclude software available for install
from host software listings.

* **Bug Fixes**
* Improved accuracy of software inventory results when filtering by
availability for install.

* **Tests**
* Added a test to verify exclusion of available-for-install software
when the relevant option is set.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ian Littman <iansltx@gmail.com>
This commit is contained in:
Konstantin Sykulev 2025-07-03 08:49:31 -07:00 committed by GitHub
parent 6aa3455634
commit 6d5ac49c74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 75 additions and 13 deletions

View file

@ -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}

View file

@ -42,13 +42,18 @@ export interface ITableSoftware extends Omit<ISoftware, "vulnerabilities"> {
vulnerabilities: string[]; // for client-side search purposes, we only want an array of cve strings
}
interface HostSoftwareQueryParams
extends ReturnType<typeof parseHostSoftwareQueryParams> {
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<typeof parseHostSoftwareQueryParams>;
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 = ({
<SoftwareFiltersModal
onExit={toggleSoftwareFiltersModal}
onSubmit={onApplyVulnFilters}
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,
})}
isPremiumTier={isPremiumTier || false}
/>
)}

View file

@ -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;

View file

@ -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

View file

@ -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)

View file

@ -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")