Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
71c8c0d
adds security.buildAlertEntityGraph and security.renderAlertNarrative…
KDKHD Mar 23, 2026
58308cd
Changes from node scripts/lint_ts_projects --fix
kibanamachine Mar 24, 2026
7fa4b20
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Mar 24, 2026
7cadf54
lint errors
KDKHD Mar 24, 2026
ae63b50
Merge branch 'feature/alert-triage-steps-2' of github.com:KDKHD/kiban…
KDKHD Mar 24, 2026
26c5f9a
add approved step defs
KDKHD Mar 24, 2026
978aaad
add approved step defs
KDKHD Mar 24, 2026
a6ee125
t:wq
KDKHD Apr 1, 2026
cd85b93
Merge branch 'main' into feature/alert-triage-steps-2
KDKHD Apr 1, 2026
7757d16
Merge branch 'main' into feature/alert-triage-steps-2
KDKHD Apr 2, 2026
3f630b5
fix test
KDKHD Apr 2, 2026
c46ef49
Merge branch 'main' of github.com:elastic/kibana into feature/alert-t…
KDKHD Apr 2, 2026
41d86ab
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Apr 2, 2026
cf52ff4
Merge branch 'main' into feature/alert-triage-steps-2
KDKHD Apr 2, 2026
3bd5053
Merge branch 'main' of github.com:elastic/kibana into feature/alert-t…
KDKHD Apr 8, 2026
9861509
Merge branch 'feature/alert-triage-steps-2' of github.com:KDKHD/kiban…
KDKHD Apr 8, 2026
973d652
code review
KDKHD Apr 20, 2026
b8d5b61
Merge branch 'main' of github.com:elastic/kibana into feature/alert-t…
KDKHD Apr 21, 2026
589d3c3
Changes from node scripts/lint_ts_projects --fix
kibanamachine Apr 21, 2026
f17832c
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Apr 21, 2026
e09cd13
Restore accidentally removed constants from common/constants.ts
KDKHD Apr 21, 2026
0b14682
fix feature flag defaulta
KDKHD Apr 21, 2026
3d897c6
code review
KDKHD Apr 21, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ export const APPROVED_STEP_DEFINITIONS: Array<{ id: string; handlerHash: string
id: 'search.rerank',
handlerHash: '2bdde599ac1b8f38faecbd72a2d17a3d7b2740b874e047e92e9c30ba0ff01a4f',
},
{
id: 'security.buildAlertEntityGraph',
handlerHash: '90e95df7b6deaa5b6ab908c1ff8f4a3606b6e8f7fce5d59e0411d3d577d0be44',
},
{
id: 'security.renderAlertNarrative',
handlerHash: '1719b8db582f3695a5bac6df5434285f101ebf4fa032702613f28dd33d978988',
},
{
id: 'cases.addAlerts',
handlerHash: '1704c6d46ccb5432e1df6c24f7ebde8d4b1686c007dcaf6a5c5cac02b0222e3e',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -722,3 +722,7 @@ export enum SecurityAgentBuilderAttachments {
}

export const SECURITY_RULE_ATTACHMENT_ID = 'ai-rule-creation';

export const REGISTER_ALERT_VALIDATION_STEPS_FEATURE_FLAG =
'securitySolution.registerAlertValidationStepsEnabled' as const;
export const REGISTER_ALERT_VALIDATION_STEP_FEATURE_FLAG_DEFAULT = false as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { z } from '@kbn/zod/v4';
import { StepCategory } from '@kbn/workflows';
import type { BaseStepDefinition } from '@kbn/workflows';
import { i18n } from '@kbn/i18n';

const DEFAULT_ENTITY_FIELDS = ['host.name', 'user.name', 'service.name'] as const;
interface EntityFieldConfig {
field: string;
score?: number;
aliases?: Array<{ field: string; score?: number }>;
}
const DEFAULT_ENTITY_FIELD_CONFIG: EntityFieldConfig[] = DEFAULT_ENTITY_FIELDS.map((field) => ({
field,
}));

export const buildAlertEntityGraphInputSchema = z.object({
alertId: z.string().describe('The alert ID to find related alerts for'),
alertIndex: z.string().describe('The alert index'),
entity_fields: z
.array(
z.object({
field: z.string(),
score: z.coerce.number().finite().optional(),
aliases: z
.array(
z.object({
field: z.string(),
score: z.coerce.number().finite().optional(),
})
)
.optional(),
})
)
.optional()
.default(DEFAULT_ENTITY_FIELD_CONFIG)
.describe(
'Entity fields to extract and use for correlation. Each entry may provide an optional score override for that field, plus optional aliases (additional fields to match against using the same value). (default: host.name, user.name, service.name)'
),
seed_window: z
.string()
.optional()
.default('1h')
.describe(
'Initial time window around the seed alert timestamp (e.g., "1h", "24h"). Default: "1h"'
),
expand_window: z
.string()
.optional()
.default('1h')
.describe(
'Time window padding applied to the growing min/max timestamps as related alerts are found. Default: "1h"'
),
max_depth: z.coerce
.number()
.finite()
.int()
.min(0)
.optional()
.default(3)
.describe('Maximum recursion depth (number of expansion rounds). Default: 3'),
max_alerts: z.coerce
.number()
.finite()
.int()
.min(1)
.optional()
.default(300)
.describe('Hard cap on number of discovered alerts (including seed if included). Default: 300'),
page_size: z.coerce
.number()
.finite()
.int()
.min(1)
.max(1000)
.optional()
.default(200)
.describe('Elasticsearch page size per query. Default: 200'),
max_terms_per_query: z.coerce
.number()
.finite()
.int()
.min(1)
.optional()
.default(500)
.describe(
'Maximum number of terms per `terms` query clause (chunked if exceeded). Default: 500'
),
max_entities_per_field: z.coerce
.number()
.finite()
.int()
.min(1)
.optional()
.default(200)
.describe('Maximum number of distinct entity values to track per entity field. Default: 200'),
ignore_entities: z
.array(z.object({ field: z.string(), values: z.array(z.string()) }))
.optional()
.default([])
.describe(
'Entity values to ignore for correlation/scoring (e.g. field=user.name values=[root,SYSTEM]). Ignored values do not contribute to edge scores and are not used for graph expansion.'
),
min_entity_score: z.coerce
.number()
.finite()
.min(1)
.optional()
.default(2)
.describe(
'Minimum total entity match score required to create an edge (sum of per-label scores). Default: 2'
),
include_seed: z
.boolean()
.optional()
.default(true)
.describe('Whether to include the seed alert as a node in the output graph. Default: true'),
});

export const buildAlertEntityGraphOutputSchema = z.object({
nodes: z.array(z.object({ id: z.string() })),
edges: z.array(
z.object({
from: z.string(),
to: z.string(),
score: z.number(),
label_scores: z.record(z.string(), z.number()),
})
),
alerts: z.array(
z.object({
alert_id: z.string(),
alert_index: z.string(),
timestamp: z.string().optional(),
rule_name: z.string().optional(),
severity: z.string().optional(),
})
),
stats: z
.object({
depth_reached: z.number(),
nodes: z.number(),
edges: z.number(),
queries: z.number(),
time_range: z.object({ gte: z.string(), lte: z.string() }),
})
.optional(),
});

export const buildAlertEntityGraphStepCommonDefinition: BaseStepDefinition<
typeof buildAlertEntityGraphInputSchema,
typeof buildAlertEntityGraphOutputSchema
> = {
id: 'security.buildAlertEntityGraph',
label: i18n.translate('xpack.securitySolution.workflows.steps.buildAlertEntityGraph.label', {
defaultMessage: 'Build Alert Entity Graph',
}),
description: i18n.translate(
'xpack.securitySolution.workflows.steps.buildAlertEntityGraph.description',
{
defaultMessage:
'Build a scored graph of correlated alerts by performing a breadth-first search over shared entity fields (host, user, process, IP, cloud identity, etc.). Starting from a seed alert, the step searches a configurable time window (seed_window) for alerts that share entity values, then iteratively expands the window (expand_window) at each hop to chain through transitively linked entities up to max_depth rounds, producing a nodes-and-edges graph with per-field edge scores.',
}
),
category: StepCategory.Kibana,
stability: 'tech_preview',
inputSchema: buildAlertEntityGraphInputSchema,
outputSchema: buildAlertEntityGraphOutputSchema,
documentation: {
details: i18n.translate(
'xpack.securitySolution.workflows.steps.buildAlertEntityGraph.documentation.details',
{
defaultMessage:
'Starting from a seed alert, performs a breadth-first search to discover correlated alerts that share entity field values (host, user, process, IP, cloud identity, service, container, etc.). ' +
'Round 0 queries a fixed time window (seed_window) around the seed timestamp. Each subsequent round widens the search bounds by expand_window based on the min/max timestamps of newly discovered alerts, chaining through transitively linked entities up to max_depth hops or max_alerts total. ' +
'Entity fields carry configurable scores that reflect identifier specificity (e.g. process.entity_id=5, host.id=4, user.name=2, source.ip=1). Aliases allow cross-field matching (e.g. source.ip ↔ destination.ip for lateral-movement detection). ' +
'An edge is created between two alerts when their summed per-label entity scores meet or exceed min_entity_score. ignore_entities filters out noisy values (service accounts, loopback IPs) that would create false correlation chains. ' +
'The output is a scored nodes-and-edges graph with per-alert metadata (rule name, severity, timestamp), per-edge label scores, and traversal stats (depth reached, query count, effective time range).',
}
),
examples: [
`## Build alert entity graph
\`\`\`yaml
- name: build_alert_entity_graph
type: security.buildAlertEntityGraph
with:
alertId: "{{ variables.alert_id }}"
alertIndex: "{{ variables.alert_index }}"
include_seed: true
entity_fields:
- field: "process.entity_id"
score: 5
- field: "agent.id"
score: 4
- field: "user.id"
score: 4
- field: "host.name"
score: 2
- field: "user.name"
score: 2
- field: "service.name"
score: 1
- field: "source.ip"
score: 1
aliases:
- field: "destination.ip"
score: 4
min_entity_score: 4
ignore_entities:
- field: "user.name"
values: ["root", "SYSTEM", "Administrator"]
seed_window: "1h"
expand_window: "1h"
max_depth: 3
max_alerts: 300
\`\`\``,
],
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { z } from '@kbn/zod/v4';
import { StepCategory } from '@kbn/workflows';
import type { BaseStepDefinition } from '@kbn/workflows';
import { i18n } from '@kbn/i18n';

export const renderAlertNarrativeInputSchema = z.object({
alertId: z.string().describe('The alert ID'),
alertIndex: z.string().describe('The index that contains the alert'),
});

export const renderAlertNarrativeOutputSchema = z.object({
alert_id: z.string(),
alert_index: z.string(),
timeline_string: z.string().describe('A Timeline-like English string for the alert'),
message: z.string(),
});

export const renderAlertNarrativeStepCommonDefinition: BaseStepDefinition<
typeof renderAlertNarrativeInputSchema,
typeof renderAlertNarrativeOutputSchema
> = {
id: 'security.renderAlertNarrative',
label: i18n.translate('xpack.securitySolution.workflows.steps.renderAlertNarrative.label', {
defaultMessage: 'Render Alert Narrative',
}),
description: i18n.translate(
'xpack.securitySolution.workflows.steps.renderAlertNarrative.description',
{
defaultMessage:
'Render a human-readable narrative string for an alert based on its event, process, network, and host fields',
}
),
category: StepCategory.Kibana,
stability: 'tech_preview',
inputSchema: renderAlertNarrativeInputSchema,
outputSchema: renderAlertNarrativeOutputSchema,
documentation: {
details: i18n.translate(
'xpack.securitySolution.workflows.steps.renderAlertNarrative.documentation.details',
{
defaultMessage:
'Fetches the alert from Elasticsearch and renders a plain-English narrative string that summarizes the event and (when present) associated process context. The output is designed for use in workflows (e.g., notes, summaries, LLM prompts).',
}
),
examples: [
`## Render an alert narrative
\`\`\`yaml
- name: render_alert_narrative
type: security.renderAlertNarrative
with:
alertId: "{{ variables.alert_id }}"
alertIndex: "{{ variables.alert_index }}"
\`\`\``,
],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"serverless",
"agentBuilder",
"llmTasks",
"workflowsExtensions",
"cps",
"searchInferenceEndpoints"
],
Expand Down
2 changes: 2 additions & 0 deletions x-pack/solutions/security/plugins/security_solution/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -285,12 +285,14 @@ dependsOn:
- '@kbn/controls-constants'
- '@kbn/anonymization-plugin'
- '@kbn/anonymization-common'
- '@kbn/workflows-extensions'
- '@kbn/shared-ux-ai-components'
- '@kbn/controls-schemas'
- '@kbn/search-inference-endpoints'
- '@kbn/evals-plugin'
- '@kbn/shared-ux-column-presets'
- '@kbn/core-overlays-browser'
- '@kbn/workflows-management-plugin'
tags:
- plugin
- prod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,29 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
): PluginSetup {
this.services.setup(core, plugins);

const { home, usageCollection, management, cases, share } = plugins;
const { home, usageCollection, management, cases, share, workflowsExtensions } = plugins;
const { productFeatureKeys$ } = this.contract;

if (share) {
share.url.locators.create(new AIValueReportLocatorDefinition());
}

// Register workflow steps
if (workflowsExtensions) {
import('./workflows/step_types')
.then(async ({ registerWorkflowSteps }) => {
const [coreStart] = await core.getStartServices();
return registerWorkflowSteps(workflowsExtensions, coreStart);
})
.catch((error) => {
this.logger.error(
`Error registering security workflow steps: ${
error instanceof Error ? error.message : String(error)
}`
);
});
}

// Lazily instantiate subPlugins and initialize services
const mountDependencies = async (params?: AppMountParameters) => {
const { renderApp } = await this.lazyApplicationDependencies();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import type { KqlPluginStart } from '@kbn/kql/public';
import type { AgentBuilderPluginStart } from '@kbn/agent-builder-plugin/public';
import type { Logger } from '@kbn/logging';
import type { CPSPluginStart } from '@kbn/cps/public';
import type { WorkflowsExtensionsPublicPluginSetup } from '@kbn/workflows-extensions/public';
import type { EvalsPublicStart } from '@kbn/evals-plugin/public';
import type { ResolverPluginSetup } from './resolver/types';
import type { Inspect } from '../common/search_strategy';
Expand Down Expand Up @@ -120,6 +121,7 @@ export interface SetupPlugins {
cases?: CasesPublicSetup;
data: DataPublicPluginSetup;
discoverShared: DiscoverSharedPublicStart;
workflowsExtensions?: WorkflowsExtensionsPublicPluginSetup;
}

/**
Expand Down
Loading
Loading