Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion openframe/docs/fork-file-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ and the heaviest standing rebase cost.
| Area | Files |
|------|-------|
| Host assignments | `server/fleet/{policies,queries,hosts,datastore,service}.go`, `server/datastore/mysql/{policies,queries,hosts}.go`, `server/service/{global_policies,queries,handler,labels_util}.go`, `server/mock/{datastore,datastore_mock}.go`, `server/mock/service/service_mock.go`, `server/datastore/mysql/mysql.go`, `cmd/fleet/prepare.go` |
| Managed queries / policies | flag: `server/fleet/{queries,policies}.go`, `server/service/{queries,global_policies,team_policies}.go`, `server/datastore/mysql/{queries,policies}.go`, `schema.sql`; queries listing opt-in (`include_openframe_managed`): `server/fleet/{app,api_queries,service}.go`, `server/service/{global_schedule,team_schedule,queries_test}.go`, `server/mock/service/service_mock.go`; tests: `server/datastore/mysql/{queries,policies}_openframe_managed_test.go` — see [managed-queries.md](managed-queries.md), [managed-policies.md](managed-policies.md) |
| osquery host id | `server/fleet/hosts.go` |
| Query-results TTL cleanup | `server/config/config.go`, `server/fleet/{cron_schedules,datastore}.go`, `server/datastore/mysql/query_results.go`, `cmd/fleet/{cron,serve}.go` |
| Redis key prefix | `server/datastore/redis/redis.go`, `server/config/config.go`, `cmd/fleet/serve.go` |
Expand Down Expand Up @@ -230,7 +231,7 @@ server/service/openframe/openframe_authorization_manager.go
server/service/openframe/openframe_token_refresher.go
```

### Modified (46)
### Modified (52)

```
.github/pull_request_template.md
Expand Down Expand Up @@ -264,6 +265,8 @@ server/datastore/mysql/policies.go
server/datastore/mysql/queries.go
server/datastore/mysql/query_results.go
server/datastore/redis/redis.go
server/fleet/api_queries.go
server/fleet/app.go
server/fleet/cron_schedules.go
server/fleet/datastore.go
server/fleet/hosts.go
Expand All @@ -275,10 +278,12 @@ server/mock/datastore_mock.go
server/mock/service/service_mock.go
server/service/base_client.go
server/service/global_policies.go
server/service/global_schedule.go
server/service/handler.go
server/service/labels_util.go
server/service/orbit_client.go
server/service/osquery_utils/queries.go
server/service/queries.go
server/service/team_schedule.go
server/vulnerabilities/nvd/cpe.go
```
29 changes: 26 additions & 3 deletions openframe/docs/managed-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,24 @@ POST /api/latest/fleet/queries
`PATCH /api/latest/fleet/queries/{id}` takes `"openframe_managed": true|false`; omitting the field
leaves the flag as-is. Every query payload returns `"openframe_managed"`.

There is no `include_managed` parameter, for the same fail-closed reason as on the policies side.
### Listing

`GET /api/latest/fleet/queries` excludes managed queries by default. Pass
`?include_openframe_managed=1` to include them. The flag is **opt-in and defaults to excluded**, so
the decluttered operator-facing listing cannot be changed by an accidental or user-supplied value;
the OpenFrame platform's own services (query sync, host auto-assign) set it to enumerate what they
own.

This stays **decluttering, not access control** — the flag decides whether the listing is tidy for a
normal operator, it is not a security boundary. Tenant isolation is enforced separately, per-tenant.
(An earlier draft of this doc planned *no* such parameter and had the platform read managed queries
by id, on a "fail-closed" argument. That argument was about the declutter being unbypassable by a
user-supplied flag, **not** about isolation — so an opt-in listing flag is an equivalent trade: the
default still excludes, and every by-id read and write path still ignores the flag. It only spares
the sync/reconcile path from id-guessing when it needs to diff the full managed set.)

This differs from the policies side, which has no equivalent include flag — queries grew one because
the query synchronizer must list what it owns to reconcile it.

## Implementation

Expand Down Expand Up @@ -102,8 +119,14 @@ written by the same `INSERT`/`UPDATE` as every other query field (`NewQuery`, `S
| `server/datastore/mysql/schema.sql` | the column, so test databases match production |
| `server/fleet/queries.go` | `OpenframeManaged` on `Query` and on `QueryPayload` |
| `server/service/queries.go` | maps the flag on query create and on modify |
| `server/datastore/mysql/queries.go` | the `INSERT`, the `UPDATE`, both SELECT lists, and the listing exclusion |
| `server/datastore/mysql/queries_openframe_managed_test.go` | MySQL coverage |
| `server/datastore/mysql/queries.go` | the `INSERT`, the `UPDATE`, both SELECT lists, and the listing exclusion (skipped when `IncludeOpenframeManaged`) |
| `server/fleet/app.go` | `IncludeOpenframeManaged` on `ListQueryOptions` |
| `server/fleet/api_queries.go` | `IncludeOpenframeManaged` on `ListQueriesRequest` (query param `include_openframe_managed`) |
| `server/fleet/service.go` | trailing `includeOpenframeManaged bool` on the `ListQueries` interface |
| `server/service/queries.go` | `listQueriesEndpoint` forwards the request flag into `ListQueryOptions` |
| `server/mock/service/service_mock.go` | regenerated `ListQueries` mock for the new parameter |
| `server/service/global_schedule.go`, `server/service/team_schedule.go`, `server/service/queries_test.go` | internal `ListQueries` callers pass `false` (keep the default exclude) |
| `server/datastore/mysql/queries_openframe_managed_test.go` | MySQL coverage, incl. the `include_openframe_managed` opt-in |

