[Security GenAI] Add Telemetry related to the Attack Discovery Alert Filtering feature (#209623)

This commit is contained in:
Steph Milovic 2025-02-06 08:11:33 -07:00 committed by GitHub
parent ee866a745a
commit f299c9fdab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 251 additions and 3 deletions

View file

@ -241,8 +241,11 @@ export const ATTACK_DISCOVERY_SUCCESS_EVENT: EventTypeOpts<{
alertsContextCount: number;
alertsCount: number;
configuredAlertsCount: number;
dateRangeDuration: number;
discoveriesGenerated: number;
durationMs: number;
hasFilter: boolean;
isDefaultDateRange: boolean;
model?: string;
provider?: string;
}> = {
@ -276,6 +279,13 @@ export const ATTACK_DISCOVERY_SUCCESS_EVENT: EventTypeOpts<{
optional: false,
},
},
dateRangeDuration: {
type: 'integer',
_meta: {
description: 'Duration of time range of request in hours',
optional: false,
},
},
discoveriesGenerated: {
type: 'integer',
_meta: {
@ -290,6 +300,20 @@ export const ATTACK_DISCOVERY_SUCCESS_EVENT: EventTypeOpts<{
optional: false,
},
},
hasFilter: {
type: 'boolean',
_meta: {
description: 'Whether a filter was applied to the alerts used as context',
optional: false,
},
},
isDefaultDateRange: {
type: 'boolean',
_meta: {
description: 'Whether the date range is the default of last 24 hours',
optional: false,
},
},
model: {
type: 'keyword',
_meta: {

View file

@ -6,11 +6,15 @@
*/
import { AuthenticatedUser } from '@kbn/core-security-common';
import { getAttackDiscoveryStats } from './helpers';
import moment from 'moment/moment';
import { getAttackDiscoveryStats, updateAttackDiscoveries } from './helpers';
import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence';
import { transformESSearchToAttackDiscovery } from '../../../lib/attack_discovery/persistence/transforms/transforms';
import { getAttackDiscoverySearchEsMock } from '../../../__mocks__/attack_discovery_schema.mock';
import { mockAnonymizedAlerts } from '../../../lib/attack_discovery/evaluation/__mocks__/mock_anonymized_alerts';
import { mockAttackDiscoveries } from '../../../lib/attack_discovery/evaluation/__mocks__/mock_attack_discoveries';
import { coreMock } from '@kbn/core/server/mocks';
import { loggerMock } from '@kbn/logging-mocks';
jest.mock('lodash/fp', () => ({
uniq: jest.fn((arr) => Array.from(new Set(arr))),
@ -270,4 +274,169 @@ describe('helpers', () => {
]);
});
});
describe('updateAttackDiscoveries', () => {
const mockTelemetry = coreMock.createSetup().analytics;
const mockLogger = loggerMock.create();
const mockStartTime = moment('2024-03-28T22:27:28.000Z');
const mockApiConfig = {
actionTypeId: '.gen-ai',
connectorId: 'my-gen-ai',
model: 'gpt-4',
};
const mockReplacements = {};
beforeEach(() => {
jest.clearAllMocks();
});
it('should update attack discovery successfully', async () => {
getAttackDiscovery.mockResolvedValue(mockCurrentAd);
updateAttackDiscovery.mockResolvedValue({});
await updateAttackDiscoveries({
anonymizedAlerts: mockAnonymizedAlerts,
apiConfig: mockApiConfig,
attackDiscoveries: mockAttackDiscoveries,
attackDiscoveryId: 'attack-discovery-id',
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
hasFilter: false,
end: 'now',
latestReplacements: mockReplacements,
logger: mockLogger,
size: 10,
start: 'now-24h',
startTime: mockStartTime,
telemetry: mockTelemetry,
});
expect(updateAttackDiscovery).toHaveBeenCalledWith({
attackDiscoveryUpdateProps: expect.objectContaining({
status: 'succeeded',
}),
authenticatedUser: mockAuthenticatedUser,
});
expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_success', {
actionTypeId: '.gen-ai',
alertsContextCount: 2,
alertsCount: 8,
configuredAlertsCount: 10,
dateRangeDuration: 24,
discoveriesGenerated: 1,
durationMs: 0,
hasFilter: false,
isDefaultDateRange: true,
model: 'gpt-4',
});
});
it('should detect non-default time range', async () => {
getAttackDiscovery.mockResolvedValue(mockCurrentAd);
updateAttackDiscovery.mockResolvedValue({});
await updateAttackDiscoveries({
anonymizedAlerts: mockAnonymizedAlerts,
apiConfig: mockApiConfig,
attackDiscoveries: mockAttackDiscoveries,
attackDiscoveryId: 'attack-discovery-id',
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
hasFilter: false,
end: 'now',
latestReplacements: mockReplacements,
logger: mockLogger,
size: 10,
start: 'now-1w',
startTime: mockStartTime,
telemetry: mockTelemetry,
});
expect(updateAttackDiscovery).toHaveBeenCalledWith({
attackDiscoveryUpdateProps: expect.objectContaining({
status: 'succeeded',
}),
authenticatedUser: mockAuthenticatedUser,
});
expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_success', {
actionTypeId: '.gen-ai',
alertsContextCount: 2,
alertsCount: 8,
configuredAlertsCount: 10,
dateRangeDuration: 168,
discoveriesGenerated: 1,
durationMs: 0,
hasFilter: false,
isDefaultDateRange: false,
model: 'gpt-4',
});
});
it('hasFilter should be true when filter exists', async () => {
getAttackDiscovery.mockResolvedValue(mockCurrentAd);
updateAttackDiscovery.mockResolvedValue({});
await updateAttackDiscoveries({
anonymizedAlerts: mockAnonymizedAlerts,
apiConfig: mockApiConfig,
attackDiscoveries: mockAttackDiscoveries,
attackDiscoveryId: 'attack-discovery-id',
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
hasFilter: true,
end: 'now',
latestReplacements: mockReplacements,
logger: mockLogger,
size: 10,
start: 'now-24h',
startTime: mockStartTime,
telemetry: mockTelemetry,
});
expect(updateAttackDiscovery).toHaveBeenCalledWith({
attackDiscoveryUpdateProps: expect.objectContaining({
status: 'succeeded',
}),
authenticatedUser: mockAuthenticatedUser,
});
expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_success', {
actionTypeId: '.gen-ai',
alertsContextCount: 2,
alertsCount: 8,
configuredAlertsCount: 10,
dateRangeDuration: 24,
discoveriesGenerated: 1,
durationMs: 0,
hasFilter: true,
isDefaultDateRange: true,
model: 'gpt-4',
});
});
it('should handle error during update', async () => {
const mockError = new Error('Update failed');
getAttackDiscovery.mockRejectedValue(mockError);
await updateAttackDiscoveries({
anonymizedAlerts: mockAnonymizedAlerts,
apiConfig: mockApiConfig,
attackDiscoveries: mockAttackDiscoveries,
attackDiscoveryId: 'attack-discovery-id',
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
hasFilter: false,
end: 'now',
latestReplacements: mockReplacements,
logger: mockLogger,
size: 10,
start: 'now-24h',
startTime: mockStartTime,
telemetry: mockTelemetry,
});
expect(mockLogger.error).toHaveBeenCalledWith(mockError);
expect(mockTelemetry.reportEvent).toHaveBeenCalledWith(
'attack_discovery_error',
expect.any(Object)
);
});
});
});

