Skip to content

Commit 55dad5f

Browse files
jonwalstedtclaude
andcommitted
feat(security): bulk add alerts to chat
Adds a "Add to chat" bulk action to the Kibana alerts table toolbar, allowing analysts to select multiple security alerts and send them to the Agent Builder AI chat in a single click. - security.alerts attachment type with server-side ES fetch (space-scoped) - alertsToAttachmentGroup helper chunks selections into batches of 20 - Bulk action wired across all 4 surfaces: Alerts page, Cases, Rule Details, and Attack Discovery - disableOnQuery: true prevents use with "Select all N" query mode - alert_count EBT telemetry event - Scout E2E test and kbn-evals-suite-security-alert-triage eval package Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ebc0cd0 commit 55dad5f

50 files changed

Lines changed: 2741 additions & 56 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.buildkite/pipelines/evals/evals.suites.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@
202202
"configPath": "x-pack/solutions/security/packages/kbn-evals-suite-security-esql-generation-regression/playwright.config.ts",
203203
"tags": ["security", "esql-generation"],
204204
"ciLabels": ["evals:security-esql-generation-regression"]
205+
},
206+
{
207+
"id": "security-alert-triage",
208+
"name": "Security Alert Triage",
209+
"slackChannel": "#security-generative-ai-evals",
210+
"configPath": "x-pack/solutions/security/packages/kbn-evals-suite-security-alert-triage/playwright.config.ts",
211+
"tags": ["security", "alert-triage"],
212+
"ciLabels": ["evals:security-alert-triage"]
205213
}
206214
]
207215
}

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,6 +1330,7 @@ x-pack/solutions/security/packages/kbn-evals-suite-endpoint @elastic/security-de
13301330
x-pack/solutions/security/packages/kbn-evals-suite-entity-analytics @elastic/security-entity-analytics
13311331
x-pack/solutions/security/packages/kbn-evals-suite-pci-compliance @elastic/security-defend-workflows
13321332
x-pack/solutions/security/packages/kbn-evals-suite-security-ai-rules @elastic/security-detection-engine
1333+
x-pack/solutions/security/packages/kbn-evals-suite-security-alert-triage @elastic/security-threat-hunting
13331334
x-pack/solutions/security/packages/kbn-evals-suite-security-automatic-migrations @elastic/security-threat-hunting
13341335
x-pack/solutions/security/packages/kbn-evals-suite-security-esql-generation-regression @elastic/security-detection-platform
13351336
x-pack/solutions/security/packages/kbn-scout-security @elastic/appex-qa @elastic/security-engineering-productivity

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,6 +1750,7 @@
17501750
"@kbn/evals-suite-observability-ai": "link:x-pack/solutions/observability/packages/kbn-evals-suite-observability-ai",
17511751
"@kbn/evals-suite-pci-compliance": "link:x-pack/solutions/security/packages/kbn-evals-suite-pci-compliance",
17521752
"@kbn/evals-suite-security-ai-rules": "link:x-pack/solutions/security/packages/kbn-evals-suite-security-ai-rules",
1753+
"@kbn/evals-suite-security-alert-triage": "link:x-pack/solutions/security/packages/kbn-evals-suite-security-alert-triage",
17531754
"@kbn/evals-suite-security-automatic-migrations": "link:x-pack/solutions/security/packages/kbn-evals-suite-security-automatic-migrations",
17541755
"@kbn/evals-suite-security-esql-generation-regression": "link:x-pack/solutions/security/packages/kbn-evals-suite-security-esql-generation-regression",
17551756
"@kbn/evals-suite-significant-events": "link:x-pack/platform/packages/shared/kbn-evals-suite-significant-events",

tsconfig.base.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,8 @@
12281228
"@kbn/evals-suite-pci-compliance/*": ["x-pack/solutions/security/packages/kbn-evals-suite-pci-compliance/*"],
12291229
"@kbn/evals-suite-security-ai-rules": ["x-pack/solutions/security/packages/kbn-evals-suite-security-ai-rules"],
12301230
"@kbn/evals-suite-security-ai-rules/*": ["x-pack/solutions/security/packages/kbn-evals-suite-security-ai-rules/*"],
1231+
"@kbn/evals-suite-security-alert-triage": ["x-pack/solutions/security/packages/kbn-evals-suite-security-alert-triage"],
1232+
"@kbn/evals-suite-security-alert-triage/*": ["x-pack/solutions/security/packages/kbn-evals-suite-security-alert-triage/*"],
12311233
"@kbn/evals-suite-security-automatic-migrations": ["x-pack/solutions/security/packages/kbn-evals-suite-security-automatic-migrations"],
12321234
"@kbn/evals-suite-security-automatic-migrations/*": ["x-pack/solutions/security/packages/kbn-evals-suite-security-automatic-migrations/*"],
12331235
"@kbn/evals-suite-security-esql-generation-regression": ["x-pack/solutions/security/packages/kbn-evals-suite-security-esql-generation-regression"],

