Skip to content

Commit afbcfc3

Browse files
[8.18] [Security GenAI] Add Telemetry related to the Attack Discovery Alert Filtering feature (#209623) (#210036)
# Backport This will backport the following commits from `main` to `8.18`: - [[Security GenAI] Add Telemetry related to the Attack Discovery Alert Filtering feature (#209623)](#209623) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Steph Milovic","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-02-06T15:11:33Z","message":"[Security GenAI] Add Telemetry related to the Attack Discovery Alert Filtering feature (#209623)","sha":"f299c9fdab11b37c882c6afbcaaa87916f71dbb6","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","Team:Security Generative AI","v8.18.0","v9.1.0","v8.19.0"],"title":"[Security GenAI] Add Telemetry related to the Attack Discovery Alert Filtering feature ","number":209623,"url":"https://github.com/elastic/kibana/pull/209623","mergeCommit":{"message":"[Security GenAI] Add Telemetry related to the Attack Discovery Alert Filtering feature (#209623)","sha":"f299c9fdab11b37c882c6afbcaaa87916f71dbb6"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18","8.x"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/209623","number":209623,"mergeCommit":{"message":"[Security GenAI] Add Telemetry related to the Attack Discovery Alert Filtering feature (#209623)","sha":"f299c9fdab11b37c882c6afbcaaa87916f71dbb6"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Steph Milovic <[email protected]>
1 parent d454b98 commit afbcfc3

File tree

5 files changed

+251
-3
lines changed

5 files changed

+251
-3
lines changed

x-pack/solutions/security/plugins/elastic_assistant/server/lib/telemetry/event_based_telemetry.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,11 @@ export const ATTACK_DISCOVERY_SUCCESS_EVENT: EventTypeOpts<{
241241
alertsContextCount: number;
242242
alertsCount: number;
243243
configuredAlertsCount: number;
244+
dateRangeDuration: number;
244245
discoveriesGenerated: number;
245246
durationMs: number;
247+
hasFilter: boolean;
248+
isDefaultDateRange: boolean;
246249
model?: string;
247250
provider?: string;
248251
}> = {
@@ -276,6 +279,13 @@ export const ATTACK_DISCOVERY_SUCCESS_EVENT: EventTypeOpts<{
276279
optional: false,
277280
},
278281
},
282+
dateRangeDuration: {
283+
type: 'integer',
284+
_meta: {
285+
description: 'Duration of time range of request in hours',
286+
optional: false,
287+
},
288+
},
279289
discoveriesGenerated: {
280290
type: 'integer',
281291
_meta: {
@@ -290,6 +300,20 @@ export const ATTACK_DISCOVERY_SUCCESS_EVENT: EventTypeOpts<{
290300
optional: false,
291301
},
292302
},
303+
hasFilter: {
304+
type: 'boolean',
305+
_meta: {
306+
description: 'Whether a filter was applied to the alerts used as context',
307+
optional: false,
308+
},
309+
},
310+
isDefaultDateRange: {
311+
type: 'boolean',
312+
_meta: {
313+
description: 'Whether the date range is the default of last 24 hours',
314+
optional: false,
315+
},
316+
},
293317
model: {
294318
type: 'keyword',
295319
_meta: {

x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.test.ts

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
*/
77

88
import { AuthenticatedUser } from '@kbn/core-security-common';
9-
10-
import { getAttackDiscoveryStats } from './helpers';
9+
import moment from 'moment/moment';
10+
import { getAttackDiscoveryStats, updateAttackDiscoveries } from './helpers';
1111
import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence';
1212
import { transformESSearchToAttackDiscovery } from '../../../lib/attack_discovery/persistence/transforms/transforms';
1313
import { getAttackDiscoverySearchEsMock } from '../../../__mocks__/attack_discovery_schema.mock';
14+
import { mockAnonymizedAlerts } from '../../../lib/attack_discovery/evaluation/__mocks__/mock_anonymized_alerts';
15+
import { mockAttackDiscoveries } from '../../../lib/attack_discovery/evaluation/__mocks__/mock_attack_discoveries';
16+
import { coreMock } from '@kbn/core/server/mocks';
17+
import { loggerMock } from '@kbn/logging-mocks';
1418

1519
jest.mock('lodash/fp', () => ({
1620
uniq: jest.fn((arr) => Array.from(new Set(arr))),
@@ -270,4 +274,169 @@ describe('helpers', () => {
270274
]);
271275
});
272276
});
277+
278+
describe('updateAttackDiscoveries', () => {
279+
const mockTelemetry = coreMock.createSetup().analytics;
280+
const mockLogger = loggerMock.create();
281+
const mockStartTime = moment('2024-03-28T22:27:28.000Z');
282+
const mockApiConfig = {
283+
actionTypeId: '.gen-ai',
284+
connectorId: 'my-gen-ai',
285+
model: 'gpt-4',
286+
};
287+
const mockReplacements = {};
288+
289+
beforeEach(() => {
290+
jest.clearAllMocks();
291+
});
292+
293+
it('should update attack discovery successfully', async () => {
294+
getAttackDiscovery.mockResolvedValue(mockCurrentAd);
295+
updateAttackDiscovery.mockResolvedValue({});
296+
297+
await updateAttackDiscoveries({
298+
anonymizedAlerts: mockAnonymizedAlerts,
299+
apiConfig: mockApiConfig,
300+
attackDiscoveries: mockAttackDiscoveries,
301+
attackDiscoveryId: 'attack-discovery-id',
302+
authenticatedUser: mockAuthenticatedUser,
303+
dataClient: mockDataClient,
304+
hasFilter: false,
305+
end: 'now',
306+
latestReplacements: mockReplacements,
307+
logger: mockLogger,
308+
size: 10,
309+
start: 'now-24h',
310+
startTime: mockStartTime,
311+
telemetry: mockTelemetry,
312+
});
313+
314+
expect(updateAttackDiscovery).toHaveBeenCalledWith({
315+
attackDiscoveryUpdateProps: expect.objectContaining({
316+
status: 'succeeded',
317+
}),
318+
authenticatedUser: mockAuthenticatedUser,
319+
});
320+
expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_success', {
321+
actionTypeId: '.gen-ai',
322+
alertsContextCount: 2,
323+
alertsCount: 8,
324+
configuredAlertsCount: 10,
325+
dateRangeDuration: 24,
326+
discoveriesGenerated: 1,
327+
durationMs: 0,
328+
hasFilter: false,
329+
isDefaultDateRange: true,
330+
model: 'gpt-4',
331+
});
332+
});
333+
it('should detect non-default time range', async () => {
334+
getAttackDiscovery.mockResolvedValue(mockCurrentAd);
335+
updateAttackDiscovery.mockResolvedValue({});
336+
337+
await updateAttackDiscoveries({
338+
anonymizedAlerts: mockAnonymizedAlerts,
339+
apiConfig: mockApiConfig,
340+
attackDiscoveries: mockAttackDiscoveries,
341+
attackDiscoveryId: 'attack-discovery-id',
342+
authenticatedUser: mockAuthenticatedUser,
343+
dataClient: mockDataClient,
344+
hasFilter: false,
345+
end: 'now',
346+
latestReplacements: mockReplacements,
347+
logger: mockLogger,
348+
size: 10,
349+
start: 'now-1w',
350+
startTime: mockStartTime,
351+
telemetry: mockTelemetry,
352+
});
353+
354+
expect(updateAttackDiscovery).toHaveBeenCalledWith({
355+
attackDiscoveryUpdateProps: expect.objectContaining({
356+
status: 'succeeded',
357+
}),
358+
authenticatedUser: mockAuthenticatedUser,
359+
});
360+
expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_success', {
361+
actionTypeId: '.gen-ai',
362+
alertsContextCount: 2,
363+
alertsCount: 8,
364+
configuredAlertsCount: 10,
365+
dateRangeDuration: 168,
366+
discoveriesGenerated: 1,
367+
durationMs: 0,
368+
hasFilter: false,
369+
isDefaultDateRange: false,
370+
model: 'gpt-4',
371+
});
372+
});
373+
it('hasFilter should be true when filter exists', async () => {
374+
getAttackDiscovery.mockResolvedValue(mockCurrentAd);
375+
updateAttackDiscovery.mockResolvedValue({});
376+
377+
await updateAttackDiscoveries({
378+
anonymizedAlerts: mockAnonymizedAlerts,
379+
apiConfig: mockApiConfig,
380+
attackDiscoveries: mockAttackDiscoveries,
381+
attackDiscoveryId: 'attack-discovery-id',
382+
authenticatedUser: mockAuthenticatedUser,
383+
dataClient: mockDataClient,
384+
hasFilter: true,
385+
end: 'now',
386+
latestReplacements: mockReplacements,
387+
logger: mockLogger,
388+
size: 10,
389+
start: 'now-24h',
390+
startTime: mockStartTime,
391+
telemetry: mockTelemetry,
392+
});
393+
394+
expect(updateAttackDiscovery).toHaveBeenCalledWith({
395+
attackDiscoveryUpdateProps: expect.objectContaining({
396+
status: 'succeeded',
397+
}),
398+
authenticatedUser: mockAuthenticatedUser,
399+
});
400+
expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_success', {
401+
actionTypeId: '.gen-ai',
402+
alertsContextCount: 2,
403+
alertsCount: 8,
404+
configuredAlertsCount: 10,
405+
dateRangeDuration: 24,
406+
discoveriesGenerated: 1,
407+
durationMs: 0,
408+
hasFilter: true,
409+
isDefaultDateRange: true,
410+
model: 'gpt-4',
411+
});
412+
});
413+
414+
it('should handle error during update', async () => {
415+
const mockError = new Error('Update failed');
416+
getAttackDiscovery.mockRejectedValue(mockError);
417+
418+
await updateAttackDiscoveries({
419+
anonymizedAlerts: mockAnonymizedAlerts,
420+
apiConfig: mockApiConfig,
421+
attackDiscoveries: mockAttackDiscoveries,
422+
attackDiscoveryId: 'attack-discovery-id',
423+
authenticatedUser: mockAuthenticatedUser,
424+
dataClient: mockDataClient,
425+
hasFilter: false,
426+
end: 'now',
427+
latestReplacements: mockReplacements,
428+
logger: mockLogger,
429+
size: 10,
430+
start: 'now-24h',
431+
startTime: mockStartTime,
432+
telemetry: mockTelemetry,
433+
});
434+
435+
expect(mockLogger.error).toHaveBeenCalledWith(mockError);
436+
expect(mockTelemetry.reportEvent).toHaveBeenCalledWith(
437+
'attack_discovery_error',
438+
expect.any(Object)
439+
);
440+
});
441+
});
273442
});

