Skip to content

Commit a3d535f

Browse files
cesco-fcursoragent
andauthored
[SigEvents] Add lifecycle view and filters for significant events (#271380)
## Summary Adds an event lifecycle endpoint and timeline UI so users can trace the full chain of detections → discoveries → verdicts → event versions when clicking a significant event. Also introduces search and filter controls on the events tab. ### Lifecycle - New `GET /internal/sig_events/events/{id}/lifecycle` endpoint that walks the event chain via `previous_event_id`, collects related discoveries and verdicts in parallel, and deduplicates detections - Flyout with event details, root cause, recommendations, evidences, and a chronological lifecycle timeline ### Filters & search - Added verdict, impact, and stream filter popovers to the events tab - Added debounced text search - Route accepts array-based query params for multi-select filters https://github.com/user-attachments/assets/2a11830b-f726-45b2-b110-10810ccf63cf --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a34e41c commit a3d535f

20 files changed

Lines changed: 1110 additions & 264 deletions

File tree

x-pack/platform/packages/shared/kbn-streams-schema/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ export type {
212212
GeneratedSignificantEventQuery,
213213
SignificantEventsQueriesGenerationResult,
214214
SignificantEventsQueriesGenerationTaskResult,
215+
LifecycleDetection,
216+
EventLifecycleResponse,
215217
} from './src/api/significant_events';
216218
export { generatedSignificantEventQuerySchema } from './src/api/significant_events';
217219

x-pack/platform/packages/shared/kbn-streams-schema/src/api/significant_events/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
type StreamQuery,
1717
} from '../../queries';
1818
import type { TaskStatus } from '../../tasks/types';
19+
import type { Discovery } from '../../sig_events/discoveries';
20+
import type { Verdict } from '../../sig_events/verdicts';
21+
import type { SigEvent } from '../../sig_events/events';
1922

2023
/**
2124
* SignificantEvents Get Response
@@ -123,6 +126,21 @@ type SignificantEventsQueriesGenerationTaskResult =
123126
status: TaskStatus.Completed | TaskStatus.Acknowledged;
124127
} & SignificantEventsQueriesGenerationResult);
125128

129+
interface LifecycleDetection {
130+
detection_id: string;
131+
rule_name?: string;
132+
stream_name?: string;
133+
change_point_type?: string;
134+
detected_at: string;
135+
}
136+
137+
interface EventLifecycleResponse {
138+
detections: LifecycleDetection[];
139+
discoveries: Discovery[];
140+
verdicts: Verdict[];
141+
events: SigEvent[];
142+
}
143+
126144
export type {
127145
SignificantEventsResponse,
128146
SignificantEventsGetResponse,
@@ -131,4 +149,6 @@ export type {
131149
SignificantEventsGenerateResponse,
132150
SignificantEventsQueriesGenerationResult,
133151
SignificantEventsQueriesGenerationTaskResult,
152+
LifecycleDetection,
153+
EventLifecycleResponse,
134154
};

x-pack/platform/plugins/shared/streams/server/lib/sig_events/detections/detection_client.ts

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77

88
import type { IDataStreamClient } from '@kbn/data-streams';
99
import { esql } from '@elastic/esql';
10+
import type { ESQLAstExpression } from '@elastic/esql/types';
1011
import type { ElasticsearchClient } from '@kbn/core/server';
1112
import {
1213
type CommonSearchOptions,
1314
type PaginatedSearchOptions,
1415
type PaginatedResponse,
1516
} from '../query_utils';
1617
import {
17-
type LatestSourceWhereCondition,
18+
andWhere,
19+
inFilter,
1820
runLatestSourceEsqlQuery,
1921
runPaginatedLatestSourceEsqlQuery,
2022
runFindByIdEsqlQuery,
@@ -25,6 +27,7 @@ import {
2527
type StoredDetection,
2628
type detectionsMappings,
2729
} from './data_stream';
30+
import { FIELD_DETECTION_ID } from '../field_names';
2831

2932
export type DetectionDataStreamClient = IDataStreamClient<
3033
typeof detectionsMappings,
@@ -41,15 +44,6 @@ export interface DetectionsPaginatedSearchOptions extends PaginatedSearchOptions
4144
rule_name?: string;
4245
}
4346

44-
const andWhere = (
45-
current: LatestSourceWhereCondition | undefined,
46-
next: LatestSourceWhereCondition
47-
): LatestSourceWhereCondition => {
48-
return current ? esql.exp`${current} AND ${next}` : next;
49-
};
50-
51-
const GROUP_BY_FIELD = 'detection_id';
52-
5347
export class DetectionClient {
5448
constructor(
5549
private readonly clients: {
@@ -66,13 +60,9 @@ export class DetectionClient {
6660
});
6761
}
6862

69-
private buildWhere(options: DetectionsSearchOptions): LatestSourceWhereCondition | undefined {
70-
let where: LatestSourceWhereCondition | undefined;
71-
72-
const ruleUuidLiterals = options.rule_uuid?.map((ruleUuid) => esql.str(ruleUuid));
73-
if (ruleUuidLiterals?.length) {
74-
where = andWhere(where, esql.exp`${esql.col('rule_uuid')} IN (${ruleUuidLiterals})`);
75-
}
63+
private buildWhere(options: DetectionsSearchOptions): ESQLAstExpression | undefined {
64+
let where: ESQLAstExpression | undefined;
65+
where = inFilter({ where, field: 'rule_uuid', values: options.rule_uuid });
7666

7767
if (options.rule_name) {
7868
where = andWhere(where, esql.exp`${esql.col('rule_name')} == ${esql.str(options.rule_name)}`);
@@ -88,7 +78,7 @@ export class DetectionClient {
8878
options,
8979
index: DETECTIONS_DATA_STREAM,
9080
where: this.buildWhere(options),
91-
groupBy: GROUP_BY_FIELD,
81+
groupBy: FIELD_DETECTION_ID,
9282
});
9383
}
9484

@@ -101,7 +91,7 @@ export class DetectionClient {
10191
options,
10292
index: DETECTIONS_DATA_STREAM,
10393
where: this.buildWhere(options),
104-
groupBy: GROUP_BY_FIELD,
94+
groupBy: FIELD_DETECTION_ID,
10595
});
10696
}
10797

@@ -110,7 +100,7 @@ export class DetectionClient {
110100
esClient: this.clients.esClient,
111101
space: this.clients.space,
112102
index: DETECTIONS_DATA_STREAM,
113-
idField: GROUP_BY_FIELD,
103+
idField: FIELD_DETECTION_ID,
114104
idValue: detectionId,
115105
});
116106
}

x-pack/platform/plugins/shared/streams/server/lib/sig_events/discoveries/discovery_client.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,21 @@ import {
1616
runLatestSourceEsqlQuery,
1717
runPaginatedLatestSourceEsqlQuery,
1818
runFindByIdEsqlQuery,
19+
runFindByIdsEsqlQuery,
1920
} from '../latest_source_query';
2021
import {
2122
DISCOVERIES_DATA_STREAM,
2223
type Discovery,
2324
type StoredDiscovery,
2425
type discoveriesMappings,
2526
} from './data_stream';
27+
import { FIELD_DISCOVERY_ID, FIELD_DISCOVERY_SLUG } from '../field_names';
2628

2729
export type DiscoveryDataStreamClient = IDataStreamClient<
2830
typeof discoveriesMappings,
2931
StoredDiscovery
3032
>;
3133

32-
const GROUP_BY_FIELD = 'discovery_id';
33-
3434
export class DiscoveryClient {
3535
constructor(
3636
private readonly clients: {
@@ -53,7 +53,7 @@ export class DiscoveryClient {
5353
space: this.clients.space,
5454
options,
5555
index: DISCOVERIES_DATA_STREAM,
56-
groupBy: GROUP_BY_FIELD,
56+
groupBy: FIELD_DISCOVERY_ID,
5757
});
5858
}
5959

@@ -65,7 +65,7 @@ export class DiscoveryClient {
6565
space: this.clients.space,
6666
options,
6767
index: DISCOVERIES_DATA_STREAM,
68-
groupBy: GROUP_BY_FIELD,
68+
groupBy: FIELD_DISCOVERY_ID,
6969
});
7070
}
7171

@@ -74,8 +74,28 @@ export class DiscoveryClient {
7474
esClient: this.clients.esClient,
7575
space: this.clients.space,
7676
index: DISCOVERIES_DATA_STREAM,
77-
idField: GROUP_BY_FIELD,
77+
idField: FIELD_DISCOVERY_ID,
7878
idValue: discoveryId,
7979
});
8080
}
81+
82+
async findByIds(discoveryIds: string[]): Promise<{ hits: Discovery[] }> {
83+
return runFindByIdsEsqlQuery<Discovery>({
84+
esClient: this.clients.esClient,
85+
space: this.clients.space,
86+
index: DISCOVERIES_DATA_STREAM,
87+
idField: FIELD_DISCOVERY_ID,
88+
idValues: discoveryIds,
89+
});
90+
}
91+
92+
async findBySlug(slug: string): Promise<{ hits: Discovery[] }> {
93+
return runFindByIdEsqlQuery<Discovery>({
94+
esClient: this.clients.esClient,
95+
space: this.clients.space,
96+
index: DISCOVERIES_DATA_STREAM,
97+
idField: FIELD_DISCOVERY_SLUG,
98+
idValue: slug,
99+
});
100+
}
81101
}

x-pack/platform/plugins/shared/streams/server/lib/sig_events/events/data_stream.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ export const eventsMappings = {
1919
event_id: mappings.keyword(),
2020
discovery_id: mappings.keyword(),
2121
discovery_slug: mappings.keyword(),
22+
previous_event_id: mappings.keyword(),
2223
rule_names: mappings.keyword(),
24+
stream_names: mappings.keyword(),
25+
verdict: mappings.keyword(),
26+
impact: mappings.keyword(),
27+
title: mappings.text(),
28+
summary: mappings.text(),
2329
},
2430
} satisfies MappingsDefinition;
2531

@@ -28,7 +34,7 @@ export type { SigEvent };
2834

2935
export const eventsDataStream: DataStreamDefinition<typeof eventsMappings, StoredEvent> = {
3036
name: EVENTS_DATA_STREAM,
31-
version: 2,
37+
version: 3,
3238
hidden: true,
3339
template: {
3440
priority: 500,

x-pack/platform/plugins/shared/streams/server/lib/sig_events/events/event_client.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66
*/
77

88
import type { IDataStreamClient } from '@kbn/data-streams';
9+
import { esql } from '@elastic/esql';
10+
import type { ESQLAstExpression } from '@elastic/esql/types';
911
import type { ElasticsearchClient } from '@kbn/core/server';
1012
import {
1113
type CommonSearchOptions,
1214
type PaginatedSearchOptions,
1315
type PaginatedResponse,
1416
} from '../query_utils';
1517
import {
18+
andWhere,
19+
inFilter,
1620
runLatestSourceEsqlQuery,
1721
runPaginatedLatestSourceEsqlQuery,
1822
runFindByIdEsqlQuery,
@@ -23,10 +27,17 @@ import {
2327
type StoredEvent,
2428
type eventsMappings,
2529
} from './data_stream';
30+
import { FIELD_EVENT_ID, FIELD_DISCOVERY_SLUG } from '../field_names';
2631

2732
export type EventDataStreamClient = IDataStreamClient<typeof eventsMappings, StoredEvent>;
2833

29-
const GROUP_BY_FIELD = 'event_id';
34+
export interface EventsFilterOptions {
35+
verdict?: string[];
36+
stream?: string[];
37+
search?: string;
38+
}
39+
40+
export interface EventsPaginatedSearchOptions extends PaginatedSearchOptions, EventsFilterOptions {}
3041

3142
export class EventClient {
3243
constructor(
@@ -37,6 +48,25 @@ export class EventClient {
3748
}
3849
) {}
3950

51+
private buildWhere(options: EventsFilterOptions): ESQLAstExpression | undefined {
52+
let where: ESQLAstExpression | undefined;
53+
where = inFilter({ where, field: 'verdict', values: options.verdict });
54+
where = inFilter({ where, field: 'stream_names', values: options.stream });
55+
56+
if (options.search) {
57+
const escaped = options.search.toLowerCase().replace(/\\/g, '\\\\').replace(/[*?]/g, '\\$&');
58+
const pattern = esql.str(`*${escaped}*`);
59+
where = andWhere(
60+
where,
61+
esql.exp`(TO_LOWER(${esql.col('title')}) LIKE ${pattern} OR TO_LOWER(${esql.col(
62+
'summary'
63+
)}) LIKE ${pattern})`
64+
);
65+
}
66+
67+
return where;
68+
}
69+
4070
async bulkCreate(events: SigEvent[]) {
4171
return this.clients.dataStreamClient.create({
4272
space: this.clients.space,
@@ -50,19 +80,20 @@ export class EventClient {
5080
space: this.clients.space,
5181
options,
5282
index: EVENTS_DATA_STREAM,
53-
groupBy: GROUP_BY_FIELD,
83+
groupBy: FIELD_DISCOVERY_SLUG,
5484
});
5585
}
5686

5787
async findLatestPaginated(
58-
options: PaginatedSearchOptions = {}
88+
options: EventsPaginatedSearchOptions = {}
5989
): Promise<PaginatedResponse<SigEvent>> {
6090
return runPaginatedLatestSourceEsqlQuery<SigEvent>({
6191
esClient: this.clients.esClient,
6292
space: this.clients.space,
6393
options,
6494
index: EVENTS_DATA_STREAM,
65-
groupBy: GROUP_BY_FIELD,
95+
where: this.buildWhere(options),
96+
groupBy: FIELD_DISCOVERY_SLUG,
6697
});
6798
}
6899

@@ -71,8 +102,18 @@ export class EventClient {
71102
esClient: this.clients.esClient,
72103
space: this.clients.space,
73104
index: EVENTS_DATA_STREAM,
74-
idField: GROUP_BY_FIELD,
105+
idField: FIELD_EVENT_ID,
75106
idValue: eventId,
76107
});
77108
}
109+
110+
async findByDiscoverySlug(slug: string): Promise<{ hits: SigEvent[] }> {
111+
return runFindByIdEsqlQuery<SigEvent>({
112+
esClient: this.clients.esClient,
113+
space: this.clients.space,
114+
index: EVENTS_DATA_STREAM,
115+
idField: FIELD_DISCOVERY_SLUG,
116+
idValue: slug,
117+
});
118+
}
78119
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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 FIELD_EVENT_ID = 'event_id';
9+
export const FIELD_DETECTION_ID = 'detection_id';
10+
export const FIELD_DISCOVERY_ID = 'discovery_id';
11+
export const FIELD_DISCOVERY_SLUG = 'discovery_slug';

0 commit comments

Comments
 (0)