## Host assignment interaction (open item)

Expand Down
7 changes: 5 additions & 2 deletions server/datastore/mysql/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -928,8 +928,11 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions
args := []interface{}{false, fleet.AggregatedStatsTypeScheduledQuery}
whereClauses := "WHERE saved = true"
// >>> OPENFRAME(managed-queries): drop platform-owned queries from this listing and from the
// count derived from it — openframe/docs/managed-queries.md
whereClauses += openframeManagedQueryExclusion
// count derived from it — openframe/docs/managed-queries.md. Opt-in callers (query sync,
// host auto-assign) pass IncludeOpenframeManaged to enumerate what OpenFrame owns.
if !opt.IncludeOpenframeManaged {
whereClauses += openframeManagedQueryExclusion
}
// <<< OPENFRAME(managed-queries)

switch {
Expand Down
8 changes: 8 additions & 0 deletions server/datastore/mysql/queries_openframe_managed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ func TestOpenframeManagedQueries(t *testing.T) {
require.Equal(t, 1, total, "managed queries must not inflate the count")
})

t.Run("included when opted in", func(t *testing.T) {
opts := fleet.ListQueryOptions{IncludeOpenframeManaged: true}
queries, total, _, _, err := ds.ListQueries(ctx, opts)
require.NoError(t, err)
require.ElementsMatch(t, []string{visible.Name, managed.Name}, queryNames(queries))
require.Equal(t, 2, total, "opt-in must surface managed queries in the count too")
})