x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { transformError } from '@kbn/securitysolution-es-utils';
2121
import moment from 'moment/moment';
2222
import { uniq } from 'lodash/fp';
2323

24+
import dateMath from '@kbn/datemath';
2425
import {
2526
ATTACK_DISCOVERY_ERROR_EVENT,
2627
ATTACK_DISCOVERY_SUCCESS_EVENT,
@@ -135,9 +136,12 @@ export const updateAttackDiscoveries = async ({
135136
attackDiscoveryId,
136137
authenticatedUser,
137138
dataClient,
139+
hasFilter,
140+
end,
138141
latestReplacements,
139142
logger,
140143
size,
144+
start,
141145
startTime,
142146
telemetry,
143147
}: {
@@ -147,9 +151,14 @@ export const updateAttackDiscoveries = async ({
147151
attackDiscoveryId: string;
148152
authenticatedUser: AuthenticatedUser;
149153
dataClient: AttackDiscoveryDataClient;
154+
end?: string;
155+
hasFilter: boolean;
150156
latestReplacements: Replacements;
151157
logger: Logger;
152158
size: number;
159+
// start of attack discovery time range
160+
start?: string;
161+
// start time of attack discovery
153162
startTime: Moment;
154163
telemetry: AnalyticsServiceSetup;
155164
}) => {
@@ -185,6 +194,8 @@ export const updateAttackDiscoveries = async ({
185194
attackDiscoveryUpdateProps: updateProps,
186195
authenticatedUser,
187196
});
197+
const { dateRangeDuration, isDefaultDateRange } = getTimeRangeDuration({ start, end });
198+
188199
telemetry.reportEvent(ATTACK_DISCOVERY_SUCCESS_EVENT.eventType, {
189200
actionTypeId: apiConfig.actionTypeId,
190201
alertsContextCount: updateProps.alertsContextCount,
@@ -195,8 +206,11 @@ export const updateAttackDiscoveries = async ({
195206
)
196207
).length ?? 0,
197208
configuredAlertsCount: size,
209+
dateRangeDuration,
198210
discoveriesGenerated: updateProps.attackDiscoveries?.length ?? 0,
199211
durationMs,
212+
hasFilter,
213+
isDefaultDateRange,
200214
model: apiConfig.model,
201215
provider: apiConfig.provider,
202216
});
@@ -266,3 +280,40 @@ export const getAttackDiscoveryStats = async ({
266280
};
267281
});
268282
};
283+
284+
const getTimeRangeDuration = ({
285+
start,
286+
end,
287+
}: {
288+
start?: string;
289+
end?: string;
290+
}): {
291+
dateRangeDuration: number;
292+
isDefaultDateRange: boolean;
293+
} => {
294+
if (start && end) {
295+
const forceNow = moment().toDate();
296+
const dateStart = dateMath.parse(start, {
297+
roundUp: false,
298+
momentInstance: moment,
299+
forceNow,
300+
});
301+
const dateEnd = dateMath.parse(end, {
302+
roundUp: false,
303+
momentInstance: moment,
304+
forceNow,
305+
});
306+
if (dateStart && dateEnd) {
307+
const dateRangeDuration = moment.duration(dateEnd.diff(dateStart)).asHours();
308+
return {
309+
dateRangeDuration,
310+
isDefaultDateRange: end === 'now' && start === 'now-24h',
311+
};
312+
}
313+
}
314+
return {
315+
// start and/or end undefined, return 0 hours
316+
dateRangeDuration: 0,
317+
isDefaultDateRange: false,
318+
};
319+
};

x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,12 @@ export const postAttackDiscoveryRoute = (
157157
attackDiscoveryId,
158158
authenticatedUser,
159159
dataClient,
160+
hasFilter: !!(filter && Object.keys(filter).length),
161+
end,
160162
latestReplacements,
161163
logger,
162164
size,
165+
start,
163166
startTime,
164167
telemetry,
165168
})

x-pack/solutions/security/plugins/elastic_assistant/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"@kbn/llm-tasks-plugin",
5555
"@kbn/product-doc-base-plugin",
5656
"@kbn/core-saved-objects-api-server-mocks",
57-
"@kbn/security-ai-prompts"
57+
"@kbn/security-ai-prompts",
58+
"@kbn/datemath"
5859
],
5960
"exclude": [
6061
"target/**/*",

0 commit comments

Comments
 (0)