From af34cc88a4f0d7ebcfd22701ebc52eac3727c832 Mon Sep 17 00:00:00 2001 From: Chenhui Wang <54903978+wangch079@users.noreply.github.com> Date: Mon, 15 Apr 2024 18:31:26 +0800 Subject: [PATCH 1/5] Collect top 5 errors for sync jobs (#180431) ## Summary This Task update the usage collector to collect the top 5 errors for sync jobs. ### Checklist Delete any items that are not applicable to this PR. - [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 ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/collect_connector_stats.ts | 15 ++ .../lib/collect_connector_stats_test_data.ts | 19 ++ .../types/connector_stats.ts | 1 + .../common/types/connector_stats.ts | 1 + .../server/collectors/connectors/telemetry.ts | 64 ++++++ .../common/types/connector_stats.ts | 1 + .../server/collectors/connectors/telemetry.ts | 64 ++++++ .../schema/xpack_plugins.json | 192 ++++++++++++++++++ 8 files changed, 357 insertions(+) diff --git a/packages/kbn-search-connectors/lib/collect_connector_stats.ts b/packages/kbn-search-connectors/lib/collect_connector_stats.ts index c2ad0fe17f3dd..80edd3b0ca3a9 100644 --- a/packages/kbn-search-connectors/lib/collect_connector_stats.ts +++ b/packages/kbn-search-connectors/lib/collect_connector_stats.ts @@ -362,6 +362,8 @@ function syncJobsStatsByState(syncJobs: ConnectorSyncJob[]): SyncJobStatsByState let idle = 0; let running = 0; let duration = 0; + const errors = new Map(); + let topErrors: string[] = []; for (const syncJob of syncJobs) { completed += syncJob.status === SyncStatus.COMPLETED ? 1 : 0; @@ -386,6 +388,18 @@ function syncJobsStatsByState(syncJobs: ConnectorSyncJob[]): SyncJobStatsByState duration += Math.floor((completedAt.getTime() - startedAt.getTime()) / 1000); } } + if (syncJob.status === SyncStatus.ERROR && syncJob.error) { + errors.set(syncJob.error, (errors.get(syncJob.error) ?? 0) + 1); + } + } + + if (errors.size <= 5) { + topErrors = [...errors.keys()]; + } else { + topErrors = [...errors.entries()] + .sort((a, b) => b[1] - a[1]) + .map((a) => a[0]) + .slice(0, 5); } return { @@ -399,5 +413,6 @@ function syncJobsStatsByState(syncJobs: ConnectorSyncJob[]): SyncJobStatsByState idle, running, totalDurationSeconds: duration, + topErrors, } as SyncJobStatsByState; } diff --git a/packages/kbn-search-connectors/lib/collect_connector_stats_test_data.ts b/packages/kbn-search-connectors/lib/collect_connector_stats_test_data.ts index 73614cfb66787..d510fe5ad5e66 100644 --- a/packages/kbn-search-connectors/lib/collect_connector_stats_test_data.ts +++ b/packages/kbn-search-connectors/lib/collect_connector_stats_test_data.ts @@ -183,6 +183,7 @@ export const spoIncrementalSyncJob: ConnectorSyncJob = { job_type: SyncJobType.INCREMENTAL, status: SyncStatus.ERROR, trigger_method: TriggerMethod.ON_DEMAND, + error: 'spo_incremental_error', connector: { id: spoConnector.id, configuration: { @@ -315,6 +316,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 220, + topErrors: ['spo_incremental_error'], }, accessControl: { total: 1, @@ -327,6 +329,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 20, + topErrors: [], }, full: { total: 1, @@ -339,6 +342,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 100, + topErrors: [], }, incremental: { total: 1, @@ -351,6 +355,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 100, + topErrors: ['spo_incremental_error'], }, }, last7Days: { @@ -365,6 +370,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 120, + topErrors: ['spo_incremental_error'], }, accessControl: { total: 1, @@ -377,6 +383,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 20, + topErrors: [], }, incremental: { total: 1, @@ -389,6 +396,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 100, + topErrors: ['spo_incremental_error'], }, }, }, @@ -406,6 +414,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 220, + topErrors: ['spo_incremental_error'], }, accessControl: { total: 1, @@ -418,6 +427,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 20, + topErrors: [], }, full: { total: 1, @@ -430,6 +440,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 100, + topErrors: [], }, incremental: { total: 1, @@ -442,6 +453,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 100, + topErrors: ['spo_incremental_error'], }, }, last7Days: { @@ -456,6 +468,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 120, + topErrors: ['spo_incremental_error'], }, accessControl: { total: 1, @@ -468,6 +481,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 20, + topErrors: [], }, incremental: { total: 1, @@ -480,6 +494,7 @@ export const expectedSpoConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 100, + topErrors: ['spo_incremental_error'], }, }, }, @@ -543,6 +558,7 @@ export const expectedMysqlConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 200, + topErrors: [], }, full: { total: 1, @@ -555,6 +571,7 @@ export const expectedMysqlConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 200, + topErrors: [], }, }, }, @@ -579,6 +596,7 @@ export const expectedDeletedConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 200, + topErrors: [], }, full: { total: 1, @@ -591,6 +609,7 @@ export const expectedDeletedConnectorStats: ConnectorStats = { idle: 0, running: 0, totalDurationSeconds: 200, + topErrors: [], }, }, }, diff --git a/packages/kbn-search-connectors/types/connector_stats.ts b/packages/kbn-search-connectors/types/connector_stats.ts index 7c72a4b669b6e..93401fd590801 100644 --- a/packages/kbn-search-connectors/types/connector_stats.ts +++ b/packages/kbn-search-connectors/types/connector_stats.ts @@ -121,4 +121,5 @@ export interface SyncJobStatsByState { idle: number; running: number; totalDurationSeconds: number; + topErrors: string[]; } diff --git a/x-pack/plugins/enterprise_search/common/types/connector_stats.ts b/x-pack/plugins/enterprise_search/common/types/connector_stats.ts index b48d75120634d..39fe79bf8dad6 100644 --- a/x-pack/plugins/enterprise_search/common/types/connector_stats.ts +++ b/x-pack/plugins/enterprise_search/common/types/connector_stats.ts @@ -118,4 +118,5 @@ export interface SyncJobStatsByState { idle: number; running: number; totalDurationSeconds: number; + topErrors: string[]; } diff --git a/x-pack/plugins/enterprise_search/server/collectors/connectors/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/connectors/telemetry.ts index 16c4feaa5626c..cad2a5cbcf43c 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/connectors/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/connectors/telemetry.ts @@ -130,6 +130,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, accessControl: { total: { type: 'long' }, @@ -142,6 +146,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, full: { total: { type: 'long' }, @@ -154,6 +162,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, incremental: { total: { type: 'long' }, @@ -166,6 +178,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, }, last7Days: { @@ -180,6 +196,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, accessControl: { total: { type: 'long' }, @@ -192,6 +212,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, full: { total: { type: 'long' }, @@ -204,6 +228,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, incremental: { total: { type: 'long' }, @@ -216,6 +244,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, }, }, @@ -233,6 +265,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, accessControl: { total: { type: 'long' }, @@ -245,6 +281,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, full: { total: { type: 'long' }, @@ -257,6 +297,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, incremental: { total: { type: 'long' }, @@ -269,6 +313,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, }, last7Days: { @@ -283,6 +331,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, accessControl: { total: { type: 'long' }, @@ -295,6 +347,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, full: { total: { type: 'long' }, @@ -307,6 +363,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, incremental: { total: { type: 'long' }, @@ -319,6 +379,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, }, }, diff --git a/x-pack/plugins/serverless_search/common/types/connector_stats.ts b/x-pack/plugins/serverless_search/common/types/connector_stats.ts index b48d75120634d..39fe79bf8dad6 100644 --- a/x-pack/plugins/serverless_search/common/types/connector_stats.ts +++ b/x-pack/plugins/serverless_search/common/types/connector_stats.ts @@ -118,4 +118,5 @@ export interface SyncJobStatsByState { idle: number; running: number; totalDurationSeconds: number; + topErrors: string[]; } diff --git a/x-pack/plugins/serverless_search/server/collectors/connectors/telemetry.ts b/x-pack/plugins/serverless_search/server/collectors/connectors/telemetry.ts index 7900265a4187c..0d03a79b9d2ee 100644 --- a/x-pack/plugins/serverless_search/server/collectors/connectors/telemetry.ts +++ b/x-pack/plugins/serverless_search/server/collectors/connectors/telemetry.ts @@ -130,6 +130,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, accessControl: { total: { type: 'long' }, @@ -142,6 +146,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, full: { total: { type: 'long' }, @@ -154,6 +162,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, incremental: { total: { type: 'long' }, @@ -166,6 +178,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, }, last7Days: { @@ -180,6 +196,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, accessControl: { total: { type: 'long' }, @@ -192,6 +212,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, full: { total: { type: 'long' }, @@ -204,6 +228,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, incremental: { total: { type: 'long' }, @@ -216,6 +244,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, }, }, @@ -233,6 +265,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, accessControl: { total: { type: 'long' }, @@ -245,6 +281,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, full: { total: { type: 'long' }, @@ -257,6 +297,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, incremental: { total: { type: 'long' }, @@ -269,6 +313,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, }, last7Days: { @@ -283,6 +331,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, accessControl: { total: { type: 'long' }, @@ -295,6 +347,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, full: { total: { type: 'long' }, @@ -307,6 +363,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, incremental: { total: { type: 'long' }, @@ -319,6 +379,10 @@ export const registerTelemetryUsageCollector = ( idle: { type: 'long' }, running: { type: 'long' }, totalDurationSeconds: { type: 'long' }, + topErrors: { + type: 'array', + items: { type: 'keyword' }, + }, }, }, }, diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 9143016851c50..7fbce312fde18 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -8114,6 +8114,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8148,6 +8154,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8182,6 +8194,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8216,6 +8234,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } } @@ -8254,6 +8278,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8288,6 +8318,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8322,6 +8358,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8356,6 +8398,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } } @@ -8401,6 +8449,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8435,6 +8489,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8469,6 +8529,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8503,6 +8569,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } } @@ -8541,6 +8613,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8575,6 +8653,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8609,6 +8693,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8643,6 +8733,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } } @@ -8903,6 +8999,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8937,6 +9039,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -8971,6 +9079,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -9005,6 +9119,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } } @@ -9043,6 +9163,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -9077,6 +9203,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -9111,6 +9243,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -9145,6 +9283,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } } @@ -9190,6 +9334,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -9224,6 +9374,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -9258,6 +9414,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -9292,6 +9454,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } } @@ -9330,6 +9498,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -9364,6 +9538,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -9398,6 +9578,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } }, @@ -9432,6 +9618,12 @@ }, "totalDurationSeconds": { "type": "long" + }, + "topErrors": { + "type": "array", + "items": { + "type": "keyword" + } } } } From 3670d5eafc018d87748265279507e4778f790006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Mon, 15 Apr 2024 12:40:06 +0200 Subject: [PATCH 2/5] [Search] Add ability to cancel syncs individually (#180739) ## Summary https://github.com/elastic/kibana/assets/1410658/2eda78f4-8f44-407d-8e45-e9447e49d3e1 Add ability to cancel syncs individually via connectors api. Add loading indicator for sync jobs table Add delete syncs confirmation modal. Add listeners for the syncs to trigger loading and refetching jobs with 2 sec delays. ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../sync_jobs/sync_job_cancel_modal.test.tsx | 50 +++++ .../sync_jobs/sync_job_cancel_modal.tsx | 58 ++++++ .../components/sync_jobs/sync_jobs_table.tsx | 48 ++++- .../lib/cancel_sync.test.ts | 32 +++ .../kbn-search-connectors/lib/cancel_sync.ts | 18 ++ packages/kbn-search-connectors/lib/index.ts | 1 + packages/kbn-search-connectors/tsconfig.json | 4 +- .../types/connectors_api.ts | 4 + .../utils/identify_exceptions.ts | 13 ++ .../utils/sync_status_to_text.test.ts | 33 +++- .../utils/sync_status_to_text.ts | 8 + .../common/types/error_codes.ts | 1 + .../connector/cancel_sync_api_logic.test.ts | 32 +++ .../api/connector/cancel_sync_api_logic.ts | 34 ++++ .../components/connector_detail/overview.tsx | 3 +- .../connector/cancel_syncs_logic.ts | 2 +- .../search_index/index_view_logic.ts | 2 + .../search_index/sync_jobs/sync_jobs.tsx | 43 +++- .../sync_jobs/sync_jobs_view_logic.test.ts | 5 + .../sync_jobs/sync_jobs_view_logic.ts | 183 ++++++++++++++++-- .../routes/enterprise_search/connectors.ts | 41 +++- 21 files changed, 578 insertions(+), 37 deletions(-) create mode 100644 packages/kbn-search-connectors/components/sync_jobs/sync_job_cancel_modal.test.tsx create mode 100644 packages/kbn-search-connectors/components/sync_jobs/sync_job_cancel_modal.tsx create mode 100644 packages/kbn-search-connectors/lib/cancel_sync.test.ts create mode 100644 packages/kbn-search-connectors/lib/cancel_sync.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/cancel_sync_api_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/cancel_sync_api_logic.ts diff --git a/packages/kbn-search-connectors/components/sync_jobs/sync_job_cancel_modal.test.tsx b/packages/kbn-search-connectors/components/sync_jobs/sync_job_cancel_modal.test.tsx new file mode 100644 index 0000000000000..12b78f2afa95c --- /dev/null +++ b/packages/kbn-search-connectors/components/sync_jobs/sync_job_cancel_modal.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { render, screen, fireEvent } from '@testing-library/react'; +import React from 'react'; +import { CancelSyncJobModal } from './sync_job_cancel_modal'; +import '@testing-library/jest-dom/extend-expect'; +import { I18nProvider } from '@kbn/i18n-react'; + +describe('CancelSyncJobModal', () => { + const mockSyncJobId = '123'; + const mockOnConfirmCb = jest.fn(); + const mockOnCancel = jest.fn(); + + beforeEach(() => { + render( + + + + ); + }); + + test('renders the sync job ID', () => { + const syncJobIdElement = screen.getByTestId('confirmModalBodyText'); + expect(syncJobIdElement).toHaveTextContent(`Sync job ID: ${mockSyncJobId}`); + }); + + test('calls onConfirmCb when confirm button is clicked', () => { + const confirmButton = screen.getByText('Confirm'); + fireEvent.click(confirmButton); + expect(mockOnConfirmCb).toHaveBeenCalledWith(mockSyncJobId); + }); + + test('calls onCancel when cancel button is clicked', () => { + const cancelButton = screen.getByTestId('confirmModalCancelButton'); + fireEvent.click(cancelButton); + expect(mockOnCancel).toHaveBeenCalled(); + }); +}); diff --git a/packages/kbn-search-connectors/components/sync_jobs/sync_job_cancel_modal.tsx b/packages/kbn-search-connectors/components/sync_jobs/sync_job_cancel_modal.tsx new file mode 100644 index 0000000000000..96f36987b3e0a --- /dev/null +++ b/packages/kbn-search-connectors/components/sync_jobs/sync_job_cancel_modal.tsx @@ -0,0 +1,58 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiConfirmModal, EuiText, EuiCode, EuiSpacer, EuiConfirmModalProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; + +export type CancelSyncModalProps = Omit & { + onConfirmCb: (syncJobId: string) => void; + syncJobId: string; + errorMessages?: string[]; +}; + +export const CancelSyncJobModal: React.FC = ({ + syncJobId, + onCancel, + onConfirmCb, + isLoading, +}) => { + return ( + onConfirmCb(syncJobId)} + cancelButtonText={i18n.translate('searchConnectors.syncJobs.cancelSyncModal.cancelButton', { + defaultMessage: 'Cancel', + })} + confirmButtonText={i18n.translate('searchConnectors.syncJobs.cancelSyncModal.confirmButton', { + defaultMessage: 'Confirm', + })} + buttonColor="danger" + confirmButtonDisabled={isLoading} + isLoading={isLoading} + > + + + + +   + {syncJobId} + + + ); +}; diff --git a/packages/kbn-search-connectors/components/sync_jobs/sync_jobs_table.tsx b/packages/kbn-search-connectors/components/sync_jobs/sync_jobs_table.tsx index 7426d7dad3dec..4d44f6e47fa5c 100644 --- a/packages/kbn-search-connectors/components/sync_jobs/sync_jobs_table.tsx +++ b/packages/kbn-search-connectors/components/sync_jobs/sync_jobs_table.tsx @@ -13,16 +13,18 @@ import { EuiBadge, EuiBasicTable, EuiBasicTableColumn, + EuiButtonIcon, Pagination, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ConnectorSyncJob, SyncJobType, SyncStatus } from '../..'; +import { ConnectorSyncJob, isSyncCancellable, SyncJobType, SyncStatus } from '../..'; import { syncJobTypeToText, syncStatusToColor, syncStatusToText } from '../..'; import { durationToText, getSyncJobDuration } from '../../utils/duration_to_text'; import { FormattedDateTime } from '../../utils/formatted_date_time'; import { SyncJobFlyout } from './sync_job_flyout'; +import { CancelSyncJobModal, CancelSyncModalProps } from './sync_job_cancel_modal'; interface SyncJobHistoryTableProps { isLoading?: boolean; @@ -30,6 +32,10 @@ interface SyncJobHistoryTableProps { pagination: Pagination; syncJobs: ConnectorSyncJob[]; type: 'content' | 'access_control'; + cancelConfirmModalProps?: Pick & { + syncJobIdToCancel?: ConnectorSyncJob['id']; + setSyncJobIdToCancel: (syncJobId: ConnectorSyncJob['id'] | undefined) => void; + }; } export const SyncJobsTable: React.FC = ({ @@ -38,6 +44,12 @@ export const SyncJobsTable: React.FC = ({ pagination, syncJobs, type, + cancelConfirmModalProps = { + onConfirmCb: () => {}, + isLoading: false, + setSyncJobIdToCancel: () => {}, + syncJobIdToCancel: undefined, + }, }) => { const [selectedSyncJob, setSelectedSyncJob] = useState(undefined); const columns: Array> = [ @@ -127,6 +139,33 @@ export const SyncJobsTable: React.FC = ({ onClick: (job) => setSelectedSyncJob(job), type: 'icon', }, + ...(cancelConfirmModalProps + ? [ + { + render: (job: ConnectorSyncJob) => { + return isSyncCancellable(job.status) ? ( + cancelConfirmModalProps.setSyncJobIdToCancel(job.id)} + aria-label={i18n.translate( + 'searchConnectors.index.syncJobs.actions.cancelSyncJob.caption', + { + defaultMessage: 'Cancel this sync job', + } + )} + > + {i18n.translate('searchConnectors.index.syncJobs.actions.deleteJob.caption', { + defaultMessage: 'Delete', + })} + + ) : ( + <> + ); + }, + }, + ] + : []), ], }, ]; @@ -136,6 +175,13 @@ export const SyncJobsTable: React.FC = ({ {Boolean(selectedSyncJob) && ( setSelectedSyncJob(undefined)} syncJob={selectedSyncJob} /> )} + {Boolean(cancelConfirmModalProps) && cancelConfirmModalProps?.syncJobIdToCancel && ( + cancelConfirmModalProps.setSyncJobIdToCancel(undefined)} + /> + )} { + const mockClient = { + transport: { + request: jest.fn(), + }, + }; + + it('should cancel a sync', async () => { + mockClient.transport.request.mockImplementation(() => ({ + success: true, + })); + + await expect(cancelSync(mockClient as unknown as ElasticsearchClient, '1234')).resolves.toEqual( + { success: true } + ); + expect(mockClient.transport.request).toHaveBeenCalledWith({ + method: 'PUT', + path: '/_connector/_sync_job/1234/_cancel', + }); + }); +}); diff --git a/packages/kbn-search-connectors/lib/cancel_sync.ts b/packages/kbn-search-connectors/lib/cancel_sync.ts new file mode 100644 index 0000000000000..b3f31bb8a41b4 --- /dev/null +++ b/packages/kbn-search-connectors/lib/cancel_sync.ts @@ -0,0 +1,18 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { ConnectorAPICancelSyncResponse } from '../types'; + +export const cancelSync = async (client: ElasticsearchClient, syncJobId: string) => { + const result = await client.transport.request({ + method: 'PUT', + path: `/_connector/_sync_job/${syncJobId}/_cancel`, + }); + return result; +}; diff --git a/packages/kbn-search-connectors/lib/index.ts b/packages/kbn-search-connectors/lib/index.ts index 80bd6c554c54c..ed2f10a7f9ea3 100644 --- a/packages/kbn-search-connectors/lib/index.ts +++ b/packages/kbn-search-connectors/lib/index.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +export * from './cancel_sync'; export * from './cancel_syncs'; export * from './collect_connector_stats'; export * from './create_connector'; diff --git a/packages/kbn-search-connectors/tsconfig.json b/packages/kbn-search-connectors/tsconfig.json index 732b92333947f..eb7decb3d1e00 100644 --- a/packages/kbn-search-connectors/tsconfig.json +++ b/packages/kbn-search-connectors/tsconfig.json @@ -5,7 +5,9 @@ "types": [ "jest", "node", - "react" + "react", + "@testing-library/jest-dom", + "@testing-library/react", ] }, "include": [ diff --git a/packages/kbn-search-connectors/types/connectors_api.ts b/packages/kbn-search-connectors/types/connectors_api.ts index 265aee65cc68c..8869847e34da5 100644 --- a/packages/kbn-search-connectors/types/connectors_api.ts +++ b/packages/kbn-search-connectors/types/connectors_api.ts @@ -22,3 +22,7 @@ export interface ConnectorsAPISyncJobResponse { export interface ConnectorSecretCreateResponse { id: string; } + +export interface ConnectorAPICancelSyncResponse { + success: boolean; +} diff --git a/packages/kbn-search-connectors/utils/identify_exceptions.ts b/packages/kbn-search-connectors/utils/identify_exceptions.ts index 0bc395710d8f5..ae825df6bced0 100644 --- a/packages/kbn-search-connectors/utils/identify_exceptions.ts +++ b/packages/kbn-search-connectors/utils/identify_exceptions.ts @@ -11,6 +11,10 @@ export interface ElasticsearchResponseError { body?: { error?: { type: string; + caused_by?: { + type?: string; + reason?: string; + }; }; }; statusCode?: number; @@ -48,3 +52,12 @@ export const isMissingAliasException = (error: ElasticsearchResponseError) => error.meta?.statusCode === 404 && typeof error.meta?.body?.error === 'string' && MISSING_ALIAS_ERROR.test(error.meta?.body?.error); + +export const isStatusTransitionException = (error: ElasticsearchResponseError) => { + return ( + error.meta?.statusCode === 400 && + error.meta?.body?.error?.type === 'status_exception' && + error.meta?.body.error?.caused_by?.type === + 'connector_sync_job_invalid_status_transition_exception' + ); +}; diff --git a/packages/kbn-search-connectors/utils/sync_status_to_text.test.ts b/packages/kbn-search-connectors/utils/sync_status_to_text.test.ts index 6c448ea241dd2..752c21e2672ec 100644 --- a/packages/kbn-search-connectors/utils/sync_status_to_text.test.ts +++ b/packages/kbn-search-connectors/utils/sync_status_to_text.test.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { SyncStatus } from '..'; - import { syncStatusToColor, syncStatusToText } from './sync_status_to_text'; +import { isSyncCancellable, SyncStatus } from '..'; describe('syncStatusToText', () => { it('should return correct value for completed', () => { @@ -57,3 +56,33 @@ describe('syncStatusToColor', () => { expect(syncStatusToColor(SyncStatus.SUSPENDED)).toEqual('warning'); }); }); + +describe('isSyncCancellable', () => { + it('should return true for in progress status', () => { + expect(isSyncCancellable(SyncStatus.IN_PROGRESS)).toBe(true); + }); + + it('should return true for pending status', () => { + expect(isSyncCancellable(SyncStatus.PENDING)).toBe(true); + }); + + it('should return true for suspended status', () => { + expect(isSyncCancellable(SyncStatus.SUSPENDED)).toBe(true); + }); + + it('should return false for canceling status', () => { + expect(isSyncCancellable(SyncStatus.CANCELING)).toBe(false); + }); + + it('should return false for completed status', () => { + expect(isSyncCancellable(SyncStatus.COMPLETED)).toBe(false); + }); + + it('should return false for error status', () => { + expect(isSyncCancellable(SyncStatus.ERROR)).toBe(false); + }); + + it('should return false for canceled status', () => { + expect(isSyncCancellable(SyncStatus.CANCELED)).toBe(false); + }); +}); diff --git a/packages/kbn-search-connectors/utils/sync_status_to_text.ts b/packages/kbn-search-connectors/utils/sync_status_to_text.ts index b00e873bd52e0..11491a02be43f 100644 --- a/packages/kbn-search-connectors/utils/sync_status_to_text.ts +++ b/packages/kbn-search-connectors/utils/sync_status_to_text.ts @@ -62,6 +62,14 @@ export function syncStatusToColor(status: SyncStatus): string { } } +export const isSyncCancellable = (syncStatus: SyncStatus): boolean => { + return ( + syncStatus === SyncStatus.IN_PROGRESS || + syncStatus === SyncStatus.PENDING || + syncStatus === SyncStatus.SUSPENDED + ); +}; + export const syncJobTypeToText = (syncType: SyncJobType): string => { switch (syncType) { case SyncJobType.FULL: diff --git a/x-pack/plugins/enterprise_search/common/types/error_codes.ts b/x-pack/plugins/enterprise_search/common/types/error_codes.ts index 5370cbab778b8..1fe2d557d15c9 100644 --- a/x-pack/plugins/enterprise_search/common/types/error_codes.ts +++ b/x-pack/plugins/enterprise_search/common/types/error_codes.ts @@ -25,6 +25,7 @@ export enum ErrorCode { SEARCH_APPLICATION_ALREADY_EXISTS = 'search_application_already_exists', SEARCH_APPLICATION_NAME_INVALID = 'search_application_name_invalid', SEARCH_APPLICATION_NOT_FOUND = 'search_application_not_found', + STATUS_TRANSITION_ERROR = 'status_transition_error', UNAUTHORIZED = 'unauthorized', UNCAUGHT_EXCEPTION = 'uncaught_exception', } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/cancel_sync_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/cancel_sync_api_logic.test.ts new file mode 100644 index 0000000000000..8b0e9ff6b17ae --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/cancel_sync_api_logic.test.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +// write tests that checks cancelSync API logic calls correct endpoint +import { mockHttpValues } from '../../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test-jest-helpers'; + +import { cancelSync } from './cancel_sync_api_logic'; + +describe('CancelSyncApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('cancelSync', () => { + it('calls correct api', async () => { + const promise = Promise.resolve({ success: true }); + http.put.mockReturnValue(promise); + const result = cancelSync({ syncJobId: 'syncJobId1' }); + await nextTick(); + expect(http.put).toHaveBeenCalledWith( + '/internal/enterprise_search/connectors/syncJobId1/cancel_sync' + ); + await expect(result).resolves.toEqual({ success: true }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/cancel_sync_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/cancel_sync_api_logic.ts new file mode 100644 index 0000000000000..21873bdf8958c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/cancel_sync_api_logic.ts @@ -0,0 +1,34 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../shared/http'; + +export interface CancelSyncApiArgs { + syncJobId: string; +} + +export interface CancelSyncApiResponse { + success: boolean; +} + +export const cancelSync = async ({ syncJobId }: CancelSyncApiArgs) => { + const route = `/internal/enterprise_search/connectors/${syncJobId}/cancel_sync`; + return await HttpLogic.values.http.put(route); +}; + +export const CancelSyncApiLogic = createApiLogic(['cancel_sync_api_logic'], cancelSync, { + showErrorFlash: true, + showSuccessFlashFn: () => + i18n.translate('xpack.enterpriseSearch.content.searchIndex.cancelSync.successMessage', { + defaultMessage: 'Successfully canceled sync', + }), +}); + +export type CancelSyncApiActions = Actions; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/overview.tsx index 123a19cefff37..f8a9b8f12bcb0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/overview.tsx @@ -35,8 +35,7 @@ import { ConnectorViewLogic } from './connector_view_logic'; export const ConnectorDetailOverview: React.FC = () => { const { indexData } = useValues(IndexViewLogic); - const { connector } = useValues(ConnectorViewLogic); - const error = null; + const { connector, error } = useValues(ConnectorViewLogic); const { isCloud } = useValues(KibanaLogic); const { showModal } = useActions(ConvertConnectorLogic); const { isModalVisible } = useValues(ConvertConnectorLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/cancel_syncs_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/cancel_syncs_logic.ts index 43ae9d81a7336..055ace3cbc013 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/cancel_syncs_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/cancel_syncs_logic.ts @@ -22,7 +22,7 @@ interface CancelSyncsLogicValues { isConnectorIndex: boolean; } -interface CancelSyncsLogicActions { +export interface CancelSyncsLogicActions { cancelSyncs: () => void; cancelSyncsApiError: CancelSyncsApiActions['apiError']; cancelSyncsApiSuccess: CancelSyncsApiActions['apiSuccess']; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_view_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_view_logic.ts index 0e0b5405eda09..57f6d8f11bfce 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_view_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_view_logic.ts @@ -240,6 +240,8 @@ export const IndexViewLogic = kea false, + startAccessControlSync: () => true, + startIncrementalSync: () => true, startSyncApiSuccess: () => true, }, ], diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx index b21b312730bc2..2013d601df4aa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; -import { type } from 'io-ts'; import { useActions, useValues } from 'kea'; import { EuiButtonGroup } from '@elastic/eui'; @@ -25,11 +24,19 @@ import { SyncJobsViewLogic } from './sync_jobs_view_logic'; export const SyncJobs: React.FC = () => { const { hasDocumentLevelSecurityFeature } = useValues(IndexViewLogic); const { productFeatures } = useValues(KibanaLogic); - const [selectedSyncJobCategory, setSelectedSyncJobCategory] = useState('content'); const shouldShowAccessSyncs = productFeatures.hasDocumentLevelSecurityEnabled && hasDocumentLevelSecurityFeature; - const { connectorId, syncJobsPagination: pagination, syncJobs } = useValues(SyncJobsViewLogic); - const { fetchSyncJobs } = useActions(SyncJobsViewLogic); + const { + connectorId, + syncJobsPagination: pagination, + syncJobs, + cancelSyncJobLoading, + syncJobToCancel, + selectedSyncJobCategory, + syncTriggeredLocally, + } = useValues(SyncJobsViewLogic); + const { fetchSyncJobs, cancelSyncJob, setCancelSyncJob, setSelectedSyncJobCategory } = + useActions(SyncJobsViewLogic); useEffect(() => { if (connectorId) { @@ -37,10 +44,10 @@ export const SyncJobs: React.FC = () => { connectorId, from: pagination.pageIndex * (pagination.pageSize || 0), size: pagination.pageSize ?? 10, - type: selectedSyncJobCategory as 'access_control' | 'content', + type: selectedSyncJobCategory, }); } - }, [connectorId, selectedSyncJobCategory, type]); + }, [connectorId, selectedSyncJobCategory]); return ( <> @@ -56,7 +63,9 @@ export const SyncJobs: React.FC = () => { )} idSelected={selectedSyncJobCategory} onChange={(optionId) => { - setSelectedSyncJobCategory(optionId); + if (optionId === 'content' || optionId === 'access_control') { + setSelectedSyncJobCategory(optionId); + } }} options={[ { @@ -79,6 +88,7 @@ export const SyncJobs: React.FC = () => { )} {selectedSyncJobCategory === 'content' ? ( { if (connectorId) { fetchSyncJobs({ @@ -92,9 +102,18 @@ export const SyncJobs: React.FC = () => { pagination={pagination} syncJobs={syncJobs} type="content" + cancelConfirmModalProps={{ + isLoading: cancelSyncJobLoading, + onConfirmCb: (syncJobId: string) => { + cancelSyncJob({ syncJobId }); + }, + setSyncJobIdToCancel: setCancelSyncJob, + syncJobIdToCancel: syncJobToCancel ?? undefined, + }} /> ) : ( { if (connectorId) { fetchSyncJobs({ @@ -105,6 +124,14 @@ export const SyncJobs: React.FC = () => { }); } }} + cancelConfirmModalProps={{ + isLoading: cancelSyncJobLoading, + onConfirmCb: (syncJobId: string) => { + cancelSyncJob({ syncJobId }); + }, + setSyncJobIdToCancel: setCancelSyncJob, + syncJobIdToCancel: syncJobToCancel ?? undefined, + }} pagination={pagination} syncJobs={syncJobs} type="access_control" diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.test.ts index d1c3b81e73b3d..08275a7e2c49f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.test.ts @@ -23,7 +23,11 @@ import { SyncJobView, SyncJobsViewLogic } from './sync_jobs_view_logic'; // We can't test fetchTimeOutId because this will get set whenever the logic is created // And the timeoutId is non-deterministic. We use expect.object.containing throughout this test file const DEFAULT_VALUES = { + cancelSyncJobLoading: false, + cancelSyncJobStatus: Status.IDLE, connectorId: null, + selectedSyncJobCategory: 'content', + syncJobToCancel: null, syncJobs: [], syncJobsData: undefined, syncJobsLoading: true, @@ -33,6 +37,7 @@ const DEFAULT_VALUES = { totalItemCount: 0, }, syncJobsStatus: Status.IDLE, + syncTriggeredLocally: false, }; describe('SyncJobsViewLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.ts index b820cfebaf8f9..e0a6176e38bff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs_view_logic.ts @@ -7,6 +7,7 @@ import { kea, MakeLogicType } from 'kea'; +import { isEqual } from 'lodash'; import moment from 'moment'; import { Pagination } from '@elastic/eui'; @@ -16,35 +17,68 @@ import { Status } from '../../../../../../common/types/api'; import { Paginate } from '../../../../../../common/types/pagination'; import { Actions } from '../../../../shared/api_logic/create_api_logic'; +import { + CancelSyncApiActions, + CancelSyncApiLogic, +} from '../../../api/connector/cancel_sync_api_logic'; import { FetchSyncJobsApiLogic, FetchSyncJobsArgs, FetchSyncJobsResponse, } from '../../../api/connector/fetch_sync_jobs_api_logic'; -import { IndexViewLogic } from '../index_view_logic'; +import { CancelSyncsLogic, CancelSyncsLogicActions } from '../connector/cancel_syncs_logic'; +import { IndexViewActions, IndexViewLogic } from '../index_view_logic'; + +const UI_REFRESH_INTERVAL = 2000; export interface SyncJobView extends ConnectorSyncJob { - duration: moment.Duration; - lastSync: string; + duration: moment.Duration | undefined; + lastSync: string | null; } -export interface IndexViewActions { +export interface SyncJobsViewActions { + cancelSyncError: CancelSyncApiActions['apiError']; + cancelSyncJob: CancelSyncApiActions['makeRequest']; + cancelSyncSuccess: CancelSyncApiActions['apiSuccess']; + cancelSyncsApiError: CancelSyncsLogicActions['cancelSyncsApiError']; + cancelSyncsApiSuccess: CancelSyncsLogicActions['cancelSyncsApiSuccess']; fetchSyncJobs: Actions['makeRequest']; + fetchSyncJobsApiSuccess: Actions['apiSuccess']; fetchSyncJobsError: Actions['apiError']; + refetchSyncJobs: () => void; + resetCancelSyncJobApi: CancelSyncApiActions['apiReset']; + setCancelSyncJob: (syncJobId: ConnectorSyncJob['id'] | undefined) => { + syncJobId: ConnectorSyncJob['id'] | null; + }; + setSelectedSyncJobCategory: (category: 'content' | 'access_control') => { + category: 'content' | 'access_control'; + }; + startAccessControlSync: IndexViewActions['startAccessControlSync']; + startIncrementalSync: IndexViewActions['startIncrementalSync']; + startSync: IndexViewActions['startSync']; } -export interface IndexViewValues { +export interface SyncJobsViewValues { + cancelSyncJobLoading: boolean; + cancelSyncJobStatus: Status; connectorId: string | null; + selectedSyncJobCategory: 'content' | 'access_control'; + syncJobToCancel: ConnectorSyncJob['id'] | null; syncJobs: SyncJobView[]; syncJobsData: Paginate | null; syncJobsLoading: boolean; syncJobsPagination: Pagination; syncJobsStatus: Status; + syncTriggeredLocally: boolean; } -export const SyncJobsViewLogic = kea>({ - actions: {}, +export const SyncJobsViewLogic = kea>({ + actions: { + refetchSyncJobs: true, + setCancelSyncJob: (syncJobId) => ({ syncJobId: syncJobId ?? null }), + setSelectedSyncJobCategory: (category) => ({ category }), + }, connect: { actions: [ FetchSyncJobsApiLogic, @@ -54,30 +88,137 @@ export const SyncJobsViewLogic = kea ({ + cancelSyncError: async (_, breakpoint) => { + actions.resetCancelSyncJobApi(); + await breakpoint(UI_REFRESH_INTERVAL); + if (values.connectorId) { + actions.refetchSyncJobs(); + } + }, + cancelSyncSuccess: async (_, breakpoint) => { + actions.resetCancelSyncJobApi(); + await breakpoint(UI_REFRESH_INTERVAL); + if (values.connectorId) { + actions.refetchSyncJobs(); + } + }, + cancelSyncsApiError: async (_, breakpoint) => { + await breakpoint(UI_REFRESH_INTERVAL); + if (values.connectorId) { + actions.refetchSyncJobs(); + } + }, + cancelSyncsApiSuccess: async (_, breakpoint) => { + await breakpoint(UI_REFRESH_INTERVAL); + if (values.connectorId) { + actions.refetchSyncJobs(); + } + }, + refetchSyncJobs: () => { + if (values.connectorId) { + actions.fetchSyncJobs({ + connectorId: values.connectorId, + from: values.syncJobsPagination.pageIndex * (values.syncJobsPagination.pageSize || 0), + size: values.syncJobsPagination.pageSize ?? 10, + type: values.selectedSyncJobCategory, + }); + } + }, + startAccessControlSync: async (_, breakpoint) => { + await breakpoint(UI_REFRESH_INTERVAL); + if (values.connectorId) { + actions.refetchSyncJobs(); + } + }, + startIncrementalSync: async (_, breakpoint) => { + await breakpoint(UI_REFRESH_INTERVAL); + if (values.connectorId) { + actions.refetchSyncJobs(); + } + }, + startSync: async (_, breakpoint) => { + await breakpoint(UI_REFRESH_INTERVAL); + if (values.connectorId) { + actions.refetchSyncJobs(); + } + }, + }), + path: ['enterprise_search', 'content', 'sync_jobs_view_logic'], - selectors: ({ selectors }) => ({ + reducers: { + selectedSyncJobCategory: [ + 'content', + { + setSelectedSyncJobCategory: (_, { category }) => category, + }, + ], + syncJobToCancel: [ + null, + { + resetCancelSyncJobApi: () => null, + setCancelSyncJob: (_, { syncJobId }) => syncJobId ?? null, + }, + ], syncJobs: [ - () => [selectors.syncJobsData], - (data?: Paginate) => - data?.data.map((syncJob) => { - return { - ...syncJob, - duration: syncJob.started_at - ? moment.duration( - moment(syncJob.completed_at || new Date()).diff(moment(syncJob.started_at)) - ) - : undefined, - lastSync: syncJob.completed_at, - }; - }) ?? [], + [], + { + fetchSyncJobsApiSuccess: (currentState, { data }) => { + const newState = + data?.map((syncJob) => { + return { + ...syncJob, + duration: syncJob.started_at + ? moment.duration( + moment(syncJob.completed_at || new Date()).diff(moment(syncJob.started_at)) + ) + : undefined, + lastSync: syncJob.completed_at, + }; + }) ?? []; + + return isEqual(currentState, newState) ? currentState : newState; + }, + }, + ], + syncTriggeredLocally: [ + false, + { + cancelSyncError: () => true, + cancelSyncJob: () => true, + cancelSyncs: () => true, + fetchSyncJobsApiSuccess: () => false, + startAccessControlSync: () => true, + startIncrementalSync: () => true, + startSync: () => true, + }, + ], + }, + selectors: ({ selectors }) => ({ + cancelSyncJobLoading: [ + () => [selectors.cancelSyncJobStatus], + (status: Status) => status === Status.LOADING, ], syncJobsLoading: [ () => [selectors.syncJobsStatus], diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts index 7ad04cf753dd7..395c114424864 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts @@ -6,11 +6,13 @@ */ import { schema } from '@kbn/config-schema'; + import { ElasticsearchErrorDetails } from '@kbn/es-errors'; import { i18n } from '@kbn/i18n'; import { CONNECTORS_INDEX, + cancelSync, deleteConnectorById, deleteConnectorSecret, fetchConnectorById, @@ -28,7 +30,10 @@ import { import { ConnectorStatus, FilteringRule, SyncJobType } from '@kbn/search-connectors'; import { cancelSyncs } from '@kbn/search-connectors/lib/cancel_syncs'; -import { isResourceNotFoundException } from '@kbn/search-connectors/utils/identify_exceptions'; +import { + isResourceNotFoundException, + isStatusTransitionException, +} from '@kbn/search-connectors/utils/identify_exceptions'; import { ErrorCode } from '../../../common/types/error_codes'; import { addConnector } from '../../lib/connectors/add_connector'; @@ -117,6 +122,40 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) { }) ); + router.put( + { + path: '/internal/enterprise_search/connectors/{syncJobId}/cancel_sync', + validate: { + params: schema.object({ + syncJobId: schema.string(), + }), + }, + }, + elasticsearchErrorHandler(log, async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + try { + await cancelSync(client.asCurrentUser, request.params.syncJobId); + } catch (error) { + if (isStatusTransitionException(error)) { + return createError({ + errorCode: ErrorCode.STATUS_TRANSITION_ERROR, + message: i18n.translate( + 'xpack.enterpriseSearch.server.routes.connectors.statusTransitionError', + { + defaultMessage: + 'Connector sync job cannot be cancelled. Connector is already cancelled or not in a cancelable state.', + } + ), + response, + statusCode: 400, + }); + } + throw error; + } + return response.ok(); + }) + ); + router.post( { path: '/internal/enterprise_search/connectors/{connectorId}/configuration', From 382be7ea3fea684c8a0a95dd9d83d621cf69a95d Mon Sep 17 00:00:00 2001 From: Bena Kansara <69037875+benakansara@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:31:34 +0200 Subject: [PATCH 3/5] [Observability rules] Add baseline alert detail pages (#180256) Closes https://github.com/elastic/kibana/issues/179338 Added baseline alert details page for: - Inventory rule - Metric threshold rule - Error count threshold rule - Failed transaction rate threshold rule - APM Anomaly rule - Elasticsearch query rule - Anomaly detection rule Example of Elasticsearch query alert details page: Screenshot 2024-04-08 at 23 35 16 --- .../alert_overview.tsx} | 73 ++++++++++++++++--- .../helpers/format_cases.ts | 0 .../helpers/get_sources.ts | 6 +- .../helpers/is_fields_same_type.ts | 0 .../map_rules_params_with_flyout.test.ts | 12 +-- .../helpers/map_rules_params_with_flyout.ts | 16 ++-- .../overview_columns.tsx | 6 +- .../alerts_flyout/alerts_flyout_body.tsx | 61 +--------------- .../alerts_flyout/alerts_flyout_footer.tsx | 34 ++++----- .../alerts_table/common/render_cell_value.tsx | 2 +- .../alert_details/alert_details.test.tsx | 2 + .../pages/alert_details/alert_details.tsx | 59 +++++++++------ .../alerts/components/alert_actions.test.tsx | 12 ++- .../pages/alerts/components/alert_actions.tsx | 5 +- .../services/observability/alerts/common.ts | 11 ++- .../observability/pages/alert_details_page.ts | 41 ++++++----- 16 files changed, 181 insertions(+), 159 deletions(-) rename x-pack/plugins/observability_solution/observability/public/components/{alerts_flyout/alert_flyout_overview/alerts_flyout_overview.tsx => alert_overview/alert_overview.tsx} (68%) rename x-pack/plugins/observability_solution/observability/public/components/{alerts_flyout/alert_flyout_overview => alert_overview}/helpers/format_cases.ts (100%) rename x-pack/plugins/observability_solution/observability/public/components/{alerts_flyout/alert_flyout_overview => alert_overview}/helpers/get_sources.ts (87%) rename x-pack/plugins/observability_solution/observability/public/components/{alerts_flyout/alert_flyout_overview => alert_overview}/helpers/is_fields_same_type.ts (100%) rename x-pack/plugins/observability_solution/observability/public/components/{alerts_flyout/alert_flyout_overview => alert_overview}/helpers/map_rules_params_with_flyout.test.ts (96%) rename x-pack/plugins/observability_solution/observability/public/components/{alerts_flyout/alert_flyout_overview => alert_overview}/helpers/map_rules_params_with_flyout.ts (92%) rename x-pack/plugins/observability_solution/observability/public/components/{alerts_flyout/alert_flyout_overview => alert_overview}/overview_columns.tsx (95%) diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/alerts_flyout_overview.tsx b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/alert_overview.tsx similarity index 68% rename from x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/alerts_flyout_overview.tsx rename to x-pack/plugins/observability_solution/observability/public/components/alert_overview/alert_overview.tsx index 126400187c8ca..a4cc5b30c3d53 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/alerts_flyout_overview.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/alert_overview.tsx @@ -6,7 +6,14 @@ */ import React, { memo, useEffect, useMemo, useState } from 'react'; -import { EuiInMemoryTable } from '@elastic/eui'; +import { + EuiTitle, + EuiSpacer, + EuiText, + EuiLink, + EuiHorizontalRule, + EuiInMemoryTable, +} from '@elastic/eui'; import { ALERT_CASE_IDS, ALERT_DURATION, @@ -24,27 +31,38 @@ import { useUiSetting } from '@kbn/kibana-react-plugin/public'; import { i18n } from '@kbn/i18n'; import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; -import { paths } from '../../../../common/locators/paths'; -import { TimeRange } from '../../../../common/custom_threshold_rule/types'; -import { TopAlert } from '../../../typings/alerts'; -import { useFetchBulkCases } from '../../../hooks/use_fetch_bulk_cases'; -import { useCaseViewNavigation } from '../../../hooks/use_case_view_navigation'; -import { useKibana } from '../../../utils/kibana_react'; +import { get } from 'lodash'; +import { paths } from '../../../common/locators/paths'; +import { TimeRange } from '../../../common/custom_threshold_rule/types'; +import { TopAlert } from '../../typings/alerts'; +import { useFetchBulkCases } from '../../hooks/use_fetch_bulk_cases'; +import { useCaseViewNavigation } from '../../hooks/use_case_view_navigation'; +import { useKibana } from '../../utils/kibana_react'; import { FlyoutThresholdData, mapRuleParamsWithFlyout, } from './helpers/map_rules_params_with_flyout'; import { ColumnIDs, overviewColumns } from './overview_columns'; import { getSources } from './helpers/get_sources'; +import { RULE_DETAILS_PAGE_ID } from '../../pages/rule_details/constants'; -export const Overview = memo(({ alert }: { alert: TopAlert }) => { - const { http } = useKibana().services; +export const AlertOverview = memo(({ alert, pageId }: { alert: TopAlert; pageId?: string }) => { + const { + http: { + basePath: { prepend }, + }, + } = useKibana().services; const { cases, isLoading } = useFetchBulkCases({ ids: alert.fields[ALERT_CASE_IDS] || [] }); const dateFormat = useUiSetting('dateFormat'); const [timeRange, setTimeRange] = useState({ from: 'now-15m', to: 'now' }); const [ruleCriteria, setRuleCriteria] = useState([]); const alertStart = alert.fields[ALERT_START]; const alertEnd = alert.fields[ALERT_END]; + const ruleId = get(alert.fields, ALERT_RULE_UUID) ?? null; + const linkToRule = + pageId !== RULE_DETAILS_PAGE_ID && ruleId + ? prepend(paths.observability.ruleDetails(ruleId)) + : null; useEffect(() => { const mappedRuleParams = mapRuleParamsWithFlyout(alert); @@ -126,7 +144,7 @@ export const Overview = memo(({ alert }: { alert: TopAlert }) => { meta: { ruleLink: alert.fields[ALERT_RULE_UUID] && - http.basePath.prepend(paths.observability.ruleDetails(alert.fields[ALERT_RULE_UUID])), + prepend(paths.observability.ruleDetails(alert.fields[ALERT_RULE_UUID])), }, }, { @@ -154,11 +172,42 @@ export const Overview = memo(({ alert }: { alert: TopAlert }) => { alertEnd, cases, dateFormat, - http.basePath, + prepend, isLoading, navigateToCaseView, ruleCriteria, timeRange, ]); - return ; + + return ( + <> + +

+ {i18n.translate('xpack.observability.alertsFlyout.reasonTitle', { + defaultMessage: 'Reason', + })} +

+
+ + {alert.reason} + + {!!linkToRule && ( + + {i18n.translate('xpack.observability.alertsFlyout.viewRulesDetailsLinkText', { + defaultMessage: 'View rule details', + })} + + )} + + +

+ {i18n.translate('xpack.observability.alertsFlyout.documentSummaryTitle', { + defaultMessage: 'Document Summary', + })} +

+
+ + + + ); }); diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/format_cases.ts b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/format_cases.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/format_cases.ts rename to x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/format_cases.ts diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/get_sources.ts b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/get_sources.ts similarity index 87% rename from x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/get_sources.ts rename to x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/get_sources.ts index 8acdcbf7b3da9..cfc64d9d414aa 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/get_sources.ts +++ b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/get_sources.ts @@ -9,8 +9,8 @@ import { ALERT_GROUP_FIELD, ALERT_GROUP_VALUE } from '@kbn/rule-data-utils'; import { apmSources, infraSources, -} from '../../../../../common/custom_threshold_rule/helpers/get_alert_source_links'; -import { TopAlert } from '../../../..'; +} from '../../../../common/custom_threshold_rule/helpers/get_alert_source_links'; +import { TopAlert } from '../../..'; interface AlertFields { [key: string]: any; @@ -36,7 +36,7 @@ export const getSources = (alert: TopAlert) => { const fieldValue = alertFields[field]; matchedSources.push({ field: source, - value: fieldValue[0], + value: Array.isArray(fieldValue) ? fieldValue[0] : fieldValue, }); } }); diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/is_fields_same_type.ts b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/is_fields_same_type.ts similarity index 100% rename from x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/is_fields_same_type.ts rename to x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/is_fields_same_type.ts diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/map_rules_params_with_flyout.test.ts b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/map_rules_params_with_flyout.test.ts similarity index 96% rename from x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/map_rules_params_with_flyout.test.ts rename to x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/map_rules_params_with_flyout.test.ts index afe0c62042bda..706fce0c75a62 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/map_rules_params_with_flyout.test.ts +++ b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/map_rules_params_with_flyout.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { TopAlert } from '../../../../typings/alerts'; +import { TopAlert } from '../../../typings/alerts'; import { mapRuleParamsWithFlyout } from './map_rules_params_with_flyout'; describe('Map rules params with flyout', () => { @@ -149,7 +149,7 @@ describe('Map rules params with flyout', () => { observedValue: [4577], threshold: [100], comparator: 'more than', - pctAboveThreshold: ' (4477.00% above the threshold)', + pctAboveThreshold: ' (4477% above the threshold)', }, ], }, @@ -179,7 +179,7 @@ describe('Map rules params with flyout', () => { observedValue: '6%', threshold: '1%', comparator: '>', - pctAboveThreshold: ' (500.00% above the threshold)', + pctAboveThreshold: ' (500% above the threshold)', }, ], }, @@ -242,7 +242,7 @@ describe('Map rules params with flyout', () => { observedValue: [1], threshold: [1], comparator: '>', - pctAboveThreshold: ' (0.00% above the threshold)', + pctAboveThreshold: ' (0% above the threshold)', }, ], }, @@ -267,7 +267,7 @@ describe('Map rules params with flyout', () => { observedValue: ['23 s'], threshold: ['1.5 s'], comparator: '>', - pctAboveThreshold: ' (1424.80% above the threshold)', + pctAboveThreshold: ' (1424.8% above the threshold)', }, ], }, @@ -291,7 +291,7 @@ describe('Map rules params with flyout', () => { observedValue: ['25%'], threshold: ['1.0%'], comparator: '>', - pctAboveThreshold: ' (2400.00% above the threshold)', + pctAboveThreshold: ' (2400% above the threshold)', }, ], }, diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/map_rules_params_with_flyout.ts b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/map_rules_params_with_flyout.ts similarity index 92% rename from x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/map_rules_params_with_flyout.ts rename to x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/map_rules_params_with_flyout.ts index 9aa262936392a..f2f58e17ea56a 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/helpers/map_rules_params_with_flyout.ts +++ b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/helpers/map_rules_params_with_flyout.ts @@ -19,16 +19,16 @@ import { } from '@kbn/rule-data-utils'; import { EsQueryRuleParams } from '@kbn/stack-alerts-plugin/public/rule_types/es_query/types'; import { i18n } from '@kbn/i18n'; -import { asDuration, asPercent } from '../../../../../common'; -import { createFormatter } from '../../../../../common/custom_threshold_rule/formatters'; -import { metricValueFormatter } from '../../../../../common/custom_threshold_rule/metric_value_formatter'; -import { METRIC_FORMATTERS } from '../../../../../common/custom_threshold_rule/formatters/snapshot_metric_formats'; -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../pages/alert_details/alert_details'; +import { asDuration, asPercent } from '../../../../common'; +import { createFormatter } from '../../../../common/custom_threshold_rule/formatters'; +import { metricValueFormatter } from '../../../../common/custom_threshold_rule/metric_value_formatter'; +import { METRIC_FORMATTERS } from '../../../../common/custom_threshold_rule/formatters/snapshot_metric_formats'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../pages/alert_details/alert_details'; import { BaseMetricExpressionParams, CustomMetricExpressionParams, -} from '../../../../../common/custom_threshold_rule/types'; -import { TopAlert } from '../../../../typings/alerts'; +} from '../../../../common/custom_threshold_rule/types'; +import { TopAlert } from '../../../typings/alerts'; import { isFieldsSameType } from './is_fields_same_type'; export interface FlyoutThresholdData { observedValue: string; @@ -42,7 +42,7 @@ const getPctAboveThreshold = (observedValue?: number, threshold?: number[]): str return i18n.translate('xpack.observability.alertFlyout.overview.aboveThresholdLabel', { defaultMessage: ' ({pctValue}% above the threshold)', values: { - pctValue: (((observedValue - threshold[0]) * 100) / threshold[0]).toFixed(2), + pctValue: parseFloat((((observedValue - threshold[0]) * 100) / threshold[0]).toFixed(2)), }, }); }; diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/overview_columns.tsx b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/overview_columns.tsx similarity index 95% rename from x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/overview_columns.tsx rename to x-pack/plugins/observability_solution/observability/public/components/alert_overview/overview_columns.tsx index 7694c49dba8a6..32820cb164309 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alert_flyout_overview/overview_columns.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alert_overview/overview_columns.tsx @@ -13,9 +13,9 @@ import { AlertStatus } from '@kbn/rule-data-utils'; import moment from 'moment'; import React from 'react'; import { Tooltip as CaseTooltip } from '@kbn/cases-components'; -import type { Group } from '../../../../common/custom_threshold_rule/types'; -import { NavigateToCaseView } from '../../../hooks/use_case_view_navigation'; -import { Groups } from '../../custom_threshold/components/alert_details_app_section/groups'; +import type { Group } from '../../../common/custom_threshold_rule/types'; +import { NavigateToCaseView } from '../../hooks/use_case_view_navigation'; +import { Groups } from '../custom_threshold/components/alert_details_app_section/groups'; import { formatCase } from './helpers/format_cases'; import { FlyoutThresholdData } from './helpers/map_rules_params_with_flyout'; diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout_body.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout_body.tsx index 1b56f512b0daf..7022a7fe55e7e 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout_body.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout_body.tsx @@ -5,27 +5,12 @@ * 2.0. */ import React, { useCallback, useMemo, useState } from 'react'; -import { get } from 'lodash'; -import { - EuiHorizontalRule, - EuiLink, - EuiPanel, - EuiSpacer, - EuiTabbedContentTab, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { ALERT_RULE_UUID } from '@kbn/rule-data-utils'; +import { EuiPanel, EuiTabbedContentTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AlertFieldsTable, ScrollableFlyoutTabbedContent } from '@kbn/alerts-ui-shared'; import { AlertsTableFlyoutBaseProps } from '@kbn/triggers-actions-ui-plugin/public'; -import { useKibana } from '../../utils/kibana_react'; - -import { paths } from '../../../common/locators/paths'; - -import { RULE_DETAILS_PAGE_ID } from '../../pages/rule_details/constants'; import type { TopAlert } from '../../typings/alerts'; -import { Overview } from './alert_flyout_overview/alerts_flyout_overview'; +import { AlertOverview } from '../alert_overview/alert_overview'; interface FlyoutProps { rawAlert: AlertsTableFlyoutBaseProps['alert']; @@ -36,18 +21,6 @@ interface FlyoutProps { type TabId = 'overview' | 'table'; export function AlertsFlyoutBody({ alert, rawAlert, id: pageId }: FlyoutProps) { - const { - http: { - basePath: { prepend }, - }, - } = useKibana().services; - - const ruleId = get(alert.fields, ALERT_RULE_UUID) ?? null; - const linkToRule = - pageId !== RULE_DETAILS_PAGE_ID && ruleId && prepend - ? prepend(paths.observability.ruleDetails(ruleId)) - : null; - const overviewTab = useMemo(() => { return { id: 'overview', @@ -57,37 +30,11 @@ export function AlertsFlyoutBody({ alert, rawAlert, id: pageId }: FlyoutProps) { }), content: ( - -

- {i18n.translate('xpack.observability.alertsFlyout.reasonTitle', { - defaultMessage: 'Reason', - })} -

-
- - {alert.reason} - - {!!linkToRule && ( - - {i18n.translate('xpack.observability.alertsFlyout.viewRulesDetailsLinkText', { - defaultMessage: 'View rule details', - })} - - )} - - -

- {i18n.translate('xpack.observability.alertsFlyout.documentSummaryTitle', { - defaultMessage: 'Document Summary', - })} -

-
- - +
), }; - }, [alert, linkToRule]); + }, [alert, pageId]); const metadataTab = useMemo( () => ({ diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout_footer.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout_footer.tsx index 324a96845137a..2d91fb7a9faf6 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout_footer.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_flyout/alerts_flyout_footer.tsx @@ -9,8 +9,6 @@ import React, { useState, useEffect } from 'react'; import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../utils/kibana_react'; -import { usePluginContext } from '../../hooks/use_plugin_context'; -import { isAlertDetailsEnabledPerApp } from '../../utils/is_alert_details_enabled'; import { paths } from '../../../common/locators/paths'; import type { TopAlert } from '../../typings/alerts'; @@ -25,7 +23,6 @@ export function AlertsFlyoutFooter({ alert, isInApp }: FlyoutProps & { isInApp: basePath: { prepend }, }, } = useKibana().services; - const { config } = usePluginContext(); const [viewInAppUrl, setViewInAppUrl] = useState(); useEffect(() => { @@ -48,23 +45,20 @@ export function AlertsFlyoutFooter({ alert, isInApp }: FlyoutProps & { isInApp: )} - - {!isAlertDetailsEnabledPerApp(alert, config) ? null : ( - - - {i18n.translate('xpack.observability.alertsFlyout.alertsDetailsButtonText', { - defaultMessage: 'Alert details', - })} - - - )} + + + {i18n.translate('xpack.observability.alertsFlyout.alertsDetailsButtonText', { + defaultMessage: 'Alert details', + })} + + ); diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.tsx index d68b11115396e..5e4091a3080b0 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/common/render_cell_value.tsx @@ -113,7 +113,7 @@ export const getRenderCellValue = ({ return ( setFlyoutAlert && setFlyoutAlert(alert.fields[ALERT_UUID])} > {alert.reason} diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx index dbc084ee739bc..cbecbfa4530ce 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx @@ -16,6 +16,7 @@ import { waitFor } from '@testing-library/react'; import { Chance } from 'chance'; import React, { Fragment } from 'react'; import { useLocation, useParams } from 'react-router-dom'; +import { from } from 'rxjs'; import { useFetchAlertDetail } from '../../hooks/use_fetch_alert_detail'; import { ConfigSchema } from '../../plugin'; import { Subset } from '../../typings'; @@ -54,6 +55,7 @@ const mockKibana = () => { services: { ...kibanaStartMock.startContract(), cases: casesPluginMock.createStartContract(), + application: { currentAppId$: from('mockedApp') }, http: { basePath: { prepend: jest.fn(), diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx index 5781fc0c850d4..f9e49e670099b 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx @@ -38,12 +38,12 @@ import { PageTitle, pageTitleContent } from './components/page_title'; import { HeaderActions } from './components/header_actions'; import { AlertSummary, AlertSummaryField } from './components/alert_summary'; import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; -import PageNotFound from '../404'; import { getTimeZone } from '../../utils/get_time_zone'; import { isAlertDetailsEnabledPerApp } from '../../utils/is_alert_details_enabled'; import { observabilityFeatureId } from '../../../common'; import { paths } from '../../../common/locators/paths'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; +import { AlertOverview } from '../../components/alert_overview/alert_overview'; import { AlertDetailContextualInsights } from './alert_details_contextual_insights'; interface AlertDetailsPathParams { @@ -133,11 +133,6 @@ export function AlertDetails() { return ; } - // Redirect to the 404 page when the user hit the page url directly in the browser while the feature flag is off. - if (alertDetail && !isAlertDetailsEnabledPerApp(alertDetail.formatted, config)) { - return ; - } - if (!isLoading && !alertDetail) return ( @@ -167,27 +162,43 @@ export function AlertDetails() { const OVERVIEW_TAB_ID = 'overview'; const METADATA_TAB_ID = 'metadata'; - const overviewTab = ( - <> - - - - - - {AlertDetailsAppSection && rule && alertDetail?.formatted && ( - - )} - + const overviewTab = alertDetail ? ( + AlertDetailsAppSection && + /* + when feature flag is enabled, show alert details page with customized overview tab, + otherwise show default overview tab + */ + isAlertDetailsEnabledPerApp(alertDetail.formatted, config) ? ( + <> + + + + + {rule && alertDetail.formatted && ( + + )} + + ) : ( + + + + + + + ) + ) : ( + <> ); const metadataTab = alertDetail?.raw && ( - + + ); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.test.tsx index 1122f190f93a7..71f7a3db59ee7 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.test.tsx @@ -147,8 +147,6 @@ describe('ObservabilityActions component', () => { wrapper.find('[data-test-subj="alertsTableRowActionMore"]').hostNodes().simulate('click'); await waitFor(() => { expect(wrapper.find('[data-test-subj~="viewRuleDetails"]').hostNodes().length).toBe(0); - wrapper.update(); - expect(wrapper.find('[data-test-subj~="viewAlertDetailsFlyout"]').exists()).toBeTruthy(); }); }); @@ -157,7 +155,15 @@ describe('ObservabilityActions component', () => { wrapper.find('[data-test-subj="alertsTableRowActionMore"]').hostNodes().simulate('click'); await waitFor(() => { expect(wrapper.find('[data-test-subj~="viewRuleDetails"]').hostNodes().length).toBe(1); - expect(wrapper.find('[data-test-subj~="viewAlertDetailsFlyout"]').hostNodes().length).toBe(1); + }); + }); + + it('"View alert details" menu item should open alert details page', async () => { + const wrapper = await setup('nothing'); + wrapper.find('[data-test-subj="alertsTableRowActionMore"]').hostNodes().simulate('click'); + await waitFor(() => { + expect(wrapper.find('[data-test-subj~="viewAlertDetailsPage"]').hostNodes().length).toBe(1); + expect(wrapper.find('[data-test-subj~="viewAlertDetailsFlyout"]').exists()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.tsx index 987cb698ea0b5..07383d9781c79 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.tsx @@ -25,7 +25,6 @@ import { useRouteMatch } from 'react-router-dom'; import { SLO_ALERTS_TABLE_ID } from '@kbn/observability-shared-plugin/common'; import { RULE_DETAILS_PAGE_ID } from '../../rule_details/constants'; import { paths, SLO_DETAIL_PATH } from '../../../../common/locators/paths'; -import { isAlertDetailsEnabledPerApp } from '../../../utils/is_alert_details_enabled'; import { useKibana } from '../../../utils/kibana_react'; import { parseAlert } from '../helpers/parse_alert'; import { observabilityFeatureId, ObservabilityRuleTypeRegistry } from '../../..'; @@ -147,7 +146,7 @@ export function AlertActions({ triggersActionsUi.getAlertsTableDefaultAlertActions({ key: 'defaultRowActions', onActionExecuted: closeActionsPopover, - isAlertDetailsEnabled: isAlertDetailsEnabledPerApp(observabilityAlert, config), + isAlertDetailsEnabled: true, resolveRulePagePath: (ruleId, currentPageId) => currentPageId !== RULE_DETAILS_PAGE_ID ? paths.observability.ruleDetails(ruleId) : null, resolveAlertPagePath: (alertId, currentPageId) => @@ -156,7 +155,7 @@ export function AlertActions({ : null, ...customActionsProps, }), - [config, customActionsProps, observabilityAlert, triggersActionsUi] + [customActionsProps, triggersActionsUi] ); const actionsMenuItems = [ diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 39edf24a96317..dcac33b952e3e 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -159,9 +159,16 @@ export function ObservabilityAlertsCommonProvider({ }; // Flyout + const getReasonMessageLinkByIndex = async (index: number) => { + const reasonMessageLinks = await find.allByCssSelector( + '[data-test-subj="o11yGetRenderCellValueLink"]' + ); + return reasonMessageLinks[index] || null; + }; + const openAlertsFlyout = retryOnStale.wrap(async (index: number = 0) => { - await openActionsMenuForRow(index); - await testSubjects.click('viewAlertDetailsFlyout'); + const reasonMessageLink = await getReasonMessageLinkByIndex(index); + await reasonMessageLink.click(); await retry.waitFor( 'flyout open', async () => await testSubjects.exists(ALERTS_FLYOUT_SELECTOR, { timeout: 2500 }) diff --git a/x-pack/test/observability_functional/apps/observability/pages/alert_details_page.ts b/x-pack/test/observability_functional/apps/observability/pages/alert_details_page.ts index 7f2b386c7c736..b95861bf42bcc 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alert_details_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alert_details_page.ts @@ -13,7 +13,7 @@ export default ({ getService }: FtrProviderContext) => { const observability = getService('observability'); const retry = getService('retry'); - describe('Observability Alert Details page - Feature flag', function () { + describe('Observability Alert Details page', function () { this.tags('includeFirefox'); before(async () => { @@ -27,29 +27,36 @@ export default ({ getService }: FtrProviderContext) => { await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); }); - it('should show 404 page when the feature flag is disabled but the alert exists', async () => { - await observability.alerts.common.navigateToAlertDetails( - '4c87bd11-ff31-4a05-8a04-833e2da94858' - ); + it('should show error when the alert does not exist', async () => { + await observability.alerts.common.navigateToAlertDetails('deleted-alert-Id'); await retry.waitFor( - 'The 404 - Not found page to be visible', - async () => await testSubjects.exists('pageNotFound') + 'Error message to be visible', + async () => await testSubjects.exists('alertDetailsError') ); }); - // This test is will be removed after removing the feature flag. - // FLAKY for the same reason: https://github.com/elastic/kibana/issues/133799 - describe.skip('Alert Detail / Alert Flyout', () => { + + describe('Alert components', () => { before(async () => { await observability.alerts.common.navigateToTimeWithData(); }); - it('should open the flyout instead of the alerts details page when clicking on "View alert details" from the... (3 dots) button when the feature flag is disabled', async () => { - await observability.alerts.common.openAlertsFlyout(); - await observability.alerts.common.getAlertsFlyoutOrFail(); + + it('should show tabbed view', async () => { + await observability.alerts.common.navigateToAlertDetails( + '4c87bd11-ff31-4a05-8a04-833e2da94858' + ); + + await retry.waitFor( + 'Overview tab to be visible', + async () => await testSubjects.exists('overviewTab') + ); + + await retry.waitFor( + 'Metadata tab to be visible', + async () => await testSubjects.exists('metadataTab') + ); }); - /* TODO: Add more test cases regarding the feature flag for: - - alert details URL from the Action variable - - alert details button from the alert flyout. - */ + + /* TODO: Add more test cases */ }); }); }; From abb8f6bb31f32a8f1e96536f752e395d99803bc2 Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Mon, 15 Apr 2024 13:32:43 +0200 Subject: [PATCH 4/5] [Security Solution][Entity Analytics] Show contributions on risk explainability expanded tab (#179657) This PR adds the contribution scores used when calculating an entity's risk score. These can be viewed by opening an entity's details flyout and clicking on `View risk contributions` which will open the explainability tab. The new tab now shows the contribution from different contexts and individual alerts. Currently, there's only one context: Asset Criticality. The alert inputs shown are the top 10 (at most) alerts used as an input at the time of the scoring calculation. If the final score used more than 10 alerts, we display a message indicating the leftover sum contribution. This work also updates the server side in order to store the contribution of each individual alert in the risk document itself. We now query the document to retrieve the inputs and then fetch the respective alerts on opening the tab. Example: ![Screenshot 2024-04-05 at 14 34 24](https://github.com/elastic/kibana/assets/2423976/a4efed46-05cd-4e31-9345-f46472358544) ### How to test 1. Generate some alerts - you can use https://github.com/elastic/security-documents-generator 2. Enable risk scoring via `Security > Manage > Entity Risk Score` 3. Open the entity details flyout and click `View risk contributions` Make sure to enable Asset Criticality in `Stack Management > Advanced Settings` if you want to see the criticality contributions. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../entity_analytics/risk_engine/types.ts | 5 +- .../get_alerts_query_for_risk_score.test.ts | 105 -------- .../common/get_alerts_query_for_risk_score.ts | 69 ----- .../components/action_column.test.tsx | 6 +- .../components/action_column.tsx | 12 +- .../components/utility_bar.test.tsx | 75 +----- .../components/utility_bar.tsx | 150 ++++------- .../hooks/use_risk_input_actions.ts | 31 +-- .../use_risk_input_actions_panels.test.tsx | 6 +- .../hooks/use_risk_input_actions_panels.tsx | 16 +- .../entity_details_flyout/mocks/index.ts | 22 +- .../tabs/risk_inputs/risk_inputs.test.tsx | 24 +- .../tabs/risk_inputs/risk_inputs_tab.tsx | 243 ++++++++++++------ .../hooks/use_risk_contributing_alerts.ts | 74 ++++-- .../host_details_left/index.test.tsx | 11 +- .../risk_score/calculate_risk_scores.mock.ts | 38 +-- .../risk_score/calculate_risk_scores.ts | 39 +-- .../entity_analytics/risk_score/constants.ts | 5 + .../server/lib/entity_analytics/types.ts | 13 +- .../translations/translations/fr-FR.json | 6 - .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - .../risk_scores_new_complete_data/data.json | 30 +++ .../mappings.json | 12 + 24 files changed, 441 insertions(+), 563 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.test.ts delete mode 100644 x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.ts diff --git a/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/types.ts b/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/types.ts index d86ddf4625372..c496cdab418b5 100644 --- a/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/types.ts +++ b/x-pack/plugins/security_solution/common/entity_analytics/risk_engine/types.ts @@ -27,13 +27,14 @@ export interface InitRiskEngineResult { errors: string[]; } -export interface SimpleRiskInput { +export interface EntityRiskInput { id: string; index: string; category: RiskCategories; description: string; risk_score: string | number | undefined; timestamp: string | undefined; + contribution_score?: number; } export interface EcsRiskScore { @@ -48,7 +49,7 @@ export interface EcsRiskScore { }; } -export type RiskInputs = SimpleRiskInput[]; +export type RiskInputs = EntityRiskInput[]; /** * The API response object representing a risk score diff --git a/x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.test.ts b/x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.test.ts deleted file mode 100644 index 394d777b30fd7..0000000000000 --- a/x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 { getAlertsQueryForRiskScore } from './get_alerts_query_for_risk_score'; -import type { RiskStats } from '../../../common/search_strategy/security_solution/risk_score/all'; -import { RiskSeverity } from '../../../common/search_strategy/security_solution/risk_score/all'; - -const risk: RiskStats = { - calculated_level: RiskSeverity.critical, - calculated_score_norm: 70, - rule_risks: [], - multipliers: [], - '@timestamp': '', - id_field: '', - id_value: '', - calculated_score: 0, - category_1_score: 0, - category_1_count: 0, - category_2_score: 0, - category_2_count: 0, - notes: [], - inputs: [], -}; - -describe('getAlertsQueryForRiskScore', () => { - it('should return query from host risk score', () => { - expect( - getAlertsQueryForRiskScore({ - riskScore: { - host: { - name: 'host-1', - risk, - }, - '@timestamp': '2023-08-10T14:00:00.000Z', - }, - riskRangeStart: 'now-30d', - }) - ).toEqual({ - _source: false, - size: 1000, - fields: ['*'], - query: { - bool: { - filter: [ - { term: { 'host.name': 'host-1' } }, - { - range: { - '@timestamp': { gte: '2023-07-11T14:00:00.000Z', lte: '2023-08-10T14:00:00.000Z' }, - }, - }, - ], - }, - }, - }); - }); - - it('should return query from user risk score', () => { - expect( - getAlertsQueryForRiskScore({ - riskScore: { - user: { - name: 'user-1', - risk, - }, - '@timestamp': '2023-08-10T14:00:00.000Z', - }, - riskRangeStart: 'now-30d', - }) - ).toEqual({ - _source: false, - size: 1000, - fields: ['*'], - query: { - bool: { - filter: [ - { term: { 'user.name': 'user-1' } }, - { - range: { - '@timestamp': { gte: '2023-07-11T14:00:00.000Z', lte: '2023-08-10T14:00:00.000Z' }, - }, - }, - ], - }, - }, - }); - }); - - it('should return query with custom fields', () => { - const query = getAlertsQueryForRiskScore({ - riskScore: { - user: { - name: 'user-1', - risk, - }, - '@timestamp': '2023-08-10T14:00:00.000Z', - }, - riskRangeStart: 'now-30d', - fields: ['event.category', 'event.action'], - }); - expect(query.fields).toEqual(['event.category', 'event.action']); - }); -}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.ts b/x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.ts deleted file mode 100644 index 8a75aa19a227d..0000000000000 --- a/x-pack/plugins/security_solution/public/entity_analytics/common/get_alerts_query_for_risk_score.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 { - isUserRiskScore, - RiskScoreFields, -} from '../../../common/search_strategy/security_solution/risk_score/all'; -import type { - UserRiskScore, - HostRiskScore, -} from '../../../common/search_strategy/security_solution/risk_score/all'; -import { getStartDateFromRiskScore } from './get_start_date_from_risk_score'; - -const ALERTS_SIZE = 1000; - -/** - * return query to fetch alerts related to the risk score - */ -export const getAlertsQueryForRiskScore = ({ - riskRangeStart, - riskScore, - fields, -}: { - riskRangeStart: string; - riskScore: UserRiskScore | HostRiskScore; - fields?: string[]; -}) => { - let entityField: string; - let entityValue: string; - - if (isUserRiskScore(riskScore)) { - entityField = RiskScoreFields.userName; - entityValue = riskScore.user.name; - } else { - entityField = RiskScoreFields.hostName; - entityValue = riskScore.host.name; - } - - const from = getStartDateFromRiskScore({ - riskScoreTimestamp: riskScore['@timestamp'], - riskRangeStart, - }); - - const riskScoreTimestamp = riskScore['@timestamp']; - - return { - fields: fields || ['*'], - size: ALERTS_SIZE, - _source: false, - query: { - bool: { - filter: [ - { term: { [entityField]: entityValue } }, - { - range: { - '@timestamp': { - gte: from, - lte: riskScoreTimestamp, - }, - }, - }, - ], - }, - }, - }; -}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/action_column.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/action_column.test.tsx index d5ff5425ccaa6..3aef24bd81c8c 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/action_column.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/action_column.test.tsx @@ -8,14 +8,14 @@ import { fireEvent, render } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../../common/mock'; -import { alertDataMock } from '../mocks'; +import { alertInputDataMock } from '../mocks'; import { ActionColumn } from './action_column'; describe('ActionColumn', () => { it('renders', () => { const { getByTestId } = render( - + ); @@ -25,7 +25,7 @@ describe('ActionColumn', () => { it('toggles the popover when button is clicked', () => { const { getByRole } = render( - + ); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/action_column.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/action_column.tsx index d993e66a562e9..838a51d066803 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/action_column.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/action_column.tsx @@ -7,20 +7,20 @@ import { EuiButtonIcon, EuiContextMenu, EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useMemo, useState } from 'react'; -import type { AlertRawData } from '../tabs/risk_inputs/risk_inputs_tab'; +import React, { useCallback, useState } from 'react'; +import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts'; + import { useRiskInputActionsPanels } from '../hooks/use_risk_input_actions_panels'; interface ActionColumnProps { - alert: AlertRawData; + input: InputAlert; } -export const ActionColumn: React.FC = ({ alert }) => { +export const ActionColumn: React.FC = ({ input }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const closePopover = useCallback(() => setIsPopoverOpen(false), []); const togglePopover = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), []); - const alerts = useMemo(() => [alert], [alert]); - const panels = useRiskInputActionsPanels(alerts, closePopover); + const panels = useRiskInputActionsPanels([input], closePopover); return ( { - it('renders', () => { + it('renders when at least one item is selected', () => { const { getByTestId } = render( - + ); - expect(getByTestId('risk-input-utility-bar')).toBeInTheDocument(); }); - it('renders current page message when totalItemCount is 1', () => { - const { getByTestId } = render( - - - - ); - - expect(getByTestId('risk-input-utility-bar')).toHaveTextContent('Showing 1 Risk contribution'); - }); - - it('renders current page message when totalItemCount is 20', () => { - const { getByTestId } = render( - - - - ); - - expect(getByTestId('risk-input-utility-bar')).toHaveTextContent( - 'Showing 1-10 of 20 Risk contribution' - ); - }); - - it('renders current page message when totalItemCount is 20 and on the second page', () => { - const { getByTestId } = render( + it('is hidden by default', () => { + const { queryByTestId } = render( - + ); - expect(getByTestId('risk-input-utility-bar')).toHaveTextContent( - 'Showing 11-20 of 20 Risk contribution' - ); + expect(queryByTestId('risk-input-utility-bar')).toBeNull(); }); it('renders selected risk input message', () => { const { getByTestId } = render( ); @@ -100,11 +47,7 @@ describe('RiskInputsUtilityBar', () => { const { getByRole } = render( ); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/utility_bar.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/utility_bar.tsx index 44837bfc2e0c1..e1131e131963a 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/utility_bar.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/components/utility_bar.tsx @@ -7,121 +7,79 @@ import type { FunctionComponent } from 'react'; import React, { useCallback, useState } from 'react'; -import type { Pagination } from '@elastic/eui'; + import { EuiButtonEmpty, EuiContextMenu, EuiFlexGroup, EuiFlexItem, EuiPopover, - EuiText, + EuiSpacer, useEuiTheme, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { useRiskInputActionsPanels } from '../hooks/use_risk_input_actions_panels'; -import type { AlertRawData } from '../tabs/risk_inputs/risk_inputs_tab'; +import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts'; interface Props { - selectedAlerts: AlertRawData[]; - pagination: Pagination; + riskInputs: InputAlert[]; } -export const RiskInputsUtilityBar: FunctionComponent = React.memo( - ({ selectedAlerts, pagination }) => { - const { euiTheme } = useEuiTheme(); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); - const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const panels = useRiskInputActionsPanels(selectedAlerts, closePopover); - const displayedCurrentPage = pagination.pageIndex + 1; - const pageSize = pagination.pageSize ?? 10; - const fromItem: number = pagination.pageIndex * pageSize + 1; - const toItem: number = Math.min(pagination.totalItemCount, pageSize * displayedCurrentPage); +export const RiskInputsUtilityBar: FunctionComponent = React.memo(({ riskInputs }) => { + const { euiTheme } = useEuiTheme(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + const panels = useRiskInputActionsPanels(riskInputs, closePopover); - return ( - <> - - - - {pagination.totalItemCount <= 1 ? ( - - - - ), - }} - /> - ) : ( + if (riskInputs.length === 0) { + return null; + } + return ( + <> + + + + {`${fromItem}-${toItem}`}, - totalContributions: pagination.totalItemCount, - riskContributions: ( - - - - ), + totalSelectedContributions: riskInputs.length, }} /> - )} - - - - {selectedAlerts.length > 0 && ( - - - - } - > - - - )} - - - - ); - } -); + + } + > + + + + + + + ); +}); RiskInputsUtilityBar.displayName = 'RiskInputsUtilityBar'; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions.ts index 87e78f7083add..926db73720aae 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions.ts @@ -11,16 +11,17 @@ import { get, noop } from 'lodash/fp'; import { AttachmentType } from '@kbn/cases-plugin/common'; import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public'; import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; + import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { useAddBulkToTimelineAction } from '../../../../detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline'; import { useKibana } from '../../../../common/lib/kibana/kibana_react'; -import type { AlertRawData } from '../tabs/risk_inputs/risk_inputs_tab'; +import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts'; /** * The returned actions only support alerts risk inputs. */ -export const useRiskInputActions = (alerts: AlertRawData[], closePopover: () => void) => { +export const useRiskInputActions = (inputs: InputAlert[], closePopover: () => void) => { const { from, to } = useGlobalTime(); const timelineAction = useAddBulkToTimelineAction({ localFilters: [], @@ -36,16 +37,16 @@ export const useRiskInputActions = (alerts: AlertRawData[], closePopover: () => const caseAttachments: CaseAttachmentsWithoutOwner = useMemo( () => - alerts.map((alert: AlertRawData) => ({ - alertId: alert._id, - index: alert._index, + inputs.map(({ input, alert }: InputAlert) => ({ + alertId: input.id, + index: input.index, type: AttachmentType.alert, rule: { - id: get(ALERT_RULE_UUID, alert.fields)[0], - name: get(ALERT_RULE_NAME, alert.fields)[0], + id: get(ALERT_RULE_UUID, alert), + name: get(ALERT_RULE_NAME, alert), }, })), - [alerts] + [inputs] ); return useMemo( @@ -61,19 +62,19 @@ export const useRiskInputActions = (alerts: AlertRawData[], closePopover: () => addToNewTimeline: () => { telemetry.reportAddRiskInputToTimelineClicked({ - quantity: alerts.length, + quantity: inputs.length, }); closePopover(); timelineAction.onClick( - alerts.map((alert: AlertRawData) => { + inputs.map(({ input }: InputAlert) => { return { - _id: alert._id, - _index: alert._index, + _id: input.id, + _index: input.index, data: [], ecs: { - _id: alert._id, - _index: alert._index, + _id: input.id, + _index: input.index, }, }; }), @@ -85,7 +86,7 @@ export const useRiskInputActions = (alerts: AlertRawData[], closePopover: () => }, }), [ - alerts, + inputs, caseAttachments, closePopover, createCaseFlyout, diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.test.tsx index 49bad231f2767..363edc4df9b1d 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.test.tsx @@ -12,7 +12,7 @@ import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { TestProviders } from '../../../../common/mock'; -import { alertDataMock } from '../mocks'; +import { alertInputDataMock } from '../mocks'; import { useRiskInputActionsPanels } from './use_risk_input_actions_panels'; const casesServiceMock = casesPluginMock.createStartContract(); @@ -47,7 +47,7 @@ const TestMenu = ({ panels }: { panels: EuiContextMenuPanelDescriptor[] }) => ( ); -const customRender = (alerts = [alertDataMock]) => { +const customRender = (alerts = [alertInputDataMock]) => { const { result } = renderHook(() => useRiskInputActionsPanels(alerts, () => {}), { wrapper: TestProviders, }); @@ -67,7 +67,7 @@ describe('useRiskInputActionsPanels', () => { }); it('displays number of selected alerts when more than one alert is selected', () => { - const { getByTestId } = customRender([alertDataMock, alertDataMock]); + const { getByTestId } = customRender([alertInputDataMock, alertInputDataMock]); expect(getByTestId('contextMenuPanelTitle')).toHaveTextContent('2 selected'); }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.tsx index 21ce285584856..03b25bc85d8db 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/hooks/use_risk_input_actions_panels.tsx @@ -15,12 +15,12 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash/fp'; import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; import { useRiskInputActions } from './use_risk_input_actions'; -import type { AlertRawData } from '../tabs/risk_inputs/risk_inputs_tab'; +import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts'; -export const useRiskInputActionsPanels = (alerts: AlertRawData[], closePopover: () => void) => { +export const useRiskInputActionsPanels = (inputs: InputAlert[], closePopover: () => void) => { const { cases: casesService } = useKibana<{ cases?: CasesService }>().services; const { addToExistingCase, addToNewCaseClick, addToNewTimeline } = useRiskInputActions( - alerts, + inputs, closePopover ); const userCasesPermissions = casesService?.helpers.canUseCases([SECURITY_SOLUTION_OWNER]); @@ -37,21 +37,21 @@ export const useRiskInputActionsPanels = (alerts: AlertRawData[], closePopover: onClick: addToNewTimeline, }; - const ruleName = get(['fields', ALERT_RULE_NAME], alerts[0]) ?? ['']; + const ruleName = get(['alert', ALERT_RULE_NAME], inputs[0]) ?? ''; const title = i18n.translate( 'xpack.securitySolution.flyout.entityDetails.riskInputs.actions.title', { defaultMessage: 'Risk input: {description}', values: { description: - alerts.length === 1 - ? ruleName[0] + inputs.length === 1 + ? ruleName : i18n.translate( 'xpack.securitySolution.flyout.entityDetails.riskInputs.actions.titleDescription', { defaultMessage: '{quantity} selected', values: { - quantity: alerts.length, + quantity: inputs.length, }, } ), @@ -96,5 +96,5 @@ export const useRiskInputActionsPanels = (alerts: AlertRawData[], closePopover: : [timelinePanel], }, ]; - }, [addToExistingCase, addToNewCaseClick, addToNewTimeline, alerts, hasCasesPermissions]); + }, [addToExistingCase, addToNewCaseClick, addToNewTimeline, inputs, hasCasesPermissions]); }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/mocks/index.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/mocks/index.ts index 90fc667f578d3..c05da8c357762 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/mocks/index.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/mocks/index.ts @@ -6,14 +6,22 @@ */ import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; -import type { AlertRawData } from '../tabs/risk_inputs/risk_inputs_tab'; +import { RiskCategories } from '../../../../../common/entity_analytics/risk_engine'; +import type { InputAlert } from '../../../hooks/use_risk_contributing_alerts'; -export const alertDataMock: AlertRawData = { +export const alertInputDataMock: InputAlert = { _id: 'test-id', - _index: 'test-index', - fields: { - [ALERT_RULE_UUID]: ['2e051244-b3c6-4779-a241-e1b4f0beceb9'], - '@timestamp': ['2023-07-20T20:31:24.896Z'], - [ALERT_RULE_NAME]: ['Rule Name'], + input: { + id: 'test-id', + index: 'test-index', + category: RiskCategories.category_1, + description: 'test-description', + timestamp: '2023-07-20T20:31:24.896Z', + risk_score: 50, + contribution_score: 20, + }, + alert: { + [ALERT_RULE_UUID]: '2e051244-b3c6-4779-a241-e1b4f0beceb9', + [ALERT_RULE_NAME]: 'Rule Name', }, }; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs.test.tsx index 0adf0b0639f6b..d9088b5fe5bb9 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs.test.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import { fireEvent, render } from '@testing-library/react'; +import { render } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../../../common/mock'; import { times } from 'lodash/fp'; import { RiskInputsTab } from './risk_inputs_tab'; -import { alertDataMock } from '../../mocks'; +import { alertInputDataMock } from '../../mocks'; import { RiskSeverity } from '../../../../../../common/search_strategy'; import { RiskScoreEntity } from '../../../../../../common/entity_analytics/risk_engine'; @@ -58,7 +58,7 @@ describe('RiskInputsTab', () => { mockUseRiskContributingAlerts.mockReturnValue({ loading: false, error: false, - data: [alertDataMock], + data: [alertInputDataMock], }); mockUseRiskScore.mockReturnValue({ loading: false, @@ -93,7 +93,7 @@ describe('RiskInputsTab', () => { it('Renders the context section if enabled and risks contains asset criticality', () => { mockUseUiSetting.mockReturnValue([true]); - const riskScorewWithAssetCriticality = { + const riskScoreWithAssetCriticality = { '@timestamp': '2021-08-19T16:00:00.000Z', user: { name: 'elastic', @@ -107,7 +107,7 @@ describe('RiskInputsTab', () => { mockUseRiskScore.mockReturnValue({ loading: false, error: false, - data: [riskScorewWithAssetCriticality], + data: [riskScoreWithAssetCriticality], }); const { queryByTestId } = render( @@ -116,13 +116,13 @@ describe('RiskInputsTab', () => { ); - expect(queryByTestId('risk-input-asset-criticality-title')).toBeInTheDocument(); + expect(queryByTestId('risk-input-contexts-title')).toBeInTheDocument(); }); - it('paginates', () => { + it('shows extra alerts contribution message', () => { const alerts = times( (number) => ({ - ...alertDataMock, + ...alertInputDataMock, _id: number.toString(), }), 11 @@ -139,16 +139,12 @@ describe('RiskInputsTab', () => { data: [riskScore], }); - const { getAllByTestId, getByLabelText } = render( + const { queryByTestId } = render( ); - expect(getAllByTestId('risk-input-table-description-cell')).toHaveLength(10); - - fireEvent.click(getByLabelText('Next page')); - - expect(getAllByTestId('risk-input-table-description-cell')).toHaveLength(1); + expect(queryByTestId('risk-input-extra-alerts-message')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab.tsx index 7db6a271e93ab..93156528e80b2 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/entity_details_flyout/tabs/risk_inputs/risk_inputs_tab.tsx @@ -5,18 +5,23 @@ * 2.0. */ -import type { EuiBasicTableColumn, Pagination } from '@elastic/eui'; -import { EuiSpacer, EuiInMemoryTable, EuiTitle, EuiCallOut, EuiText } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import type { EuiBasicTableColumn } from '@elastic/eui'; +import { EuiSpacer, EuiInMemoryTable, EuiTitle, EuiCallOut } from '@elastic/eui'; +import type { ReactNode } from 'react'; +import React, { useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { get } from 'lodash/fp'; -import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; +import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; + +import { get } from 'lodash/fp'; +import type { + InputAlert, + UseRiskContributingAlertsResult, +} from '../../../../hooks/use_risk_contributing_alerts'; +import { useRiskContributingAlerts } from '../../../../hooks/use_risk_contributing_alerts'; import { ENABLE_ASSET_CRITICALITY_SETTING } from '../../../../../../common/constants'; import { PreferenceFormattedDate } from '../../../../../common/components/formatted_date'; -import { ActionColumn } from '../../components/action_column'; -import { RiskInputsUtilityBar } from '../../components/utility_bar'; -import { useRiskContributingAlerts } from '../../../../hooks/use_risk_contributing_alerts'; + import { useRiskScore } from '../../../../api/hooks/use_risk_score'; import type { HostRiskScore, UserRiskScore } from '../../../../../../common/search_strategy'; import { @@ -26,25 +31,21 @@ import { } from '../../../../../../common/search_strategy'; import { RiskScoreEntity } from '../../../../../../common/entity_analytics/risk_engine'; import { AssetCriticalityBadge } from '../../../asset_criticality'; +import { RiskInputsUtilityBar } from '../../components/utility_bar'; +import { ActionColumn } from '../../components/action_column'; export interface RiskInputsTabProps extends Record { entityType: RiskScoreEntity; entityName: string; } -export interface AlertRawData { - fields: Record; - _index: string; - _id: string; -} - const FIRST_RECORD_PAGINATION = { cursorStart: 0, querySize: 1, }; export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) => { - const [selectedItems, setSelectedItems] = useState([]); + const [selectedItems, setSelectedItems] = useState([]); const nameFilterQuery = useMemo(() => { if (entityType === RiskScoreEntity.host) { @@ -67,24 +68,19 @@ export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) => }); const riskScore = riskScoreData && riskScoreData.length > 0 ? riskScoreData[0] : undefined; - const { - loading: loadingAlerts, - data: alertsData, - error: riskAlertsError, - } = useRiskContributingAlerts({ riskScore }); + + const alerts = useRiskContributingAlerts({ riskScore }); const euiTableSelectionProps = useMemo( () => ({ - onSelectionChange: (selected: AlertRawData[]) => { - setSelectedItems(selected); - }, initialSelected: [], selectable: () => true, + onSelectionChange: setSelectedItems, }), [] ); - const alertsColumns: Array> = useMemo( + const inputColumns: Array> = useMemo( () => [ { name: ( @@ -94,12 +90,10 @@ export const RiskInputsTab = ({ entityType, entityName }: RiskInputsTabProps) => /> ), width: '80px', - render: (alert: AlertRawData) => { - return ; - }, + render: (data: InputAlert) => , }, { - field: 'fields.@timestamp', + field: 'input.timestamp', name: ( render: (timestamp: string) => , }, { - field: 'fields', + field: 'alert', 'data-test-subj': 'risk-input-table-description-cell', name: ( truncateText: true, mobileOptions: { show: true }, sortable: true, - render: (fields: AlertRawData['fields']) => get(ALERT_RULE_NAME, fields), + render: (alert: InputAlert['alert']) => get(ALERT_RULE_NAME, alert), + }, + { + field: 'input.contribution_score', + 'data-test-subj': 'risk-input-table-contribution-cell', + name: ( + + ), + truncateText: false, + mobileOptions: { show: true }, + sortable: true, + align: 'right', + render: (contribution: number) => contribution.toFixed(2), }, ], [] ); - const [currentPage, setCurrentPage] = useState<{ - index: number; - size: number; - }>({ index: 0, size: 10 }); - - const onTableChange = useCallback(({ page }) => { - setCurrentPage(page); - }, []); - - const pagination: Pagination = useMemo( - () => ({ - totalItemCount: alertsData?.length ?? 0, - pageIndex: currentPage.index, - pageSize: currentPage.size, - }), - [currentPage.index, currentPage.size, alertsData?.length] - ); - const [isAssetCriticalityEnabled] = useUiSetting$(ENABLE_ASSET_CRITICALITY_SETTING); - if (riskScoreError || riskAlertsError) { + if (riskScoreError) { return ( - - + + + ); return ( <> {isAssetCriticalityEnabled && ( - + )} + {riskInputsAlertSection} ); }; -const RiskInputsAssetCriticalitySection: React.FC<{ +RiskInputsTab.displayName = 'RiskInputsTab'; + +const ContextsSection: React.FC<{ riskScore?: UserRiskScore | HostRiskScore; loading: boolean; }> = ({ riskScore, loading }) => { - const criticalityLevel = useMemo(() => { + const criticality = useMemo(() => { if (!riskScore) { return undefined; } if (isUserRiskScore(riskScore)) { - return riskScore.user.risk.criticality_level; + return { + level: riskScore.user.risk.criticality_level, + contribution: riskScore.user.risk.category_2_score, + }; } - return riskScore.host.risk.criticality_level; + return { + level: riskScore.host.risk.criticality_level, + contribution: riskScore.host.risk.category_2_score, + }; }, [riskScore]); - if (loading || criticalityLevel === undefined) { + if (loading || criticality === undefined) { return null; } return ( <> - +

- - - - - + ), + value: ( + + ), + contribution: (criticality.contribution || 0).toFixed(2), + }, + ]} /> - - ); }; -RiskInputsTab.displayName = 'RiskInputsTab'; +interface ContextRow { + field: ReactNode; + value: ReactNode; + contribution: string; +} + +const contextColumns: Array> = [ + { + field: 'field', + name: ( + + ), + width: '30%', + render: (field: ContextRow['field']) => field, + }, + { + field: 'value', + name: ( + + ), + width: '30%', + render: (val: ContextRow['value']) => val, + }, + { + field: 'contribution', + width: '30%', + align: 'right', + name: ( + + ), + render: (score: ContextRow['contribution']) => score, + }, +]; + +interface ExtraAlertsMessageProps { + riskScore?: UserRiskScore | HostRiskScore; + alerts: UseRiskContributingAlertsResult; +} +const ExtraAlertsMessage: React.FC = ({ riskScore, alerts }) => { + const totals = !riskScore + ? { count: 0, score: 0 } + : isUserRiskScore(riskScore) + ? { count: riskScore.user.risk.category_1_count, score: riskScore.user.risk.category_1_score } + : { count: riskScore.host.risk.category_1_count, score: riskScore.host.risk.category_1_score }; + + const displayed = { + count: alerts.data?.length || 0, + score: alerts.data?.reduce((sum, { input }) => sum + (input.contribution_score || 0), 0) || 0, + }; + + if (displayed.count >= totals.count) { + return null; + } + return ( + + } + iconType="annotation" + /> + ); +}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/hooks/use_risk_contributing_alerts.ts b/x-pack/plugins/security_solution/public/entity_analytics/hooks/use_risk_contributing_alerts.ts index f315908be2a71..65743d468ed2a 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/hooks/use_risk_contributing_alerts.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/hooks/use_risk_contributing_alerts.ts @@ -6,6 +6,9 @@ */ import { useEffect } from 'react'; +import type { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; +import type { EntityRiskInput } from '../../../common/entity_analytics/risk_engine'; + import { useQueryAlerts } from '../../detections/containers/detection_engine/alerts/use_query'; import { ALERTS_QUERY_NAMES } from '../../detections/containers/detection_engine/alerts/constants'; @@ -13,25 +16,33 @@ import type { UserRiskScore, HostRiskScore, } from '../../../common/search_strategy/security_solution/risk_score/all'; -import { getAlertsQueryForRiskScore } from '../common/get_alerts_query_for_risk_score'; - -import { useRiskEngineSettings } from '../api/hooks/use_risk_engine_settings'; +import { isUserRiskScore } from '../../../common/search_strategy/security_solution/risk_score/all'; interface UseRiskContributingAlerts { riskScore: UserRiskScore | HostRiskScore | undefined; - fields?: string[]; } -interface Hit { - fields: Record; +interface AlertData { + [ALERT_RULE_UUID]: string; + [ALERT_RULE_NAME]: string; +} + +interface AlertHit { + _id: string; _index: string; + _source: AlertData; +} + +export interface InputAlert { + alert: AlertData; + input: EntityRiskInput; _id: string; } -interface UseRiskContributingAlertsResult { +export interface UseRiskContributingAlertsResult { loading: boolean; error: boolean; - data?: Hit[]; + data?: InputAlert[]; } /** @@ -39,33 +50,48 @@ interface UseRiskContributingAlertsResult { */ export const useRiskContributingAlerts = ({ riskScore, - fields, }: UseRiskContributingAlerts): UseRiskContributingAlertsResult => { - const { data: riskEngineSettings } = useRiskEngineSettings(); - - const { loading, data, setQuery } = useQueryAlerts({ - // is empty query, to skip fetching alert, until we have risk engine settings + const { loading, data, setQuery } = useQueryAlerts({ query: {}, queryName: ALERTS_QUERY_NAMES.BY_ID, }); - useEffect(() => { - if (!riskEngineSettings?.range?.start || !riskScore) return; + const inputs = getInputs(riskScore); - setQuery( - getAlertsQueryForRiskScore({ - riskRangeStart: riskEngineSettings.range.start, - riskScore, - fields, - }) - ); - }, [setQuery, riskScore, riskEngineSettings?.range?.start, fields]); + useEffect(() => { + if (!riskScore) return; + setQuery({ + query: { + ids: { + values: inputs.map((input) => input.id), + }, + }, + }); + }, [riskScore, inputs, setQuery]); const error = !loading && data === undefined; + const alerts = inputs.map((input) => ({ + _id: input.id, + input, + alert: (data?.hits.hits.find((alert) => alert._id === input.id)?._source || {}) as AlertData, + })); + return { loading, error, - data: data?.hits.hits, + data: alerts, }; }; + +const getInputs = (riskScore?: UserRiskScore | HostRiskScore) => { + if (!riskScore) { + return []; + } + + if (isUserRiskScore(riskScore)) { + return riskScore.user.risk.inputs; + } + + return riskScore.host.risk.inputs; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx index 6d2c7121b6ad3..68c2b4868a312 100644 --- a/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/entity_details/host_details_left/index.test.tsx @@ -10,17 +10,26 @@ import { render } from '@testing-library/react'; import React from 'react'; import { HostDetailsPanel } from '.'; import { TestProviders } from '../../../common/mock'; +import type { HostRiskScore } from '../../../../common/search_strategy'; import { RiskSeverity } from '../../../../common/search_strategy'; -const riskScore = { +const riskScore: HostRiskScore = { '@timestamp': '2021-08-19T16:00:00.000Z', host: { name: 'elastic', risk: { + '@timestamp': '2021-08-19T16:00:00.000Z', + id_field: 'host.name', + id_value: 'elastic', rule_risks: [], calculated_score_norm: 100, + calculated_score: 150, + category_1_score: 150, + category_1_count: 1, multipliers: [], calculated_level: RiskSeverity.critical, + inputs: [], + notes: [], }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.mock.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.mock.ts index f05d13edc825c..4789ecffe1f7e 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.mock.ts @@ -5,10 +5,6 @@ * 2.0. */ -import { - ALERT_RISK_SCORE, - ALERT_RULE_NAME, -} from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; import { RiskCategories, RiskLevels } from '../../../../common/entity_analytics/risk_engine'; import type { RiskScore } from '../../../../common/entity_analytics/risk_engine'; import type { @@ -28,36 +24,19 @@ const buildRiskScoreBucketMock = (overrides: Partial = {}): Ris notes: [], category_1_score: 30, category_1_count: 1, - }, - }, - inputs: { - took: 17, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 1, - relation: 'eq', - }, - hits: [ + risk_inputs: [ { - _id: '_id', - _index: '_index', - fields: { - '@timestamp': ['2023-07-20T20:31:24.896Z'], - [ALERT_RISK_SCORE]: [21], - [ALERT_RULE_NAME]: ['Rule Name'], - }, - sort: [21], + id: 'test_id', + index: '_index', + rule_name: 'Test rule', + time: '2021-08-19T18:55:59.000Z', + score: 30, + contribution: 20, }, ], }, }, + doc_count: 2, }, ...overrides, @@ -108,6 +87,7 @@ const buildResponseMock = ( description: 'Alert from Rule: My rule', risk_score: 30, timestamp: '2021-08-19T18:55:59.000Z', + contribution_score: 20, }, ], }, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts index 00e6f582367b1..86d0d3cfd2294 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/calculate_risk_scores.ts @@ -14,6 +14,7 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { ALERT_RISK_SCORE, ALERT_RULE_NAME, + ALERT_UUID, ALERT_WORKFLOW_STATUS, EVENT_KIND, } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; @@ -48,6 +49,7 @@ import type { RiskScoreBucket, } from '../types'; import { + MAX_INPUTS_COUNT, RISK_SCORING_INPUTS_COUNT_MAX, RISK_SCORING_SUM_MAX, RISK_SCORING_SUM_VALUE, @@ -67,7 +69,6 @@ const formatForResponse = ({ includeNewFields: boolean; }): RiskScore => { const riskDetails = bucket.top_inputs.risk_details; - const inputs = bucket.top_inputs.inputs; const criticalityModifier = getCriticalityModifier(criticality?.criticality_level); const normalizedScoreWithCriticality = applyCriticalityToScore({ @@ -98,15 +99,14 @@ const formatForResponse = ({ }), category_1_count: riskDetails.value.category_1_count, notes: riskDetails.value.notes, - inputs: inputs.hits.hits.map((riskInput) => ({ - id: riskInput._id, - index: riskInput._index, - description: `Alert from Rule: ${ - riskInput.fields?.[ALERT_RULE_NAME]?.[0] ?? 'RULE_NOT_FOUND' - }`, + inputs: riskDetails.value.risk_inputs.map((riskInput) => ({ + id: riskInput.id, + index: riskInput.index, + description: `Alert from Rule: ${riskInput.rule_name ?? 'RULE_NOT_FOUND'}`, category: RiskCategories.category_1, - risk_score: riskInput.fields?.[ALERT_RISK_SCORE]?.[0] ?? undefined, - timestamp: riskInput.fields?.['@timestamp']?.[0] ?? undefined, + risk_score: riskInput.score, + timestamp: riskInput.time, + contribution_score: riskInput.contribution, })), ...(includeNewFields ? newFields : {}), }; @@ -140,9 +140,15 @@ const buildReduceScript = ({ double total_score = 0; double current_score = 0; + List risk_inputs = []; for (int i = 0; i < num_inputs_to_score; i++) { current_score = inputs[i].weighted_score / Math.pow(i + 1, params.p); + if (i < ${MAX_INPUTS_COUNT}) { + inputs[i]["contribution"] = 100 * current_score / params.risk_cap; + risk_inputs.add(inputs[i]); + } + ${buildCategoryAssignment()} total_score += current_score; } @@ -151,6 +157,7 @@ const buildReduceScript = ({ double score_norm = 100 * total_score / params.risk_cap; results['score'] = total_score; results['normalized_score'] = score_norm; + results['risk_inputs'] = risk_inputs; return results; `; @@ -191,14 +198,8 @@ const buildIdentifierTypeAggregation = ({ sampler: { shard_size: alertSampleSizePerShard, }, + aggs: { - inputs: { - top_hits: { - size: 5, - _source: false, - docvalue_fields: ['@timestamp', ALERT_RISK_SCORE, ALERT_RULE_NAME], - }, - }, risk_details: { scripted_metric: { init_script: 'state.inputs = []', @@ -209,8 +210,13 @@ const buildIdentifierTypeAggregation = ({ double weighted_score = 0.0; fields.put('time', doc['@timestamp'].value); + fields.put('rule_name', doc['${ALERT_RULE_NAME}'].value); + fields.put('category', category); + fields.put('index', doc['_index'].value); + fields.put('id', doc['${ALERT_UUID}'].value); fields.put('score', score); + ${buildWeightingOfScoreByCategory({ userWeights: weights, identifierType })} fields.put('weighted_score', weighted_score); @@ -308,7 +314,6 @@ export const calculateRiskScores = async ({ filter.push(userFilter as QueryDslQueryContainer); } const identifierTypes: IdentifierType[] = identifierType ? [identifierType] : ['host', 'user']; - const request = { size: 0, _source: false, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/constants.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/constants.ts index 57e67960f96e2..73f93d71b11f9 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/constants.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/constants.ts @@ -25,3 +25,8 @@ export const RISK_SCORING_INPUTS_COUNT_MAX = 999999; * This value represents the maximum possible risk score after normalization. */ export const RISK_SCORING_NORMALIZATION_MAX = 100; + +/** + * This value represents the max amount of alert inputs we store, per entity, in the risk document. + */ +export const MAX_INPUTS_COUNT = 10; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts index eb989716153ca..55fc71cd7c3c5 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { MappingRuntimeFields, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; import type { AfterKey, AfterKeys, @@ -113,6 +113,15 @@ export interface CalculateRiskScoreAggregations { }; } +export interface SearchHitRiskInput { + id: string; + index: string; + rule_name?: string; + time?: string; + score?: number; + contribution?: number; +} + export interface RiskScoreBucket { key: { [identifierField: string]: string }; doc_count: number; @@ -125,9 +134,9 @@ export interface RiskScoreBucket { notes: string[]; category_1_score: number; category_1_count: number; + risk_inputs: SearchHitRiskInput[]; }; }; - inputs: SearchResponse; }; } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index cc91e715030d4..fe8eeb773ad9f 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -33024,8 +33024,6 @@ "xpack.securitySolution.flyout.entityDetails.observedEntityUpdatedTime": "Mis à jour le {time}", "xpack.securitySolution.flyout.entityDetails.riskInputs.actions.title": "Entrée des risques : {description}", "xpack.securitySolution.flyout.entityDetails.riskInputs.actions.titleDescription": "{quantity} sélectionnée", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextRange": "Affichage de {displayedRange} sur {totalContributions} {riskContributions}", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextSingle": "Affichage de {totalContributions} {riskInputs}", "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.text": "{totalSelectedContributions} contributions de risque sélectionnées", "xpack.securitySolution.flyout.entityDetails.riskUpdatedTime": "Mis à jour le {time}", "xpack.securitySolution.flyout.entityDetails.title": "Sommaire des risques de {entity} ", @@ -36390,14 +36388,10 @@ "xpack.securitySolution.flyout.entityDetails.riskInputs.actions.ariaLabel": "Actions", "xpack.securitySolution.flyout.entityDetails.riskInputs.actionsColumn": "Actions", "xpack.securitySolution.flyout.entityDetails.riskInputs.alertsTitle": "Alertes", - "xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityDescription": "La criticité affectée au moment du calcul du score de risque.", - "xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityTitle": "Criticité des ressources", "xpack.securitySolution.flyout.entityDetails.riskInputs.dateColumn": "Date", "xpack.securitySolution.flyout.entityDetails.riskInputs.errorBody": "Erreur lors de la récupération des entrées des risques. Réessayez plus tard.", "xpack.securitySolution.flyout.entityDetails.riskInputs.errorTitle": "Un problème est survenu", "xpack.securitySolution.flyout.entityDetails.riskInputs.riskInputColumn": "Contribution au risque", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInput": "Contribution au risque", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInputs": "Contributions au risque", "xpack.securitySolution.flyout.entityDetails.riskSummary.casesAttachmentLabel": "Score de risque pour {entityType, select, host {host} user {user}} {entityName}", "xpack.securitySolution.flyout.entityDetails.scoreColumnLabel": "Score", "xpack.securitySolution.flyout.entityDetails.showAllRiskInputs": "Montrer toutes les entrées des risques", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d0e0deea4c4e6..99f104ae06ff4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -32991,8 +32991,6 @@ "xpack.securitySolution.flyout.entityDetails.observedEntityUpdatedTime": "更新日時{time}", "xpack.securitySolution.flyout.entityDetails.riskInputs.actions.title": "リスクインプット:{description}", "xpack.securitySolution.flyout.entityDetails.riskInputs.actions.titleDescription": "{quantity}選択済み", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextRange": "{displayedRange}/{totalContributions} {riskContributions}を表示しています", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextSingle": "{totalContributions} {riskInputs}を表示しています", "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.text": "{totalSelectedContributions}選択されたリスク寄与", "xpack.securitySolution.flyout.entityDetails.riskUpdatedTime": "更新日時{time}", "xpack.securitySolution.flyout.entityDetails.title": "{entity}リスク概要", @@ -36359,14 +36357,10 @@ "xpack.securitySolution.flyout.entityDetails.riskInputs.actions.ariaLabel": "アクション", "xpack.securitySolution.flyout.entityDetails.riskInputs.actionsColumn": "アクション", "xpack.securitySolution.flyout.entityDetails.riskInputs.alertsTitle": "アラート", - "xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityDescription": "リスクスコア計算時に割り当てられた重要度。", - "xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityTitle": "アセット重要度", "xpack.securitySolution.flyout.entityDetails.riskInputs.dateColumn": "日付", "xpack.securitySolution.flyout.entityDetails.riskInputs.errorBody": "リスク情報の取得中にエラーが発生しました。しばらくたってから再試行してください。", "xpack.securitySolution.flyout.entityDetails.riskInputs.errorTitle": "問題が発生しました", "xpack.securitySolution.flyout.entityDetails.riskInputs.riskInputColumn": "リスク寄与", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInput": "リスク寄与", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInputs": "リスク寄与", "xpack.securitySolution.flyout.entityDetails.riskSummary.casesAttachmentLabel": "{entityType, select, host {ホスト} user {ユーザー}} {entityName}のリスクスコア", "xpack.securitySolution.flyout.entityDetails.scoreColumnLabel": "スコア", "xpack.securitySolution.flyout.entityDetails.showAllRiskInputs": "すべてのリスクインプットを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index eb5efe21adb79..c4df07aade234 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -33035,8 +33035,6 @@ "xpack.securitySolution.flyout.entityDetails.observedEntityUpdatedTime": "已更新 {time}", "xpack.securitySolution.flyout.entityDetails.riskInputs.actions.title": "风险输入:{description}", "xpack.securitySolution.flyout.entityDetails.riskInputs.actions.titleDescription": "{quantity} 个已选定", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextRange": "正在显示 {displayedRange} 个(共 {totalContributions} 个){riskContributions}", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.selectionTextSingle": "正在显示 {totalContributions} 个{riskInputs}", "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.text": "{totalSelectedContributions} 个选定的风险贡献", "xpack.securitySolution.flyout.entityDetails.riskUpdatedTime": "已更新 {time}", "xpack.securitySolution.flyout.entityDetails.title": "{entity} 风险摘要", @@ -36402,14 +36400,10 @@ "xpack.securitySolution.flyout.entityDetails.riskInputs.actions.ariaLabel": "操作", "xpack.securitySolution.flyout.entityDetails.riskInputs.actionsColumn": "操作", "xpack.securitySolution.flyout.entityDetails.riskInputs.alertsTitle": "告警", - "xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityDescription": "在计算风险分数时分配的关键度。", - "xpack.securitySolution.flyout.entityDetails.riskInputs.assetCriticalityTitle": "资产关键度", "xpack.securitySolution.flyout.entityDetails.riskInputs.dateColumn": "日期", "xpack.securitySolution.flyout.entityDetails.riskInputs.errorBody": "提取风险输入时出错。请稍后重试。", "xpack.securitySolution.flyout.entityDetails.riskInputs.errorTitle": "出问题了", "xpack.securitySolution.flyout.entityDetails.riskInputs.riskInputColumn": "风险贡献", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInput": "风险贡献", - "xpack.securitySolution.flyout.entityDetails.riskInputs.utilityBar.riskInputs": "风险贡献", "xpack.securitySolution.flyout.entityDetails.riskSummary.casesAttachmentLabel": "{entityType, select, host {主机} user {用户}} {entityName} 的风险分数", "xpack.securitySolution.flyout.entityDetails.scoreColumnLabel": "分数", "xpack.securitySolution.flyout.entityDetails.showAllRiskInputs": "显示所有风险输入", diff --git a/x-pack/test/security_solution_cypress/es_archives/risk_scores_new_complete_data/data.json b/x-pack/test/security_solution_cypress/es_archives/risk_scores_new_complete_data/data.json index ab66422fb9175..96b9c111aa605 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risk_scores_new_complete_data/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/risk_scores_new_complete_data/data.json @@ -22,6 +22,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "New Rule Test", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" }, @@ -30,6 +31,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "New Rule Test", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -64,6 +66,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -99,6 +102,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -133,6 +137,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -167,6 +172,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -201,6 +207,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -238,6 +245,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Endpoint Security", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -273,6 +281,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -307,6 +316,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -341,6 +351,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -375,6 +386,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -409,6 +421,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -446,6 +459,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Endpoint Security", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -480,6 +494,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -516,6 +531,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "New Rule Test", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" }, @@ -524,6 +540,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "New Rule Test", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -559,6 +576,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -595,6 +613,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -630,6 +649,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -665,6 +685,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -700,6 +721,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -738,6 +760,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Endpoint Security", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -774,6 +797,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -809,6 +833,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -844,6 +869,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -879,6 +905,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -914,6 +941,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -952,6 +980,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Endpoint Security", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } @@ -987,6 +1016,7 @@ "index": ".internal.alerts-security.alerts-default-000001", "description": "Alert from Rule: Rule 2", "category": "category_1", + "contribution_score": 50, "risk_score": 70, "timestamp": "2023-08-14T09:08:18.664Z" } diff --git a/x-pack/test/security_solution_cypress/es_archives/risk_scores_new_complete_data/mappings.json b/x-pack/test/security_solution_cypress/es_archives/risk_scores_new_complete_data/mappings.json index 950fc2b610f6e..33b460c379eab 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risk_scores_new_complete_data/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/risk_scores_new_complete_data/mappings.json @@ -64,6 +64,9 @@ }, "timestamp": { "type": "date" + }, + "contribution_score": { + "type": "float" } } }, @@ -130,6 +133,9 @@ }, "timestamp": { "type": "date" + }, + "contribution_score": { + "type": "float" } } }, @@ -222,6 +228,9 @@ }, "timestamp": { "type": "date" + }, + "contribution_score": { + "type": "float" } } }, @@ -279,6 +288,9 @@ }, "timestamp": { "type": "date" + }, + "contribution_score": { + "type": "float" } } }, From 59edae292c0702240e216a06a96e74ff62187089 Mon Sep 17 00:00:00 2001 From: Tomasz Ciecierski Date: Mon, 15 Apr 2024 13:33:10 +0200 Subject: [PATCH 5/5] [Osquery] Unskip add_integration.cy.ts (#180733) --- .../osquery/cypress/e2e/all/add_integration.cy.ts | 14 ++++---------- .../plugins/osquery/cypress/tasks/integrations.ts | 4 ++-- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts index bdb648a32a0e2..6b847fb396967 100644 --- a/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts +++ b/x-pack/plugins/osquery/cypress/e2e/all/add_integration.cy.ts @@ -39,10 +39,7 @@ import { } from '../../tasks/integrations'; import { ServerlessRoleName } from '../../support/roles'; -// Failing: See https://github.com/elastic/kibana/issues/170445 -// Failing: See https://github.com/elastic/kibana/issues/169701 -// Failing: See https://github.com/elastic/kibana/issues/170593 -describe.skip('ALL - Add Integration', { tags: ['@ess', '@serverless'] }, () => { +describe('ALL - Add Integration', { tags: ['@ess', '@serverless'] }, () => { let savedQueryId: string; before(() => { @@ -77,8 +74,7 @@ describe.skip('ALL - Add Integration', { tags: ['@ess', '@serverless'] }, () => } ); - // FLAKY: https://github.com/elastic/kibana/issues/169701 - describe.skip('Add and upgrade integration', { tags: ['@ess', '@serverless'] }, () => { + describe('Add and upgrade integration', { tags: ['@ess', '@serverless'] }, () => { const oldVersion = '0.7.4'; const [integrationName, policyName] = generateRandomStringName(2); let policyId: string; @@ -107,8 +103,7 @@ describe.skip('ALL - Add Integration', { tags: ['@ess', '@serverless'] }, () => cy.contains(`version: ${oldVersion}`).should('not.exist'); }); }); - // FLAKY: https://github.com/elastic/kibana/issues/170593 - describe.skip('Add integration to policy', () => { + describe('Add integration to policy', () => { const [integrationName, policyName] = generateRandomStringName(2); let policyId: string; beforeEach(() => { @@ -148,8 +143,7 @@ describe.skip('ALL - Add Integration', { tags: ['@ess', '@serverless'] }, () => }); }); - // FLAKY: https://github.com/elastic/kibana/issues/170445 - describe.skip('Upgrade policy with existing packs', () => { + describe('Upgrade policy with existing packs', () => { const oldVersion = '1.2.0'; const [policyName, integrationName, packName] = generateRandomStringName(3); let policyId: string; diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts index b4c1049ffc7f6..e4c38854a5faf 100644 --- a/x-pack/plugins/osquery/cypress/tasks/integrations.ts +++ b/x-pack/plugins/osquery/cypress/tasks/integrations.ts @@ -162,9 +162,9 @@ export const installPackageWithVersion = (integration: string, version: string) }; const extractSemanticVersion = (str: string) => { - const match = str.match(/(\d+\.\d+\.\d+)/); + const match = str.match(/(Managerv\d+\.\d+\.\d+)/); if (match && match[1]) { - return match[1]; + return match[1].replace('Managerv', ''); } else { return null; // Return null if no match found }