diff --git a/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/constants.ts b/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/constants.ts new file mode 100644 index 0000000000000..422e362ca5923 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/constants.ts @@ -0,0 +1,47 @@ +/* + * 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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; + +import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; + +export const CAI_ACTIVITY_INDEX_NAME = '.internal.cases-activity'; + +export const CAI_ACTIVITY_INDEX_VERSION = 1; + +export const CAI_ACTIVITY_SOURCE_QUERY: QueryDslQueryContainer = { + term: { + type: 'cases-user-actions', + }, +}; + +export const CAI_ACTIVITY_SOURCE_INDEX = ALERTING_CASES_SAVED_OBJECT_INDEX; + +export const CAI_ACTIVITY_BACKFILL_TASK_ID = 'cai_activity_backfill_task'; + +export const CAI_ACTIVITY_SYNCHRONIZATION_TASK_ID = 'cai_cases_activity_synchronization_task'; + +export const getActivitySynchronizationSourceQuery = ( + lastSyncAt: Date +): QueryDslQueryContainer => ({ + bool: { + must: [ + { + term: { + type: 'cases-user-actions', + }, + }, + { + range: { + 'cases-user-actions.created_at': { + gte: lastSyncAt.toISOString(), + }, + }, + }, + ], + }, +}); diff --git a/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/index.ts b/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/index.ts new file mode 100644 index 0000000000000..f6d90cfbb2e04 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/index.ts @@ -0,0 +1,67 @@ +/* + * 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 type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; +import { AnalyticsIndex } from '../analytics_index'; +import { + CAI_ACTIVITY_INDEX_NAME, + CAI_ACTIVITY_INDEX_VERSION, + CAI_ACTIVITY_SOURCE_INDEX, + CAI_ACTIVITY_SOURCE_QUERY, + CAI_ACTIVITY_BACKFILL_TASK_ID, + CAI_ACTIVITY_SYNCHRONIZATION_TASK_ID, +} from './constants'; +import { CAI_ACTIVITY_INDEX_MAPPINGS } from './mappings'; +import { CAI_ACTIVITY_INDEX_SCRIPT, CAI_ACTIVITY_INDEX_SCRIPT_ID } from './painless_scripts'; +import { scheduleCAISynchronizationTask } from '../tasks/synchronization_task'; + +export const createActivityAnalyticsIndex = ({ + esClient, + logger, + isServerless, + taskManager, +}: { + esClient: ElasticsearchClient; + logger: Logger; + isServerless: boolean; + taskManager: TaskManagerStartContract; +}): AnalyticsIndex => + new AnalyticsIndex({ + logger, + esClient, + isServerless, + taskManager, + indexName: CAI_ACTIVITY_INDEX_NAME, + indexVersion: CAI_ACTIVITY_INDEX_VERSION, + mappings: CAI_ACTIVITY_INDEX_MAPPINGS, + painlessScriptId: CAI_ACTIVITY_INDEX_SCRIPT_ID, + painlessScript: CAI_ACTIVITY_INDEX_SCRIPT, + taskId: CAI_ACTIVITY_BACKFILL_TASK_ID, + sourceIndex: CAI_ACTIVITY_SOURCE_INDEX, + sourceQuery: CAI_ACTIVITY_SOURCE_QUERY, + }); + +export const scheduleActivityAnalyticsSyncTask = ({ + taskManager, + logger, +}: { + taskManager: TaskManagerStartContract; + logger: Logger; +}) => { + scheduleCAISynchronizationTask({ + taskId: CAI_ACTIVITY_SYNCHRONIZATION_TASK_ID, + sourceIndex: CAI_ACTIVITY_SOURCE_INDEX, + destIndex: CAI_ACTIVITY_INDEX_NAME, + taskManager, + logger, + }).catch((e) => { + logger.error( + `Error scheduling ${CAI_ACTIVITY_SYNCHRONIZATION_TASK_ID} task, received ${e.message}` + ); + }); +}; diff --git a/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/mappings.ts b/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/mappings.ts new file mode 100644 index 0000000000000..a5d19f1a615c2 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/mappings.ts @@ -0,0 +1,70 @@ +/* + * 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 type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; + +export const CAI_ACTIVITY_INDEX_MAPPINGS: MappingTypeMapping = { + dynamic: false, + properties: { + '@timestamp': { + type: 'date', + }, + case_id: { + type: 'keyword', + }, + action: { + type: 'keyword', + }, + type: { + type: 'keyword', + }, + payload: { + properties: { + status: { + type: 'keyword', + }, + tags: { + type: 'keyword', + }, + category: { + type: 'keyword', + }, + severity: { + type: 'keyword', + }, + }, + }, + created_at: { + type: 'date', + }, + created_at_ms: { + type: 'long', + }, + created_by: { + properties: { + username: { + type: 'keyword', + }, + profile_uid: { + type: 'keyword', + }, + full_name: { + type: 'keyword', + }, + email: { + type: 'keyword', + }, + }, + }, + owner: { + type: 'keyword', + }, + space_ids: { + type: 'keyword', + }, + }, +}; diff --git a/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/painless_scripts.ts b/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/painless_scripts.ts new file mode 100644 index 0000000000000..5dbe37e1c8e2b --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/server/cases_analytics/activity_index/painless_scripts.ts @@ -0,0 +1,64 @@ +/* + * 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 type { StoredScript } from '@elastic/elasticsearch/lib/api/types'; +import { CAI_ACTIVITY_INDEX_VERSION } from './constants'; + +export const CAI_ACTIVITY_INDEX_SCRIPT_ID = `cai_activity_script_${CAI_ACTIVITY_INDEX_VERSION}`; +export const CAI_ACTIVITY_INDEX_SCRIPT: StoredScript = { + lang: 'painless', + source: ` + def source = [:]; + source.putAll(ctx._source); + ctx._source.clear(); + + ctx._source.action = source["cases-user-actions"].action; + ctx._source.type = source["cases-user-actions"].type; + + ZonedDateTime zdt_created = + ZonedDateTime.parse(source["cases-user-actions"].created_at); + ctx._source.created_at_ms = zdt_created.toInstant().toEpochMilli(); + ctx._source.created_at = source["cases-user-actions"].created_at; + + if (source["cases-user-actions"].created_by != null) { + ctx._source.created_by = new HashMap(); + ctx._source.created_by.full_name = source["cases-user-actions"].created_by.full_name; + ctx._source.created_by.username = source["cases-user-actions"].created_by.username; + ctx._source.created_by.profile_uid = source["cases-user-actions"].created_by.profile_uid; + ctx._source.created_by.email = source["cases-user-actions"].created_by.email; + } + + if (source["cases-user-actions"].payload != null) { + ctx._source.payload = new HashMap(); + + if (source["cases-user-actions"].type == "severity" && source["cases-user-actions"].payload.severity != null) { + ctx._source.payload.severity = source["cases-user-actions"].payload.severity; + } + + if (source["cases-user-actions"].type == "category" && source["cases-user-actions"].payload.category != null) { + ctx._source.payload.category = source["cases-user-actions"].payload.category; + } + + if (source["cases-user-actions"].type == "status" && source["cases-user-actions"].payload.status != null) { + ctx._source.payload.status = source["cases-user-actions"].payload.status; + } + + if (source["cases-user-actions"].type == "tags" && source["cases-user-actions"].payload.tags != null) { + ctx._source.payload.tags = source["cases-user-actions"].payload.tags; + } + } + + for (item in source.references) { + if (item.type == "cases") { + ctx._source.case_id = item.id; + } + } + + ctx._source.owner = source["cases-user-actions"].owner; + ctx._source.space_ids = source.namespaces; + `, +}; diff --git a/x-pack/platform/plugins/shared/cases/server/cases_analytics/constants.ts b/x-pack/platform/plugins/shared/cases/server/cases_analytics/constants.ts index 0ef6d6414f696..de72f8812556d 100644 --- a/x-pack/platform/plugins/shared/cases/server/cases_analytics/constants.ts +++ b/x-pack/platform/plugins/shared/cases/server/cases_analytics/constants.ts @@ -16,6 +16,10 @@ import { CAI_COMMENTS_INDEX_NAME, getCommentsSynchronizationSourceQuery, } from './comments_index/constants'; +import { + CAI_ACTIVITY_INDEX_NAME, + getActivitySynchronizationSourceQuery, +} from './activity_index/constants'; export const CAI_NUMBER_OF_SHARDS = 1; /** Allocate 1 replica if there are enough data nodes, otherwise continue with 0 */ @@ -41,4 +45,5 @@ export const SYNCHRONIZATION_QUERIES_DICTIONARY: Record< [CAI_CASES_INDEX_NAME]: getCasesSynchronizationSourceQuery, [CAI_COMMENTS_INDEX_NAME]: getCommentsSynchronizationSourceQuery, [CAI_ATTACHMENTS_INDEX_NAME]: getAttachmentsSynchronizationSourceQuery, + [CAI_ACTIVITY_INDEX_NAME]: getActivitySynchronizationSourceQuery, }; diff --git a/x-pack/platform/plugins/shared/cases/server/cases_analytics/index.ts b/x-pack/platform/plugins/shared/cases/server/cases_analytics/index.ts index d6b0cbbad41b7..e55a7ad8de29b 100644 --- a/x-pack/platform/plugins/shared/cases/server/cases_analytics/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/cases_analytics/index.ts @@ -19,6 +19,7 @@ import { } from './attachments_index'; import { createCasesAnalyticsIndex, scheduleCasesAnalyticsSyncTask } from './cases_index'; import { createCommentsAnalyticsIndex, scheduleCommentsAnalyticsSyncTask } from './comments_index'; +import { createActivityAnalyticsIndex, scheduleActivityAnalyticsSyncTask } from './activity_index'; export const createCasesAnalyticsIndexes = ({ esClient, @@ -49,11 +50,18 @@ export const createCasesAnalyticsIndexes = ({ isServerless, taskManager, }); + const casesActivityIndex = createActivityAnalyticsIndex({ + logger, + esClient, + isServerless, + taskManager, + }); return Promise.all([ casesIndex.upsertIndex(), casesAttachmentsIndex.upsertIndex(), casesCommentsIndex.upsertIndex(), + casesActivityIndex.upsertIndex(), ]); }; @@ -77,6 +85,7 @@ export const scheduleCasesAnalyticsSyncTasks = ({ taskManager: TaskManagerStartContract; logger: Logger; }) => { + scheduleActivityAnalyticsSyncTask({ taskManager, logger }); scheduleCasesAnalyticsSyncTask({ taskManager, logger }); scheduleCommentsAnalyticsSyncTask({ taskManager, logger }); scheduleAttachmentsAnalyticsSyncTask({ taskManager, logger }); diff --git a/x-pack/platform/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/platform/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 928ddd37847e7..16527b4536b57 100644 --- a/x-pack/platform/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/platform/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -141,9 +141,9 @@ export default function ({ getService }: FtrProviderContext) { 'alerts_invalidate_api_keys', 'apm-source-map-migration-task', 'apm-telemetry-task', + 'cai:cases_analytics_index_backfill', + 'cai:cases_analytics_index_synchronization', 'cases-telemetry-task', - 'cases:analytics_index_backfill', - 'cases:analytics_index_synchronization', 'cloud_security_posture-stats_task', 'dashboard_telemetry', 'endpoint:complete-external-response-actions',