Skip to content

[EDR Workflows] Add cross-project search read-path support for osquery#280273

Draft
szwarckonrad wants to merge 16 commits into
elastic:mainfrom
szwarckonrad:osquery-cps-read-paths
Draft

[EDR Workflows] Add cross-project search read-path support for osquery#280273
szwarckonrad wants to merge 16 commits into
elastic:mainfrom
szwarckonrad:osquery-cps-read-paths

Conversation

@szwarckonrad

Copy link
Copy Markdown
Contributor

Summary

On serverless with Cross-Project Search (CPS) enabled, this lets osquery read its query results and responses across all linked projects, so a user in a central project can investigate their whole fleet in one place. It is read-only and fully gated: when CPS is off, or on stateful Kibana, osquery behaves exactly as before.

Cross-project reads run as the logged-in user, which is the only identity Elasticsearch allows to fan out across projects. Action metadata and Fleet indices stay on the internal user and remain origin-only.

Test plan

  • Local Scout cps_local (origin plus one linked project), signed in as a stock role user (for example soc_manager or t2_analyst, not the operator user): osquery results and responses from the linked project appear in the origin project.
  • Same flow works for a role without Fleet index grants (editor, platform_engineer).
  • With CPS disabled, and on stateful Kibana, behavior is unchanged (local data only).
  • Cross-project live query dispatch is not possible from the origin project (read-only milestone).
LLM description

What and why

Adds CPS read-path support to the osquery server plugin, gated behind the platform cps.cpsEnabled config (serverless only, default false). When enabled, osquery data reads fan out across linked projects using the end-user identity; otherwise every path is byte-identical to today.

Gate

  • cps added to osquery optionalPlugins in kibana.jsonc.
  • cpsEnabled is derived from plugins.cps?.getCpsEnabled() ?? false in server/plugin.ts and stored on the osquery app context.

Client selection (Option B)

Two helpers under server/utils:

  • getReadEsClient: when cpsEnabled, returns clusterClient.asScoped(request, { projectRouting: 'space' }).asCurrentUser; otherwise asInternalUser.
  • getScopedSearch: when cpsEnabled, returns data.search.asScoped(request, { projectRouting: 'space' }); otherwise context.search.
  • cps_read_routing.ts classifies indices and decides which client the search strategy uses.

Option B means only osquery result and response data reads fan out as the current user. Fleet indices and osquery action metadata reads stay on asInternalUser and remain origin-only, for two reasons: action metadata is origin-only for this milestone, and two stock roles (editor, platform_engineer) have no Fleet index grants, so flipping those reads to the current user would break them.

Roles mirror

The kbn-es serverless roles mirror grants read on the legacy dotted .logs-osquery_manager.action.responses-* to the twelve stock security roles. The existing logs-* grant already covers results and modern responses, and these roles already have the read_project_routing cluster privilege. The .logs-osquery_manager.actions-* (action metadata) pattern is intentionally not granted, because under Option B the current user never reads it. The reserved-role grant for QA and production is a separate elasticsearch-controller change.

Picker

Registers osquery as read-only app access through plugins.cps?.cpsManager?.registerAppAccess.

Tests

Unit tests cover the index classification, the read client selection, and the search strategy client choice, plus updates to affected route tests. tsc and the relevant jest suites pass.

Made with Cursor

Gate osquery read paths on cpsEnabled: scoped asCurrentUser with projectRouting space on serverless CPS, unchanged asInternalUser elsewhere. Includes search strategy, routes, export PIT, roles mirror, and picker registration.
Correct getStartServices typing, Option B index routing (internal for fleet/actions metadata, scoped for result/response data), legacy responses role grant, and test/type fixes.
@szwarckonrad szwarckonrad added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting Team:Defend Workflows “EDR Workflows” sub-team of Security Solution Feature:CPS labels Jul 22, 2026
@szwarckonrad szwarckonrad self-assigned this Jul 22, 2026
@infra-vault-gh-plugin-prod

infra-vault-gh-plugin-prod Bot commented Jul 22, 2026

