mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security GenAI] Add Telemetry related to the Attack Discovery Alert Filtering feature (#209623)
This commit is contained in:
parent
ee866a745a
commit
f299c9fdab
5 changed files with 251 additions and 3 deletions
|
@ -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: {
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -157,9 +157,12 @@ export const postAttackDiscoveryRoute = (
|
|||
attackDiscoveryId,
|
||||
authenticatedUser,
|
||||
dataClient,
|
||||
hasFilter: !!(filter && Object.keys(filter).length),
|
||||
end,
|
||||
latestReplacements,
|
||||
logger,
|
||||
size,
|
||||
start,
|
||||
startTime,
|
||||
telemetry,
|
||||
})
|
||||
|
|
|
@ -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/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue