Skip to content

Commit fa61b2b

Browse files
crespocarloscursoragentkibanamachine
authored
[SigEvents][Discovery] sig events UI refactor (elastic#271329)
## Summary Refactors the Significant Events discovery UI adapting to the recent workflow changes. **UI changes:** ### Detections <img width="1727" height="618" alt="image" src="https://github.com/user-attachments/assets/f3784f21-7656-485b-bf2b-2fef1c151d4d" /> <img width="1727" height="743" alt="image" src="https://github.com/user-attachments/assets/422eff6b-b53a-4d18-a40d-2286bd56e826" /> ### Discoveries <img width="1725" height="624" alt="image" src="https://github.com/user-attachments/assets/f764ea5a-15c4-4638-87fe-5d36ec0ce236" /> <img width="1726" height="738" alt="image" src="https://github.com/user-attachments/assets/e86de8d7-cb68-43f9-8288-c35e2887f583" /> ### Verdicts (TO BE REMOVED SOON) <img width="1728" height="472" alt="image" src="https://github.com/user-attachments/assets/bf46a2b6-c4f2-4812-907f-eecd3ff1cac5" /> >[!NOTE] > Sigevents tab is being worked on here elastic#271380 ### Checklist - [x] Any text added follows EUI's writing guidelines, uses sentence case text and includes i18n support - [ ] Documentation was added for features that require explanation or tutorials - [ ] Unit or functional tests were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the docker list - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] Flaky Test Runner was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the guidelines - [ ] Review the backport guidelines and apply applicable `backport:*` labels. 🤖 Co-authored with AI assistance. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent f252b85 commit fa61b2b

28 files changed

Lines changed: 1202 additions & 614 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const detectionEvidenceSchema = z.object({
1414

1515
export const detectionSchema = z.object({
1616
'@timestamp': z.iso.datetime(),
17-
silent: z.boolean(),
17+
detected_at: z.iso.datetime().optional(),
18+
kind: z.enum(['detection', 'quiet', 'handled']),
1819
processed: z.boolean(),
1920
detection_id: z.string().optional(),
2021
rule_uuid: z.string(),
@@ -24,7 +25,7 @@ export const detectionSchema = z.object({
2425
alert_index: z.string().optional(),
2526
workflow_execution_id: z.string().optional(),
2627
resolution_lookback_minutes: z.number().optional(),
27-
peak_30m_alert_count: z.number().optional(),
28+
peak_alert_count: z.number().optional(),
2829
detection_evidence: detectionEvidenceSchema.optional(),
2930
alert_samples: z.array(z.record(z.string(), z.unknown())).optional(),
3031
rules_activity: z.array(z.record(z.string(), z.unknown())).optional(),

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

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,44 +12,51 @@ import {
1212
causeKiSchema,
1313
evidenceSchema,
1414
} from '../common_schemas';
15+
import { MAX_STREAM_NAME_LENGTH } from '../../helpers/stream_name_validation';
16+
17+
const MAX_ID_LENGTH = 256;
18+
const MAX_RULE_NAME_LENGTH = 256;
19+
const MAX_TITLE_LENGTH = 512;
20+
const MAX_TEXT_LENGTH = 10_000;
1521

1622
const discoveryDetectionSchema = z.object({
17-
detection_id: z.string().optional(),
18-
rule_name: z.string().optional(),
19-
rule_uuid: z.string().optional(),
20-
stream_name: z.string().optional(),
21-
change_point_type: z.string().optional(),
23+
detection_id: z.string().max(MAX_ID_LENGTH).optional(),
24+
rule_name: z.string().max(MAX_RULE_NAME_LENGTH).optional(),
25+
rule_uuid: z.string().max(MAX_ID_LENGTH).optional(),
26+
stream_name: z.string().max(MAX_STREAM_NAME_LENGTH).optional(),
27+
change_point_type: z.string().max(MAX_ID_LENGTH).optional(),
2228
event_count: z.number().optional(),
29+
alert_count: z.number().optional(),
2330
detected_at: z.string().optional(),
2431
});
2532

2633
export const discoverySchema = z.object({
2734
'@timestamp': z.iso.datetime(),
28-
kind: z.string(),
29-
discovery_id: z.string(),
30-
discovery_slug: z.string(),
31-
rule_names: z.array(z.string()),
32-
stream_names: z.array(z.string()),
33-
title: z.string(),
34-
summary: z.string(),
35-
root_cause: z.string(),
35+
kind: z.enum(['finding', 'clearance']),
36+
discovery_id: z.string().max(MAX_ID_LENGTH),
37+
discovery_slug: z.string().max(MAX_ID_LENGTH),
38+
discovered_at: z.iso.datetime().optional(),
39+
rule_names: z.array(z.string().max(MAX_RULE_NAME_LENGTH)),
40+
stream_names: z.array(z.string().max(MAX_STREAM_NAME_LENGTH)),
41+
title: z.string().max(MAX_TITLE_LENGTH),
42+
summary: z.string().max(MAX_TEXT_LENGTH),
43+
root_cause: z.string().max(MAX_TEXT_LENGTH),
3644
criticality: z.number(),
3745
confidence: z.number(),
38-
impact: z.string(),
46+
impact: z.string().max(MAX_TEXT_LENGTH),
3947
detections: z.array(discoveryDetectionSchema),
4048
dependency_edges: z.array(dependencyEdgeSchema).optional(),
4149
infra_components: z.array(infraComponentSchema).optional(),
4250
cause_kis: z.array(causeKiSchema).optional(),
4351
evidences: z.array(evidenceSchema).optional(),
44-
closes: z.string().optional(),
45-
grouped_into: z.string().optional(),
46-
grouped_discovery_ids: z.array(z.string()).optional(),
47-
grouping_rationale: z.string().optional(),
48-
previous_discovery_id: z.string().optional(),
49-
change_point_occurrence: z.string().optional(),
50-
workflow_execution_id: z.string().optional(),
51-
conversation_id: z.string().optional(),
52-
closed_by_execution_id: z.string().optional(),
52+
closes_discovery_id: z.string().max(MAX_ID_LENGTH).optional(),
53+
grouped_discovery_ids: z.array(z.string().max(MAX_ID_LENGTH)).optional(),
54+
grouping_rationale: z.string().max(MAX_TEXT_LENGTH).optional(),
55+
previous_discovery_id: z.string().max(MAX_ID_LENGTH).optional(),
56+
change_point_occurrence: z.string().max(MAX_ID_LENGTH).optional(),
57+
workflow_execution_id: z.string().max(MAX_ID_LENGTH).optional(),
58+
conversation_id: z.string().max(MAX_ID_LENGTH).optional(),
59+
closed_by_execution_id: z.string().max(MAX_ID_LENGTH).optional(),
5360
});
5461

5562
export type Discovery = z.infer<typeof discoverySchema>;

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,23 @@ import {
1212
causeKiSchema,
1313
evidenceSchema,
1414
} from '../common_schemas';
15+
import { MAX_STREAM_NAME_LENGTH } from '../../helpers/stream_name_validation';
16+
17+
const MAX_RULE_NAME_LENGTH = 256;
1518

1619
export const verdictSchema = z.object({
1720
'@timestamp': z.iso.datetime(),
18-
verdict: z.string(),
21+
verdict: z.enum(['promoted', 'demoted', 'acknowledged']),
1922
verdict_id: z.string().optional(),
2023
discovery_id: z.string(),
2124
discovery_slug: z.string(),
22-
rule_names: z.array(z.string()),
23-
stream_names: z.array(z.string()),
25+
rule_names: z.array(z.string().max(MAX_RULE_NAME_LENGTH)).optional(),
26+
stream_names: z.array(z.string().max(MAX_STREAM_NAME_LENGTH)).optional(),
2427
title: z.string(),
2528
summary: z.string(),
2629
root_cause: z.string(),
2730
criticality: z.number(),
28-
confidence: z.number(),
31+
confidence: z.number().optional(),
2932
impact: z.string().optional(),
3033
recommended_action: z.string().optional(),
3134
recommendations: z.array(z.string()).optional(),

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ export const detectionsMappings = {
1616
dynamic: false,
1717
properties: {
1818
'@timestamp': mappings.date({ format: 'strict_date_optional_time' }),
19-
silent: mappings.boolean(),
20-
processed: mappings.boolean(),
19+
detected_at: mappings.date({ format: 'strict_date_optional_time' }),
20+
kind: mappings.keyword(),
2121
detection_id: mappings.keyword(),
2222
rule_uuid: mappings.keyword(),
2323
rule_name: mappings.keyword(),
24-
peak_30m_alert_count: mappings.long(),
24+
peak_alert_count: mappings.long(),
2525
detection_evidence: mappings.object({
2626
properties: {
27+
change_point_type: mappings.keyword(),
2728
p_value: { type: 'double' as const },
2829
},
2930
}),
@@ -38,7 +39,7 @@ export const detectionsDataStream: DataStreamDefinition<
3839
StoredDetection
3940
> = {
4041
name: DETECTIONS_DATA_STREAM,
41-
version: 3,
42+
version: 4,
4243
hidden: true,
4344
template: {
4445
priority: 500,

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

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import {
1616
} from '../query_utils';
1717
import {
1818
andWhere,
19-
inFilter,
2019
runLatestSourceEsqlQuery,
2120
runPaginatedLatestSourceEsqlQuery,
22-
runFindByIdEsqlQuery,
21+
queryEsql,
22+
esqlToObjects,
2323
} from '../latest_source_query';
2424
import {
2525
DETECTIONS_DATA_STREAM,
@@ -34,6 +34,10 @@ export type DetectionDataStreamClient = IDataStreamClient<
3434
StoredDetection
3535
>;
3636

37+
// _source holds all stored fields; `processed` is derived at query time, not stored.
38+
// Using Omit<Detection, 'processed'> avoids the string | string[] widening from GetFieldsOf.
39+
type RawDetection = Omit<Detection, 'processed'>;
40+
3741
export interface DetectionsSearchOptions extends CommonSearchOptions {
3842
rule_uuid?: string[];
3943
rule_name?: string;
@@ -44,6 +48,10 @@ export interface DetectionsPaginatedSearchOptions extends PaginatedSearchOptions
4448
rule_name?: string;
4549
}
4650

51+
const KIND_HANDLED = 'handled' satisfies Detection['kind'];
52+
const KIND_QUIET = 'quiet' satisfies Detection['kind'];
53+
const PROCESSED_IDS_CHUNK_SIZE = 250;
54+
4755
export class DetectionClient {
4856
constructor(
4957
private readonly clients: {
@@ -60,9 +68,15 @@ export class DetectionClient {
6068
});
6169
}
6270

63-
private buildWhere(options: DetectionsSearchOptions): ESQLAstExpression | undefined {
64-
let where: ESQLAstExpression | undefined;
65-
where = inFilter({ where, field: 'rule_uuid', values: options.rule_uuid });
71+
// Exclude kind:handled from the main query — handled docs are pipeline stamps,
72+
// not anomaly state. processed is derived separately via getProcessedIds.
73+
private buildWhere(options: DetectionsSearchOptions): ESQLAstExpression {
74+
let where: ESQLAstExpression = esql.exp`${esql.col('kind')} != ${esql.str(KIND_HANDLED)}`;
75+
76+
const ruleUuidLiterals = options.rule_uuid?.map((ruleUuid) => esql.str(ruleUuid));
77+
if (ruleUuidLiterals?.length) {
78+
where = andWhere(where, esql.exp`${esql.col('rule_uuid')} IN (${ruleUuidLiterals})`);
79+
}
6680

6781
if (options.rule_name) {
6882
where = andWhere(where, esql.exp`${esql.col('rule_name')} == ${esql.str(options.rule_name)}`);
@@ -71,37 +85,81 @@ export class DetectionClient {
7185
return where;
7286
}
7387

88+
// Returns detection_ids where the latest kind:handled timestamp is on or after the latest
89+
// kind:detection OR kind:quiet timestamp, or where only handled docs exist.
90+
// Chunked to avoid oversized ES|QL IN clauses when many detections are on screen.
91+
private async getProcessedIds(detectionIds: string[]): Promise<Set<string>> {
92+
if (!detectionIds.length) return new Set();
93+
94+
const processed = new Set<string>();
95+
for (let i = 0; i < detectionIds.length; i += PROCESSED_IDS_CHUNK_SIZE) {
96+
const batch = detectionIds.slice(i, i + PROCESSED_IDS_CHUNK_SIZE);
97+
const idLiterals = batch.map((id) => esql.str(id));
98+
const kindState = [esql.str('detection'), esql.str(KIND_QUIET)];
99+
const allKinds = [...kindState, esql.str(KIND_HANDLED)];
100+
const query = esql`FROM ${DETECTIONS_DATA_STREAM}
101+
| WHERE kibana.space_ids == ${esql.str(this.clients.space)} OR kibana.space_ids IS NULL
102+
| WHERE kind IN (${allKinds})
103+
| WHERE detection_id IN (${idLiterals})
104+
| STATS max_state_ts = MAX(CASE(kind IN (${kindState}), @timestamp, null)),
105+
max_handled_ts = MAX(CASE(kind == ${esql.str(KIND_HANDLED)}, @timestamp, null))
106+
BY detection_id
107+
| WHERE max_handled_ts >= max_state_ts OR max_state_ts IS NULL
108+
| KEEP detection_id`;
109+
110+
const response = await queryEsql({ esClient: this.clients.esClient, query });
111+
const rows = esqlToObjects<{ detection_id?: string }>(response);
112+
for (const r of rows) {
113+
if (r.detection_id) processed.add(r.detection_id);
114+
}
115+
}
116+
return processed;
117+
}
118+
119+
private toDetection(raw: RawDetection, processed: boolean): Detection {
120+
return { ...raw, processed };
121+
}
122+
74123
async findLatest(options: DetectionsSearchOptions = {}): Promise<{ hits: Detection[] }> {
75-
return runLatestSourceEsqlQuery<Detection>({
124+
const result = await runLatestSourceEsqlQuery<RawDetection>({
76125
esClient: this.clients.esClient,
77126
space: this.clients.space,
78127
options,
79128
index: DETECTIONS_DATA_STREAM,
80129
where: this.buildWhere(options),
81130
groupBy: FIELD_DETECTION_ID,
82131
});
132+
const processedIds = await this.getProcessedIds(
133+
result.hits.map((h) => h.detection_id).filter((id): id is string => Boolean(id))
134+
);
135+
return {
136+
hits: result.hits.map((raw) =>
137+
this.toDetection(raw, processedIds.has(raw.detection_id ?? ''))
138+
),
139+
};
83140
}
84141

85142
async findLatestPaginated(
86143
options: DetectionsPaginatedSearchOptions = {}
87144
): Promise<PaginatedResponse<Detection>> {
88-
return runPaginatedLatestSourceEsqlQuery<Detection>({
145+
const result = await runPaginatedLatestSourceEsqlQuery<RawDetection>({
89146
esClient: this.clients.esClient,
90147
space: this.clients.space,
91148
options,
92149
index: DETECTIONS_DATA_STREAM,
93150
where: this.buildWhere(options),
94151
groupBy: FIELD_DETECTION_ID,
152+
sort: [['@timestamp', 'DESC']],
95153
});
96-
}
97154

98-
async findById(detectionId: string): Promise<{ hits: Detection[] }> {
99-
return runFindByIdEsqlQuery<Detection>({
100-
esClient: this.clients.esClient,
101-
space: this.clients.space,
102-
index: DETECTIONS_DATA_STREAM,
103-
idField: FIELD_DETECTION_ID,
104-
idValue: detectionId,
105-
});
155+
const processedIds = await this.getProcessedIds(
156+
result.hits.map((h) => h.detection_id).filter((id): id is string => Boolean(id))
157+
);
158+
return {
159+
...result,
160+
hits: result.hits.map((raw) =>
161+
this.toDetection(raw, processedIds.has(raw.detection_id ?? ''))
162+
),
163+
};
106164
}
107165
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@ export const discoveriesMappings = {
1919
kind: mappings.keyword(),
2020
discovery_id: mappings.keyword(),
2121
discovery_slug: mappings.keyword(),
22-
closes: mappings.keyword(),
23-
grouped_into: mappings.keyword(),
22+
closes_discovery_id: mappings.keyword(),
23+
grouped_discovery_ids: mappings.keyword(),
2424
criticality: mappings.integer(),
2525
closed_by_execution_id: mappings.keyword(),
26+
detections: mappings.object({
27+
properties: {
28+
rule_uuid: { type: 'keyword' as const },
29+
},
30+
}),
2631
},
2732
} satisfies MappingsDefinition;
2833

@@ -34,7 +39,7 @@ export const discoveriesDataStream: DataStreamDefinition<
3439
StoredDiscovery
3540
> = {
3641
name: DISCOVERIES_DATA_STREAM,
37-
version: 2,
42+
version: 3,
3843
hidden: true,
3944
template: {
4045
priority: 500,

0 commit comments

Comments
 (0)