Back to Kibana

Attack Discovery Action Execution: Hybrid Architecture (Option C)

x-pack/solutions/security/plugins/discoveries/docs/action_execution_comparison.md

9.5.012.0 KB
Original Source

Attack Discovery Action Execution: Hybrid Architecture (Option C)

Branch: attack_discovery_workflows_integration Date: 2026-03-27 Feature flag: securitySolution.attackDiscoveryWorkflowsEnabled


Executive Summary

Attack Discovery scheduling uses a unified hybrid architecture (Option C) regardless of the feature flag state:

  • Alerting Framework always owns scheduling, alert persistence, and action execution — with full throttling and frequency support.
  • Workflows engine owns only the generation pipeline (alert retrieval → generation → validation).

The feature flag controls which generation API and schedule CRUD API the UI calls, but it does not change how schedules are stored or how actions are executed. Both FF ON and FF OFF use alerting-backed schedules. Action frequency settings (onActiveAlert, onThrottleInterval, onActionGroupChange) are always enforced by the Alerting Framework's ActionScheduler.


1. Feature Flag Mechanics

Server Side

The feature flag is not read directly on the server for schedule storage. Both the public API (elastic_assistant) and the internal API (discoveries) create alerting rules via AttackDiscoveryScheduleDataClient. The workflowConfig field on the alerting rule's params tells the executor whether to delegate generation to the workflows engine.

File: x-pack/solutions/security/plugins/discoveries/server/lib/schedules/create_schedule_data_client/index.ts

UI Side

The UI reads the feature flag asynchronously and swaps which CRUD API it calls:

// use_schedule_api.ts
const enabled = await featureFlags.getBooleanValue(
  'securitySolution.attackDiscoveryWorkflowsEnabled',
  false
);
setIsWorkflowsEnabled(enabled);

When isWorkflowsEnabled is true, CRUD hooks target the internal discoveries API. When false, they target the public elastic_assistant API.

File: x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/logic/use_schedule_api.ts

For generation, useAttackDiscovery checks the flag and calls either callInternalGenerateApi() or callPublicGenerateApi():

File: x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_attack_discovery/index.tsx


2. Unified Architecture Summary

DimensionFF OFF (Legacy)FF ON (Internal)
Generation APIPOST /api/attack_discovery/_generate (public, elastic_assistant)POST /internal/attack_discovery/_generate (internal, discoveries)
Schedule APIPOST /api/attack_discovery/schedules (public)POST /internal/attack_discovery/schedules (internal)
Schedule storageAlerting RulesAlerting Rules (same)
Execution engineAlerting Framework task runnerAlerting Framework task runner (same)
Pipeline shapeMonolithic: retrieve + generate + validate in one executor callThree-phase: retrieval workflow → generation workflow → validation workflow
Action executionImplicit: alertsClient.report()ActionScheduler queues actions asyncImplicit: alertsClient.report()ActionScheduler queues actions async (same)
Action frequency/throttlingFully enforcedFully enforced (same)
Data clientAttackDiscoveryScheduleDataClientAttackDiscoveryScheduleDataClient (same)
TagNoneattack-discovery-schedule
Workflow configNot supportedworkflow_config field enables delegation to workflows engine

3. Action Execution Deep Dive

3.1 Legacy Path (FF OFF)

File: x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.ts

The alerting rule executor:

  1. Retrieves anonymization fields
  2. Calls generateAttackDiscoveries() — a monolithic graph that handles alert retrieval, LLM invocation, and validation
  3. Filters hallucinated alerts
  4. Deduplicates discoveries against existing ones
  5. For each discovery, reports an alert to the framework:
    const { uuid: alertDocId } = alertsClient.report({
      id: alertInstanceId,
      actionGroup: 'default',
    });
    alertsClient.setAlertData({ id: alertInstanceId, payload: baseAlertDocument, context });
    
  6. The executor exits. It never calls actionsClient.execute().

After the executor completes, the Alerting Framework's ActionScheduler:

  • Reads the rule's configured actions array
  • Applies frequency/throttling settings per action
  • Enqueues qualifying actions to the task queue via actionsClient.bulkEnqueueExecution()
  • Actions execute asynchronously in a separate task manager cycle

3.2 Hybrid Path (FF ON)

Branch point (shared executor): x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.ts Registered workflow executor (discoveries): x-pack/solutions/security/plugins/discoveries/server/lib/schedules/workflow_executor/index.ts (workflowExecutor)

Both modes run through the same alerting rule executor (attackDiscoveryScheduleExecutor in elastic_assistant). That executor inspects params.workflowConfig: when it is present, it looks up the workflow executor factory the discoveries plugin registered during setup (getWorkflowExecutorFactory()) and delegates to it (throwing a user-facing TaskRunError if no factory is registered). The discoveries workflowExecutor then:

  1. Calls executeGenerationWorkflow() — invokes the three-phase workflows pipeline (alert retrieval → generation → validation)
  2. For each discovery returned by the pipeline, reports an alert to the framework:
    alertsClient.report({ id: alertInstanceId, actionGroup: 'default' });
    alertsClient.setAlertData({ id: alertInstanceId, payload: baseAlertDocument, context });
    
  3. The executor exits. It never calls actionsClient.execute() directly.

