[8.x] [Attack Discovery][Scheduling] Decouple AD generation logic from the postAttackDiscoveryRoute (#12036) (#216274) (#216625)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Attack Discovery][Scheduling] Decouple AD generation logic from the
`postAttackDiscoveryRoute` (#12036)
(#216274)](https://github.com/elastic/kibana/pull/216274)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Ievgen
Sorokopud","email":"ievgen.sorokopud@elastic.co"},"sourceCommit":{"committedDate":"2025-04-01T11:00:41Z","message":"[Attack
Discovery][Scheduling] Decouple AD generation logic from the
`postAttackDiscoveryRoute` (#12036) (#216274)\n\n## Summary\n\nThese
changes decouple the core attack discovery generation\nfunctionality
from the `POST\n/internal/elastic_assistant/attack_discovery`
route.\n\nThis will allow us to use this functionality within the
upcoming attack\ndiscovery schedule execution handler
([internal\nlink](https://github.com/elastic/security-team/issues/12004)).\n\nThere
are no changes in the business logic of the attack discovery\ngeneration
process and everything should continue working as
before.","sha":"437065d9c81181f740033ad5db7bfab4ce6e679f","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:
SecuritySolution","Team:Security Generative
AI","backport:version","v9.1.0","v8.19.0"],"title":"[Attack
Discovery][Scheduling] Decouple AD generation logic from the
`postAttackDiscoveryRoute`
(#12036)","number":216274,"url":"https://github.com/elastic/kibana/pull/216274","mergeCommit":{"message":"[Attack
Discovery][Scheduling] Decouple AD generation logic from the
`postAttackDiscoveryRoute` (#12036) (#216274)\n\n## Summary\n\nThese
changes decouple the core attack discovery generation\nfunctionality
from the `POST\n/internal/elastic_assistant/attack_discovery`
route.\n\nThis will allow us to use this functionality within the
upcoming attack\ndiscovery schedule execution handler
([internal\nlink](https://github.com/elastic/security-team/issues/12004)).\n\nThere
are no changes in the business logic of the attack discovery\ngeneration
process and everything should continue working as
before.","sha":"437065d9c81181f740033ad5db7bfab4ce6e679f"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/216274","number":216274,"mergeCommit":{"message":"[Attack
Discovery][Scheduling] Decouple AD generation logic from the
`postAttackDiscoveryRoute` (#12036) (#216274)\n\n## Summary\n\nThese
changes decouple the core attack discovery generation\nfunctionality
from the `POST\n/internal/elastic_assistant/attack_discovery`
route.\n\nThis will allow us to use this functionality within the
upcoming attack\ndiscovery schedule execution handler
([internal\nlink](https://github.com/elastic/security-team/issues/12004)).\n\nThere
are no changes in the business logic of the attack discovery\ngeneration
process and everything should continue working as
before.","sha":"437065d9c81181f740033ad5db7bfab4ce6e679f"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Ievgen Sorokopud <ievgen.sorokopud@elastic.co>
This commit is contained in:
Kibana Machine 2025-04-01 15:06:34 +02:00 committed by GitHub
parent 3a1f8398fb
commit 009adee75e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 573 additions and 144 deletions

View file

@ -18,6 +18,7 @@ import { z } from '@kbn/zod';
import { NonEmptyString, User } from '../common_attributes.gen';
import { Replacements, ApiConfig } from '../conversations/common_attributes.gen';
import { AnonymizationFieldResponse } from '../anonymization_fields/bulk_crud_anonymization_fields_route.gen';
/**
* An attack discovery generated from one or more alerts
@ -247,3 +248,22 @@ export const AttackDiscoveryCreateProps = z.object({
apiConfig: ApiConfig,
replacements: Replacements.optional(),
});
export type AttackDiscoveryGenerationConfig = z.infer<typeof AttackDiscoveryGenerationConfig>;
export const AttackDiscoveryGenerationConfig = z.object({
alertsIndexPattern: z.string(),
anonymizationFields: z.array(AnonymizationFieldResponse),
/**
* LLM API configuration.
*/
apiConfig: ApiConfig,
end: z.string().optional(),
filter: z.object({}).catchall(z.unknown()).optional(),
langSmithProject: z.string().optional(),
langSmithApiKey: z.string().optional(),
model: z.string().optional(),
replacements: Replacements.optional(),
size: z.number(),
start: z.string().optional(),
subAction: z.enum(['invokeAI', 'invokeStream']),
});

View file

@ -114,7 +114,6 @@ components:
items:
$ref: '#/components/schemas/AttackDiscoveryStat'
AttackDiscoveryResponse:
type: object
required:
@ -142,8 +141,8 @@ components:
description: The last time attack discovery was viewed in the browser.
type: string
alertsContextCount:
type: integer
description: The number of alerts in the context.
type: integer
description: The number of alerts in the context.
createdAt:
description: The time attack discovery was created.
type: string
@ -242,3 +241,43 @@ components:
replacements:
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
AttackDiscoveryGenerationConfig:
type: object
required:
- apiConfig
- alertsIndexPattern
- anonymizationFields
- size
- subAction
properties:
alertsIndexPattern:
type: string
anonymizationFields:
items:
$ref: '../anonymization_fields/bulk_crud_anonymization_fields_route.schema.yaml#/components/schemas/AnonymizationFieldResponse'
type: array
apiConfig:
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/ApiConfig'
description: LLM API configuration.
end:
type: string
filter:
type: object
additionalProperties: true
langSmithProject:
type: string
langSmithApiKey:
type: string
model:
type: string
replacements:
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
size:
type: number
start:
type: string
subAction:
type: string
enum:
- invokeAI
- invokeStream

View file

@ -16,28 +16,10 @@
import { z } from '@kbn/zod';
import { AnonymizationFieldResponse } from '../anonymization_fields/bulk_crud_anonymization_fields_route.gen';
import { ApiConfig, Replacements } from '../conversations/common_attributes.gen';
import { AttackDiscoveryResponse } from './common_attributes.gen';
import { AttackDiscoveryGenerationConfig, AttackDiscoveryResponse } from './common_attributes.gen';
export type AttackDiscoveryPostRequestBody = z.infer<typeof AttackDiscoveryPostRequestBody>;
export const AttackDiscoveryPostRequestBody = z.object({
alertsIndexPattern: z.string(),
anonymizationFields: z.array(AnonymizationFieldResponse),
/**
* LLM API configuration.
*/
apiConfig: ApiConfig,
end: z.string().optional(),
filter: z.object({}).catchall(z.unknown()).optional(),
langSmithProject: z.string().optional(),
langSmithApiKey: z.string().optional(),
model: z.string().optional(),
replacements: Replacements.optional(),
size: z.number(),
start: z.string().optional(),
subAction: z.enum(['invokeAI', 'invokeStream']),
});
export const AttackDiscoveryPostRequestBody = AttackDiscoveryGenerationConfig;
export type AttackDiscoveryPostRequestBodyInput = z.input<typeof AttackDiscoveryPostRequestBody>;
export type AttackDiscoveryPostResponse = z.infer<typeof AttackDiscoveryPostResponse>;

View file

@ -21,45 +21,7 @@ paths:
content:
application/json:
schema:
type: object
required:
- apiConfig
- alertsIndexPattern
- anonymizationFields
- size
- subAction
properties:
alertsIndexPattern:
type: string
anonymizationFields:
items:
$ref: '../anonymization_fields/bulk_crud_anonymization_fields_route.schema.yaml#/components/schemas/AnonymizationFieldResponse'
type: array
apiConfig:
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/ApiConfig'
description: LLM API configuration.
end:
type: string
filter:
type: object
additionalProperties: true
langSmithProject:
type: string
langSmithApiKey:
type: string
model:
type: string
replacements:
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacements'
size:
type: number
start:
type: string
subAction:
type: string
enum:
- invokeAI
- invokeStream
$ref: './common_attributes.schema.yaml#/components/schemas/AttackDiscoveryGenerationConfig'
responses:
'200':
description: Successful response
@ -80,4 +42,3 @@ paths:
type: string
message:
type: string

View file

@ -0,0 +1,360 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AuthenticatedUser } from '@kbn/core-security-common';
import { coreMock, elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
import { loggerMock } from '@kbn/logging-mocks';
import { actionsClientMock } from '@kbn/actions-plugin/server/mocks';
import { AttackDiscoveryGenerationConfig } from '@kbn/elastic-assistant-common';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
import { generateAttackDiscoveries } from './generate_discoveries';
import { updateAttackDiscoveries } from './helpers';
import { handleGraphError } from '../post/helpers/handle_graph_error';
import { invokeAttackDiscoveryGraph } from '../post/helpers/invoke_attack_discovery_graph';
import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence';
import { mockAnonymizedAlerts } from '../../../lib/attack_discovery/evaluation/__mocks__/mock_anonymized_alerts';
import { mockAttackDiscoveries } from '../../../lib/attack_discovery/evaluation/__mocks__/mock_attack_discoveries';
jest.mock('./helpers', () => ({
...jest.requireActual('./helpers'),
updateAttackDiscoveries: jest.fn(),
}));
jest.mock('../post/helpers/handle_graph_error', () => ({
...jest.requireActual('../post/helpers/handle_graph_error'),
handleGraphError: jest.fn(),
}));
jest.mock('../post/helpers/invoke_attack_discovery_graph', () => ({
...jest.requireActual('../post/helpers/invoke_attack_discovery_graph'),
invokeAttackDiscoveryGraph: jest.fn(),
}));
const findAttackDiscoveryByConnectorId = jest.fn();
const updateAttackDiscovery = jest.fn();
const createAttackDiscovery = jest.fn();
const getAttackDiscovery = jest.fn();
const findAllAttackDiscoveries = jest.fn();
const mockDataClient = {
findAttackDiscoveryByConnectorId,
updateAttackDiscovery,
createAttackDiscovery,
getAttackDiscovery,
findAllAttackDiscoveries,
} as unknown as AttackDiscoveryDataClient;
const mockActionsClient = actionsClientMock.create();
const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
const mockLogger = loggerMock.create();
const mockSavedObjectsClient = savedObjectsClientMock.create();
const mockTelemetry = coreMock.createSetup().analytics;
const mockAuthenticatedUser = {
username: 'user',
profile_uid: '1234',
authentication_realm: {
type: 'my_realm_type',
name: 'my_realm_name',
},
} as AuthenticatedUser;
const mockApiConfig = {
connectorId: 'connector-id',
actionTypeId: '.bedrock',
model: 'model',
provider: OpenAiProviderType.OpenAi,
};
const mockConfig: AttackDiscoveryGenerationConfig = {
subAction: 'invokeAI',
apiConfig: mockApiConfig,
alertsIndexPattern: 'alerts-*',
anonymizationFields: [],
replacements: {},
model: 'gpt-4',
size: 20,
langSmithProject: 'langSmithProject',
langSmithApiKey: 'langSmithApiKey',
};
describe('generateAttackDiscoveries', () => {
const testInvokeError = new Error('Failed to invoke AD graph.');
const testUpdateError = new Error('Failed to update attack discoveries.');
beforeEach(() => {
jest.clearAllMocks();
(invokeAttackDiscoveryGraph as jest.Mock).mockResolvedValue({
anonymizedAlerts: mockAnonymizedAlerts,
attackDiscoveries: mockAttackDiscoveries,
});
});
describe('when passed valid arguments', () => {
it('should call `invokeAttackDiscoveryGraph`', async () => {
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(invokeAttackDiscoveryGraph).toHaveBeenCalledWith(
expect.objectContaining({
actionsClient: mockActionsClient,
alertsIndexPattern: mockConfig.alertsIndexPattern,
anonymizationFields: mockConfig.anonymizationFields,
apiConfig: mockConfig.apiConfig,
connectorTimeout: 580000,
end: mockConfig.end,
esClient: mockEsClient,
filter: mockConfig.filter,
langSmithProject: mockConfig.langSmithProject,
langSmithApiKey: mockConfig.langSmithApiKey,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
size: mockConfig.size,
start: mockConfig.start,
})
);
});
it('should call `updateAttackDiscoveries`', async () => {
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(updateAttackDiscoveries).toHaveBeenCalledWith(
expect.objectContaining({
anonymizedAlerts: mockAnonymizedAlerts,
apiConfig: mockConfig.apiConfig,
attackDiscoveries: mockAttackDiscoveries,
executionUuid,
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
hasFilter: false,
end: mockConfig.end,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
size: mockConfig.size,
start: mockConfig.start,
telemetry: mockTelemetry,
})
);
});
it('should not call `handleGraphError`', async () => {
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(handleGraphError).not.toBeCalled();
});
it('should return valid results', async () => {
const executionUuid = 'test-1';
const results = await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(results).toEqual({
anonymizedAlerts: mockAnonymizedAlerts,
attackDiscoveries: mockAttackDiscoveries,
replacements: mockConfig.replacements,
});
});
});
describe('when `invokeAttackDiscoveryGraph` throws an error', () => {
it('should call `handleGraphError`', async () => {
(invokeAttackDiscoveryGraph as jest.Mock).mockRejectedValue(testInvokeError);
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(handleGraphError).toHaveBeenCalledWith(
expect.objectContaining({
apiConfig: mockConfig.apiConfig,
executionUuid,
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
err: testInvokeError,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
telemetry: mockTelemetry,
})
);
});
it('should not call `updateAttackDiscoveries`', async () => {
(invokeAttackDiscoveryGraph as jest.Mock).mockRejectedValue(testInvokeError);
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(updateAttackDiscoveries).not.toBeCalled();
});
it('should return an error', async () => {
(invokeAttackDiscoveryGraph as jest.Mock).mockRejectedValue(testInvokeError);
const executionUuid = 'test-1';
const results = await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(results).toEqual({ error: testInvokeError });
});
});
describe('when `updateAttackDiscoveries` throws an error', () => {
it('should call `invokeAttackDiscoveryGraph`', async () => {
(updateAttackDiscoveries as jest.Mock).mockRejectedValue(testUpdateError);
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(invokeAttackDiscoveryGraph).toHaveBeenCalledWith(
expect.objectContaining({
actionsClient: mockActionsClient,
alertsIndexPattern: mockConfig.alertsIndexPattern,
anonymizationFields: mockConfig.anonymizationFields,
apiConfig: mockConfig.apiConfig,
connectorTimeout: 580000,
end: mockConfig.end,
esClient: mockEsClient,
filter: mockConfig.filter,
langSmithProject: mockConfig.langSmithProject,
langSmithApiKey: mockConfig.langSmithApiKey,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
size: mockConfig.size,
start: mockConfig.start,
})
);
});
it('should call `handleGraphError`', async () => {
(updateAttackDiscoveries as jest.Mock).mockRejectedValue(testUpdateError);
const executionUuid = 'test-1';
await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(handleGraphError).toHaveBeenCalledWith(
expect.objectContaining({
apiConfig: mockConfig.apiConfig,
executionUuid,
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
err: testUpdateError,
latestReplacements: mockConfig.replacements,
logger: mockLogger,
telemetry: mockTelemetry,
})
);
});
it('should return an error', async () => {
(updateAttackDiscoveries as jest.Mock).mockRejectedValue(testUpdateError);
const executionUuid = 'test-1';
const results = await generateAttackDiscoveries({
actionsClient: mockActionsClient,
authenticatedUser: mockAuthenticatedUser,
config: mockConfig,
dataClient: mockDataClient,
esClient: mockEsClient,
executionUuid,
logger: mockLogger,
savedObjectsClient: mockSavedObjectsClient,
telemetry: mockTelemetry,
});
expect(results).toEqual({ error: testUpdateError });
});
});
});

View file

@ -0,0 +1,126 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment';
import {
AnalyticsServiceSetup,
AuthenticatedUser,
Logger,
SavedObjectsClientContract,
} from '@kbn/core/server';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { AttackDiscoveryGenerationConfig, Replacements } from '@kbn/elastic-assistant-common';
import { PublicMethodsOf } from '@kbn/utility-types';
import { ActionsClient } from '@kbn/actions-plugin/server';
import { updateAttackDiscoveries } from './helpers';
import { handleGraphError } from '../post/helpers/handle_graph_error';
import { invokeAttackDiscoveryGraph } from '../post/helpers/invoke_attack_discovery_graph';
import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence';
const ROUTE_HANDLER_TIMEOUT = 10 * 60 * 1000; // 10 * 60 seconds = 10 minutes
const LANG_CHAIN_TIMEOUT = ROUTE_HANDLER_TIMEOUT - 10_000; // 9 minutes 50 seconds
const CONNECTOR_TIMEOUT = LANG_CHAIN_TIMEOUT - 10_000; // 9 minutes 40 seconds
export interface GenerateAttackDiscoveriesParams {
actionsClient: PublicMethodsOf<ActionsClient>;
authenticatedUser: AuthenticatedUser;
config: AttackDiscoveryGenerationConfig;
dataClient: AttackDiscoveryDataClient;
esClient: ElasticsearchClient;
executionUuid: string;
logger: Logger;
savedObjectsClient: SavedObjectsClientContract;
telemetry: AnalyticsServiceSetup;
}
export const generateAttackDiscoveries = async ({
actionsClient,
authenticatedUser,
config,
dataClient,
esClient,
executionUuid,
logger,
savedObjectsClient,
telemetry,
}: GenerateAttackDiscoveriesParams) => {
const startTime = moment(); // start timing the generation
// get parameters from the request body
const alertsIndexPattern = decodeURIComponent(config.alertsIndexPattern);
const {
apiConfig,
anonymizationFields,
end,
filter,
langSmithApiKey,
langSmithProject,
replacements,
size,
start,
} = config;
// callback to accumulate the latest replacements:
let latestReplacements: Replacements = { ...replacements };
const onNewReplacements = (newReplacements: Replacements) => {
latestReplacements = { ...latestReplacements, ...newReplacements };
};
try {
const { anonymizedAlerts, attackDiscoveries } = await invokeAttackDiscoveryGraph({
actionsClient,
alertsIndexPattern,
anonymizationFields,
apiConfig,
connectorTimeout: CONNECTOR_TIMEOUT,
end,
esClient,
filter,
langSmithProject,
langSmithApiKey,
latestReplacements,
logger,
onNewReplacements,
savedObjectsClient,
size,
start,
});
await updateAttackDiscoveries({
anonymizedAlerts,
apiConfig,
attackDiscoveries,
executionUuid,
authenticatedUser,
dataClient,
hasFilter: !!(filter && Object.keys(filter).length),
end,
latestReplacements,
logger,
size,
start,
startTime,
telemetry,
});
return { anonymizedAlerts, attackDiscoveries, replacements: latestReplacements };
} catch (err) {
await handleGraphError({
apiConfig,
executionUuid,
authenticatedUser,
dataClient,
err,
latestReplacements,
logger,
telemetry,
});
return { error: err };
}
};

View file

@ -298,7 +298,7 @@ describe('helpers', () => {
anonymizedAlerts: mockAnonymizedAlerts,
apiConfig: mockApiConfig,
attackDiscoveries: mockAttackDiscoveries,
attackDiscoveryId: 'attack-discovery-id',
executionUuid: 'attack-discovery-id',
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
hasFilter: false,
@ -338,7 +338,7 @@ describe('helpers', () => {
anonymizedAlerts: mockAnonymizedAlerts,
apiConfig: mockApiConfig,
attackDiscoveries: mockAttackDiscoveries,
attackDiscoveryId: 'attack-discovery-id',
executionUuid: 'attack-discovery-id',
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
hasFilter: false,
@ -378,7 +378,7 @@ describe('helpers', () => {
anonymizedAlerts: mockAnonymizedAlerts,
apiConfig: mockApiConfig,
attackDiscoveries: mockAttackDiscoveries,
attackDiscoveryId: 'attack-discovery-id',
executionUuid: 'attack-discovery-id',
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
hasFilter: true,
@ -419,7 +419,7 @@ describe('helpers', () => {
anonymizedAlerts: mockAnonymizedAlerts,
apiConfig: mockApiConfig,
attackDiscoveries: mockAttackDiscoveries,
attackDiscoveryId: 'attack-discovery-id',
executionUuid: 'attack-discovery-id',
authenticatedUser: mockAuthenticatedUser,
dataClient: mockDataClient,
hasFilter: false,

View file

@ -133,7 +133,7 @@ export const updateAttackDiscoveries = async ({
anonymizedAlerts,
apiConfig,
attackDiscoveries,
attackDiscoveryId,
executionUuid,
authenticatedUser,
dataClient,
hasFilter,
@ -148,7 +148,7 @@ export const updateAttackDiscoveries = async ({
anonymizedAlerts: Document[];
apiConfig: ApiConfig;
attackDiscoveries: AttackDiscovery[] | null;
attackDiscoveryId: string;
executionUuid: string;
authenticatedUser: AuthenticatedUser;
dataClient: AttackDiscoveryDataClient;
end?: string;
@ -164,7 +164,7 @@ export const updateAttackDiscoveries = async ({
}) => {
try {
const currentAd = await dataClient.getAttackDiscovery({
id: attackDiscoveryId,
id: executionUuid,
authenticatedUser,
});
if (currentAd === null || currentAd?.status === 'canceled') {
@ -185,7 +185,7 @@ export const updateAttackDiscoveries = async ({
date: new Date().toISOString(),
}),
}),
id: attackDiscoveryId,
id: executionUuid,
replacements: latestReplacements,
backingIndex: currentAd.backingIndex,
};

View file

@ -15,7 +15,7 @@ import { ATTACK_DISCOVERY_ERROR_EVENT } from '../../../../../lib/telemetry/event
export const handleGraphError = async ({
apiConfig,
attackDiscoveryId,
executionUuid,
authenticatedUser,
dataClient,
err,
@ -24,7 +24,7 @@ export const handleGraphError = async ({
telemetry,
}: {
apiConfig: ApiConfig;
attackDiscoveryId: string;
executionUuid: string;
authenticatedUser: AuthenticatedUser;
dataClient: AttackDiscoveryDataClient;
err: Error;
@ -36,7 +36,7 @@ export const handleGraphError = async ({
logger.error(err);
const error = transformError(err);
const currentAd = await dataClient.getAttackDiscovery({
id: attackDiscoveryId,
id: executionUuid,
authenticatedUser,
});
@ -48,7 +48,7 @@ export const handleGraphError = async ({
attackDiscoveryUpdateProps: {
attackDiscoveries: [],
status: attackDiscoveryStatus.failed,
id: attackDiscoveryId,
id: executionUuid,
replacements: latestReplacements,
backingIndex: currentAd.backingIndex,
failureReason: error.message,

View file

@ -10,23 +10,18 @@ import {
AttackDiscoveryPostRequestBody,
AttackDiscoveryPostResponse,
API_VERSIONS,
Replacements,
} from '@kbn/elastic-assistant-common';
import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common';
import { transformError } from '@kbn/securitysolution-es-utils';
import moment from 'moment/moment';
import { ATTACK_DISCOVERY } from '../../../../common/constants';
import { handleGraphError } from './helpers/handle_graph_error';
import { updateAttackDiscoveries, updateAttackDiscoveryStatusToRunning } from '../helpers/helpers';
import { updateAttackDiscoveryStatusToRunning } from '../helpers/helpers';
import { buildResponse } from '../../../lib/build_response';
import { ElasticAssistantRequestHandlerContext } from '../../../types';
import { invokeAttackDiscoveryGraph } from './helpers/invoke_attack_discovery_graph';
import { requestIsValid } from './helpers/request_is_valid';
import { generateAttackDiscoveries } from '../helpers/generate_discoveries';
const ROUTE_HANDLER_TIMEOUT = 10 * 60 * 1000; // 10 * 60 seconds = 10 minutes
const LANG_CHAIN_TIMEOUT = ROUTE_HANDLER_TIMEOUT - 10_000; // 9 minutes 50 seconds
const CONNECTOR_TIMEOUT = LANG_CHAIN_TIMEOUT - 10_000; // 9 minutes 40 seconds
export const postAttackDiscoveryRoute = (
router: IRouter<ElasticAssistantRequestHandlerContext>
@ -61,7 +56,6 @@ export const postAttackDiscoveryRoute = (
},
},
async (context, request, response): Promise<IKibanaResponse<AttackDiscoveryPostResponse>> => {
const startTime = moment(); // start timing the generation
const resp = buildResponse(response);
const assistantContext = await context.elasticAssistant;
const logger: Logger = assistantContext.logger;
@ -89,17 +83,7 @@ export const postAttackDiscoveryRoute = (
// get parameters from the request body
const alertsIndexPattern = decodeURIComponent(request.body.alertsIndexPattern);
const {
apiConfig,
anonymizationFields,
end,
filter,
langSmithApiKey,
langSmithProject,
replacements,
size,
start,
} = request.body;
const { apiConfig, size } = request.body;
if (
!requestIsValid({
@ -117,12 +101,6 @@ export const postAttackDiscoveryRoute = (
// get an Elasticsearch client for the authenticated user:
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
// callback to accumulate the latest replacements:
let latestReplacements: Replacements = { ...replacements };
const onNewReplacements = (newReplacements: Replacements) => {
latestReplacements = { ...latestReplacements, ...newReplacements };
};
const { currentAd, attackDiscoveryId } = await updateAttackDiscoveryStatusToRunning(
dataClient,
authenticatedUser,
@ -131,54 +109,17 @@ export const postAttackDiscoveryRoute = (
);
// Don't await the results of invoking the graph; (just the metadata will be returned from the route handler):
invokeAttackDiscoveryGraph({
generateAttackDiscoveries({
actionsClient,
alertsIndexPattern,
anonymizationFields,
apiConfig,
connectorTimeout: CONNECTOR_TIMEOUT,
end,
executionUuid: attackDiscoveryId,
authenticatedUser,
config: request.body,
dataClient,
esClient,
filter,
langSmithProject,
langSmithApiKey,
latestReplacements,
logger,
onNewReplacements,
savedObjectsClient,
size,
start,
})
.then(({ anonymizedAlerts, attackDiscoveries }) =>
updateAttackDiscoveries({
anonymizedAlerts,
apiConfig,
attackDiscoveries,
attackDiscoveryId,
authenticatedUser,
dataClient,
hasFilter: !!(filter && Object.keys(filter).length),
end,
latestReplacements,
logger,
size,
start,
startTime,
telemetry,
})
)
.catch((err) =>
handleGraphError({
apiConfig,
attackDiscoveryId,
authenticatedUser,
dataClient,
err,
latestReplacements,
logger,
telemetry,
})
);
telemetry,
}).catch(() => {}); // to silence @typescript-eslint/no-floating-promises
return response.ok({
body: currentAd,