Fix filtering in /api/v1/fleet/labels/:id/hosts endpoint (#44293)

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [X] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

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

* **Bug Fixes**
* Fixed filtering in the /api/v1/fleet/labels/:id/hosts endpoint and
tightened validation to reject invalid sort/order keys with HTTP 422
responses.
* Enforced ordering restrictions tied to feature flags (issues and
device-mapping), rejecting unsupported order_key values.

* **Tests**
* Added extensive integration tests for order_key validation,
deterministic sorting across allowed keys, and cursor pagination.

* **Documentation**
  * Added a changelog entry noting the hosts-in-label filtering fix.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Lucas Manuel Rodriguez
2026-04-29 10:43:39 -03:00
committed by GitHub
parent 3b8795dd8d
commit 52caba768c
5 changed files with 451 additions and 9 deletions
+1
View File
@@ -0,0 +1 @@
* Fixed filtering in `/api/v1/fleet/labels/:id/hosts` endpoint.
+78 -3
View File
@@ -14,6 +14,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/jmoiron/sqlx"
)
@@ -1009,6 +1010,80 @@ func (ds *Datastore) ListLabelsForHost(ctx context.Context, hid uint) ([]*fleet.
return labels, nil
}
// hostsInLabelAllowedOrderKeys defines the allowed order keys for the hosts in label endpoint.
// SECURITY: This prevents information disclosure via arbitrary column sorting.
// Sensitive columns like 'node_key' and 'orbit_node_key'.
// are intentionally excluded to prevent binary search extraction attacks.
var hostsInLabelAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
"id": "h.id",
"osquery_host_id": "h.osquery_host_id",
"created_at": "h.created_at",
"updated_at": "h.updated_at",
"detail_updated_at": "h.detail_updated_at",
"hostname": "h.hostname",
"uuid": "h.uuid",
"platform": "h.platform",
"osquery_version": "h.osquery_version",
"os_version": "h.os_version",
"build": "h.build",
"platform_like": "h.platform_like",
"code_name": "h.code_name",
"uptime": "h.uptime",
"memory": "h.memory",
"cpu_type": "h.cpu_type",
"cpu_subtype": "h.cpu_subtype",
"cpu_brand": "h.cpu_brand",
"cpu_physical_cores": "h.cpu_physical_cores",
"cpu_logical_cores": "h.cpu_logical_cores",
"hardware_vendor": "h.hardware_vendor",
"hardware_model": "h.hardware_model",
"hardware_version": "h.hardware_version",
"hardware_serial": "h.hardware_serial",
"computer_name": "h.computer_name",
"primary_ip_id": "h.primary_ip_id",
"distributed_interval": "h.distributed_interval",
"logger_tls_period": "h.logger_tls_period",
"config_tls_refresh": "h.config_tls_refresh",
"primary_ip": "h.primary_ip",
"primary_mac": "h.primary_mac",
"label_updated_at": "h.label_updated_at",
"last_enrolled_at": "h.last_enrolled_at",
"refetch_requested": "h.refetch_requested",
"refetch_critical_queries_until": "h.refetch_critical_queries_until",
"team_id": "h.team_id",
"policy_updated_at": "h.policy_updated_at",
"public_ip": "h.public_ip",
"display_name": "hdn.display_name",
// COALESCE required on the following:
// must match SELECT clause so cursor pagination (WHERE) and ORDER BY are consistent
"gigs_disk_space_available": "COALESCE(hd.gigs_disk_space_available, 0)",
"percent_disk_space_available": "COALESCE(hd.percent_disk_space_available, 0)",
"gigs_total_disk_space": "COALESCE(hd.gigs_total_disk_space, 0)",
"seen_time": "COALESCE(hst.seen_time, h.created_at)",
"software_updated_at": "COALESCE(hu.software_updated_at, h.created_at)",
"last_restarted_at": "h.last_restarted_at",
"timezone": "h.timezone",
// must match SELECT clause subquery so cursor pagination (WHERE) and
// ORDER BY are consistent — MySQL disallows SELECT aliases in WHERE.
"team_name": "(SELECT name FROM teams t WHERE t.id = h.team_id)",
// COALESCE required on the following:
// must match SELECT clause so cursor pagination (WHERE) and ORDER BY are consistent
"failing_policies_count": "COALESCE(host_issues.failing_policies_count, 0)",
"critical_vulnerabilities_count": "COALESCE(host_issues.critical_vulnerabilities_count, 0)",
"total_issues_count": "COALESCE(host_issues.total_issues_count, 0)",
"device_mapping": "COALESCE(dm.device_mapping, 'null')",
"issues": "COALESCE(host_issues.total_issues_count, 0)",
//
// SECURITY:
// Note: 'h.node_key', 'h.orbit_node_key' intentionally EXCLUDED
}
// ListHostsInLabel returns a list of fleet.Host that are associated
// with fleet.Label referenced by Label ID
func (ds *Datastore) ListHostsInLabel(ctx context.Context, filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) ([]*fleet.Host, error) {
@@ -1244,10 +1319,10 @@ func (ds *Datastore) applyHostLabelFilters(ctx context.Context, filter fleet.Tea
// TODO: should search columns include display_name (requires join to host_display_names)?
query, whereParams = hostSearchLike(query, whereParams, opt.MatchQuery, hostSearchColumns...)
if opt.ListOptions.OrderKey == "issues" {
opt.ListOptions.OrderKey = "host_issues.total_issues_count"
query, whereParams, err = appendListOptionsWithCursorToSQLSecure(query, whereParams, &opt.ListOptions, hostsInLabelAllowedOrderKeys)
if err != nil {
return "", nil, ctxerr.Wrap(ctx, err, "apply host list options")
}
query, whereParams = appendListOptionsWithCursorToSQL(query, whereParams, &opt.ListOptions)
return query, append(joinParams, whereParams...), nil
}
+300
View File
@@ -5268,6 +5268,81 @@ func (s *integrationTestSuite) TestLabels() {
assert.Equal(t, fleet.WellKnownMDMSimpleMDM, listHostsResp.MDMSolution.Name)
assert.Equal(t, "https://simplemdm.com", listHostsResp.MDMSolution.ServerURL)
// invalid order_key returns 422 (sensitive columns must not be sortable to prevent
// information disclosure via binary search extraction).
for _, key := range []string{
"node_key", "h.node_key",
"orbit_node_key", "h.orbit_node_key",
"invalid_column", "h.invalid_column",
// computer_name is a valid field, but must not contain the table alias
// (previous version of the endpoint allowed setting aliases here).
"h.computer_name",
} {
res := s.Do("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", lbl2.ID), nil, http.StatusUnprocessableEntity, "order_key", key)
errMsg := extractServerErrorText(res.Body)
assert.Contains(t, errMsg, "invalid order_key")
assert.Contains(t, errMsg, key)
}
// all allowed order_key values must be accepted by the endpoint and return the
// hosts in lbl2.
allowedOrderKeys := []string{
"id", "osquery_host_id", "created_at", "updated_at", "detail_updated_at",
"hostname", "uuid", "platform", "osquery_version", "os_version", "build",
"platform_like", "code_name", "uptime", "memory", "cpu_type", "cpu_subtype",
"cpu_brand", "cpu_physical_cores", "cpu_logical_cores",
"hardware_vendor", "hardware_model", "hardware_version", "hardware_serial",
"computer_name", "primary_ip_id", "distributed_interval", "logger_tls_period",
"config_tls_refresh", "primary_ip", "primary_mac", "label_updated_at",
"last_enrolled_at", "refetch_requested", "refetch_critical_queries_until",
"team_id", "policy_updated_at", "public_ip",
"gigs_disk_space_available", "percent_disk_space_available",
"gigs_total_disk_space", "seen_time", "software_updated_at",
"last_restarted_at", "timezone", "team_name",
"failing_policies_count", "critical_vulnerabilities_count",
"total_issues_count",
"issues", // supported as alias for "total_issues_count"
"device_mapping",
"display_name",
}
for _, key := range allowedOrderKeys {
listHostsResp = listHostsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", lbl2.ID), nil, http.StatusOK, &listHostsResp, "order_key", key, "device_mapping", "true")
assert.Len(t, listHostsResp.Hosts, len(lbl2Hosts), "order_key=%s", key)
}
// every allowed order_key must also accept the `after` cursor without
// erroring — guards against SELECT-list aliases leaking into the WHERE
// clause (MySQL disallows aliases in WHERE).
for _, key := range allowedOrderKeys {
listHostsResp = listHostsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", lbl2.ID), nil, http.StatusOK, &listHostsResp,
"order_key", key, "order_direction", "asc", "after", "0", "device_mapping", "true")
}
// issue-related order_keys are rejected when disable_issues=true.
for _, key := range []string{"issues", "failing_policies_count", "critical_vulnerabilities_count", "total_issues_count"} {
res := s.Do("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", lbl2.ID), nil, http.StatusBadRequest,
"order_key", key, "disable_issues", "true")
errMsg := extractServerErrorText(res.Body)
assert.Contains(t, errMsg, "Invalid order_key")
assert.Contains(t, errMsg, key)
}
// device_mapping order_key is rejected when device_mapping is not enabled.
res = s.Do("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", lbl2.ID), nil, http.StatusBadRequest,
"order_key", "device_mapping")
errMsg = extractServerErrorText(res.Body)
assert.Contains(t, errMsg, "Invalid order_key")
assert.Contains(t, errMsg, "device_mapping")
// device_mapping=false is also rejected.
res = s.Do("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", lbl2.ID), nil, http.StatusBadRequest,
"order_key", "device_mapping", "device_mapping", "false")
errMsg = extractServerErrorText(res.Body)
assert.Contains(t, errMsg, "Invalid order_key")
assert.Contains(t, errMsg, "device_mapping")
// delete a label by id
var delIDResp fleet.DeleteLabelByIDResponse
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/labels/id/%d", lbl1.ID), nil, http.StatusOK, &delIDResp)
@@ -5549,6 +5624,231 @@ func (s *integrationTestSuite) TestLabels() {
})
})
})
t.Run("Sort by order_key", func(t *testing.T) {
ctx := context.Background()
// Create three teams so each host has a distinct team_id and team_name.
// Names sort A < B < C and team IDs are assigned in creation order.
teamPrefix := strings.ReplaceAll(t.Name(), "/", "_")
teamA, err := s.ds.NewTeam(ctx, &fleet.Team{Name: teamPrefix + "-team-A"})
require.NoError(t, err)
teamB, err := s.ds.NewTeam(ctx, &fleet.Team{Name: teamPrefix + "-team-B"})
require.NoError(t, err)
teamC, err := s.ds.NewTeam(ctx, &fleet.Team{Name: teamPrefix + "-team-C"})
require.NoError(t, err)
teamIDs := []*uint{&teamA.ID, &teamB.ID, &teamC.ID}
// Each sortHost[i] is set up with field values that sort in the same direction
// as the index: sortHost[0] is "smallest", sortHost[2] is "largest", for every
// orderable field below. This means ASC order is always [h0, h1, h2] and DESC
// order is always [h2, h1, h0], which keeps the per-field test cases compact.
base := time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)
platforms := []string{"darwin", "linux", "windows"}
sortHosts := make([]*fleet.Host, 3)
for i := range 3 {
h, err := s.ds.NewHost(ctx, &fleet.Host{
OsqueryHostID: ptr.String(fmt.Sprintf("sort-osq-%d", i)),
NodeKey: ptr.String(fmt.Sprintf("sort-nk-%d", i)),
UUID: fmt.Sprintf("aaaaaaaa-0000-0000-0000-00000000000%d", i+1),
Hostname: fmt.Sprintf("sort-host-%d", i),
ComputerName: fmt.Sprintf("sort-comp-%d", i),
HardwareSerial: fmt.Sprintf("sort-ser-%d", i),
Platform: platforms[i],
PlatformLike: fmt.Sprintf("sort-plike-%d", i),
OsqueryVersion: fmt.Sprintf("%d.0.0", i+1),
OSVersion: fmt.Sprintf("OS-%d.0", i+1),
Uptime: time.Duration(i+1) * time.Hour,
Memory: int64(i+1) * 1024,
DistributedInterval: uint(i+1) * 10,
LoggerTLSPeriod: uint(i+1) * 100,
ConfigTLSRefresh: uint(i+1) * 5,
DetailUpdatedAt: base.Add(time.Duration(i+1) * time.Hour),
LabelUpdatedAt: base.Add(time.Duration(i+1) * 2 * time.Hour),
PolicyUpdatedAt: base.Add(time.Duration(i+1) * 3 * time.Hour),
RefetchCriticalQueriesUntil: ptr.Time(base.Add(time.Duration(i+1) * 4 * time.Hour)),
RefetchRequested: i == 2, // only the "largest" host has refetch_requested = true
TeamID: teamIDs[i],
})
require.NoError(t, err)
sortHosts[i] = h
}
// Set columns not handled by NewHost via direct UPDATEs.
for i, h := range sortHosts {
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(ctx, `
UPDATE hosts SET
created_at = ?,
updated_at = ?,
last_enrolled_at = ?,
last_restarted_at = ?,
primary_ip_id = ?,
primary_ip = ?,
primary_mac = ?,
public_ip = ?,
timezone = ?,
build = ?,
code_name = ?,
cpu_type = ?,
cpu_subtype = ?,
cpu_brand = ?,
cpu_physical_cores = ?,
cpu_logical_cores = ?,
hardware_vendor = ?,
hardware_model = ?,
hardware_version = ?
WHERE id = ?`,
base.Add(time.Duration(i+1)*5*time.Hour),
base.Add(time.Duration(i+1)*6*time.Hour),
base.Add(time.Duration(i+1)*7*time.Hour),
base.Add(time.Duration(i+1)*8*time.Hour),
uint(i+1)*100, //nolint:gosec // ignore G115
fmt.Sprintf("10.0.0.%d", i+1),
fmt.Sprintf("aa:bb:cc:00:00:0%d", i+1),
fmt.Sprintf("8.0.0.%d", i+1),
fmt.Sprintf("UTC-%d", i),
fmt.Sprintf("sort-build-%d", i),
fmt.Sprintf("sort-code-%d", i),
fmt.Sprintf("sort-ct-%d", i),
fmt.Sprintf("sort-cs-%d", i),
fmt.Sprintf("sort-cb-%d", i),
i+1,
(i+1)*2,
fmt.Sprintf("sort-hv-%d", i),
fmt.Sprintf("sort-hm-%d", i),
fmt.Sprintf("sort-hver-%d", i),
h.ID,
)
return err
})
}
// Populate joined tables (host_disks, host_seen_times, host_updates,
// host_issues, host_emails) with values that also sort by host index.
for i, h := range sortHosts {
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(ctx, `
INSERT INTO host_disks (host_id, gigs_disk_space_available, percent_disk_space_available, gigs_total_disk_space)
VALUES (?, ?, ?, ?)`,
h.ID, float64((i+1)*100), float64((i+1)*10), float64((i+1)*1000))
return err
})
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(ctx,
`UPDATE host_seen_times SET seen_time = ? WHERE host_id = ?`,
base.Add(time.Duration(i+1)*9*time.Hour), h.ID)
return err
})
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(ctx,
`INSERT INTO host_updates (host_id, software_updated_at) VALUES (?, ?)`,
h.ID, base.Add(time.Duration(i+1)*10*time.Hour))
return err
})
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(ctx, `
INSERT INTO host_issues (host_id, failing_policies_count, critical_vulnerabilities_count, total_issues_count)
VALUES (?, ?, ?, ?)`,
h.ID, uint(i+1), uint(i+1)*2, uint(i+1)*3) //nolint:gosec // ignore G115
return err
})
mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(ctx,
`INSERT INTO host_emails (host_id, email, source) VALUES (?, ?, ?)`,
h.ID, fmt.Sprintf("sort-%d@example.com", i), "src")
return err
})
}
// Create a dynamic label and add the three hosts to it.
var createResp fleet.CreateLabelResponse
s.DoJSON("POST", "/api/latest/fleet/labels",
&fleet.LabelPayload{Name: teamPrefix + "-sort", Query: "select 1"}, http.StatusOK, &createResp)
sortLabel := createResp.Label.Label
for _, h := range sortHosts {
require.NoError(t, s.ds.RecordLabelQueryExecutions(ctx, h,
map[uint]*bool{sortLabel.ID: new(true)}, time.Now(), false))
}
ascIDs := []uint{sortHosts[0].ID, sortHosts[1].ID, sortHosts[2].ID}
descIDs := []uint{sortHosts[2].ID, sortHosts[1].ID, sortHosts[0].ID}
labelHostsURL := fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", sortLabel.ID)
// orderKeys lists every field for which sortHosts is set up to produce a
// strict, deterministic ASC ordering of [h0, h1, h2]. Each field gets its
// own subtest verifying both ASC and DESC orderings.
orderKeys := []string{
"id", "osquery_host_id", "created_at", "updated_at", "detail_updated_at",
"hostname", "uuid", "platform", "osquery_version", "os_version", "build",
"platform_like", "code_name", "uptime", "memory", "cpu_type", "cpu_subtype",
"cpu_brand", "cpu_physical_cores", "cpu_logical_cores",
"hardware_vendor", "hardware_model", "hardware_version", "hardware_serial",
"computer_name", "primary_ip_id", "distributed_interval", "logger_tls_period",
"config_tls_refresh", "primary_ip", "primary_mac", "label_updated_at",
"last_enrolled_at", "refetch_critical_queries_until", "team_id",
"policy_updated_at", "public_ip",
"gigs_disk_space_available", "percent_disk_space_available",
"gigs_total_disk_space", "seen_time", "software_updated_at",
"last_restarted_at", "timezone", "team_name",
"failing_policies_count", "critical_vulnerabilities_count",
"total_issues_count", "issues",
"device_mapping",
"display_name",
}
for _, key := range orderKeys {
t.Run(key, func(t *testing.T) {
params := []string{"order_key", key, "order_direction", "asc"}
if key == "device_mapping" {
params = append(params, "device_mapping", "true")
}
var resp listHostsResponse
s.DoJSON("GET", labelHostsURL, nil, http.StatusOK, &resp, params...)
require.Len(t, resp.Hosts, 3)
gotAsc := []uint{resp.Hosts[0].ID, resp.Hosts[1].ID, resp.Hosts[2].ID}
assert.Equal(t, ascIDs, gotAsc, "asc order mismatch")
params[3] = "desc"
resp = listHostsResponse{}
s.DoJSON("GET", labelHostsURL, nil, http.StatusOK, &resp, params...)
require.Len(t, resp.Hosts, 3)
gotDesc := []uint{resp.Hosts[0].ID, resp.Hosts[1].ID, resp.Hosts[2].ID}
assert.Equal(t, descIDs, gotDesc, "desc order mismatch")
})
}
// refetch_requested is a bool, so only h2 (true) has a unique value. ASC must
// place h2 last; DESC must place h2 first. The order between h0 and h1 (both
// false) is not deterministic, so we only assert the position of h2.
t.Run("refetch_requested", func(t *testing.T) {
var resp listHostsResponse
s.DoJSON("GET", labelHostsURL, nil, http.StatusOK, &resp,
"order_key", "refetch_requested", "order_direction", "asc")
require.Len(t, resp.Hosts, 3)
assert.Equal(t, sortHosts[2].ID, resp.Hosts[2].ID, "asc: h2 should be last")
resp = listHostsResponse{}
s.DoJSON("GET", labelHostsURL, nil, http.StatusOK, &resp,
"order_key", "refetch_requested", "order_direction", "desc")
require.Len(t, resp.Hosts, 3)
assert.Equal(t, sortHosts[2].ID, resp.Hosts[0].ID, "desc: h2 should be first")
})
// Cursor pagination (`after`) injects the order_key into the WHERE
// clause; SELECT-list aliases like team_name would error there. Verify
// that paging through team_name with a cursor returns the expected
// hosts and does not error.
t.Run("team_name with after cursor", func(t *testing.T) {
var resp listHostsResponse
s.DoJSON("GET", labelHostsURL, nil, http.StatusOK, &resp,
"order_key", "team_name", "order_direction", "asc",
"after", teamA.Name, "per_page", "10")
require.Len(t, resp.Hosts, 2)
assert.Equal(t, sortHosts[1].ID, resp.Hosts[0].ID)
assert.Equal(t, sortHosts[2].ID, resp.Hosts[1].ID)
})
})
}
// Sanity test to make sure fleet/labels/<all>/hosts and fleet/hosts return the same thing.
+35 -6
View File
@@ -330,12 +330,33 @@ func hostListOptionsFromRequest(r *http.Request) (fleet.HostListOptions, error)
}
hopt.DisableIssues = boolVal
}
if hopt.DisableIssues && r.URL.Query().Get("order_key") == "issues" {
return hopt, ctxerr.Wrap(
r.Context(), badRequest(
"Invalid order_key (issues cannot be ordered when they are disabled)",
),
)
if hopt.DisableIssues {
switch {
case r.URL.Query().Get("order_key") == "issues":
return hopt, ctxerr.Wrap(
r.Context(), badRequest(
"Invalid order_key (issues cannot be ordered when they are disabled)",
),
)
case r.URL.Query().Get("order_key") == "failing_policies_count":
return hopt, ctxerr.Wrap(
r.Context(), badRequest(
"Invalid order_key (failing_policies_count cannot be ordered when they are disabled)",
),
)
case r.URL.Query().Get("order_key") == "critical_vulnerabilities_count":
return hopt, ctxerr.Wrap(
r.Context(), badRequest(
"Invalid order_key (critical_vulnerabilities_count cannot be ordered when they are disabled)",
),
)
case r.URL.Query().Get("order_key") == "total_issues_count":
return hopt, ctxerr.Wrap(
r.Context(), badRequest(
"Invalid order_key (total_issues_count cannot be ordered when they are disabled)",
),
)
}
}
deviceMapping := r.URL.Query().Get("device_mapping")
@@ -347,6 +368,14 @@ func hostListOptionsFromRequest(r *http.Request) (fleet.HostListOptions, error)
hopt.DeviceMapping = boolVal
}
if !hopt.DeviceMapping && r.URL.Query().Get("order_key") == "device_mapping" {
return hopt, ctxerr.Wrap(
r.Context(), badRequest(
"Invalid order_key (device_mapping cannot be ordered when they are disabled)",
),
)
}
mdmID := r.URL.Query().Get("mdm_id")
if mdmID != "" {
id, err := strconv.ParseUint(mdmID, 10, 32)
+37
View File
@@ -308,6 +308,43 @@ func TestHostListOptionsFromRequest(t *testing.T) {
url: "/foo?disable_failing_policies=true&order_key=issues",
errorMessage: "Invalid order_key",
},
"error in failing_policies_count order key when disable_issues is set": {
url: "/foo?disable_issues=true&order_key=failing_policies_count",
errorMessage: "Invalid order_key (failing_policies_count cannot be ordered when they are disabled)",
},
"error in critical_vulnerabilities_count order key when disable_issues is set": {
url: "/foo?disable_issues=true&order_key=critical_vulnerabilities_count",
errorMessage: "Invalid order_key (critical_vulnerabilities_count cannot be ordered when they are disabled)",
},
"error in total_issues_count order key when disable_issues is set": {
url: "/foo?disable_issues=true&order_key=total_issues_count",
errorMessage: "Invalid order_key (total_issues_count cannot be ordered when they are disabled)",
},
"failing_policies_count order key allowed when disable_issues is not set": {
url: "/foo?order_key=failing_policies_count",
hostListOptions: fleet.HostListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "failing_policies_count",
},
},
},
"error in device_mapping order key when device_mapping is not enabled": {
url: "/foo?order_key=device_mapping",
errorMessage: "Invalid order_key (device_mapping cannot be ordered when they are disabled)",
},
"error in device_mapping order key when device_mapping is explicitly false": {
url: "/foo?device_mapping=false&order_key=device_mapping",
errorMessage: "Invalid order_key (device_mapping cannot be ordered when they are disabled)",
},
"device_mapping order key allowed when device_mapping is enabled": {
url: "/foo?device_mapping=true&order_key=device_mapping",
hostListOptions: fleet.HostListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "device_mapping",
},
DeviceMapping: true,
},
},
"error in device_mapping": {
url: "/foo?device_mapping=foo",
errorMessage: "Invalid device_mapping",