Copy link
Copy Markdown
🤖 Jobs for this PR can be triggered through checkboxes. 🚧

ℹ️ To trigger the CI, please tick the checkbox below 👇

  • Click to trigger kibana-pull-request for this PR!
  • Click to trigger kibana-deploy-project-from-pr for this PR!
  • Click to trigger kibana-deploy-cloud-from-pr for this PR!
  • Click to trigger kibana-entity-store-performance-from-pr for this PR!
  • Click to trigger kibana-storybooks-from-pr for this PR!

@szwarckonrad szwarckonrad added ci:project-deploy-security Create a Security Serverless Project ci:project-persist-deployment Persist project deployment indefinitely labels Jul 23, 2026
… flag

Flips the crossProjectSearch experimental flag default to true so the PR
serverless image ships with the flag on for QAF-based CPS e2e testing on QA.
Revert before marking this PR ready for review or merging; the feature must
land dark (default off) so QA can flip it on a BC.
@szwarckonrad

Copy link
Copy Markdown
Contributor Author

@elasticmachine merge upstream

@szwarckonrad

Copy link
Copy Markdown
Contributor Author

/ci

The CPS read-only project picker registration increases the osquery public
bundle past the previous 48128 limit. Raise it to 48500 to cover the current
size with a small buffer.
@szwarckonrad

Copy link
Copy Markdown
Contributor Author

@elasticmachine merge upstream

@elasticmachine

Copy link
Copy Markdown
Contributor

There are no new commits on the base branch.

@szwarckonrad

Copy link
Copy Markdown
Contributor Author

/ci

Under cross-project search the osquery History list and detail views read
action metadata from .logs-osquery_manager.actions-*, which was pinned to the
internal user and therefore origin-only. That made a linked project's queries
unreachable from the origin project, so the result and response data that does
fan out had no UI surface to appear on.

Route osquery-owned indices (action metadata, results, action responses)
through the request user so CPS fans them out, and keep only Fleet-owned
indices (.fleet-actions*, .fleet-actions-results*, .fleet-agents*) on the
internal client. The action dispatch path is unchanged and stays origin-only.

The serverless security roles now grant read on .logs-osquery_manager.actions-*
in a dedicated read-only block alongside the legacy
.logs-osquery_manager.action.responses-* pattern, which until now was granted
together with write on six of the twelve roles.

Index existence checks stay on the internal client because a read-only grant
cannot perform index admin actions, and the history routes fall back to the
internal client when the Fleet actions index is the target. Tagging an action
still resolves it origin-only, since tags are a write and cross-project writes
are not supported this milestone.
@szwarckonrad

Copy link
Copy Markdown
Contributor Author

@elasticmachine merge upstream

@szwarckonrad

Copy link
Copy Markdown
Contributor Author

/ci

Scheduled history rows and Status tab agent names were rendered only
from project-local sources, so a row fanned in from a linked project
showed a blank Query column and a raw agent UUID.

Both labels already exist on the response documents, so fall back to
them when the local lookup misses: pack_id, pack_name and query_name via
size-1 terms sub-aggregations on the scheduled_executions buckets, and
agent.name or host.name for the agent display name. The pack saved
object and the Fleet agent lookup still win whenever they resolve, so
same-project behaviour is unchanged, and rows whose labels are absent
degrade to today's blank/UUID rendering rather than failing.

The sub-aggregations run on keyword fields and cost +6ms at 500 buckets,
+140ms at the 10k bucket ceiling.
@szwarckonrad

Copy link
Copy Markdown
Contributor Author

/ci

@kibanamachine

kibanamachine commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

💔 Build Failed

Failed CI Steps

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
osquery 832 848 +16

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
osquery 1.4MB 1.4MB +166.0B

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
osquery 46.8KB 47.0KB +293.0B

History

cc @szwarckonrad

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting ci:build-serverless-image ci:project-deploy-security Create a Security Serverless Project ci:project-persist-deployment Persist project deployment indefinitely Feature:CPS release_note:skip Skip the PR/issue when compiling release notes Team:Defend Workflows “EDR Workflows” sub-team of Security Solution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants