Skip to content

Commit 647a183

Browse files
authored
[Security Solution] defend insights langgraph upgrade (elastic#211038)
## Summary This is intended to be a "minimal" migration for Defend Insights to langgraph + output chunking. Other than the increased events due to the context increase from output chunking, the functionality is unchanged. * migrates defend insights to langgraph * adds output chunking / refinement ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
1 parent d3d44de commit 647a183

File tree

122 files changed

+6783
-677
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

122 files changed

+6783
-677
lines changed

.github/CODEOWNERS

+2
Original file line numberDiff line numberDiff line change
@@ -2507,6 +2507,8 @@ x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/defend
25072507
x-pack/plugins/elastic_assistant/server/__mocks__/defend_insights_schema.mock.ts @elastic/security-defend-workflows
25082508
x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/defend_insights @elastic/security-defend-workflows
25092509
x-pack/plugins/elastic_assistant/server/routes/defend_insights @elastic/security-defend-workflows
2510+
x-pack/solutions/security/plugins/elastic_assistant/server/lib/defend_insights @elastic/security-defend-workflows
2511+
x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights @elastic/security-defend-workflows
25102512
/x-pack/solutions/security/plugins/security_solution/public/common/components/response_actions @elastic/security-defend-workflows
25112513
/x-pack/solutions/security/plugins/security_solution_serverless/public/upselling/pages/osquery_automated_response_actions.tsx @elastic/security-defend-workflows
25122514

x-pack/platform/packages/shared/kbn-elastic-assistant-common/constants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@ export const ELASTIC_AI_ASSISTANT_EVALUATE_URL =
5757
`${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/evaluate` as const;
5858

5959
// Defend insights
60-
export const DEFEND_INSIGHTS_TOOL_ID = 'defend-insights';
60+
export const DEFEND_INSIGHTS_ID = 'defend-insights';
6161
export const DEFEND_INSIGHTS = `${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/defend_insights`;
6262
export const DEFEND_INSIGHTS_BY_ID = `${DEFEND_INSIGHTS}/{id}`;

x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/alerts/helpers/get_raw_data_or_default/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
* 2.0.
66
*/
77

8-
import { isRawDataValid } from '../is_raw_data_valid';
98
import type { MaybeRawData } from '../types';
9+
import { isRawDataValid } from '../is_raw_data_valid';
1010

1111
/** Returns the raw data if it valid, or a default if it's not */
1212
export const getRawDataOrDefault = (rawData: MaybeRawData): Record<string, unknown[]> =>

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import type { estypes } from '@elastic/elasticsearch';
99
import { DefendInsightStatus, DefendInsightType } from '@kbn/elastic-assistant-common';
1010

11-
import type { EsDefendInsightSchema } from '../ai_assistant_data_clients/defend_insights/types';
11+
import type { EsDefendInsightSchema } from '../lib/defend_insights/persistence/types';
1212

1313
export const getDefendInsightsSearchEsMock = () => {
1414
const searchResponse: estypes.SearchResponse<EsDefendInsightSchema> = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { DefendInsightStatus, DefendInsightType } from '@kbn/elastic-assistant-common';
9+
10+
import type { EsDefendInsightSchema } from '../lib/defend_insights/persistence/types';
11+
12+
export const getParsedDefendInsightsMock = (timestamp: string): EsDefendInsightSchema[] => [
13+
{
14+
'@timestamp': timestamp,
15+
created_at: timestamp,
16+
updated_at: timestamp,
17+
last_viewed_at: timestamp,
18+
users: [
19+
{
20+
id: 'u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0',
21+
name: 'elastic',
22+
},
23+
],
24+
status: DefendInsightStatus.Enum.succeeded,
25+
api_config: {
26+
action_type_id: '.bedrock',
27+
connector_id: 'ac4e19d1-e2e2-49af-bf4b-59428473101c',
28+
model: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
29+
},
30+
endpoint_ids: ['6e09ec1c-644c-4148-a02d-be451c35400d'],
31+
insight_type: DefendInsightType.Enum.incompatible_antivirus,
32+
insights: [
33+
{
34+
group: 'windows_defenders',
35+
events: [],
36+
},
37+
],
38+
namespace: 'default',
39+
id: '655c52ec-49ee-4d20-87e5-7edd6d8f84e8',
40+
generation_intervals: [
41+
{
42+
date: timestamp,
43+
duration_ms: 13113,
44+
},
45+
],
46+
average_interval_ms: 13113,
47+
events_context_count: 100,
48+
replacements: [
49+
{
50+
uuid: '2009c67b-89b8-43d9-b502-2c32f71875a0',
51+
value: 'root',
52+
},
53+
{
54+
uuid: '9f7f91b6-6853-48b7-bfb8-403f5efb2364',
55+
value: 'joey-dev-default-3539',
56+
},
57+
],
58+
},
59+
{
60+
'@timestamp': timestamp,
61+
created_at: timestamp,
62+
updated_at: timestamp,
63+
last_viewed_at: timestamp,
64+
users: [
65+
{
66+
id: '00468e82-e37f-4224-80c1-c62e594c74b1',
67+
name: 'ubuntu',
68+
},
69+
],
70+
status: DefendInsightStatus.Enum.succeeded,
71+
api_config: {
72+
action_type_id: '.bedrock',
73+
connector_id: 'bc5e19d1-e2e2-49af-bf4b-59428473101d',
74+
model: 'anthropic.claude-3-5-sonnet-20240620-v1:0',
75+
},
76+
endpoint_ids: ['b557bb12-8206-44b6-b2a5-dbcce5b1e65e'],
77+
insight_type: DefendInsightType.Enum.noisy_process_tree,
78+
insights: [
79+
{
80+
group: 'linux_security',
81+
events: [],
82+
},
83+
],
84+
namespace: 'default',
85+
id: '7a1b52ec-49ee-4d20-87e5-7edd6d8f84e9',
86+
generation_intervals: [
87+
{
88+
date: timestamp,
89+
duration_ms: 13113,
90+
},
91+
],
92+
average_interval_ms: 13113,
93+
events_context_count: 100,
94+
replacements: [
95+
{
96+
uuid: '3119c67b-89b8-43d9-b502-2c32f71875b1',
97+
value: 'ubuntu',
98+
},
99+
{
100+
uuid: '8e7f91b6-6853-48b7-bfb8-403f5efb2365',
101+
value: 'ubuntu-dev-default-3540',
102+
},
103+
],
104+
},
105+
];
106+
107+
export const getRawDefendInsightsMock = (timestamp: string) =>
108+
JSON.stringify(getParsedDefendInsightsMock(timestamp));

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
} from '../ai_assistant_data_clients/knowledge_base';
3030
import { defaultAssistantFeatures } from '@kbn/elastic-assistant-common';
3131
import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence';
32-
import { DefendInsightsDataClient } from '../ai_assistant_data_clients/defend_insights';
32+
import { DefendInsightsDataClient } from '../lib/defend_insights/persistence';
3333
import { authenticatedUser } from './user';
3434

3535
export const createMockClients = () => {

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { omit } from 'lodash';
2121
import { InstallationStatus } from '@kbn/product-doc-base-plugin/common/install_status';
2222
import { TrainedModelsProvider } from '@kbn/ml-plugin/server/shared_services/providers';
2323
import { attackDiscoveryFieldMap } from '../lib/attack_discovery/persistence/field_maps_configuration/field_maps_configuration';
24-
import { defendInsightsFieldMap } from '../ai_assistant_data_clients/defend_insights/field_maps_configuration';
24+
import { defendInsightsFieldMap } from '../lib/defend_insights/persistence/field_maps_configuration';
2525
import { getDefaultAnonymizationFields } from '../../common/anonymization';
2626
import { AssistantResourceNames, GetElser } from '../types';
2727
import {
@@ -49,7 +49,7 @@ import {
4949
GetAIAssistantKnowledgeBaseDataClientParams,
5050
} from '../ai_assistant_data_clients/knowledge_base';
5151
import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence';
52-
import { DefendInsightsDataClient } from '../ai_assistant_data_clients/defend_insights';
52+
import { DefendInsightsDataClient } from '../lib/defend_insights/persistence';
5353
import { createGetElserId, ensureProductDocumentationInstalled } from './helpers';
5454
import { hasAIAssistantLicense } from '../routes/helpers';
5555

x-pack/solutions/security/plugins/security_solution/server/assistant/tools/defend_insights/errors.ts x-pack/solutions/security/plugins/elastic_assistant/server/lib/defend_insights/errors.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
* 2.0.
66
*/
77

8-
import { EndpointError } from '../../../../common/endpoint/errors';
9-
10-
export class InvalidDefendInsightTypeError extends EndpointError {
8+
export class InvalidDefendInsightTypeError extends Error {
119
constructor() {
1210
super('invalid defend insight type');
1311
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
// LangGraph metadata
9+
export const DEFEND_INSIGHTS_GRAPH_RUN_NAME = 'Defend insights';
10+
11+
// Limits
12+
export const DEFAULT_MAX_GENERATION_ATTEMPTS = 10;
13+
export const DEFAULT_MAX_HALLUCINATION_FAILURES = 5;
14+
export const DEFAULT_MAX_REPEATED_GENERATIONS = 3;
15+
16+
export const NodeType = {
17+
GENERATE_NODE: 'generate',
18+
REFINE_NODE: 'refine',
19+
RETRIEVE_ANONYMIZED_EVENTS_NODE: 'retrieve_anonymized_events',
20+
} as const;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { getGenerateOrEndDecision } from '.';
9+
10+
describe('getGenerateOrEndDecision', () => {
11+
it('returns "end" when hasZeroEvents is true', () => {
12+
const result = getGenerateOrEndDecision(true);
13+
14+
expect(result).toEqual('end');
15+
});
16+
17+
it('returns "generate" when hasZeroEvents is false', () => {
18+
const result = getGenerateOrEndDecision(false);
19+
20+
expect(result).toEqual('generate');
21+
});
22+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
export const getGenerateOrEndDecision = (hasZeroEvents: boolean): 'end' | 'generate' =>
9+
hasZeroEvents ? 'end' : 'generate';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { loggerMock } from '@kbn/logging-mocks';
9+
10+
import { getGenerateOrEndEdge } from '.';
11+
import type { GraphState } from '../../types';
12+
13+
const logger = loggerMock.create();
14+
15+
const graphState: GraphState = {
16+
insights: null,
17+
prompt: 'prompt',
18+
anonymizedEvents: [
19+
{
20+
metadata: {},
21+
pageContent:
22+
'@timestamp,2024-10-10T21:01:24.148Z\n' +
23+
'_id,e809ffc5e0c2e731c1f146e0f74250078136a87574534bf8e9ee55445894f7fc\n' +
24+
'host.name,e1cb3cf0-30f3-4f99-a9c8-518b955c6f90\n' +
25+
'user.name,039c15c5-3964-43e7-a891-42fe2ceeb9ff',
26+
},
27+
{
28+
metadata: {},
29+
pageContent:
30+
'@timestamp,2024-10-10T21:01:24.148Z\n' +
31+
'_id,c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b\n' +
32+
'host.name,e1cb3cf0-30f3-4f99-a9c8-518b955c6f90\n' +
33+
'user.name,039c15c5-3964-43e7-a891-42fe2ceeb9ff',
34+
},
35+
],
36+
combinedGenerations: 'generations',
37+
combinedRefinements: 'refinements',
38+
errors: [],
39+
generationAttempts: 0,
40+
generations: [],
41+
hallucinationFailures: 0,
42+
maxGenerationAttempts: 10,
43+
maxHallucinationFailures: 5,
44+
maxRepeatedGenerations: 10,
45+
refinements: [],
46+
refinePrompt: 'refinePrompt',
47+
replacements: {},
48+
unrefinedResults: null,
49+
};
50+
51+
describe('getGenerateOrEndEdge', () => {
52+
beforeEach(() => jest.clearAllMocks());
53+
54+
it("returns 'end' when there are zero events", () => {
55+
const state: GraphState = {
56+
...graphState,
57+
anonymizedEvents: [], // <-- zero events
58+
};
59+
60+
const edge = getGenerateOrEndEdge(logger);
61+
const result = edge(state);
62+
63+
expect(result).toEqual('end');
64+
});
65+
66+
it("returns 'generate' when there are events", () => {
67+
const edge = getGenerateOrEndEdge(logger);
68+
const result = edge(graphState);
69+
70+
expect(result).toEqual('generate');
71+
});
72+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { Logger } from '@kbn/core/server';
9+
10+
import type { GraphState } from '../../types';
11+
import { getGenerateOrEndDecision } from './helpers/get_generate_or_end_decision';
12+
13+
export const getGenerateOrEndEdge = (logger?: Logger) => {
14+
const edge = (state: GraphState): 'end' | 'generate' => {
15+
logger?.debug(() => '---GENERATE OR END---');
16+
const { anonymizedEvents } = state;
17+
18+
const hasZeroEvents = !anonymizedEvents.length;
19+
20+
const decision = getGenerateOrEndDecision(hasZeroEvents);
21+
22+
logger?.debug(
23+
() => `generatOrEndEdge evaluated the following (derived) state:\n${JSON.stringify(
24+
{
25+
anonymizedEvents: anonymizedEvents.length,
26+
hasZeroEvents,
27+
},
28+
null,
29+
2
30+
)}
31+
\n---GENERATE OR END: ${decision}---`
32+
);
33+
return decision;
34+
};
35+
36+
return edge;
37+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { getGenerateOrRefineOrEndDecision } from '.';
9+
10+
describe('getGenerateOrRefineOrEndDecision', () => {
11+
it("returns 'end' if getShouldEnd returns true", () => {
12+
const result = getGenerateOrRefineOrEndDecision({
13+
hasUnrefinedResults: false,
14+
hasZeroEvents: true,
15+
maxHallucinationFailuresReached: true,
16+
maxRetriesReached: true,
17+
});
18+
19+
expect(result).toEqual('end');
20+
});
21+
22+
it("returns 'refine' if hasUnrefinedResults is true and getShouldEnd returns false", () => {
23+
const result = getGenerateOrRefineOrEndDecision({
24+
hasUnrefinedResults: true,
25+
hasZeroEvents: false,
26+
maxHallucinationFailuresReached: false,
27+
maxRetriesReached: false,
28+
});
29+
30+
expect(result).toEqual('refine');
31+
});
32+
33+
it("returns 'generate' if hasUnrefinedResults is false and getShouldEnd returns false", () => {
34+
const result = getGenerateOrRefineOrEndDecision({
35+
hasUnrefinedResults: false,
36+
hasZeroEvents: false,
37+
maxHallucinationFailuresReached: false,
38+
maxRetriesReached: false,
39+
});
40+
41+
expect(result).toEqual('generate');
42+
});
43+
});

0 commit comments

Comments
 (0)