x-pack/platform/packages/shared/response-ops/alerts-table/components/alerts_data_grid.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export const AlertsDataGrid = typedMemo(
8080
cellActionsOptions,
8181
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
8282
height,
83+
bulkAddToChatConfig,
8384
...euiDataGridProps
8485
} = props;
8586
const {
@@ -98,7 +99,14 @@ export const AlertsDataGrid = typedMemo(
9899
refresh: refreshQueries,
99100
columns,
100101
dataGridRef,
101-
services: { http, notifications, application, cases: casesService, settings },
102+
services: {
103+
http,
104+
notifications,
105+
application,
106+
cases: casesService,
107+
agentBuilder: agentBuilderService,
108+
settings,
109+
},
102110
} = renderContext;
103111

104112
const { colorMode, euiTheme } = useEuiTheme();
@@ -126,6 +134,8 @@ export const AlertsDataGrid = typedMemo(
126134
notifications,
127135
application,
128136
casesService,
137+
agentBuilderService,
138+
bulkAddToChatConfig,
129139
});
130140

131141
const refresh = useCallback(() => {

x-pack/platform/packages/shared/response-ops/alerts-table/hooks/use_bulk_actions.test.tsx

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ import { AlertsQueryContext } from '@kbn/alerts-ui-shared/src/common/contexts/al
1515
import {
1616
useBulkActions,
1717
useBulkAddToCaseActions,
18+
useBulkAddToChatActions,
1819
useBulkUntrackActions,
1920
useBulkMuteActions,
2021
} from './use_bulk_actions';
2122
import { createCasesServiceMock } from '../mocks/cases.mock';
2223
import { BulkActionsVerbs, type PublicAlertsDataGridProps } from '../types';
23-
import type { AdditionalContext, RenderContext } from '../types';
24+
import type { AdditionalContext, OpenChatService, RenderContext, TimelineItem } from '../types';
2425
import { useAlertsTableContext } from '../contexts/alerts_table_context';
2526
import { createPartialObjectMock, testQueryClientConfig } from '../utils/test';
2627
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
@@ -427,6 +428,97 @@ describe('bulk action hooks', () => {
427428
});
428429
});
429430

