mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
🤖 Chore: remove deprecated appendListOptionsWithCursorToSQL (#44385)
Some checks are pending
Build binaries / build-binaries (push) Waiting to run
Check automated documentation is up-to-date / check-doc-gen (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
Deploy Fleet website / build (20.x) (push) Waiting to run
Apply latest configuration to dogfood with GitOps / fleet-gitops (push) Waiting to run
Test latest changes in fleetctl preview / test-preview (ubuntu-latest) (push) Waiting to run
golangci-lint / lint (push) Waiting to run
golangci-lint / lint-incremental (push) Waiting to run
Docker publish / publish (push) Waiting to run
Ingest maintained apps / build (push) Waiting to run
OSSF Scorecard / Validate Gradle wrapper (push) Waiting to run
OSSF Scorecard / Scorecard analysis (push) Waiting to run
Sync Maintained Apps Outputs to R2 / sync-to-r2 (push) Waiting to run
Test DB Changes / test-db-changes (push) Waiting to run
Run fleetd-chrome tests / test-fleetd-chrome (ubuntu-latest) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, main) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, mysql) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, service) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, vuln) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, fleetctl) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, integration-core) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, integration-enterprise) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, main) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, mysql) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, service) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, vuln) (push) Waiting to run
Go Tests / test-go-nanomdm (push) Waiting to run
Go Tests / test-go-no-db (fast) (push) Waiting to run
Go Tests / test-go-no-db (scripts) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, fleetctl) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, fleetctl) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, integration-core) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, integration-enterprise) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, integration-core) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, main) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, mysql) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, service) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, vuln) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, fleetctl) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, integration-core) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, integration-enterprise) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, integration-enterprise) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, main) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, mysql) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, service) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, vuln) (push) Waiting to run
Go Tests / upload-coverage (push) Blocked by required conditions
Go Tests / aggregate-result (push) Blocked by required conditions
JavaScript Tests / test-js (ubuntu-latest) (push) Waiting to run
JavaScript Tests / lint-js (ubuntu-latest) (push) Waiting to run
Test Mock Changes / test-mock-changes (push) Waiting to run
Test native tooling packaging / test-packaging (local, ubuntu-latest) (push) Waiting to run
Test native tooling packaging / test-packaging (remote, ubuntu-latest) (push) Waiting to run
Test Puppet / test-puppet (push) Waiting to run
Some checks are pending
Build binaries / build-binaries (push) Waiting to run
Check automated documentation is up-to-date / check-doc-gen (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
Deploy Fleet website / build (20.x) (push) Waiting to run
Apply latest configuration to dogfood with GitOps / fleet-gitops (push) Waiting to run
Test latest changes in fleetctl preview / test-preview (ubuntu-latest) (push) Waiting to run
golangci-lint / lint (push) Waiting to run
golangci-lint / lint-incremental (push) Waiting to run
Docker publish / publish (push) Waiting to run
Ingest maintained apps / build (push) Waiting to run
OSSF Scorecard / Validate Gradle wrapper (push) Waiting to run
OSSF Scorecard / Scorecard analysis (push) Waiting to run
Sync Maintained Apps Outputs to R2 / sync-to-r2 (push) Waiting to run
Test DB Changes / test-db-changes (push) Waiting to run
Run fleetd-chrome tests / test-fleetd-chrome (ubuntu-latest) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, main) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, mysql) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, service) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, vuln) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, fleetctl) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, integration-core) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, integration-enterprise) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, main) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, mysql) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, service) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, vuln) (push) Waiting to run
Go Tests / test-go-nanomdm (push) Waiting to run
Go Tests / test-go-no-db (fast) (push) Waiting to run
Go Tests / test-go-no-db (scripts) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, fleetctl) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, fleetctl) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, integration-core) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, integration-enterprise) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, integration-core) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, main) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, mysql) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, service) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, vuln) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, fleetctl) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, integration-core) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, integration-enterprise) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, integration-enterprise) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, main) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, mysql) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, service) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, vuln) (push) Waiting to run
Go Tests / upload-coverage (push) Blocked by required conditions
Go Tests / aggregate-result (push) Blocked by required conditions
JavaScript Tests / test-js (ubuntu-latest) (push) Waiting to run
JavaScript Tests / lint-js (ubuntu-latest) (push) Waiting to run
Test Mock Changes / test-mock-changes (push) Waiting to run
Test native tooling packaging / test-packaging (local, ubuntu-latest) (push) Waiting to run
Test native tooling packaging / test-packaging (remote, ubuntu-latest) (push) Waiting to run
Test Puppet / test-puppet (push) Waiting to run
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #44723 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [ ] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Strengthened validation of sorting/order parameters across many list and cursor-based endpoints — unsupported sort keys now return explicit errors and prevent unsafe queries. * Labels listing: label-list pagination query name changed; ordering by host_count is rejected when host counts are disabled (validated at request parsing). * **Tests** * Added/expanded tests covering allowed order keys, rejection of unknown keys, and pagination behavior for multiple listing APIs. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
This commit is contained in:
parent
8b073e3bf6
commit
227e94de5b
31 changed files with 551 additions and 89 deletions
1
changes/cleanup-list-opts
Normal file
1
changes/cleanup-list-opts
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Improved validation of order parameters on list endpoints
|
||||
|
|
@ -12,11 +12,16 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var deleteIDsBatchSize = 1000
|
||||
|
||||
// hostUpcomingActivitiesAllowedOrderKeys is empty: the query supplies its own
|
||||
// ORDER BY and the service layer forces opt.OrderKey to "".
|
||||
var hostUpcomingActivitiesAllowedOrderKeys = common_mysql.OrderKeyAllowlist{}
|
||||
|
||||
// ListHostUpcomingActivities returns the list of activities pending execution
|
||||
// or processing for the specific host. It is the "unified queue" of work to be
|
||||
// done on the host. That queue is "virtual" in the sense that it pulls from a
|
||||
|
|
@ -279,7 +284,10 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
|||
// the ListOptions supported for this query are limited, only the pagination
|
||||
// OFFSET and LIMIT can be added, so it's fine to have the ORDER BY already
|
||||
// in the query before calling this (enforced at the server layer).
|
||||
stmt, args := appendListOptionsWithCursorToSQL(listStmt, args, &opt)
|
||||
stmt, args, err := appendListOptionsWithCursorToSQLSecure(listStmt, args, &opt, hostUpcomingActivitiesAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "list upcoming activities")
|
||||
}
|
||||
|
||||
var activities []*fleet.UpcomingActivity
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &activities, stmt, args...); err != nil {
|
||||
|
|
|
|||
|
|
@ -535,6 +535,11 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("rejects_unknown_order_key", func(t *testing.T) {
|
||||
_, _, err := ds.ListHostUpcomingActivities(ctx, h1.ID, fleet.ListOptions{OrderKey: "h.node_key"})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testCleanupExpiredLiveQueries(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,26 @@ import (
|
|||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var carvesAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"id": "id",
|
||||
"host_id": "host_id",
|
||||
"created_at": "created_at",
|
||||
"name": "name",
|
||||
"block_count": "block_count",
|
||||
"block_size": "block_size",
|
||||
"carve_size": "carve_size",
|
||||
"carve_id": "carve_id",
|
||||
"request_id": "request_id",
|
||||
"session_id": "session_id",
|
||||
"expired": "expired",
|
||||
"max_block": "max_block",
|
||||
"error": "error",
|
||||
}
|
||||
|
||||
func upsertCarveDB(ctx context.Context, writer sqlx.ExecerContext, metadata *fleet.CarveMetadata) (int64, error) {
|
||||
stmt := `INSERT INTO carve_metadata (
|
||||
host_id,
|
||||
|
|
@ -234,7 +251,10 @@ func (ds *Datastore) ListCarves(ctx context.Context, opt fleet.CarveListOptions)
|
|||
if !opt.Expired {
|
||||
stmt += ` WHERE NOT expired `
|
||||
}
|
||||
stmt, params := appendListOptionsToSQL(stmt, &opt.ListOptions)
|
||||
stmt, params, err := appendListOptionsToSQLSecure(stmt, &opt.ListOptions, carvesAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "list carves")
|
||||
}
|
||||
carves := []*fleet.CarveMetadata{}
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &carves, stmt, params...); err != nil && err != sql.ErrNoRows {
|
||||
return nil, ctxerr.Wrap(ctx, err, "list carves")
|
||||
|
|
|
|||
|
|
@ -241,6 +241,39 @@ func testCarvesList(t *testing.T, ds *Datastore) {
|
|||
carves, err = ds.ListCarves(context.Background(), fleet.CarveListOptions{Expired: true})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, carves, 2)
|
||||
|
||||
for _, key := range []string{
|
||||
"id",
|
||||
"host_id",
|
||||
"created_at",
|
||||
"name",
|
||||
"block_count",
|
||||
"block_size",
|
||||
"carve_size",
|
||||
"carve_id",
|
||||
"request_id",
|
||||
"session_id",
|
||||
"expired",
|
||||
"max_block",
|
||||
"error",
|
||||
} {
|
||||
t.Run("allowed order_"+key, func(t *testing.T) {
|
||||
result, err := ds.ListCarves(context.Background(), fleet.CarveListOptions{
|
||||
Expired: true,
|
||||
ListOptions: fleet.ListOptions{OrderKey: key, PerPage: 10},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("rejects_unknown_key", func(t *testing.T) {
|
||||
_, err := ds.ListCarves(context.Background(), fleet.CarveListOptions{
|
||||
Expired: true,
|
||||
ListOptions: fleet.ListOptions{OrderKey: "h.node_key"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testCarvesUpdate(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,25 @@ import (
|
|||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var labelsAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"id": "l.id",
|
||||
"created_at": "l.created_at",
|
||||
"updated_at": "l.updated_at",
|
||||
"name": "l.name",
|
||||
"description": "l.description",
|
||||
"query": "l.query",
|
||||
"platform": "l.platform",
|
||||
"label_type": "l.label_type",
|
||||
"label_membership_type": "l.label_membership_type",
|
||||
"author_id": "l.author_id",
|
||||
"criteria": "l.criteria",
|
||||
"team_id": "l.team_id",
|
||||
|
||||
// dependent on include_host_counts being set on request
|
||||
// (checked on transport layer).
|
||||
"host_count": "host_count",
|
||||
}
|
||||
|
||||
func (ds *Datastore) ApplyLabelSpecs(ctx context.Context, specs []*fleet.LabelSpec) (err error) {
|
||||
return ds.ApplyLabelSpecsWithAuthor(ctx, specs, nil)
|
||||
}
|
||||
|
|
@ -814,7 +833,10 @@ func (ds *Datastore) ListLabels(ctx context.Context, filter fleet.TeamFilter, op
|
|||
return nil, err
|
||||
}
|
||||
|
||||
query, params = appendListOptionsWithCursorToSQL(query, params, &opt)
|
||||
query, params, err = appendListOptionsWithCursorToSQLSecure(query, params, &opt, labelsAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "list labels")
|
||||
}
|
||||
var labels []*fleet.Label
|
||||
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &labels, query, params...); err != nil {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ func TestLabels(t *testing.T) {
|
|||
{"ApplyLabelSpecsWithManualTeamLabels", testApplyLabelSpecsWithManualTeamLabels},
|
||||
{"ApplyLabelSpecsErrorsWhenLabelExistsOnAnotherTeam", testApplyLabelSpecsErrorsWhenLabelExistsOnAnotherTeam},
|
||||
{"ApplyLabelSpecsManualNilHosts", testApplyLabelSpecsManualNilHosts},
|
||||
{"ListLabelsOrderKeys", testListLabelsOrderKeys},
|
||||
{"LabelMembershipHostIDs", testLabelMembershipHostIDs},
|
||||
}
|
||||
// call TruncateTables first to remove migration-created labels
|
||||
|
|
@ -3608,6 +3609,53 @@ func testApplyLabelSpecsManualNilHosts(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, h1.ID, hosts[0].ID)
|
||||
}
|
||||
|
||||
func testListLabelsOrderKeys(t *testing.T, ds *Datastore) {
|
||||
ctx := t.Context()
|
||||
|
||||
for _, name := range []string{"alpha", "beta", "gamma"} {
|
||||
err := ds.ApplyLabelSpecs(ctx, []*fleet.LabelSpec{{Name: name, Query: "select 1"}})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
filter := fleet.TeamFilter{User: test.UserAdmin}
|
||||
for _, key := range []string{
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"name",
|
||||
"description",
|
||||
"query",
|
||||
"platform",
|
||||
"label_type",
|
||||
"label_membership_type",
|
||||
"author_id",
|
||||
"criteria",
|
||||
"team_id",
|
||||
"host_count",
|
||||
} {
|
||||
t.Run("order_"+key, func(t *testing.T) {
|
||||
labels, err := ds.ListLabels(ctx, filter, fleet.ListOptions{OrderKey: key, PerPage: 100}, true)
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, len(labels), 3)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("rejects_unknown_key", func(t *testing.T) {
|
||||
_, err := ds.ListLabels(ctx, filter, fleet.ListOptions{OrderKey: "l.id; SELECT 1"}, false)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("page_pagination_with_allowed_key", func(t *testing.T) {
|
||||
page0, err := ds.ListLabels(ctx, filter, fleet.ListOptions{OrderKey: "name", PerPage: 2, Page: 0}, false)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, page0)
|
||||
page1, err := ds.ListLabels(ctx, filter, fleet.ListOptions{OrderKey: "name", PerPage: 2, Page: 1}, false)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, page1)
|
||||
require.NotEqual(t, page0[0].Name, page1[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func testLabelMembershipHostIDs(t *testing.T, ds *Datastore) {
|
||||
ctx := t.Context()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,17 @@ import (
|
|||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var maintainedAppsAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"id": "fma.id",
|
||||
"name": "fma.name",
|
||||
"platform": "fma.platform",
|
||||
"slug": "fma.slug",
|
||||
}
|
||||
|
||||
func (ds *Datastore) UpsertMaintainedApp(ctx context.Context, app *fleet.MaintainedApp) (*fleet.MaintainedApp, error) {
|
||||
const upsertStmt = `
|
||||
INSERT INTO
|
||||
|
|
@ -198,7 +206,10 @@ func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamI
|
|||
}
|
||||
}
|
||||
|
||||
stmtPaged, args := appendListOptionsWithCursorToSQL(stmt, args, &opt)
|
||||
stmtPaged, args, err := appendListOptionsWithCursorToSQLSecure(stmt, args, &opt, maintainedAppsAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "list fleet maintained apps")
|
||||
}
|
||||
|
||||
var avail []fleet.MaintainedApp
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmtPaged, args...); err != nil {
|
||||
|
|
|
|||
|
|
@ -452,6 +452,19 @@ func testListAndGetAvailableApps(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
maintained3.TitleID = nil
|
||||
require.Equal(t, maintained3, gotApp)
|
||||
|
||||
for _, key := range []string{"id", "name", "platform", "slug"} {
|
||||
t.Run("order_"+key, func(t *testing.T) {
|
||||
result, _, err := ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{OrderKey: key, PerPage: 10, IncludeMetadata: true})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("rejects_unknown_key", func(t *testing.T) {
|
||||
_, _, err := ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{OrderKey: "h.node_key", IncludeMetadata: true})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testSyncAndRemoveApps(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ import (
|
|||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var mdmConfigProfilesAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"profile_uuid": "profile_uuid",
|
||||
"team_id": "team_id",
|
||||
"name": "name",
|
||||
"platform": "platform",
|
||||
"identifier": "identifier",
|
||||
"created_at": "created_at",
|
||||
"uploaded_at": "uploaded_at",
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetMDMCommandPlatform(ctx context.Context, commandUUID string) (string, error) {
|
||||
stmt := `
|
||||
SELECT CASE
|
||||
|
|
@ -704,9 +714,12 @@ FROM (
|
|||
}
|
||||
|
||||
args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID, fleetNames, globalOrTeamID, fleetNames, globalOrTeamID, fleetNames}
|
||||
stmt, args := appendListOptionsWithCursorToSQL(selectStmt, args, &opt)
|
||||
stmt, args, err := appendListOptionsWithCursorToSQLSecure(selectStmt, args, &opt, mdmConfigProfilesAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "list MDM config profiles")
|
||||
}
|
||||
|
||||
stmt, args, err := sqlx.In(stmt, args...)
|
||||
stmt, args, err = sqlx.In(stmt, args...)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "sqlx.In ListMDMConfigProfiles")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1043,13 +1043,12 @@ func testListMDMCommandsOrderKeys(t *testing.T, ds *Datastore) {
|
|||
ctx,
|
||||
fleet.TeamFilter{User: test.UserAdmin},
|
||||
&fleet.MDMCommandListOptions{
|
||||
ListOptions: fleet.ListOptions{OrderKey: "command_uuid", PerPage: 1},
|
||||
ListOptions: fleet.ListOptions{OrderKey: "command_uuid", PerPage: 1, IncludeMetadata: true},
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, cmds, 1)
|
||||
afterCursor := cmds[0].CommandUUID
|
||||
|
||||
next, _, _, err := ds.ListMDMCommands(
|
||||
ctx,
|
||||
fleet.TeamFilter{User: test.UserAdmin},
|
||||
|
|
@ -1119,7 +1118,6 @@ func testListMDMAppleCommandsOrderKeys(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, cmds, 1)
|
||||
afterCursor := cmds[0].CommandUUID
|
||||
|
||||
next, err := ds.ListMDMAppleCommands(
|
||||
ctx,
|
||||
fleet.TeamFilter{User: test.UserAdmin},
|
||||
|
|
@ -1857,6 +1855,11 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, c.wantMeta, gotMeta)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("rejects_unknown_order_key", func(t *testing.T) {
|
||||
_, _, err := ds.ListMDMConfigProfiles(ctx, nil, fleet.ListOptions{OrderKey: "h.node_key"})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testBulkSetPendingMDMHostProfilesBatch2(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -853,13 +853,6 @@ func sanitizeColumn(col string) string {
|
|||
return common_mysql.SanitizeColumn(col)
|
||||
}
|
||||
|
||||
// appendListOptionsToSQL is a facade that calls common_mysql.AppendListOptions.
|
||||
//
|
||||
// Deprecated: this method will be removed in favor of appendListOptionsWithCursorToSQL
|
||||
func appendListOptionsToSQL(sql string, opts *fleet.ListOptions) (string, []any) {
|
||||
return appendListOptionsWithCursorToSQL(sql, nil, opts)
|
||||
}
|
||||
|
||||
// appendListOptionsToSQLSecure is a facade that calls common_mysql.AppendListOptionsWithParamsSecure.
|
||||
// The allowlist parameter maps user-facing order key names to actual SQL column expressions.
|
||||
// This prevents SQL injection and information disclosure via arbitrary column sorting.
|
||||
|
|
@ -868,17 +861,6 @@ func appendListOptionsToSQLSecure(sql string, opts *fleet.ListOptions, allowlist
|
|||
return appendListOptionsWithCursorToSQLSecure(sql, nil, opts, allowlist)
|
||||
}
|
||||
|
||||
// appendListOptionsWithCursorToSQL is a facade that calls common_mysql.AppendListOptionsWithParams.
|
||||
// NOTE: this method will mutate opts.PerPage if it is 0, setting it to the default value.
|
||||
//
|
||||
// Deprecated: this method will be removed in favor of appendListOptionsWithCursorToSQLSecure
|
||||
func appendListOptionsWithCursorToSQL(sql string, params []any, opts *fleet.ListOptions) (string, []any) {
|
||||
if opts.PerPage == 0 {
|
||||
opts.PerPage = fleet.DefaultPerPage
|
||||
}
|
||||
return common_mysql.AppendListOptionsWithParams(sql, params, opts)
|
||||
}
|
||||
|
||||
// appendListOptionsWithCursorToSQLSecure is a facade that calls common_mysql.AppendListOptionsWithParamsSecure.
|
||||
// NOTE: this method will mutate opts.PerPage if it is 0, setting it to the default value.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -416,55 +416,6 @@ func TestAppendListOptionsToSQLSecure(t *testing.T) {
|
|||
require.Equal(t, "invalid_column", invalidKeyErr.Key)
|
||||
}
|
||||
|
||||
func TestAppendListOptionsToSQL(t *testing.T) {
|
||||
sql := "SELECT * FROM my_table"
|
||||
opts := fleet.ListOptions{
|
||||
OrderKey: "***name***",
|
||||
}
|
||||
|
||||
actual, _ := appendListOptionsToSQL(sql, &opts)
|
||||
expected := "SELECT * FROM my_table ORDER BY `name` ASC LIMIT 1000000"
|
||||
if actual != expected {
|
||||
t.Error("Expected", expected, "Actual", actual)
|
||||
}
|
||||
|
||||
sql = "SELECT * FROM my_table"
|
||||
opts.OrderDirection = fleet.OrderDescending
|
||||
actual, _ = appendListOptionsToSQL(sql, &opts)
|
||||
expected = "SELECT * FROM my_table ORDER BY `name` DESC LIMIT 1000000"
|
||||
if actual != expected {
|
||||
t.Error("Expected", expected, "Actual", actual)
|
||||
}
|
||||
|
||||
opts = fleet.ListOptions{
|
||||
PerPage: 10,
|
||||
}
|
||||
|
||||
sql = "SELECT * FROM my_table"
|
||||
actual, _ = appendListOptionsToSQL(sql, &opts)
|
||||
expected = "SELECT * FROM my_table LIMIT 10"
|
||||
if actual != expected {
|
||||
t.Error("Expected", expected, "Actual", actual)
|
||||
}
|
||||
|
||||
sql = "SELECT * FROM my_table"
|
||||
opts.Page = 2
|
||||
actual, _ = appendListOptionsToSQL(sql, &opts)
|
||||
expected = "SELECT * FROM my_table LIMIT 10 OFFSET 20"
|
||||
if actual != expected {
|
||||
t.Error("Expected", expected, "Actual", actual)
|
||||
}
|
||||
|
||||
opts = fleet.ListOptions{}
|
||||
sql = "SELECT * FROM my_table"
|
||||
actual, _ = appendListOptionsToSQL(sql, &opts)
|
||||
expected = "SELECT * FROM my_table LIMIT 1000000"
|
||||
|
||||
if actual != expected {
|
||||
t.Error("Expected", expected, "Actual", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhereFilterHostsByTeams(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,21 @@ import (
|
|||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var packsAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"id": "id",
|
||||
"name": "name",
|
||||
"description": "description",
|
||||
"platform": "platform",
|
||||
"disabled": "disabled",
|
||||
"pack_type": "pack_type",
|
||||
"created_at": "created_at",
|
||||
"updated_at": "updated_at",
|
||||
}
|
||||
|
||||
func (ds *Datastore) ApplyPackSpecs(ctx context.Context, specs []*fleet.PackSpec) (err error) {
|
||||
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
for _, spec := range specs {
|
||||
|
|
@ -455,8 +467,11 @@ func (ds *Datastore) ListPacks(ctx context.Context, opt fleet.PackListOptions) (
|
|||
query = `SELECT * FROM packs`
|
||||
}
|
||||
var packs []*fleet.Pack
|
||||
query, params := appendListOptionsToSQL(query, &opt.ListOptions)
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &packs, query, params...)
|
||||
query, params, err := appendListOptionsToSQLSecure(query, &opt.ListOptions, packsAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "list packs")
|
||||
}
|
||||
err = sqlx.SelectContext(ctx, ds.reader(ctx), &packs, query, params...)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, ctxerr.Wrap(ctx, err, "listing packs")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,6 +136,23 @@ func testPacksList(t *testing.T, ds *Datastore) {
|
|||
packs, err = ds.ListPacks(context.Background(), fleet.PackListOptions{IncludeSystemPacks: false})
|
||||
require.Nil(t, err)
|
||||
assert.Len(t, packs, 2)
|
||||
|
||||
for _, key := range []string{"id", "name", "description", "platform", "disabled", "pack_type", "created_at", "updated_at"} {
|
||||
t.Run("order_"+key, func(t *testing.T) {
|
||||
result, err := ds.ListPacks(context.Background(), fleet.PackListOptions{
|
||||
ListOptions: fleet.ListOptions{OrderKey: key, PerPage: 10},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("rejects_unknown_key", func(t *testing.T) {
|
||||
_, err := ds.ListPacks(context.Background(), fleet.PackListOptions{
|
||||
ListOptions: fleet.ListOptions{OrderKey: "h.node_key"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func setupPackSpecsTest(t *testing.T, ds fleet.Datastore) []*fleet.PackSpec {
|
||||
|
|
|
|||
|
|
@ -11,8 +11,35 @@ import (
|
|||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
)
|
||||
|
||||
var scheduledQueriesAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"id": "sq.id",
|
||||
"pack_id": "sq.pack_id",
|
||||
"name": "sq.name",
|
||||
"query_name": "sq.query_name",
|
||||
"description": "sq.description",
|
||||
"interval": "sq.interval",
|
||||
"snapshot": "sq.snapshot",
|
||||
"removed": "sq.removed",
|
||||
"platform": "sq.platform",
|
||||
"version": "sq.version",
|
||||
"shard": "sq.shard",
|
||||
"denylist": "sq.denylist",
|
||||
|
||||
"query": "q.query", // from queries table
|
||||
"query_id": "query_id", // from queries table
|
||||
|
||||
// JSON_EXTRACT required on the following:
|
||||
// must match SELECT clause so cursor pagination (WHERE) and ORDER BY are consistent
|
||||
"user_time_p50": "JSON_EXTRACT(ag.json_value, '$.user_time_p50')",
|
||||
"user_time_p95": "JSON_EXTRACT(ag.json_value, '$.user_time_p95')",
|
||||
"system_time_p50": "JSON_EXTRACT(ag.json_value, '$.system_time_p50')",
|
||||
"system_time_p95": "JSON_EXTRACT(ag.json_value, '$.system_time_p95')",
|
||||
"total_executions": "JSON_EXTRACT(ag.json_value, '$.total_executions')",
|
||||
}
|
||||
|
||||
// ListScheduledQueriesInPackWithStats loads a pack's scheduled queries and its aggregated stats.
|
||||
func (ds *Datastore) ListScheduledQueriesInPackWithStats(ctx context.Context, id uint, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) {
|
||||
query := `
|
||||
|
|
@ -42,7 +69,10 @@ func (ds *Datastore) ListScheduledQueriesInPackWithStats(ctx context.Context, id
|
|||
WHERE sq.pack_id = ?
|
||||
`
|
||||
params := []interface{}{false, fleet.AggregatedStatsTypeScheduledQuery, id}
|
||||
query, params = appendListOptionsWithCursorToSQL(query, params, &opts)
|
||||
query, params, err := appendListOptionsWithCursorToSQLSecure(query, params, &opts, scheduledQueriesAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "listing scheduled queries")
|
||||
}
|
||||
results := []*fleet.ScheduledQuery{}
|
||||
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, query, params...); err != nil {
|
||||
|
|
|
|||
|
|
@ -127,6 +127,42 @@ func testScheduledQueriesListInPackWithStats(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
}
|
||||
require.True(t, foundAgg)
|
||||
|
||||
for _, key := range []string{
|
||||
"id",
|
||||
"pack_id",
|
||||
"name",
|
||||
"query_name",
|
||||
"description",
|
||||
"interval",
|
||||
"snapshot",
|
||||
"removed",
|
||||
"platform",
|
||||
"version",
|
||||
"shard",
|
||||
"denylist",
|
||||
"query",
|
||||
"query_id",
|
||||
"user_time_p50",
|
||||
"user_time_p95",
|
||||
"system_time_p50",
|
||||
"system_time_p95",
|
||||
"total_executions",
|
||||
} {
|
||||
t.Run("order_"+key, func(t *testing.T) {
|
||||
_, err := ds.ListScheduledQueriesInPackWithStats(context.Background(), 1, fleet.ListOptions{
|
||||
OrderKey: key,
|
||||
PerPage: 10,
|
||||
After: " ",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("rejects_unknown_key", func(t *testing.T) {
|
||||
_, err := ds.ListScheduledQueriesInPackWithStats(context.Background(), 1, fleet.ListOptions{OrderKey: "h.node_key"})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testScheduledQueriesListInPack(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -15,11 +15,25 @@ import (
|
|||
constants "github.com/fleetdm/fleet/v4/pkg/scripts"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var scriptsAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"id": "s.id",
|
||||
"name": "s.name",
|
||||
"created_at": "s.created_at",
|
||||
"updated_at": "s.updated_at",
|
||||
}
|
||||
|
||||
// hostScriptDetailsAllowedOrderKeys is intentionally minimal: the service layer
|
||||
// pins OrderKey to "name" before reaching this datastore method.
|
||||
var hostScriptDetailsAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"name": "s.name",
|
||||
}
|
||||
|
||||
func (ds *Datastore) NewHostScriptExecutionRequest(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) {
|
||||
var res *fleet.HostScriptResult
|
||||
return res, ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
|
|
@ -949,7 +963,10 @@ WHERE
|
|||
}
|
||||
|
||||
args := []any{globalOrTeamID}
|
||||
stmt, args := appendListOptionsWithCursorToSQL(selectStmt, args, &opt)
|
||||
stmt, args, err := appendListOptionsWithCursorToSQLSecure(selectStmt, args, &opt, scriptsAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "list scripts")
|
||||
}
|
||||
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &scripts, stmt, args...); err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "select scripts")
|
||||
|
|
@ -1115,7 +1132,10 @@ WHERE
|
|||
)
|
||||
`
|
||||
}
|
||||
stmt, args := appendListOptionsWithCursorToSQL(sql, args, &opt)
|
||||
stmt, args, err := appendListOptionsWithCursorToSQLSecure(sql, args, &opt, hostScriptDetailsAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "get host script details")
|
||||
}
|
||||
|
||||
var rows []*row
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, args...); err != nil {
|
||||
|
|
|
|||
|
|
@ -560,6 +560,19 @@ func testListScripts(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, c.wantNames, gotNames)
|
||||
})
|
||||
}
|
||||
|
||||
for _, key := range []string{"id", "name", "created_at", "updated_at"} {
|
||||
t.Run("order_"+key, func(t *testing.T) {
|
||||
result, _, err := ds.ListScripts(ctx, nil, fleet.ListOptions{OrderKey: key, PerPage: 10})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("rejects_unknown_key", func(t *testing.T) {
|
||||
_, _, err := ds.ListScripts(ctx, nil, fleet.ListOptions{OrderKey: "h.node_key"})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testGetHostScriptDetails(t *testing.T, ds *Datastore) {
|
||||
|
|
@ -788,6 +801,11 @@ func testGetHostScriptDetails(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, pending, 0)
|
||||
})
|
||||
|
||||
t.Run("rejects_unknown_order_key", func(t *testing.T) {
|
||||
_, _, err := ds.GetHostScriptDetails(ctx, 42, nil, fleet.ListOptions{OrderKey: "h.node_key"}, "darwin")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testBatchSetScripts(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -4513,6 +4513,13 @@ func promoteSoftwareTitleInHouseApp(softwareTitleRecord *hostSoftware) {
|
|||
}
|
||||
}
|
||||
|
||||
// hostSoftwareAllowedOrderKeys is minimal: the service layer pins OrderKey to "name".
|
||||
// "source" is included for test determinism (used as the secondary order key in tests).
|
||||
var hostSoftwareAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"name": "name",
|
||||
"source": "source",
|
||||
}
|
||||
|
||||
func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opts fleet.HostSoftwareTitleListOptions) ([]*fleet.HostSoftwareWithInstaller, *fleet.PaginationMetadata, error) {
|
||||
if !opts.VulnerableOnly && (opts.MinimumCVSS > 0 || opts.MaximumCVSS > 0 || opts.KnownExploit) {
|
||||
return nil, nil, fleet.NewInvalidArgumentError(
|
||||
|
|
@ -5970,7 +5977,10 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt
|
|||
}
|
||||
stmt = fmt.Sprintf(stmt, replacements...)
|
||||
stmt = fmt.Sprintf("SELECT * FROM (%s) AS combined_results", stmt)
|
||||
stmt, _ = appendListOptionsToSQL(stmt, &opts.ListOptions)
|
||||
stmt, _, err = appendListOptionsToSQLSecure(stmt, &opts.ListOptions, hostSoftwareAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "list host software")
|
||||
}
|
||||
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hostSoftwareList, stmt, args...); err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "list host software")
|
||||
|
|
|
|||
|
|
@ -5417,6 +5417,13 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
}
|
||||
require.False(t, found, "Expected not find software %s in the list", softwareAlreadyInstalled.Name)
|
||||
|
||||
t.Run("rejects_unknown_order_key", func(t *testing.T) {
|
||||
_, _, err := ds.ListHostSoftware(ctx, host, fleet.HostSoftwareTitleListOptions{
|
||||
ListOptions: fleet.ListOptions{OrderKey: "h.node_key"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testListLinuxHostSoftware(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@ import (
|
|||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var softwareTitlesAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"id": "st.id",
|
||||
"name": "st.name",
|
||||
"source": "st.source",
|
||||
"extension_for": "st.extension_for",
|
||||
"bundle_identifier": "st.bundle_identifier",
|
||||
"hosts_count": "hosts_count",
|
||||
"counts_updated_at": "counts_updated_at",
|
||||
}
|
||||
|
||||
func (ds *Datastore) SoftwareTitleByID(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error) {
|
||||
var (
|
||||
teamFilter string // used to filter software titles host counts by team
|
||||
|
|
@ -269,9 +279,12 @@ func (ds *Datastore) ListSoftwareTitles(
|
|||
getTitlesCountStmt := fmt.Sprintf(`SELECT COUNT(DISTINCT s.id) FROM (%s) AS s`, getTitlesStmt)
|
||||
|
||||
var softwareList []*softwareTitleWithInstallerFields
|
||||
getTitlesStmt, args = appendListOptionsWithCursorToSQL(getTitlesStmt, args, &opt.ListOptions)
|
||||
// appendListOptionsWithCursorToSQL doesn't support multicolumn sort, so
|
||||
// we need to add it here
|
||||
getTitlesStmt, args, err = appendListOptionsWithCursorToSQLSecure(getTitlesStmt, args, &opt.ListOptions, softwareTitlesAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, 0, nil, ctxerr.Wrap(ctx, err, "list software titles")
|
||||
}
|
||||
// secondary sort columns must be added separately since the helper above
|
||||
// only handles a single ORDER BY column.
|
||||
getTitlesStmt = spliceSecondaryOrderBySoftwareTitlesSQL(getTitlesStmt, opt.ListOptions)
|
||||
|
||||
// Run list and count queries in parallel.
|
||||
|
|
|
|||
|
|
@ -557,6 +557,14 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
|
|||
require.Len(t, titles, 1)
|
||||
require.Equal(t, "installer1", titles[0].Name)
|
||||
require.Equal(t, "apps", titles[0].Source)
|
||||
|
||||
t.Run("rejects_unknown_order_key", func(t *testing.T) {
|
||||
_, _, _, err := ds.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{
|
||||
ListOptions: fleet.ListOptions{OrderKey: "h.node_key"},
|
||||
TeamID: ptr.Uint(0),
|
||||
}, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func listSoftwareTitlesCheckCount(t *testing.T, ds *Datastore, expectedListCount int, expectedFullCount int, opts fleet.SoftwareTitleListOptions) []fleet.SoftwareTitleListResult {
|
||||
|
|
|
|||
|
|
@ -13,12 +13,21 @@ import (
|
|||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var teamSearchColumns = []string{"name"}
|
||||
|
||||
var teamsAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"id": "t.id",
|
||||
"name": "t.name",
|
||||
"created_at": "t.created_at",
|
||||
"user_count": "user_count",
|
||||
"host_count": "host_count",
|
||||
}
|
||||
|
||||
const teamColumns = `id, created_at, name, filename, description, config`
|
||||
|
||||
func (ds *Datastore) NewTeam(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
|
|
@ -423,7 +432,10 @@ func (ds *Datastore) ListTeams(ctx context.Context, filter fleet.TeamFilter, opt
|
|||
// We must normalize the name for full Unicode support (Unicode equivalence).
|
||||
matchQuery := norm.NFC.String(opt.MatchQuery)
|
||||
query, params := searchLike(query, nil, matchQuery, teamSearchColumns...)
|
||||
query, params = appendListOptionsWithCursorToSQL(query, params, &opt)
|
||||
query, params, err := appendListOptionsWithCursorToSQLSecure(query, params, &opt, teamsAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "list teams")
|
||||
}
|
||||
teams := []*fleet.Team{}
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &teams, query, params...); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "list teams")
|
||||
|
|
|
|||
|
|
@ -457,6 +457,19 @@ func testTeamsList(t *testing.T, ds *Datastore) {
|
|||
t2.Users = nil
|
||||
require.Equal(t, t1, t2)
|
||||
}
|
||||
|
||||
for _, key := range []string{"id", "name", "created_at", "user_count", "host_count"} {
|
||||
t.Run("order_"+key, func(t *testing.T) {
|
||||
result, err := ds.ListTeams(context.Background(), fleet.TeamFilter{User: &user1}, fleet.ListOptions{OrderKey: key, PerPage: 10})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("rejects_unknown_key", func(t *testing.T) {
|
||||
_, err := ds.ListTeams(context.Background(), fleet.TeamFilter{User: &user1}, fleet.ListOptions{OrderKey: "h.node_key"})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testTeamsSummary(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,23 @@ import (
|
|||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var vulnerabilitiesAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
||||
"cve": "cve",
|
||||
"cvss_score": "cvss_score",
|
||||
"epss_probability": "epss_probability",
|
||||
"cisa_known_exploit": "cisa_known_exploit",
|
||||
"cve_published": "cve_published",
|
||||
"created_at": "created_at",
|
||||
"host_count": "hosts_count",
|
||||
"hosts_count": "hosts_count",
|
||||
"host_count_updated_at": "hosts_count_updated_at",
|
||||
"hosts_count_updated_at": "hosts_count_updated_at",
|
||||
}
|
||||
|
||||
func (ds *Datastore) Vulnerability(ctx context.Context, cve string, teamID *uint, includeCVEScores bool) (*fleet.VulnerabilityWithMetadata, error) {
|
||||
var vuln fleet.VulnerabilityWithMetadata
|
||||
|
||||
|
|
@ -303,7 +317,10 @@ func (ds *Datastore) ListVulnerabilities(ctx context.Context, opt fleet.VulnList
|
|||
}
|
||||
|
||||
opt.ListOptions.IncludeMetadata = !(opt.ListOptions.UsesCursorPagination())
|
||||
selectStmt, args = appendListOptionsWithCursorToSQL(selectStmt, args, &opt.ListOptions)
|
||||
selectStmt, args, err := appendListOptionsWithCursorToSQLSecure(selectStmt, args, &opt.ListOptions, vulnerabilitiesAllowedOrderKeys)
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "list vulnerabilities")
|
||||
}
|
||||
|
||||
// Execute the query
|
||||
var vulns []fleet.VulnerabilityWithMetadata
|
||||
|
|
|
|||
|
|
@ -509,7 +509,7 @@ func testListVulnerabilitiesSort(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, "CVE-2020-1237", list[3].CVE.CVE)
|
||||
require.Equal(t, "CVE-2020-1236", list[4].CVE.CVE)
|
||||
|
||||
opts.ListOptions.OrderKey = "published"
|
||||
opts.ListOptions.OrderKey = "cve_published"
|
||||
opts.ListOptions.OrderDirection = fleet.OrderAscending
|
||||
list, _, err = ds.ListVulnerabilities(context.Background(), opts)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -519,6 +519,13 @@ func testListVulnerabilitiesSort(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, "CVE-2020-1236", list[2].CVE.CVE)
|
||||
require.Equal(t, "CVE-2020-1235", list[3].CVE.CVE)
|
||||
require.Equal(t, "CVE-2020-1237", list[4].CVE.CVE)
|
||||
|
||||
t.Run("rejects_unknown_key", func(t *testing.T) {
|
||||
_, _, err := ds.ListVulnerabilities(context.Background(), fleet.VulnListOptions{
|
||||
ListOptions: fleet.ListOptions{OrderKey: "h.node_key"},
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func testVulnerabilitiesFilters(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func (r GetLabelResponse) Error() error { return r.Err }
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type ListLabelsRequest struct {
|
||||
ListOptions ListOptions `url:"list_options"`
|
||||
ListOptions ListOptions `url:"label_list_options"`
|
||||
TeamID *string `query:"team_id,optional" renameto:"fleet_id"` // string because it's an int or "global"
|
||||
IncludeHostCounts *bool `query:"include_host_counts,optional"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,14 @@ func parseCustomTags(urlTagValue string, r *http.Request, field reflect.Value) (
|
|||
}
|
||||
field.Set(reflect.ValueOf(opts))
|
||||
return true, nil
|
||||
|
||||
case "label_list_options":
|
||||
opts, err := labelListOptionsFromRequest(r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
field.Set(reflect.ValueOf(opts))
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2981,6 +2981,22 @@ func (s *integrationTestSuite) TestGetHostSummary() {
|
|||
// 'after' param is not supported for labels
|
||||
s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusBadRequest, &listResp, "order_key", "id", "after", "1")
|
||||
|
||||
// ordering by host_count when include_host_counts=false is rejected
|
||||
res := s.Do("GET", "/api/latest/fleet/labels", nil, http.StatusBadRequest, "order_key", "host_count", "include_host_counts", "false")
|
||||
require.Contains(t, extractServerErrorText(res.Body), "Invalid order_key (host_count cannot be ordered when they are disabled)")
|
||||
|
||||
// ordering by host_count with include_host_counts=true is allowed
|
||||
listResp = fleet.ListLabelsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "order_key", "host_count", "include_host_counts", "true")
|
||||
|
||||
// ordering by host_count without include_host_counts (default true) is allowed
|
||||
listResp = fleet.ListLabelsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "order_key", "host_count")
|
||||
|
||||
// include_host_counts=false with a different order_key is allowed
|
||||
listResp = fleet.ListLabelsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/labels", nil, http.StatusOK, &listResp, "order_key", "name", "include_host_counts", "false")
|
||||
|
||||
// team filter, no host
|
||||
s.DoJSON("GET", "/api/latest/fleet/host_summary", nil, http.StatusOK, &resp, "team_id", fmt.Sprint(team2.ID))
|
||||
require.Equal(t, resp.TotalsHostsCount, uint(0))
|
||||
|
|
@ -4170,6 +4186,90 @@ func (s *integrationTestSuite) TestScheduledQueries() {
|
|||
assert.Equal(t, uint(0), delBatchResp.Deleted)
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestScheduledQueriesInPackOrderKey() {
|
||||
t := s.T()
|
||||
|
||||
// create a pack
|
||||
var createPackResp createPackResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/packs", &createPackRequest{
|
||||
PackPayload: fleet.PackPayload{
|
||||
Name: new(strings.ReplaceAll(t.Name(), "/", "_")),
|
||||
},
|
||||
}, http.StatusOK, &createPackResp)
|
||||
pack := createPackResp.Pack.Pack
|
||||
|
||||
// create a query
|
||||
var createQueryResp fleet.CreateQueryResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/queries", &fleet.QueryPayload{
|
||||
Name: new(strings.ReplaceAll(t.Name(), "/", "_")),
|
||||
Query: new("select 1"),
|
||||
}, http.StatusOK, &createQueryResp)
|
||||
query := createQueryResp.Query
|
||||
|
||||
// schedule the query in the pack so the listing has at least one row
|
||||
var createSchedResp fleet.ScheduleQueryResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/packs/schedule", &fleet.ScheduleQueryRequest{
|
||||
PackID: pack.ID,
|
||||
QueryID: query.ID,
|
||||
Interval: 60,
|
||||
}, http.StatusOK, &createSchedResp)
|
||||
|
||||
// every key in scheduledQueriesAllowedOrderKeys must work end-to-end with cursor pagination.
|
||||
allowedOrderKeys := []string{
|
||||
"id",
|
||||
"pack_id",
|
||||
"name",
|
||||
"query_name",
|
||||
"description",
|
||||
"interval",
|
||||
"snapshot",
|
||||
"removed",
|
||||
"platform",
|
||||
"version",
|
||||
"shard",
|
||||
"denylist",
|
||||
"query",
|
||||
"query_id",
|
||||
"user_time_p50",
|
||||
"user_time_p95",
|
||||
"system_time_p50",
|
||||
"system_time_p95",
|
||||
"total_executions",
|
||||
}
|
||||
for _, orderKey := range allowedOrderKeys {
|
||||
t.Run(orderKey, func(t *testing.T) {
|
||||
var getInPackResp fleet.GetScheduledQueriesInPackResponse
|
||||
s.DoJSON(
|
||||
"GET", fmt.Sprintf("/api/latest/fleet/packs/%d/scheduled", pack.ID),
|
||||
nil, http.StatusOK, &getInPackResp,
|
||||
"order_key", orderKey,
|
||||
"after", "0",
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestScheduledQueriesInPackInvalidOrderKey() {
|
||||
t := s.T()
|
||||
|
||||
// create a pack so the endpoint has a real id to operate on
|
||||
var createPackResp createPackResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/packs", &createPackRequest{
|
||||
PackPayload: fleet.PackPayload{
|
||||
Name: new(strings.ReplaceAll(t.Name(), "/", "_")),
|
||||
},
|
||||
}, http.StatusOK, &createPackResp)
|
||||
pack := createPackResp.Pack.Pack
|
||||
|
||||
var getInPackResp fleet.GetScheduledQueriesInPackResponse
|
||||
s.DoJSON(
|
||||
"GET", fmt.Sprintf("/api/latest/fleet/packs/%d/scheduled", pack.ID),
|
||||
nil, http.StatusUnprocessableEntity, &getInPackResp,
|
||||
"order_key", "not_a_real_column",
|
||||
"after", "0",
|
||||
)
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestQueriesPaginationAndPlatformFilter() {
|
||||
t := s.T()
|
||||
|
||||
|
|
|
|||
|
|
@ -664,6 +664,27 @@ func hostListOptionsFromRequest(r *http.Request) (fleet.HostListOptions, error)
|
|||
return hopt, nil
|
||||
}
|
||||
|
||||
func labelListOptionsFromRequest(r *http.Request) (fleet.ListOptions, error) {
|
||||
opt, err := listOptionsFromRequest(r)
|
||||
if err != nil {
|
||||
return fleet.ListOptions{}, err
|
||||
}
|
||||
|
||||
includeHostCountsStr := r.URL.Query().Get("include_host_counts")
|
||||
if includeHostCountsStr != "" && opt.OrderKey == "host_count" {
|
||||
includeHostCounts, parseErr := strconv.ParseBool(includeHostCountsStr)
|
||||
if parseErr == nil && !includeHostCounts {
|
||||
return fleet.ListOptions{}, ctxerr.Wrap(
|
||||
r.Context(), badRequest(
|
||||
"Invalid order_key (host_count cannot be ordered when they are disabled)",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return opt, nil
|
||||
}
|
||||
|
||||
func carveListOptionsFromRequest(r *http.Request) (fleet.CarveListOptions, error) {
|
||||
opt, err := listOptionsFromRequest(r)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Reference in a new issue