View file

@ -21,6 +21,7 @@ import { transformError } from '@kbn/securitysolution-es-utils';
import moment from 'moment/moment';
import { uniq } from 'lodash/fp';
import dateMath from '@kbn/datemath';
import {
ATTACK_DISCOVERY_ERROR_EVENT,
ATTACK_DISCOVERY_SUCCESS_EVENT,
@ -135,9 +136,12 @@ export const updateAttackDiscoveries = async ({
attackDiscoveryId,
authenticatedUser,
dataClient,
hasFilter,
end,
latestReplacements,
logger,
size,
start,
startTime,
telemetry,
}: {
@ -147,9 +151,14 @@ export const updateAttackDiscoveries = async ({
attackDiscoveryId: string;
authenticatedUser: AuthenticatedUser;
dataClient: AttackDiscoveryDataClient;
end?: string;
hasFilter: boolean;
latestReplacements: Replacements;
logger: Logger;
size: number;
// start of attack discovery time range
start?: string;
// start time of attack discovery
startTime: Moment;
telemetry: AnalyticsServiceSetup;
}) => {
@ -185,6 +194,8 @@ export const updateAttackDiscoveries = async ({
attackDiscoveryUpdateProps: updateProps,
authenticatedUser,
});
const { dateRangeDuration, isDefaultDateRange } = getTimeRangeDuration({ start, end });
telemetry.reportEvent(ATTACK_DISCOVERY_SUCCESS_EVENT.eventType, {
actionTypeId: apiConfig.actionTypeId,
alertsContextCount: updateProps.alertsContextCount,
@ -195,8 +206,11 @@ export const updateAttackDiscoveries = async ({
)
).length ?? 0,
configuredAlertsCount: size,
dateRangeDuration,
discoveriesGenerated: updateProps.attackDiscoveries?.length ?? 0,
durationMs,
hasFilter,
isDefaultDateRange,
model: apiConfig.model,
provider: apiConfig.provider,
});
@ -266,3 +280,40 @@ export const getAttackDiscoveryStats = async ({
};
});
};
const getTimeRangeDuration = ({
start,
end,
}: {
start?: string;
end?: string;
}): {
dateRangeDuration: number;
isDefaultDateRange: boolean;
} => {
if (start && end) {
const forceNow = moment().toDate();
const dateStart = dateMath.parse(start, {
roundUp: false,
momentInstance: moment,
forceNow,
});
const dateEnd = dateMath.parse(end, {
roundUp: false,
momentInstance: moment,
forceNow,
});
if (dateStart && dateEnd) {
const dateRangeDuration = moment.duration(dateEnd.diff(dateStart)).asHours();
return {
dateRangeDuration,
isDefaultDateRange: end === 'now' && start === 'now-24h',
};
}
}
return {
// start and/or end undefined, return 0 hours
dateRangeDuration: 0,
isDefaultDateRange: false,
};
};

View file

@ -157,9 +157,12 @@ export const postAttackDiscoveryRoute = (
attackDiscoveryId,
authenticatedUser,
dataClient,
hasFilter: !!(filter && Object.keys(filter).length),
end,
latestReplacements,
logger,
size,
start,
startTime,
telemetry,
})

View file

@ -54,7 +54,8 @@
"@kbn/llm-tasks-plugin",
"@kbn/product-doc-base-plugin",
"@kbn/core-saved-objects-api-server-mocks",
"@kbn/security-ai-prompts"
"@kbn/security-ai-prompts",
"@kbn/datemath"
],
"exclude": [
"target/**/*",