t.Run("still readable by id", func(t *testing.T) {
got, err := ds.Query(ctx, managed.ID)
require.NoError(t, err)
Expand Down
6 changes: 6 additions & 0 deletions server/fleet/api_queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ type ListQueriesRequest struct {
MergeInherited bool `query:"merge_inherited,optional"`
// only return queries targeted to run on this platform
Platform string `query:"platform,optional"`
// >>> OPENFRAME(managed-queries): opt-in to list platform-owned queries — openframe/docs/managed-queries.md
// IncludeOpenframeManaged, when true, keeps OpenFrame-managed queries in the listing instead of
// hiding them. Defaults false so the operator-facing UI stays decluttered; OpenFrame's own
// services (query sync, host auto-assign) opt in to enumerate what they own.
IncludeOpenframeManaged bool `query:"include_openframe_managed,optional"`
// <<< OPENFRAME(managed-queries)
Comment thread
kirill-567 marked this conversation as resolved.
}

type ListQueriesResponse struct {
Expand Down
5 changes: 5 additions & 0 deletions server/fleet/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,11 @@ type ListQueryOptions struct {
// Return queries that are scheduled to run on this platform. One of "macos",
// "windows", or "linux"
Platform *string
// >>> OPENFRAME(managed-queries): opt-in to include platform-owned queries — openframe/docs/managed-queries.md
// IncludeOpenframeManaged, when true, disables the openframe_managed exclusion so the listing
// returns platform-owned queries too. Default false keeps them hidden.
IncludeOpenframeManaged bool
// <<< OPENFRAME(managed-queries)
}

// ListHostReportsOptions defines options for listing reports (queries) associated with a host.
Expand Down
3 changes: 2 additions & 1 deletion server/fleet/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,8 @@ type Service interface {
// and only non-scheduled queries will be returned if `*scheduled == false`.
// If mergeInherited is true and a teamID is provided, then queries from the global team will be
// included in the results. The inherited count is only meaningful when mergeInherited is true.
ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string) ([]*Query, int, int, *PaginationMetadata, error)
// OPENFRAME(managed-queries): trailing includeOpenframeManaged param opts in to platform-owned queries — openframe/docs/managed-queries.md
ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string, includeOpenframeManaged bool) ([]*Query, int, int, *PaginationMetadata, error)
GetQuery(ctx context.Context, id uint) (*Query, error)
// GetQueryReportResults returns all the stored results of a query for hosts the requestor has access to.
// Returns a boolean indicating whether the report is clipped.
Expand Down
6 changes: 3 additions & 3 deletions server/mock/service/service_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ type GetQuerySpecsFunc func(ctx context.Context, teamID *uint) ([]*fleet.QuerySp

type GetQuerySpecFunc func(ctx context.Context, teamID *uint, name string) (*fleet.QuerySpec, error)

type ListQueriesFunc func(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error)
type ListQueriesFunc func(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string, includeOpenframeManaged bool) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error)

type GetQueryFunc func(ctx context.Context, id uint) (*fleet.Query, error)

Expand Down Expand Up @@ -2938,11 +2938,11 @@ func (s *Service) GetQuerySpec(ctx context.Context, teamID *uint, name string) (
return s.GetQuerySpecFunc(ctx, teamID, name)
}

func (s *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) {
func (s *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string, includeOpenframeManaged bool) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) {
s.mu.Lock()
s.ListQueriesFuncInvoked = true
s.mu.Unlock()
return s.ListQueriesFunc(ctx, opt, teamID, scheduled, mergeInherited, platform)
return s.ListQueriesFunc(ctx, opt, teamID, scheduled, mergeInherited, platform, includeOpenframeManaged)
}

func (s *Service) GetQuery(ctx context.Context, id uint) (*fleet.Query, error) {
Expand Down
2 changes: 1 addition & 1 deletion server/service/global_schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func getGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fle
}

func (svc *Service) GetGlobalScheduledQueries(ctx context.Context, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) {
queries, _, _, _, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true), false, nil) // teamID == nil means global
queries, _, _, _, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true), false, nil, false) // teamID == nil means global
if err != nil {
return nil, err
}
Expand Down
17 changes: 9 additions & 8 deletions server/service/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
urlPlatform = &req.Platform
}

queries, count, inheritedCount, meta, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil, req.MergeInherited, urlPlatform)
queries, count, inheritedCount, meta, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil, req.MergeInherited, urlPlatform, req.IncludeOpenframeManaged)
if err != nil {
return fleet.ListQueriesResponse{Err: err}, nil
}
Expand All @@ -76,7 +76,7 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
}, nil
}

func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, urlPlatform *string) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) {
func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, urlPlatform *string, includeOpenframeManaged bool) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) {
// Check the user is allowed to list queries on the given team.
if err := svc.authz.Authorize(ctx, &fleet.Query{
TeamID: teamID,
Expand Down Expand Up @@ -106,11 +106,12 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, team
}

queries, count, inheritedCount, meta, err := svc.ds.ListQueries(ctx, fleet.ListQueryOptions{
ListOptions: opt,
TeamID: teamID,
IsScheduled: scheduled,
MergeInherited: mergeInherited,
Platform: dbPlatform,
ListOptions: opt,
TeamID: teamID,
IsScheduled: scheduled,
MergeInherited: mergeInherited,
Platform: dbPlatform,
IncludeOpenframeManaged: includeOpenframeManaged,
})
if err != nil {
return nil, 0, 0, nil, err
Expand Down Expand Up @@ -889,7 +890,7 @@ func getQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.S
}

func (svc *Service) GetQuerySpecs(ctx context.Context, teamID *uint) ([]*fleet.QuerySpec, error) {
queries, _, _, _, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil, false, nil)
queries, _, _, _, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil, false, nil, false)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting queries")
}
Expand Down
2 changes: 1 addition & 1 deletion server/service/queries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,7 @@ func TestQueryAuth(t *testing.T) {
_, err = svc.QueryReportIsClipped(ctx, tt.qid, fleet.DefaultMaxQueryReportRows)
checkAuthErr(t, tt.shouldFailRead, err)

_, _, _, _, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil, false, nil)
_, _, _, _, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil, false, nil, false)
checkAuthErr(t, tt.shouldFailRead, err)

teamName := ""
Expand Down
2 changes: 1 addition & 1 deletion server/service/team_schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (svc Service) GetTeamScheduledQueries(ctx context.Context, teamID uint, opt
if teamID != 0 {
teamID_ = &teamID
}
queries, _, _, _, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false, nil)
queries, _, _, _, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false, nil, false)
if err != nil {
return nil, err
}
Expand Down
Loading