The Alerting Framework's ActionScheduler then handles action execution identically to the legacy path — with full frequency/throttling enforcement.

Key insight: The shared executor's params.workflowConfig check is the only branch point. Actions are always handled by the framework regardless of which branch the executor takes.


4. Schedule CRUD Operations

4.1 Data Client Factory

File: x-pack/solutions/security/plugins/discoveries/server/lib/schedules/create_schedule_data_client/index.ts

The createScheduleDataClient() factory always returns AttackDiscoveryScheduleDataClient (alerting-backed). There is no AttackDiscoveryWorkflowScheduleDataClient — the dual-client architecture has been removed.

4.2 Route Pattern

All CRUD routes follow a simple pattern:

typescript
const disabledResponse = await assertWorkflowsEnabled({ context, response });
if (disabledResponse) return disabledResponse; // 404 when the FF is OFF

const dataClient = await createScheduleDataClient({ ... });
await dataClient.someOperation({ id, ...params });

No branching, no fallback, no dual-client try/catch.

4.3 Route guards (internal API)

Every internal schedule route is gated by assertWorkflowsEnabled — when securitySolution.attackDiscoveryWorkflowsEnabled is OFF the route returns 404 Not Found (not 403). Additionally, the create and update routes call assertAlertsIndexPatternInSpace to reject a client-supplied alerts_index_pattern that targets another space or a cross-space wildcard (returning 400), so a persisted schedule is space-correct at rest.


5. Cross-Mode Compatibility

5.1 Schedule Created with FF OFF, then FF Turned ON

Behavior: Works seamlessly.

Schedules are alerting rules in both modes. The two APIs apply an asymmetric tag strategy:

  • Public API (legacy): no applyTags; filterTags: { excludeTags: ['attack-discovery-schedule'] } on read — creates untagged schedules and hides workflow-tagged schedules from its _find/by-id results.
  • Internal API: applyTags: ['attack-discovery-schedule'] on write and no filterTags on read — it is the superset view, surfacing both its own tagged rules and untagged legacy schedules.

When FF is turned ON, previously untagged schedules remain visible and manageable via both APIs (the internal API sees everything). The UI automatically switches to the internal API for new schedules.

Why the asymmetry: the goal is migration continuity, not data-loss protection. The internal API deliberately surfaces legacy untagged schedules so they remain visible after the flag is turned on. The public API excludes workflow-tagged schedules so the legacy UI does not surface schedules whose workflow-only fields it cannot present. Cross-API update is safe: rulesClient.update() does a full params replacement, but the public update route now reads the existing schedule and re-attaches workflowConfig before calling updateSchedule, so editing a workflow schedule via the public API no longer silently wipes ES|QL queries or custom workflow IDs.

5.2 Schedule Created with FF ON, then FF Turned OFF

Behavior: Works seamlessly for execution; internally-tagged schedules are not visible via the public API.

Schedules created via the internal API are tagged with attack-discovery-schedule. When FF is OFF, the UI targets the public API. Because the public API's filterTags excludes the workflow tag on read, internally-tagged schedules do not appear in the public API's _find results. (The internal API has no filterTags, so it would still show them — but the UI does not call it while the flag is off.)

However, because all schedules are stored as alerting rules, they continue to execute normally. Re-enabling the flag restores full UI visibility via the internal API.

5.3 No Orphaned Schedules

Because both APIs use the same underlying storage (alerting rules), toggling the feature flag never orphans schedules. There is no separate workflow-definition storage that could become inaccessible.


6. Action Frequency/Throttling

The Alerting Framework's ActionScheduler supports:

SettingBehavior
onActiveAlertExecute action every time the alert is active
onThrottleIntervalExecute at most once per interval (e.g., "1h")
onActionGroupChangeExecute only when alert transitions between action groups
summary: trueAggregate all alerts into a single action execution

These settings are configured per-action on the alerting rule and always enforced — in both the legacy path (FF OFF) and the hybrid path (FF ON). There is no path where frequency/throttling settings are ignored.


7. Key File Reference

FilePurpose
x-pack/solutions/security/plugins/discoveries/server/lib/schedules/create_schedule_data_client/index.tsData client factory — always returns alerting-backed client
x-pack/solutions/security/plugins/discoveries/server/lib/schedules/workflow_executor/index.tsRegistered workflow executor (workflowExecutor) — delegates generation to workflows, reports via alertsClient
x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/register_schedule/executor.tsShared rule executor + branch point — checks params.workflowConfig; runs monolithic legacy generation or delegates to the registered workflow executor
x-pack/solutions/security/plugins/discoveries/server/routes/post/schedules/create_schedule.tsCreate route (internal API)
x-pack/solutions/security/plugins/discoveries/server/routes/get/schedules/find_schedules.tsFind route (internal API — unfiltered superset, no filterTags)
x-pack/solutions/security/plugins/discoveries/server/routes/put/schedules/update_schedule.tsUpdate route (internal API)
x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/settings_flyout/schedule/logic/use_schedule_api.tsUI hook swapper based on FF
x-pack/solutions/security/plugins/security_solution/public/attack_discovery/pages/use_attack_discovery/index.tsxGeneration hook — FF branch for API selection