diff --git a/.buildkite/ftr_oblt_stateful_configs.yml b/.buildkite/ftr_oblt_stateful_configs.yml index 4edf75f385816a..55f3c1b603fcab 100644 --- a/.buildkite/ftr_oblt_stateful_configs.yml +++ b/.buildkite/ftr_oblt_stateful_configs.yml @@ -30,6 +30,7 @@ enabled: - x-pack/test/api_integration/apis/synthetics/config.ts - x-pack/test/api_integration/apis/slos/config.ts - x-pack/test/api_integration/apis/uptime/config.ts + - x-pack/test/api_integration/apis/entity_manager/config.ts - x-pack/test/apm_api_integration/basic/config.ts - x-pack/test/apm_api_integration/cloud/config.ts - x-pack/test/apm_api_integration/rules/config.ts @@ -48,4 +49,3 @@ enabled: - x-pack/test/observability_ai_assistant_functional/enterprise/config.ts - x-pack/test/profiling_api_integration/cloud/config.ts - x-pack/test/functional/apps/apm/config.ts - \ No newline at end of file diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index aff3a490c8d9b0..00b2bd8b6a624b 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -487,7 +487,7 @@ The plugin exposes the static DefaultEditorController class to consume. |{kib-repo}blob/{branch}/x-pack/plugins/cloud/README.md[cloud] -|The cloud plugin adds Cloud-specific features to Kibana. +|The cloud plugin exposes Cloud-specific metadata to Kibana. |{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_chat/README.md[cloudChat] diff --git a/packages/core/http/core-http-router-server-internal/src/response_adapter.ts b/packages/core/http/core-http-router-server-internal/src/response_adapter.ts index a5cc1ca1f8d96a..1ddb5572d01f72 100644 --- a/packages/core/http/core-http-router-server-internal/src/response_adapter.ts +++ b/packages/core/http/core-http-router-server-internal/src/response_adapter.ts @@ -115,7 +115,7 @@ export class HapiResponseAdapter { return response; } - private toError(kibanaResponse: KibanaResponse) { + private toError(kibanaResponse: KibanaResponse) { const { payload } = kibanaResponse; // Special case for when we are proxying requests and want to enable streaming back error responses opaquely. @@ -153,7 +153,12 @@ function getErrorMessage(payload?: ResponseError): string { if (!payload) { throw new Error('expected error message to be provided'); } - if (typeof payload === 'string') return payload; + if (typeof payload === 'string') { + return payload; + } + if (isStreamOrBuffer(payload)) { + throw new Error(`can't resolve error message from stream or buffer`); + } // for ES response errors include nested error reason message. it doesn't contain sensitive data. if (isElasticsearchResponseError(payload)) { return `[${payload.message}]: ${ @@ -164,6 +169,10 @@ function getErrorMessage(payload?: ResponseError): string { return getErrorMessage(payload.message); } +function isStreamOrBuffer(payload: ResponseError): payload is stream.Stream | Buffer { + return Buffer.isBuffer(payload) || stream.isReadable(payload as stream.Readable); +} + function getErrorAttributes(payload?: ResponseError): ResponseErrorAttributes | undefined { return typeof payload === 'object' && 'attributes' in payload ? payload.attributes : undefined; } diff --git a/packages/core/http/core-http-server/src/router/response.ts b/packages/core/http/core-http-server/src/router/response.ts index 385a03f518e7f1..7e318f443a1cff 100644 --- a/packages/core/http/core-http-server/src/router/response.ts +++ b/packages/core/http/core-http-server/src/router/response.ts @@ -39,6 +39,8 @@ export type ResponseErrorAttributes = Record; */ export type ResponseError = | string + | Buffer + | Stream | Error | { message: string | Error; diff --git a/packages/core/http/core-http-server/src/router/response_factory.ts b/packages/core/http/core-http-server/src/router/response_factory.ts index f7c763da024bfe..c4455be73e16f2 100644 --- a/packages/core/http/core-http-server/src/router/response_factory.ts +++ b/packages/core/http/core-http-server/src/router/response_factory.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import type { Stream } from 'stream'; import type { CustomHttpResponseOptions, HttpResponseOptions, @@ -139,7 +138,7 @@ export interface KibanaErrorResponseFactory { * Creates an error response with defined status code and payload. * @param options - {@link CustomHttpResponseOptions} configures HTTP response headers, error message and other error details to pass to the client */ - customError(options: CustomHttpResponseOptions): IKibanaResponse; + customError(options: CustomHttpResponseOptions): IKibanaResponse; } /** diff --git a/packages/kbn-analytics/README.md b/packages/kbn-analytics/README.md new file mode 100644 index 00000000000000..7cd705ea223fd0 --- /dev/null +++ b/packages/kbn-analytics/README.md @@ -0,0 +1,12 @@ +# `@kbn/analytics` + +> [!NOTE] +> The term _analytics_ here refers to _Usage Analytics_, and should not be confused with the Kibana (Data) Analytics tools. + +> [!IMPORTANT] +> This package is exclusively used by the plugin `usage_collection` and it's not expected to be used elsewhere. +> If you are still here for _Usage Analytics_, you might be looking for [core-analytics](../core/analytics), the [EBT packages](../analytics). + +This package implements the report that batches updates from Application Usage, UI Counters, and User Agent. +It defines the contract of the report, and the strategy to ship it to the server. + diff --git a/packages/kbn-search-api-panels/components/code_box.tsx b/packages/kbn-search-api-panels/components/code_box.tsx index 11bf2bea833186..a3ab17009f54a7 100644 --- a/packages/kbn-search-api-panels/components/code_box.tsx +++ b/packages/kbn-search-api-panels/components/code_box.tsx @@ -65,6 +65,13 @@ export const CodeBox: React.FC = ({ { if (setSelectedLanguage) { setSelectedLanguage(language); diff --git a/test/plugin_functional/plugins/core_http/server/plugin.ts b/test/plugin_functional/plugins/core_http/server/plugin.ts index 534d55a97bdfe5..c5e8dd01847be1 100644 --- a/test/plugin_functional/plugins/core_http/server/plugin.ts +++ b/test/plugin_functional/plugins/core_http/server/plugin.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { Readable } from 'stream'; import type { Plugin, CoreSetup } from '@kbn/core/server'; export class CoreHttpPlugin implements Plugin { @@ -87,6 +88,32 @@ export class CoreHttpPlugin implements Plugin { }, }); }); + + router.get( + { + path: '/api/core_http/error_stream', + validate: false, + }, + async (ctx, req, res) => { + return res.customError({ + body: Readable.from(['error stream'], { objectMode: false }), + statusCode: 501, + }); + } + ); + + router.get( + { + path: '/api/core_http/error_buffer', + validate: false, + }, + async (ctx, req, res) => { + return res.customError({ + body: Buffer.from('error buffer', 'utf8'), + statusCode: 501, + }); + } + ); } public start() {} diff --git a/test/plugin_functional/test_suites/core_plugins/error_response.ts b/test/plugin_functional/test_suites/core_plugins/error_response.ts new file mode 100644 index 00000000000000..0a87b4c9a6bd4e --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/error_response.ts @@ -0,0 +1,38 @@ +/* + * 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 expect from '@kbn/expect'; +import '@kbn/core-provider-plugin/types'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + + // routes defined in the `core_http` test plugin + describe('Custom errors', () => { + it('can serve an error response from stream', async () => { + await supertest + .get('/api/core_http/error_stream') + .expect(501) + .then((response) => { + const res = response.body.toString(); + expect(res).to.eql('error stream'); + }); + }); + + it('can serve an error response from buffer', async () => { + await supertest + .get('/api/core_http/error_buffer') + .expect(501) + .then((response) => { + const res = response.body.toString(); + expect(res).to.eql('error buffer'); + }); + }); + }); +} diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts index 07e258e34e3f1f..5e3d969bb02778 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.ts +++ b/test/plugin_functional/test_suites/core_plugins/index.ts @@ -26,5 +26,6 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) { loadTestFile(require.resolve('./http')); loadTestFile(require.resolve('./http_versioned')); loadTestFile(require.resolve('./dynamic_contract_resolving')); + loadTestFile(require.resolve('./error_response')); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index a5b5cba1469ba9..27addbfc274a29 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -246,6 +246,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.cloud_integrations.full_story.eventTypesAllowlist (array)', 'xpack.cloud_integrations.full_story.pageVarsDebounceTime (duration)', 'xpack.cloud.id (string)', + 'xpack.cloud.organization_id (string)', 'xpack.cloud.organization_url (string)', 'xpack.cloud.billing_url (string)', 'xpack.cloud.profile_url (string)', @@ -256,6 +257,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.cloud.serverless.project_id (string)', 'xpack.cloud.serverless.project_name (string)', 'xpack.cloud.serverless.project_type (string)', + 'xpack.cloud.serverless.orchestrator_target (string)', 'xpack.cloud.onboarding.default_solution (string)', 'xpack.discoverEnhanced.actions.exploreDataInChart.enabled (boolean)', 'xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled (boolean)', diff --git a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_1.bundled.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_1.bundled.schema.yaml new file mode 100644 index 00000000000000..be9cb47ef033d4 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_1.bundled.schema.yaml @@ -0,0 +1,1133 @@ +openapi: 3.0.3 +info: + description: Manage and interact with Security Assistant resources. + title: Security AI Assistant API (Elastic Cloud & self-hosted) + version: '1' +servers: + - url: 'http://{kibana_host}:{port}' + variables: + kibana_host: + default: localhost + port: + default: '5601' +paths: + /api/security_ai_assistant/anonymization_fields/_bulk_action: + post: + description: >- + The bulk action is applied to all anonymization fields that match the + filter or to the list of anonymization fields by their IDs. + operationId: PerformBulkAction + requestBody: + content: + application/json: + schema: + type: object + properties: + create: + items: + $ref: '#/components/schemas/AnonymizationFieldCreateProps' + type: array + delete: + type: object + properties: + ids: + description: Array of anonymization fields IDs + items: + type: string + minItems: 1 + type: array + query: + description: Query to filter anonymization fields + type: string + update: + items: + $ref: '#/components/schemas/AnonymizationFieldUpdateProps' + type: array + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AnonymizationFieldsBulkCrudActionResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Applies a bulk action to multiple anonymization fields + tags: + - Bulk API + /api/security_ai_assistant/anonymization_fields/_find: + get: + description: Finds anonymization fields that match the given query. + operationId: FindAnonymizationFields + parameters: + - in: query + name: fields + required: false + schema: + items: + type: string + type: array + - description: Search query + in: query + name: filter + required: false + schema: + type: string + - description: Field to sort by + in: query + name: sort_field + required: false + schema: + $ref: '#/components/schemas/FindAnonymizationFieldsSortField' + - description: Sort order + in: query + name: sort_order + required: false + schema: + $ref: '#/components/schemas/SortOrder' + - description: Page number + in: query + name: page + required: false + schema: + default: 1 + minimum: 1 + type: integer + - description: AnonymizationFields per page + in: query + name: per_page + required: false + schema: + default: 20 + minimum: 0 + type: integer + responses: + '200': + content: + application/json: + schema: + type: object + properties: + data: + items: + $ref: '#/components/schemas/AnonymizationFieldResponse' + type: array + page: + type: integer + perPage: + type: integer + total: + type: integer + required: + - page + - perPage + - total + - data + description: Successful response + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Finds anonymization fields that match the given query. + tags: + - AnonymizationFields API + /api/security_ai_assistant/current_user/conversations: + post: + description: Create a conversation + operationId: CreateConversation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationCreateProps' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Create a conversation + tags: + - Conversation API + /api/security_ai_assistant/current_user/conversations/_find: + get: + description: Finds conversations that match the given query. + operationId: FindConversations + parameters: + - in: query + name: fields + required: false + schema: + items: + type: string + type: array + - description: Search query + in: query + name: filter + required: false + schema: + type: string + - description: Field to sort by + in: query + name: sort_field + required: false + schema: + $ref: '#/components/schemas/FindConversationsSortField' + - description: Sort order + in: query + name: sort_order + required: false + schema: + $ref: '#/components/schemas/SortOrder' + - description: Page number + in: query + name: page + required: false + schema: + default: 1 + minimum: 1 + type: integer + - description: Conversations per page + in: query + name: per_page + required: false + schema: + default: 20 + minimum: 0 + type: integer + responses: + '200': + content: + application/json: + schema: + type: object + properties: + data: + items: + $ref: '#/components/schemas/ConversationResponse' + type: array + page: + type: integer + perPage: + type: integer + total: + type: integer + required: + - page + - perPage + - total + - data + description: Successful response + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Finds conversations that match the given query. + tags: + - Conversations API + '/api/security_ai_assistant/current_user/conversations/{id}': + delete: + description: Deletes a single conversation using the `id` field. + operationId: DeleteConversation + parameters: + - description: The conversation's `id` value. + in: path + name: id + required: true + schema: + $ref: '#/components/schemas/NonEmptyString' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Deletes a single conversation using the `id` field. + tags: + - Conversation API + get: + description: Read a single conversation + operationId: ReadConversation + parameters: + - description: The conversation's `id` value. + in: path + name: id + required: true + schema: + $ref: '#/components/schemas/NonEmptyString' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Read a single conversation + tags: + - Conversations API + put: + description: Update a single conversation + operationId: UpdateConversation + parameters: + - description: The conversation's `id` value. + in: path + name: id + required: true + schema: + $ref: '#/components/schemas/NonEmptyString' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationUpdateProps' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Update a conversation + tags: + - Conversation API + /api/security_ai_assistant/prompts/_bulk_action: + post: + description: >- + The bulk action is applied to all prompts that match the filter or to + the list of prompts by their IDs. + operationId: PerformBulkAction + requestBody: + content: + application/json: + schema: + type: object + properties: + create: + items: + $ref: '#/components/schemas/PromptCreateProps' + type: array + delete: + type: object + properties: + ids: + description: Array of prompts IDs + items: + type: string + minItems: 1 + type: array + query: + description: Query to filter promps + type: string + update: + items: + $ref: '#/components/schemas/PromptUpdateProps' + type: array + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PromptsBulkCrudActionResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Applies a bulk action to multiple prompts + tags: + - Bulk API + /api/security_ai_assistant/prompts/_find: + get: + description: Finds prompts that match the given query. + operationId: FindPrompts + parameters: + - in: query + name: fields + required: false + schema: + items: + type: string + type: array + - description: Search query + in: query + name: filter + required: false + schema: + type: string + - description: Field to sort by + in: query + name: sort_field + required: false + schema: + $ref: '#/components/schemas/FindPromptsSortField' + - description: Sort order + in: query + name: sort_order + required: false + schema: + $ref: '#/components/schemas/SortOrder' + - description: Page number + in: query + name: page + required: false + schema: + default: 1 + minimum: 1 + type: integer + - description: Prompts per page + in: query + name: per_page + required: false + schema: + default: 20 + minimum: 0 + type: integer + responses: + '200': + content: + application/json: + schema: + type: object + properties: + data: + items: + $ref: '#/components/schemas/PromptResponse' + type: array + page: + type: integer + perPage: + type: integer + total: + type: integer + required: + - page + - perPage + - total + - data + description: Successful response + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Finds prompts that match the given query. + tags: + - Prompts API +components: + schemas: + AnonymizationFieldCreateProps: + type: object + properties: + allowed: + type: boolean + anonymized: + type: boolean + field: + type: string + required: + - field + AnonymizationFieldDetailsInError: + type: object + properties: + id: + type: string + name: + type: string + required: + - id + AnonymizationFieldResponse: + type: object + properties: + allowed: + type: boolean + anonymized: + type: boolean + createdAt: + type: string + createdBy: + type: string + field: + type: string + id: + $ref: '#/components/schemas/NonEmptyString' + namespace: + description: Kibana space + type: string + timestamp: + $ref: '#/components/schemas/NonEmptyString' + updatedAt: + type: string + updatedBy: + type: string + required: + - id + - field + AnonymizationFieldsBulkActionSkipReason: + enum: + - ANONYMIZATION_FIELD_NOT_MODIFIED + type: string + AnonymizationFieldsBulkActionSkipResult: + type: object + properties: + id: + type: string + name: + type: string + skip_reason: + $ref: '#/components/schemas/AnonymizationFieldsBulkActionSkipReason' + required: + - id + - skip_reason + AnonymizationFieldsBulkCrudActionResponse: + type: object + properties: + anonymization_fields_count: + type: integer + attributes: + type: object + properties: + errors: + items: + $ref: '#/components/schemas/NormalizedAnonymizationFieldError' + type: array + results: + $ref: '#/components/schemas/AnonymizationFieldsBulkCrudActionResults' + summary: + $ref: '#/components/schemas/BulkCrudActionSummary' + required: + - results + - summary + message: + type: string + status_code: + type: integer + success: + type: boolean + required: + - attributes + AnonymizationFieldsBulkCrudActionResults: + type: object + properties: + created: + items: + $ref: '#/components/schemas/AnonymizationFieldResponse' + type: array + deleted: + items: + type: string + type: array + skipped: + items: + $ref: '#/components/schemas/AnonymizationFieldsBulkActionSkipResult' + type: array + updated: + items: + $ref: '#/components/schemas/AnonymizationFieldResponse' + type: array + required: + - updated + - created + - deleted + - skipped + AnonymizationFieldUpdateProps: + type: object + properties: + allowed: + type: boolean + anonymized: + type: boolean + id: + type: string + required: + - id + ApiConfig: + type: object + properties: + actionTypeId: + description: action type id + type: string + connectorId: + description: connector id + type: string + defaultSystemPromptId: + description: defaultSystemPromptId + type: string + model: + description: model + type: string + provider: + $ref: '#/components/schemas/Provider' + description: Provider + required: + - connectorId + - actionTypeId + BulkCrudActionSummary: + type: object + properties: + failed: + type: integer + skipped: + type: integer + succeeded: + type: integer + total: + type: integer + required: + - failed + - skipped + - succeeded + - total + ConversationCategory: + description: The conversation category. + enum: + - assistant + - insights + type: string + ConversationConfidence: + description: The conversation confidence. + enum: + - low + - medium + - high + type: string + ConversationCreateProps: + type: object + properties: + apiConfig: + $ref: '#/components/schemas/ApiConfig' + description: LLM API configuration. + category: + $ref: '#/components/schemas/ConversationCategory' + description: The conversation category. + excludeFromLastConversationStorage: + description: excludeFromLastConversationStorage. + type: boolean + id: + description: The conversation id. + type: string + isDefault: + description: Is default conversation. + type: boolean + messages: + description: The conversation messages. + items: + $ref: '#/components/schemas/Message' + type: array + replacements: + $ref: '#/components/schemas/Replacements' + title: + description: The conversation title. + type: string + required: + - title + ConversationResponse: + type: object + properties: + apiConfig: + $ref: '#/components/schemas/ApiConfig' + description: LLM API configuration. + category: + $ref: '#/components/schemas/ConversationCategory' + description: The conversation category. + createdAt: + description: The last time conversation was updated. + type: string + excludeFromLastConversationStorage: + description: excludeFromLastConversationStorage. + type: boolean + id: + $ref: '#/components/schemas/NonEmptyString' + isDefault: + description: Is default conversation. + type: boolean + messages: + description: The conversation messages. + items: + $ref: '#/components/schemas/Message' + type: array + namespace: + description: Kibana space + type: string + replacements: + $ref: '#/components/schemas/Replacements' + summary: + $ref: '#/components/schemas/ConversationSummary' + timestamp: + $ref: '#/components/schemas/NonEmptyString' + title: + description: The conversation title. + type: string + updatedAt: + description: The last time conversation was updated. + type: string + users: + items: + $ref: '#/components/schemas/User' + type: array + required: + - id + - title + - createdAt + - users + - namespace + - category + ConversationSummary: + type: object + properties: + confidence: + $ref: '#/components/schemas/ConversationConfidence' + description: >- + How confident you are about this being a correct and useful + learning. + content: + description: Summary text of the conversation over time. + type: string + public: + description: Define if summary is marked as publicly available. + type: boolean + timestamp: + $ref: '#/components/schemas/NonEmptyString' + description: The timestamp summary was updated. + ConversationUpdateProps: + type: object + properties: + apiConfig: + $ref: '#/components/schemas/ApiConfig' + description: LLM API configuration. + category: + $ref: '#/components/schemas/ConversationCategory' + description: The conversation category. + excludeFromLastConversationStorage: + description: excludeFromLastConversationStorage. + type: boolean + id: + $ref: '#/components/schemas/NonEmptyString' + messages: + description: The conversation messages. + items: + $ref: '#/components/schemas/Message' + type: array + replacements: + $ref: '#/components/schemas/Replacements' + summary: + $ref: '#/components/schemas/ConversationSummary' + title: + description: The conversation title. + type: string + required: + - id + FindAnonymizationFieldsSortField: + enum: + - created_at + - anonymized + - allowed + - field + - updated_at + type: string + FindConversationsSortField: + enum: + - created_at + - is_default + - title + - updated_at + type: string + FindPromptsSortField: + enum: + - created_at + - is_default + - name + - updated_at + type: string + Message: + description: AI assistant conversation message. + type: object + properties: + content: + description: Message content. + type: string + isError: + description: Is error message. + type: boolean + reader: + $ref: '#/components/schemas/Reader' + description: Message content. + role: + $ref: '#/components/schemas/MessageRole' + description: Message role. + timestamp: + $ref: '#/components/schemas/NonEmptyString' + description: The timestamp message was sent or received. + traceData: + $ref: '#/components/schemas/TraceData' + description: trace Data + required: + - timestamp + - content + - role + MessageRole: + description: Message role. + enum: + - system + - user + - assistant + type: string + NonEmptyString: + description: A string that is not empty and does not contain only whitespace + minLength: 1 + pattern: ^(?! *$).+$ + type: string + NormalizedAnonymizationFieldError: + type: object + properties: + anonymization_fields: + items: + $ref: '#/components/schemas/AnonymizationFieldDetailsInError' + type: array + err_code: + type: string + message: + type: string + status_code: + type: integer + required: + - message + - status_code + - anonymization_fields + NormalizedPromptError: + type: object + properties: + err_code: + type: string + message: + type: string + prompts: + items: + $ref: '#/components/schemas/PromptDetailsInError' + type: array + status_code: + type: integer + required: + - message + - status_code + - prompts + PromptCreateProps: + type: object + properties: + categories: + items: + type: string + type: array + color: + type: string + consumer: + type: string + content: + type: string + isDefault: + type: boolean + isNewConversationDefault: + type: boolean + name: + type: string + promptType: + $ref: '#/components/schemas/PromptType' + required: + - name + - content + - promptType + PromptDetailsInError: + type: object + properties: + id: + type: string + name: + type: string + required: + - id + PromptResponse: + type: object + properties: + categories: + items: + type: string + type: array + color: + type: string + consumer: + type: string + content: + type: string + createdAt: + type: string + createdBy: + type: string + id: + $ref: '#/components/schemas/NonEmptyString' + isDefault: + type: boolean + isNewConversationDefault: + type: boolean + name: + type: string + namespace: + description: Kibana space + type: string + promptType: + $ref: '#/components/schemas/PromptType' + timestamp: + $ref: '#/components/schemas/NonEmptyString' + updatedAt: + type: string + updatedBy: + type: string + users: + items: + $ref: '#/components/schemas/User' + type: array + required: + - id + - name + - promptType + - content + PromptsBulkActionSkipReason: + enum: + - PROMPT_FIELD_NOT_MODIFIED + type: string + PromptsBulkActionSkipResult: + type: object + properties: + id: + type: string + name: + type: string + skip_reason: + $ref: '#/components/schemas/PromptsBulkActionSkipReason' + required: + - id + - skip_reason + PromptsBulkCrudActionResponse: + type: object + properties: + attributes: + type: object + properties: + errors: + items: + $ref: '#/components/schemas/NormalizedPromptError' + type: array + results: + $ref: '#/components/schemas/PromptsBulkCrudActionResults' + summary: + $ref: '#/components/schemas/BulkCrudActionSummary' + required: + - results + - summary + message: + type: string + prompts_count: + type: integer + status_code: + type: integer + success: + type: boolean + required: + - attributes + PromptsBulkCrudActionResults: + type: object + properties: + created: + items: + $ref: '#/components/schemas/PromptResponse' + type: array + deleted: + items: + type: string + type: array + skipped: + items: + $ref: '#/components/schemas/PromptsBulkActionSkipResult' + type: array + updated: + items: + $ref: '#/components/schemas/PromptResponse' + type: array + required: + - updated + - created + - deleted + - skipped + PromptType: + description: Prompt type + enum: + - system + - quick + type: string + PromptUpdateProps: + type: object + properties: + categories: + items: + type: string + type: array + color: + type: string + consumer: + type: string + content: + type: string + id: + type: string + isDefault: + type: boolean + isNewConversationDefault: + type: boolean + required: + - id + Provider: + description: Provider + enum: + - OpenAI + - Azure OpenAI + type: string + Reader: + additionalProperties: true + type: object + Replacements: + additionalProperties: + type: string + description: Replacements object used to anonymize/deanomymize messsages + type: object + SortOrder: + enum: + - asc + - desc + type: string + TraceData: + description: trace Data + type: object + properties: + traceId: + description: 'Could be any string, not necessarily a UUID' + type: string + transactionId: + description: 'Could be any string, not necessarily a UUID' + type: string + User: + description: 'Could be any string, not necessarily a UUID' + type: object + properties: + id: + description: User id + type: string + name: + description: User name + type: string + securitySchemes: + BasicAuth: + scheme: basic + type: http +security: + - BasicAuth: [] +tags: ! '' diff --git a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml new file mode 100644 index 00000000000000..701df70006d887 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -0,0 +1,114 @@ +openapi: 3.0.3 +info: + description: Manage and interact with Security Assistant resources. + title: Security AI Assistant API (Elastic Cloud & self-hosted) + version: '2023-10-31' +servers: + - url: 'http://{kibana_host}:{port}' + variables: + kibana_host: + default: localhost + port: + default: '5601' +paths: + /api/security_ai_assistant/chat/complete: + post: + description: Creates a model response for the given chat conversation. + operationId: ChatComplete + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ChatCompleteProps' + required: true + responses: + '200': + content: + application/octet-stream: + schema: + format: binary + type: string + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Creates a model response for the given chat conversation. + tags: + - Chat Complete API +components: + schemas: + ChatCompleteProps: + type: object + properties: + connectorId: + type: string + conversationId: + type: string + isStream: + type: boolean + langSmithApiKey: + type: string + langSmithProject: + type: string + messages: + items: + $ref: '#/components/schemas/ChatMessage' + type: array + model: + type: string + persist: + type: boolean + promptId: + type: string + responseLanguage: + type: string + required: + - messages + - persist + - connectorId + ChatMessage: + description: AI assistant message. + type: object + properties: + content: + description: Message content. + type: string + data: + $ref: '#/components/schemas/MessageData' + description: ECS object to attach to the context of the message. + fields_to_anonymize: + items: + type: string + type: array + role: + $ref: '#/components/schemas/ChatMessageRole' + description: Message role. + required: + - role + ChatMessageRole: + description: Message role. + enum: + - system + - user + - assistant + type: string + MessageData: + additionalProperties: true + type: object + securitySchemes: + BasicAuth: + scheme: basic + type: http +security: + - BasicAuth: [] +tags: ! '' diff --git a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_1.bundled.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_1.bundled.schema.yaml new file mode 100644 index 00000000000000..e0579f55dfbb9a --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_1.bundled.schema.yaml @@ -0,0 +1,1133 @@ +openapi: 3.0.3 +info: + description: Manage and interact with Security Assistant resources. + title: Security AI Assistant API (Elastic Cloud Serverless) + version: '1' +servers: + - url: 'http://{kibana_host}:{port}' + variables: + kibana_host: + default: localhost + port: + default: '5601' +paths: + /api/security_ai_assistant/anonymization_fields/_bulk_action: + post: + description: >- + The bulk action is applied to all anonymization fields that match the + filter or to the list of anonymization fields by their IDs. + operationId: PerformBulkAction + requestBody: + content: + application/json: + schema: + type: object + properties: + create: + items: + $ref: '#/components/schemas/AnonymizationFieldCreateProps' + type: array + delete: + type: object + properties: + ids: + description: Array of anonymization fields IDs + items: + type: string + minItems: 1 + type: array + query: + description: Query to filter anonymization fields + type: string + update: + items: + $ref: '#/components/schemas/AnonymizationFieldUpdateProps' + type: array + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AnonymizationFieldsBulkCrudActionResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Applies a bulk action to multiple anonymization fields + tags: + - Bulk API + /api/security_ai_assistant/anonymization_fields/_find: + get: + description: Finds anonymization fields that match the given query. + operationId: FindAnonymizationFields + parameters: + - in: query + name: fields + required: false + schema: + items: + type: string + type: array + - description: Search query + in: query + name: filter + required: false + schema: + type: string + - description: Field to sort by + in: query + name: sort_field + required: false + schema: + $ref: '#/components/schemas/FindAnonymizationFieldsSortField' + - description: Sort order + in: query + name: sort_order + required: false + schema: + $ref: '#/components/schemas/SortOrder' + - description: Page number + in: query + name: page + required: false + schema: + default: 1 + minimum: 1 + type: integer + - description: AnonymizationFields per page + in: query + name: per_page + required: false + schema: + default: 20 + minimum: 0 + type: integer + responses: + '200': + content: + application/json: + schema: + type: object + properties: + data: + items: + $ref: '#/components/schemas/AnonymizationFieldResponse' + type: array + page: + type: integer + perPage: + type: integer + total: + type: integer + required: + - page + - perPage + - total + - data + description: Successful response + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Finds anonymization fields that match the given query. + tags: + - AnonymizationFields API + /api/security_ai_assistant/current_user/conversations: + post: + description: Create a conversation + operationId: CreateConversation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationCreateProps' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Create a conversation + tags: + - Conversation API + /api/security_ai_assistant/current_user/conversations/_find: + get: + description: Finds conversations that match the given query. + operationId: FindConversations + parameters: + - in: query + name: fields + required: false + schema: + items: + type: string + type: array + - description: Search query + in: query + name: filter + required: false + schema: + type: string + - description: Field to sort by + in: query + name: sort_field + required: false + schema: + $ref: '#/components/schemas/FindConversationsSortField' + - description: Sort order + in: query + name: sort_order + required: false + schema: + $ref: '#/components/schemas/SortOrder' + - description: Page number + in: query + name: page + required: false + schema: + default: 1 + minimum: 1 + type: integer + - description: Conversations per page + in: query + name: per_page + required: false + schema: + default: 20 + minimum: 0 + type: integer + responses: + '200': + content: + application/json: + schema: + type: object + properties: + data: + items: + $ref: '#/components/schemas/ConversationResponse' + type: array + page: + type: integer + perPage: + type: integer + total: + type: integer + required: + - page + - perPage + - total + - data + description: Successful response + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Finds conversations that match the given query. + tags: + - Conversations API + '/api/security_ai_assistant/current_user/conversations/{id}': + delete: + description: Deletes a single conversation using the `id` field. + operationId: DeleteConversation + parameters: + - description: The conversation's `id` value. + in: path + name: id + required: true + schema: + $ref: '#/components/schemas/NonEmptyString' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Deletes a single conversation using the `id` field. + tags: + - Conversation API + get: + description: Read a single conversation + operationId: ReadConversation + parameters: + - description: The conversation's `id` value. + in: path + name: id + required: true + schema: + $ref: '#/components/schemas/NonEmptyString' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Read a single conversation + tags: + - Conversations API + put: + description: Update a single conversation + operationId: UpdateConversation + parameters: + - description: The conversation's `id` value. + in: path + name: id + required: true + schema: + $ref: '#/components/schemas/NonEmptyString' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationUpdateProps' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ConversationResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Update a conversation + tags: + - Conversation API + /api/security_ai_assistant/prompts/_bulk_action: + post: + description: >- + The bulk action is applied to all prompts that match the filter or to + the list of prompts by their IDs. + operationId: PerformBulkAction + requestBody: + content: + application/json: + schema: + type: object + properties: + create: + items: + $ref: '#/components/schemas/PromptCreateProps' + type: array + delete: + type: object + properties: + ids: + description: Array of prompts IDs + items: + type: string + minItems: 1 + type: array + query: + description: Query to filter promps + type: string + update: + items: + $ref: '#/components/schemas/PromptUpdateProps' + type: array + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PromptsBulkCrudActionResponse' + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Applies a bulk action to multiple prompts + tags: + - Bulk API + /api/security_ai_assistant/prompts/_find: + get: + description: Finds prompts that match the given query. + operationId: FindPrompts + parameters: + - in: query + name: fields + required: false + schema: + items: + type: string + type: array + - description: Search query + in: query + name: filter + required: false + schema: + type: string + - description: Field to sort by + in: query + name: sort_field + required: false + schema: + $ref: '#/components/schemas/FindPromptsSortField' + - description: Sort order + in: query + name: sort_order + required: false + schema: + $ref: '#/components/schemas/SortOrder' + - description: Page number + in: query + name: page + required: false + schema: + default: 1 + minimum: 1 + type: integer + - description: Prompts per page + in: query + name: per_page + required: false + schema: + default: 20 + minimum: 0 + type: integer + responses: + '200': + content: + application/json: + schema: + type: object + properties: + data: + items: + $ref: '#/components/schemas/PromptResponse' + type: array + page: + type: integer + perPage: + type: integer + total: + type: integer + required: + - page + - perPage + - total + - data + description: Successful response + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Finds prompts that match the given query. + tags: + - Prompts API +components: + schemas: + AnonymizationFieldCreateProps: + type: object + properties: + allowed: + type: boolean + anonymized: + type: boolean + field: + type: string + required: + - field + AnonymizationFieldDetailsInError: + type: object + properties: + id: + type: string + name: + type: string + required: + - id + AnonymizationFieldResponse: + type: object + properties: + allowed: + type: boolean + anonymized: + type: boolean + createdAt: + type: string + createdBy: + type: string + field: + type: string + id: + $ref: '#/components/schemas/NonEmptyString' + namespace: + description: Kibana space + type: string + timestamp: + $ref: '#/components/schemas/NonEmptyString' + updatedAt: + type: string + updatedBy: + type: string + required: + - id + - field + AnonymizationFieldsBulkActionSkipReason: + enum: + - ANONYMIZATION_FIELD_NOT_MODIFIED + type: string + AnonymizationFieldsBulkActionSkipResult: + type: object + properties: + id: + type: string + name: + type: string + skip_reason: + $ref: '#/components/schemas/AnonymizationFieldsBulkActionSkipReason' + required: + - id + - skip_reason + AnonymizationFieldsBulkCrudActionResponse: + type: object + properties: + anonymization_fields_count: + type: integer + attributes: + type: object + properties: + errors: + items: + $ref: '#/components/schemas/NormalizedAnonymizationFieldError' + type: array + results: + $ref: '#/components/schemas/AnonymizationFieldsBulkCrudActionResults' + summary: + $ref: '#/components/schemas/BulkCrudActionSummary' + required: + - results + - summary + message: + type: string + status_code: + type: integer + success: + type: boolean + required: + - attributes + AnonymizationFieldsBulkCrudActionResults: + type: object + properties: + created: + items: + $ref: '#/components/schemas/AnonymizationFieldResponse' + type: array + deleted: + items: + type: string + type: array + skipped: + items: + $ref: '#/components/schemas/AnonymizationFieldsBulkActionSkipResult' + type: array + updated: + items: + $ref: '#/components/schemas/AnonymizationFieldResponse' + type: array + required: + - updated + - created + - deleted + - skipped + AnonymizationFieldUpdateProps: + type: object + properties: + allowed: + type: boolean + anonymized: + type: boolean + id: + type: string + required: + - id + ApiConfig: + type: object + properties: + actionTypeId: + description: action type id + type: string + connectorId: + description: connector id + type: string + defaultSystemPromptId: + description: defaultSystemPromptId + type: string + model: + description: model + type: string + provider: + $ref: '#/components/schemas/Provider' + description: Provider + required: + - connectorId + - actionTypeId + BulkCrudActionSummary: + type: object + properties: + failed: + type: integer + skipped: + type: integer + succeeded: + type: integer + total: + type: integer + required: + - failed + - skipped + - succeeded + - total + ConversationCategory: + description: The conversation category. + enum: + - assistant + - insights + type: string + ConversationConfidence: + description: The conversation confidence. + enum: + - low + - medium + - high + type: string + ConversationCreateProps: + type: object + properties: + apiConfig: + $ref: '#/components/schemas/ApiConfig' + description: LLM API configuration. + category: + $ref: '#/components/schemas/ConversationCategory' + description: The conversation category. + excludeFromLastConversationStorage: + description: excludeFromLastConversationStorage. + type: boolean + id: + description: The conversation id. + type: string + isDefault: + description: Is default conversation. + type: boolean + messages: + description: The conversation messages. + items: + $ref: '#/components/schemas/Message' + type: array + replacements: + $ref: '#/components/schemas/Replacements' + title: + description: The conversation title. + type: string + required: + - title + ConversationResponse: + type: object + properties: + apiConfig: + $ref: '#/components/schemas/ApiConfig' + description: LLM API configuration. + category: + $ref: '#/components/schemas/ConversationCategory' + description: The conversation category. + createdAt: + description: The last time conversation was updated. + type: string + excludeFromLastConversationStorage: + description: excludeFromLastConversationStorage. + type: boolean + id: + $ref: '#/components/schemas/NonEmptyString' + isDefault: + description: Is default conversation. + type: boolean + messages: + description: The conversation messages. + items: + $ref: '#/components/schemas/Message' + type: array + namespace: + description: Kibana space + type: string + replacements: + $ref: '#/components/schemas/Replacements' + summary: + $ref: '#/components/schemas/ConversationSummary' + timestamp: + $ref: '#/components/schemas/NonEmptyString' + title: + description: The conversation title. + type: string + updatedAt: + description: The last time conversation was updated. + type: string + users: + items: + $ref: '#/components/schemas/User' + type: array + required: + - id + - title + - createdAt + - users + - namespace + - category + ConversationSummary: + type: object + properties: + confidence: + $ref: '#/components/schemas/ConversationConfidence' + description: >- + How confident you are about this being a correct and useful + learning. + content: + description: Summary text of the conversation over time. + type: string + public: + description: Define if summary is marked as publicly available. + type: boolean + timestamp: + $ref: '#/components/schemas/NonEmptyString' + description: The timestamp summary was updated. + ConversationUpdateProps: + type: object + properties: + apiConfig: + $ref: '#/components/schemas/ApiConfig' + description: LLM API configuration. + category: + $ref: '#/components/schemas/ConversationCategory' + description: The conversation category. + excludeFromLastConversationStorage: + description: excludeFromLastConversationStorage. + type: boolean + id: + $ref: '#/components/schemas/NonEmptyString' + messages: + description: The conversation messages. + items: + $ref: '#/components/schemas/Message' + type: array + replacements: + $ref: '#/components/schemas/Replacements' + summary: + $ref: '#/components/schemas/ConversationSummary' + title: + description: The conversation title. + type: string + required: + - id + FindAnonymizationFieldsSortField: + enum: + - created_at + - anonymized + - allowed + - field + - updated_at + type: string + FindConversationsSortField: + enum: + - created_at + - is_default + - title + - updated_at + type: string + FindPromptsSortField: + enum: + - created_at + - is_default + - name + - updated_at + type: string + Message: + description: AI assistant conversation message. + type: object + properties: + content: + description: Message content. + type: string + isError: + description: Is error message. + type: boolean + reader: + $ref: '#/components/schemas/Reader' + description: Message content. + role: + $ref: '#/components/schemas/MessageRole' + description: Message role. + timestamp: + $ref: '#/components/schemas/NonEmptyString' + description: The timestamp message was sent or received. + traceData: + $ref: '#/components/schemas/TraceData' + description: trace Data + required: + - timestamp + - content + - role + MessageRole: + description: Message role. + enum: + - system + - user + - assistant + type: string + NonEmptyString: + description: A string that is not empty and does not contain only whitespace + minLength: 1 + pattern: ^(?! *$).+$ + type: string + NormalizedAnonymizationFieldError: + type: object + properties: + anonymization_fields: + items: + $ref: '#/components/schemas/AnonymizationFieldDetailsInError' + type: array + err_code: + type: string + message: + type: string + status_code: + type: integer + required: + - message + - status_code + - anonymization_fields + NormalizedPromptError: + type: object + properties: + err_code: + type: string + message: + type: string + prompts: + items: + $ref: '#/components/schemas/PromptDetailsInError' + type: array + status_code: + type: integer + required: + - message + - status_code + - prompts + PromptCreateProps: + type: object + properties: + categories: + items: + type: string + type: array + color: + type: string + consumer: + type: string + content: + type: string + isDefault: + type: boolean + isNewConversationDefault: + type: boolean + name: + type: string + promptType: + $ref: '#/components/schemas/PromptType' + required: + - name + - content + - promptType + PromptDetailsInError: + type: object + properties: + id: + type: string + name: + type: string + required: + - id + PromptResponse: + type: object + properties: + categories: + items: + type: string + type: array + color: + type: string + consumer: + type: string + content: + type: string + createdAt: + type: string + createdBy: + type: string + id: + $ref: '#/components/schemas/NonEmptyString' + isDefault: + type: boolean + isNewConversationDefault: + type: boolean + name: + type: string + namespace: + description: Kibana space + type: string + promptType: + $ref: '#/components/schemas/PromptType' + timestamp: + $ref: '#/components/schemas/NonEmptyString' + updatedAt: + type: string + updatedBy: + type: string + users: + items: + $ref: '#/components/schemas/User' + type: array + required: + - id + - name + - promptType + - content + PromptsBulkActionSkipReason: + enum: + - PROMPT_FIELD_NOT_MODIFIED + type: string + PromptsBulkActionSkipResult: + type: object + properties: + id: + type: string + name: + type: string + skip_reason: + $ref: '#/components/schemas/PromptsBulkActionSkipReason' + required: + - id + - skip_reason + PromptsBulkCrudActionResponse: + type: object + properties: + attributes: + type: object + properties: + errors: + items: + $ref: '#/components/schemas/NormalizedPromptError' + type: array + results: + $ref: '#/components/schemas/PromptsBulkCrudActionResults' + summary: + $ref: '#/components/schemas/BulkCrudActionSummary' + required: + - results + - summary + message: + type: string + prompts_count: + type: integer + status_code: + type: integer + success: + type: boolean + required: + - attributes + PromptsBulkCrudActionResults: + type: object + properties: + created: + items: + $ref: '#/components/schemas/PromptResponse' + type: array + deleted: + items: + type: string + type: array + skipped: + items: + $ref: '#/components/schemas/PromptsBulkActionSkipResult' + type: array + updated: + items: + $ref: '#/components/schemas/PromptResponse' + type: array + required: + - updated + - created + - deleted + - skipped + PromptType: + description: Prompt type + enum: + - system + - quick + type: string + PromptUpdateProps: + type: object + properties: + categories: + items: + type: string + type: array + color: + type: string + consumer: + type: string + content: + type: string + id: + type: string + isDefault: + type: boolean + isNewConversationDefault: + type: boolean + required: + - id + Provider: + description: Provider + enum: + - OpenAI + - Azure OpenAI + type: string + Reader: + additionalProperties: true + type: object + Replacements: + additionalProperties: + type: string + description: Replacements object used to anonymize/deanomymize messsages + type: object + SortOrder: + enum: + - asc + - desc + type: string + TraceData: + description: trace Data + type: object + properties: + traceId: + description: 'Could be any string, not necessarily a UUID' + type: string + transactionId: + description: 'Could be any string, not necessarily a UUID' + type: string + User: + description: 'Could be any string, not necessarily a UUID' + type: object + properties: + id: + description: User id + type: string + name: + description: User name + type: string + securitySchemes: + BasicAuth: + scheme: basic + type: http +security: + - BasicAuth: [] +tags: ! '' diff --git a/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml new file mode 100644 index 00000000000000..f04e55d6c5a557 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -0,0 +1,114 @@ +openapi: 3.0.3 +info: + description: Manage and interact with Security Assistant resources. + title: Security AI Assistant API (Elastic Cloud Serverless) + version: '2023-10-31' +servers: + - url: 'http://{kibana_host}:{port}' + variables: + kibana_host: + default: localhost + port: + default: '5601' +paths: + /api/security_ai_assistant/chat/complete: + post: + description: Creates a model response for the given chat conversation. + operationId: ChatComplete + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ChatCompleteProps' + required: true + responses: + '200': + content: + application/octet-stream: + schema: + format: binary + type: string + description: Indicates a successful call. + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + message: + type: string + statusCode: + type: number + description: Generic Error + summary: Creates a model response for the given chat conversation. + tags: + - Chat Complete API +components: + schemas: + ChatCompleteProps: + type: object + properties: + connectorId: + type: string + conversationId: + type: string + isStream: + type: boolean + langSmithApiKey: + type: string + langSmithProject: + type: string + messages: + items: + $ref: '#/components/schemas/ChatMessage' + type: array + model: + type: string + persist: + type: boolean + promptId: + type: string + responseLanguage: + type: string + required: + - messages + - persist + - connectorId + ChatMessage: + description: AI assistant message. + type: object + properties: + content: + description: Message content. + type: string + data: + $ref: '#/components/schemas/MessageData' + description: ECS object to attach to the context of the message. + fields_to_anonymize: + items: + type: string + type: array + role: + $ref: '#/components/schemas/ChatMessageRole' + description: Message role. + required: + - role + ChatMessageRole: + description: Message role. + enum: + - system + - user + - assistant + type: string + MessageData: + additionalProperties: true + type: object + securitySchemes: + BasicAuth: + scheme: basic + type: http +security: + - BasicAuth: [] +tags: ! '' diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/actions_connector/post_actions_connector_execute_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/actions_connector/post_actions_connector_execute_route.schema.yaml index c21d3b080767f0..e4a1e1ce0f7b94 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/actions_connector/post_actions_connector_execute_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/actions_connector/post_actions_connector_execute_route.schema.yaml @@ -5,8 +5,9 @@ info: paths: /internal/elastic_assistant/actions/connector/{connectorId}/_execute: post: - operationId: ExecuteConnector x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: ExecuteConnector description: Execute Elastic Assistant connector by id summary: Execute Elastic Assistant connector tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen.ts index 96b9b7fa643f78..7c94bf17484995 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen.ts @@ -10,7 +10,7 @@ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. * * info: - * title: Bulk Actions API endpoint + * title: Bulk Anonymization Fields Actions API endpoint * version: 1 */ @@ -18,14 +18,20 @@ import { z } from 'zod'; import { NonEmptyString } from '../common_attributes.gen'; -export type BulkActionSkipReason = z.infer; -export const BulkActionSkipReason = z.literal('ANONYMIZATION_FIELD_NOT_MODIFIED'); - -export type BulkActionSkipResult = z.infer; -export const BulkActionSkipResult = z.object({ +export type AnonymizationFieldsBulkActionSkipReason = z.infer< + typeof AnonymizationFieldsBulkActionSkipReason +>; +export const AnonymizationFieldsBulkActionSkipReason = z.literal( + 'ANONYMIZATION_FIELD_NOT_MODIFIED' +); + +export type AnonymizationFieldsBulkActionSkipResult = z.infer< + typeof AnonymizationFieldsBulkActionSkipResult +>; +export const AnonymizationFieldsBulkActionSkipResult = z.object({ id: z.string(), name: z.string().optional(), - skip_reason: BulkActionSkipReason, + skip_reason: AnonymizationFieldsBulkActionSkipReason, }); export type AnonymizationFieldDetailsInError = z.infer; @@ -59,12 +65,14 @@ export const AnonymizationFieldResponse = z.object({ namespace: z.string().optional(), }); -export type BulkCrudActionResults = z.infer; -export const BulkCrudActionResults = z.object({ +export type AnonymizationFieldsBulkCrudActionResults = z.infer< + typeof AnonymizationFieldsBulkCrudActionResults +>; +export const AnonymizationFieldsBulkCrudActionResults = z.object({ updated: z.array(AnonymizationFieldResponse), created: z.array(AnonymizationFieldResponse), deleted: z.array(z.string()), - skipped: z.array(BulkActionSkipResult), + skipped: z.array(AnonymizationFieldsBulkActionSkipResult), }); export type BulkCrudActionSummary = z.infer; @@ -75,14 +83,16 @@ export const BulkCrudActionSummary = z.object({ total: z.number().int(), }); -export type BulkCrudActionResponse = z.infer; -export const BulkCrudActionResponse = z.object({ +export type AnonymizationFieldsBulkCrudActionResponse = z.infer< + typeof AnonymizationFieldsBulkCrudActionResponse +>; +export const AnonymizationFieldsBulkCrudActionResponse = z.object({ success: z.boolean().optional(), status_code: z.number().int().optional(), message: z.string().optional(), anonymization_fields_count: z.number().int().optional(), attributes: z.object({ - results: BulkCrudActionResults, + results: AnonymizationFieldsBulkCrudActionResults, summary: BulkCrudActionSummary, errors: z.array(NormalizedAnonymizationFieldError).optional(), }), @@ -123,4 +133,4 @@ export const PerformBulkActionRequestBody = z.object({ export type PerformBulkActionRequestBodyInput = z.input; export type PerformBulkActionResponse = z.infer; -export const PerformBulkActionResponse = BulkCrudActionResponse; +export const PerformBulkActionResponse = AnonymizationFieldsBulkCrudActionResponse; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.schema.yaml index 9e2623966f129a..f077f70396df57 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.schema.yaml @@ -1,12 +1,13 @@ openapi: 3.0.0 info: - title: Bulk Actions API endpoint + title: Bulk Anonymization Fields Actions API endpoint version: '1' paths: - /internal/elastic_assistant/anonymization_fields/_bulk_action: + /api/security_ai_assistant/anonymization_fields/_bulk_action: post: - operationId: PerformBulkAction x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: PerformBulkAction summary: Applies a bulk action to multiple anonymization fields description: The bulk action is applied to all anonymization fields that match the filter or to the list of anonymization fields by their IDs. tags: @@ -33,7 +34,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BulkCrudActionResponse' + $ref: '#/components/schemas/AnonymizationFieldsBulkCrudActionResponse' 400: description: Generic Error content: @@ -50,12 +51,12 @@ paths: components: schemas: - BulkActionSkipReason: + AnonymizationFieldsBulkActionSkipReason: type: string enum: - ANONYMIZATION_FIELD_NOT_MODIFIED - BulkActionSkipResult: + AnonymizationFieldsBulkActionSkipResult: type: object properties: id: @@ -63,7 +64,7 @@ components: name: type: string skip_reason: - $ref: '#/components/schemas/BulkActionSkipReason' + $ref: '#/components/schemas/AnonymizationFieldsBulkActionSkipReason' required: - id - skip_reason @@ -124,7 +125,7 @@ components: type: string description: Kibana space - BulkCrudActionResults: + AnonymizationFieldsBulkCrudActionResults: type: object properties: updated: @@ -142,7 +143,7 @@ components: skipped: type: array items: - $ref: '#/components/schemas/BulkActionSkipResult' + $ref: '#/components/schemas/AnonymizationFieldsBulkActionSkipResult' required: - updated - created @@ -166,7 +167,7 @@ components: - succeeded - total - BulkCrudActionResponse: + AnonymizationFieldsBulkCrudActionResponse: type: object properties: success: @@ -181,7 +182,7 @@ components: type: object properties: results: - $ref: '#/components/schemas/BulkCrudActionResults' + $ref: '#/components/schemas/AnonymizationFieldsBulkCrudActionResults' summary: $ref: '#/components/schemas/BulkCrudActionSummary' errors: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.schema.yaml index 3541c3a1c649b5..3ec1f398df65c3 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/anonymization_fields/find_anonymization_fields_route.schema.yaml @@ -3,10 +3,11 @@ info: title: Find AnonymizationFields API endpoint version: '1' paths: - /internal/elastic_assistant/anonymization_fields/_find: + /api/security_ai_assistant/anonymization_fields/_find: get: - operationId: FindAnonymizationFields x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: FindAnonymizationFields description: Finds anonymization fields that match the given query. summary: Finds anonymization fields that match the given query. tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/cancel_attack_discovery_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/cancel_attack_discovery_route.schema.yaml index 553d741089cd0b..fb9ae33f57cfb9 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/cancel_attack_discovery_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/cancel_attack_discovery_route.schema.yaml @@ -5,8 +5,9 @@ info: paths: /internal/elastic_assistant/attack_discovery/cancel/{connectorId}: put: - operationId: AttackDiscoveryCancel x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: AttackDiscoveryCancel description: Cancel relevant data for performing an attack discovery like pending requests summary: Cancel relevant data for performing an attack discovery tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/get_attack_discovery_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/get_attack_discovery_route.schema.yaml index 33638a588679a6..bd4bfb7c667b22 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/get_attack_discovery_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/get_attack_discovery_route.schema.yaml @@ -5,8 +5,9 @@ info: paths: /internal/elastic_assistant/attack_discovery/{connectorId}: get: - operationId: AttackDiscoveryGet x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: AttackDiscoveryGet description: Get relevant data for performing an attack discovery like pending requests summary: Get relevant data for performing an attack discovery tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/post_attack_discovery_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/post_attack_discovery_route.schema.yaml index 1c658174abd5c8..e613fbf841a219 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/post_attack_discovery_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/post_attack_discovery_route.schema.yaml @@ -8,8 +8,9 @@ components: paths: /internal/elastic_assistant/attack_discovery: post: - operationId: AttackDiscoveryPost x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: AttackDiscoveryPost description: Generate attack discoveries from alerts summary: Generate attack discoveries from alerts via the Elastic Assistant tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml index 8e8325e1501c7e..0f07d1f58afe5b 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.schema.yaml @@ -5,8 +5,9 @@ info: paths: /internal/elastic_assistant/capabilities: get: - operationId: GetCapabilities x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: GetCapabilities description: Get Elastic Assistant capabilities for the requesting plugin summary: Get Elastic Assistant capabilities tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/chat/post_chat_complete_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/chat/post_chat_complete_route.schema.yaml index 21c348251b0399..8758e34b135981 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/chat/post_chat_complete_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/chat/post_chat_complete_route.schema.yaml @@ -3,10 +3,11 @@ info: title: Chat Complete API endpoint version: '2023-10-31' paths: - /api/elastic_assistant/chat/complete: + /api/security_ai_assistant/chat/complete: post: - operationId: ChatComplete x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: ChatComplete description: Creates a model response for the given chat conversation. summary: Creates a model response for the given chat conversation. tags: @@ -86,7 +87,7 @@ components: promptId: type: string isStream: - type: boolean + type: boolean responseLanguage: type: string langSmithProject: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/bulk_crud_conversations_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/bulk_crud_conversations_route.gen.ts index 40ff05b4961782..65ea85b3f01908 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/bulk_crud_conversations_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/bulk_crud_conversations_route.gen.ts @@ -10,7 +10,7 @@ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. * * info: - * title: Bulk Actions API endpoint + * title: Bulk Conversations Actions API endpoint * version: 1 */ @@ -22,14 +22,14 @@ import { ConversationResponse, } from './common_attributes.gen'; -export type BulkActionSkipReason = z.infer; -export const BulkActionSkipReason = z.literal('CONVERSATION_NOT_MODIFIED'); +export type ConversationsBulkActionSkipReason = z.infer; +export const ConversationsBulkActionSkipReason = z.literal('CONVERSATION_NOT_MODIFIED'); -export type BulkActionSkipResult = z.infer; -export const BulkActionSkipResult = z.object({ +export type ConversationsBulkActionSkipResult = z.infer; +export const ConversationsBulkActionSkipResult = z.object({ id: z.string(), name: z.string().optional(), - skip_reason: BulkActionSkipReason, + skip_reason: ConversationsBulkActionSkipReason, }); export type ConversationDetailsInError = z.infer; @@ -46,12 +46,12 @@ export const NormalizedConversationError = z.object({ conversations: z.array(ConversationDetailsInError), }); -export type BulkCrudActionResults = z.infer; -export const BulkCrudActionResults = z.object({ +export type ConversationsBulkCrudActionResults = z.infer; +export const ConversationsBulkCrudActionResults = z.object({ updated: z.array(ConversationResponse), created: z.array(ConversationResponse), deleted: z.array(z.string()), - skipped: z.array(BulkActionSkipResult), + skipped: z.array(ConversationsBulkActionSkipResult), }); export type BulkCrudActionSummary = z.infer; @@ -62,14 +62,16 @@ export const BulkCrudActionSummary = z.object({ total: z.number().int(), }); -export type BulkCrudActionResponse = z.infer; -export const BulkCrudActionResponse = z.object({ +export type ConversationsBulkCrudActionResponse = z.infer< + typeof ConversationsBulkCrudActionResponse +>; +export const ConversationsBulkCrudActionResponse = z.object({ success: z.boolean().optional(), status_code: z.number().int().optional(), message: z.string().optional(), conversations_count: z.number().int().optional(), attributes: z.object({ - results: BulkCrudActionResults, + results: ConversationsBulkCrudActionResults, summary: BulkCrudActionSummary, errors: z.array(NormalizedConversationError).optional(), }), @@ -96,4 +98,4 @@ export const PerformBulkActionRequestBody = z.object({ export type PerformBulkActionRequestBodyInput = z.input; export type PerformBulkActionResponse = z.infer; -export const PerformBulkActionResponse = BulkCrudActionResponse; +export const PerformBulkActionResponse = ConversationsBulkCrudActionResponse; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/bulk_crud_conversations_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/bulk_crud_conversations_route.schema.yaml index 07685082057084..7517c32b1075bb 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/bulk_crud_conversations_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/bulk_crud_conversations_route.schema.yaml @@ -1,12 +1,13 @@ openapi: 3.0.0 info: - title: Bulk Actions API endpoint + title: Bulk Conversations Actions API endpoint version: '1' paths: - /internal/elastic_assistant/conversations/_bulk_action: + /internal/elastic_assistant/current_user/conversations/_bulk_action: post: - operationId: PerformBulkAction x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: PerformBulkAction summary: Applies a bulk action to multiple conversations description: The bulk action is applied to all conversations that match the filter or to the list of conversations by their IDs. tags: @@ -33,7 +34,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BulkCrudActionResponse' + $ref: '#/components/schemas/ConversationsBulkCrudActionResponse' 400: description: Generic Error content: @@ -50,12 +51,12 @@ paths: components: schemas: - BulkActionSkipReason: + ConversationsBulkActionSkipReason: type: string enum: - CONVERSATION_NOT_MODIFIED - BulkActionSkipResult: + ConversationsBulkActionSkipResult: type: object properties: id: @@ -63,7 +64,7 @@ components: name: type: string skip_reason: - $ref: '#/components/schemas/BulkActionSkipReason' + $ref: '#/components/schemas/ConversationsBulkActionSkipReason' required: - id - skip_reason @@ -96,7 +97,7 @@ components: - status_code - conversations - BulkCrudActionResults: + ConversationsBulkCrudActionResults: type: object properties: updated: @@ -114,7 +115,7 @@ components: skipped: type: array items: - $ref: '#/components/schemas/BulkActionSkipResult' + $ref: '#/components/schemas/ConversationsBulkActionSkipResult' required: - updated - created @@ -138,7 +139,7 @@ components: - succeeded - total - BulkCrudActionResponse: + ConversationsBulkCrudActionResponse: type: object properties: success: @@ -153,7 +154,7 @@ components: type: object properties: results: - $ref: '#/components/schemas/BulkCrudActionResults' + $ref: '#/components/schemas/ConversationsBulkCrudActionResults' summary: $ref: '#/components/schemas/BulkCrudActionSummary' errors: @@ -180,4 +181,3 @@ components: minItems: 1 items: type: string - \ No newline at end of file diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/crud_conversation_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/crud_conversation_route.schema.yaml index fc2f86e8a86543..ae5ca7d325a6a6 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/crud_conversation_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/crud_conversation_route.schema.yaml @@ -3,10 +3,11 @@ info: title: Create Conversation API endpoint version: '1' paths: - /internal/elastic_assistant/conversations: + /api/security_ai_assistant/current_user/conversations: post: - operationId: CreateConversation x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: CreateConversation description: Create a conversation summary: Create a conversation tags: @@ -38,10 +39,11 @@ paths: message: type: string - /internal/elastic_assistant/conversations/{id}: + /api/security_ai_assistant/current_user/conversations/{id}: get: - operationId: ReadConversation x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: ReadConversation description: Read a single conversation summary: Read a single conversation tags: @@ -74,8 +76,9 @@ paths: message: type: string put: - operationId: UpdateConversation x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: UpdateConversation description: Update a single conversation summary: Update a conversation tags: @@ -114,8 +117,9 @@ paths: message: type: string delete: - operationId: DeleteConversation x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: DeleteConversation description: Deletes a single conversation using the `id` field. summary: Deletes a single conversation using the `id` field. tags: @@ -148,10 +152,11 @@ paths: message: type: string - /internal/elastic_assistant/conversations/{id}/messages: + /internal/elastic_assistant/current_user/conversations/{id}/messages: post: - operationId: AppendConversationMessage x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: AppendConversationMessage description: Append a message to the conversation summary: Append a message to the conversation tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.gen.ts index 6f8607640e2629..f5d0962d13a778 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.gen.ts @@ -63,42 +63,3 @@ export const FindConversationsResponse = z.object({ total: z.number().int(), data: z.array(ConversationResponse), }); -export type FindCurrentUserConversationsRequestQuery = z.infer< - typeof FindCurrentUserConversationsRequestQuery ->; -export const FindCurrentUserConversationsRequestQuery = z.object({ - fields: ArrayFromString(z.string()).optional(), - /** - * Search query - */ - filter: z.string().optional(), - /** - * Field to sort by - */ - sort_field: FindConversationsSortField.optional(), - /** - * Sort order - */ - sort_order: SortOrder.optional(), - /** - * Page number - */ - page: z.coerce.number().int().min(1).optional().default(1), - /** - * Conversations per page - */ - per_page: z.coerce.number().int().min(0).optional().default(20), -}); -export type FindCurrentUserConversationsRequestQueryInput = z.input< - typeof FindCurrentUserConversationsRequestQuery ->; - -export type FindCurrentUserConversationsResponse = z.infer< - typeof FindCurrentUserConversationsResponse ->; -export const FindCurrentUserConversationsResponse = z.object({ - page: z.number().int(), - perPage: z.number().int(), - total: z.number().int(), - data: z.array(ConversationResponse), -}); diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.schema.yaml index fcb4c0a013eaa3..ea1f9b637186da 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/conversations/find_conversations_route.schema.yaml @@ -3,10 +3,11 @@ info: title: Find Conversations API endpoint version: '1' paths: - /internal/elastic_assistant/conversations/_find: + /api/security_ai_assistant/current_user/conversations/_find: get: - operationId: FindConversations x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: FindConversations description: Finds conversations that match the given query. summary: Finds conversations that match the given query. tags: @@ -91,94 +92,6 @@ paths: message: type: string - /internal/elastic_assistant/conversations/current_user/_find: - get: - operationId: FindCurrentUserConversations - x-codegen-enabled: true - description: Finds current user conversations that match the given query. - summary: Finds current user conversations that match the given query. - tags: - - Conversations API - parameters: - - name: 'fields' - in: query - required: false - schema: - type: array - items: - type: string - - name: 'filter' - in: query - description: Search query - required: false - schema: - type: string - - name: 'sort_field' - in: query - description: Field to sort by - required: false - schema: - $ref: '#/components/schemas/FindConversationsSortField' - - name: 'sort_order' - in: query - description: Sort order - required: false - schema: - $ref: '../common_attributes.schema.yaml#/components/schemas/SortOrder' - - name: 'page' - in: query - description: Page number - required: false - schema: - type: integer - minimum: 1 - default: 1 - - name: 'per_page' - in: query - description: Conversations per page - required: false - schema: - type: integer - minimum: 0 - default: 20 - - responses: - 200: - description: Successful response - content: - application/json: - schema: - type: object - properties: - page: - type: integer - perPage: - type: integer - total: - type: integer - data: - type: array - items: - $ref: './common_attributes.schema.yaml#/components/schemas/ConversationResponse' - required: - - page - - perPage - - total - - data - 400: - description: Generic Error - content: - application/json: - schema: - type: object - properties: - statusCode: - type: number - error: - type: string - message: - type: string - components: schemas: FindConversationsSortField: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/get_evaluate_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/get_evaluate_route.schema.yaml index b0c0c218eb9ac3..deccfb9f2488c1 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/get_evaluate_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/get_evaluate_route.schema.yaml @@ -5,8 +5,9 @@ info: paths: /internal/elastic_assistant/evaluate: get: - operationId: GetEvaluate x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: GetEvaluate description: Get relevant data for performing an evaluation like available sample data, agents, and evaluators summary: Get relevant data for performing an evaluation tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.schema.yaml index 41a7230e85ac5a..4b567f9cd118a9 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.schema.yaml @@ -5,8 +5,9 @@ info: paths: /internal/elastic_assistant/evaluate: post: - operationId: PostEvaluate x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: PostEvaluate description: Perform an evaluation using sample data against a combination of Agents and Connectors summary: Performs an evaluation of the Elastic Assistant tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/bulk_crud_knowledge_base_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/bulk_crud_knowledge_base_route.gen.ts index a7578d722d797b..5c5f7e2faafb91 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/bulk_crud_knowledge_base_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/bulk_crud_knowledge_base_route.gen.ts @@ -11,7 +11,7 @@ * * info: * title: Bulk Knowledge Base Actions API endpoint - * version: 2023-10-31 + * version: 1 */ import { z } from 'zod'; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/bulk_crud_knowledge_base_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/bulk_crud_knowledge_base_route.schema.yaml index f8a2ee49d399a9..7670114c7164ab 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/bulk_crud_knowledge_base_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/bulk_crud_knowledge_base_route.schema.yaml @@ -1,12 +1,15 @@ openapi: 3.0.0 info: title: Bulk Knowledge Base Actions API endpoint - version: '2023-10-31' + version: '1' paths: - /api/elastic_assistant/knowledge_base/entries/_bulk_action: + /internal/elastic_assistant/knowledge_base/entries/_bulk_action: post: - operationId: PerformKnowledgeBaseEntryBulkAction x-codegen-enabled: true + # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + x-internal: true + x-labels: [ess, serverless] + operationId: PerformKnowledgeBaseEntryBulkAction summary: Applies a bulk action to multiple Knowledge Base Entries description: The bulk action is applied to all Knowledge Base Entries that match the filter or to the list of Knowledge Base Entries by their IDs tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts index bd60c296743ae3..b6b1c86f959c36 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen.ts @@ -11,7 +11,7 @@ * * info: * title: KnowledgeBase API endpoints - * version: 2023-10-31 + * version: 1 */ import { z } from 'zod'; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml index 16d13c24f23ea4..6dccf1f1b2e099 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.schema.yaml @@ -1,12 +1,13 @@ openapi: 3.0.0 info: title: KnowledgeBase API endpoints - version: '2023-10-31' + version: '1' paths: /internal/elastic_assistant/knowledge_base/{resource}: post: - operationId: CreateKnowledgeBase x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: CreateKnowledgeBase summary: Create a KnowledgeBase description: Create a KnowledgeBase tags: @@ -38,8 +39,9 @@ paths: message: type: string get: - operationId: ReadKnowledgeBase x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: ReadKnowledgeBase description: Read a single KB summary: Read a KnowledgeBase tags: @@ -82,8 +84,9 @@ paths: message: type: string delete: - operationId: DeleteKnowledgeBase x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: DeleteKnowledgeBase description: Deletes KnowledgeBase with the `resource` field. summary: Deletes a KnowledgeBase tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_knowledge_base_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_knowledge_base_route.gen.ts index 01c3427f80c859..669292e955ec9a 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_knowledge_base_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_knowledge_base_route.gen.ts @@ -11,7 +11,7 @@ * * info: * title: Manage Knowledge Base Entries API endpoint - * version: 2023-10-31 + * version: 1 */ import { z } from 'zod'; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_knowledge_base_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_knowledge_base_route.schema.yaml index 6db7da89f55e51..32e66efffc13ca 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_knowledge_base_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/crud_knowledge_base_route.schema.yaml @@ -1,12 +1,15 @@ openapi: 3.0.0 info: title: Manage Knowledge Base Entries API endpoint - version: '2023-10-31' + version: '1' paths: - /api/elastic_assistant/knowledge_base/entries: + /internnal/elastic_assistant/knowledge_base/entries: post: - operationId: CreateKnowledgeBaseEntry x-codegen-enabled: true + # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + x-internal: true + x-labels: [ess, serverless] + operationId: CreateKnowledgeBaseEntry description: Create a Knowledge Base Entry summary: Create a Knowledge Base Entry tags: @@ -31,10 +34,13 @@ paths: schema: $ref: './common_attributes.schema.yaml#/components/schemas/KnowledgeBaseEntryErrorSchema' - /api/elastic_assistant/knowledge_base/entries/{id}: + /internal/elastic_assistant/knowledge_base/entries/{id}: get: - operationId: ReadKnowledgeBaseEntry x-codegen-enabled: true + # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + x-internal: true + x-labels: [ess, serverless] + operationId: ReadKnowledgeBaseEntry description: Read a Knowledge Base Entry summary: Read a Knowledge Base Entry tags: @@ -60,8 +66,11 @@ paths: schema: $ref: './common_attributes.schema.yaml#/components/schemas/KnowledgeBaseEntryErrorSchema' put: - operationId: UpdateKnowledgeBaseEntry x-codegen-enabled: true + # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + x-internal: true + x-labels: [ess, serverless] + operationId: UpdateKnowledgeBaseEntry description: Update a Knowledge Base Entry summary: Update a Knowledge Base Entry tags: @@ -93,8 +102,11 @@ paths: schema: $ref: './common_attributes.schema.yaml#/components/schemas/KnowledgeBaseEntryErrorSchema' delete: - operationId: DeleteKnowledgeBaseEntry x-codegen-enabled: true + # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + x-internal: true + x-labels: [ess, serverless] + operationId: DeleteKnowledgeBaseEntry description: Deletes a single Knowledge Base Entry using the `id` field summary: Deletes a single Knowledge Base Entry using the `id` field tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/find_knowledge_base_entries_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/find_knowledge_base_entries_route.schema.yaml index d5298ff2ccbdc5..cf88de73df1dbe 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/find_knowledge_base_entries_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/knowledge_base/find_knowledge_base_entries_route.schema.yaml @@ -5,8 +5,11 @@ info: paths: /internal/elastic_assistant/knowledge_base/entries/_find: get: - operationId: FindKnowledgeBaseEntries x-codegen-enabled: true + # This API is still behind the `assistantKnowledgeBaseByDefault` feature flag + x-internal: true + x-labels: [ess, serverless] + operationId: FindKnowledgeBaseEntries description: Finds Knowledge Base Entries that match the given query. summary: Finds Knowledge Base Entries that match the given query. tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen.ts index 2d7a4d762eecc6..09e7fc85f9363d 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.gen.ts @@ -10,7 +10,7 @@ * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. * * info: - * title: Bulk Actions API endpoint + * title: Bulk Prompts Actions API endpoint * version: 1 */ @@ -18,14 +18,14 @@ import { z } from 'zod'; import { NonEmptyString, User } from '../common_attributes.gen'; -export type BulkActionSkipReason = z.infer; -export const BulkActionSkipReason = z.literal('PROMPT_FIELD_NOT_MODIFIED'); +export type PromptsBulkActionSkipReason = z.infer; +export const PromptsBulkActionSkipReason = z.literal('PROMPT_FIELD_NOT_MODIFIED'); -export type BulkActionSkipResult = z.infer; -export const BulkActionSkipResult = z.object({ +export type PromptsBulkActionSkipResult = z.infer; +export const PromptsBulkActionSkipResult = z.object({ id: z.string(), name: z.string().optional(), - skip_reason: BulkActionSkipReason, + skip_reason: PromptsBulkActionSkipReason, }); export type PromptDetailsInError = z.infer; @@ -73,12 +73,12 @@ export const PromptResponse = z.object({ namespace: z.string().optional(), }); -export type BulkCrudActionResults = z.infer; -export const BulkCrudActionResults = z.object({ +export type PromptsBulkCrudActionResults = z.infer; +export const PromptsBulkCrudActionResults = z.object({ updated: z.array(PromptResponse), created: z.array(PromptResponse), deleted: z.array(z.string()), - skipped: z.array(BulkActionSkipResult), + skipped: z.array(PromptsBulkActionSkipResult), }); export type BulkCrudActionSummary = z.infer; @@ -89,14 +89,14 @@ export const BulkCrudActionSummary = z.object({ total: z.number().int(), }); -export type BulkCrudActionResponse = z.infer; -export const BulkCrudActionResponse = z.object({ +export type PromptsBulkCrudActionResponse = z.infer; +export const PromptsBulkCrudActionResponse = z.object({ success: z.boolean().optional(), status_code: z.number().int().optional(), message: z.string().optional(), prompts_count: z.number().int().optional(), attributes: z.object({ - results: BulkCrudActionResults, + results: PromptsBulkCrudActionResults, summary: BulkCrudActionSummary, errors: z.array(NormalizedPromptError).optional(), }), @@ -146,4 +146,4 @@ export const PerformBulkActionRequestBody = z.object({ export type PerformBulkActionRequestBodyInput = z.input; export type PerformBulkActionResponse = z.infer; -export const PerformBulkActionResponse = BulkCrudActionResponse; +export const PerformBulkActionResponse = PromptsBulkCrudActionResponse; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.schema.yaml index 5be6bde140b858..f5b6ee918429c5 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/bulk_crud_prompts_route.schema.yaml @@ -1,12 +1,13 @@ openapi: 3.0.0 info: - title: Bulk Actions API endpoint + title: Bulk Prompts Actions API endpoint version: '1' paths: - /internal/elastic_assistant/prompts/_bulk_action: + /api/security_ai_assistant/prompts/_bulk_action: post: - operationId: PerformBulkAction x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: PerformBulkAction summary: Applies a bulk action to multiple prompts description: The bulk action is applied to all prompts that match the filter or to the list of prompts by their IDs. tags: @@ -33,7 +34,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BulkCrudActionResponse' + $ref: '#/components/schemas/PromptsBulkCrudActionResponse' 400: description: Generic Error content: @@ -50,12 +51,12 @@ paths: components: schemas: - BulkActionSkipReason: + PromptsBulkActionSkipReason: type: string enum: - PROMPT_FIELD_NOT_MODIFIED - BulkActionSkipResult: + PromptsBulkActionSkipResult: type: object properties: id: @@ -63,7 +64,7 @@ components: name: type: string skip_reason: - $ref: '#/components/schemas/BulkActionSkipReason' + $ref: '#/components/schemas/PromptsBulkActionSkipReason' required: - id - skip_reason @@ -149,7 +150,7 @@ components: type: string description: Kibana space - BulkCrudActionResults: + PromptsBulkCrudActionResults: type: object properties: updated: @@ -167,7 +168,7 @@ components: skipped: type: array items: - $ref: '#/components/schemas/BulkActionSkipResult' + $ref: '#/components/schemas/PromptsBulkActionSkipResult' required: - updated - created @@ -191,7 +192,7 @@ components: - succeeded - total - BulkCrudActionResponse: + PromptsBulkCrudActionResponse: type: object properties: success: @@ -206,7 +207,7 @@ components: type: object properties: results: - $ref: '#/components/schemas/BulkCrudActionResults' + $ref: '#/components/schemas/PromptsBulkCrudActionResults' summary: $ref: '#/components/schemas/BulkCrudActionSummary' errors: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/find_prompts_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/find_prompts_route.schema.yaml index 1902f4e9ae3d99..dbd5590b29ee1e 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/find_prompts_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/prompts/find_prompts_route.schema.yaml @@ -3,10 +3,11 @@ info: title: Find Prompts API endpoint version: '1' paths: - /internal/elastic_assistant/prompts/_find: + /api/security_ai_assistant/prompts/_find: get: - operationId: FindPrompts x-codegen-enabled: true + x-labels: [ess, serverless] + operationId: FindPrompts description: Finds prompts that match the given query. summary: Finds prompts that match the given query. tags: diff --git a/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js b/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js index 138d1255b865b2..eb45fe104ad480 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js +++ b/x-pack/packages/kbn-elastic-assistant-common/scripts/openapi/bundle.js @@ -8,12 +8,38 @@ require('../../../../../src/setup_node_env'); const { bundle } = require('@kbn/openapi-bundler'); // eslint-disable-next-line import/no-nodejs-modules -const { resolve } = require('path'); +const { join, resolve } = require('path'); const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..'); -bundle({ - rootDir: ELASTIC_ASSISTANT_ROOT, - sourceGlob: './impl/schemas/**/*.schema.yaml', - outputFilePath: './target/openapi/elastic_assistant.bundled.schema.yaml', -}); +(async () => { + await bundle({ + sourceGlob: join(ELASTIC_ASSISTANT_ROOT, 'impl/schemas/**/*.schema.yaml'), + outputFilePath: join( + ELASTIC_ASSISTANT_ROOT, + 'docs/openapi/serverless/elastic_assistant_api_{version}.bundled.schema.yaml' + ), + options: { + includeLabels: ['serverless'], + specInfo: { + title: 'Security AI Assistant API (Elastic Cloud Serverless)', + description: 'Manage and interact with Security Assistant resources.', + }, + }, + }); + + await bundle({ + sourceGlob: join(ELASTIC_ASSISTANT_ROOT, 'impl/schemas/**/*.schema.yaml'), + outputFilePath: join( + ELASTIC_ASSISTANT_ROOT, + 'docs/openapi/ess/elastic_assistant_api_{version}.bundled.schema.yaml' + ), + options: { + includeLabels: ['ess'], + specInfo: { + title: 'Security AI Assistant API (Elastic Cloud & self-hosted)', + description: 'Manage and interact with Security Assistant resources.', + }, + }, + }); +})(); diff --git a/x-pack/packages/kbn-entities-schema/src/schema/__snapshots__/common.test.ts.snap b/x-pack/packages/kbn-entities-schema/src/schema/__snapshots__/common.test.ts.snap new file mode 100644 index 00000000000000..4067947f7ddcf0 --- /dev/null +++ b/x-pack/packages/kbn-entities-schema/src/schema/__snapshots__/common.test.ts.snap @@ -0,0 +1,132 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`schemas metadataSchema should error on empty string 1`] = ` +Object { + "error": [ZodError: [ + { + "path": [ + "source" + ], + "code": "custom", + "message": "source should not be empty" + }, + { + "path": [ + "destination" + ], + "code": "custom", + "message": "destination should not be empty" + } +]], + "success": false, +} +`; + +exports[`schemas metadataSchema should error on empty string for destination 1`] = ` +Object { + "error": [ZodError: [ + { + "path": [ + "destination" + ], + "code": "custom", + "message": "destination should not be empty" + } +]], + "success": false, +} +`; + +exports[`schemas metadataSchema should error on empty string for source 1`] = ` +Object { + "error": [ZodError: [ + { + "path": [ + "source" + ], + "code": "custom", + "message": "source should not be empty" + }, + { + "path": [ + "destination" + ], + "code": "custom", + "message": "destination should not be empty" + } +]], + "success": false, +} +`; + +exports[`schemas metadataSchema should error when limit is too low 1`] = ` +Object { + "error": [ZodError: [ + { + "path": [ + "limit" + ], + "code": "custom", + "message": "limit should be greater than 1" + } +]], + "success": false, +} +`; + +exports[`schemas metadataSchema should parse successfully with a source and desitination 1`] = ` +Object { + "data": Object { + "destination": "hostName", + "limit": 1000, + "source": "host.name", + }, + "success": true, +} +`; + +exports[`schemas metadataSchema should parse successfully with an valid string 1`] = ` +Object { + "data": Object { + "destination": "host.name", + "limit": 1000, + "source": "host.name", + }, + "success": true, +} +`; + +exports[`schemas metadataSchema should parse successfully with just a source 1`] = ` +Object { + "data": Object { + "destination": "host.name", + "limit": 1000, + "source": "host.name", + }, + "success": true, +} +`; + +exports[`schemas metadataSchema should parse successfully with valid object 1`] = ` +Object { + "data": Object { + "destination": "hostName", + "limit": 1000, + "source": "host.name", + }, + "success": true, +} +`; + +exports[`schemas semVerSchema should not validate with 0.9 1`] = ` +Object { + "error": [ZodError: [ + { + "code": "custom", + "message": "The string does use the Semantic Versioning (Semver) format of {major}.{minor}.{patch} (e.g., 1.0.0), ensure each part contains only digits.", + "path": [] + } +]], + "success": false, +} +`; diff --git a/x-pack/packages/kbn-entities-schema/src/schema/common.test.ts b/x-pack/packages/kbn-entities-schema/src/schema/common.test.ts new file mode 100644 index 00000000000000..c03bff2db74c0c --- /dev/null +++ b/x-pack/packages/kbn-entities-schema/src/schema/common.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { SafeParseSuccess } from 'zod'; +import { durationSchema, metadataSchema, semVerSchema } from './common'; +import moment from 'moment'; + +describe('schemas', () => { + describe('metadataSchema', () => { + it('should error on empty string', () => { + const result = metadataSchema.safeParse(''); + expect(result.success).toBeFalsy(); + expect(result).toMatchSnapshot(); + }); + it('should error on empty string for source', () => { + const result = metadataSchema.safeParse({ source: '' }); + expect(result.success).toBeFalsy(); + expect(result).toMatchSnapshot(); + }); + it('should error on empty string for destination', () => { + const result = metadataSchema.safeParse({ source: 'host.name', destination: '', limit: 10 }); + expect(result.success).toBeFalsy(); + expect(result).toMatchSnapshot(); + }); + it('should error when limit is too low', () => { + const result = metadataSchema.safeParse({ + source: 'host.name', + destination: 'host.name', + limit: 0, + }); + expect(result.success).toBeFalsy(); + expect(result).toMatchSnapshot(); + }); + it('should parse successfully with an valid string', () => { + const result = metadataSchema.safeParse('host.name'); + expect(result.success).toBeTruthy(); + expect(result).toMatchSnapshot(); + }); + it('should parse successfully with just a source', () => { + const result = metadataSchema.safeParse({ source: 'host.name' }); + expect(result.success).toBeTruthy(); + expect(result).toMatchSnapshot(); + }); + it('should parse successfully with a source and desitination', () => { + const result = metadataSchema.safeParse({ source: 'host.name', destination: 'hostName' }); + expect(result.success).toBeTruthy(); + expect(result).toMatchSnapshot(); + }); + it('should parse successfully with valid object', () => { + const result = metadataSchema.safeParse({ + source: 'host.name', + destination: 'hostName', + size: 1, + }); + expect(result.success).toBeTruthy(); + expect(result).toMatchSnapshot(); + }); + }); + describe('durationSchema', () => { + it('should work with 1m', () => { + const result = durationSchema.safeParse('1m'); + expect(result.success).toBeTruthy(); + expect((result as SafeParseSuccess).data.toJSON()).toBe('1m'); + expect((result as SafeParseSuccess).data.asSeconds()).toEqual(60); + }); + it('should work with 10s', () => { + const result = durationSchema.safeParse('10s'); + expect(result.success).toBeTruthy(); + expect((result as SafeParseSuccess).data.toJSON()).toBe('10s'); + expect((result as SafeParseSuccess).data.asSeconds()).toEqual(10); + }); + it('should work with 999h', () => { + const result = durationSchema.safeParse('999h'); + expect(result.success).toBeTruthy(); + expect((result as SafeParseSuccess).data.toJSON()).toBe('999h'); + expect((result as SafeParseSuccess).data.asSeconds()).toEqual(999 * 60 * 60); + }); + it('should work with 90d', () => { + const result = durationSchema.safeParse('90d'); + expect(result.success).toBeTruthy(); + expect((result as SafeParseSuccess).data.toJSON()).toBe('90d'); + expect((result as SafeParseSuccess).data.asSeconds()).toEqual( + 90 * 24 * 60 * 60 + ); + }); + it('should not work with 1ms', () => { + const result = durationSchema.safeParse('1ms'); + expect(result.success).toBeFalsy(); + }); + }); + describe('semVerSchema', () => { + it('should validate with 999.999.999', () => { + const result = semVerSchema.safeParse('999.999.999'); + expect(result.success).toBeTruthy(); + }); + it('should not validate with 0.9', () => { + const result = semVerSchema.safeParse('0.9'); + expect(result.success).toBeFalsy(); + expect(result).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/packages/kbn-entities-schema/src/schema/common.ts b/x-pack/packages/kbn-entities-schema/src/schema/common.ts index df6d20ef1d44be..6576a1c650a107 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/common.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/common.ts @@ -45,7 +45,7 @@ export const docCountMetricSchema = z.object({ export const durationSchema = z .string() - .regex(/\d+[m|d|s|h]/) + .regex(/^\d+[m|d|s|h]$/) .transform((val: string) => { const parts = val.match(/(\d+)([m|s|h|d])/); if (parts === null) { @@ -93,7 +93,30 @@ export const metadataSchema = z destination: metadata.destination ?? metadata.source, limit: metadata.limit ?? 1000, })) - .or(z.string().transform((value) => ({ source: value, destination: value, limit: 1000 }))); + .or(z.string().transform((value) => ({ source: value, destination: value, limit: 1000 }))) + .superRefine((value, ctx) => { + if (value.limit < 1) { + ctx.addIssue({ + path: ['limit'], + code: z.ZodIssueCode.custom, + message: 'limit should be greater than 1', + }); + } + if (value.source.length === 0) { + ctx.addIssue({ + path: ['source'], + code: z.ZodIssueCode.custom, + message: 'source should not be empty', + }); + } + if (value.destination.length === 0) { + ctx.addIssue({ + path: ['destination'], + code: z.ZodIssueCode.custom, + message: 'destination should not be empty', + }); + } + }); export const identityFieldsSchema = z .object({ diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts index d433cc473a538f..c297a2d5542ae1 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity_definition.ts @@ -40,6 +40,9 @@ export const entityDefinitionSchema = z.object({ syncField: z.optional(z.string()), syncDelay: z.optional(z.string()), frequency: z.optional(z.string()), + backfillSyncDelay: z.optional(z.string()), + backfillLookbackPeriod: z.optional(durationSchema), + backfillFrequency: z.optional(z.string()), }) ), }), diff --git a/x-pack/packages/security-solution/data_table/common/types/detail_panel.ts b/x-pack/packages/security-solution/data_table/common/types/detail_panel.ts index d14a0b8d352134..4162d3711b991e 100644 --- a/x-pack/packages/security-solution/data_table/common/types/detail_panel.ts +++ b/x-pack/packages/security-solution/data_table/common/types/detail_panel.ts @@ -5,26 +5,11 @@ * 2.0. */ -type EmptyObject = Record; - export enum FlowTargetSourceDest { destination = 'destination', source = 'source', } -export type ExpandedEventType = - | { - panelView?: 'eventDetail'; - params?: { - eventId: string; - indexName: string; - refetch?: () => void; - }; - } - | EmptyObject; - -export type ExpandedDetailType = ExpandedEventType; - export enum TimelineTabs { query = 'query', graph = 'graph', @@ -33,9 +18,3 @@ export enum TimelineTabs { eql = 'eql', session = 'session', } - -export type ExpandedDetailTimeline = { - [tab in TimelineTabs]?: ExpandedDetailType; -}; - -export type ExpandedDetail = Partial>; diff --git a/x-pack/packages/security-solution/data_table/mock/global_state.ts b/x-pack/packages/security-solution/data_table/mock/global_state.ts index 557c2c0dd0aca7..d33538f6ddf636 100644 --- a/x-pack/packages/security-solution/data_table/mock/global_state.ts +++ b/x-pack/packages/security-solution/data_table/mock/global_state.ts @@ -24,7 +24,6 @@ export const mockGlobalState = { defaultColumns: defaultHeaders, dataViewId: 'security-solution-default', deletedEventIds: [], - expandedDetail: {}, filters: [], indexNames: ['.alerts-security.alerts-default'], isSelectAllChecked: false, diff --git a/x-pack/packages/security-solution/data_table/store/data_table/actions.ts b/x-pack/packages/security-solution/data_table/store/data_table/actions.ts index 5d5e0fb31ddddb..40cfb8a727f3a1 100644 --- a/x-pack/packages/security-solution/data_table/store/data_table/actions.ts +++ b/x-pack/packages/security-solution/data_table/store/data_table/actions.ts @@ -7,7 +7,6 @@ import actionCreatorFactory from 'typescript-fsa'; import { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; -import type { ExpandedDetailType } from '../../common/types/detail_panel'; import type { ColumnHeaderOptions, SessionViewConfig, @@ -43,13 +42,6 @@ export const updateColumnWidth = actionCreator<{ width: number; }>('UPDATE_COLUMN_WIDTH'); -export type TableToggleDetailPanel = ExpandedDetailType & { - tabType?: string; - id: string; -}; - -export const toggleDetailPanel = actionCreator('TOGGLE_DETAIL_PANEL'); - export const removeColumn = actionCreator<{ id: string; columnId: string; diff --git a/x-pack/packages/security-solution/data_table/store/data_table/defaults.ts b/x-pack/packages/security-solution/data_table/store/data_table/defaults.ts index 29e28af5da28c8..8c74b2d2139274 100644 --- a/x-pack/packages/security-solution/data_table/store/data_table/defaults.ts +++ b/x-pack/packages/security-solution/data_table/store/data_table/defaults.ts @@ -64,7 +64,6 @@ export const tableDefaults: SubsetDataTableModel = { defaultColumns: defaultHeaders, dataViewId: null, deletedEventIds: [], - expandedDetail: {}, filters: [], indexNames: [], isSelectAllChecked: false, diff --git a/x-pack/packages/security-solution/data_table/store/data_table/helpers.ts b/x-pack/packages/security-solution/data_table/store/data_table/helpers.ts index 035c865f1fc828..8b33005685953f 100644 --- a/x-pack/packages/security-solution/data_table/store/data_table/helpers.ts +++ b/x-pack/packages/security-solution/data_table/store/data_table/helpers.ts @@ -9,16 +9,13 @@ import { omit, union } from 'lodash/fp'; import { isEmpty } from 'lodash'; import type { EuiDataGridColumn } from '@elastic/eui'; -import type { ExpandedDetail, ExpandedDetailType } from '../../common/types/detail_panel'; import type { ColumnHeaderOptions, SessionViewConfig, SortColumnTable } from '../../common/types'; -import type { TableToggleDetailPanel } from './actions'; import type { DataTablePersistInput, TableById } from './types'; import type { DataTableModelSettings } from './model'; import { getDataTableManageDefaults, tableDefaults } from './defaults'; import { DEFAULT_TABLE_COLUMN_MIN_WIDTH } from '../../components/data_table/constants'; -export const isNotNull = (value: T | null): value is T => value !== null; export type Maybe = T | null; /** The minimum width of a resized column */ @@ -438,22 +435,6 @@ export const setSelectedTableEvents = ({ }; }; -export const updateTableDetailsPanel = (action: TableToggleDetailPanel): ExpandedDetail => { - const { tabType, id, ...expandedDetails } = action; - - const panelViewOptions = new Set(['eventDetail', 'hostDetail', 'networkDetail', 'userDetail']); - const expandedTabType = tabType ?? 'query'; - const newExpandDetails = { - params: expandedDetails.params ? { ...expandedDetails.params } : {}, - panelView: expandedDetails.panelView, - } as ExpandedDetailType; - return { - [expandedTabType]: panelViewOptions.has(expandedDetails.panelView ?? '') - ? newExpandDetails - : {}, - }; -}; - export const updateTableGraphEventId = ({ id, graphEventId, diff --git a/x-pack/packages/security-solution/data_table/store/data_table/model.ts b/x-pack/packages/security-solution/data_table/store/data_table/model.ts index f8d0d22d5ea88b..dc03918095bbaf 100644 --- a/x-pack/packages/security-solution/data_table/store/data_table/model.ts +++ b/x-pack/packages/security-solution/data_table/store/data_table/model.ts @@ -8,7 +8,6 @@ import type { EuiDataGridColumn } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; import { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; -import { ExpandedDetail } from '../../common/types/detail_panel'; import type { ColumnHeaderOptions, SessionViewConfig, @@ -44,8 +43,6 @@ export interface DataTableModel extends DataTableModelSettings { dataViewId: string | null; // null if legacy pre-8.0 data table /** Events to not be rendered **/ deletedEventIds: string[]; - /** This holds the view information for the flyout when viewing data in a consuming view (i.e. hosts page) or the side panel in the primary data view */ - expandedDetail: ExpandedDetail; filters?: Filter[]; /** When non-empty, display a graph view for this event */ graphEventId?: string; @@ -82,7 +79,6 @@ export type SubsetDataTableModel = Readonly< | 'defaultColumns' | 'dataViewId' | 'deletedEventIds' - | 'expandedDetail' | 'filters' | 'indexNames' | 'isLoading' diff --git a/x-pack/packages/security-solution/data_table/store/data_table/reducer.ts b/x-pack/packages/security-solution/data_table/store/data_table/reducer.ts index 1ad08a9332503a..06dff26eba57ff 100644 --- a/x-pack/packages/security-solution/data_table/store/data_table/reducer.ts +++ b/x-pack/packages/security-solution/data_table/store/data_table/reducer.ts @@ -19,7 +19,6 @@ import { setEventsLoading, setDataTableSelectAll, setSelected, - toggleDetailPanel, updateColumnOrder, updateColumns, updateColumnWidth, @@ -52,7 +51,6 @@ import { updateTablePerPageOptions, updateTableSort, upsertTableColumn, - updateTableDetailsPanel, updateTableGraphEventId, updateTableSessionViewConfig, } from './helpers'; @@ -87,21 +85,6 @@ export const dataTableReducer = reducerWithInitialState(initialDataTableState) dataTableSettingsProps, }), })) - .case(toggleDetailPanel, (state, action) => { - return { - ...state, - tableById: { - ...state.tableById, - [action.id]: { - ...state.tableById[action.id], - expandedDetail: { - ...state.tableById[action.id]?.expandedDetail, - ...updateTableDetailsPanel(action), - }, - }, - }, - }; - }) .case(applyDeltaToColumnWidth, (state, { id, columnId, delta }) => ({ ...state, tableById: applyDeltaToTableColumnWidth({ diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index 62892c143146f5..8084213ab03e3d 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -227,7 +227,6 @@ Arguments: | timelineIntegration?.editor_plugins.uiPlugin? | `EuiMarkdownEditorUiPlugin` | | timelineIntegration?.hooks.useInsertTimeline | `(value: string, onChange: (newValue: string) => void): UseInsertTimelineReturn` | | timelineIntegration?.ui?.renderInvestigateInTimelineActionComponent? | `(alertIds: string[]) => JSX.Element;` space to render `InvestigateInTimelineActionComponent` | -| timelineIntegration?.ui?renderTimelineDetailsPanel? | `() => JSX.Element;` space to render `TimelineDetailsPanel` | #### `getCases` UI component: diff --git a/x-pack/plugins/cases/public/components/__mock__/timeline.tsx b/x-pack/plugins/cases/public/components/__mock__/timeline.tsx index d576b0ef1732cd..15bdedf4259861 100644 --- a/x-pack/plugins/cases/public/components/__mock__/timeline.tsx +++ b/x-pack/plugins/cases/public/components/__mock__/timeline.tsx @@ -23,9 +23,6 @@ export const timelineIntegrationMock = { hooks: { useInsertTimeline: jest.fn(), }, - ui: { - renderTimelineDetailsPanel: () => mockTimelineComponent('timeline-details-panel'), - }, }; export const useTimelineContextMock = useTimelineContext as jest.Mock; diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx index 3a518c00fbe47a..adc2b41f6ce6a8 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx @@ -13,7 +13,6 @@ import { useCasesContext } from '../cases_context/use_cases_context'; import { CaseActionBar } from '../case_action_bar'; import { HeaderPage } from '../header_page'; import { EditableTitle } from '../header_page/editable_title'; -import { useTimelineContext } from '../timeline_context/use_timeline_context'; import { useCasesTitleBreadcrumbs } from '../use_breadcrumbs'; import { CaseViewActivity } from './components/case_view_activity'; import { CaseViewAlerts } from './components/case_view_alerts'; @@ -49,8 +48,6 @@ export const CaseViewPage = React.memo( const activeTabId = getActiveTabId(urlParams?.tabId); - const timelineUi = useTimelineContext()?.ui; - const { onUpdateField, isLoading, loadingKey } = useOnUpdateField({ caseData, }); @@ -126,7 +123,6 @@ export const CaseViewPage = React.memo( )} {activeTabId === CASE_VIEW_PAGE_TABS.FILES && } - {timelineUi?.renderTimelineDetailsPanel ? timelineUi.renderTimelineDetailsPanel() : null} ); } diff --git a/x-pack/plugins/cases/public/components/timeline_context/index.tsx b/x-pack/plugins/cases/public/components/timeline_context/index.tsx index 3b5e4949150c09..5968e2ea87fc99 100644 --- a/x-pack/plugins/cases/public/components/timeline_context/index.tsx +++ b/x-pack/plugins/cases/public/components/timeline_context/index.tsx @@ -42,9 +42,6 @@ export interface CasesTimelineIntegration { onChange: (newValue: string) => void ) => UseInsertTimelineReturn; }; - ui?: { - renderTimelineDetailsPanel?: () => JSX.Element; - }; } // This context is available to all children of the stateful_event component where the provider is currently set diff --git a/x-pack/plugins/cloud/README.md b/x-pack/plugins/cloud/README.md index 00aa160fb3600c..6878c72eb4c5fc 100644 --- a/x-pack/plugins/cloud/README.md +++ b/x-pack/plugins/cloud/README.md @@ -1,3 +1,3 @@ # `cloud` plugin -The `cloud` plugin adds Cloud-specific features to Kibana. \ No newline at end of file +The `cloud` plugin exposes Cloud-specific metadata to Kibana. \ No newline at end of file diff --git a/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts index 204b940c45cd5a..e4c5a88a847c40 100644 --- a/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts +++ b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts @@ -11,12 +11,14 @@ import { parseDeploymentIdFromDeploymentUrl } from './parse_deployment_id_from_d export interface CloudDeploymentMetadata { id?: string; + organization_id?: string; trial_end_date?: string; is_elastic_staff_owned?: boolean; deployment_url?: string; serverless?: { project_id?: string; project_type?: string; + orchestrator_target?: string; }; } @@ -29,26 +31,40 @@ export function registerCloudDeploymentMetadataAnalyticsContext( } const { id: cloudId, + organization_id: organizationId, trial_end_date: cloudTrialEndDate, is_elastic_staff_owned: cloudIsElasticStaffOwned, - serverless: { project_id: projectId, project_type: projectType } = {}, + serverless: { + project_id: projectId, + project_type: projectType, + orchestrator_target: orchestratorTarget, + } = {}, } = cloudMetadata; analytics.registerContextProvider({ name: 'Cloud Deployment Metadata', context$: of({ cloudId, + organizationId, deploymentId: parseDeploymentIdFromDeploymentUrl(cloudMetadata.deployment_url), cloudTrialEndDate, cloudIsElasticStaffOwned, projectId, projectType, + orchestratorTarget, }), schema: { cloudId: { type: 'keyword', _meta: { description: 'The Cloud ID' }, }, + organizationId: { + type: 'keyword', + _meta: { + description: 'The Elastic Cloud Organization ID that owns the deployment/project', + optional: true, + }, + }, deploymentId: { type: 'keyword', _meta: { description: 'The Deployment ID', optional: true }, @@ -72,6 +88,13 @@ export function registerCloudDeploymentMetadataAnalyticsContext( type: 'keyword', _meta: { description: 'The Serverless Project type', optional: true }, }, + orchestratorTarget: { + type: 'keyword', + _meta: { + description: 'The Orchestrator Target where it is deployed (canary/non-canary)', + optional: true, + }, + }, }, }); } diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index 0d071900418c3e..d2671b18e4d684 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -21,6 +21,7 @@ import { getSupportUrl } from './utils'; export interface CloudConfigType { id?: string; + organization_id?: string; cname?: string; base_url?: string; profile_url?: string; @@ -40,6 +41,7 @@ export interface CloudConfigType { project_id: string; project_name?: string; project_type?: string; + orchestrator_target?: string; }; } @@ -89,6 +91,7 @@ export class CloudPlugin implements Plugin { return { cloudId: id, + organizationId: this.config.organization_id, deploymentId: parseDeploymentIdFromDeploymentUrl(this.config.deployment_url), cname, baseUrl, @@ -108,6 +111,7 @@ export class CloudPlugin implements Plugin { projectId: this.config.serverless?.project_id, projectName: this.config.serverless?.project_name, projectType: this.config.serverless?.project_type, + orchestratorTarget: this.config.serverless?.orchestrator_target, }, registerCloudService: (contextProvider) => { this.contextProviders.push(contextProvider); diff --git a/x-pack/plugins/cloud/public/types.ts b/x-pack/plugins/cloud/public/types.ts index a7e34c79a8505e..dd3dcf27c1a61a 100644 --- a/x-pack/plugins/cloud/public/types.ts +++ b/x-pack/plugins/cloud/public/types.ts @@ -97,6 +97,10 @@ export interface CloudSetup { * Cloud ID. Undefined if not running on Cloud. */ cloudId?: string; + /** + * The Elastic Cloud Organization that owns this deployment/project. + */ + organizationId?: string; /** * The deployment's ID. Only available when running on Elastic Cloud. */ @@ -208,5 +212,10 @@ export interface CloudSetup { * Will always be present if `isServerlessEnabled` is `true` */ projectType?: string; + /** + * The serverless orchestrator target. The potential values are `canary` or `non-canary` + * Will always be present if `isServerlessEnabled` is `true` + */ + orchestratorTarget?: string; }; } diff --git a/x-pack/plugins/cloud/server/__snapshots__/plugin.test.ts.snap b/x-pack/plugins/cloud/server/__snapshots__/plugin.test.ts.snap index 41002d0c48e6b8..fa873a89a85d75 100644 --- a/x-pack/plugins/cloud/server/__snapshots__/plugin.test.ts.snap +++ b/x-pack/plugins/cloud/server/__snapshots__/plugin.test.ts.snap @@ -20,8 +20,10 @@ Object { "onboarding": Object { "defaultSolution": undefined, }, + "organizationId": undefined, "projectsUrl": "https://cloud.elastic.co/projects/", "serverless": Object { + "orchestratorTarget": undefined, "projectId": undefined, "projectName": undefined, "projectType": undefined, diff --git a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts index b9442fb74f94ff..ec9a81ad0272ba 100644 --- a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts +++ b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts @@ -38,10 +38,12 @@ describe('createCloudUsageCollector', () => { expect(await collector.fetch(collectorFetchContext)).toStrictEqual({ isCloudEnabled: true, isElasticStaffOwned: undefined, + organizationId: undefined, trialEndDate: undefined, deploymentId: undefined, projectId: undefined, projectType: undefined, + orchestratorTarget: undefined, }); }); @@ -54,11 +56,13 @@ describe('createCloudUsageCollector', () => { expect(await collector.fetch(collectorFetchContext)).toStrictEqual({ isCloudEnabled: true, isElasticStaffOwned: undefined, + organizationId: undefined, trialEndDate: '2020-10-01T14:30:16Z', inTrial: false, deploymentId: undefined, projectId: undefined, projectType: undefined, + orchestratorTarget: undefined, }); }); @@ -67,9 +71,11 @@ describe('createCloudUsageCollector', () => { isCloudEnabled: true, trialEndDate: '2020-10-01T14:30:16Z', isElasticStaffOwned: true, + organizationId: '1234', deploymentId: 'a-deployment-id', projectId: 'a-project-id', projectType: 'security', + orchestratorTarget: 'canary', }); expect(await collector.fetch(collectorFetchContext)).toStrictEqual({ @@ -77,9 +83,11 @@ describe('createCloudUsageCollector', () => { trialEndDate: '2020-10-01T14:30:16Z', inTrial: false, isElasticStaffOwned: true, + organizationId: '1234', deploymentId: 'a-deployment-id', projectId: 'a-project-id', projectType: 'security', + orchestratorTarget: 'canary', }); }); }); diff --git a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts index 0b8415f755a765..2d1924817e56e5 100644 --- a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts +++ b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts @@ -12,9 +12,11 @@ export interface CloudUsageCollectorConfig { // Using * | undefined instead of ?: to force the calling code to list all the options (even when they can be undefined) trialEndDate: string | undefined; isElasticStaffOwned: boolean | undefined; + organizationId: string | undefined; deploymentId: string | undefined; projectId: string | undefined; projectType: string | undefined; + orchestratorTarget: string | undefined; } interface CloudUsage { @@ -22,9 +24,11 @@ interface CloudUsage { trialEndDate?: string; inTrial?: boolean; isElasticStaffOwned?: boolean; + organizationId?: string; deploymentId?: string; projectId?: string; projectType?: string; + orchestratorTarget?: string; } export function createCloudUsageCollector( @@ -35,19 +39,36 @@ export function createCloudUsageCollector( isCloudEnabled, trialEndDate, isElasticStaffOwned, + organizationId, deploymentId, projectId, projectType, + orchestratorTarget, } = config; const trialEndDateMs = trialEndDate ? new Date(trialEndDate).getTime() : undefined; return usageCollection.makeUsageCollector({ type: 'cloud', isReady: () => true, schema: { - isCloudEnabled: { type: 'boolean' }, - trialEndDate: { type: 'date' }, - inTrial: { type: 'boolean' }, - isElasticStaffOwned: { type: 'boolean' }, + isCloudEnabled: { + type: 'boolean', + _meta: { description: 'Is the deployment running in Elastic Cloud (ESS or Serverless)?' }, + }, + trialEndDate: { type: 'date', _meta: { description: 'End of the trial period' } }, + inTrial: { + type: 'boolean', + _meta: { description: 'Is the organization during the trial period?' }, + }, + isElasticStaffOwned: { + type: 'boolean', + _meta: { description: 'Is the deploymend owned by an Elastician' }, + }, + organizationId: { + type: 'keyword', + _meta: { + description: 'The Elastic Cloud Organization ID that owns the deployment/project', + }, + }, deploymentId: { type: 'keyword', _meta: { description: 'The ESS Deployment ID' }, @@ -60,16 +81,22 @@ export function createCloudUsageCollector( type: 'keyword', _meta: { description: 'The Serverless Project type' }, }, + orchestratorTarget: { + type: 'keyword', + _meta: { description: 'The Orchestrator Target where it is deployed (canary/non-canary)' }, + }, }, fetch: () => { return { isCloudEnabled, isElasticStaffOwned, + organizationId, trialEndDate, ...(trialEndDateMs ? { inTrial: Date.now() <= trialEndDateMs } : {}), deploymentId, projectId, projectType, + orchestratorTarget, }; }, }); diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index de4ebd94b6f2b5..371f895b92e092 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -25,6 +25,7 @@ const configSchema = schema.object({ deployments_url: schema.string({ defaultValue: '/deployments' }), deployment_url: schema.maybe(schema.string()), id: schema.maybe(schema.string()), + organization_id: schema.maybe(schema.string()), billing_url: schema.maybe(schema.string()), performance_url: schema.maybe(schema.string()), users_and_roles_url: schema.maybe(schema.string()), @@ -44,6 +45,7 @@ const configSchema = schema.object({ project_id: schema.maybe(schema.string()), project_name: schema.maybe(schema.string()), project_type: schema.maybe(schema.string()), + orchestrator_target: schema.maybe(schema.string()), }, // avoid future chicken-and-egg situation with the component populating the config { unknowns: 'ignore' } @@ -60,6 +62,7 @@ export const config: PluginConfigDescriptor = { deployments_url: true, deployment_url: true, id: true, + organization_id: true, billing_url: true, users_and_roles_url: true, performance_url: true, @@ -72,6 +75,7 @@ export const config: PluginConfigDescriptor = { project_id: true, project_name: true, project_type: true, + orchestrator_target: true, }, onboarding: { default_solution: true, diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index d8d5d397655e39..362a69b4ac0a6a 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -35,6 +35,10 @@ export interface CloudSetup { * @note The `cloudId` is a concatenation of the deployment name and a hash. Users can update the deployment name, changing the `cloudId`. However, the changed `cloudId` will not be re-injected into `kibana.yml`. If you need the current `cloudId` the best approach is to split the injected `cloudId` on the semi-colon, and replace the first element with the `persistent.cluster.metadata.display_name` value as provided by a call to `GET _cluster/settings`. */ cloudId?: string; + /** + * The Elastic Cloud Organization that owns this deployment/project. + */ + organizationId?: string; /** * The deployment's ID. Only available when running on Elastic Cloud. */ @@ -127,6 +131,11 @@ export interface CloudSetup { * Will always be present if `isServerlessEnabled` is `true` */ projectType?: string; + /** + * The serverless orchestrator target. The potential values are `canary` or `non-canary` + * Will always be present if `isServerlessEnabled` is `true` + */ + orchestratorTarget?: string; }; } @@ -163,19 +172,23 @@ export class CloudPlugin implements Plugin { public setup(core: CoreSetup, { usageCollection }: PluginsSetup): CloudSetup { const isCloudEnabled = getIsCloudEnabled(this.config.id); + const organizationId = this.config.organization_id; const projectId = this.config.serverless?.project_id; const projectType = this.config.serverless?.project_type; + const orchestratorTarget = this.config.serverless?.orchestrator_target; const isServerlessEnabled = !!projectId; const deploymentId = parseDeploymentIdFromDeploymentUrl(this.config.deployment_url); registerCloudDeploymentMetadataAnalyticsContext(core.analytics, this.config); registerCloudUsageCollector(usageCollection, { isCloudEnabled, + organizationId, trialEndDate: this.config.trial_end_date, isElasticStaffOwned: this.config.is_elastic_staff_owned, deploymentId, projectId, projectType, + orchestratorTarget, }); let decodedId: DecodedCloudId | undefined; @@ -186,6 +199,7 @@ export class CloudPlugin implements Plugin { return { ...this.getCloudUrls(), cloudId: this.config.id, + organizationId, instanceSizeMb: readInstanceSizeMb(), deploymentId, elasticsearchUrl: decodedId?.elasticsearchUrl, @@ -207,6 +221,7 @@ export class CloudPlugin implements Plugin { projectId, projectName: this.config.serverless?.project_name, projectType, + orchestratorTarget, }, }; } diff --git a/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts b/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts index 94788d2d1d9260..f7b87b49dd2a7a 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts @@ -16,9 +16,9 @@ import { import { AnonymizationFieldResponse, - BulkActionSkipResult, - BulkCrudActionResponse, - BulkCrudActionResults, + AnonymizationFieldsBulkActionSkipResult, + AnonymizationFieldsBulkCrudActionResponse, + AnonymizationFieldsBulkCrudActionResults, BulkCrudActionSummary, PerformBulkActionRequestBody, PerformBulkActionResponse, @@ -63,9 +63,9 @@ const buildBulkResponse = ( updated?: AnonymizationFieldResponse[]; created?: AnonymizationFieldResponse[]; deleted?: string[]; - skipped?: BulkActionSkipResult[]; + skipped?: AnonymizationFieldsBulkActionSkipResult[]; } -): IKibanaResponse => { +): IKibanaResponse => { const numSucceeded = updated.length + created.length + deleted.length; const numSkipped = skipped.length; const numFailed = errors.length; @@ -77,7 +77,7 @@ const buildBulkResponse = ( total: numSucceeded + numFailed + numSkipped, }; - const results: BulkCrudActionResults = { + const results: AnonymizationFieldsBulkCrudActionResults = { updated, created, deleted, @@ -85,7 +85,7 @@ const buildBulkResponse = ( }; if (numFailed > 0) { - return response.custom({ + return response.custom({ headers: { 'content-type': 'application/json' }, body: { message: summary.succeeded > 0 ? 'Bulk edit partially failed' : 'Bulk edit failed', @@ -103,7 +103,7 @@ const buildBulkResponse = ( }); } - const responseBody: BulkCrudActionResponse = { + const responseBody: AnonymizationFieldsBulkCrudActionResponse = { success: true, anonymization_fields_count: summary.total, attributes: { results, summary }, diff --git a/x-pack/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts b/x-pack/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts index d90b01b78cfa70..c2713f28088d84 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts @@ -16,9 +16,9 @@ import { import { PromptResponse, - BulkActionSkipResult, - BulkCrudActionResponse, - BulkCrudActionResults, + PromptsBulkActionSkipResult, + PromptsBulkCrudActionResponse, + PromptsBulkCrudActionResults, BulkCrudActionSummary, PerformBulkActionRequestBody, PerformBulkActionResponse, @@ -60,9 +60,9 @@ const buildBulkResponse = ( updated?: PromptResponse[]; created?: PromptResponse[]; deleted?: string[]; - skipped?: BulkActionSkipResult[]; + skipped?: PromptsBulkActionSkipResult[]; } -): IKibanaResponse => { +): IKibanaResponse => { const numSucceeded = updated.length + created.length + deleted.length; const numSkipped = skipped.length; const numFailed = errors.length; @@ -74,7 +74,7 @@ const buildBulkResponse = ( total: numSucceeded + numFailed + numSkipped, }; - const results: BulkCrudActionResults = { + const results: PromptsBulkCrudActionResults = { updated, created, deleted, @@ -82,7 +82,7 @@ const buildBulkResponse = ( }; if (numFailed > 0) { - return response.custom({ + return response.custom({ headers: { 'content-type': 'application/json' }, body: { message: summary.succeeded > 0 ? 'Bulk edit partially failed' : 'Bulk edit failed', @@ -100,7 +100,7 @@ const buildBulkResponse = ( }); } - const responseBody: BulkCrudActionResponse = { + const responseBody: PromptsBulkCrudActionResponse = { success: true, prompts_count: summary.total, attributes: { results, summary }, diff --git a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.ts b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.ts index 56adf57a9b7a4b..e0844687490818 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.ts @@ -11,9 +11,9 @@ import type { IKibanaResponse, KibanaResponseFactory, Logger } from '@kbn/core/s import { transformError } from '@kbn/securitysolution-es-utils'; import { ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION, - BulkActionSkipResult, - BulkCrudActionResponse, - BulkCrudActionResults, + ConversationsBulkActionSkipResult, + ConversationsBulkCrudActionResponse, + ConversationsBulkCrudActionResults, BulkCrudActionSummary, PerformBulkActionRequestBody, PerformBulkActionResponse, @@ -61,9 +61,9 @@ const buildBulkResponse = ( updated?: ConversationResponse[]; created?: ConversationResponse[]; deleted?: string[]; - skipped?: BulkActionSkipResult[]; + skipped?: ConversationsBulkActionSkipResult[]; } -): IKibanaResponse => { +): IKibanaResponse => { const numSucceeded = updated.length + created.length + deleted.length; const numSkipped = skipped.length; const numFailed = errors.length; @@ -75,7 +75,7 @@ const buildBulkResponse = ( total: numSucceeded + numFailed + numSkipped, }; - const results: BulkCrudActionResults = { + const results: ConversationsBulkCrudActionResults = { updated, created, deleted, @@ -83,7 +83,7 @@ const buildBulkResponse = ( }; if (numFailed > 0) { - return response.custom({ + return response.custom({ headers: { 'content-type': 'application/json' }, body: { message: summary.succeeded > 0 ? 'Bulk edit partially failed' : 'Bulk edit failed', @@ -101,7 +101,7 @@ const buildBulkResponse = ( }); } - const responseBody: BulkCrudActionResponse = { + const responseBody: ConversationsBulkCrudActionResponse = { success: true, conversations_count: summary.total, attributes: { results, summary }, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_names_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_names_api_logic.ts new file mode 100644 index 00000000000000..5583c8c8e22e4a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector/generate_connector_names_api_logic.ts @@ -0,0 +1,26 @@ +/* + * 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 { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../shared/http'; + +export interface GenerateConnectorNamesApiArgs { + connectorType?: string; +} + +export const generateConnectorNames = async ( + { connectorType }: GenerateConnectorNamesApiArgs = { connectorType: 'custom' } +) => { + const route = `/internal/enterprise_search/connectors/generate_connector_name`; + return await HttpLogic.values.http.post(route, { + body: JSON.stringify({ connectorType }), + }); +}; + +export const GenerateConnectorNamesApiLogic = createApiLogic( + ['generate_config_api_logic'], + generateConnectorNames +); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx index fb64279019849e..2c209027930936 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/deployment.tsx @@ -119,9 +119,9 @@ export const ConnectorDeployment: React.FC = () => { ), status: selectedDeploymentMethod === null ? 'incomplete' : 'complete', title: i18n.translate( - 'xpack.enterpriseSearch.content.connector_detail.configurationConnector.steps.runConnectorService.title', + 'xpack.enterpriseSearch.content.connector_detail.configurationConnector.steps.chooseDeployment.title', { - defaultMessage: 'Run connector service', + defaultMessage: 'Choose your deployment method', } ), titleSize: 'xs', @@ -131,27 +131,20 @@ export const ConnectorDeployment: React.FC = () => { <> - {selectedDeploymentMethod === 'source' ? ( - - {i18n.translate( - 'xpack.enterpriseSearch.connectorConfiguration.configymlCodeBlockLabel', - { defaultMessage: 'config.yml' } - )} - - ), - }} - /> - ) : ( - - )} + + {i18n.translate( + 'xpack.enterpriseSearch.connectorConfiguration.configymlCodeBlockLabel', + { defaultMessage: 'config.yml' } + )} + + ), + }} + /> @@ -235,7 +228,7 @@ export const ConnectorDeployment: React.FC = () => { title: i18n.translate( 'xpack.enterpriseSearch.content.connector_detail.configurationConnector.steps.waitingForConnector.title', { - defaultMessage: 'Waiting for your connector', + defaultMessage: 'Waiting for your connector to check in', } ), titleSize: 'xs', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/header_actions/syncs_context_menu.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/header_actions/syncs_context_menu.test.tsx index 13088a0d4e8ecf..6b1114a5687695 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/header_actions/syncs_context_menu.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/header_actions/syncs_context_menu.test.tsx @@ -17,7 +17,7 @@ import { EuiResizeObserver, } from '@elastic/eui'; -import { IngestionStatus, IngestionMethod } from '@kbn/search-connectors'; +import { IngestionStatus, IngestionMethod, ConnectorStatus } from '@kbn/search-connectors'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { Status } from '../../../../../../common/types/api'; @@ -45,6 +45,7 @@ describe('SyncsContextMenu', () => { status: Status.SUCCESS, connector: { index_name: 'index_name', + status: ConnectorStatus.CONFIGURED, }, }; @@ -117,7 +118,10 @@ describe('SyncsContextMenu', () => { }); it('Cannot start a sync without an index name', () => { - setMockValues({ ...mockValues, connector: { index_name: null } }); + setMockValues({ + ...mockValues, + connector: { index_name: null, status: ConnectorStatus.CONFIGURED }, + }); const wrapper = mountWithIntl(); const button = wrapper.find( 'button[data-telemetry-id="entSearchContent-connector-header-sync-openSyncMenu"]' @@ -139,4 +143,13 @@ describe('SyncsContextMenu', () => { }) ); }); + + it("Sync button is disabled when connector isn't configured", () => { + setMockValues({ ...mockValues, connector: { status: null } }); + const wrapper = mountWithIntl(); + const button = wrapper.find( + 'button[data-telemetry-id="entSearchContent-connector-header-sync-openSyncMenu"]' + ); + expect(button.prop('disabled')).toEqual(true); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/header_actions/syncs_context_menu.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/header_actions/syncs_context_menu.tsx index b4c8f39c253df8..bdae6e0e2853c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/header_actions/syncs_context_menu.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/shared/header_actions/syncs_context_menu.tsx @@ -73,6 +73,8 @@ export const SyncsContextMenu: React.FC = ({ disabled = f const syncLoading = (isSyncing || isWaitingForSync) && ingestionStatus !== IngestionStatus.ERROR; + const isWaitingForConnector = !connector?.status || connector?.status === ConnectorStatus.CREATED; + const shouldShowDocumentLevelSecurity = productFeatures.hasDocumentLevelSecurityEnabled && hasDocumentLevelSecurityFeature; const shouldShowIncrementalSync = @@ -175,7 +177,7 @@ export const SyncsContextMenu: React.FC = ({ disabled = f => { + const prefix = toAlphanumeric(connectorType); + if (!prefix || prefix.length === 0) { + throw new Error('Connector type is required'); + } + for (let i = 0; i < 20; i++) { + const connectorName = `${prefix}-${uuidv4().split('-')[1]}`; + const indexName = `connector-${connectorName}`; + + const result = await indexOrAliasExists(client, indexName); + if (!result) { + return { + apiKeyName: indexName, + connectorName, + indexName, + }; + } + } + throw new Error(ErrorCode.GENERATE_INDEX_NAME_ERROR); +}; 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 e3ba2bd9d53cbc..21b00e82b6aa09 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 @@ -38,6 +38,7 @@ import { import { ErrorCode } from '../../../common/types/error_codes'; import { addConnector } from '../../lib/connectors/add_connector'; import { generateConfig } from '../../lib/connectors/generate_config'; +import { generateConnectorName } from '../../lib/connectors/generate_connector_name'; import { startSync } from '../../lib/connectors/start_sync'; import { deleteAccessControlIndex } from '../../lib/indices/delete_access_control_index'; import { fetchIndexCounts } from '../../lib/indices/fetch_index_counts'; @@ -835,4 +836,41 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) { }); }) ); + router.post( + { + path: '/internal/enterprise_search/connectors/generate_connector_name', + validate: { + body: schema.object({ + connectorType: schema.string(), + }), + }, + }, + elasticsearchErrorHandler(log, async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + const { connectorType } = request.body; + try { + const generatedNames = await generateConnectorName(client, connectorType ?? 'custom'); + return response.ok({ + body: generatedNames, + headers: { 'content-type': 'application/json' }, + }); + } catch (error) { + if (error.message === ErrorCode.GENERATE_INDEX_NAME_ERROR) { + return createError({ + errorCode: ErrorCode.GENERATE_INDEX_NAME_ERROR, + message: i18n.translate( + 'xpack.enterpriseSearch.server.routes.connectors.generateConfiguration.indexAlreadyExistsError', + { + defaultMessage: 'Cannot find a unique connector name', + } + ), + response, + statusCode: 409, + }); + } else { + throw error; + } + } + }) + ); } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts index 8543c54a9ea2fc..b3fadedcc964ec 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts @@ -157,7 +157,7 @@ describe('useAgentless', () => { }); describe('useSetupTechnology', () => { - const updateNewAgentPolicyMock = jest.fn(); + const setNewAgentPolicy = jest.fn(); const updateAgentPoliciesMock = jest.fn(); const setSelectedPolicyTabMock = jest.fn(); const newAgentPolicyMock = { @@ -202,7 +202,7 @@ describe('useSetupTechnology', () => { const { result } = renderHook(() => useSetupTechnology({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -217,7 +217,7 @@ describe('useSetupTechnology', () => { it('should fetch agentless policy if agentless feature is enabled and isServerless is true', async () => { const { waitForNextUpdate } = renderHook(() => useSetupTechnology({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -246,7 +246,7 @@ describe('useSetupTechnology', () => { }); const { result, waitForNextUpdate } = renderHook(() => useSetupTechnology({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -262,7 +262,7 @@ describe('useSetupTechnology', () => { waitForNextUpdate(); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); - expect(updateNewAgentPolicyMock).toHaveBeenCalledWith({ + expect(setNewAgentPolicy).toHaveBeenCalledWith({ name: 'Agentless policy for endpoint-1', supports_agentless: true, }); @@ -284,7 +284,7 @@ describe('useSetupTechnology', () => { }); const { result, rerender } = renderHook(() => useSetupTechnology({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -299,13 +299,13 @@ describe('useSetupTechnology', () => { }); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); - expect(updateNewAgentPolicyMock).toHaveBeenCalledWith({ + expect(setNewAgentPolicy).toHaveBeenCalledWith({ name: 'Agentless policy for endpoint-1', supports_agentless: true, }); rerender({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -316,7 +316,7 @@ describe('useSetupTechnology', () => { }); waitFor(() => { - expect(updateNewAgentPolicyMock).toHaveBeenCalledWith({ + expect(setNewAgentPolicy).toHaveBeenCalledWith({ name: 'Agentless policy for endpoint-2', supports_agentless: true, }); @@ -333,7 +333,7 @@ describe('useSetupTechnology', () => { const { result, waitForNextUpdate } = renderHook(() => useSetupTechnology({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -348,7 +348,7 @@ describe('useSetupTechnology', () => { }); waitForNextUpdate(); - expect(updateNewAgentPolicyMock).toHaveBeenCalledTimes(0); + expect(setNewAgentPolicy).toHaveBeenCalledTimes(0); }); it('should not fetch agentless policy if agentless is enabled but serverless is disabled', async () => { @@ -360,7 +360,7 @@ describe('useSetupTechnology', () => { const { result } = renderHook(() => useSetupTechnology({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -375,7 +375,7 @@ describe('useSetupTechnology', () => { it('should update agent policy and selected policy tab when setup technology is agentless', async () => { const { result, waitForNextUpdate } = renderHook(() => useSetupTechnology({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -396,7 +396,7 @@ describe('useSetupTechnology', () => { it('should update new agent policy and selected policy tab when setup technology is agent-based', async () => { const { result, waitForNextUpdate } = renderHook(() => useSetupTechnology({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -420,7 +420,7 @@ describe('useSetupTechnology', () => { expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); - expect(updateNewAgentPolicyMock).toHaveBeenCalledWith(newAgentPolicyMock); + expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); expect(setSelectedPolicyTabMock).toHaveBeenCalledWith(SelectedPolicyTab.NEW); }); @@ -431,7 +431,7 @@ describe('useSetupTechnology', () => { const { result } = renderHook(() => useSetupTechnology({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -451,7 +451,7 @@ describe('useSetupTechnology', () => { it('should not update agent policy and selected policy tab when setup technology matches the current one ', async () => { const { result, waitForNextUpdate } = renderHook(() => useSetupTechnology({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -469,14 +469,14 @@ describe('useSetupTechnology', () => { expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); - expect(updateNewAgentPolicyMock).not.toHaveBeenCalled(); + expect(setNewAgentPolicy).not.toHaveBeenCalled(); expect(setSelectedPolicyTabMock).not.toHaveBeenCalled(); }); it('should revert the agent policy name to the original value when switching from agentless back to agent-based', async () => { const { result, waitForNextUpdate } = renderHook(() => useSetupTechnology({ - updateNewAgentPolicy: updateNewAgentPolicyMock, + setNewAgentPolicy, newAgentPolicy: newAgentPolicyMock, updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, @@ -495,7 +495,7 @@ describe('useSetupTechnology', () => { expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); waitFor(() => { - expect(updateNewAgentPolicyMock).toHaveBeenCalledWith({ + expect(setNewAgentPolicy).toHaveBeenCalledWith({ name: 'Agentless policy for endpoint-1', supports_agentless: true, }); @@ -506,6 +506,6 @@ describe('useSetupTechnology', () => { }); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); - expect(updateNewAgentPolicyMock).toHaveBeenCalledWith(newAgentPolicyMock); + expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index 367fde516ae32e..cb72bfd8da245b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -75,7 +75,7 @@ export const useAgentless = () => { }; export function useSetupTechnology({ - updateNewAgentPolicy, + setNewAgentPolicy, newAgentPolicy, updateAgentPolicies, setSelectedPolicyTab, @@ -83,7 +83,7 @@ export function useSetupTechnology({ packagePolicy, isEditPage, }: { - updateNewAgentPolicy: (policy: NewAgentPolicy) => void; + setNewAgentPolicy: (policy: NewAgentPolicy) => void; newAgentPolicy: NewAgentPolicy; updateAgentPolicies: (policies: AgentPolicy[]) => void; setSelectedPolicyTab: (tab: SelectedPolicyTab) => void; @@ -121,7 +121,7 @@ export function useSetupTechnology({ }; if (nextNewAgentlessPolicy.name !== newAgentlessPolicy.name) { setNewAgentlessPolicy(nextNewAgentlessPolicy); - updateNewAgentPolicy(nextNewAgentlessPolicy as NewAgentPolicy); + setNewAgentPolicy(nextNewAgentlessPolicy as NewAgentPolicy); updateAgentPolicies([nextNewAgentlessPolicy] as AgentPolicy[]); } } @@ -132,7 +132,7 @@ export function useSetupTechnology({ packagePolicy.name, selectedSetupTechnology, updateAgentPolicies, - updateNewAgentPolicy, + setNewAgentPolicy, ]); useEffect(() => { @@ -168,23 +168,23 @@ export function useSetupTechnology({ if (setupTechnology === SetupTechnology.AGENTLESS) { if (isAgentlessCloudEnabled) { - updateNewAgentPolicy(newAgentlessPolicy as NewAgentPolicy); + setNewAgentPolicy(newAgentlessPolicy as NewAgentPolicy); setSelectedPolicyTab(SelectedPolicyTab.NEW); updateAgentPolicies([newAgentlessPolicy] as AgentPolicy[]); } // tech debt: remove this when Serverless uses the Agentless API // https://github.com/elastic/security-team/issues/9781 if (isAgentlessServerlessEnabled) { - updateNewAgentPolicy(newAgentlessPolicy as AgentPolicy); + setNewAgentPolicy(newAgentlessPolicy as AgentPolicy); updateAgentPolicies([newAgentlessPolicy] as AgentPolicy[]); setSelectedPolicyTab(SelectedPolicyTab.EXISTING); } } else if (setupTechnology === SetupTechnology.AGENT_BASED) { - updateNewAgentPolicy({ + setNewAgentPolicy({ ...newAgentBasedPolicy.current, supports_agentless: false, is_managed: false, - } as NewAgentPolicy); + }); setSelectedPolicyTab(SelectedPolicyTab.NEW); updateAgentPolicies([newAgentBasedPolicy.current] as AgentPolicy[]); } @@ -195,7 +195,7 @@ export function useSetupTechnology({ selectedSetupTechnology, isAgentlessCloudEnabled, isAgentlessServerlessEnabled, - updateNewAgentPolicy, + setNewAgentPolicy, newAgentlessPolicy, setSelectedPolicyTab, updateAgentPolicies, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 67275a3cf40364..7190a90d561984 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -352,7 +352,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ const { isAgentlessEnabled } = useAgentless(); const { handleSetupTechnologyChange, selectedSetupTechnology } = useSetupTechnology({ newAgentPolicy, - updateNewAgentPolicy, + setNewAgentPolicy, updateAgentPolicies, setSelectedPolicyTab, packageInfo, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx index 9ade778c74f310..dd349fca9909e4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx @@ -139,7 +139,7 @@ export function usePackagePolicySteps({ const { selectedSetupTechnology } = useSetupTechnology({ newAgentPolicy, - updateNewAgentPolicy, + setNewAgentPolicy, updateAgentPolicies, setSelectedPolicyTab, packageInfo, diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/unprivileged_info.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/unprivileged_info.tsx index e8d36591998cb5..67ec5696df07ad 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/unprivileged_info.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/unprivileged_info.tsx @@ -19,10 +19,10 @@ export const UnprivilegedInfo: React.FC = () => {

--unprivileged, - command: sudo ./elastic-agent, + command: elastic-agent install, guideLink: ( { + const { has_all_requested: hasAllRequested } = await client.security.hasPrivileges({ + body: entityDefinitionRuntimePrivileges, + }); + + return hasAllRequested; +}; + +const canDeleteEntityDefinition = async (client: ElasticsearchClient) => { + const { has_all_requested: hasAllRequested } = await client.security.hasPrivileges({ + body: entityDefinitionDeletionPrivileges, + }); + + return hasAllRequested; +}; + +const canManageAPIKey = async (client: ElasticsearchClient) => { + const { has_all_requested: hasAllRequested } = await client.security.hasPrivileges({ + body: apiKeyCreationPrivileges, + }); + + return hasAllRequested; +}; + +const canDeleteAPIKey = async (client: ElasticsearchClient) => { + const { has_all_requested: hasAllRequested } = await client.security.hasPrivileges({ + body: apiKeyDeletionPrivileges, + }); + + return hasAllRequested; +}; + +export const canEnableEntityDiscovery = async (client: ElasticsearchClient) => { + return Promise.all([canManageAPIKey(client), canManageEntityDefinition(client)]).then((results) => + results.every(Boolean) + ); +}; + +export const canDisableEntityDiscovery = async (client: ElasticsearchClient) => { + return Promise.all([canDeleteAPIKey(client), canDeleteEntityDefinition(client)]).then((results) => + results.every(Boolean) + ); +}; + +export const entityDefinitionRuntimePrivileges = { + cluster: ['manage_transform', 'manage_ingest_pipelines', 'manage_index_templates'], index: [ { names: [ENTITY_INDICES_PATTERN], @@ -21,50 +66,48 @@ export const requiredRunTimePrivileges = { privileges: ['read', 'view_index_metadata'], }, ], - cluster: [ - 'manage_transform', - 'monitor_transform', - 'manage_ingest_pipelines', - 'monitor', - 'manage_index_templates', - ], application: [ { application: 'kibana-.kibana', - privileges: ['saved_object:entity-definition/*'], + privileges: [`saved_object:${SO_ENTITY_DEFINITION_TYPE}/*`], resources: ['*'], }, ], }; -export const requiredEnablementPrivileges = { - // any one of - cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'], +export const entityDefinitionDeletionPrivileges = { + cluster: ['manage_transform', 'manage_ingest_pipelines', 'manage_index_templates'], + index: [ + { + names: [ENTITY_INDICES_PATTERN], + privileges: ['delete_index'], + }, + ], + application: [ + { + application: 'kibana-.kibana', + privileges: [`saved_object:${SO_ENTITY_DEFINITION_TYPE}/delete`], + resources: ['*'], + }, + ], }; -export const canRunEntityDiscovery = async (client: ElasticsearchClient) => { - const { has_all_requested: hasAllRequested } = await client.security.hasPrivileges({ - body: { - cluster: requiredRunTimePrivileges.cluster, - index: requiredRunTimePrivileges.index, - application: requiredRunTimePrivileges.application, +export const apiKeyCreationPrivileges = { + application: [ + { + application: 'kibana-.kibana', + privileges: [`saved_object:${SO_ENTITY_DISCOVERY_API_KEY_TYPE}/*`], + resources: ['*'], }, - }); - - return hasAllRequested; + ], }; -export const canEnableEntityDiscovery = async (client: ElasticsearchClient) => { - const [canRun, { cluster: grantedClusterPrivileges }] = await Promise.all([ - canRunEntityDiscovery(client), - client.security.hasPrivileges({ - body: { - cluster: requiredEnablementPrivileges.cluster, - }, - }), - ]); - - return ( - canRun && requiredEnablementPrivileges.cluster.some((k) => grantedClusterPrivileges[k] === true) - ); +const apiKeyDeletionPrivileges = { + application: [ + { + application: 'kibana-.kibana', + privileges: [`saved_object:${SO_ENTITY_DISCOVERY_API_KEY_TYPE}/delete`], + resources: ['*'], + }, + ], }; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/create_and_install_transform.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/create_and_install_transform.ts index aca51df235feab..99c089ac14600d 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/create_and_install_transform.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/create_and_install_transform.ts @@ -27,6 +27,24 @@ export async function createAndInstallHistoryTransform( } } +export async function createAndInstallHistoryBackfillTransform( + esClient: ElasticsearchClient, + definition: EntityDefinition, + logger: Logger +) { + try { + const historyTransform = generateHistoryTransform(definition, true); + await retryTransientEsErrors(() => esClient.transform.putTransform(historyTransform), { + logger, + }); + } catch (e) { + logger.error( + `Cannot create entity history backfill transform for [${definition.id}] entity definition` + ); + throw e; + } +} + export async function createAndInstallLatestTransform( esClient: ElasticsearchClient, definition: EntityDefinition, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts new file mode 100644 index 00000000000000..6d4026973ca380 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts @@ -0,0 +1,51 @@ +/* + * 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 { entityDefinitionSchema } from '@kbn/entities-schema'; +export const entityDefinitionWithBackfill = entityDefinitionSchema.parse({ + id: 'admin-console-services', + version: '999.999.999', + name: 'Services for Admin Console', + type: 'service', + indexPatterns: ['kbn-data-forge-fake_stack.*'], + history: { + timestampField: '@timestamp', + interval: '1m', + settings: { + backfillSyncDelay: '15m', + backfillLookbackPeriod: '72h', + backfillFrequency: '5m', + }, + }, + identityFields: ['log.logger', { field: 'event.category', optional: true }], + displayNameTemplate: '{{log.logger}}{{#event.category}}:{{.}}{{/event.category}}', + metadata: ['tags', 'host.name', 'host.os.name', { source: '_index', destination: 'sourceIndex' }], + metrics: [ + { + name: 'logRate', + equation: 'A', + metrics: [ + { + name: 'A', + aggregation: 'doc_count', + filter: 'log.level: *', + }, + ], + }, + { + name: 'errorRate', + equation: 'A', + metrics: [ + { + name: 'A', + aggregation: 'doc_count', + filter: 'log.level: "ERROR"', + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/generate_component_id.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/generate_component_id.ts index c41aae4f213476..1be53fc0af8c98 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/generate_component_id.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/generate_component_id.ts @@ -7,6 +7,7 @@ import { EntityDefinition } from '@kbn/entities-schema'; import { + ENTITY_HISTORY_BACKFILL_PREFIX_V1, ENTITY_HISTORY_INDEX_PREFIX_V1, ENTITY_HISTORY_PREFIX_V1, ENTITY_LATEST_INDEX_PREFIX_V1, @@ -18,6 +19,11 @@ function generateHistoryId(definition: EntityDefinition) { return `${ENTITY_HISTORY_PREFIX_V1}-${definition.id}`; } +// History Backfill +export function generateHistoryBackfillTransformId(definition: EntityDefinition) { + return `${ENTITY_HISTORY_BACKFILL_PREFIX_V1}-${definition.id}`; +} + export const generateHistoryTransformId = generateHistoryId; export const generateHistoryIngestPipelineId = generateHistoryId; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/is_backfill_enabled.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/is_backfill_enabled.ts new file mode 100644 index 00000000000000..6a97d3c950eec3 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/is_backfill_enabled.ts @@ -0,0 +1,12 @@ +/* + * 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 { EntityDefinition } from '@kbn/entities-schema'; + +export function isBackfillEnabled(definition: EntityDefinition) { + return definition.history.settings?.backfillSyncDelay != null; +} diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts index 43f18b2b81bf00..36c3f32342477e 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts @@ -26,7 +26,7 @@ function createMetadataPainlessScript(definition: EntityDefinition) { } return definition.metadata.reduce((acc, def) => { - const destination = def.destination || def.source; + const destination = def.destination; const optionalFieldPath = destination.replaceAll('.', '?.'); const next = ` if (ctx.entity?.metadata?.${optionalFieldPath} != null) { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts index b47f17b6b00fa1..875242f73d7519 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts @@ -18,6 +18,7 @@ import { createAndInstallLatestIngestPipeline, } from './create_and_install_ingest_pipeline'; import { + createAndInstallHistoryBackfillTransform, createAndInstallHistoryTransform, createAndInstallLatestTransform, } from './create_and_install_transform'; @@ -28,10 +29,12 @@ import { findEntityDefinitions } from './find_entity_definition'; import { saveEntityDefinition } from './save_entity_definition'; import { startTransform } from './start_transform'; import { + stopAndDeleteHistoryBackfillTransform, stopAndDeleteHistoryTransform, stopAndDeleteLatestTransform, } from './stop_and_delete_transform'; import { uninstallEntityDefinition } from './uninstall_entity_definition'; +import { isBackfillEnabled } from './helpers/is_backfill_enabled'; import { deleteTemplate, upsertTemplate } from '../manage_index_templates'; import { getEntitiesLatestIndexTemplateConfig } from '../../templates/entities_latest_template'; import { getEntitiesHistoryIndexTemplateConfig } from '../../templates/entities_history_template'; @@ -56,6 +59,7 @@ export async function installEntityDefinition({ }, transforms: { history: false, + backfill: false, latest: false, }, definition: false, @@ -98,6 +102,10 @@ export async function installEntityDefinition({ logger.debug(`Installing transforms for definition ${definition.id}`); await createAndInstallHistoryTransform(esClient, entityDefinition, logger); installState.transforms.history = true; + if (isBackfillEnabled(entityDefinition)) { + await createAndInstallHistoryBackfillTransform(esClient, entityDefinition, logger); + installState.transforms.backfill = true; + } await createAndInstallLatestTransform(esClient, entityDefinition, logger); installState.transforms.latest = true; @@ -120,6 +128,10 @@ export async function installEntityDefinition({ await stopAndDeleteHistoryTransform(esClient, definition, logger); } + if (installState.transforms.backfill) { + await stopAndDeleteHistoryBackfillTransform(esClient, definition, logger); + } + if (installState.transforms.latest) { await stopAndDeleteLatestTransform(esClient, definition, logger); } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/start_transform.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/start_transform.ts index 7de64394fabeee..46bb16ff00ae3f 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/start_transform.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/start_transform.ts @@ -8,10 +8,12 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { EntityDefinition } from '@kbn/entities-schema'; import { + generateHistoryBackfillTransformId, generateHistoryTransformId, generateLatestTransformId, } from './helpers/generate_component_id'; import { retryTransientEsErrors } from './helpers/retry'; +import { isBackfillEnabled } from './helpers/is_backfill_enabled'; export async function startTransform( esClient: ElasticsearchClient, @@ -26,6 +28,17 @@ export async function startTransform( esClient.transform.startTransform({ transform_id: historyTransformId }, { ignore: [409] }), { logger } ); + if (isBackfillEnabled(definition)) { + const historyBackfillTransformId = generateHistoryBackfillTransformId(definition); + await retryTransientEsErrors( + () => + esClient.transform.startTransform( + { transform_id: historyBackfillTransformId }, + { ignore: [409] } + ), + { logger } + ); + } await retryTransientEsErrors( () => esClient.transform.startTransform({ transform_id: latestTransformId }, { ignore: [409] }), diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts index 2b978217fdf98e..d49165be22106f 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts @@ -8,6 +8,7 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { EntityDefinition } from '@kbn/entities-schema'; import { + generateHistoryBackfillTransformId, generateHistoryTransformId, generateLatestTransformId, } from './helpers/generate_component_id'; @@ -42,6 +43,35 @@ export async function stopAndDeleteHistoryTransform( } } +export async function stopAndDeleteHistoryBackfillTransform( + esClient: ElasticsearchClient, + definition: EntityDefinition, + logger: Logger +) { + try { + const historyBackfillTransformId = generateHistoryBackfillTransformId(definition); + await retryTransientEsErrors( + () => + esClient.transform.stopTransform( + { transform_id: historyBackfillTransformId, wait_for_completion: true, force: true }, + { ignore: [409, 404] } + ), + { logger } + ); + await retryTransientEsErrors( + () => + esClient.transform.deleteTransform( + { transform_id: historyBackfillTransformId, force: true }, + { ignore: [404] } + ), + { logger } + ); + } catch (e) { + logger.error(`Cannot stop or delete history backfill transform [${definition.id}]`); + throw e; + } +} + export async function stopAndDeleteLatestTransform( esClient: ElasticsearchClient, definition: EntityDefinition, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap index b76cd81f6ecf9c..4ecdd0c3ab0243 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap @@ -1,6 +1,153 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`generateHistoryTransform(definition) should generate a valid latest transform 1`] = ` +exports[`generateHistoryTransform(definition) should generate a valid history backfill transform 1`] = ` +Object { + "_meta": Object { + "definitionVersion": "999.999.999", + "managed": false, + }, + "defer_validation": true, + "dest": Object { + "index": ".entities.v1.history.noop", + "pipeline": "entities-v1-history-admin-console-services", + }, + "frequency": "5m", + "pivot": Object { + "aggs": Object { + "_errorRate_A": Object { + "filter": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "log.level": "ERROR", + }, + }, + ], + }, + }, + }, + "_logRate_A": Object { + "filter": Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "exists": Object { + "field": "log.level", + }, + }, + ], + }, + }, + }, + "entity.lastSeenTimestamp": Object { + "max": Object { + "field": "@timestamp", + }, + }, + "entity.metadata.host.name": Object { + "terms": Object { + "field": "host.name", + "size": 1000, + }, + }, + "entity.metadata.host.os.name": Object { + "terms": Object { + "field": "host.os.name", + "size": 1000, + }, + }, + "entity.metadata.sourceIndex": Object { + "terms": Object { + "field": "_index", + "size": 1000, + }, + }, + "entity.metadata.tags": Object { + "terms": Object { + "field": "tags", + "size": 1000, + }, + }, + "entity.metrics.errorRate": Object { + "bucket_script": Object { + "buckets_path": Object { + "A": "_errorRate_A>_count", + }, + "script": Object { + "lang": "painless", + "source": "params.A", + }, + }, + }, + "entity.metrics.logRate": Object { + "bucket_script": Object { + "buckets_path": Object { + "A": "_logRate_A>_count", + }, + "script": Object { + "lang": "painless", + "source": "params.A", + }, + }, + }, + }, + "group_by": Object { + "@timestamp": Object { + "date_histogram": Object { + "field": "@timestamp", + "fixed_interval": "1m", + }, + }, + "entity.identity.event.category": Object { + "terms": Object { + "field": "event.category", + "missing_bucket": true, + }, + }, + "entity.identity.log.logger": Object { + "terms": Object { + "field": "log.logger", + "missing_bucket": false, + }, + }, + }, + }, + "settings": Object { + "deduce_mappings": false, + "unattended": true, + }, + "source": Object { + "index": Array [ + "kbn-data-forge-fake_stack.*", + ], + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-72h", + }, + }, + }, + ], + }, + }, + }, + "sync": Object { + "time": Object { + "delay": "15m", + "field": "@timestamp", + }, + }, + "transform_id": "entities-v1-history-backfill-admin-console-services", +} +`; + +exports[`generateHistoryTransform(definition) should generate a valid history transform 1`] = ` Object { "_meta": Object { "definitionVersion": "999.999.999", diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.test.ts index 8bb9f494d5f4e5..cde87d670c8c2b 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.test.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.test.ts @@ -6,11 +6,16 @@ */ import { entityDefinition } from '../helpers/fixtures/entity_definition'; +import { entityDefinitionWithBackfill } from '../helpers/fixtures/entity_definition_with_backfill'; import { generateHistoryTransform } from './generate_history_transform'; describe('generateHistoryTransform(definition)', () => { - it('should generate a valid latest transform', () => { + it('should generate a valid history transform', () => { const transform = generateHistoryTransform(entityDefinition); expect(transform).toMatchSnapshot(); }); + it('should generate a valid history backfill transform', () => { + const transform = generateHistoryTransform(entityDefinitionWithBackfill, true); + expect(transform).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts index 645225eaf688c0..05b0e7ee7fd544 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts @@ -21,19 +21,50 @@ import { generateHistoryTransformId, generateHistoryIngestPipelineId, generateHistoryIndexName, + generateHistoryBackfillTransformId, } from '../helpers/generate_component_id'; +import { isBackfillEnabled } from '../helpers/is_backfill_enabled'; export function generateHistoryTransform( - definition: EntityDefinition + definition: EntityDefinition, + backfill = false ): TransformPutTransformRequest { + if (backfill && !isBackfillEnabled(definition)) { + throw new Error( + 'This function was called with backfill=true without history.settings.backfillSyncDelay' + ); + } + const filter: QueryDslQueryContainer[] = []; if (definition.filter) { filter.push(getElasticsearchQueryOrThrow(definition.filter)); } + if (backfill && definition.history.settings?.backfillLookbackPeriod) { + filter.push({ + range: { + [definition.history.timestampField]: { + gte: `now-${definition.history.settings?.backfillLookbackPeriod.toJSON()}`, + }, + }, + }); + } + + const syncDelay = backfill + ? definition.history.settings?.backfillSyncDelay + : definition.history.settings?.syncDelay; + + const transformId = backfill + ? generateHistoryBackfillTransformId(definition) + : generateHistoryTransformId(definition); + + const frequency = backfill + ? definition.history.settings?.backfillFrequency + : definition.history.settings?.frequency; + return { - transform_id: generateHistoryTransformId(definition), + transform_id: transformId, _meta: { definitionVersion: definition.version, managed: definition.managed, @@ -53,11 +84,11 @@ export function generateHistoryTransform( index: `${generateHistoryIndexName({ id: 'noop' } as EntityDefinition)}`, pipeline: generateHistoryIngestPipelineId(definition), }, - frequency: definition.history.settings?.frequency ?? ENTITY_DEFAULT_HISTORY_FREQUENCY, + frequency: frequency || ENTITY_DEFAULT_HISTORY_FREQUENCY, sync: { time: { field: definition.history.settings?.syncField ?? definition.history.timestampField, - delay: definition.history.settings?.syncDelay ?? ENTITY_DEFAULT_HISTORY_SYNC_DELAY, + delay: syncDelay || ENTITY_DEFAULT_HISTORY_SYNC_DELAY, }, }, settings: { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts index 809ed5f2b57b9b..31ba3e9add0dc1 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_metadata_aggregations.ts @@ -34,7 +34,7 @@ export function generateLatestMetadataAggregations(definition: EntityDefinition) return definition.metadata.reduce( (aggs, metadata) => ({ ...aggs, - [`entity.metadata.${metadata.destination ?? metadata.source}`]: { + [`entity.metadata.${metadata.destination}`]: { filter: { range: { 'event.ingested': { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts index 9b8685031642a0..11f772ce2e9383 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts @@ -18,9 +18,11 @@ import { deleteIndices } from './delete_index'; import { deleteHistoryIngestPipeline, deleteLatestIngestPipeline } from './delete_ingest_pipeline'; import { findEntityDefinitions } from './find_entity_definition'; import { + stopAndDeleteHistoryBackfillTransform, stopAndDeleteHistoryTransform, stopAndDeleteLatestTransform, } from './stop_and_delete_transform'; +import { isBackfillEnabled } from './helpers/is_backfill_enabled'; import { deleteTemplate } from '../manage_index_templates'; export async function uninstallEntityDefinition({ @@ -37,6 +39,9 @@ export async function uninstallEntityDefinition({ deleteData?: boolean; }) { await stopAndDeleteHistoryTransform(esClient, definition, logger); + if (isBackfillEnabled(definition)) { + await stopAndDeleteHistoryBackfillTransform(esClient, definition, logger); + } await stopAndDeleteLatestTransform(esClient, definition, logger); await deleteHistoryIngestPipeline(esClient, definition, logger); await deleteLatestIngestPipeline(esClient, definition, logger); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts index 66fabf2c91327f..8ee8de3751ab21 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts @@ -6,7 +6,6 @@ */ import { RequestHandlerContext } from '@kbn/core/server'; -import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request'; import { SetupRouteOptions } from '../types'; import { checkIfEntityDiscoveryAPIKeyIsValid, readEntityDiscoveryAPIKey } from '../../lib/auth'; import { @@ -44,9 +43,8 @@ export function checkEntityDiscoveryEnabledRoute { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts index a81e1a28cd1a17..4a0500e7efbca0 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts @@ -6,16 +6,13 @@ */ import { RequestHandlerContext } from '@kbn/core/server'; -import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request'; import { schema } from '@kbn/config-schema'; import { SetupRouteOptions } from '../types'; -import { - checkIfEntityDiscoveryAPIKeyIsValid, - deleteEntityDiscoveryAPIKey, - readEntityDiscoveryAPIKey, -} from '../../lib/auth'; -import { ERROR_API_KEY_NOT_FOUND, ERROR_API_KEY_NOT_VALID } from '../../../common/errors'; +import { deleteEntityDiscoveryAPIKey, readEntityDiscoveryAPIKey } from '../../lib/auth'; +import { ERROR_USER_NOT_AUTHORIZED } from '../../../common/errors'; import { uninstallBuiltInEntityDefinitions } from '../../lib/entities/uninstall_entity_definition'; +import { canDisableEntityDiscovery } from '../../lib/auth/privileges'; +import { EntityDiscoveryApiKeyType } from '../../saved_objects'; export function disableEntityDiscoveryRoute({ router, @@ -33,23 +30,21 @@ export function disableEntityDiscoveryRoute({ }, async (context, req, res) => { try { - server.logger.debug('reading entity discovery API key from saved object'); - const apiKey = await readEntityDiscoveryAPIKey(server); - - if (apiKey === undefined) { - return res.ok({ body: { success: false, reason: ERROR_API_KEY_NOT_FOUND } }); - } - - server.logger.debug('validating existing entity discovery API key'); - const isValid = await checkIfEntityDiscoveryAPIKeyIsValid(server, apiKey); - - if (!isValid) { - return res.ok({ body: { success: false, reason: ERROR_API_KEY_NOT_VALID } }); + const esClient = (await context.core).elasticsearch.client.asCurrentUser; + const canDisable = await canDisableEntityDiscovery(esClient); + if (!canDisable) { + return res.ok({ + body: { + success: false, + reason: ERROR_USER_NOT_AUTHORIZED, + message: + 'Current Kibana user does not have the required permissions to disable entity discovery', + }, + }); } - - const fakeRequest = getFakeKibanaRequest({ id: apiKey.id, api_key: apiKey.apiKey }); - const soClient = server.core.savedObjects.getScopedClient(fakeRequest); - const esClient = server.core.elasticsearch.client.asScoped(fakeRequest).asCurrentUser; + const soClient = (await context.core).savedObjects.getClient({ + includedHiddenTypes: [EntityDiscoveryApiKeyType.name], + }); await uninstallBuiltInEntityDefinitions({ soClient, @@ -58,10 +53,16 @@ export function disableEntityDiscoveryRoute({ deleteData: req.query.deleteData, }); - await deleteEntityDiscoveryAPIKey((await context.core).savedObjects.client); - await server.security.authc.apiKeys.invalidateAsInternalUser({ - ids: [apiKey.id], - }); + server.logger.debug('reading entity discovery API key from saved object'); + const apiKey = await readEntityDiscoveryAPIKey(server); + // api key could be deleted outside of the apis, it does not affect the + // disablement flow + if (apiKey) { + await deleteEntityDiscoveryAPIKey(soClient); + await server.security.authc.apiKeys.invalidateAsInternalUser({ + ids: [apiKey.id], + }); + } return res.ok({ body: { success: true } }); } catch (err) { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts index 84977cf785ce26..d9af1105e42b40 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts @@ -6,7 +6,6 @@ */ import { RequestHandlerContext } from '@kbn/core/server'; -import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request'; import { SetupRouteOptions } from '../types'; import { canEnableEntityDiscovery, @@ -19,8 +18,8 @@ import { } from '../../lib/auth'; import { builtInDefinitions } from '../../lib/entities/built_in'; import { installBuiltInEntityDefinitions } from '../../lib/entities/install_entity_definition'; -import { EntityDiscoveryApiKeyType } from '../../saved_objects'; import { ERROR_API_KEY_SERVICE_DISABLED, ERROR_USER_NOT_AUTHORIZED } from '../../../common/errors'; +import { EntityDiscoveryApiKeyType } from '../../saved_objects'; export function enableEntityDiscoveryRoute({ router, @@ -62,7 +61,6 @@ export function enableEntityDiscoveryRoute({ const soClient = (await context.core).savedObjects.getClient({ includedHiddenTypes: [EntityDiscoveryApiKeyType.name], }); - const existingApiKey = await readEntityDiscoveryAPIKey(server); if (existingApiKey !== undefined) { @@ -79,20 +77,19 @@ export function enableEntityDiscoveryRoute({ const apiKey = await generateEntityDiscoveryAPIKey(server, req); if (apiKey === undefined) { - throw new Error('could not generate entity discovery API key'); + return res.customError({ + statusCode: 500, + body: new Error('could not generate entity discovery API key'), + }); } await saveEntityDiscoveryAPIKey(soClient, apiKey); - const fakeRequest = getFakeKibanaRequest({ id: apiKey.id, api_key: apiKey.apiKey }); - const scopedSoClient = server.core.savedObjects.getScopedClient(fakeRequest); - const scopedEsClient = server.core.elasticsearch.client.asScoped(fakeRequest).asCurrentUser; - await installBuiltInEntityDefinitions({ logger, builtInDefinitions, - esClient: scopedEsClient, - soClient: scopedSoClient, + esClient, + soClient, }); return res.ok({ body: { success: true } }); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/get.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/get.ts index 3f1ffde5afef4d..e9a6a8dbd31672 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/get.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/get.ts @@ -31,7 +31,7 @@ export function getEntityDefinitionRoute({ page: req.query.page ?? 1, perPage: req.query.perPage ?? 10, }); - return res.ok({ body: definitions }); + return res.ok({ body: { definitions } }); } catch (e) { return res.customError({ body: e, statusCode: 500 }); } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/reset.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/reset.ts index 6f97a5fbe0d519..7755fcf65b3c31 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/reset.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/reset.ts @@ -13,6 +13,7 @@ import { EntitySecurityException } from '../../lib/entities/errors/entity_securi import { InvalidTransformError } from '../../lib/entities/errors/invalid_transform_error'; import { readEntityDefinition } from '../../lib/entities/read_entity_definition'; import { + stopAndDeleteHistoryBackfillTransform, stopAndDeleteHistoryTransform, stopAndDeleteLatestTransform, } from '../../lib/entities/stop_and_delete_transform'; @@ -26,11 +27,13 @@ import { createAndInstallLatestIngestPipeline, } from '../../lib/entities/create_and_install_ingest_pipeline'; import { + createAndInstallHistoryBackfillTransform, createAndInstallHistoryTransform, createAndInstallLatestTransform, } from '../../lib/entities/create_and_install_transform'; import { startTransform } from '../../lib/entities/start_transform'; import { EntityDefinitionNotFound } from '../../lib/entities/errors/entity_not_found'; +import { isBackfillEnabled } from '../../lib/entities/helpers/is_backfill_enabled'; export function resetEntityDefinitionRoute({ router, @@ -52,6 +55,9 @@ export function resetEntityDefinitionRoute({ // Delete the transform and ingest pipeline await stopAndDeleteHistoryTransform(esClient, definition, logger); + if (isBackfillEnabled(definition)) { + await stopAndDeleteHistoryBackfillTransform(esClient, definition, logger); + } await stopAndDeleteLatestTransform(esClient, definition, logger); await deleteHistoryIngestPipeline(esClient, definition, logger); await deleteLatestIngestPipeline(esClient, definition, logger); @@ -61,6 +67,9 @@ export function resetEntityDefinitionRoute({ await createAndInstallHistoryIngestPipeline(esClient, definition, logger); await createAndInstallLatestIngestPipeline(esClient, definition, logger); await createAndInstallHistoryTransform(esClient, definition, logger); + if (isBackfillEnabled(definition)) { + await createAndInstallHistoryBackfillTransform(esClient, definition, logger); + } await createAndInstallLatestTransform(esClient, definition, logger); await startTransform(esClient, definition, logger); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/entity_discovery_api_key.ts b/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/entity_discovery_api_key.ts index bed5a5ca736735..7508f56636da38 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/entity_discovery_api_key.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/entity_discovery_api_key.ts @@ -7,11 +7,11 @@ import { SavedObjectsType } from '@kbn/core/server'; -const SO_ENTITY_DISCOVERY_API_KEY_TYPE = 'entity-discovery-api-key'; +export const SO_ENTITY_DISCOVERY_API_KEY_TYPE = 'entity-discovery-api-key'; export const EntityDiscoveryApiKeyType: SavedObjectsType = { name: SO_ENTITY_DISCOVERY_API_KEY_TYPE, - hidden: false, + hidden: true, namespaceType: 'multiple-isolated', mappings: { dynamic: false, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/index.ts b/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/index.ts index fd88e2e16e4a22..d787672d17d854 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/saved_objects/index.ts @@ -6,4 +6,7 @@ */ export { entityDefinition, SO_ENTITY_DEFINITION_TYPE } from './entity_definition'; -export { EntityDiscoveryApiKeyType } from './entity_discovery_api_key'; +export { + EntityDiscoveryApiKeyType, + SO_ENTITY_DISCOVERY_API_KEY_TYPE, +} from './entity_discovery_api_key'; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index d06b793f73499e..4e9e452ec35af0 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -169,22 +169,12 @@ export const allowedExperimentalValues = Object.freeze({ */ disableTimelineSaveTour: false, - /** - * Enables alerts suppression for ES|QL rules - */ - alertSuppressionForEsqlRuleEnabled: false, - /** * Enables the risk engine privileges route * and associated callout in the UI */ riskEnginePrivilegesRouteEnabled: true, - /** - * Enables alerts suppression for machine learning rules - */ - alertSuppressionForMachineLearningRuleEnabled: false, - /** * Enables experimental Experimental S1 integration data to be available in Analyzer */ diff --git a/x-pack/plugins/security_solution/common/types/detail_panel/index.ts b/x-pack/plugins/security_solution/common/types/detail_panel/index.ts deleted file mode 100644 index 454c1c5ff3a260..00000000000000 --- a/x-pack/plugins/security_solution/common/types/detail_panel/index.ts +++ /dev/null @@ -1,29 +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 type { TimelineTabs } from '../timeline'; - -type EmptyObject = Record; - -export type ExpandedEventType = - | { - panelView?: 'eventDetail'; - params?: { - eventId: string; - indexName: string; - refetch?: () => void; - }; - } - | EmptyObject; - -export type ExpandedDetailType = ExpandedEventType; - -export type ExpandedDetailTimeline = { - [tab in TimelineTabs]?: ExpandedDetailType; -}; - -export type ExpandedDetail = Partial>; diff --git a/x-pack/plugins/security_solution/common/types/index.ts b/x-pack/plugins/security_solution/common/types/index.ts index 42a3c10fc48e4f..78c42b5ed0871f 100644 --- a/x-pack/plugins/security_solution/common/types/index.ts +++ b/x-pack/plugins/security_solution/common/types/index.ts @@ -8,7 +8,6 @@ import type { Status } from '../api/detection_engine'; export * from './timeline'; -export * from './detail_panel'; export * from './header_actions'; export * from './session_view'; export * from './bulk_actions'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index e9d482344a28e0..6404fbe2a48a20 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -11,8 +11,6 @@ export * from './data_provider'; export * from './rows'; export * from './store'; -import type { ExpandedDetailType } from '../detail_panel'; - /** * Used for scrolling top inside a tab. Especially when swiching tabs. */ @@ -24,11 +22,6 @@ export interface ScrollToTopEvent { timestamp: number; } -export type ToggleDetailPanel = ExpandedDetailType & { - tabType?: TimelineTabs; - id: string; -}; - export enum TimelineTabs { query = 'query', graph = 'graph', diff --git a/x-pack/plugins/security_solution/common/types/timeline/store.ts b/x-pack/plugins/security_solution/common/types/timeline/store.ts index 433e3d934344f8..5beb765f3d623f 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/store.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/store.ts @@ -9,7 +9,6 @@ import type { Filter } from '@kbn/es-query'; import type { RowRendererId, TimelineTypeLiteral } from '../../api/timeline/model/api'; import type { Direction } from '../../search_strategy'; -import type { ExpandedDetailTimeline } from '../detail_panel'; import type { ColumnHeaderOptions, ColumnId } from '../header_actions'; import type { DataProvider } from './data_provider'; @@ -43,7 +42,6 @@ export interface TimelinePersistInput { }; defaultColumns?: ColumnHeaderOptions[]; excludedRowRendererIds?: RowRendererId[]; - expandedDetail?: ExpandedDetailTimeline; filters?: Filter[]; id: string; indexNames: string[]; diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index 79909414e6f968..1ba63b8c43662c 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -24,31 +24,15 @@ import { getRuleDetailsUrl, useFormatUrl } from '../../common/components/link_to import { useKibana, useNavigation } from '../../common/lib/kibana'; import { APP_ID, CASES_PATH, SecurityPageName } from '../../../common/constants'; import { timelineActions } from '../../timelines/store'; -import { useSourcererDataView } from '../../sourcerer/containers'; -import { SourcererScopeName } from '../../sourcerer/store/model'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { getEndpointDetailsPath } from '../../management/common/routing'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useInsertTimeline } from '../components/use_insert_timeline'; import * as timelineMarkdownPlugin from '../../common/components/markdown_editor/plugins/timeline'; -import { DetailsPanel } from '../../timelines/components/side_panel'; import { useFetchAlertData } from './use_fetch_alert_data'; import { useUpsellingMessage } from '../../common/hooks/use_upselling'; import { useFetchNotes } from '../../notes/hooks/use_fetch_notes'; -const TimelineDetailsPanel = () => { - const { browserFields, runtimeMappings } = useSourcererDataView(SourcererScopeName.detections); - return ( - - ); -}; - const CaseContainerComponent: React.FC = () => { const { cases, telemetry } = useKibana().services; const { getAppUrl, navigateTo } = useNavigation(); @@ -115,7 +99,6 @@ const CaseContainerComponent: React.FC = () => { columns: [], dataViewId: null, indexNames: [], - expandedDetail: {}, show: false, }) ); @@ -175,9 +158,6 @@ const CaseContainerComponent: React.FC = () => { hooks: { useInsertTimeline, }, - ui: { - renderTimelineDetailsPanel: TimelineDetailsPanel, - }, }, useFetchAlertData, onAlertsTableLoaded, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap deleted file mode 100644 index a5d72e7a8f3a55..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap +++ /dev/null @@ -1,223 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`JSON View rendering should match snapshot 1`] = ` - - - { - "_index": ".ds-logs-endpoint.events.network-default-2021.09.28-000001", - "_id": "TUWyf3wBFCFU0qRJTauW", - "_score": 1, - "fields": { - "host.os.full.text": [ - "Debian 10" - ], - "event.category": [ - "network" - ], - "process.name.text": [ - "filebeat" - ], - "host.os.name.text": [ - "Linux" - ], - "host.os.full": [ - "Debian 10" - ], - "host.hostname": [ - "test-linux-1" - ], - "process.pid": [ - 22535 - ], - "host.mac": [ - "42:01:0a:c8:00:32" - ], - "elastic.agent.id": [ - "abcdefg-f6d5-4ce6-915d-8f1f8f413624" - ], - "host.os.version": [ - "10" - ], - "host.os.name": [ - "Linux" - ], - "source.ip": [ - "127.0.0.1" - ], - "destination.address": [ - "127.0.0.1" - ], - "host.name": [ - "test-linux-1" - ], - "event.agent_id_status": [ - "verified" - ], - "event.kind": [ - "event" - ], - "event.outcome": [ - "unknown" - ], - "group.name": [ - "root" - ], - "user.id": [ - "0" - ], - "host.os.type": [ - "linux" - ], - "process.Ext.ancestry": [ - "MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTIyMzY0LTEzMjc4NjA2NTAyLjA=", - "MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTEtMTMyNzA3Njg2OTIuMA==" - ], - "user.Ext.real.id": [ - "0" - ], - "data_stream.type": [ - "logs" - ], - "host.architecture": [ - "x86_64" - ], - "process.name": [ - "filebeat" - ], - "agent.id": [ - "2ac9e9b3-f6d5-4ce6-915d-8f1f8f413624" - ], - "source.port": [ - 54146 - ], - "ecs.version": [ - "1.11.0" - ], - "event.created": [ - "2021-10-14T16:45:58.031Z" - ], - "agent.version": [ - "8.0.0-SNAPSHOT" - ], - "host.os.family": [ - "debian" - ], - "destination.port": [ - 9200 - ], - "group.id": [ - "0" - ], - "user.name": [ - "root" - ], - "source.address": [ - "127.0.0.1" - ], - "process.entity_id": [ - "MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTIyNTM1LTEzMjc4NjA2NTI4LjA=" - ], - "host.ip": [ - "127.0.0.1", - "::1", - "10.1.2.3", - "2001:0DB8:AC10:FE01::" - ], - "process.executable.caseless": [ - "/opt/elastic/agent/data/elastic-agent-058c40/install/filebeat-8.0.0-snapshot-linux-x86_64/filebeat" - ], - "event.sequence": [ - 44872 - ], - "agent.type": [ - "endpoint" - ], - "process.executable.text": [ - "/opt/Elastic/Agent/data/elastic-agent-058c40/install/filebeat-8.0.0-SNAPSHOT-linux-x86_64/filebeat" - ], - "group.Ext.real.name": [ - "root" - ], - "event.module": [ - "endpoint" - ], - "host.os.kernel": [ - "4.19.0-17-cloud-amd64 #1 SMP Debian 4.19.194-2 (2021-06-21)" - ], - "host.os.full.caseless": [ - "debian 10" - ], - "host.id": [ - "76ea303129f249aa7382338e4263eac1" - ], - "process.name.caseless": [ - "filebeat" - ], - "network.type": [ - "ipv4" - ], - "process.executable": [ - "/opt/Elastic/Agent/data/elastic-agent-058c40/install/filebeat-8.0.0-SNAPSHOT-linux-x86_64/filebeat" - ], - "user.Ext.real.name": [ - "root" - ], - "data_stream.namespace": [ - "default" - ], - "message": [ - "Endpoint network event" - ], - "destination.ip": [ - "127.0.0.1" - ], - "network.transport": [ - "tcp" - ], - "host.os.Ext.variant": [ - "Debian" - ], - "group.Ext.real.id": [ - "0" - ], - "event.ingested": [ - "2021-10-14T16:46:04.000Z" - ], - "event.action": [ - "connection_attempted" - ], - "@timestamp": [ - "2021-10-14T16:45:58.031Z" - ], - "host.os.platform": [ - "debian" - ], - "data_stream.dataset": [ - "endpoint.events.network" - ], - "event.type": [ - "start" - ], - "event.id": [ - "MKPXftjGeHiQzUNj++++nn6R" - ], - "host.os.name.caseless": [ - "linux" - ], - "event.dataset": [ - "endpoint.events.network" - ], - "user.name.text": [ - "root" - ] - } -} - - -`; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx deleted file mode 100644 index a92ec9901d7ef8..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx +++ /dev/null @@ -1,816 +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 React from 'react'; -import { waitFor, render, act } from '@testing-library/react'; - -import { AlertSummaryView } from './alert_summary_view'; -import { mockAlertDetailsData } from './__mocks__'; -import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; - -import { TestProviders, TestProvidersComponent } from '../../mock'; -import { TimelineId } from '../../../../common/types'; -import { mockBrowserFields } from '../../containers/source/mock'; -import * as i18n from './translations'; - -jest.mock('../../lib/kibana'); - -jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback', () => { - return { - useRuleWithFallback: jest.fn(), - }; -}); - -jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback', () => { - return { - useRuleWithFallback: jest.fn(), - }; -}); - -jest.mock('@kbn/cell-actions/src/hooks/use_load_actions', () => { - const actual = jest.requireActual('@kbn/cell-actions/src/hooks/use_load_actions'); - return { - ...actual, - useLoadActions: jest.fn().mockImplementation(() => ({ - value: [], - error: undefined, - loading: false, - })), - }; -}); - -jest.mock('../../hooks/use_get_field_spec'); - -const props = { - data: mockAlertDetailsData as TimelineEventsDetailsItem[], - browserFields: mockBrowserFields, - eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31', - scopeId: 'alerts-page', - title: '', - goToTable: jest.fn(), -}; - -describe('AlertSummaryView', () => { - beforeEach(() => { - jest.clearAllMocks(); - (useRuleWithFallback as jest.Mock).mockReturnValue({ - rule: { - note: 'investigation guide', - }, - }); - }); - test('render correct items', async () => { - await act(async () => { - const { getByTestId } = render( - - - - ); - expect(getByTestId('summary-view')).toBeInTheDocument(); - }); - }); - - test('it renders the action cell by default', async () => { - await act(async () => { - const { getAllByTestId } = render( - - - - ); - - expect(getAllByTestId('inlineActions').length).toBeGreaterThan(0); - }); - }); - - test('Renders the correct global fields', async () => { - await act(async () => { - const { getByText } = render( - - - - ); - - ['host.name', 'user.name', i18n.RULE_TYPE, 'query', 'rule.name'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - - test('it does NOT render the action cell for the active timeline', async () => { - await act(async () => { - const { queryAllByTestId } = render( - - - - ); - - expect(queryAllByTestId('inlineActions').length).toEqual(0); - }); - }); - - test('it does NOT render the action cell when readOnly is passed', async () => { - await act(async () => { - const { queryAllByTestId } = render( - - - - ); - expect(queryAllByTestId('inlineActions').length).toEqual(0); - }); - }); - - test("render no investigation guide if it doesn't exist", async () => { - (useRuleWithFallback as jest.Mock).mockReturnValue({ - rule: { - note: null, - }, - }); - await act(async () => { - const { queryByTestId } = render( - - - - ); - await waitFor(() => { - expect(queryByTestId('summary-view-guide')).not.toBeInTheDocument(); - }); - }); - }); - test('User specified investigation fields appear in summary rows', async () => { - const mockData = mockAlertDetailsData.map((item) => { - if (item.category === 'event' && item.field === 'event.category') { - return { - ...item, - values: ['network'], - originalValue: ['network'], - }; - } - return item; - }); - const renderProps = { - ...props, - investigationFields: ['custom.field'], - data: [ - ...mockData, - { category: 'custom', field: 'custom.field', values: ['blob'], originalValue: 'blob' }, - ] as TimelineEventsDetailsItem[], - }; - await act(async () => { - const { getByText } = render( - - - - ); - - [ - 'custom.field', - 'host.name', - 'user.name', - 'destination.address', - 'source.address', - 'source.port', - 'process.name', - ].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - test('Network event renders the correct summary rows', async () => { - const renderProps = { - ...props, - data: mockAlertDetailsData.map((item) => { - if (item.category === 'event' && item.field === 'event.category') { - return { - ...item, - values: ['network'], - originalValue: ['network'], - }; - } - return item; - }) as TimelineEventsDetailsItem[], - }; - await act(async () => { - const { getByText } = render( - - - - ); - - [ - 'host.name', - 'user.name', - 'destination.address', - 'source.address', - 'source.port', - 'process.name', - ].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - - test('DNS network event renders the correct summary rows', async () => { - const renderProps = { - ...props, - data: [ - ...(mockAlertDetailsData.map((item) => { - if (item.category === 'event' && item.field === 'event.category') { - return { - ...item, - values: ['network'], - originalValue: ['network'], - }; - } - return item; - }) as TimelineEventsDetailsItem[]), - { - category: 'dns', - field: 'dns.question.name', - values: ['www.example.com'], - originalValue: ['www.example.com'], - } as TimelineEventsDetailsItem, - ], - }; - await act(async () => { - const { getByText } = render( - - - - ); - - ['dns.question.name', 'process.name'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - - test('Memory event code renders additional summary rows', async () => { - const renderProps = { - ...props, - data: mockAlertDetailsData.map((item) => { - if (item.category === 'event' && item.field === 'event.code') { - return { - ...item, - values: ['shellcode_thread'], - originalValue: ['shellcode_thread'], - }; - } - return item; - }) as TimelineEventsDetailsItem[], - }; - await act(async () => { - const { getByText } = render( - - - - ); - ['host.name', 'user.name', 'Target.process.executable'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - test('Behavior event code renders additional summary rows', async () => { - const actualRuleDescription = 'The actual rule description'; - const renderProps = { - ...props, - data: [ - ...mockAlertDetailsData.map((item) => { - if (item.category === 'event' && item.field === 'event.code') { - return { - ...item, - values: ['behavior'], - originalValue: ['behavior'], - }; - } - if (item.category === 'event' && item.field === 'event.category') { - return { - ...item, - values: ['malware', 'process', 'file'], - originalValue: ['malware', 'process', 'file'], - }; - } - return item; - }), - { - category: 'rule', - field: 'rule.description', - values: [actualRuleDescription], - originalValue: [actualRuleDescription], - }, - ] as TimelineEventsDetailsItem[], - }; - await act(async () => { - const { getByText } = render( - - - - ); - ['host.name', 'user.name', 'process.name', actualRuleDescription].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - - test('Malware event category shows file fields', async () => { - const enhancedData = [ - ...mockAlertDetailsData.map((item) => { - if (item.category === 'event' && item.field === 'event.category') { - return { - ...item, - values: ['malware'], - originalValue: ['malware'], - }; - } - return item; - }), - { category: 'file', field: 'file.name', values: ['malware.exe'] }, - { - category: 'file', - field: 'file.hash.sha256', - values: ['3287rhf3847gb38fb3o984g9384g7b3b847gb'], - }, - ] as TimelineEventsDetailsItem[]; - const renderProps = { - ...props, - data: enhancedData, - }; - await act(async () => { - const { getByText } = render( - - - - ); - ['host.name', 'user.name', 'file.name', 'file.hash.sha256'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - - test('Ransomware event code shows correct fields', async () => { - const enhancedData = [ - ...mockAlertDetailsData.map((item) => { - if (item.category === 'event' && item.field === 'event.code') { - return { - ...item, - values: ['ransomware'], - originalValue: ['ransomware'], - }; - } - return item; - }), - { category: 'Ransomware', field: 'Ransomware.feature', values: ['mbr'] }, - { - category: 'process', - field: 'process.hash.sha256', - values: ['3287rhf3847gb38fb3o984g9384g7b3b847gb'], - }, - ] as TimelineEventsDetailsItem[]; - const renderProps = { - ...props, - data: enhancedData, - }; - await act(async () => { - const { getByText } = render( - - - - ); - ['process.hash.sha256', 'Ransomware.feature'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - - test('Machine learning events show correct fields', async () => { - const enhancedData = [ - ...mockAlertDetailsData.map((item) => { - if (item.category === 'kibana' && item.field === 'kibana.alert.rule.type') { - return { - ...item, - values: ['machine_learning'], - originalValue: ['machine_learning'], - }; - } - return item; - }), - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.machine_learning_job_id', - values: ['i_am_the_ml_job_id'], - }, - { category: 'kibana', field: 'kibana.alert.rule.parameters.anomaly_threshold', values: [2] }, - ] as TimelineEventsDetailsItem[]; - const renderProps = { - ...props, - data: enhancedData, - }; - await act(async () => { - const { getByText } = render( - - - - ); - ['i_am_the_ml_job_id', 'kibana.alert.rule.parameters.anomaly_threshold'].forEach( - (fieldId) => { - expect(getByText(fieldId)); - } - ); - }); - }); - - test('[legacy] Machine learning events show correct fields', async () => { - const enhancedData = [ - ...mockAlertDetailsData.map((item) => { - if (item.category === 'kibana' && item.field === 'kibana.alert.rule.type') { - return { - ...item, - values: ['machine_learning'], - originalValue: ['machine_learning'], - }; - } - return item; - }), - { - category: 'signal', - field: 'signal.rule.machine_learning_job_id', - values: ['i_am_the_ml_job_id'], - }, - { category: 'signal', field: 'signal.rule.anomaly_threshold', values: [2] }, - ] as TimelineEventsDetailsItem[]; - const renderProps = { - ...props, - data: enhancedData, - }; - await act(async () => { - const { getByText } = render( - - - - ); - ['i_am_the_ml_job_id', 'signal.rule.anomaly_threshold'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - - test('Threat match events show correct fields', async () => { - const enhancedData = [ - ...mockAlertDetailsData.map((item) => { - if (item.category === 'kibana' && item.field === 'kibana.alert.rule.type') { - return { - ...item, - values: ['threat_match'], - originalValue: ['threat_match'], - }; - } - return item; - }), - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.threat_index', - values: ['threat_index*'], - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.threat_query', - values: ['*query*'], - }, - ] as TimelineEventsDetailsItem[]; - const renderProps = { - ...props, - data: enhancedData, - }; - await act(async () => { - const { getByText } = render( - - - - ); - ['threat_index*', '*query*'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - - test('[legacy] Threat match events show correct fields', async () => { - const enhancedData = [ - ...mockAlertDetailsData.map((item) => { - if (item.category === 'kibana' && item.field === 'kibana.alert.rule.type') { - return { - ...item, - values: ['threat_match'], - originalValue: ['threat_match'], - }; - } - return item; - }), - { - category: 'signal', - field: 'signal.rule.threat_index', - values: ['threat_index*'], - }, - { - category: 'signal', - field: 'signal.rule.threat_query', - values: ['*query*'], - }, - ] as TimelineEventsDetailsItem[]; - const renderProps = { - ...props, - data: enhancedData, - }; - await act(async () => { - const { getByText } = render( - - - - ); - ['threat_index*', '*query*'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - - test('Ransomware event code resolves fields from the source event', async () => { - const renderProps = { - ...props, - data: mockAlertDetailsData.map((item) => { - if (item.category === 'event' && item.field === 'event.code') { - return { - ...item, - values: ['ransomware'], - originalValue: ['ransomware'], - }; - } - if (item.category === 'event' && item.field === 'event.category') { - return { - ...item, - values: ['malware', 'process', 'file'], - originalValue: ['malware', 'process', 'file'], - }; - } - return item; - }) as TimelineEventsDetailsItem[], - }; - await act(async () => { - const { getByText } = render( - - - - ); - ['host.name', 'user.name', 'process.name'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - - test('Threshold events have special fields', async () => { - const enhancedData = [ - ...mockAlertDetailsData.map((item) => { - if (item.category === 'kibana' && item.field === 'kibana.alert.rule.type') { - return { - ...item, - values: ['threshold'], - originalValue: ['threshold'], - }; - } - return item; - }), - { - category: 'kibana', - field: 'kibana.alert.threshold_result.count', - values: [9001], - originalValue: [9001], - }, - { - category: 'kibana', - field: 'kibana.alert.threshold_result.terms.value', - values: ['host-23084y2', '3084hf3n84p8934r8h'], - originalValue: ['host-23084y2', '3084hf3n84p8934r8h'], - }, - { - category: 'kibana', - field: 'kibana.alert.threshold_result.terms.field', - values: ['host.name', 'host.id'], - originalValue: ['host.name', 'host.id'], - }, - { - category: 'kibana', - field: 'kibana.alert.threshold_result.cardinality.field', - values: ['host.name'], - originalValue: ['host.name'], - }, - { - category: 'kibana', - field: 'kibana.alert.threshold_result.cardinality.value', - values: [9001], - originalValue: [9001], - }, - ] as TimelineEventsDetailsItem[]; - const renderProps = { - ...props, - data: enhancedData, - }; - await act(async () => { - const { getByText } = render( - - - - ); - - ['Event Count', 'Event Cardinality', 'host.name', 'host.id'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - }); - - test('Threshold fields are not shown when data is malformated', async () => { - const enhancedData = [ - ...mockAlertDetailsData.map((item) => { - if (item.category === 'kibana' && item.field === 'kibana.alert.rule.type') { - return { - ...item, - values: ['threshold'], - originalValue: ['threshold'], - }; - } - return item; - }), - { - category: 'kibana', - field: 'kibana.alert.threshold_result.count', - values: [9001], - originalValue: [9001], - }, - { - category: 'kibana', - field: 'kibana.alert.threshold_result.terms.field', - // This would be expected to have two entries - values: ['host.id'], - originalValue: ['host.id'], - }, - { - category: 'kibana', - field: 'kibana.alert.threshold_result.terms.value', - values: ['host-23084y2', '3084hf3n84p8934r8h'], - originalValue: ['host-23084y2', '3084hf3n84p8934r8h'], - }, - { - category: 'kibana', - field: 'kibana.alert.threshold_result.cardinality.field', - values: ['host.name'], - originalValue: ['host.name'], - }, - { - category: 'kibana', - field: 'kibana.alert.threshold_result.cardinality.value', - // This would be expected to have one entry - values: [], - originalValue: [], - }, - ] as TimelineEventsDetailsItem[]; - const renderProps = { - ...props, - data: enhancedData, - }; - await act(async () => { - const { getByText } = render( - - - - ); - - ['Event Count'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - - [ - 'host.name [threshold]', - 'host.id [threshold]', - 'Event Cardinality', - 'count(host.name) >= 9001', - ].forEach((fieldText) => { - expect(() => getByText(fieldText)).toThrow(); - }); - }); - }); - - test('Threshold fields are not shown when data is partially missing', async () => { - const enhancedData = [ - ...mockAlertDetailsData.map((item) => { - if (item.category === 'kibana' && item.field === 'kibana.alert.rule.type') { - return { - ...item, - values: ['threshold'], - originalValue: ['threshold'], - }; - } - return item; - }), - { - category: 'kibana', - field: 'kibana.alert.threshold_result.terms.field', - // This would be expected to have two entries - values: ['host.id'], - originalValue: ['host.id'], - }, - { - category: 'kibana', - field: 'kibana.alert.threshold_result.cardinality.field', - values: ['host.name'], - originalValue: ['host.name'], - }, - ] as TimelineEventsDetailsItem[]; - const renderProps = { - ...props, - data: enhancedData, - }; - await act(async () => { - const { getByText } = render( - - - - ); - - // The `value` fields are missing here, so the enriched field info cannot be calculated correctly - ['host.id [threshold]', 'Threshold Cardinality', 'count(host.name) >= 9001'].forEach( - (fieldText) => { - expect(() => getByText(fieldText)).toThrow(); - } - ); - }); - }); - - test('New terms events have special fields', () => { - const enhancedData = [ - ...mockAlertDetailsData.map((item) => { - if (item.category === 'kibana' && item.field === 'kibana.alert.rule.type') { - return { - ...item, - values: ['new_terms'], - originalValue: ['new_terms'], - }; - } - return item; - }), - { - category: 'kibana', - field: 'kibana.alert.new_terms', - values: ['127.0.0.1'], - originalValue: ['127.0.0.1'], - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.new_terms_fields', - values: ['host.ip'], - originalValue: ['host.ip'], - }, - ] as TimelineEventsDetailsItem[]; - const renderProps = { - ...props, - data: enhancedData, - }; - - const { getByText } = render( - - - - ); - - ['New Terms', '127.0.0.1', 'New Terms fields', 'host.ip'].forEach((fieldId) => { - expect(getByText(fieldId)); - }); - }); - - test("doesn't render empty fields", async () => { - const renderProps = { - ...props, - data: mockAlertDetailsData.map((item) => { - if (item.category === 'kibana' && item.field === 'kibana.alert.rule.name') { - return { - category: 'kibana', - field: 'kibana.alert.rule.name', - values: undefined, - originalValue: undefined, - }; - } - return item; - }) as TimelineEventsDetailsItem[], - }; - - await act(async () => { - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId('event-field-kibana.alert.rule.name')).not.toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx deleted file mode 100644 index 74971bfb901629..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ /dev/null @@ -1,51 +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 React from 'react'; - -import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; -import { useSummaryRows } from './get_alert_summary_rows'; -import { SummaryView } from './summary_view'; - -const AlertSummaryViewComponent: React.FC<{ - browserFields: BrowserFields; - data: TimelineEventsDetailsItem[]; - eventId: string; - isDraggable?: boolean; - scopeId: string; - title: string; - goToTable: () => void; - isReadOnly?: boolean; - investigationFields?: string[]; -}> = ({ - browserFields, - data, - eventId, - isDraggable, - scopeId, - title, - goToTable, - isReadOnly, - investigationFields, -}) => { - const summaryRows = useSummaryRows({ - browserFields, - data, - eventId, - isDraggable, - scopeId, - isReadOnly, - investigationFields, - }); - - return ( - - ); -}; - -export const AlertSummaryView = React.memo(AlertSummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx deleted file mode 100644 index d07343516963d4..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx +++ /dev/null @@ -1,222 +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 styled from 'styled-components'; -import { get } from 'lodash/fp'; -import React, { useMemo } from 'react'; -import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { partition } from 'lodash'; -import { - SecurityCellActions, - CellActionsMode, - SecurityCellActionsTrigger, -} from '../../cell_actions'; -import * as i18n from './translations'; -import type { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; -import { getEnrichmentIdentifiers, isInvestigationTimeEnrichment } from './helpers'; - -import type { FieldsData } from '../types'; -import type { - BrowserFields, - TimelineEventsDetailsItem, -} from '../../../../../common/search_strategy'; -import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; -import { EnrichedDataRow, ThreatSummaryPanelHeader } from './threat_summary_view'; -import { getSourcererScopeId } from '../../../../helpers'; -import { getFieldFormat } from '../get_field_format'; - -export interface ThreatSummaryDescription { - data: FieldsData | undefined; - eventId: string; - index: number; - feedName: string | undefined; - scopeId: string; - value: string | undefined; - isDraggable?: boolean; - isReadOnly?: boolean; -} - -const EnrichmentFieldFeedName = styled.span` - white-space: nowrap; - font-style: italic; -`; - -export const StyledEuiFlexGroup = styled(EuiFlexGroup)` - .inlineActions { - opacity: 0; - } - - .inlineActions-popoverOpen { - opacity: 1; - } - - &:hover { - .inlineActions { - opacity: 1; - } - } -`; - -const EnrichmentDescription: React.FC = ({ - data, - eventId, - index, - feedName, - scopeId, - value, - isDraggable, - isReadOnly, -}) => { - const metadata = useMemo(() => ({ scopeId }), [scopeId]); - - if (!data || !value) return null; - const key = `alert-details-value-formatted-field-value-${scopeId}-${eventId}-${data.field}-${value}-${index}-${feedName}`; - - return ( - - -

- - {feedName && ( - - {' '} - {i18n.FEED_NAME_PREPOSITION} {feedName} - - )} -
- - - {value && !isReadOnly && ( - - )} - - - ); -}; - -const EnrichmentSummaryComponent: React.FC<{ - browserFields: BrowserFields; - data: TimelineEventsDetailsItem[]; - enrichments: CtiEnrichment[]; - scopeId: string; - eventId: string; - isDraggable?: boolean; - isReadOnly?: boolean; -}> = ({ browserFields, data, enrichments, scopeId, eventId, isDraggable, isReadOnly }) => { - const parsedEnrichments = enrichments.map((enrichment, index) => { - const { field, type, feedName, value } = getEnrichmentIdentifiers(enrichment); - const eventData = data.find((item) => item.field === field); - const category = eventData?.category ?? ''; - const browserField = get([category, 'fields', field ?? ''], browserFields); - - const fieldsData: FieldsData = { - field: field ?? '', - format: getFieldFormat(browserField) ?? '', - type: browserField?.type ?? '', - isObjectArray: eventData?.isObjectArray ?? false, - }; - - return { - fieldsData, - type, - feedName, - index, - field, - browserField, - value, - }; - }); - - const [investigation, indicator] = partition(parsedEnrichments, ({ type }) => - isInvestigationTimeEnrichment(type) - ); - - return ( - <> - {indicator.length > 0 && ( - - - - - {indicator.map(({ fieldsData, index, field, feedName, browserField, value }) => ( - - } - /> - ))} - - - )} - - {investigation.length > 0 && ( - - - - - {investigation.map(({ fieldsData, index, field, feedName, browserField, value }) => ( - - } - /> - ))} - - - )} - - ); -}; -export const EnrichmentSummary = React.memo(EnrichmentSummaryComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index e811b33e3b572e..d3a27857098025 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -72,6 +72,7 @@ const EnrichmentSection: React.FC<{ ); }; +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 const ThreatDetailsViewComponent: React.FC<{ enrichments: CtiEnrichment[]; showInvestigationTimeEnrichments: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx deleted file mode 100644 index 2ca1ba45e01cb8..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.test.tsx +++ /dev/null @@ -1,73 +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 React from 'react'; - -import { ThreatSummaryView } from './threat_summary_view'; -import { TestProviders } from '../../../mock'; -import { render } from '@testing-library/react'; -import { buildEventEnrichmentMock } from '../../../../../common/search_strategy/security_solution/cti/index.mock'; -import { mockAlertDetailsData } from '../__mocks__'; -import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; -import { mockBrowserFields } from '../../../containers/source/mock'; -import { mockTimelines } from '../../../mock/mock_timelines_plugin'; - -jest.mock('../../../lib/kibana', () => ({ - useKibana: () => ({ - services: { - timelines: { ...mockTimelines }, - }, - }), -})); - -jest.mock('../../../../helper_hooks', () => ({ - useHasSecurityCapability: () => true, -})); - -jest.mock('../table/field_name_cell'); - -const RISK_SCORE_DATA_ROWS = 2; - -const EMPTY_RISK_SCORE = { - loading: false, - isModuleEnabled: true, - result: [], -}; - -describe('ThreatSummaryView', () => { - const eventId = '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31'; - const scopeId = 'alerts-page'; - const data = mockAlertDetailsData as TimelineEventsDetailsItem[]; - const browserFields = mockBrowserFields; - - it("renders 'Enriched with Threat Intelligence' panel with fields", () => { - const enrichments = [ - buildEventEnrichmentMock({ 'matched.id': ['test.id'], 'matched.field': ['test.field'] }), - buildEventEnrichmentMock({ 'matched.id': ['other.id'], 'matched.field': ['other.field'] }), - ]; - - const { getByText, getAllByTestId } = render( - - - - ); - - expect(getByText('Enriched with threat intelligence')).toBeInTheDocument(); - - expect(getAllByTestId('EnrichedDataRow')).toHaveLength( - enrichments.length + RISK_SCORE_DATA_ROWS - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx deleted file mode 100644 index 3f32ba4fd5a009..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx +++ /dev/null @@ -1,171 +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 styled from 'styled-components'; -import type { FC, PropsWithChildren } from 'react'; -import React from 'react'; -import { EuiTitle, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import type { HostRisk, UserRisk } from '../../../../entity_analytics/api/types'; -import * as i18n from './translations'; -import type { CtiEnrichment } from '../../../../../common/search_strategy/security_solution/cti'; - -import type { - BrowserFields, - TimelineEventsDetailsItem, - RiskSeverity, -} from '../../../../../common/search_strategy'; -import { RiskSummaryPanel } from '../../../../entity_analytics/components/risk_summary_panel'; -import { EnrichmentSummary } from './enrichment_summary'; -import { RiskScoreEntity } from '../../../../../common/search_strategy'; -import { useHasSecurityCapability } from '../../../../helper_hooks'; -import { RiskScoreInfoTooltip } from '../../../../overview/components/common'; - -const UppercaseEuiTitle = styled(EuiTitle)` - text-transform: uppercase; -`; - -const ThreatSummaryPanelTitle: FC> = ({ children }) => ( - -
{children}
-
-); - -const StyledEnrichmentFieldTitle = styled(EuiTitle)` - width: 220px; -`; - -const EnrichmentFieldTitle: React.FC<{ - title: string | React.ReactNode | undefined; -}> = ({ title }) => ( - -
{title}
-
-); - -const StyledEuiFlexGroup = styled(EuiFlexGroup)` - font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; - margin-top: ${({ theme }) => theme.eui.euiSizeS}; -`; - -export const EnrichedDataRow: React.FC<{ - field: string | React.ReactNode | undefined; - value: React.ReactNode; -}> = ({ field, value }) => ( - - - - - {value} - -); - -export const ThreatSummaryPanelHeader: React.FC<{ - title: string | React.ReactNode; - toolTipContent: React.ReactNode; - toolTipTitle?: React.ReactNode; -}> = ({ title, toolTipContent, toolTipTitle }) => { - return ( - - - {title} - - - - - - ); -}; - -const ThreatSummaryViewComponent: React.FC<{ - browserFields: BrowserFields; - data: TimelineEventsDetailsItem[]; - enrichments: CtiEnrichment[]; - eventId: string; - scopeId: string; - hostRisk: HostRisk; - userRisk: UserRisk; - isDraggable?: boolean; - isReadOnly?: boolean; -}> = ({ - browserFields, - data, - enrichments, - eventId, - scopeId, - hostRisk, - userRisk, - isDraggable, - isReadOnly, -}) => { - const originalHostRisk = data?.find( - (eventDetail) => eventDetail?.field === 'host.risk.calculated_level' - )?.values?.[0] as RiskSeverity | undefined; - - const originalUserRisk = data?.find( - (eventDetail) => eventDetail?.field === 'user.risk.calculated_level' - )?.values?.[0] as RiskSeverity | undefined; - - const hasEntityAnalyticsCapability = useHasSecurityCapability('entity-analytics'); - - if (!hasEntityAnalyticsCapability && enrichments.length === 0) { - return null; - } - - return ( - <> - - - -
{i18n.ENRICHED_DATA}
-
- - - - {hasEntityAnalyticsCapability && ( - <> - - - - - - - - - )} - - - - - ); -}; - -export const ThreatSummaryView = React.memo(ThreatSummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts index ecc5dec40d99e3..b1d84eadc8c228 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts @@ -6,8 +6,6 @@ */ import { i18n } from '@kbn/i18n'; -import { getRiskEntityTranslation } from '../../../../entity_analytics/components/risk_score/translations'; -import type { RiskScoreEntity } from '../../../../../common/search_strategy'; export * from '../../../../entity_analytics/components/risk_score/translations'; export const FEED_NAME_PREPOSITION = i18n.translate( @@ -46,13 +44,6 @@ export const INVESTIGATION_TOOLTIP_CONTENT = i18n.translate( } ); -export const NO_INVESTIGATION_ENRICHMENTS_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.alertDetails.noInvestigationEnrichmentsDescription', - { - defaultMessage: 'This alert does not have supplemental threat intelligence data.', - } -); - export const NO_ENRICHMENTS_FOUND_DESCRIPTION = i18n.translate( 'xpack.securitySolution.alertDetails.noEnrichmentsFoundDescription', { @@ -85,13 +76,6 @@ export const REFRESH = i18n.translate('xpack.securitySolution.alertDetails.refre defaultMessage: 'Refresh', }); -export const ENRICHED_DATA = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.enrichedDataTitle', - { - defaultMessage: 'Enriched data', - } -); - export const NESTED_OBJECT_VALUES_NOT_RENDERED = i18n.translate( 'xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentObjectValuesNotRendered', { @@ -99,27 +83,3 @@ export const NESTED_OBJECT_VALUES_NOT_RENDERED = i18n.translate( 'This field contains nested object values, which are not rendered here. See the full document for all fields/values', } ); - -export const CURRENT_RISK_LEVEL = (riskEntity: RiskScoreEntity) => - i18n.translate('xpack.securitySolution.alertDetails.overview.hostRiskLevel', { - defaultMessage: 'Current {riskEntity} risk level', - values: { - riskEntity: getRiskEntityTranslation(riskEntity, true), - }, - }); - -export const ORIGINAL_RISK_LEVEL = (riskEntity: RiskScoreEntity) => - i18n.translate('xpack.securitySolution.alertDetails.overview.originalHostRiskLevel', { - defaultMessage: 'Original {riskEntity} risk level', - values: { - riskEntity: getRiskEntityTranslation(riskEntity, true), - }, - }); - -export const RISK_DATA_TITLE = (riskEntity: RiskScoreEntity) => - i18n.translate('xpack.securitySolution.alertDetails.overview.hostRiskDataTitle', { - defaultMessage: '{riskEntity} Risk Data', - values: { - riskEntity: getRiskEntityTranslation(riskEntity), - }, - }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx deleted file mode 100644 index d7e4a92fe5a2a0..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ /dev/null @@ -1,278 +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 { waitFor } from '@testing-library/react'; -import { mount } from 'enzyme'; -import type { ReactWrapper } from 'enzyme'; -import React from 'react'; - -import '../../mock/react_beautiful_dnd'; -import { - mockDetailItemData, - mockDetailItemDataId, - mockEcsDataWithAlert, - rawEventData, - TestProviders, -} from '../../mock'; - -import { EventDetails, EVENT_DETAILS_CONTEXT_ID, EventsViewType } from './event_details'; -import { mockBrowserFields } from '../../containers/source/mock'; -import { mockAlertDetailsData } from './__mocks__'; -import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { TimelineTabs } from '../../../../common/types/timeline'; -import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enrichment'; -import { useKibana } from '../../lib/kibana'; -import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; -import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; - -jest.mock('../../hooks/use_experimental_features'); -jest.mock('../../../timelines/components/timeline/body/renderers', () => { - return { - defaultRowRenderers: [ - { - id: 'test', - isInstance: () => true, - renderRow: jest.fn(), - }, - ], - }; -}); - -jest.mock('../../lib/kibana'); -const useKibanaMock = useKibana as jest.Mocked; - -jest.mock('../../containers/cti/event_enrichment'); - -jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback', () => { - return { - useRuleWithFallback: jest.fn().mockReturnValue({ - rule: { - note: 'investigation guide', - }, - }), - }; -}); - -jest.mock('../guided_onboarding_tour/tour_step', () => ({ - GuidedOnboardingTourStep: jest.fn(({ children }) => ( -
{children}
- )), -})); - -jest.mock('../link_to'); -describe('EventDetails', () => { - const defaultProps = { - browserFields: mockBrowserFields, - data: mockDetailItemData, - detailsEcsData: mockEcsDataWithAlert, - id: mockDetailItemDataId, - isAlert: false, - onEventViewSelected: jest.fn(), - onThreatViewSelected: jest.fn(), - timelineTabType: TimelineTabs.query, - scopeId: 'table-test', - eventView: EventsViewType.summaryView, - hostRisk: { fields: [], loading: true }, - indexName: 'test', - handleOnEventClosed: jest.fn(), - rawEventData, - }; - - const alertsProps = { - ...defaultProps, - data: mockAlertDetailsData as TimelineEventsDetailsItem[], - isAlert: true, - }; - - let wrapper: ReactWrapper; - let alertsWrapper: ReactWrapper; - beforeAll(async () => { - (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ - result: [], - range: { to: 'now', from: 'now-30d' }, - setRange: jest.fn(), - loading: false, - }); - wrapper = mount( - - - - ) as ReactWrapper; - alertsWrapper = mount( - - - - ) as ReactWrapper; - await waitFor(() => wrapper.update()); - }); - - describe('tabs', () => { - ['Table', 'JSON'].forEach((tab) => { - test(`it renders the ${tab} tab`, () => { - expect( - wrapper - .find('[data-test-subj="eventDetails"]') - .find('[role="tablist"]') - .containsMatchingElement({tab}) - ).toBeTruthy(); - }); - }); - - test('the Table tab is selected by default', () => { - expect( - wrapper.find('[data-test-subj="eventDetails"]').find('.euiTab-isSelected').first().text() - ).toEqual('Table'); - }); - }); - - describe('alerts tabs', () => { - ['Overview', 'Threat Intel', 'Table', 'JSON'].forEach((tab) => { - test(`it renders the ${tab} tab`, () => { - expect( - alertsWrapper - .find('[data-test-subj="eventDetails"]') - .find('[role="tablist"]') - .containsMatchingElement({tab}) - ).toBeTruthy(); - }); - }); - - test('the Overview tab is selected by default', () => { - expect( - alertsWrapper - .find('[data-test-subj="eventDetails"]') - .find('.euiTab-isSelected') - .first() - .text() - ).toEqual('Overview'); - }); - - test('Enrichment count is displayed as a notification', () => { - expect( - alertsWrapper.find('[data-test-subj="enrichment-count-notification"]').hostNodes().text() - ).toEqual('1'); - }); - }); - - describe('summary view tab', () => { - it('render investigation guide', () => { - expect(alertsWrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(true); - }); - - test('it renders the alert / event via a renderer', () => { - expect(alertsWrapper.find('[data-test-subj="renderer"]').first().text()).toEqual( - 'Access event with source 192.168.0.1:80, destination 192.168.0.3:6343, by john.dee on apache' - ); - }); - - test('it invokes `renderRow()` with the expected `contextId`, to ensure unique drag & drop IDs', () => { - expect((defaultRowRenderers[0].renderRow as jest.Mock).mock.calls[0][0].contextId).toEqual( - EVENT_DETAILS_CONTEXT_ID - ); - }); - - test('renders GuidedOnboardingTourStep', () => { - expect(alertsWrapper.find('[data-test-subj="guided-onboarding"]').exists()).toEqual(true); - }); - }); - - describe('threat intel tab', () => { - it('renders a "no enrichments" panel view if there are no enrichments', () => { - alertsWrapper.find('[data-test-subj="threatIntelTab"]').first().simulate('click'); - expect(alertsWrapper.find('[data-test-subj="no-enrichments-found"]').exists()).toEqual(true); - }); - it('does not render if readOnly prop is passed', async () => { - const newProps = { ...defaultProps, isReadOnly: true }; - wrapper = mount( - - - - ) as ReactWrapper; - alertsWrapper = mount( - - - - ) as ReactWrapper; - await waitFor(() => wrapper.update()); - expect(alertsWrapper.find('[data-test-subj="threatIntelTab"]').exists()).toBeFalsy(); - }); - }); - - describe('osquery tab', () => { - let featureFlags: { endpointResponseActionsEnabled: boolean; responseActionsEnabled: boolean }; - - beforeEach(() => { - featureFlags = { endpointResponseActionsEnabled: false, responseActionsEnabled: true }; - - const useIsExperimentalFeatureEnabledMock = (feature: keyof typeof featureFlags) => - featureFlags[feature]; - - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( - useIsExperimentalFeatureEnabledMock - ); - }); - it('should not be rendered if not provided with specific raw data', () => { - expect(alertsWrapper.find('[data-test-subj="osqueryViewTab"]').exists()).toEqual(false); - }); - - it('render osquery tab', async () => { - const { - services: { osquery }, - } = useKibanaMock(); - if (osquery) { - jest.spyOn(osquery, 'fetchAllLiveQueries').mockReturnValue({ - data: { - // @ts-expect-error - we don't need all the response details to test the functionality - data: { - items: [ - { - _id: 'testId', - _index: 'testIndex', - fields: { - action_id: ['testActionId'], - 'queries.action_id': ['testQueryActionId'], - 'queries.query': ['select * from users'], - '@timestamp': ['2022-09-08T18:16:30.256Z'], - }, - }, - ], - }, - }, - }); - } - const newProps = { - ...defaultProps, - rawEventData: { - ...rawEventData, - fields: { - ...rawEventData.fields, - 'agent.id': ['testAgent'], - 'kibana.alert.rule.name': ['test-rule'], - 'kibana.alert.rule.parameters': [ - { - response_actions: [{ action_type_id: '.osquery' }], - }, - ], - }, - }, - }; - wrapper = mount( - - - - ) as ReactWrapper; - alertsWrapper = mount( - - - - ) as ReactWrapper; - await waitFor(() => wrapper.update()); - - expect(alertsWrapper.find('[data-test-subj="osqueryViewTab"]').exists()).toEqual(true); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 70d0c29eeda4b0..92381ec3846bcb 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -5,490 +5,7 @@ * 2.0. */ -import type { EuiTabbedContentTab } from '@elastic/eui'; -import { - EuiFlexGroup, - EuiHorizontalRule, - EuiSkeletonText, - EuiLoadingSpinner, - EuiNotificationBadge, - EuiSpacer, - EuiTabbedContent, - EuiTitle, -} from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash'; - -import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; -import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; -import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; -import type { RawEventData } from '../../../../common/types/response_actions'; -import { useResponseActionsView } from './response_actions_view'; -import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; -import type { SearchHit } from '../../../../common/search_strategy'; -import { getMitreComponentParts } from '../../../detections/mitre/get_mitre_threat_component'; -import { GuidedOnboardingTourStep } from '../guided_onboarding_tour/tour_step'; -import { isDetectionsAlertsTable } from '../top_n/helpers'; -import { - AlertsCasesTourSteps, - getTourAnchor, - SecurityStepId, -} from '../guided_onboarding_tour/tour_config'; -import { EventFieldsBrowser } from './event_fields_browser'; -import { JsonView } from './json_view'; -import { ThreatSummaryView } from './cti_details/threat_summary_view'; -import { ThreatDetailsView } from './cti_details/threat_details_view'; -import * as i18n from './translations'; -import { AlertSummaryView } from './alert_summary_view'; -import type { BrowserFields } from '../../containers/source'; -import { useInvestigationTimeEnrichment } from '../../containers/cti/event_enrichment'; -import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; -import type { TimelineTabs } from '../../../../common/types/timeline'; -import { - filterDuplicateEnrichments, - getEnrichmentFields, - parseExistingEnrichments, - timelineDataToEnrichment, -} from './cti_details/helpers'; -import { EnrichmentRangePicker } from './cti_details/enrichment_range_picker'; -import { InvestigationGuideView } from './investigation_guide_view'; -import { Overview } from './overview'; -import { Insights } from './insights/insights'; -import { useRiskScoreData } from '../../../entity_analytics/api/hooks/use_risk_score_data'; -import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer'; -import { DETAILS_CLASS_NAME } from '../../../timelines/components/timeline/body/renderers/helpers'; -import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; -import { useOsqueryTab } from './osquery_tab'; - -export const EVENT_DETAILS_CONTEXT_ID = 'event-details'; - -type EventViewTab = EuiTabbedContentTab; - -export type EventViewId = - | EventsViewType.tableView - | EventsViewType.jsonView - | EventsViewType.summaryView - | EventsViewType.threatIntelView - // Depending on endpointResponseActionsEnabled flag whether to render Osquery Tab or the commonTab (osquery + endpoint results) - | EventsViewType.osqueryView - | EventsViewType.responseActionsView; - export enum EventsViewType { - tableView = 'table-view', - jsonView = 'json-view', - summaryView = 'summary-view', - threatIntelView = 'threat-intel-view', osqueryView = 'osquery-results-view', responseActionsView = 'response-actions-results-view', } - -interface Props { - browserFields: BrowserFields; - data: TimelineEventsDetailsItem[]; - detailsEcsData: Ecs | null; - id: string; - isAlert: boolean; - isDraggable?: boolean; - rawEventData: object | undefined; - timelineTabType: TimelineTabs | 'flyout'; - scopeId: string; - handleOnEventClosed: () => void; - isReadOnly?: boolean; -} - -const StyledEuiTabbedContent = styled(EuiTabbedContent)` - display: flex; - flex: 1; - flex-direction: column; - overflow: hidden; - - > [role='tabpanel'] { - display: flex; - flex: 1; - flex-direction: column; - overflow: hidden; - overflow-y: auto; - - ::-webkit-scrollbar { - -webkit-appearance: none; - width: 7px; - } - - ::-webkit-scrollbar-thumb { - border-radius: 4px; - background-color: rgba(0, 0, 0, 0.5); - -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); - } - } -`; - -const TabContentWrapper = styled.div` - height: 100%; - position: relative; -`; - -const RendererContainer = styled.div` - overflow-x: auto; - padding-right: ${(props) => props.theme.eui.euiSizeXS}; - - & .${DETAILS_CLASS_NAME} .euiFlexGroup { - justify-content: flex-start; - } -`; - -const ThreatTacticContainer = styled(EuiFlexGroup)` - flex-grow: 0; - flex-wrap: nowrap; - - & .euiFlexGroup { - flex-wrap: nowrap; - } -`; - -const ThreatTacticDescription = styled.div` - padding-left: ${(props) => props.theme.eui.euiSizeL}; -`; - -const EventDetailsComponent: React.FC = ({ - browserFields, - data, - detailsEcsData, - id, - isAlert, - isDraggable, - rawEventData, - scopeId, - timelineTabType, - handleOnEventClosed, - isReadOnly, -}) => { - const [selectedTabId, setSelectedTabId] = useState(EventsViewType.summaryView); - const handleTabClick = useCallback( - (tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EventViewId), - [] - ); - const goToTableTab = useCallback(() => setSelectedTabId(EventsViewType.tableView), []); - - const eventFields = useMemo(() => getEnrichmentFields(data), [data]); - const basicAlertData = useBasicDataFromDetailsData(data); - const { rule: maybeRule } = useRuleWithFallback(basicAlertData.ruleId); - const existingEnrichments = useMemo( - () => - isAlert - ? parseExistingEnrichments(data).map((enrichmentData) => - timelineDataToEnrichment(enrichmentData) - ) - : [], - [data, isAlert] - ); - const { - result: enrichmentsResponse, - loading: isEnrichmentsLoading, - setRange, - range, - } = useInvestigationTimeEnrichment(eventFields); - - const threatDetails = useMemo( - () => getMitreComponentParts(rawEventData as SearchHit), - [rawEventData] - ); - const allEnrichments = useMemo(() => { - if (isEnrichmentsLoading || !enrichmentsResponse?.enrichments) { - return existingEnrichments; - } - return filterDuplicateEnrichments([...existingEnrichments, ...enrichmentsResponse.enrichments]); - }, [isEnrichmentsLoading, enrichmentsResponse, existingEnrichments]); - - const enrichmentCount = allEnrichments.length; - - const { hostRisk, userRisk, isAuthorized } = useRiskScoreData(data); - - const renderer = useMemo( - () => - detailsEcsData != null - ? getRowRenderer({ data: detailsEcsData, rowRenderers: defaultRowRenderers }) - : null, - [detailsEcsData] - ); - - const isTourAnchor = useMemo(() => isDetectionsAlertsTable(scopeId), [scopeId]); - - const showThreatSummary = useMemo(() => { - const hasEnrichments = enrichmentCount > 0; - const hasRiskInfoWithLicense = isAuthorized && (hostRisk || userRisk); - return hasEnrichments || hasRiskInfoWithLicense; - }, [enrichmentCount, hostRisk, isAuthorized, userRisk]); - const endpointResponseActionsEnabled = useIsExperimentalFeatureEnabled( - 'endpointResponseActionsEnabled' - ); - - const summaryTab: EventViewTab | undefined = useMemo( - () => - isAlert - ? { - id: EventsViewType.summaryView, - name: i18n.OVERVIEW, - 'data-test-subj': 'overviewTab', - content: ( - - <> - - - - {threatDetails && threatDetails[0] && ( - - <> - -
{threatDetails[0].title}
-
- - {threatDetails[0].description} - - -
- )} - - {renderer != null && detailsEcsData != null && ( -
- -
{i18n.ALERT_REASON}
-
- - - {renderer.renderRow({ - contextId: EVENT_DETAILS_CONTEXT_ID, - data: detailsEcsData, - isDraggable: isDraggable ?? false, - scopeId, - })} - -
- )} - - - - - - {showThreatSummary && ( - - )} - - {isEnrichmentsLoading && ( - <> - - - )} - - {basicAlertData.ruleId && maybeRule?.note && ( - - )} - -
- ), - } - : undefined, - [ - isAlert, - isTourAnchor, - browserFields, - scopeId, - data, - id, - handleOnEventClosed, - isReadOnly, - threatDetails, - renderer, - detailsEcsData, - isDraggable, - goToTableTab, - maybeRule?.investigation_fields?.field_names, - maybeRule?.note, - showThreatSummary, - hostRisk, - userRisk, - allEnrichments, - isEnrichmentsLoading, - basicAlertData, - ] - ); - - const threatIntelTab = useMemo( - () => - isAlert && !isReadOnly - ? { - id: EventsViewType.threatIntelView, - 'data-test-subj': 'threatIntelTab', - name: i18n.THREAT_INTEL, - append: ( - <> - {isEnrichmentsLoading ? ( - - ) : ( - - {enrichmentCount} - - )} - - ), - content: ( - } - loading={isEnrichmentsLoading} - enrichments={allEnrichments} - showInvestigationTimeEnrichments={!isEmpty(eventFields)} - > - <> - - - - - ), - } - : undefined, - [ - allEnrichments, - setRange, - range, - enrichmentCount, - isAlert, - eventFields, - isEnrichmentsLoading, - isReadOnly, - ] - ); - - const tableTab = useMemo( - () => ({ - id: EventsViewType.tableView, - 'data-test-subj': 'tableTab', - name: i18n.TABLE, - content: ( - <> - - - - ), - }), - [browserFields, data, id, isDraggable, scopeId, timelineTabType, isReadOnly] - ); - - const jsonTab = useMemo( - () => ({ - id: EventsViewType.jsonView, - 'data-test-subj': 'jsonViewTab', - name: i18n.JSON_VIEW, - content: ( - <> - - - - - - ), - }), - [rawEventData] - ); - const responseActionsTab = useResponseActionsView({ - rawEventData: rawEventData as RawEventData, - ...(detailsEcsData !== null ? { ecsData: detailsEcsData } : {}), - }); - const osqueryTab = useOsqueryTab({ - rawEventData: rawEventData as RawEventData, - ...(detailsEcsData !== null ? { ecsData: detailsEcsData } : {}), - }); - - const responseActionsTabs = useMemo(() => { - return endpointResponseActionsEnabled ? [responseActionsTab] : [osqueryTab]; - }, [endpointResponseActionsEnabled, osqueryTab, responseActionsTab]); - - const tabs = useMemo(() => { - return [summaryTab, threatIntelTab, tableTab, jsonTab, ...responseActionsTabs].filter( - (tab: EventViewTab | undefined): tab is EventViewTab => !!tab - ); - }, [summaryTab, threatIntelTab, tableTab, jsonTab, responseActionsTabs]); - - const selectedTab = useMemo( - () => tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0], - [tabs, selectedTabId] - ); - - const tourAnchor = useMemo( - () => (isTourAnchor ? { 'tour-step': getTourAnchor(3, SecurityStepId.alertsCases) } : {}), - [isTourAnchor] - ); - - return ( - <> - - - - ); -}; -EventDetailsComponent.displayName = 'EventDetailsComponent'; - -export const EventDetails = React.memo(EventDetailsComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index 796d96333f1b2c..0a8bef2fb88512 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -166,8 +166,8 @@ const useFieldBrowserPagination = () => { * This callback, invoked via `EuiInMemoryTable`'s `rowProps, assigns * attributes to every ``. */ - /** Renders a table view or JSON view of the `ECS` `data` */ +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 export const EventFieldsBrowser = React.memo( ({ browserFields, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/helpers.ts b/x-pack/plugins/security_solution/public/common/components/event_details/insights/helpers.ts deleted file mode 100644 index 8864e99bd1ed3a..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/helpers.ts +++ /dev/null @@ -1,21 +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 type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; - -type TimelineEventsDetailsItemWithValues = TimelineEventsDetailsItem & { - values: string[]; -}; - -/** - * Checks if the `item` has a non-empty `values` array - */ -export function hasData( - item?: TimelineEventsDetailsItem -): item is TimelineEventsDetailsItemWithValues { - return Boolean(item && item.values && item.values.length); -} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insight_accordion.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insight_accordion.test.tsx deleted file mode 100644 index bd324c582455f0..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insight_accordion.test.tsx +++ /dev/null @@ -1,58 +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 { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { TestProviders } from '../../../mock'; - -import { InsightAccordion } from './insight_accordion'; - -const noopRenderer = () => null; - -describe('InsightAccordion', () => { - it("shows a loading indicator when it's in the loading state", () => { - const loadingText = 'loading text'; - render( - - - - ); - - expect(screen.getByText(loadingText)).toBeInTheDocument(); - }); - - it("shows an error when it's in the error state", () => { - const errorText = 'error text'; - render( - - - - ); - - expect(screen.getByText(errorText)).toBeInTheDocument(); - }); - - it('shows the text and renders the correct content', () => { - const text = 'the text'; - const contentText = 'content text'; - const contentRenderer = () => {contentText}; - render( - - - - ); - - expect(screen.getByText(text)).toBeInTheDocument(); - expect(screen.getByText(contentText)).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insight_accordion.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insight_accordion.tsx deleted file mode 100644 index 5983f08bb522bc..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insight_accordion.tsx +++ /dev/null @@ -1,84 +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 type { ReactNode } from 'react'; -import React from 'react'; -import { noop } from 'lodash/fp'; -import type { EuiAccordionProps } from '@elastic/eui'; -import { EuiAccordion, EuiIcon, useGeneratedHtmlId } from '@elastic/eui'; -import { euiStyled } from '@kbn/kibana-react-plugin/common'; - -const StyledAccordion = euiStyled(EuiAccordion)` - border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - padding: 10px 8px; - border-radius: 6px; -`; - -export type InsightAccordionState = 'loading' | 'error' | 'success'; - -interface Props { - prefix: string; - state: InsightAccordionState; - text: string; - renderContent: () => ReactNode; - extraAction?: EuiAccordionProps['extraAction']; - onToggle?: EuiAccordionProps['onToggle']; - forceState?: EuiAccordionProps['forceState']; -} - -/** - * A special accordion that is used in the Insights section on the alert flyout. - * It wraps logic and custom styling around the loading, error and success states of an insight section. - */ -export const InsightAccordion = React.memo( - ({ prefix, state, text, renderContent, onToggle = noop, extraAction, forceState }) => { - const accordionId = useGeneratedHtmlId({ prefix }); - - switch (state) { - case 'loading': - // Don't render content when loading - return ( - - ); - case 'error': - // Display an alert icon and don't render content when there was an error - return ( - - - {text} - - } - onToggle={onToggle} - extraAction={extraAction} - /> - ); - case 'success': - // The accordion can display the content now - return ( - - {renderContent()} - - ); - default: - return null; - } - } -); - -InsightAccordion.displayName = 'InsightAccordion'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx deleted file mode 100644 index b4387fcf88b3cf..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.test.tsx +++ /dev/null @@ -1,177 +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 { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { TestProviders } from '../../../mock'; - -import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; -import { useKibana as mockUseKibana } from '../../../lib/kibana/__mocks__'; -import { licenseService } from '../../../hooks/use_license'; -import { noCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; -import { Insights } from './insights'; -import * as i18n from './translations'; - -const mockedUseKibana = mockUseKibana(); -const mockCanUseCases = jest.fn(); - -jest.mock('../../../lib/kibana', () => { - const original = jest.requireActual('../../../lib/kibana'); - - return { - ...original, - useToasts: jest.fn().mockReturnValue({ addWarning: jest.fn() }), - useKibana: () => ({ - ...mockedUseKibana, - services: { - ...mockedUseKibana.services, - cases: { - api: { - getRelatedCases: jest.fn(), - }, - helpers: { canUseCases: mockCanUseCases }, - }, - }, - }), - }; -}); - -jest.mock('../../../hooks/use_license', () => { - const licenseServiceInstance = { - isPlatinumPlus: jest.fn(), - isEnterprise: jest.fn(() => true), - }; - return { - licenseService: licenseServiceInstance, - useLicense: () => { - return licenseServiceInstance; - }, - }; -}); -const licenseServiceMock = licenseService as jest.Mocked; - -const dataWithoutAgentType: TimelineEventsDetailsItem[] = [ - { - category: 'process', - field: 'process.entity_id', - isObjectArray: false, - values: ['32082y34028u34'], - }, - { - category: 'kibana', - field: 'kibana.alert.ancestors.id', - isObjectArray: false, - values: ['woeurhw98rhwr'], - }, - { - category: 'kibana', - field: 'kibana.alert.rule.parameters.index', - isObjectArray: false, - values: ['fakeindex'], - }, -]; - -const data: TimelineEventsDetailsItem[] = [ - ...dataWithoutAgentType, - { - category: 'agent', - field: 'agent.type', - isObjectArray: false, - values: ['endpoint'], - }, -]; - -describe('Insights', () => { - beforeEach(() => { - mockCanUseCases.mockReturnValue(noCasesPermissions()); - }); - - it('does not render when there is no content to show', () => { - render( - - - - ); - - expect( - screen.queryByRole('heading', { - name: i18n.INSIGHTS, - }) - ).not.toBeInTheDocument(); - }); - - it('renders when there is at least one insight element to show', () => { - // One of the insights modules is the module showing related cases. - // It will show for all users that are able to read case data. - // Enabling that permission, will show the case insight module which - // is necessary to pass this test. - mockCanUseCases.mockReturnValue(readCasesPermissions()); - - render( - - - - ); - - expect( - screen.queryByRole('heading', { - name: i18n.INSIGHTS, - }) - ).toBeInTheDocument(); - }); - - describe('with feature flag enabled', () => { - describe('with platinum license', () => { - beforeAll(() => { - licenseServiceMock.isPlatinumPlus.mockReturnValue(true); - }); - - it('should show insights for related alerts by process ancestry', () => { - render( - - - - ); - - expect(screen.getByTestId('related-alerts-by-ancestry')).toBeInTheDocument(); - expect( - screen.queryByRole('button', { name: new RegExp(i18n.INSIGHTS_UPSELL) }) - ).not.toBeInTheDocument(); - }); - - describe('without process ancestry info', () => { - it('should not show the related alerts by process ancestry insights module', () => { - render( - - - - ); - - expect(screen.queryByTestId('related-alerts-by-ancestry')).not.toBeInTheDocument(); - }); - }); - }); - - describe('without platinum license', () => { - it('should show an upsell for related alerts by process ancestry', () => { - licenseServiceMock.isPlatinumPlus.mockReturnValue(false); - - render( - - - - ); - - expect( - screen.getByRole('button', { name: new RegExp(i18n.INSIGHTS_UPSELL) }) - ).toBeInTheDocument(); - expect(screen.queryByTestId('related-alerts-by-ancestry')).not.toBeInTheDocument(); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx deleted file mode 100644 index 5fbbdc9289b341..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/insights.tsx +++ /dev/null @@ -1,189 +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 React from 'react'; -import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle } from '@elastic/eui'; -import { euiStyled } from '@kbn/kibana-react-plugin/common'; -import { ALERT_SUPPRESSION_DOCS_COUNT } from '@kbn/rule-data-utils'; -import { find } from 'lodash/fp'; - -import { APP_ID } from '../../../../../common'; -import * as i18n from './translations'; - -import type { BrowserFields } from '../../../containers/source'; -import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; -import { hasData } from './helpers'; -import { useLicense } from '../../../hooks/use_license'; -import { RelatedAlertsByProcessAncestry } from './related_alerts_by_process_ancestry'; -import { RelatedCases } from './related_cases'; -import { RelatedAlertsBySourceEvent } from './related_alerts_by_source_event'; -import { RelatedAlertsBySession } from './related_alerts_by_session'; -import { RelatedAlertsUpsell } from './related_alerts_upsell'; -import { useKibana } from '../../../lib/kibana'; - -const StyledInsightItem = euiStyled(EuiFlexItem)` - border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - padding: 10px 8px; - border-radius: 6px; - display: inline-flex; -`; - -interface Props { - browserFields: BrowserFields; - eventId: string; - data: TimelineEventsDetailsItem[]; - scopeId: string; - isReadOnly?: boolean; -} - -/** - * Displays several key insights for the associated alert. - */ -export const Insights = React.memo( - ({ browserFields, eventId, data, isReadOnly, scopeId }) => { - const { cases } = useKibana().services; - const hasAtLeastPlatinum = useLicense().isPlatinumPlus(); - const originalDocumentId = find( - { category: 'kibana', field: 'kibana.alert.ancestors.id' }, - data - ); - const originalDocumentIndex = find( - { category: 'kibana', field: 'kibana.alert.rule.parameters.index' }, - data - ); - const agentTypeField = find({ category: 'agent', field: 'agent.type' }, data); - const eventModuleField = find({ category: 'event', field: 'event.module' }, data); - const processEntityField = find({ category: 'process', field: 'process.entity_id' }, data); - const hasProcessEntityInfo = - hasData(processEntityField) && - hasCorrectAgentTypeAndEventModule(agentTypeField, eventModuleField); - - const processSessionField = find( - { category: 'process', field: 'process.entry_leader.entity_id' }, - data - ); - const hasProcessSessionInfo = hasData(processSessionField); - - const sourceEventField = find( - { category: 'kibana', field: 'kibana.alert.original_event.id' }, - data - ); - const hasSourceEventInfo = hasData(sourceEventField); - - const alertSuppressionField = find( - { category: 'kibana', field: ALERT_SUPPRESSION_DOCS_COUNT }, - data - ); - const hasAlertSuppressionField = hasData(alertSuppressionField); - - const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); - const hasCasesReadPermissions = userCasesPermissions.read; - - // Make sure that the alert has at least one of the associated fields - // or the user has the required permissions for features/fields that - // we can provide insights for - const canShowAtLeastOneInsight = - hasCasesReadPermissions || - hasProcessEntityInfo || - hasSourceEventInfo || - hasProcessSessionInfo; - - const canShowAncestryInsight = - hasProcessEntityInfo && originalDocumentId && originalDocumentIndex; - - // If we're in read-only mode or don't have any insight-related data, - // don't render anything. - if (isReadOnly || !canShowAtLeastOneInsight) { - return null; - } - - return ( -
- - - -
{i18n.INSIGHTS}
-
-
- - {hasAlertSuppressionField && ( - -
- - {i18n.SUPPRESSED_ALERTS_COUNT(parseInt(alertSuppressionField.values[0], 10))} - -
-
- )} - - {hasCasesReadPermissions && ( - - - - )} - - {sourceEventField && sourceEventField.values && ( - - - - )} - - {processSessionField && processSessionField.values && ( - - - - )} - - {canShowAncestryInsight && - (hasAtLeastPlatinum ? ( - - - - ) : ( - - - - ))} -
-
- ); - } -); - -export function hasCorrectAgentTypeAndEventModule( - agentTypeField?: TimelineEventsDetailsItem, - eventModuleField?: TimelineEventsDetailsItem -): boolean { - return ( - hasData(agentTypeField) && - (agentTypeField.values[0] === 'endpoint' || - (agentTypeField.values[0] === 'winlogbeat' && - hasData(eventModuleField) && - eventModuleField.values[0] === 'sysmon')) - ); -} - -Insights.displayName = 'Insights'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.test.tsx deleted file mode 100644 index c74640c9cdc779..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.test.tsx +++ /dev/null @@ -1,166 +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 { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; - -import { TestProviders } from '../../../mock'; - -import { useAlertPrevalenceFromProcessTree } from '../../../containers/alerts/use_alert_prevalence_from_process_tree'; -import { RelatedAlertsByProcessAncestry } from './related_alerts_by_process_ancestry'; -import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; -import { - PROCESS_ANCESTRY, - PROCESS_ANCESTRY_COUNT, - PROCESS_ANCESTRY_ERROR, - PROCESS_ANCESTRY_EMPTY, -} from './translations'; -import type { StatsNode } from '../../../containers/alerts/use_alert_prevalence_from_process_tree'; - -jest.mock('../../../containers/alerts/use_alert_prevalence_from_process_tree', () => ({ - useAlertPrevalenceFromProcessTree: jest.fn(), -})); -const mockUseAlertPrevalenceFromProcessTree = useAlertPrevalenceFromProcessTree as jest.Mock; - -const props = { - eventId: 'random', - data: { - field: 'testfield', - values: ['test value'], - isObjectArray: false, - }, - index: { - field: 'index', - values: ['test value'], - isObjectArray: false, - }, - originalDocumentId: { - field: '_id', - values: ['original'], - isObjectArray: false, - }, - scopeId: 'table-test', - isActiveTimelines: false, -}; -describe('RelatedAlertsByProcessAncestry', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('shows an accordion and does not fetch data right away', () => { - render( - - - - ); - - expect(screen.getByText(PROCESS_ANCESTRY)).toBeInTheDocument(); - expect(mockUseAlertPrevalenceFromProcessTree).not.toHaveBeenCalled(); - }); - - it('shows a loading indicator and starts to fetch data when clicked', () => { - mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ - loading: true, - }); - - render( - - - - ); - - userEvent.click(screen.getByText(PROCESS_ANCESTRY)); - expect(mockUseAlertPrevalenceFromProcessTree).toHaveBeenCalled(); - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - }); - - it('shows an error message when the request fails', () => { - mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ - loading: false, - error: true, - }); - - render( - - - - ); - - userEvent.click(screen.getByText(PROCESS_ANCESTRY)); - expect(screen.getByText(PROCESS_ANCESTRY_ERROR)).toBeInTheDocument(); - }); - - it('renders the text with a count and a timeline button when the request works', async () => { - const mockAlertIds = ['1', '2']; - const mockStatsNodes = [ - { id: 'testid', name: 'process', parent: 'testid2' }, - { id: 'testid2', name: 'iexplore' }, - ]; - mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ - loading: false, - error: false, - alertIds: mockAlertIds, - statsNodes: mockStatsNodes, - }); - - render( - - - - ); - - userEvent.click(screen.getByText(PROCESS_ANCESTRY)); - await waitFor(() => { - expect(screen.getByText(PROCESS_ANCESTRY_COUNT(2))).toBeInTheDocument(); - - expect( - screen.getByRole('button', { name: ACTION_INVESTIGATE_IN_TIMELINE }) - ).toBeInTheDocument(); - }); - }); - - it('renders a special message when there are no alerts to display (empty response)', async () => { - mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ - loading: false, - error: false, - alertIds: [] as string[], - statsNodes: [] as StatsNode[], - }); - - render( - - - - ); - - userEvent.click(screen.getByText(PROCESS_ANCESTRY)); - await waitFor(() => { - expect(screen.getByText(PROCESS_ANCESTRY_EMPTY)).toBeInTheDocument(); - }); - }); - - it('renders a special message when there are no alerts to display (undefined case)', async () => { - mockUseAlertPrevalenceFromProcessTree.mockReturnValue({ - loading: false, - error: false, - alertIds: undefined, - statsNodes: undefined, - }); - - render( - - - - ); - - userEvent.click(screen.getByText(PROCESS_ANCESTRY)); - await waitFor(() => { - expect(screen.getByText(PROCESS_ANCESTRY_EMPTY)).toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.tsx deleted file mode 100644 index c57fb62c15a9b4..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.tsx +++ /dev/null @@ -1,235 +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 React, { useMemo, useCallback, useEffect, useState } from 'react'; -import { EuiSpacer, EuiLoadingSpinner } from '@elastic/eui'; - -import type { Filter } from '@kbn/es-query'; -import { isActiveTimeline } from '../../../../helpers'; -import type { DataProvider } from '../../../../../common/types'; -import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; -import { getDataProvider } from '../table/use_action_cell_data_provider'; -import { useAlertPrevalenceFromProcessTree } from '../../../containers/alerts/use_alert_prevalence_from_process_tree'; -import { InsightAccordion } from './insight_accordion'; -import { SimpleAlertTable } from './simple_alert_table'; -import { InvestigateInTimelineButton } from '../table/investigate_in_timeline_button'; -import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; -import { - PROCESS_ANCESTRY, - PROCESS_ANCESTRY_COUNT, - PROCESS_ANCESTRY_EMPTY, - PROCESS_ANCESTRY_ERROR, - PROCESS_ANCESTRY_FILTER, -} from './translations'; - -interface Props { - eventId: string; - index: TimelineEventsDetailsItem; - originalDocumentId: TimelineEventsDetailsItem; - scopeId?: string; -} - -interface Cache { - alertIds: string[]; -} - -const dataProviderLimit = 5; - -/** - * Fetches and displays alerts that were generated in the associated process' - * process tree. - * Offers the ability to dive deeper into the investigation by opening - * the related alerts in a timeline investigation. - * - * In contrast to other insight accordions, this one does not fetch the - * count and alerts on mount since the call to fetch the process tree - * and its associated alerts is quite expensive. - * The component requires users to click on the accordion in order to - * initiate the fetch of the associated events. - * - * In order to achieve this, this component orchestrates two helper - * components: - * - * RelatedAlertsByProcessAncestry (empty cache) - * user clicks --> - * FetchAndNotifyCachedAlertsByProcessAncestry (fetches data, shows loading state) - * cache loaded --> - * ActualRelatedAlertsByProcessAncestry (displays data) - * - * The top-level component maintains a "cache" state that is used for - * state management and to prevent double-fetching in case the - * accordion is closed and re-opened. - * - * Due to the ephemeral nature of the data, it was decided to keep the - * state inside the component rather than to add it to Redux. - */ -export const RelatedAlertsByProcessAncestry = React.memo( - ({ originalDocumentId, index, eventId, scopeId }) => { - const [showContent, setShowContent] = useState(false); - const [cache, setCache] = useState>({}); - - const onToggle = useCallback((isOpen: boolean) => setShowContent(isOpen), []); - - // Makes sure the component is not fetching data before the accordion - // has been openend. - const renderContent = useCallback(() => { - if (!showContent) { - return null; - } else if (cache.alertIds) { - return ( - - ); - } - return ( - - ); - }, [showContent, cache.alertIds, index, originalDocumentId, eventId, scopeId]); - - return ( - - ); - } -); - -RelatedAlertsByProcessAncestry.displayName = 'RelatedAlertsByProcessAncestry'; - -/** - * Fetches data, displays a loading and error state and notifies about on success - */ -const FetchAndNotifyCachedAlertsByProcessAncestry: React.FC<{ - eventId: string; - index: TimelineEventsDetailsItem; - originalDocumentId: TimelineEventsDetailsItem; - isActiveTimelines: boolean; - onCacheLoad: (cache: Cache) => void; -}> = ({ originalDocumentId, index, isActiveTimelines, onCacheLoad, eventId }) => { - const { values: indices } = index; - const { values: wrappedDocumentId } = originalDocumentId; - const documentId = Array.isArray(wrappedDocumentId) ? wrappedDocumentId[0] : ''; - const { loading, error, alertIds } = useAlertPrevalenceFromProcessTree({ - isActiveTimeline: isActiveTimelines, - documentId, - indices: indices ?? [], - }); - - useEffect(() => { - if (alertIds && alertIds.length !== 0) { - onCacheLoad({ alertIds }); - } - }, [alertIds, onCacheLoad]); - - if (loading) { - return ; - } else if (error) { - return <>{PROCESS_ANCESTRY_ERROR}; - } else if (!alertIds || alertIds.length === 0) { - return <>{PROCESS_ANCESTRY_EMPTY}; - } - - return null; -}; - -FetchAndNotifyCachedAlertsByProcessAncestry.displayName = - 'FetchAndNotifyCachedAlertsByProcessAncestry'; - -/** - * Renders the alert table and the timeline button from a filled cache. - */ -const ActualRelatedAlertsByProcessAncestry: React.FC<{ - alertIds: string[]; - eventId: string; - scopeId?: string; -}> = ({ alertIds, eventId, scopeId }) => { - const shouldUseFilters = alertIds && alertIds.length && alertIds.length >= dataProviderLimit; - const dataProviders = useMemo(() => { - if (alertIds && alertIds.length) { - if (shouldUseFilters) { - return null; - } else { - return alertIds.reduce((result, alertId, index) => { - const id = `${scopeId}-${eventId}-event.id-${index}-${alertId}`; - result.push(getDataProvider('_id', id, alertId)); - return result; - }, []); - } - } - return null; - }, [alertIds, shouldUseFilters, scopeId, eventId]); - - const filters: Filter[] | null = useMemo(() => { - if (shouldUseFilters) { - return [ - { - meta: { - alias: PROCESS_ANCESTRY_FILTER, - type: 'phrases', - key: '_id', - params: [...alertIds], - negate: false, - disabled: false, - value: alertIds.join(), - }, - query: { - bool: { - should: alertIds.map((id) => { - return { - match_phrase: { - _id: id, - }, - }; - }), - minimum_should_match: 1, - }, - }, - }, - ]; - } else { - return null; - } - }, [alertIds, shouldUseFilters]); - - if (!dataProviders && !filters) { - return null; - } - - return ( - <> - - - - {ACTION_INVESTIGATE_IN_TIMELINE} - - - ); -}; -ActualRelatedAlertsByProcessAncestry.displayName = 'ActualRelatedAlertsByProcessAncestry'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_session.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_session.test.tsx deleted file mode 100644 index c464cd831784fc..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_session.test.tsx +++ /dev/null @@ -1,122 +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 { render, screen, waitFor } from '@testing-library/react'; -import React from 'react'; - -import { TestProviders } from '../../../mock'; - -import { useActionCellDataProvider } from '../table/use_action_cell_data_provider'; -import { useAlertPrevalence } from '../../../containers/alerts/use_alert_prevalence'; -import { RelatedAlertsBySession } from './related_alerts_by_session'; -import { SESSION_LOADING, SESSION_ERROR, SESSION_COUNT } from './translations'; -import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; - -jest.mock('../table/use_action_cell_data_provider', () => ({ - useActionCellDataProvider: jest.fn(), -})); -const mockUseActionCellDataProvider = useActionCellDataProvider as jest.Mock; -jest.mock('../../../containers/alerts/use_alert_prevalence', () => ({ - useAlertPrevalence: jest.fn(), -})); -const mockUseAlertPrevalence = useAlertPrevalence as jest.Mock; - -const testEventId = '20398h209482'; -const testData = { - field: 'process.entry_leader.entity_id', - data: ['2938hr29348h9489r8'], - isObjectArray: false, -}; - -describe('RelatedAlertsBySession', () => { - it('shows a loading message when data is loading', () => { - mockUseAlertPrevalence.mockReturnValue({ - error: false, - count: undefined, - alertIds: undefined, - }); - render( - - - - ); - - expect(screen.getByText(SESSION_LOADING)).toBeInTheDocument(); - }); - - it('shows an error message when data failed to load', () => { - mockUseAlertPrevalence.mockReturnValue({ - error: true, - count: undefined, - alertIds: undefined, - }); - render( - - - - ); - - expect(screen.getByText(SESSION_ERROR)).toBeInTheDocument(); - }); - - it('shows an empty state when no alerts exist', () => { - mockUseAlertPrevalence.mockReturnValue({ - error: false, - count: 0, - alertIds: [], - }); - render( - - - - ); - - expect(screen.getByText(SESSION_COUNT(0))).toBeInTheDocument(); - }); - - it('shows the correct count and renders the timeline button', async () => { - mockUseAlertPrevalence.mockReturnValue({ - error: false, - count: 2, - alertIds: ['223', '2323'], - }); - mockUseActionCellDataProvider.mockReturnValue({ - dataProviders: [{}, {}], - }); - - render( - - - - ); - - await waitFor(() => { - expect(screen.getByText(SESSION_COUNT(2))).toBeInTheDocument(); - expect(screen.getByText(ACTION_INVESTIGATE_IN_TIMELINE)).toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_session.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_session.tsx deleted file mode 100644 index 87707620f0985f..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_session.tsx +++ /dev/null @@ -1,123 +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 React, { useCallback } from 'react'; -import { EuiSpacer } from '@elastic/eui'; - -import { isActiveTimeline } from '../../../../helpers'; -import type { BrowserFields } from '../../../containers/source'; -import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; -import { useActionCellDataProvider } from '../table/use_action_cell_data_provider'; -import { useAlertPrevalence } from '../../../containers/alerts/use_alert_prevalence'; -import type { InsightAccordionState } from './insight_accordion'; -import { InsightAccordion } from './insight_accordion'; -import { InvestigateInTimelineButton } from '../table/investigate_in_timeline_button'; -import { SimpleAlertTable } from './simple_alert_table'; -import { getEnrichedFieldInfo } from '../helpers'; -import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; -import { SESSION_LOADING, SESSION_EMPTY, SESSION_ERROR, SESSION_COUNT } from './translations'; -import { getFieldFormat } from '../get_field_format'; - -interface Props { - browserFields: BrowserFields; - data: TimelineEventsDetailsItem; - eventId: string; - scopeId: string; -} - -/** - * Fetches the count of alerts that were generated in the same session - * and displays an accordion with a mini table representation of the - * related cases. - * Offers the ability to dive deeper into the investigation by opening - * the related alerts in a timeline investigation. - */ -export const RelatedAlertsBySession = React.memo( - ({ browserFields, data, eventId, scopeId }) => { - const { field, values } = data; - const { error, count, alertIds } = useAlertPrevalence({ - field, - value: values, - isActiveTimelines: isActiveTimeline(scopeId), - signalIndexName: null, - includeAlertIds: true, - ignoreTimerange: true, - }); - - const { fieldFromBrowserField } = getEnrichedFieldInfo({ - browserFields, - contextId: scopeId, - eventId, - field: { id: data.field }, - scopeId, - item: data, - }); - - const cellData = useActionCellDataProvider({ - field, - values, - contextId: scopeId, - eventId, - fieldFromBrowserField, - fieldFormat: getFieldFormat(fieldFromBrowserField), - fieldType: fieldFromBrowserField?.type, - }); - - const isEmpty = count === 0; - - let state: InsightAccordionState = 'loading'; - if (error) { - state = 'error'; - } else if (alertIds || isEmpty) { - state = 'success'; - } - - const renderContent = useCallback(() => { - if (!alertIds || !cellData?.dataProviders) { - return null; - } else if (isEmpty && state !== 'loading') { - return SESSION_EMPTY; - } - return ( - <> - - - - {ACTION_INVESTIGATE_IN_TIMELINE} - - - ); - }, [alertIds, cellData?.dataProviders, isEmpty, state]); - - return ( - - ); - } -); - -RelatedAlertsBySession.displayName = 'RelatedAlertsBySession'; - -function getTextFromState(state: InsightAccordionState, count: number | undefined) { - switch (state) { - case 'loading': - return SESSION_LOADING; - case 'error': - return SESSION_ERROR; - case 'success': - return SESSION_COUNT(count); - default: - return ''; - } -} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_source_event.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_source_event.test.tsx deleted file mode 100644 index e5376abd3b3ed6..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_source_event.test.tsx +++ /dev/null @@ -1,122 +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 { render, screen, waitFor } from '@testing-library/react'; -import React from 'react'; - -import { TestProviders } from '../../../mock'; - -import { useActionCellDataProvider } from '../table/use_action_cell_data_provider'; -import { useAlertPrevalence } from '../../../containers/alerts/use_alert_prevalence'; -import { RelatedAlertsBySourceEvent } from './related_alerts_by_source_event'; -import { SOURCE_EVENT_LOADING, SOURCE_EVENT_ERROR, SOURCE_EVENT_COUNT } from './translations'; -import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; - -jest.mock('../table/use_action_cell_data_provider', () => ({ - useActionCellDataProvider: jest.fn(), -})); -const mockUseActionCellDataProvider = useActionCellDataProvider as jest.Mock; -jest.mock('../../../containers/alerts/use_alert_prevalence', () => ({ - useAlertPrevalence: jest.fn(), -})); -const mockUseAlertPrevalence = useAlertPrevalence as jest.Mock; - -const testEventId = '20398h209482'; -const testData = { - field: 'kibana.alert.original_event.id', - data: ['2938hr29348h9489r8'], - isObjectArray: false, -}; - -describe('RelatedAlertsBySourceEvent', () => { - it('shows a loading message when data is loading', () => { - mockUseAlertPrevalence.mockReturnValue({ - error: false, - count: undefined, - alertIds: undefined, - }); - render( - - - - ); - - expect(screen.getByText(SOURCE_EVENT_LOADING)).toBeInTheDocument(); - }); - - it('shows an error message when data failed to load', () => { - mockUseAlertPrevalence.mockReturnValue({ - error: true, - count: undefined, - alertIds: undefined, - }); - render( - - - - ); - - expect(screen.getByText(SOURCE_EVENT_ERROR)).toBeInTheDocument(); - }); - - it('shows an empty state when no alerts exist', () => { - mockUseAlertPrevalence.mockReturnValue({ - error: false, - count: 0, - alertIds: [], - }); - render( - - - - ); - - expect(screen.getByText(SOURCE_EVENT_COUNT(0))).toBeInTheDocument(); - }); - - it('shows the correct count and renders the timeline button', async () => { - mockUseAlertPrevalence.mockReturnValue({ - error: false, - count: 2, - alertIds: ['223', '2323'], - }); - mockUseActionCellDataProvider.mockReturnValue({ - dataProviders: [{}, {}], - }); - - render( - - - - ); - - await waitFor(() => { - expect(screen.getByText(SOURCE_EVENT_COUNT(2))).toBeInTheDocument(); - expect(screen.getByText(ACTION_INVESTIGATE_IN_TIMELINE)).toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_source_event.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_source_event.tsx deleted file mode 100644 index 399e1fa3a1afba..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_source_event.tsx +++ /dev/null @@ -1,127 +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 React, { useCallback } from 'react'; -import { EuiSpacer } from '@elastic/eui'; - -import { isActiveTimeline } from '../../../../helpers'; -import type { BrowserFields } from '../../../containers/source'; -import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; -import { useActionCellDataProvider } from '../table/use_action_cell_data_provider'; -import { useAlertPrevalence } from '../../../containers/alerts/use_alert_prevalence'; -import type { InsightAccordionState } from './insight_accordion'; -import { InsightAccordion } from './insight_accordion'; -import { InvestigateInTimelineButton } from '../table/investigate_in_timeline_button'; -import { SimpleAlertTable } from './simple_alert_table'; -import { getEnrichedFieldInfo } from '../helpers'; -import { ACTION_INVESTIGATE_IN_TIMELINE } from '../../../../detections/components/alerts_table/translations'; -import { - SOURCE_EVENT_LOADING, - SOURCE_EVENT_EMPTY, - SOURCE_EVENT_ERROR, - SOURCE_EVENT_COUNT, -} from './translations'; -import { getFieldFormat } from '../get_field_format'; - -interface Props { - browserFields: BrowserFields; - data: TimelineEventsDetailsItem; - eventId: string; - scopeId: string; -} - -/** - * Fetches the count of alerts that were generated by the same source - * event and displays an accordion with a mini table representation of - * the related cases. - * Offers the ability to dive deeper into the investigation by opening - * the related alerts in a timeline investigation. - */ -export const RelatedAlertsBySourceEvent = React.memo( - ({ browserFields, data, eventId, scopeId }) => { - const { field, values } = data; - const { error, count, alertIds } = useAlertPrevalence({ - field, - value: values, - isActiveTimelines: isActiveTimeline(scopeId), - signalIndexName: null, - includeAlertIds: true, - }); - - const { fieldFromBrowserField } = getEnrichedFieldInfo({ - browserFields, - contextId: scopeId, - eventId, - field: { id: data.field }, - scopeId, - item: data, - }); - - const cellData = useActionCellDataProvider({ - field, - values, - contextId: scopeId, - eventId, - fieldFromBrowserField, - fieldFormat: getFieldFormat(fieldFromBrowserField), - fieldType: fieldFromBrowserField?.type, - }); - - const isEmpty = count === 0; - - let state: InsightAccordionState = 'loading'; - if (error) { - state = 'error'; - } else if (alertIds) { - state = 'success'; - } - - const renderContent = useCallback(() => { - if (!alertIds || !cellData?.dataProviders) { - return null; - } else if (isEmpty && state !== 'loading') { - return SOURCE_EVENT_EMPTY; - } - return ( - <> - - - - {ACTION_INVESTIGATE_IN_TIMELINE} - - - ); - }, [alertIds, cellData?.dataProviders, isEmpty, state]); - - return ( - - ); - } -); - -function getTextFromState(state: InsightAccordionState, count: number | undefined) { - switch (state) { - case 'loading': - return SOURCE_EVENT_LOADING; - case 'error': - return SOURCE_EVENT_ERROR; - case 'success': - return SOURCE_EVENT_COUNT(count); - default: - return ''; - } -} - -RelatedAlertsBySourceEvent.displayName = 'RelatedAlertsBySourceEvent'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_upsell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_upsell.tsx deleted file mode 100644 index 10a9c872e39112..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_upsell.tsx +++ /dev/null @@ -1,51 +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 React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiText } from '@elastic/eui'; - -import { euiStyled } from '@kbn/kibana-react-plugin/common'; -import { INSIGHTS_UPSELL } from './translations'; -import { useKibana } from '../../../lib/kibana'; - -const UpsellContainer = euiStyled.div` - border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - padding: 12px; - border-radius: 6px; -`; - -const StyledIcon = euiStyled(EuiIcon)` - margin-right: 10px; -`; - -export const RelatedAlertsUpsell = React.memo(() => { - const { application } = useKibana().services; - return ( - - - - - - - - - {INSIGHTS_UPSELL} - - - - - - ); -}); - -RelatedAlertsUpsell.displayName = 'RelatedAlertsUpsell'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.test.tsx deleted file mode 100644 index 54c30fa38a5887..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.test.tsx +++ /dev/null @@ -1,148 +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 { act, render, screen } from '@testing-library/react'; -import React from 'react'; -import { casesPluginMock } from '@kbn/cases-plugin/public/mocks'; -import { TestProviders } from '../../../mock'; -import { useKibana as mockUseKibana } from '../../../lib/kibana/__mocks__'; -import { RelatedCases } from './related_cases'; -import { noCasesPermissions, readCasesPermissions } from '../../../../cases_test_utils'; -import { CASES_LOADING, CASES_COUNT } from './translations'; -import { useTourContext } from '../../guided_onboarding_tour'; -import { AlertsCasesTourSteps } from '../../guided_onboarding_tour/tour_config'; - -const mockedUseKibana = mockUseKibana(); - -const mockCasesContract = casesPluginMock.createStartContract(); -const mockGetRelatedCases = mockCasesContract.api.getRelatedCases as jest.Mock; -mockGetRelatedCases.mockReturnValue([{ id: '789', title: 'Test Case' }]); -const mockCanUseCases = mockCasesContract.helpers.canUseCases as jest.Mock; -mockCanUseCases.mockReturnValue(readCasesPermissions()); - -const mockUseTourContext = useTourContext as jest.Mock; - -jest.mock('../../../lib/kibana', () => { - const original = jest.requireActual('../../../lib/kibana'); - return { - ...original, - useToasts: jest.fn().mockReturnValue({ addWarning: jest.fn() }), - useKibana: () => ({ - ...mockedUseKibana, - services: { - ...mockedUseKibana.services, - cases: mockCasesContract, - }, - }), - }; -}); - -jest.mock('../../guided_onboarding_tour'); -const defaultUseTourContextValue = { - activeStep: AlertsCasesTourSteps.viewCase, - incrementStep: () => null, - endTourStep: () => null, - isTourShown: () => false, -}; - -jest.mock('../../guided_onboarding_tour/tour_step'); - -const eventId = '1c84d9bff4884dabe6aa1bb15f08433463b848d9269e587078dc56669550d27a'; -const scrollToMock = jest.fn(); -window.HTMLElement.prototype.scrollIntoView = scrollToMock; - -describe('Related Cases', () => { - beforeEach(() => { - mockUseTourContext.mockReturnValue(defaultUseTourContextValue); - jest.clearAllMocks(); - }); - - describe('When user does not have cases read permissions', () => { - beforeEach(() => { - mockCanUseCases.mockReturnValue(noCasesPermissions()); - }); - - test('should not show related cases when user does not have permissions', async () => { - await act(async () => { - render(, { wrapper: TestProviders }); - }); - expect(screen.queryByText('cases')).toBeNull(); - }); - }); - - describe('When user does have case read permissions', () => { - beforeEach(() => { - mockCanUseCases.mockReturnValue(readCasesPermissions()); - }); - - test('Should show the loading message', async () => { - mockGetRelatedCases.mockReturnValueOnce([]); - await act(async () => { - render(, { wrapper: TestProviders }); - expect(screen.queryByText(CASES_LOADING)).toBeInTheDocument(); - }); - expect(screen.queryByText(CASES_LOADING)).not.toBeInTheDocument(); - }); - - test('Should show 0 related cases when there are none', async () => { - mockGetRelatedCases.mockReturnValueOnce([]); - await act(async () => { - render(, { wrapper: TestProviders }); - }); - - expect(screen.getByText(CASES_COUNT(0))).toBeInTheDocument(); - }); - - test('Should show 1 related case', async () => { - await act(async () => { - render(, { wrapper: TestProviders }); - }); - expect(screen.getByText(CASES_COUNT(1))).toBeInTheDocument(); - expect(screen.getByTestId('case-details-link')).toHaveTextContent('Test Case'); - }); - - test('Should show 2 related cases', async () => { - mockGetRelatedCases.mockReturnValueOnce([ - { id: '789', title: 'Test Case 1' }, - { id: '456', title: 'Test Case 2' }, - ]); - await act(async () => { - render(, { wrapper: TestProviders }); - }); - expect(screen.getByText(CASES_COUNT(2))).toBeInTheDocument(); - const cases = screen.getAllByTestId('case-details-link'); - expect(cases).toHaveLength(2); - expect(cases[0]).toHaveTextContent('Test Case 1'); - expect(cases[1]).toHaveTextContent('Test Case 2'); - }); - - test('Should not open the related cases accordion when isTourActive=false', async () => { - await act(async () => { - render(, { wrapper: TestProviders }); - }); - expect(scrollToMock).not.toHaveBeenCalled(); - expect( - screen.getByTestId('RelatedCases-accordion').classList.contains('euiAccordion-isOpen') - ).toBe(false); - }); - - test('Should automatically open the related cases accordion when isTourActive=true', async () => { - // this hook is called twice, so we can not use mockReturnValueOnce - mockUseTourContext.mockReturnValue({ - ...defaultUseTourContextValue, - isTourShown: () => true, - }); - await act(async () => { - render(, { wrapper: TestProviders }); - }); - expect(scrollToMock).toHaveBeenCalled(); - expect( - screen.getByTestId('RelatedCases-accordion').classList.contains('euiAccordion-isOpen') - ).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.tsx deleted file mode 100644 index 8444e10d11cfd1..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_cases.tsx +++ /dev/null @@ -1,149 +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 React, { useCallback, useState, useEffect, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { AlertsCasesTourSteps, SecurityStepId } from '../../guided_onboarding_tour/tour_config'; -import { useTourContext } from '../../guided_onboarding_tour'; -import { useKibana, useToasts } from '../../../lib/kibana'; -import { CaseDetailsLink } from '../../links'; -import { APP_ID } from '../../../../../common/constants'; -import type { InsightAccordionState } from './insight_accordion'; -import { InsightAccordion } from './insight_accordion'; -import { CASES_LOADING, CASES_ERROR, CASES_ERROR_TOAST, CASES_COUNT } from './translations'; - -type RelatedCaseList = Array<{ id: string; title: string }>; - -interface Props { - eventId: string; -} - -/** - * Fetches and displays case links of cases that include the associated event (id). - */ -export const RelatedCases = React.memo(({ eventId }) => { - const { - services: { cases }, - } = useKibana(); - const toasts = useToasts(); - - const [relatedCases, setRelatedCases] = useState(undefined); - const [hasError, setHasError] = useState(false); - - const { activeStep, isTourShown } = useTourContext(); - const isTourActive = useMemo( - () => activeStep === AlertsCasesTourSteps.viewCase && isTourShown(SecurityStepId.alertsCases), - [activeStep, isTourShown] - ); - const renderContent = useCallback(() => renderCaseContent(relatedCases), [relatedCases]); - - const [shouldFetch, setShouldFetch] = useState(false); - - useEffect(() => { - if (!shouldFetch) { - return; - } - let ignore = false; - const fetch = async () => { - let relatedCaseList: RelatedCaseList = []; - try { - if (eventId) { - relatedCaseList = - (await cases.api.getRelatedCases(eventId, { - owner: APP_ID, - })) ?? []; - } - } catch (error) { - if (!ignore) { - setHasError(true); - } - toasts.addWarning(CASES_ERROR_TOAST(error)); - } - if (!ignore) { - setRelatedCases(relatedCaseList); - setShouldFetch(false); - } - }; - fetch(); - return () => { - ignore = true; - }; - }, [cases.api, eventId, shouldFetch, toasts]); - - useEffect(() => { - setShouldFetch(true); - }, [eventId]); - - let state: InsightAccordionState = 'loading'; - if (hasError) { - state = 'error'; - } else if (relatedCases) { - state = 'success'; - } - - return ( - - ); -}); - -function renderCaseContent(relatedCases: RelatedCaseList = []) { - const caseCount = relatedCases.length; - return ( - - - - - ), - }} - /> - {relatedCases.map(({ id, title }, index) => - id && title ? ( - - {' '} - - {title} - - {relatedCases[index + 1] ? ',' : ''} - - ) : ( - <> - ) - )} - - ); -} - -RelatedCases.displayName = 'RelatedCases'; - -function getTextFromState(state: InsightAccordionState, caseCount = 0) { - switch (state) { - case 'loading': - return CASES_LOADING; - case 'error': - return CASES_ERROR; - case 'success': - return CASES_COUNT(caseCount); - default: - return ''; - } -} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/simple_alert_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/simple_alert_table.test.tsx deleted file mode 100644 index 45a3e6b68a5c31..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/simple_alert_table.test.tsx +++ /dev/null @@ -1,111 +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 { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { TestProviders } from '../../../mock'; - -import { useAlertsByIds } from '../../../containers/alerts/use_alerts_by_ids'; -import { SimpleAlertTable } from './simple_alert_table'; - -jest.mock('../../../containers/alerts/use_alerts_by_ids', () => ({ - useAlertsByIds: jest.fn(), -})); -const mockUseAlertsByIds = useAlertsByIds as jest.Mock; - -const testIds = ['wer34r34', '234234']; -const tooManyTestIds = [ - '234', - '234', - '234', - '234', - '234', - '234', - '234', - '234', - '234', - '234', - '234', -]; -const testResponse = [ - { - fields: { - 'kibana.alert.rule.name': ['test rule name'], - '@timestamp': ['2022-07-18T15:07:21.753Z'], - 'kibana.alert.severity': ['high'], - }, - }, -]; - -describe('SimpleAlertTable', () => { - it('shows a loading indicator when the data is loading', () => { - mockUseAlertsByIds.mockReturnValue({ - loading: true, - error: false, - }); - render( - - - - ); - - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - }); - - it('shows an error message when there was an error fetching the alerts', () => { - mockUseAlertsByIds.mockReturnValue({ - loading: false, - error: true, - }); - render( - - - - ); - - expect(screen.getByText(/Failed/)).toBeInTheDocument(); - }); - - it('shows the results', () => { - mockUseAlertsByIds.mockReturnValue({ - loading: false, - error: false, - data: testResponse, - }); - render( - - - - ); - - // Renders to table headers - expect(screen.getByRole('columnheader', { name: 'Rule' })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: '@timestamp' })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: 'Severity' })).toBeInTheDocument(); - - // Renders the row - expect(screen.getByText('test rule name')).toBeInTheDocument(); - expect(screen.getByText(/Jul 18/)).toBeInTheDocument(); - expect(screen.getByText('High')).toBeInTheDocument(); - }); - - it('shows a note about limited results', () => { - mockUseAlertsByIds.mockReturnValue({ - loading: false, - error: false, - data: testResponse, - }); - render( - - - - ); - - expect(screen.getByText(/Showing only/)).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/simple_alert_table.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/simple_alert_table.tsx deleted file mode 100644 index c3d2c826021e06..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/simple_alert_table.tsx +++ /dev/null @@ -1,80 +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 React, { useMemo } from 'react'; -import type { EuiBasicTableColumn } from '@elastic/eui'; -import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; -import { EuiBasicTable, EuiSkeletonText, EuiSpacer } from '@elastic/eui'; - -import { PreferenceFormattedDate } from '../../formatted_date'; -import { SeverityBadge } from '../../severity_badge'; -import { useAlertsByIds } from '../../../containers/alerts/use_alerts_by_ids'; -import { SIMPLE_ALERT_TABLE_ERROR, SIMPLE_ALERT_TABLE_LIMITED } from './translations'; - -const TABLE_FIELDS = ['@timestamp', 'kibana.alert.rule.name', 'kibana.alert.severity']; - -const columns: Array>> = [ - { - field: 'kibana.alert.rule.name', - name: 'Rule', - }, - { - field: '@timestamp', - name: '@timestamp', - render: (timestamp: string) => , - }, - { - field: 'kibana.alert.severity', - name: 'Severity', - render: (severity: Severity) => , - }, -]; - -/** 10 alert rows in this table has been deemed a balanced amount for the flyout */ -const alertLimit = 10; - -/** - * Displays a simplified alert table for the given alert ids. - * It will only fetch the latest 10 ids and in case more ids - * are passed in, it will add a note about omitted alerts. - */ -export const SimpleAlertTable = React.memo<{ alertIds: string[] }>(({ alertIds }) => { - const sampledData = useMemo(() => alertIds.slice(0, alertLimit), [alertIds]); - - const { loading, error, data } = useAlertsByIds({ - alertIds: sampledData, - fields: TABLE_FIELDS, - }); - const mappedData = useMemo(() => { - if (!data) { - return undefined; - } - return data.map((doc) => doc.fields); - }, [data]); - - if (loading) { - return ; - } else if (error) { - return <>{SIMPLE_ALERT_TABLE_ERROR}; - } else if (mappedData) { - const showLimitedDataNote = alertIds.length > alertLimit; - return ( - <> - {showLimitedDataNote && ( -
- {SIMPLE_ALERT_TABLE_LIMITED} - -
- )} - - - ); - } - return null; -}); - -SimpleAlertTable.displayName = 'SimpleAlertTable'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts index 4b2056566ea797..6afd236caf3d6f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/translations.ts @@ -7,155 +7,7 @@ import { i18n } from '@kbn/i18n'; -export const INSIGHTS = i18n.translate('xpack.securitySolution.alertDetails.overview.insights', { - defaultMessage: 'Insights', -}); - -export const PROCESS_ANCESTRY = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_process_ancestry', - { - defaultMessage: 'Related alerts by process ancestry', - } -); - -export const PROCESS_ANCESTRY_COUNT = (count: number) => - i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_process_ancestry_count', - { - defaultMessage: '{count} {count, plural, =1 {alert} other {alerts}} by process ancestry', - values: { count }, - } - ); - -export const PROCESS_ANCESTRY_ERROR = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_process_ancestry_error', - { - defaultMessage: 'Failed to fetch alerts.', - } -); - -export const PROCESS_ANCESTRY_FILTER = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.processAncestryFilter', - { - defaultMessage: 'Process Ancestry Alert IDs', - } -); - -export const PROCESS_ANCESTRY_EMPTY = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_process_ancestry_empty', - { - defaultMessage: 'There are no related alerts by process ancestry.', - } -); - -export const SESSION_LOADING = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_source_event_loading', - { defaultMessage: 'Loading related alerts by source event' } -); - -export const SESSION_ERROR = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_session_error', - { - defaultMessage: 'Failed to load related alerts by session', - } -); - -export const SESSION_EMPTY = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_session_empty', - { - defaultMessage: 'There are no related alerts by session', - } -); - -export const SESSION_COUNT = (count?: number) => - i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_session_count', - { - defaultMessage: '{count} {count, plural, =1 {alert} other {alerts}} related by session', - values: { count }, - } - ); -export const SOURCE_EVENT_LOADING = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_source_event_loading', - { defaultMessage: 'Loading related alerts by source event' } -); - -export const SOURCE_EVENT_ERROR = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_source_event_error', - { - defaultMessage: 'Failed to load related alerts by source event', - } -); - -export const SOURCE_EVENT_EMPTY = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_alerts_by_source_event_empty', - { - defaultMessage: 'There are no related alerts by source event', - } -); - -export const SOURCE_EVENT_COUNT = (count?: number) => - i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights_related_alerts_by_source_event_count', - { - defaultMessage: '{count} {count, plural, =1 {alert} other {alerts}} related by source event', - values: { count }, - } - ); - -export const CASES_LOADING = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_cases_loading', - { - defaultMessage: 'Loading related cases', - } -); - -export const CASES_ERROR = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.related_cases_error', - { - defaultMessage: 'Failed to load related cases', - } -); - -export const CASES_COUNT = (count: number) => - i18n.translate('xpack.securitySolution.alertDetails.overview.insights.related_cases_count', { - defaultMessage: '{count} {count, plural, =1 {case} other {cases}} related to this alert', - values: { count }, - }); - -export const CASES_ERROR_TOAST = (error: string) => - i18n.translate('xpack.securitySolution.alertDetails.overview.insights.relatedCasesFailure', { - defaultMessage: 'Unable to load related cases: "{error}"', - values: { error }, - }); - -export const SIMPLE_ALERT_TABLE_ERROR = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.simpleAlertTable.error', - { - defaultMessage: 'Failed to load the alerts.', - } -); - -export const SIMPLE_ALERT_TABLE_LIMITED = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.limitedAlerts', - { - defaultMessage: 'Showing only the latest 10 alerts. View the rest of alerts in timeline.', - } -); - -export const INSIGHTS_UPSELL = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.insights.alertUpsellTitle', - { - defaultMessage: 'Get more insights with a platinum subscription', - } -); - -export const SUPPRESSED_ALERTS_COUNT = (count?: number) => - i18n.translate('xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCount', { - defaultMessage: '{count} suppressed {count, plural, =1 {alert} other {alerts}}', - values: { count }, - }); - +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 export const SUPPRESSED_ALERTS_COUNT_TECHNICAL_PREVIEW = i18n.translate( 'xpack.securitySolution.alertDetails.overview.insights.suppressedAlertsCountTechnicalPreview', { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx index cb3eb321bd3fab..29e2354f7454a6 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx @@ -43,6 +43,7 @@ interface InvestigationGuideViewProps { /** * Investigation guide that shows the markdown text of rule.note */ +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 const InvestigationGuideViewComponent: React.FC = ({ basicData, ruleNote, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx deleted file mode 100644 index b20270266602d8..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx +++ /dev/null @@ -1,22 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { rawEventData } from '../../mock'; - -import { JsonView } from './json_view'; - -describe('JSON View', () => { - describe('rendering', () => { - test('should match snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx deleted file mode 100644 index 0227d44f32305a..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx +++ /dev/null @@ -1,50 +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 { EuiCodeBlock } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import styled from 'styled-components'; - -import { omitTypenameAndEmpty } from '../../../timelines/components/timeline/body/helpers'; - -interface Props { - rawEventData: object | undefined; -} - -const EuiCodeEditorContainer = styled.div` - .euiCodeEditorWrapper { - position: absolute; - } -`; - -export const JsonView = React.memo(({ rawEventData }) => { - const value = useMemo( - () => - JSON.stringify( - rawEventData, - omitTypenameAndEmpty, - 2 // indent level - ), - [rawEventData] - ); - - return ( - - - {value} - - - ); -}); - -JsonView.displayName = 'JsonView'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx index 274c649ece9dcf..be2bcddfca3e61 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/osquery_tab.tsx @@ -27,6 +27,7 @@ const TabContentWrapper = styled.div` position: relative; `; +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 export const useOsqueryTab = ({ rawEventData, ecsData, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap deleted file mode 100644 index d22fb49ab6627b..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,189 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = ` - - .c3 { - text-transform: capitalize; -} - -.c4 { - margin-left: 8px; -} - -.c1.c1.c1 { - background-color: #25262e; - padding: 8px; - height: 78px; -} - -.c1:hover .inlineActions { - opacity: 1; - width: auto; - -webkit-transform: translate(0); - -ms-transform: translate(0); - transform: translate(0); -} - -.c1 .inlineActions { - opacity: 0; - width: 0; - -webkit-transform: translate(6px); - -ms-transform: translate(6px); - transform: translate(6px); - -webkit-transition: -webkit-transform 50ms ease-in-out; - -webkit-transition: transform 50ms ease-in-out; - transition: transform 50ms ease-in-out; -} - -.c1 .inlineActions.inlineActions-popoverOpen { - opacity: 1; - width: auto; - -webkit-transform: translate(0); - -ms-transform: translate(0); - transform: translate(0); -} - -.c2 { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.c0 { - -webkit-box-flex: 0; - -webkit-flex-grow: 0; - -ms-flex-positive: 0; - flex-grow: 0; -} - -
-
-
-
-
- Status -
-
-
-
-
- -
-
-
-
-
-
-
-
-
- Risk Score -
-
-
-
- 47 -
-
-
-
-
-
-
-
-
-
-
- Rule -
-
-
-
- -
-
-
-
-
-
-
- -`; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx deleted file mode 100644 index 0b9f78ac7a74fd..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.test.tsx +++ /dev/null @@ -1,230 +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 React from 'react'; -import { act, render } from '@testing-library/react'; -import { Overview } from '.'; -import { TestProviders } from '../../../mock'; - -jest.mock('../../../lib/kibana'); -jest.mock('../../utils', () => ({ - useThrottledResizeObserver: () => ({ width: 400 }), // force row-chunking -})); - -jest.mock( - '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges', - () => ({ - useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), - }) -); - -describe('Event Details Overview Cards', () => { - it('renders all cards', async () => { - await act(async () => { - const { getByText } = render( - - - - ); - - getByText('Status'); - getByText('Severity'); - getByText('Risk Score'); - getByText('Rule'); - }); - }); - - it('renders only readOnly cards', async () => { - await act(async () => { - const { getByText, queryByText } = render( - - - - ); - - getByText('Severity'); - getByText('Risk Score'); - - expect(queryByText('Status')).not.toBeInTheDocument(); - expect(queryByText('Rule')).not.toBeInTheDocument(); - }); - }); - - it('renders all cards it has data for', async () => { - await act(async () => { - const { getByText, queryByText } = render( - - - - ); - - getByText('Status'); - getByText('Risk Score'); - getByText('Rule'); - - expect(queryByText('Severity')).not.toBeInTheDocument(); - }); - }); - - it('renders rows and spacers correctly', async () => { - await act(async () => { - const { asFragment } = render( - - - - ); - - expect(asFragment()).toMatchSnapshot(); - }); - }); -}); - -const props = { - handleOnEventClosed: jest.fn(), - contextId: 'alerts-page', - eventId: 'testId', - indexName: 'testIndex', - scopeId: 'page', - data: [ - { - category: 'kibana', - field: 'kibana.alert.risk_score', - values: ['47'], - originalValue: ['47'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.uuid', - values: ['d9f537c0-47b2-11ec-9517-c1c68c44dec0'], - originalValue: ['d9f537c0-47b2-11ec-9517-c1c68c44dec0'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.workflow_status', - values: ['open'], - originalValue: ['open'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.rule.name', - values: ['More than one event with user name'], - originalValue: ['More than one event with user name'], - isObjectArray: false, - }, - { - category: 'kibana', - field: 'kibana.alert.severity', - values: ['medium'], - originalValue: ['medium'], - isObjectArray: false, - }, - ], - browserFields: { - kibana: { - fields: { - 'kibana.alert.severity': { - category: 'kibana', - count: 0, - name: 'kibana.alert.severity', - type: 'string', - esTypes: ['keyword'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - format: { id: 'string' }, - shortDotsEnable: false, - isMapped: true, - indexes: ['apm-*-transaction*'], - }, - 'kibana.alert.risk_score': { - category: 'kibana', - count: 0, - name: 'kibana.alert.risk_score', - type: 'number', - esTypes: ['float'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - format: { id: 'number' }, - shortDotsEnable: false, - isMapped: true, - indexes: ['apm-*-transaction*'], - }, - 'kibana.alert.rule.uuid': { - category: 'kibana', - count: 0, - name: 'kibana.alert.rule.uuid', - type: 'string', - esTypes: ['keyword'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - format: { id: 'string' }, - shortDotsEnable: false, - isMapped: true, - indexes: ['apm-*-transaction*'], - }, - 'kibana.alert.workflow_status': { - category: 'kibana', - count: 0, - name: 'kibana.alert.workflow_status', - type: 'string', - esTypes: ['keyword'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - format: { id: 'string' }, - shortDotsEnable: false, - isMapped: true, - indexes: ['apm-*-transaction*'], - }, - 'kibana.alert.rule.name': { - category: 'kibana', - count: 0, - name: 'kibana.alert.rule.name', - type: 'string', - esTypes: ['keyword'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - format: { id: 'string' }, - shortDotsEnable: false, - isMapped: true, - indexes: ['apm-*-transaction*'], - }, - }, - }, - }, -}; - -const dataWithoutSeverity = props.data.filter((data) => data.field !== 'kibana.alert.severity'); - -const fieldsWithoutSeverity = { - 'kibana.alert.risk_score': props.browserFields.kibana.fields['kibana.alert.risk_score'], - 'kibana.alert.rule.uuid': props.browserFields.kibana.fields['kibana.alert.rule.uuid'], - 'kibana.alert.workflow_status': props.browserFields.kibana.fields['kibana.alert.workflow_status'], - 'kibana.alert.rule.name': props.browserFields.kibana.fields['kibana.alert.rule.name'], -}; - -const propsWithoutSeverity = { - ...props, - browserFields: { kibana: { fields: fieldsWithoutSeverity } }, - data: dataWithoutSeverity, -}; - -const propsWithReadOnly = { - ...props, - isReadOnly: true, -}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx deleted file mode 100644 index de5b5be193c898..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/index.tsx +++ /dev/null @@ -1,226 +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 { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React, { useMemo, Fragment } from 'react'; -import { chunk, find } from 'lodash/fp'; -import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; - -import { euiStyled } from '@kbn/kibana-react-plugin/common'; -import type { BrowserFields } from '../../../containers/source'; -import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; -import type { EnrichedFieldInfo, EnrichedFieldInfoWithValues } from '../types'; -import { getEnrichedFieldInfo } from '../helpers'; -import { - ALERTS_HEADERS_RISK_SCORE, - ALERTS_HEADERS_RULE, - ALERTS_HEADERS_SEVERITY, - SIGNAL_STATUS, -} from '../../../../detections/components/alerts_table/translations'; -import { - SIGNAL_RULE_NAME_FIELD_NAME, - SIGNAL_STATUS_FIELD_NAME, -} from '../../../../timelines/components/timeline/body/renderers/constants'; -import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; -import { OverviewCardWithActions, OverviewCard } from './overview_card'; -import { StatusPopoverButton } from './status_popover_button'; -import { SeverityBadge } from '../../severity_badge'; -import { useThrottledResizeObserver } from '../../utils'; -import { getFieldFormat } from '../get_field_format'; - -export const NotGrowingFlexGroup = euiStyled(EuiFlexGroup)` - flex-grow: 0; -`; - -interface Props { - browserFields: BrowserFields; - contextId: string; - data: TimelineEventsDetailsItem[]; - eventId: string; - handleOnEventClosed: () => void; - scopeId: string; - isReadOnly?: boolean; -} - -export const Overview = React.memo( - ({ browserFields, contextId, data, eventId, handleOnEventClosed, scopeId, isReadOnly }) => { - const statusData = useMemo(() => { - const item = find({ field: SIGNAL_STATUS_FIELD_NAME, category: 'kibana' }, data); - return ( - item && - getEnrichedFieldInfo({ - eventId, - contextId, - scopeId, - browserFields, - item, - }) - ); - }, [browserFields, contextId, data, eventId, scopeId]); - - const severityData = useMemo(() => { - const item = find({ field: 'kibana.alert.severity', category: 'kibana' }, data); - return ( - item && - getEnrichedFieldInfo({ - eventId, - contextId, - scopeId, - browserFields, - item, - }) - ); - }, [browserFields, contextId, data, eventId, scopeId]); - - const riskScoreData = useMemo(() => { - const item = find({ field: 'kibana.alert.risk_score', category: 'kibana' }, data); - return ( - item && - getEnrichedFieldInfo({ - eventId, - contextId, - scopeId, - browserFields, - item, - }) - ); - }, [browserFields, contextId, data, eventId, scopeId]); - - const ruleNameData = useMemo(() => { - const item = find({ field: SIGNAL_RULE_NAME_FIELD_NAME, category: 'kibana' }, data); - const linkValueField = find({ field: 'kibana.alert.rule.uuid', category: 'kibana' }, data); - return ( - item && - getEnrichedFieldInfo({ - eventId, - contextId, - scopeId, - browserFields, - item, - linkValueField, - }) - ); - }, [browserFields, contextId, data, eventId, scopeId]); - - const signalCard = - hasData(statusData) && !isReadOnly ? ( - - - - - - ) : null; - - const severityCard = hasData(severityData) ? ( - - {!isReadOnly ? ( - - - - ) : ( - - - - )} - - ) : null; - - const riskScoreCard = hasData(riskScoreData) ? ( - - {!isReadOnly ? ( - - {riskScoreData.values[0]} - - ) : ( - {riskScoreData.values[0]} - )} - - ) : null; - - const ruleNameCard = - hasData(ruleNameData) && !isReadOnly ? ( - - - - - - ) : null; - - const { width, ref } = useThrottledResizeObserver(); - - // 675px is the container width at which none of the cards, when hovered, - // creates a visual overflow in a single row setup - const showAsSingleRow = width === 0 || (width && width >= 675); - - // Only render cards with content - const cards = [signalCard, severityCard, riskScoreCard, ruleNameCard].filter(isNotNull); - - // If there is enough space, render a single row. - // Otherwise, render two rows with each two cards. - const content = showAsSingleRow ? ( - {cards} - ) : ( - <> - {chunk(2, cards).map((elements, index, { length }) => { - // Add a spacer between rows but not after the last row - const addSpacer = index < length - 1; - return ( - - {elements} - {addSpacer && } - - ); - })} - - ); - - return
{content}
; - } -); - -function hasData(fieldInfo?: EnrichedFieldInfo): fieldInfo is EnrichedFieldInfoWithValues { - return !!fieldInfo && Array.isArray(fieldInfo.values); -} - -function isNotNull(value: T | null): value is T { - return value !== null; -} - -Overview.displayName = 'Overview'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx deleted file mode 100644 index c5f755ebcb9e41..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.test.tsx +++ /dev/null @@ -1,99 +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 React from 'react'; -import { act, render } from '@testing-library/react'; -import { OverviewCardWithActions } from './overview_card'; -import { createMockStore, mockGlobalState, TestProviders } from '../../../mock'; -import { SeverityBadge } from '../../severity_badge'; -import type { State } from '../../../store'; -import { TimelineId } from '../../../../../common/types'; -import { createAction } from '@kbn/ui-actions-plugin/public'; - -const state: State = { - ...mockGlobalState, - timeline: { - ...mockGlobalState.timeline, - timelineById: { - [TimelineId.casePage]: { - ...mockGlobalState.timeline.timelineById[TimelineId.test], - id: TimelineId.casePage, - }, - }, - }, -}; - -const store = createMockStore(state); - -const props = { - title: 'Severity', - contextId: 'timeline-case', - enrichedFieldInfo: { - contextId: 'timeline-case', - eventId: 'testid', - fieldType: 'string', - data: { - field: 'kibana.alert.rule.severity', - format: 'string', - type: 'string', - isObjectArray: false, - }, - values: ['medium'], - fieldFromBrowserField: { - category: 'kibana', - count: 0, - name: 'kibana.alert.rule.severity', - type: 'string', - esTypes: ['keyword'], - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - format: { id: 'string' }, - shortDotsEnable: false, - isMapped: true, - indexes: ['apm-*-transaction*'], - description: '', - example: '', - fields: {}, - }, - scopeId: 'timeline-case', - }, -}; - -jest.mock('../../../lib/kibana'); - -jest.mock('../../../hooks/use_get_field_spec'); - -const mockAction = createAction({ - id: 'test_action', - execute: async () => {}, - getIconType: () => 'test-icon', - getDisplayName: () => 'test-actions', -}); - -describe('OverviewCardWithActions', () => { - test('it renders correctly', async () => { - await act(async () => { - const { getByText, findByTestId } = render( - - - - - - ); - // Headline - getByText('Severity'); - - // Content - getByText('Medium'); - - // Hover actions - await findByTestId('actionItem-test_action'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx deleted file mode 100644 index ffebdf330a3f1c..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/overview_card.tsx +++ /dev/null @@ -1,112 +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 { EuiFlexGroup, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; -import type { FC, PropsWithChildren } from 'react'; -import React from 'react'; - -import { euiStyled } from '@kbn/kibana-react-plugin/common'; -import { - SecurityCellActions, - CellActionsMode, - SecurityCellActionsTrigger, -} from '../../cell_actions'; -import type { EnrichedFieldInfo } from '../types'; -import { getSourcererScopeId } from '../../../../helpers'; - -const ActionWrapper = euiStyled.div` - margin-left: ${({ theme }) => theme.eui.euiSizeS}; -`; - -const OverviewPanel = euiStyled(EuiPanel)` - &&& { - background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; - padding: ${({ theme }) => theme.eui.euiSizeS}; - height: 78px; - } - - &:hover { - .inlineActions { - opacity: 1; - width: auto; - transform: translate(0); - } - } - - .inlineActions { - opacity: 0; - width: 0; - transform: translate(6px); - transition: transform 50ms ease-in-out; - - &.inlineActions-popoverOpen { - opacity: 1; - width: auto; - transform: translate(0); - } - } -`; - -interface OverviewCardProps { - title: string; -} - -export const OverviewCard: FC> = ({ title, children }) => ( - - {title} - - {children} - -); - -OverviewCard.displayName = 'OverviewCard'; - -const ClampedContent = euiStyled.div` - /* Clamp text content to 2 lines */ - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -`; - -ClampedContent.displayName = 'ClampedContent'; - -type OverviewCardWithActionsProps = OverviewCardProps & { - contextId: string; - enrichedFieldInfo: EnrichedFieldInfo; - dataTestSubj?: string; -}; - -export const OverviewCardWithActions: FC> = ({ - title, - children, - contextId, - dataTestSubj, - enrichedFieldInfo, -}) => ( - - - {children} - - - - - - -); - -OverviewCardWithActions.displayName = 'OverviewCardWithActions'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx index 2dac89716bcf4a..f02b37672545af 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/overview/status_popover_button.tsx @@ -29,6 +29,7 @@ interface StatusPopoverButtonProps { handleOnEventClosed: () => void; } +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 export const StatusPopoverButton = React.memo( ({ eventId, contextId, enrichedFieldInfo, scopeId, handleOnEventClosed }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx index 58e18114f08f9a..4f6bbac5df4194 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/response_actions_view.tsx @@ -58,6 +58,7 @@ const EmptyResponseActions = () => { ); }; +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 export const useResponseActionsView = ({ rawEventData, ecsData, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx deleted file mode 100644 index 470e1df81ac278..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx +++ /dev/null @@ -1,122 +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 { render, screen } from '@testing-library/react'; -import React from 'react'; - -import type { BrowserField } from '../../containers/source'; -import { TestProviders } from '../../mock'; -import type { EventFieldsData } from './types'; -import { SummaryView } from './summary_view'; -import { TimelineId } from '../../../../common/types'; -import type { AlertSummaryRow } from './helpers'; - -jest.mock('../../lib/kibana'); - -const eventId = 'TUWyf3wBFCFU0qRJTauW'; -const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::']; -const hostIpFieldFromBrowserField: BrowserField = { - aggregatable: true, - name: 'host.ip', - readFromDocValues: false, - searchable: true, - type: 'ip', -}; -const hostIpData: EventFieldsData = { - ...hostIpFieldFromBrowserField, - ariaRowindex: 35, - field: 'host.ip', - isObjectArray: false, - originalValue: [...hostIpValues], - values: [...hostIpValues], -}; - -const enrichedHostIpData: AlertSummaryRow['description'] = { - data: { ...hostIpData }, - eventId, - fieldFromBrowserField: { ...hostIpFieldFromBrowserField }, - isDraggable: false, - scopeId: TimelineId.test, - values: [...hostIpValues], -}; - -const mockCount = 90019001; - -jest.mock('../../containers/alerts/use_alert_prevalence', () => ({ - useAlertPrevalence: () => ({ - loading: false, - count: mockCount, - error: false, - }), -})); - -describe('Summary View', () => { - describe('when no data is provided', () => { - test('should show an empty table', () => { - render( - - - - ); - expect(screen.getByText('No items found')).toBeInTheDocument(); - }); - }); - - describe('when data is provided', () => { - test('should show the data', () => { - const sampleRows: AlertSummaryRow[] = [ - { - title: hostIpData.field, - description: enrichedHostIpData, - }, - ]; - - render( - - - - ); - // Shows the field name - expect(screen.getByText(hostIpData.field)).toBeInTheDocument(); - // Shows all the field values - hostIpValues.forEach((ipValue) => { - expect(screen.getByText(ipValue)).toBeInTheDocument(); - }); - - // Shows alert prevalence information - expect(screen.getByText(mockCount)).toBeInTheDocument(); - // Shows the Investigate in timeline button - expect(screen.getByLabelText('Investigate in timeline')).toBeInTheDocument(); - }); - }); - - describe('when in readOnly mode', () => { - test('should only show the name and value cell', () => { - const sampleRows: AlertSummaryRow[] = [ - { - title: hostIpData.field, - description: enrichedHostIpData, - }, - ]; - - render( - - - - ); - - // Does not render the prevalence and timeline items - expect(screen.queryByText(mockCount)).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Investigate in timeline')).not.toBeInTheDocument(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx deleted file mode 100644 index d4ddc993a9fc62..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx +++ /dev/null @@ -1,103 +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 type { EuiBasicTableColumn } from '@elastic/eui'; -import { - EuiLink, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiText, - EuiIconTip, -} from '@elastic/eui'; -import React from 'react'; - -import type { AlertSummaryRow } from './helpers'; -import * as i18n from './translations'; -import { VIEW_ALL_FIELDS } from './translations'; -import { SummaryTable } from './table/summary_table'; -import { SummaryValueCell } from './table/summary_value_cell'; -import { PrevalenceCellRenderer } from './table/prevalence_cell'; - -const baseColumns: Array> = [ - { - field: 'title', - truncateText: false, - name: i18n.HIGHLIGHTED_FIELDS_FIELD, - textOnly: true, - }, - { - field: 'description', - truncateText: false, - render: SummaryValueCell, - name: i18n.HIGHLIGHTED_FIELDS_VALUE, - }, -]; - -const allColumns: Array> = [ - ...baseColumns, - { - field: 'description', - truncateText: true, - render: PrevalenceCellRenderer, - name: ( - <> - {i18n.HIGHLIGHTED_FIELDS_ALERT_PREVALENCE}{' '} - {i18n.HIGHLIGHTED_FIELDS_ALERT_PREVALENCE_TOOLTIP}} - /> - - ), - align: 'right', - width: '130px', - }, -]; - -const rowProps = { - // Class name for each row. On hover of a row, all actions for that row will be shown. - className: 'flyoutTableHoverActions', -}; - -const SummaryViewComponent: React.FC<{ - goToTable: () => void; - title: string; - rows: AlertSummaryRow[]; - isReadOnly?: boolean; -}> = ({ goToTable, rows, title, isReadOnly }) => { - const columns = isReadOnly ? baseColumns : allColumns; - - return ( -
- - - -
{title}
-
-
- - - {VIEW_ALL_FIELDS} - - -
- - -
- ); -}; - -export const SummaryView = React.memo(SummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_table.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_table.tsx deleted file mode 100644 index 8f80e640756573..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/summary_table.tsx +++ /dev/null @@ -1,28 +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 type { AnyStyledComponent } from 'styled-components'; -import styled from 'styled-components'; -import { EuiInMemoryTable } from '@elastic/eui'; - -export const SummaryTable = styled(EuiInMemoryTable as unknown as AnyStyledComponent)` - .inlineActions { - opacity: 0; - } - - .flyoutTableHoverActions { - .inlineActions-popoverOpen { - opacity: 1; - } - - &:hover { - .inlineActions { - opacity: 1; - } - } - } -`; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index a4c8f894cc4302..7ebeee21fd1edb 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -7,10 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const THREAT_INTEL = i18n.translate('xpack.securitySolution.alertDetails.threatIntel', { - defaultMessage: 'Threat Intel', -}); - export const INVESTIGATION_GUIDE = i18n.translate( 'xpack.securitySolution.alertDetails.overview.investigationGuide', { @@ -18,54 +14,10 @@ export const INVESTIGATION_GUIDE = i18n.translate( } ); -export const OVERVIEW = i18n.translate('xpack.securitySolution.alertDetails.overview', { - defaultMessage: 'Overview', -}); - -export const HIGHLIGHTED_FIELDS = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.highlightedFields', - { - defaultMessage: 'Highlighted fields', - } -); - -export const HIGHLIGHTED_FIELDS_FIELD = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.highlightedFields.field', - { - defaultMessage: 'Field', - } -); - -export const HIGHLIGHTED_FIELDS_VALUE = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.highlightedFields.value', - { - defaultMessage: 'Value', - } -); - -export const HIGHLIGHTED_FIELDS_ALERT_PREVALENCE = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.highlightedFields.alertPrevalence', - { - defaultMessage: 'Alert prevalence', - } -); - -export const HIGHLIGHTED_FIELDS_ALERT_PREVALENCE_TOOLTIP = i18n.translate( - 'xpack.securitySolution.alertDetails.overview.highlightedFields.alertPrevalenceTooltip', - { - defaultMessage: - 'The total count of alerts with the same value within the currently selected timerange. This value is not affected by additional filters.', - } -); - export const TABLE = i18n.translate('xpack.securitySolution.eventDetails.table', { defaultMessage: 'Table', }); -export const JSON_VIEW = i18n.translate('xpack.securitySolution.eventDetails.jsonView', { - defaultMessage: 'JSON', -}); - export const OSQUERY_VIEW = i18n.translate('xpack.securitySolution.eventDetails.osqueryView', { defaultMessage: 'Osquery Results', }); @@ -96,19 +48,6 @@ export const PLACEHOLDER = i18n.translate( } ); -export const VIEW_COLUMN = (field: string) => - i18n.translate('xpack.securitySolution.eventDetails.viewColumnCheckboxAriaLabel', { - values: { field }, - defaultMessage: 'View {field} column', - }); - -export const NESTED_COLUMN = (field: string) => - i18n.translate('xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel', { - values: { field }, - defaultMessage: - 'The {field} field is an object, and is broken down into nested fields which can be added as column', - }); - export const AGENT_STATUS = i18n.translate('xpack.securitySolution.detections.alerts.agentStatus', { defaultMessage: 'Agent status', }); @@ -146,10 +85,6 @@ export const ALERT_REASON = i18n.translate('xpack.securitySolution.eventDetails. defaultMessage: 'Alert reason', }); -export const VIEW_ALL_FIELDS = i18n.translate('xpack.securitySolution.eventDetails.viewAllFields', { - defaultMessage: 'View all fields in table', -}); - export const ENDPOINT_COMMANDS = Object.freeze({ tried: (command: string) => i18n.translate('xpack.securitySolution.eventDetails.responseActions.endpoint.tried', { @@ -177,10 +112,6 @@ export const SUMMARY_VIEW = i18n.translate('xpack.securitySolution.eventDetails. defaultMessage: 'summary', }); -export const TIMELINE_VIEW = i18n.translate('xpack.securitySolution.eventDetails.timelineView', { - defaultMessage: 'Timeline', -}); - export const ALERT_SUMMARY_CONVERSATION_ID = i18n.translate( 'xpack.securitySolution.alertSummaryView.alertSummaryViewConversationId', { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index e8401d28c67fb5..80d598679930fb 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -219,8 +219,7 @@ const StatefulEventsViewerComponent: React.FC - {DetailsPanel} ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/selectors/mock_state.ts b/x-pack/plugins/security_solution/public/common/components/events_viewer/selectors/mock_state.ts index 32510c2d342d00..f616a9b48f61bd 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/selectors/mock_state.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/selectors/mock_state.ts @@ -188,7 +188,6 @@ const timeline = { 'threat_match', 'zeek', ], - expandedDetail: {}, filters: [], kqlQuery: { filterQuery: null }, indexNames: ['.alerts-security.alerts-default'], diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts index d470e4e85f1cd7..cc3ff5507ec409 100644 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts +++ b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts @@ -33,6 +33,7 @@ interface UserAlertPrevalenceResult { alertIds?: string[]; } +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 export const useAlertPrevalence = ({ field, value, diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alerts_by_ids.test.tsx b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alerts_by_ids.test.tsx deleted file mode 100644 index 1a88afcb5542ca..00000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alerts_by_ids.test.tsx +++ /dev/null @@ -1,102 +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 { renderHook } from '@testing-library/react-hooks'; - -import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; -import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants'; -import { useAlertsByIds } from './use_alerts_by_ids'; - -jest.mock('../../../detections/containers/detection_engine/alerts/use_query', () => ({ - useQueryAlerts: jest.fn(), -})); -const mockUseQueryAlerts = useQueryAlerts as jest.Mock; - -const alertIds = ['1', '2', '3']; -const testResult = { - hits: { - hits: [{ result: 1 }, { result: 2 }], - }, -}; - -describe('useAlertsByIds', () => { - beforeEach(() => { - mockUseQueryAlerts.mockReset(); - }); - - it('passes down the loading state', () => { - mockUseQueryAlerts.mockReturnValue({ - loading: true, - setQuery: jest.fn(), - }); - - const { result } = renderHook(() => useAlertsByIds({ alertIds })); - - expect(result.current).toEqual({ loading: true, error: false }); - }); - - it('calculates the error state', () => { - mockUseQueryAlerts.mockReturnValue({ - loading: false, - data: undefined, - setQuery: jest.fn(), - }); - - const { result } = renderHook(() => useAlertsByIds({ alertIds })); - - expect(result.current).toEqual({ loading: false, error: true, data: undefined }); - }); - - it('returns the results', () => { - mockUseQueryAlerts.mockReturnValue({ - loading: false, - data: testResult, - setQuery: jest.fn(), - }); - - const { result } = renderHook(() => useAlertsByIds({ alertIds })); - - expect(result.current).toEqual({ loading: false, error: false, data: testResult.hits.hits }); - }); - - it('constructs the correct query', () => { - mockUseQueryAlerts.mockReturnValue({ - loading: true, - setQuery: jest.fn(), - }); - - renderHook(() => useAlertsByIds({ alertIds })); - - expect(mockUseQueryAlerts).toHaveBeenCalledWith({ - queryName: ALERTS_QUERY_NAMES.BY_ID, - query: expect.objectContaining({ - fields: ['*'], - _source: false, - query: { - ids: { - values: alertIds, - }, - }, - }), - }); - }); - - it('requests the specified fields', () => { - const testFields = ['test.*']; - mockUseQueryAlerts.mockReturnValue({ - loading: true, - setQuery: jest.fn(), - }); - - renderHook(() => useAlertsByIds({ alertIds, fields: testFields })); - - expect(mockUseQueryAlerts).toHaveBeenCalledWith({ - queryName: ALERTS_QUERY_NAMES.BY_ID, - query: expect.objectContaining({ fields: testFields }), - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alerts_by_ids.ts b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alerts_by_ids.ts deleted file mode 100644 index 1ac9948818bccd..00000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alerts_by_ids.ts +++ /dev/null @@ -1,72 +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 { useEffect, useState } from 'react'; - -import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; -import { ALERTS_QUERY_NAMES } from '../../../detections/containers/detection_engine/alerts/constants'; - -interface UseAlertByIdsOptions { - alertIds: string[]; - fields?: string[]; -} - -interface Hit { - fields: Record; - _index: string; - _id: string; -} - -interface UserAlertByIdsResult { - loading: boolean; - error: boolean; - data?: Hit[]; -} - -// It prevents recreating the array on every hook call -const ALL_FIELD = ['*']; - -/** - * Fetches the alert documents associated to the ids that are passed. - * By default it fetches all fields but they can be limited by passing - * the `fields` parameter. - */ -export const useAlertsByIds = ({ - alertIds, - fields = ALL_FIELD, -}: UseAlertByIdsOptions): UserAlertByIdsResult => { - const [initialQuery] = useState(() => generateAlertByIdsQuery(alertIds, fields)); - - const { loading, data, setQuery } = useQueryAlerts({ - query: initialQuery, - queryName: ALERTS_QUERY_NAMES.BY_ID, - }); - - useEffect(() => { - setQuery(generateAlertByIdsQuery(alertIds, fields)); - }, [setQuery, alertIds, fields]); - - const error = !loading && data === undefined; - - return { - loading, - error, - data: data?.hits.hits, - }; -}; - -const generateAlertByIdsQuery = (alertIds: string[], fields: string[]) => { - return { - fields, - _source: false, - query: { - ids: { - values: alertIds, - }, - }, - }; -}; diff --git a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts index 915813bd702161..c8a91c41cb8470 100644 --- a/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts +++ b/x-pack/plugins/security_solution/public/common/containers/cti/event_enrichment/use_investigation_enrichment.ts @@ -27,6 +27,7 @@ export const QUERY_ID = 'investigation_time_enrichment'; const noop = () => {}; const noEnrichments = { enrichments: [] }; +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 export const useInvestigationTimeEnrichment = (eventFields: EventFields) => { const { addError } = useAppToasts(); const { data, uiSettings } = useKibana().services; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts b/x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts index 4190010301a4e0..12f6c5fbd0cbb3 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts @@ -96,6 +96,7 @@ const getFieldsValue = ( export type GetFieldsDataValue = string | string[] | null | undefined; export type GetFieldsData = (field: string) => GetFieldsDataValue; +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 export const useGetFieldsData = (fieldsData: SearchHit['fields'] | undefined): GetFieldsData => { // TODO: Move cache to top level container such as redux or context. Make it store type agnostic if possible // TODO: Handle updates where data is re-requested and the cache is reset. diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index cacbbd243be7d9..5a073684ef23dd 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -356,7 +356,6 @@ export const mockGlobalState: State = { }, eventIdToNoteIds: { '1': ['1'] }, excludedRowRendererIds: [], - expandedDetail: {}, highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, @@ -410,7 +409,6 @@ export const mockGlobalState: State = { defaultColumns: defaultHeaders, dataViewId: 'security-solution-default', deletedEventIds: [], - expandedDetail: {}, filters: [], indexNames: ['.alerts-security.alerts-default'], isSelectAllChecked: false, diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 8c823e2e836061..2125c234765cba 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -1871,7 +1871,6 @@ export const mockTimelineModel: TimelineModel = { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], - expandedDetail: {}, filters: [ { $state: { @@ -1938,7 +1937,6 @@ export const mockDataTableModel: DataTableModel = { defaultColumns: mockTimelineModelColumns, dataViewId: null, deletedEventIds: [], - expandedDetail: {}, filters: [ { $state: { @@ -2072,7 +2070,6 @@ export const defaultTimelineProps: CreateTimelineProps = { RowRendererId.threat_match, RowRendererId.zeek, ], - expandedDetail: {}, filters: [], highlightedDropAndProviderId: '', historyIds: [], diff --git a/x-pack/plugins/security_solution/public/common/store/data_table/middleware_local_storage.ts b/x-pack/plugins/security_solution/public/common/store/data_table/middleware_local_storage.ts index 0d8b63f8c283a4..a24992997649ff 100644 --- a/x-pack/plugins/security_solution/public/common/store/data_table/middleware_local_storage.ts +++ b/x-pack/plugins/security_solution/public/common/store/data_table/middleware_local_storage.ts @@ -20,7 +20,6 @@ const { applyDeltaToColumnWidth, changeViewMode, removeColumn, - toggleDetailPanel, updateColumnOrder, updateColumns, updateColumnWidth, @@ -44,7 +43,6 @@ const tableActionTypes = new Set([ updateShowBuildingBlockAlertsFilter.type, updateTotalCount.type, updateIsLoading.type, - toggleDetailPanel.type, ]); export const dataTableLocalStorageMiddleware: (storage: Storage) => Middleware<{}, State> = diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.tsx index 2469d519ad1973..9d2f3f521f6c1a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_histogram.tsx @@ -24,8 +24,6 @@ import { HeaderSection } from '../../../../common/components/header_section'; import { getAlertsPreviewDefaultModel } from '../../../../detections/components/alerts_table/default_config'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; -import { useSourcererDataView } from '../../../../sourcerer/containers'; -import { DetailsPanel } from '../../../../timelines/components/side_panel'; import { PreviewRenderCellValue } from './preview_table_cell_renderer'; import { getPreviewTableControlColumn } from './preview_table_control_columns'; import { useGlobalFullScreen } from '../../../../common/containers/use_full_screen'; @@ -95,7 +93,6 @@ const PreviewHistogramComponent = ({ ); const license = useLicense(); - const { browserFields, runtimeMappings } = useSourcererDataView(SourcererScopeName.detections); const { globalFullScreen } = useGlobalFullScreen(); const previousPreviewId = usePrevious(previewId); @@ -202,13 +199,6 @@ const PreviewHistogramComponent = ({ bulkActions={false} /> - ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_experimental_feature_fields_transform.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_experimental_feature_fields_transform.ts index 1bca12d4611110..c035fef5af6e44 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_experimental_feature_fields_transform.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/use_experimental_feature_fields_transform.ts @@ -7,8 +7,6 @@ import { useCallback } from 'react'; import type { DefineStepRule } from '../../../../detections/pages/detection_engine/rules/types'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; -import { isEsqlRule, isMlRule } from '../../../../../common/detection_engine/utils'; /** * transforms DefineStepRule fields according to experimental feature flags @@ -16,34 +14,9 @@ import { isEsqlRule, isMlRule } from '../../../../../common/detection_engine/uti export const useExperimentalFeatureFieldsTransform = >(): (( fields: T ) => T) => { - const isAlertSuppressionForMachineLearningRuleEnabled = useIsExperimentalFeatureEnabled( - 'alertSuppressionForMachineLearningRuleEnabled' - ); - const isAlertSuppressionForEsqlRuleEnabled = useIsExperimentalFeatureEnabled( - 'alertSuppressionForEsqlRuleEnabled' - ); - - const transformer = useCallback( - (fields: T) => { - const isSuppressionDisabled = - (isMlRule(fields.ruleType) && !isAlertSuppressionForMachineLearningRuleEnabled) || - (isEsqlRule(fields.ruleType) && !isAlertSuppressionForEsqlRuleEnabled); - - // reset any alert suppression values hidden behind feature flag - if (isSuppressionDisabled) { - return { - ...fields, - groupByFields: [], - groupByRadioSelection: undefined, - groupByDuration: undefined, - suppressionMissingFields: undefined, - }; - } - - return fields; - }, - [isAlertSuppressionForEsqlRuleEnabled, isAlertSuppressionForMachineLearningRuleEnabled] - ); + const transformer = useCallback((fields: T) => { + return fields; + }, []); return transformer; }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_alert_suppression.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_alert_suppression.test.tsx index fb00b73e88ffd1..949c957bf83c18 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_alert_suppression.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_alert_suppression.test.tsx @@ -6,25 +6,27 @@ */ import { renderHook } from '@testing-library/react-hooks'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; -import * as useIsExperimentalFeatureEnabledMock from '../../../common/hooks/use_experimental_features'; import { useAlertSuppression } from './use_alert_suppression'; describe('useAlertSuppression', () => { - beforeEach(() => { - jest - .spyOn(useIsExperimentalFeatureEnabledMock, 'useIsExperimentalFeatureEnabled') - .mockReturnValue(false); - }); - - (['new_terms', 'threat_match', 'saved_query', 'query', 'threshold', 'eql'] as Type[]).forEach( - (ruleType) => { - it(`should return the isSuppressionEnabled true for ${ruleType} rule type that exists in SUPPRESSIBLE_ALERT_RULES`, () => { - const { result } = renderHook(() => useAlertSuppression(ruleType)); + ( + [ + 'new_terms', + 'threat_match', + 'saved_query', + 'query', + 'threshold', + 'eql', + 'esql', + 'machine_learning', + ] as Type[] + ).forEach((ruleType) => { + it(`should return the isSuppressionEnabled true for ${ruleType} rule type that exists in SUPPRESSIBLE_ALERT_RULES`, () => { + const { result } = renderHook(() => useAlertSuppression(ruleType)); - expect(result.current.isSuppressionEnabled).toBe(true); - }); - } - ); + expect(result.current.isSuppressionEnabled).toBe(true); + }); + }); it('should return false if rule type is undefined', () => { const { result } = renderHook(() => useAlertSuppression(undefined)); @@ -36,39 +38,4 @@ describe('useAlertSuppression', () => { expect(result.current.isSuppressionEnabled).toBe(false); }); - - describe('ML rules', () => { - it('is true if the feature flag is enabled', () => { - jest - .spyOn(useIsExperimentalFeatureEnabledMock, 'useIsExperimentalFeatureEnabled') - .mockReset() - .mockReturnValue(true); - const { result } = renderHook(() => useAlertSuppression('machine_learning')); - - expect(result.current.isSuppressionEnabled).toBe(true); - }); - - it('is false if the feature flag is disabled', () => { - const { result } = renderHook(() => useAlertSuppression('machine_learning')); - - expect(result.current.isSuppressionEnabled).toBe(false); - }); - }); - - describe('ES|QL rules', () => { - it('should return isSuppressionEnabled false if ES|QL Feature Flag is disabled', () => { - const { result } = renderHook(() => useAlertSuppression('esql')); - - expect(result.current.isSuppressionEnabled).toBe(false); - }); - - it('should return isSuppressionEnabled true if ES|QL Feature Flag is enabled', () => { - jest - .spyOn(useIsExperimentalFeatureEnabledMock, 'useIsExperimentalFeatureEnabled') - .mockImplementation((flag) => flag === 'alertSuppressionForEsqlRuleEnabled'); - const { result } = renderHook(() => useAlertSuppression('esql')); - - expect(result.current.isSuppressionEnabled).toBe(true); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_alert_suppression.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_alert_suppression.tsx index 6d0ecefe8345de..6e1b2a4d6163f5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_alert_suppression.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/use_alert_suppression.tsx @@ -6,40 +6,20 @@ */ import { useCallback } from 'react'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; -import { isMlRule, isSuppressibleAlertRule } from '../../../../common/detection_engine/utils'; -import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import { isSuppressibleAlertRule } from '../../../../common/detection_engine/utils'; export interface UseAlertSuppressionReturn { isSuppressionEnabled: boolean; } export const useAlertSuppression = (ruleType: Type | undefined): UseAlertSuppressionReturn => { - const isAlertSuppressionForMachineLearningRuleEnabled = useIsExperimentalFeatureEnabled( - 'alertSuppressionForMachineLearningRuleEnabled' - ); - const isAlertSuppressionForEsqlRuleEnabled = useIsExperimentalFeatureEnabled( - 'alertSuppressionForEsqlRuleEnabled' - ); - const isSuppressionEnabledForRuleType = useCallback(() => { if (!ruleType) { return false; } - // Remove this condition when the Feature Flag for enabling Suppression in the New terms rule is removed. - if (ruleType === 'esql') { - return isSuppressibleAlertRule(ruleType) && isAlertSuppressionForEsqlRuleEnabled; - } - - if (isMlRule(ruleType)) { - return isSuppressibleAlertRule(ruleType) && isAlertSuppressionForMachineLearningRuleEnabled; - } return isSuppressibleAlertRule(ruleType); - }, [ - isAlertSuppressionForEsqlRuleEnabled, - isAlertSuppressionForMachineLearningRuleEnabled, - ruleType, - ]); + }, [ruleType]); return { isSuppressionEnabled: isSuppressionEnabledForRuleType(), diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index b14e007f36754d..0908083a30a18c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -381,7 +381,6 @@ describe('alert actions', () => { eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], - expandedDetail: {}, filters: [ { $state: { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 77c6fcc1801915..b556f81523fc5a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -331,8 +331,7 @@ export const AlertsTableComponent: FC = ({ scopeId: tableId, }); - const { DetailsPanel, SessionView } = useSessionView({ - entityType: 'events', + const { SessionView } = useSessionView({ scopeId: tableId, }); @@ -356,7 +355,6 @@ export const AlertsTableComponent: FC = ({ {AlertTable} - {DetailsPanel}
); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index 4a7ea8e77cc92b..b90484e4a795f6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -7,17 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const PAGE_TITLE = i18n.translate('xpack.securitySolution.detectionEngine.pageTitle', { - defaultMessage: 'Detection engine', -}); - -export const ALERTS_DOCUMENT_TYPE = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.documentTypeTitle', - { - defaultMessage: 'Alerts', - } -); - export const OPEN_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.openAlertsTitle', { @@ -39,13 +28,6 @@ export const ACKNOWLEDGED_ALERTS = i18n.translate( } ); -export const LOADING_ALERTS = i18n.translate( - 'xpack.securitySolution.detectionEngine.alerts.loadingAlertsTitle', - { - defaultMessage: 'Loading Alerts', - } -); - export const TOTAL_COUNT_OF_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.totalCountOfAlertsTitle', { @@ -295,13 +277,6 @@ export const CLICK_TO_CHANGE_ALERT_STATUS = i18n.translate( } ); -export const SIGNAL_STATUS = i18n.translate( - 'xpack.securitySolution.eventsViewer.alerts.overviewTable.signalStatusTitle', - { - defaultMessage: 'Status', - } -); - export const TRIGGERED = i18n.translate( 'xpack.securitySolution.eventsViewer.alerts.defaultHeaders.triggeredTitle', { @@ -334,13 +309,6 @@ export const SESSIONS_TITLE = i18n.translate('xpack.securitySolution.sessionsVie defaultMessage: 'Sessions', }); -export const TAKE_ACTION = i18n.translate( - 'xpack.securitySolution.detectionEngine.groups.additionalActions.takeAction', - { - defaultMessage: 'Take actions', - } -); - export const STATS_GROUP_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.groups.stats.alertsCount', { @@ -355,20 +323,6 @@ export const STATS_GROUP_HOSTS = i18n.translate( } ); -export const STATS_GROUP_IPS = i18n.translate( - 'xpack.securitySolution.detectionEngine.groups.stats.ipsCount', - { - defaultMessage: `IP's:`, - } -); - -export const GROUP_ALERTS_SELECTOR = i18n.translate( - 'xpack.securitySolution.detectionEngine.selectGroup.title', - { - defaultMessage: `Group alerts by`, - } -); - export const STATS_GROUP_USERS = i18n.translate( 'xpack.securitySolution.detectionEngine.groups.stats.usersCount', { diff --git a/x-pack/plugins/security_solution/public/detections/mitre/get_mitre_threat_component.ts b/x-pack/plugins/security_solution/public/detections/mitre/get_mitre_threat_component.ts index 2e32a8b81f0359..0848cbcca15c62 100644 --- a/x-pack/plugins/security_solution/public/detections/mitre/get_mitre_threat_component.ts +++ b/x-pack/plugins/security_solution/public/detections/mitre/get_mitre_threat_component.ts @@ -9,6 +9,7 @@ import type { Threats } from '@kbn/securitysolution-io-ts-alerting-types'; import type { SearchHit } from '../../../common/search_strategy'; import { buildThreatDescription } from '../../detection_engine/rule_creation_ui/components/description_step/helpers'; +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 export const getMitreComponentParts = (searchHit?: SearchHit) => { const ruleParameters = searchHit?.fields ? searchHit?.fields['kibana.alert.rule.parameters'] diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score_data.test.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score_data.test.ts deleted file mode 100644 index 123d4662768c19..00000000000000 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score_data.test.ts +++ /dev/null @@ -1,98 +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 { renderHook } from '@testing-library/react-hooks'; -import { TestProviders } from '../../../common/mock'; -import { ONLY_FIRST_ITEM_PAGINATION, useRiskScoreData } from './use_risk_score_data'; -import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; -import { RiskScoreEntity } from '../../../../common/search_strategy'; -import { useRiskScore } from './use_risk_score'; - -jest.mock('./use_risk_score'); -jest.mock('../../../timelines/components/side_panel/event_details/helpers'); -const mockUseRiskScore = useRiskScore as jest.Mock; -const mockUseBasicDataFromDetailsData = useBasicDataFromDetailsData as jest.Mock; -const defaultResult = { - data: [], - inspect: {}, - isInspected: false, - isAuthorized: true, - isModuleEnabled: true, - refetch: () => {}, - totalCount: 0, - loading: false, -}; -const defaultRisk = { - loading: false, - isModuleEnabled: true, - result: [], -}; - -const defaultArgs = [ - { - field: 'host.name', - isObjectArray: false, - }, -]; - -describe('useRiskScoreData', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockUseRiskScore.mockReturnValue(defaultResult); - mockUseBasicDataFromDetailsData.mockReturnValue({ - hostName: 'host', - userName: 'user', - }); - }); - test('returns expected default values', () => { - const { result } = renderHook(() => useRiskScoreData(defaultArgs), { - wrapper: TestProviders, - }); - expect(result.current).toEqual({ - hostRisk: defaultRisk, - userRisk: defaultRisk, - isAuthorized: true, - }); - }); - - test('builds filter query for risk score hooks', () => { - renderHook(() => useRiskScoreData(defaultArgs), { - wrapper: TestProviders, - }); - expect(mockUseRiskScore).toHaveBeenCalledWith({ - filterQuery: { terms: { 'user.name': ['user'] } }, - pagination: ONLY_FIRST_ITEM_PAGINATION, - skip: false, - riskEntity: RiskScoreEntity.user, - }); - expect(mockUseRiskScore).toHaveBeenCalledWith({ - filterQuery: { terms: { 'host.name': ['host'] } }, - pagination: ONLY_FIRST_ITEM_PAGINATION, - skip: false, - riskEntity: RiskScoreEntity.host, - }); - }); - - test('skips risk score hooks with no entity name', () => { - mockUseBasicDataFromDetailsData.mockReturnValue({ hostName: undefined, userName: undefined }); - renderHook(() => useRiskScoreData(defaultArgs), { - wrapper: TestProviders, - }); - expect(mockUseRiskScore).toHaveBeenCalledWith({ - filterQuery: undefined, - pagination: ONLY_FIRST_ITEM_PAGINATION, - skip: true, - riskEntity: RiskScoreEntity.user, - }); - expect(mockUseRiskScore).toHaveBeenCalledWith({ - filterQuery: undefined, - pagination: ONLY_FIRST_ITEM_PAGINATION, - skip: true, - riskEntity: RiskScoreEntity.host, - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score_data.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score_data.ts deleted file mode 100644 index 545d12d0851f7d..00000000000000 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/hooks/use_risk_score_data.ts +++ /dev/null @@ -1,83 +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 { useMemo } from 'react'; -import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; -import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { - buildHostNamesFilter, - buildUserNamesFilter, - RiskScoreEntity, -} from '../../../../common/search_strategy'; -import { useRiskScore } from './use_risk_score'; -import type { HostRisk, UserRisk } from '../types'; - -export const ONLY_FIRST_ITEM_PAGINATION = { - cursorStart: 0, - querySize: 1, -}; - -export const useRiskScoreData = (data: TimelineEventsDetailsItem[]) => { - const { hostName, userName } = useBasicDataFromDetailsData(data); - - const hostNameFilterQuery = useMemo( - () => (hostName ? buildHostNamesFilter([hostName]) : undefined), - [hostName] - ); - - const { - data: hostRiskData, - loading: hostRiskLoading, - isAuthorized: isHostRiskScoreAuthorized, - isModuleEnabled: isHostRiskModuleEnabled, - } = useRiskScore({ - filterQuery: hostNameFilterQuery, - pagination: ONLY_FIRST_ITEM_PAGINATION, - riskEntity: RiskScoreEntity.host, - skip: !hostNameFilterQuery, - }); - - const hostRisk: HostRisk = useMemo( - () => ({ - loading: hostRiskLoading, - isModuleEnabled: isHostRiskModuleEnabled, - result: hostRiskData, - }), - [hostRiskData, hostRiskLoading, isHostRiskModuleEnabled] - ); - - const userNameFilterQuery = useMemo( - () => (userName ? buildUserNamesFilter([userName]) : undefined), - [userName] - ); - - const { - data: userRiskData, - loading: userRiskLoading, - isAuthorized: isUserRiskScoreAuthorized, - isModuleEnabled: isUserRiskModuleEnabled, - } = useRiskScore({ - filterQuery: userNameFilterQuery, - pagination: ONLY_FIRST_ITEM_PAGINATION, - riskEntity: RiskScoreEntity.user, - skip: !userNameFilterQuery, - }); - - const userRisk: UserRisk = useMemo( - () => ({ - loading: userRiskLoading, - isModuleEnabled: isUserRiskModuleEnabled, - result: userRiskData, - }), - [userRiskLoading, isUserRiskModuleEnabled, userRiskData] - ); - - return { - userRisk, - hostRisk, - isAuthorized: isHostRiskScoreAuthorized && isUserRiskScoreAuthorized, - }; -}; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_panel.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_panel.test.tsx deleted file mode 100644 index cfe1e4ec898c46..00000000000000 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_panel.test.tsx +++ /dev/null @@ -1,96 +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 React from 'react'; - -import { render } from '@testing-library/react'; -import { TestProviders } from '../../common/mock'; -import type { RiskEntity } from './risk_summary_panel'; -import * as i18n from '../../common/components/event_details/cti_details/translations'; -import { RiskSummaryPanel } from './risk_summary_panel'; -import { RiskScoreEntity, RiskSeverity } from '../../../common/search_strategy'; -import { getEmptyValue } from '../../common/components/empty_value'; - -describe.each([RiskScoreEntity.host, RiskScoreEntity.user])( - 'RiskSummary entityType: %s', - (riskEntity) => { - it(`renders ${riskEntity} risk data`, () => { - const riskSeverity = RiskSeverity.Low; - const risk = { - loading: false, - isModuleEnabled: true, - result: [ - { - '@timestamp': '1641902481', - [riskEntity === RiskScoreEntity.host ? 'host' : 'user']: { - name: 'test-host-name', - risk: { - multipliers: [], - calculated_score_norm: 9999, - calculated_level: riskSeverity, - rule_risks: [], - }, - }, - }, - ], // as unknown as HostRiskScore[] | UserRiskScore[], - } as unknown as RiskEntity['risk']; - - const props = { - riskEntity, - risk, - } as RiskEntity; - - const { getByText } = render( - - - - ); - - expect(getByText(riskSeverity)).toBeInTheDocument(); - expect(getByText(i18n.RISK_DATA_TITLE(riskEntity))).toBeInTheDocument(); - }); - - it('renders spinner when loading', () => { - const risk = { - loading: true, - isModuleEnabled: true, - result: [], - }; - - const props = { - riskEntity, - risk, - } as RiskEntity; - const { getByTestId } = render( - - - - ); - - expect(getByTestId('loading')).toBeInTheDocument(); - }); - - it(`renders empty value when there is no ${riskEntity} data`, () => { - const risk = { - loading: false, - isModuleEnabled: true, - result: [], - }; - const props = { - riskEntity, - risk, - } as RiskEntity; - const { getByText } = render( - - - - ); - - expect(getByText(getEmptyValue())).toBeInTheDocument(); - }); - } -); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_panel.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_panel.tsx deleted file mode 100644 index 80d0558f5cf558..00000000000000 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_summary_panel.tsx +++ /dev/null @@ -1,103 +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 React from 'react'; -import { EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import * as i18n from '../../common/components/event_details/cti_details/translations'; -import { - EnrichedDataRow, - ThreatSummaryPanelHeader, -} from '../../common/components/event_details/cti_details/threat_summary_view'; -import { RiskScoreLevel } from './severity/common'; -import type { RiskSeverity } from '../../../common/search_strategy'; -import { RiskScoreEntity } from '../../../common/search_strategy'; -import { getEmptyValue } from '../../common/components/empty_value'; -import { EntityAnalyticsLearnMoreLink } from './risk_score_onboarding/entity_analytics_doc_link'; -import { RiskScoreHeaderTitle } from './risk_score_onboarding/risk_score_header_title'; -import type { HostRisk, UserRisk } from '../api/types'; - -interface HostRiskEntity { - originalRisk?: RiskSeverity | undefined; - risk: HostRisk; - riskEntity: RiskScoreEntity.host; -} - -interface UserRiskEntity { - originalRisk?: RiskSeverity | undefined; - risk: UserRisk; - riskEntity: RiskScoreEntity.user; -} - -export type RiskEntity = HostRiskEntity | UserRiskEntity; - -const RiskSummaryPanelComponent: React.FC = ({ risk, riskEntity, originalRisk }) => { - const currentRiskScore = - riskEntity === RiskScoreEntity.host - ? risk?.result?.[0]?.host?.risk?.calculated_level - : risk?.result?.[0]?.user?.risk?.calculated_level; - - return ( - <> - - - } - toolTipTitle={ - - } - toolTipContent={ - - ), - }} - /> - } - /> - - {risk.loading && } - - {!risk.loading && ( - <> - - ) : ( - getEmptyValue() - ) - } - /> - - {originalRisk && currentRiskScore !== originalRisk && ( - <> - } - /> - - )} - - )} - - - ); -}; -export const RiskSummaryPanel = React.memo(RiskSummaryPanelComponent); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx index 138714693a7969..e47d46fcff1e69 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/tabs/table_tab.tsx @@ -70,6 +70,7 @@ export const getColumns: ColumnsProvider = ({ /** * Table view displayed in the document details expandable flyout right section */ +// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462 export const TableTab = memo(() => { const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId } = useDocumentDetailsContext(); diff --git a/x-pack/plugins/security_solution/public/helpers.tsx b/x-pack/plugins/security_solution/public/helpers.tsx index d8a20904f1b231..ffe7b2dd9668de 100644 --- a/x-pack/plugins/security_solution/public/helpers.tsx +++ b/x-pack/plugins/security_solution/public/helpers.tsx @@ -313,14 +313,6 @@ export const getScopedActions = (scopeId: string) => { } }; -export const getScopedSelectors = (scopeId: string) => { - if (isTimelineScope(scopeId)) { - return timelineActions; - } else if (isInTableScope(scopeId)) { - return dataTableActions; - } -}; - export const isActiveTimeline = (timelineId: string) => timelineId === TimelineId.active; export const getSourcererScopeId = (scopeId: string): SourcererScopeName => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx index fae840426cd0b1..8a4f911468e4a3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx @@ -11,7 +11,6 @@ import userEvent from '@testing-library/user-event'; import { FormattedIp } from '.'; import { TestProviders } from '../../../common/mock'; import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; -import { timelineActions } from '../../store'; import { StatefulEventContext } from '../../../common/components/events_viewer/stateful_event_context'; import { NetworkPanelKey } from '../../../flyout/network_details'; @@ -44,16 +43,7 @@ jest.mock('../../../common/components/drag_and_drop/draggable_wrapper', () => { }; }); -jest.mock('../../store', () => { - const original = jest.requireActual('../../store'); - return { - ...original, - timelineActions: { - ...original.timelineActions, - toggleDetailPanel: jest.fn(), - }, - }; -}); +jest.mock('../../store'); const mockOpenFlyout = jest.fn(); jest.mock('@kbn/expandable-flyout', () => ({ @@ -97,17 +87,6 @@ describe('FormattedIp', () => { expect(screen.getByTestId('DraggableWrapper')).toBeInTheDocument(); }); - test('if not enableIpDetailsFlyout, should go to network details page', () => { - render( - - - - ); - - userEvent.click(screen.getByTestId('network-details')); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); - }); - test('if enableIpDetailsFlyout, should open NetworkDetails expandable flyout', () => { const context = { enableHostDetailsFlyout: true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 0763fc3c969a64..00000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,286 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Details Panel Component DetailsPanel: rendering it should not render the DetailsPanel if an expanded detail with a panelView, but not params have been set: - - 1`] = ` - -`; - -exports[`Details Panel Component DetailsPanel: rendering it should not render the DetailsPanel if no expanded detail has been set in the reducer 1`] = ` - -`; - -exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should render the Event Details Panel when the panelView is set and the associated params are set 1`] = ` -Array [ - .c0 { - -webkit-flex: 0 1 auto; - -ms-flex: 0 1 auto; - flex: 0 1 auto; - margin-top: 8px; -} - -
-
-
-
-
- -
-
-
-
-
-
-
, -
, -
- - - - - - - - - - - - -
, - .c0 .side-panel-flyout-footer { - background-color: transparent; -} - -
- , -] -`; - -exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should render the Event Details view of the Details Panel in the flyout when the panelView is eventDetail and the eventId is set 1`] = ` -.c0 { - -webkit-flex: 0 1 auto; - -ms-flex: 0 1 auto; - flex: 0 1 auto; - margin-top: 8px; -} - -.c1 .euiFlyoutBody__overflow { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - overflow: hidden; -} - -.c1 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - overflow: hidden; - padding: 0 12px 12px; -} - -.c2 .side-panel-flyout-footer { - background-color: transparent; -} - -
-