Skip to content

Commit cd753de

Browse files
e40pudzacharyparikh
authored andcommitted
[Attack Discovery][Scheduling] System actions + Extended telemetries (elastic#221797)
## Summary Main ticket ([Internal link](elastic/security-team#12008)) ## Changes in this PR: 1. Fix elastic#221770 There was a bug in handling system actions which did not allow to create/update attack discovery schedules when adding one of the system actions - for example Cases. See screenshots and error description in the elastic#221770 To test, just create a new attack discovery schedule with the Cases action which should just work and new schedule should appear in the attack discovery schedules list. 2. Extended telemetries To improve telemetries for attack discovery schedules, we decided to add the information about the used actions within the schedule. For more details, see the [internal conversation](https://elastic.slack.com/archives/C08D26QFR18/p1747673752620379). Both the success and failure events will contain schedule info object of a next structure: ``` "scheduleInfo": { "id": "9a132e03-f9f2-48ff-8e4e-8182f470656f", "interval": "1m", "actions": [ ".email", ".slack", ".slack" ] }, ``` <img width="528" alt="Screenshot 2025-05-28 at 16 48 02" src="https://github.com/user-attachments/assets/7eab674d-2d81-48ef-93e1-99c14552eb57" /> To test: 1. Enable `telemetry.optIn: true` in `kibana.dev.yml` 2. Create AD schedule and make sure it generates some discoveries or fails during the rules execution (for example due to timeout) 3. Check reported events at https://p.elstc.co/paste/Vp6Buk8R#Z-F8UWse90Lldf2sMVQK+RPtJSiLJkc2eJGyGX85+b9 (might take couple hours to appear) ## NOTES The feature is hidden behind the feature flag (in `kibana.dev.yml`): ``` feature_flags.overrides: securitySolution.assistantAttackDiscoverySchedulingEnabled: true ```
1 parent af13362 commit cd753de

File tree

28 files changed

+435
-107
lines changed

28 files changed

+435
-107
lines changed

src/platform/packages/shared/kbn-rule-data-utils/src/alerts_as_data_rbac.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ describe('alertsAsDataRbac', () => {
1313
describe('isSiemRuleType', () => {
1414
test('returns true for siem rule types', () => {
1515
expect(isSiemRuleType('siem.esqlRuleType')).toBe(true);
16+
expect(isSiemRuleType('attack-discovery')).toBe(true);
1617
});
1718

1819
test('returns false for NON siem rule types', () => {
1920
expect(isSiemRuleType('apm.anomaly')).toBe(false);
21+
expect(isSiemRuleType('attack-discovery.test')).toBe(false);
2022
});
2123
});
2224
});

src/platform/packages/shared/kbn-rule-data-utils/src/alerts_as_data_rbac.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,5 @@ export const getEsQueryConfig = (params?: GetEsQueryConfigParamType): EsQueryCon
101101
* TODO: Remove when checks for specific rule type ids is not needed
102102
*in the codebase.
103103
*/
104-
export const isSiemRuleType = (ruleTypeId: string) => ruleTypeId.startsWith('siem.');
104+
export const isSiemRuleType = (ruleTypeId: string) =>
105+
ruleTypeId.startsWith('siem.') || ruleTypeId === 'attack-discovery';

x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/schedules.gen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export const AttackDiscoveryScheduleAction = z.object({
140140
* The action type used for sending notifications.
141141
*/
142142
actionTypeId: z.string(),
143-
group: AttackDiscoveryScheduleActionGroup,
143+
group: AttackDiscoveryScheduleActionGroup.optional(),
144144
id: AttackDiscoveryScheduleActionId,
145145
params: AttackDiscoveryScheduleActionParams,
146146
uuid: NonEmptyString.optional(),

x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/attack_discovery/schedules.schema.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,6 @@ components:
193193
required:
194194
- actionTypeId
195195
- id
196-
- group
197196
- params
198197

199198
AttackDiscoveryScheduleExecutionStatus:

x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/attack_discovery_schedules.mock.ts

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,19 @@
55
* 2.0.
66
*/
77

8-
import { CreateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/create';
9-
import { UpdateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/update';
108
import {
11-
ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
129
AttackDiscoverySchedule,
10+
AttackDiscoveryScheduleAction,
1311
AttackDiscoveryScheduleCreateProps,
1412
AttackDiscoveryScheduleParams,
13+
AttackDiscoveryScheduleUpdateProps,
1514
} from '@kbn/elastic-assistant-common';
1615

1716
import { SanitizedRule, SanitizedRuleAction } from '@kbn/alerting-types';
1817

1918
export const getAttackDiscoveryCreateScheduleMock = (
2019
enabled = true
21-
): CreateRuleData<AttackDiscoveryScheduleParams> => {
20+
): AttackDiscoveryScheduleCreateProps => {
2221
return {
2322
name: 'Test Schedule 1',
2423
schedule: {
@@ -35,20 +34,17 @@ export const getAttackDiscoveryCreateScheduleMock = (
3534
size: 100,
3635
start: 'now-24h',
3736
},
38-
actions: [],
39-
alertTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
40-
consumer: 'siem',
4137
enabled,
42-
tags: [],
4338
};
4439
};
4540

4641
export const getAttackDiscoveryUpdateScheduleMock = (
4742
id: string,
48-
overrides: Partial<CreateRuleData<AttackDiscoveryScheduleParams>>
49-
): UpdateRuleData<AttackDiscoveryScheduleParams> & { id: string } => {
43+
overrides: Partial<AttackDiscoveryScheduleUpdateProps>
44+
): AttackDiscoveryScheduleUpdateProps & { id: string } => {
5045
return {
5146
id,
47+
actions: [],
5248
...getAttackDiscoveryCreateScheduleMock(),
5349
...overrides,
5450
};
@@ -119,13 +115,55 @@ export const getAttackDiscoveryScheduleMock = (
119115
};
120116
};
121117

122-
export const getInternalFindAttackDiscoverySchedulesMock = (
123-
schedules: Array<SanitizedRule<AttackDiscoveryScheduleParams>>
124-
) => {
118+
export const getFindAttackDiscoverySchedulesMock = (schedules: AttackDiscoverySchedule[]) => {
125119
return {
126-
page: 1,
127-
perPage: 20,
128120
total: schedules.length,
129121
data: schedules,
130122
};
131123
};
124+
125+
export const getScheduleActions = (): AttackDiscoveryScheduleAction[] => {
126+
return [
127+
{
128+
id: 'ab81485e-3685-4215-9804-7693d0271d1b',
129+
actionTypeId: '.email',
130+
group: 'default',
131+
params: {
132+
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
133+
134+
subject: 'Hello there',
135+
},
136+
frequency: {
137+
summary: true,
138+
notifyWhen: 'onActiveAlert',
139+
throttle: null,
140+
},
141+
},
142+
{
143+
id: 'a6c9e92a-b701-41f3-9e26-aca7563e6908',
144+
actionTypeId: '.slack',
145+
group: 'default',
146+
params: {
147+
message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts',
148+
},
149+
frequency: {
150+
summary: true,
151+
notifyWhen: 'onActiveAlert',
152+
throttle: null,
153+
},
154+
},
155+
{
156+
id: '74ad56aa-cc3d-45a4-a944-654b625ed054',
157+
actionTypeId: '.cases',
158+
params: {
159+
subAction: 'run',
160+
subActionParams: {
161+
timeWindow: '5m',
162+
reopenClosedCases: false,
163+
groupingBy: ['kibana.alert.attack_discovery.alert_ids'],
164+
templateId: null,
165+
},
166+
},
167+
},
168+
];
169+
};

x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_service/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,8 @@ export class AIAssistantService {
667667
opts: CreateAttackDiscoveryScheduleDataClientParams
668668
): Promise<AttackDiscoveryScheduleDataClient | null> {
669669
return new AttackDiscoveryScheduleDataClient({
670+
actionsClient: opts.actionsClient,
671+
logger: opts.logger,
670672
rulesClient: opts.rulesClient,
671673
});
672674
}

x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/data_client/index.test.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,67 @@
66
*/
77

88
import { rulesClientMock } from '@kbn/alerting-plugin/server/rules_client.mock';
9+
import { actionsClientMock } from '@kbn/actions-plugin/server/mocks';
10+
import { loggerMock } from '@kbn/logging-mocks';
11+
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
912

1013
import { AttackDiscoveryScheduleDataClient, AttackDiscoveryScheduleDataClientParams } from '.';
1114
import {
1215
getAttackDiscoveryCreateScheduleMock,
1316
getAttackDiscoveryUpdateScheduleMock,
17+
getInternalAttackDiscoveryScheduleMock,
1418
} from '../../../../__mocks__/attack_discovery_schedules.mock';
19+
import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common';
20+
21+
const mockApiConfig = {
22+
connectorId: 'connector-id',
23+
actionTypeId: '.bedrock',
24+
model: 'model',
25+
name: 'Test Bedrock',
26+
provider: OpenAiProviderType.OpenAi,
27+
};
28+
const mockBasicScheduleParams = {
29+
name: 'Test Schedule 1',
30+
schedule: {
31+
interval: '10m',
32+
},
33+
params: {
34+
alertsIndexPattern: '.alerts-security.alerts-default',
35+
apiConfig: mockApiConfig,
36+
end: 'now',
37+
size: 25,
38+
start: 'now-24h',
39+
},
40+
enabled: true,
41+
};
42+
const mockInternalAttackDiscovery = getInternalAttackDiscoveryScheduleMock(
43+
getInternalAttackDiscoveryScheduleMock(mockBasicScheduleParams)
44+
);
1545

1646
describe('AttackDiscoveryScheduleDataClient', () => {
1747
let scheduleDataClientParams: AttackDiscoveryScheduleDataClientParams;
1848

1949
beforeEach(() => {
2050
jest.clearAllMocks();
2151
scheduleDataClientParams = {
52+
actionsClient: actionsClientMock.create(),
53+
logger: loggerMock.create(),
2254
rulesClient: rulesClientMock.create(),
2355
};
56+
57+
(scheduleDataClientParams.rulesClient.find as jest.Mock).mockResolvedValue({
58+
total: 1,
59+
data: [mockInternalAttackDiscovery],
60+
});
61+
(scheduleDataClientParams.rulesClient.get as jest.Mock).mockResolvedValue(
62+
mockInternalAttackDiscovery
63+
);
64+
(scheduleDataClientParams.rulesClient.create as jest.Mock).mockResolvedValue(
65+
mockInternalAttackDiscovery
66+
);
67+
(scheduleDataClientParams.rulesClient.update as jest.Mock).mockResolvedValue(
68+
mockInternalAttackDiscovery
69+
);
2470
});
2571

2672
describe('findSchedules', () => {
@@ -106,7 +152,13 @@ describe('AttackDiscoveryScheduleDataClient', () => {
106152
await scheduleDataClient.createSchedule(scheduleCreateData);
107153

108154
expect(scheduleDataClientParams.rulesClient.create).toHaveBeenCalledWith({
109-
data: scheduleCreateData,
155+
data: {
156+
actions: [],
157+
alertTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
158+
consumer: 'siem',
159+
tags: [],
160+
...scheduleCreateData,
161+
},
110162
});
111163
});
112164
});
@@ -123,7 +175,12 @@ describe('AttackDiscoveryScheduleDataClient', () => {
123175

124176
expect(scheduleDataClientParams.rulesClient.update).toHaveBeenCalledWith({
125177
id: scheduleId,
126-
data: { ...getAttackDiscoveryCreateScheduleMock(), name: 'Updated schedule 5' },
178+
data: {
179+
actions: [],
180+
tags: [],
181+
...getAttackDiscoveryCreateScheduleMock(),
182+
name: 'Updated schedule 5',
183+
},
127184
});
128185
});
129186
});

x-pack/solutions/security/plugins/elastic_assistant/server/lib/attack_discovery/schedules/data_client/index.ts

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,33 @@
55
* 2.0.
66
*/
77

8+
import { ActionsClient } from '@kbn/actions-plugin/server';
89
import { RulesClient } from '@kbn/alerting-plugin/server';
9-
import { CreateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/create';
10-
import { UpdateRuleData } from '@kbn/alerting-plugin/server/application/rule/methods/update';
10+
import { Logger } from '@kbn/core/server';
1111
import {
1212
ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
13+
AttackDiscoverySchedule,
14+
AttackDiscoveryScheduleCreateProps,
1315
AttackDiscoveryScheduleParams,
16+
AttackDiscoveryScheduleUpdateProps,
1417
} from '@kbn/elastic-assistant-common';
18+
import { convertAlertingRuleToSchedule } from '../../../../routes/attack_discovery/schedules/utils/convert_alerting_rule_to_schedule';
1519
import { AttackDiscoveryScheduleFindOptions } from '../types';
20+
import { convertScheduleActionsToAlertingActions } from './utils/transform_actions';
1621

1722
/**
1823
* Params for when creating AttackDiscoveryScheduleDataClient in Request Context Factory. Useful if needing to modify
1924
* configuration after initial plugin start
2025
*/
2126
export interface CreateAttackDiscoveryScheduleDataClientParams {
27+
actionsClient: ActionsClient;
28+
logger: Logger;
2229
rulesClient: RulesClient;
2330
}
2431

2532
export interface AttackDiscoveryScheduleDataClientParams {
33+
actionsClient: ActionsClient;
34+
logger: Logger;
2635
rulesClient: RulesClient;
2736
}
2837

@@ -35,7 +44,7 @@ export class AttackDiscoveryScheduleDataClient {
3544
sort: sortParam = {},
3645
}: AttackDiscoveryScheduleFindOptions = {}) => {
3746
// TODO: add filtering
38-
const rules = await this.options.rulesClient.find<AttackDiscoveryScheduleParams>({
47+
const results = await this.options.rulesClient.find<AttackDiscoveryScheduleParams>({
3948
options: {
4049
page: page + 1,
4150
perPage,
@@ -44,30 +53,63 @@ export class AttackDiscoveryScheduleDataClient {
4453
ruleTypeIds: [ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID],
4554
},
4655
});
47-
return rules;
56+
57+
const { total, data } = results;
58+
const schedules = data.map(convertAlertingRuleToSchedule);
59+
60+
return { total, data: schedules };
4861
};
4962

50-
public getSchedule = async (id: string) => {
63+
public getSchedule = async (id: string): Promise<AttackDiscoverySchedule> => {
5164
const rule = await this.options.rulesClient.get<AttackDiscoveryScheduleParams>({ id });
52-
return rule;
65+
const schedule = convertAlertingRuleToSchedule(rule);
66+
return schedule;
5367
};
5468

55-
public createSchedule = async (ruleToCreate: CreateRuleData<AttackDiscoveryScheduleParams>) => {
69+
public createSchedule = async (
70+
ruleToCreate: AttackDiscoveryScheduleCreateProps
71+
): Promise<AttackDiscoverySchedule> => {
72+
const { enabled = false, actions: _, ...restScheduleAttributes } = ruleToCreate;
73+
const { actions, systemActions } = convertScheduleActionsToAlertingActions({
74+
actionsClient: this.options.actionsClient,
75+
logger: this.options.logger,
76+
scheduleActions: ruleToCreate.actions,
77+
});
5678
const rule = await this.options.rulesClient.create<AttackDiscoveryScheduleParams>({
57-
data: ruleToCreate,
79+
data: {
80+
actions,
81+
...(systemActions.length ? { systemActions } : {}),
82+
alertTypeId: ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID,
83+
consumer: 'siem',
84+
enabled,
85+
tags: [],
86+
...restScheduleAttributes,
87+
},
5888
});
59-
return rule;
89+
const schedule = convertAlertingRuleToSchedule(rule);
90+
return schedule;
6091
};
6192

6293
public updateSchedule = async (
63-
ruleToUpdate: UpdateRuleData<AttackDiscoveryScheduleParams> & { id: string }
64-
) => {
65-
const { id, ...updatePayload } = ruleToUpdate;
94+
ruleToUpdate: AttackDiscoveryScheduleUpdateProps & { id: string }
95+
): Promise<AttackDiscoverySchedule> => {
96+
const { id, actions: _, ...updatePayload } = ruleToUpdate;
97+
const { actions, systemActions } = convertScheduleActionsToAlertingActions({
98+
actionsClient: this.options.actionsClient,
99+
logger: this.options.logger,
100+
scheduleActions: ruleToUpdate.actions,
101+
});
66102
const rule = await this.options.rulesClient.update<AttackDiscoveryScheduleParams>({
67103
id,
68-
data: updatePayload,
104+
data: {
105+
actions,
106+
...(systemActions.length ? { systemActions } : {}),
107+
tags: [],
108+
...updatePayload,
109+
},
69110
});
70-
return rule;
111+
const schedule = convertAlertingRuleToSchedule(rule);
112+
return schedule;
71113
};
72114

73115
public deleteSchedule = async (ruleToDelete: { id: string }) => {

0 commit comments

Comments
 (0)