431+
describe('useBulkAddToChatActions', () => {
432+
const mockOpenChat = jest.fn();
433+
const agentBuilderService: OpenChatService = { openChat: mockOpenChat };
434+
const mockAttachments = [{ type: 'security.alerts', data: { alertIds: ['id1'] } }];
435+
const convertAlertToAttachment = jest.fn().mockReturnValue(mockAttachments);
436+
437+
beforeEach(() => {
438+
jest.clearAllMocks();
439+
});
440+
441+
it('returns empty array when agentBuilderService is not provided', () => {
442+
const { result } = renderHook(
443+
() =>
444+
useBulkAddToChatActions({
445+
bulkAddToChatConfig: { convertAlertToAttachment },
446+
}),
447+
{ wrapper }
448+
);
449+
450+
expect(result.current).toEqual([]);
451+
});
452+
453+
it('returns empty array when bulkAddToChatConfig is not provided', () => {
454+
const { result } = renderHook(() => useBulkAddToChatActions({ agentBuilderService }), {
455+
wrapper,
456+
});
457+
458+
expect(result.current).toEqual([]);
459+
});
460+
461+
it('returns the add-to-chat action when both service and config are provided', () => {
462+
const { result } = renderHook(
463+
() =>
464+
useBulkAddToChatActions({
465+
agentBuilderService,
466+
bulkAddToChatConfig: { convertAlertToAttachment },
467+
}),
468+
{ wrapper }
469+
);
470+
471+
expect(result.current).toHaveLength(1);
472+
expect(result.current[0].key).toBe('bulk-add-to-chat');
473+
expect(result.current[0]['data-test-subj']).toBe('bulk-add-to-chat');
474+
});
475+
476+
it('calls openChat with converted attachments when the action is clicked', () => {
477+
const alerts: TimelineItem[] = [
478+
{ _id: 'id1', _index: 'idx', data: [], ecs: { _id: 'id1', _index: 'idx' } },
479+
];
480+
481+
const { result } = renderHook(
482+
() =>
483+
useBulkAddToChatActions({
484+
agentBuilderService,
485+
bulkAddToChatConfig: { convertAlertToAttachment },
486+
}),
487+
{ wrapper }
488+
);
489+
490+
result.current[0].onClick(alerts);
491+
492+
expect(convertAlertToAttachment).toHaveBeenCalledWith(alerts);
493+
expect(mockOpenChat).toHaveBeenCalledWith({
494+
autoSendInitialMessage: false,
495+
newConversation: true,
496+
initialMessage: undefined,
497+
attachments: mockAttachments,
498+
});
499+
});
500+
501+
it('passes initialMessage to openChat', () => {
502+
const { result } = renderHook(
503+
() =>
504+
useBulkAddToChatActions({
505+
agentBuilderService,
506+
bulkAddToChatConfig: {
507+
convertAlertToAttachment,
508+
initialMessage: 'Please triage these alerts.',
509+
},
510+
}),
511+
{ wrapper }
512+
);
513+
514+
result.current[0].onClick([]);
515+
516+
expect(mockOpenChat).toHaveBeenCalledWith(
517+
expect.objectContaining({ initialMessage: 'Please triage these alerts.' })
518+
);
519+
});
520+
});
521+
430522
describe('useBulkUntrackActions', () => {
431523
beforeEach(() => {
432524
jest.clearAllMocks();

x-pack/platform/packages/shared/response-ops/alerts-table/hooks/use_bulk_actions.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ import type {
2323
BulkActionsReducerAction,
2424
TimelineItem,
2525
BulkEditTagsFlyoutState,
26+
BulkAddToChatConfig,
27+
OpenChatService,
2628
} from '../types';
2729
import { BulkActionsVerbs } from '../types';
2830
import type { CasesService, PublicAlertsDataGridProps } from '../types';
2931
import {
3032
ADD_TO_EXISTING_CASE,
3133
ADD_TO_NEW_CASE,
34+
ADD_TO_CHAT,
3235
ALERTS_ALREADY_ATTACHED_TO_CASE,
3336
EDIT_TAGS,
3437
MARK_AS_UNTRACKED,
@@ -49,6 +52,8 @@ interface BulkActionsProps {
4952
hideBulkActions?: boolean;
5053
application: ApplicationStart;
5154
casesService?: CasesService;
55+
agentBuilderService?: OpenChatService;
56+
bulkAddToChatConfig?: BulkAddToChatConfig;
5257
http: HttpStart;
5358
notifications: NotificationsStart;
5459
}
@@ -408,6 +413,43 @@ export const useBulkMuteActions = ({
408413
);
409414
};
410415

416+
export const useBulkAddToChatActions = ({
417+
agentBuilderService,
418+
bulkAddToChatConfig,
419+
}: {
420+
agentBuilderService?: OpenChatService;
421+
bulkAddToChatConfig?: BulkAddToChatConfig;
422+
}) => {
423+
const { convertAlertToAttachment, initialMessage } = bulkAddToChatConfig ?? {};
424+
425+
const onAddToChatClick = useCallback(
426+
(alerts?: TimelineItem[]) => {
427+
if (!agentBuilderService || !convertAlertToAttachment) return;
428+
agentBuilderService.openChat({
429+
autoSendInitialMessage: false,
430+
newConversation: true,
431+
initialMessage,
432+
attachments: convertAlertToAttachment(alerts ?? []),
433+
});
434+
},
435+
[agentBuilderService, convertAlertToAttachment, initialMessage]
436+
);
437+
438+
return useMemo(() => {
439+
if (!agentBuilderService || !convertAlertToAttachment) return [];
440+
return [
441+
{
442+
label: ADD_TO_CHAT,
443+
key: 'bulk-add-to-chat',
444+
disableOnQuery: true,
445+
disabledLabel: ADD_TO_CHAT,
446+
'data-test-subj': 'bulk-add-to-chat',
447+
onClick: onAddToChatClick,
448+
},
449+
];
450+
}, [agentBuilderService, convertAlertToAttachment, onAddToChatClick]);
451+
};
452+
411453
const EMPTY_BULK_ACTIONS_CONFIG: BulkActionsPanelConfig[] = [];
412454

413455
export function useBulkActions({
@@ -422,6 +464,8 @@ export function useBulkActions({
422464
notifications,
423465
application,
424466
casesService,
467+
agentBuilderService,
468+
bulkAddToChatConfig,
425469
}: BulkActionsProps): UseBulkActions {
426470
const {
427471
bulkActionsStore: [bulkActionsState, updateBulkActionsState],
@@ -491,16 +535,29 @@ export function useBulkActions({
491535
},
492536
];
493537
}, [tagsAction, application?.capabilities]);
538+
const addToChatActions = useBulkAddToChatActions({
539+
agentBuilderService,
540+
bulkAddToChatConfig,
541+
});
494542

495543
const initialItems = useMemo(() => {
496544
const isSiem = ruleTypeIds?.some(isSiemRuleType);
497545
return [
498546
...caseBulkActions,
547+
...(agentBuilderService ? addToChatActions : []),
499548
...(isSiem ? [] : untrackBulkActions),
500549
...(isSiem ? [] : tagsBulkActions),
501550
...(isSiem ? [] : muteBulkActions),
502551
];
503-
}, [caseBulkActions, ruleTypeIds, untrackBulkActions, tagsBulkActions, muteBulkActions]);
552+
}, [
553+
caseBulkActions,
554+
ruleTypeIds,
555+
untrackBulkActions,
556+
tagsBulkActions,
557+
muteBulkActions,
558+
addToChatActions,
559+
agentBuilderService,
560+
]);
504561

505562
const bulkActions = useMemo(() => {
506563
if (hideBulkActions) {

x-pack/platform/packages/shared/response-ops/alerts-table/translations.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ export const ADD_TO_EXISTING_CASE = i18n.translate(
251251
}
252252
);
253253

254+
export const ADD_TO_CHAT = i18n.translate('xpack.responseOpsAlertsTable.actions.addChat', {
255+
defaultMessage: 'Add to chat',
256+
});
257+
254258
export const ADD_TO_NEW_CASE = i18n.translate('xpack.responseOpsAlertsTable.actions.addToNewCase', {
255259
defaultMessage: 'Add to new case',
256260
});

x-pack/platform/packages/shared/response-ops/alerts-table/types.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,33 @@ import type { SetRequired } from 'type-fest';
4343
import type { MaintenanceWindow } from '@kbn/maintenance-windows-plugin/common';
4444
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
4545
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
46+
47+
/**
48+
* A single conversation attachment or a group of attachments.
49+
* Defined structurally here to avoid a compile-time dependency on agent-builder packages.
50+
*/
51+
export type ConversationAttachmentInput =
52+
| { type: string; data?: unknown; hidden?: boolean }
53+
| { type: 'group'; id: string; label: string; items: Array<{ type: string; data?: unknown }> };
54+
55+
/**
56+
* Minimal structural interface for the chat service required by the alerts table.
57+
* Using a local structural type avoids a compile-time dependency on agent-builder packages
58+
* from this shared platform package.
59+
*/
60+
export interface OpenChatService {
61+
openChat(options?: {
62+
attachments?: ConversationAttachmentInput[];
63+
newConversation?: boolean;
64+
initialMessage?: string;
65+
autoSendInitialMessage?: boolean;
66+
}): void;
67+
}
68+
69+
export interface BulkAddToChatConfig {
70+
convertAlertToAttachment: (alerts: TimelineItem[]) => ConversationAttachmentInput[];
71+
initialMessage?: string;
72+
}
4673
import type { FieldBrowserOptions } from '@kbn/response-ops-alerts-fields-browser';
4774
import type { MutedAlerts } from '@kbn/response-ops-alerts-apis/types';
4875
import type { NotificationsStart } from '@kbn/core-notifications-browser';
@@ -411,6 +438,14 @@ export interface AlertsTableProps<AC extends AdditionalContext = AdditionalConte
411438
* @default new LocalStorageWrapper(window.localStorage)
412439
*/
413440
configurationStorage?: IStorageWrapper | null;
441+
442+
/**
443+
* Configuration for the bulk "Add to chat" action. When provided, a bulk
444+
* action will appear in the table allowing users to add selected alerts to
445+
* the Agent Builder chat.
446+
*/
447+
bulkAddToChatConfig?: BulkAddToChatConfig;
448+
414449
/**
415450
* Show a CSV export button in the toolbar. The button exports all alerts matching
416451
* the current filters using the reporting CSV endpoint.
@@ -438,6 +473,7 @@ export interface AlertsTableProps<AC extends AdditionalContext = AdditionalConte
438473
* The cases service is optional: cases features will be disabled if not provided
439474
*/
440475
cases?: CasesService;
476+
agentBuilder?: OpenChatService;
441477
};
442478
}
443479

@@ -613,6 +649,7 @@ export interface AlertsDataGridProps<AC extends AdditionalContext = AdditionalCo
613649
extends PublicAlertsDataGridProps,
614650
Pick<EuiDataGridProps, 'columnVisibility'> {
615651
renderContext: RenderContext<AC>;
652+
bulkAddToChatConfig?: BulkAddToChatConfig;
616653
additionalToolbarControls?: ReactNode;
617654
pageSizeOptions?: number[];
618655
leadingControlColumns?: EuiDataGridControlColumn[];
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
/** @type {import('eslint').Linter.Config} */
9+
module.exports = {
10+
overrides: [
11+
// This package is Node-only (Playwright eval suite). Allow Node.js builtins.
12+
{
13+
files: ['**/*.{js,mjs,ts,tsx}'],
14+
rules: {
15+
'import/no-nodejs-modules': 'off',
16+
// functional-tests packages intentionally import from test-helper packages (e.g. @kbn/evals, @kbn/scout)
17+
'@kbn/imports/no_boundary_crossing': 'off',
18+
},
19+
},
20+
],
21+
};

0 commit comments

Comments
 (0)