diff --git a/.github/workflows/alert-failed-test.yml b/.github/workflows/alert-failed-test.yml index 03d46cb65fcfd..92916629d80f3 100644 --- a/.github/workflows/alert-failed-test.yml +++ b/.github/workflows/alert-failed-test.yml @@ -7,7 +7,7 @@ jobs: name: Alert on failed test if: | !github.event.issue.pull_request - && github.event.comment.user.login == 'kibanamachine' + && (github.event.comment.user.login == 'kibanamachine' || github.event.comment.user.login == 'elasticmachine') runs-on: ubuntu-latest steps: - name: Checkout kibana-operations diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index aff3a490c8d9b..00b2bd8b6a624 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 a5cc1ca1f8d96..1ddb5572d01f7 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 385a03f518e7f..7e318f443a1cf 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 f7c763da024bf..c4455be73e16f 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 0000000000000..7cd705ea223fd --- /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-esql-ast/src/walker/walker.test.ts b/packages/kbn-esql-ast/src/walker/walker.test.ts index 86a6f6dca102d..2bc666a1e0421 100644 --- a/packages/kbn-esql-ast/src/walker/walker.test.ts +++ b/packages/kbn-esql-ast/src/walker/walker.test.ts @@ -592,8 +592,7 @@ describe('Walker.params', () => { }); test('can collect all params from grouping functions', () => { - const query = - 'ROW x=1, time=2024-07-10 | stats z = avg(x) by bucket(time, 20, ?earliest,?latest)'; + const query = 'ROW x=1, time=2024-07-10 | stats z = avg(x) by bucket(time, 20, ?start,?end)'; const { ast } = getAstAndSyntaxErrors(query); const params = Walker.params(ast); @@ -602,13 +601,13 @@ describe('Walker.params', () => { type: 'literal', literalType: 'param', paramType: 'named', - value: 'earliest', + value: 'start', }, { type: 'literal', literalType: 'param', paramType: 'named', - value: 'latest', + value: 'end', }, ]); }); diff --git a/packages/kbn-esql-utils/index.ts b/packages/kbn-esql-utils/index.ts index 2e04d2a5540fc..ffb424ebcf0ae 100644 --- a/packages/kbn-esql-utils/index.ts +++ b/packages/kbn-esql-utils/index.ts @@ -21,8 +21,8 @@ export { getESQLQueryColumnsRaw, getESQLResults, getTimeFieldFromESQLQuery, - getEarliestLatestParams, - hasEarliestLatestParams, + getStartEndParams, + hasStartEndParams, TextBasedLanguages, } from './src'; diff --git a/packages/kbn-esql-utils/src/index.ts b/packages/kbn-esql-utils/src/index.ts index 879e92e3ffa56..5ac459b7564b8 100644 --- a/packages/kbn-esql-utils/src/index.ts +++ b/packages/kbn-esql-utils/src/index.ts @@ -22,6 +22,6 @@ export { getESQLQueryColumns, getESQLQueryColumnsRaw, getESQLResults, - getEarliestLatestParams, - hasEarliestLatestParams, + getStartEndParams, + hasStartEndParams, } from './utils/run_query'; diff --git a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts index 0366b1aba9f36..e324e3ece274c 100644 --- a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts +++ b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts @@ -150,12 +150,10 @@ describe('esql query helpers', () => { }); it('should return the time field if there is at least one time param', () => { - expect(getTimeFieldFromESQLQuery('from a | eval b = 1 | where time >= ?earliest')).toBe( - 'time' - ); + expect(getTimeFieldFromESQLQuery('from a | eval b = 1 | where time >= ?start')).toBe('time'); }); - it('should return undefined if there is one named param but is not ?earliest or ?latest', () => { + it('should return undefined if there is one named param but is not ?start or ?end', () => { expect( getTimeFieldFromESQLQuery('from a | eval b = 1 | where time >= ?late') ).toBeUndefined(); @@ -163,14 +161,14 @@ describe('esql query helpers', () => { it('should return undefined if there is one named param but is used without a time field', () => { expect( - getTimeFieldFromESQLQuery('from a | eval b = DATE_TRUNC(1 day, ?earliest)') + getTimeFieldFromESQLQuery('from a | eval b = DATE_TRUNC(1 day, ?start)') ).toBeUndefined(); }); it('should return the time field if there is at least one time param in the bucket function', () => { expect( getTimeFieldFromESQLQuery( - 'from a | stats meow = avg(bytes) by bucket(event.timefield, 200, ?earliest, ?latest)' + 'from a | stats meow = avg(bytes) by bucket(event.timefield, 200, ?start, ?end)' ) ).toBe('event.timefield'); }); diff --git a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts index e683bd8b511c7..52bd46f2927cf 100644 --- a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts +++ b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts @@ -56,7 +56,7 @@ export function removeDropCommandsFromESQLQuery(esql?: string): string { } /** - * When the ?earliest and ?latest params are used, we want to retrieve the timefield from the query. + * When the ?start and ?end params are used, we want to retrieve the timefield from the query. * @param esql:string * @returns string */ @@ -69,9 +69,7 @@ export const getTimeFieldFromESQLQuery = (esql: string) => { }); const params = Walker.params(ast); - const timeNamedParam = params.find( - (param) => param.value === 'earliest' || param.value === 'latest' - ); + const timeNamedParam = params.find((param) => param.value === 'start' || param.value === 'end'); if (!timeNamedParam || !functions.length) { return undefined; } diff --git a/packages/kbn-esql-utils/src/utils/run_query.test.ts b/packages/kbn-esql-utils/src/utils/run_query.test.ts index ba8a22f98a7a7..4f5c4dfb9e47d 100644 --- a/packages/kbn-esql-utils/src/utils/run_query.test.ts +++ b/packages/kbn-esql-utils/src/utils/run_query.test.ts @@ -5,38 +5,38 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { getEarliestLatestParams } from './run_query'; +import { getStartEndParams } from './run_query'; -describe('getEarliestLatestParams', () => { +describe('getStartEndParams', () => { it('should return an empty array if there are no time params', () => { const time = { from: 'now-15m', to: 'now' }; const query = 'FROM foo'; - const params = getEarliestLatestParams(query, time); + const params = getStartEndParams(query, time); expect(params).toEqual([]); }); - it('should return an array with the earliest param if exists at the query', () => { + it('should return an array with the start param if exists at the query', () => { const time = { from: 'Jul 5, 2024 @ 08:03:56.849', to: 'Jul 5, 2024 @ 10:03:56.849' }; - const query = 'FROM foo | where time > ?earliest'; - const params = getEarliestLatestParams(query, time); + const query = 'FROM foo | where time > ?start'; + const params = getStartEndParams(query, time); expect(params).toHaveLength(1); - expect(params[0]).toHaveProperty('earliest'); + expect(params[0]).toHaveProperty('start'); }); - it('should return an array with the latest param if exists at the query', () => { + it('should return an array with the end param if exists at the query', () => { const time = { from: 'Jul 5, 2024 @ 08:03:56.849', to: 'Jul 5, 2024 @ 10:03:56.849' }; - const query = 'FROM foo | where time < ?latest'; - const params = getEarliestLatestParams(query, time); + const query = 'FROM foo | where time < ?end'; + const params = getStartEndParams(query, time); expect(params).toHaveLength(1); - expect(params[0]).toHaveProperty('latest'); + expect(params[0]).toHaveProperty('end'); }); - it('should return an array with the latest and earliest params if exist at the query', () => { + it('should return an array with the end and start params if exist at the query', () => { const time = { from: 'Jul 5, 2024 @ 08:03:56.849', to: 'Jul 5, 2024 @ 10:03:56.849' }; - const query = 'FROM foo | where time < ?latest amd time > ?earliest'; - const params = getEarliestLatestParams(query, time); + const query = 'FROM foo | where time < ?end amd time > ?start'; + const params = getStartEndParams(query, time); expect(params).toHaveLength(2); - expect(params[0]).toHaveProperty('earliest'); - expect(params[1]).toHaveProperty('latest'); + expect(params[0]).toHaveProperty('start'); + expect(params[1]).toHaveProperty('end'); }); }); diff --git a/packages/kbn-esql-utils/src/utils/run_query.ts b/packages/kbn-esql-utils/src/utils/run_query.ts index e562f7a882a3c..2041e686cb797 100644 --- a/packages/kbn-esql-utils/src/utils/run_query.ts +++ b/packages/kbn-esql-utils/src/utils/run_query.ts @@ -14,22 +14,22 @@ import { esFieldTypeToKibanaFieldType } from '@kbn/field-types'; import type { ESQLColumn, ESQLSearchResponse, ESQLSearchParams } from '@kbn/es-types'; import { lastValueFrom } from 'rxjs'; -export const hasEarliestLatestParams = (query: string) => /\?earliest|\?latest/i.test(query); +export const hasStartEndParams = (query: string) => /\?start|\?end/i.test(query); -export const getEarliestLatestParams = (query: string, time?: TimeRange) => { - const earliestNamedParams = /\?earliest/i.test(query); - const latestNamedParams = /\?latest/i.test(query); - if (time && (earliestNamedParams || latestNamedParams)) { +export const getStartEndParams = (query: string, time?: TimeRange) => { + const startNamedParams = /\?start/i.test(query); + const endNamedParams = /\?end/i.test(query); + if (time && (startNamedParams || endNamedParams)) { const timeParams = { - earliest: earliestNamedParams ? dateMath.parse(time.from)?.toISOString() : undefined, - latest: latestNamedParams ? dateMath.parse(time.to)?.toISOString() : undefined, + start: startNamedParams ? dateMath.parse(time.from)?.toISOString() : undefined, + end: endNamedParams ? dateMath.parse(time.to)?.toISOString() : undefined, }; const namedParams = []; - if (timeParams?.earliest) { - namedParams.push({ earliest: timeParams.earliest }); + if (timeParams?.start) { + namedParams.push({ start: timeParams.start }); } - if (timeParams?.latest) { - namedParams.push({ latest: timeParams.latest }); + if (timeParams?.end) { + namedParams.push({ end: timeParams.end }); } return namedParams; } @@ -61,7 +61,7 @@ export async function getESQLQueryColumnsRaw({ timeRange?: TimeRange; }): Promise { try { - const namedParams = getEarliestLatestParams(esqlQuery, timeRange); + const namedParams = getStartEndParams(esqlQuery, timeRange); const response = await lastValueFrom( search( { @@ -135,7 +135,7 @@ export async function getESQLResults({ response: ESQLSearchResponse; params: ESQLSearchParams; }> { - const namedParams = getEarliestLatestParams(esqlQuery, timeRange); + const namedParams = getStartEndParams(esqlQuery, timeRange); const result = await lastValueFrom( search( { diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index f7075e955c1ef..13591ddc01345 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -28,7 +28,7 @@ const allFunctions = statsAggregationFunctionDefinitions .concat(evalFunctionDefinitions) .concat(groupingFunctionDefinitions); -export const TIME_SYSTEM_PARAMS = ['?earliest', '?latest']; +export const TIME_SYSTEM_PARAMS = ['?start', '?end']; export const TRIGGER_SUGGESTION_COMMAND = { title: 'Trigger Suggestion Dialog', diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts index 17f91a72c52a4..db132d4d3e488 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts @@ -23,16 +23,16 @@ test('should allow param inside agg function argument', async () => { test('allow params in WHERE command expressions', async () => { const { validate } = await setup(); - const res1 = await validate('FROM index | WHERE stringField >= ?earliest'); + const res1 = await validate('FROM index | WHERE stringField >= ?start'); const res2 = await validate(` FROM index - | WHERE stringField >= ?earliest + | WHERE stringField >= ?start | WHERE stringField <= ?0 | WHERE stringField == ? `); const res3 = await validate(` FROM index - | WHERE stringField >= ?earliest + | WHERE stringField >= ?start AND stringField <= ?0 AND stringField == ? `); diff --git a/src/plugins/data/common/search/expressions/esql.ts b/src/plugins/data/common/search/expressions/esql.ts index c36024bdb1b5d..d7af8ac5ffed1 100644 --- a/src/plugins/data/common/search/expressions/esql.ts +++ b/src/plugins/data/common/search/expressions/esql.ts @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import type { IKibanaSearchResponse, IKibanaSearchRequest } from '@kbn/search-types'; import type { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; -import { getEarliestLatestParams } from '@kbn/esql-utils'; +import { getStartEndParams } from '@kbn/esql-utils'; import { zipObject } from 'lodash'; import { Observable, defer, throwError } from 'rxjs'; @@ -154,7 +154,7 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => { uiSettings as Parameters[0] ); - const namedParams = getEarliestLatestParams(query, input.timeRange); + const namedParams = getStartEndParams(query, input.timeRange); if (namedParams.length) { params.params = namedParams; diff --git a/src/plugins/discover/public/application/main/state_management/utils/get_esql_data_view.test.ts b/src/plugins/discover/public/application/main/state_management/utils/get_esql_data_view.test.ts index 715a096429c53..8dc7c046b02ce 100644 --- a/src/plugins/discover/public/application/main/state_management/utils/get_esql_data_view.test.ts +++ b/src/plugins/discover/public/application/main/state_management/utils/get_esql_data_view.test.ts @@ -32,7 +32,7 @@ describe('getEsqlDataView', () => { }); it('returns an adhoc dataview if it is adhoc with named params and query index pattern is the same as the dataview index pattern', async () => { - const query = { esql: 'from data-view-ad-hoc-title | where time >= ?earliest' }; + const query = { esql: 'from data-view-ad-hoc-title | where time >= ?start' }; const dataView = await getEsqlDataView(query, dataViewAdHocNoAtTimestamp, services); expect(dataView.timeFieldName).toBe('time'); }); diff --git a/test/functional/apps/discover/esql/_esql_view.ts b/test/functional/apps/discover/esql/_esql_view.ts index 0753a70837afe..f9e179112533d 100644 --- a/test/functional/apps/discover/esql/_esql_view.ts +++ b/test/functional/apps/discover/esql/_esql_view.ts @@ -113,11 +113,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await testSubjects.exists('unifiedHistogramChart')).to.be(false); }); - it('should render the histogram for indices with no @timestamp field when the ?earliest, ?latest params are in the query', async function () { + it('should render the histogram for indices with no @timestamp field when the ?start, ?end params are in the query', async function () { await PageObjects.discover.selectTextBaseLang(); await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded(); - const testQuery = `from kibana_sample_data_flights | limit 10 | where timestamp >= ?earliest and timestamp <= ?latest`; + const testQuery = `from kibana_sample_data_flights | limit 10 | where timestamp >= ?start and timestamp <= ?end`; await monacoEditor.setCodeEditorValue(testQuery); await testSubjects.click('querySubmitButton'); diff --git a/test/plugin_functional/plugins/core_http/server/plugin.ts b/test/plugin_functional/plugins/core_http/server/plugin.ts index 534d55a97bdfe..c5e8dd01847be 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 0000000000000..0a87b4c9a6bd4 --- /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 07e258e34e3f1..5e3d969bb0277 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 a5b5cba1469ba..27addbfc274a2 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/security-solution/data_table/common/types/detail_panel.ts b/x-pack/packages/security-solution/data_table/common/types/detail_panel.ts index d14a0b8d35213..4162d3711b991 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 557c2c0dd0aca..d33538f6ddf63 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 5d5e0fb31dddd..40cfb8a727f3a 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 29e28af5da28c..8c74b2d213927 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 035c865f1fc82..8b33005685953 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 f8d0d22d5ea88..dc03918095bba 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 1ad08a9332503..06dff26eba57f 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/alerting/common/routes/rule/apis/update_api_key/index.ts b/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/index.ts new file mode 100644 index 0000000000000..d253ba47d4053 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/index.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. + */ + +export { updateApiKeyParamsSchema } from './schemas/latest'; +export type { UpdateApiKeyParams } from './types/latest'; + +export { updateApiKeyParamsSchema as updateApiKeyParamsSchemaV1 } from './schemas/v1'; +export type { UpdateApiKeyParams as UpdateApiKeyParamsV1 } from './types/v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/schemas/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/schemas/latest.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/schemas/v1.ts new file mode 100644 index 0000000000000..3cd4ab1ad6054 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/schemas/v1.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 { schema } from '@kbn/config-schema'; + +export const updateApiKeyParamsSchema = schema.object({ + id: schema.string(), +}); diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/types/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/types/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/types/latest.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export * from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/types/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/types/v1.ts new file mode 100644 index 0000000000000..8966e2875eb5a --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/update_api_key/types/v1.ts @@ -0,0 +1,11 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { updateApiKeyParamsSchemaV1 } from '..'; + +export type UpdateApiKeyParams = TypeOf; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/index.ts new file mode 100644 index 0000000000000..8c8c459a339f4 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/index.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export type { updateApiKeyParamsSchema } from './schemas'; +export type { UpdateApiKeyParams } from './types'; + +export { updateRuleApiKey } from './update_rule_api_key'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/schemas/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/schemas/index.ts new file mode 100644 index 0000000000000..4cb6fd520e23b --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/schemas/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export * from './update_rule_api_key_schemas'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/schemas/update_rule_api_key_schemas.ts b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/schemas/update_rule_api_key_schemas.ts new file mode 100644 index 0000000000000..3cd4ab1ad6054 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/schemas/update_rule_api_key_schemas.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 { schema } from '@kbn/config-schema'; + +export const updateApiKeyParamsSchema = schema.object({ + id: schema.string(), +}); diff --git a/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/types/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/types/index.ts new file mode 100644 index 0000000000000..3c9f9b1552ccf --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/types/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export * from './update_rule_api_key_types'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/types/update_rule_api_key_types.ts b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/types/update_rule_api_key_types.ts new file mode 100644 index 0000000000000..07dffdc3124b8 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/types/update_rule_api_key_types.ts @@ -0,0 +1,11 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { updateApiKeyParamsSchema } from '../schemas'; + +export type UpdateApiKeyParams = TypeOf; diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update_api_key.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/update_rule_api_key.test.ts similarity index 84% rename from x-pack/plugins/alerting/server/rules_client/tests/update_api_key.test.ts rename to x-pack/plugins/alerting/server/application/rule/methods/update_api_key/update_rule_api_key.test.ts index dbf80eb978d2f..ef9979a8c8b88 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/update_rule_api_key.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { RulesClient, ConstructorOptions } from '../rules_client'; +import { RulesClient, ConstructorOptions } from '../../../../rules_client/rules_client'; import { savedObjectsClientMock, loggingSystemMock, @@ -13,20 +13,20 @@ import { uiSettingsServiceMock, } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; -import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; +import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; -import { AlertingAuthorization } from '../../authorization/alerting_authorization'; +import { AlertingAuthorization } from '../../../../authorization/alerting_authorization'; import { ActionsAuthorization } from '@kbn/actions-plugin/server'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; -import { getBeforeSetup, setGlobalDate } from './lib'; -import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; -import { ConnectorAdapterRegistry } from '../../connector_adapters/connector_adapter_registry'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; -import { backfillClientMock } from '../../backfill_client/backfill_client.mock'; +import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib'; +import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry'; +import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; +import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock'; -jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({ +jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({ bulkMarkApiKeysForInvalidation: jest.fn(), })); @@ -77,7 +77,7 @@ beforeEach(() => { setGlobalDate(); -describe('updateApiKey()', () => { +describe('updateRuleApiKey()', () => { let rulesClient: RulesClient; const existingAlert = { id: '1', @@ -123,7 +123,7 @@ describe('updateApiKey()', () => { apiKeysEnabled: true, result: { id: '234', name: '123', api_key: 'abc' }, }); - await rulesClient.updateApiKey({ id: '1' }); + await rulesClient.updateRuleApiKey({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith( RULE_SAVED_OBJECT_TYPE, @@ -184,7 +184,7 @@ describe('updateApiKey()', () => { apiKeysEnabled: true, result: { id: '234', name: '123', api_key: 'abc' }, }); - await rulesClient.updateApiKey({ id: '1' }); + await rulesClient.updateRuleApiKey({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith( RULE_SAVED_OBJECT_TYPE, @@ -240,7 +240,7 @@ describe('updateApiKey()', () => { apiKeysEnabled: true, result: { id: '234', name: '123', api_key: 'abc' }, }); - await rulesClient.updateApiKey({ id: '1' }); + await rulesClient.updateRuleApiKey({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith( RULE_SAVED_OBJECT_TYPE, @@ -288,12 +288,21 @@ describe('updateApiKey()', () => { throw new Error('no'); }); await expect( - async () => await rulesClient.updateApiKey({ id: '1' }) + async () => await rulesClient.updateRuleApiKey({ id: '1' }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Error updating API key for rule: could not create API key - no"` ); }); + test('throws an error if API params do not match the schema', async () => { + await expect( + // @ts-ignore: this is what we are testing + async () => await rulesClient.updateRuleApiKey({ id: 1 }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error validating update api key parameters - [id]: expected value of type [string] but got [number]"` + ); + }); + test('falls back to SOC when getDecryptedAsInternalUser throws an error', async () => { rulesClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, @@ -301,7 +310,7 @@ describe('updateApiKey()', () => { }); encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - await rulesClient.updateApiKey({ id: '1' }); + await rulesClient.updateRuleApiKey({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith(RULE_SAVED_OBJECT_TYPE, '1'); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith( RULE_SAVED_OBJECT_TYPE, @@ -346,7 +355,7 @@ describe('updateApiKey()', () => { test('swallows error when invalidate API key throws', async () => { bulkMarkApiKeysForInvalidationMock.mockImplementationOnce(() => new Error('Fail')); - await rulesClient.updateApiKey({ id: '1' }); + await rulesClient.updateRuleApiKey({ id: '1' }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1); expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith( @@ -359,7 +368,7 @@ describe('updateApiKey()', () => { test('swallows error when getting decrypted object throws', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - await rulesClient.updateApiKey({ id: '1' }); + await rulesClient.updateRuleApiKey({ id: '1' }); expect(rulesClientParams.logger.error).toHaveBeenCalledWith( 'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail' ); @@ -373,9 +382,9 @@ describe('updateApiKey()', () => { }); unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); - await expect(rulesClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail"` - ); + await expect( + rulesClient.updateRuleApiKey({ id: '1' }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1); expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith( { apiKeys: ['MjM0OmFiYw=='] }, @@ -385,8 +394,8 @@ describe('updateApiKey()', () => { }); describe('authorization', () => { - test('ensures user is authorised to updateApiKey this type of alert under the consumer', async () => { - await rulesClient.updateApiKey({ id: '1' }); + test('ensures user is authorised to updateRuleApiKey this type of alert under the consumer', async () => { + await rulesClient.updateRuleApiKey({ id: '1' }); expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'execute' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ @@ -397,13 +406,13 @@ describe('updateApiKey()', () => { }); }); - test('throws when user is not authorised to updateApiKey this type of alert', async () => { + test('throws when user is not authorised to updateRuleApiKey this type of alert', async () => { authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to updateApiKey a "myType" alert for "myApp"`) + new Error(`Unauthorized to updateRuleApiKey a "myType" alert for "myApp"`) ); - await expect(rulesClient.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` + await expect(rulesClient.updateRuleApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to updateRuleApiKey a "myType" alert for "myApp"]` ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ @@ -417,7 +426,7 @@ describe('updateApiKey()', () => { describe('auditLogger', () => { test('logs audit event when updating the API key of a rule', async () => { - await rulesClient.updateApiKey({ id: '1' }); + await rulesClient.updateRuleApiKey({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ @@ -433,7 +442,7 @@ describe('updateApiKey()', () => { test('logs audit event when not authorised to update the API key of a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); - await expect(rulesClient.updateApiKey({ id: '1' })).rejects.toThrow(); + await expect(rulesClient.updateRuleApiKey({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/update_rule_api_key.ts similarity index 78% rename from x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts rename to x-pack/plugins/alerting/server/application/rule/methods/update_api_key/update_rule_api_key.ts index ae9367b13963d..f81dfd64f5684 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/update_api_key/update_rule_api_key.ts @@ -5,32 +5,41 @@ * 2.0. */ -import { RawRule } from '../../types'; -import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; -import { retryIfConflicts } from '../../lib/retry_if_conflicts'; -import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; -import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; -import { createNewAPIKeySet, updateMeta } from '../lib'; -import { RulesClientContext } from '../types'; -import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects'; +import Boom from '@hapi/boom'; +import { RawRule } from '../../../../types'; +import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization'; +import { retryIfConflicts } from '../../../../lib/retry_if_conflicts'; +import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events'; +import { createNewAPIKeySet, updateMeta } from '../../../../rules_client/lib'; +import { RulesClientContext } from '../../../../rules_client/types'; +import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects'; +import { UpdateApiKeyParams } from './types'; +import { updateApiKeyParamsSchema } from './schemas'; -export async function updateApiKey( +export async function updateRuleApiKey( context: RulesClientContext, - { id }: { id: string } + { id }: UpdateApiKeyParams ): Promise { return await retryIfConflicts( context.logger, - `rulesClient.updateApiKey('${id}')`, + `rulesClient.updateRuleApiKey('${id}')`, async () => await updateApiKeyWithOCC(context, { id }) ); } -async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: string }) { +async function updateApiKeyWithOCC(context: RulesClientContext, { id }: UpdateApiKeyParams) { let oldApiKeyToInvalidate: string | null = null; let oldApiKeyCreatedByUser: boolean | undefined | null = false; let attributes: RawRule; let version: string | undefined; + try { + updateApiKeyParamsSchema.validate({ id }); + } catch (error) { + throw Boom.badRequest(`Error validating update api key parameters - ${error.message}`); + } + try { const decryptedAlert = await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser( diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index c695a0420c55b..5cd547ba08350 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -35,7 +35,7 @@ import { muteAllRuleRoute } from './mute_all_rule'; import { muteAlertRoute } from './rule/apis/mute_alert/mute_alert'; import { unmuteAllRuleRoute } from './unmute_all_rule'; import { unmuteAlertRoute } from './unmute_alert'; -import { updateRuleApiKeyRoute } from './update_rule_api_key'; +import { updateRuleApiKeyRoute } from './rule/apis/update_api_key/update_rule_api_key_route'; import { bulkEditInternalRulesRoute } from './rule/apis/bulk_edit/bulk_edit_rules_route'; import { snoozeRuleRoute } from './rule/apis/snooze'; import { unsnoozeRuleRoute } from './rule/apis/unsnooze'; diff --git a/x-pack/plugins/alerting/server/routes/legacy/update_api_key.test.ts b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.test.ts index e4fa2539c391f..3b726898138c0 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.test.ts @@ -38,7 +38,7 @@ describe('updateApiKeyRoute', () => { expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_update_api_key"`); - rulesClient.updateApiKey.mockResolvedValueOnce(); + rulesClient.updateRuleApiKey.mockResolvedValueOnce(); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -52,8 +52,8 @@ describe('updateApiKeyRoute', () => { expect(await handler(context, req, res)).toEqual(undefined); - expect(rulesClient.updateApiKey).toHaveBeenCalledTimes(1); - expect(rulesClient.updateApiKey.mock.calls[0]).toMatchInlineSnapshot(` + expect(rulesClient.updateRuleApiKey).toHaveBeenCalledTimes(1); + expect(rulesClient.updateRuleApiKey.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "id": "1", @@ -72,7 +72,7 @@ describe('updateApiKeyRoute', () => { const [, handler] = router.post.mock.calls[0]; - rulesClient.updateApiKey.mockRejectedValue( + rulesClient.updateRuleApiKey.mockRejectedValue( new RuleTypeDisabledError('Fail', 'license_invalid') ); diff --git a/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts index f0f7716b00771..a3ba03728f1b9 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/update_api_key.ts @@ -41,7 +41,7 @@ export const updateApiKeyRoute = ( const rulesClient = (await context.alerting).getRulesClient(); const { id } = req.params; try { - await rulesClient.updateApiKey({ id }); + await rulesClient.updateRuleApiKey({ id }); return res.noContent(); } catch (e) { if (e instanceof RuleTypeDisabledError) { diff --git a/x-pack/plugins/alerting/server/routes/update_rule_api_key.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/update_api_key/update_rule_api_key_route.test.ts similarity index 73% rename from x-pack/plugins/alerting/server/routes/update_rule_api_key.test.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/update_api_key/update_rule_api_key_route.test.ts index ea88e2eba19bd..1faeef9a99b5a 100644 --- a/x-pack/plugins/alerting/server/routes/update_rule_api_key.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/update_api_key/update_rule_api_key_route.test.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { updateRuleApiKeyRoute } from './update_rule_api_key'; +import { updateRuleApiKeyRoute } from './update_rule_api_key_route'; import { httpServiceMock } from '@kbn/core/server/mocks'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { rulesClientMock } from '../rules_client.mock'; -import { RuleTypeDisabledError } from '../lib/errors/rule_type_disabled'; +import { licenseStateMock } from '../../../../lib/license_state.mock'; +import { mockHandlerArguments } from '../../../_mock_handler_arguments'; +import { rulesClientMock } from '../../../../rules_client.mock'; +import { RuleTypeDisabledError } from '../../../../lib/errors/rule_type_disabled'; const rulesClient = rulesClientMock.create(); -jest.mock('../lib/license_api_access', () => ({ +jest.mock('../../../../lib/license_api_access', () => ({ verifyApiAccess: jest.fn(), })); @@ -32,7 +32,7 @@ describe('updateRuleApiKeyRoute', () => { expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id}/_update_api_key"`); - rulesClient.updateApiKey.mockResolvedValueOnce(); + rulesClient.updateRuleApiKey.mockResolvedValueOnce(); const [context, req, res] = mockHandlerArguments( { rulesClient }, @@ -46,8 +46,8 @@ describe('updateRuleApiKeyRoute', () => { expect(await handler(context, req, res)).toEqual(undefined); - expect(rulesClient.updateApiKey).toHaveBeenCalledTimes(1); - expect(rulesClient.updateApiKey.mock.calls[0]).toMatchInlineSnapshot(` + expect(rulesClient.updateRuleApiKey).toHaveBeenCalledTimes(1); + expect(rulesClient.updateRuleApiKey.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "id": "1", @@ -66,7 +66,7 @@ describe('updateRuleApiKeyRoute', () => { const [, handler] = router.post.mock.calls[0]; - rulesClient.updateApiKey.mockRejectedValue( + rulesClient.updateRuleApiKey.mockRejectedValue( new RuleTypeDisabledError('Fail', 'license_invalid') ); diff --git a/x-pack/plugins/alerting/server/routes/update_rule_api_key.ts b/x-pack/plugins/alerting/server/routes/rule/apis/update_api_key/update_rule_api_key_route.ts similarity index 68% rename from x-pack/plugins/alerting/server/routes/update_rule_api_key.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/update_api_key/update_rule_api_key_route.ts index 8591f273eeeb3..aee5e9801f635 100644 --- a/x-pack/plugins/alerting/server/routes/update_rule_api_key.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/update_api_key/update_rule_api_key_route.ts @@ -6,14 +6,13 @@ */ import { IRouter } from '@kbn/core/server'; -import { schema } from '@kbn/config-schema'; -import { ILicenseState, RuleTypeDisabledError } from '../lib'; -import { verifyAccessAndContext } from './lib'; -import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../types'; - -const paramSchema = schema.object({ - id: schema.string(), -}); +import { + UpdateApiKeyParamsV1, + updateApiKeyParamsSchemaV1, +} from '../../../../../common/routes/rule/apis/update_api_key'; +import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; +import { verifyAccessAndContext } from '../../../lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../types'; export const updateRuleApiKeyRoute = ( router: IRouter, @@ -24,18 +23,19 @@ export const updateRuleApiKeyRoute = ( path: `${BASE_ALERTING_API_PATH}/rule/{id}/_update_api_key`, options: { access: 'public', - summary: `Update the API key for a rule`, + summary: 'Update the API key for a rule', }, validate: { - params: paramSchema, + params: updateApiKeyParamsSchemaV1, }, }, router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { const rulesClient = (await context.alerting).getRulesClient(); - const { id } = req.params; + const { id }: UpdateApiKeyParamsV1 = req.params; + try { - await rulesClient.updateApiKey({ id }); + await rulesClient.updateRuleApiKey({ id }); return res.noContent(); } catch (e) { if (e instanceof RuleTypeDisabledError) { diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index eedd46eaa71ec..e8efa75b625bd 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -23,7 +23,7 @@ const createRulesClientMock = () => { update: jest.fn(), enable: jest.fn(), disable: jest.fn(), - updateApiKey: jest.fn(), + updateRuleApiKey: jest.fn(), muteAll: jest.fn(), unmuteAll: jest.fn(), muteInstance: jest.fn(), diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index bc5918875c193..696067fbcefcb 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -53,7 +53,7 @@ import { BulkEditOptions, } from '../application/rule/methods/bulk_edit/bulk_edit_rules'; import { bulkEnableRules, BulkEnableRulesParams } from '../application/rule/methods/bulk_enable'; -import { updateApiKey } from './methods/update_api_key'; +import { updateRuleApiKey } from '../application/rule/methods/update_api_key/update_rule_api_key'; import { enable } from './methods/enable'; import { disable } from './methods/disable'; import { clearExpiredSnoozes } from './methods/clear_expired_snoozes'; @@ -164,7 +164,7 @@ export class RulesClient { public bulkDisableRules = (options: BulkDisableRulesRequestBody) => bulkDisableRules(this.context, options); - public updateApiKey = (options: { id: string }) => updateApiKey(this.context, options); + public updateRuleApiKey = (params: { id: string }) => updateRuleApiKey(this.context, params); public enable = (options: { id: string }) => enable(this.context, options); public disable = (options: { id: string; untrack?: boolean }) => disable(this.context, options); diff --git a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts index dd96f5a51905c..ea35fe008866a 100644 --- a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts +++ b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts @@ -149,7 +149,7 @@ async function update(success: boolean) { async function updateApiKey(success: boolean) { try { - await rulesClient.updateApiKey({ id: MockRuleId }); + await rulesClient.updateRuleApiKey({ id: MockRuleId }); } catch (err) { return expectConflict(success, err); } diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index 62892c143146f..8084213ab03e3 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 d576b0ef1732c..15bdedf425986 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 3a518c00fbe47..adc2b41f6ce6a 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 3b5e4949150c0..5968e2ea87fc9 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 00aa160fb3600..6878c72eb4c5f 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 204b940c45cd5..e4c5a88a847c4 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 0d071900418c3..d2671b18e4d68 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 a7e34c79a8505..dd3dcf27c1a61 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 41002d0c48e6b..fa873a89a85d7 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 b9442fb74f94f..ec9a81ad0272b 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 0b8415f755a76..2d1924817e56e 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 de4ebd94b6f2b..371f895b92e09 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 d8d5d397655e3..362a69b4ac0a6 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/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts index 84567075db046..21fbb8c3c11e7 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts @@ -9,7 +9,7 @@ import { ESQL_ASYNC_SEARCH_STRATEGY, KBN_FIELD_TYPES } from '@kbn/data-plugin/co import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; import type { AggregateQuery, TimeRange } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; -import { getEarliestLatestParams } from '@kbn/esql-utils'; +import { getStartEndParams } from '@kbn/esql-utils'; import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { type UseCancellableSearch, useCancellableSearch } from '@kbn/ml-cancellable-search'; import type { estypes } from '@elastic/elasticsearch'; @@ -83,7 +83,7 @@ const getESQLDocumentCountStats = async ( const esqlBaseQuery = query.esql; let earliestMs = Infinity; let latestMs = -Infinity; - const namedParams = getEarliestLatestParams(esqlBaseQuery, timeRange); + const namedParams = getStartEndParams(esqlBaseQuery, timeRange); if (timeFieldName) { const aggQuery = appendToESQLQuery( @@ -298,7 +298,7 @@ export const useESQLOverallStatsData = ( // And use this one query to // 1) identify populated/empty fields // 2) gather examples for populated text fields - const namedParams = getEarliestLatestParams(esqlBaseQuery, timeRange); + const namedParams = getStartEndParams(esqlBaseQuery, timeRange); const columnsResp = (await runRequest( { params: { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_boolean_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_boolean_field_stats.ts index 8538c78f93a5a..c4ae9bb51a56c 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_boolean_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_boolean_field_stats.ts @@ -9,7 +9,7 @@ import type { UseCancellableSearch } from '@kbn/ml-cancellable-search'; import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; import { ESQL_ASYNC_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; import pLimit from 'p-limit'; -import { appendToESQLQuery, getEarliestLatestParams } from '@kbn/esql-utils'; +import { appendToESQLQuery, getStartEndParams } from '@kbn/esql-utils'; import type { Column } from '../../hooks/esql/use_esql_overall_stats_data'; import { getSafeESQLName } from '../requests/esql_utils'; import { isFulfilled, isRejected } from '../../../common/util/promise_all_settled_utils'; @@ -33,7 +33,7 @@ export const getESQLBooleanFieldStats = async ({ timeRange, }: Params): Promise> => { const limiter = pLimit(MAX_CONCURRENT_REQUESTS); - const namedParams = getEarliestLatestParams(esqlBaseQuery, timeRange); + const namedParams = getStartEndParams(esqlBaseQuery, timeRange); const booleanFields = columns .filter((f) => f.secondaryType === 'boolean') .map((field) => { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_count_and_cardinality.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_count_and_cardinality.ts index 35ff176acec13..e1f6b45b37e00 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_count_and_cardinality.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_count_and_cardinality.ts @@ -10,7 +10,7 @@ import { chunk } from 'lodash'; import { isDefined } from '@kbn/ml-is-defined'; import type { TimeRange } from '@kbn/es-query'; import type { ESQLSearchResponse } from '@kbn/es-types'; -import { appendToESQLQuery, getEarliestLatestParams } from '@kbn/esql-utils'; +import { appendToESQLQuery, getStartEndParams } from '@kbn/esql-utils'; import type { UseCancellableSearch } from '@kbn/ml-cancellable-search'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { i18n } from '@kbn/i18n'; @@ -94,7 +94,7 @@ const getESQLOverallStatsInChunk = async ({ let countQuery = fieldsToFetch.length > 0 ? '| STATS ' : ''; countQuery += fieldsToFetch.map((field) => field.query).join(','); const query = appendToESQLQuery(esqlBaseQueryWithLimit, countQuery); - const namedParams = getEarliestLatestParams(esqlBaseQueryWithLimit, timeRange); + const namedParams = getStartEndParams(esqlBaseQueryWithLimit, timeRange); const request = { params: { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_date_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_date_field_stats.ts index 52ac27770a78b..70b1202b4fc95 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_date_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_date_field_stats.ts @@ -9,7 +9,7 @@ import type { UseCancellableSearch } from '@kbn/ml-cancellable-search'; import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; import type { TimeRange } from '@kbn/es-query'; import { ESQL_ASYNC_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; -import { appendToESQLQuery, getEarliestLatestParams } from '@kbn/esql-utils'; +import { appendToESQLQuery, getStartEndParams } from '@kbn/esql-utils'; import type { Column } from '../../hooks/esql/use_esql_overall_stats_data'; import { getSafeESQLName } from '../requests/esql_utils'; import type { DateFieldStats, FieldStatsError } from '../../../../../common/types/field_stats'; @@ -39,7 +39,7 @@ export const getESQLDateFieldStats = async ({ }); if (dateFields.length > 0) { - const namedParams = getEarliestLatestParams(esqlBaseQuery, timeRange); + const namedParams = getStartEndParams(esqlBaseQuery, timeRange); const dateStatsQuery = ' | STATS ' + dateFields.map(({ query }) => query).join(','); const query = appendToESQLQuery(esqlBaseQuery, dateStatsQuery); const request = { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_keyword_fields.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_keyword_fields.ts index a7779dad1aa55..89b703140cb6e 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_keyword_fields.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_keyword_fields.ts @@ -9,7 +9,7 @@ import type { UseCancellableSearch } from '@kbn/ml-cancellable-search'; import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; import { ESQL_ASYNC_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; import pLimit from 'p-limit'; -import { appendToESQLQuery, getEarliestLatestParams } from '@kbn/esql-utils'; +import { appendToESQLQuery, getStartEndParams } from '@kbn/esql-utils'; import type { Column } from '../../hooks/esql/use_esql_overall_stats_data'; import { getSafeESQLName } from '../requests/esql_utils'; import { isFulfilled, isRejected } from '../../../common/util/promise_all_settled_utils'; @@ -32,7 +32,7 @@ export const getESQLKeywordFieldStats = async ({ timeRange, }: Params) => { const limiter = pLimit(MAX_CONCURRENT_REQUESTS); - const namedParams = getEarliestLatestParams(esqlBaseQuery, timeRange); + const namedParams = getStartEndParams(esqlBaseQuery, timeRange); const keywordFields = columns.map((field) => { const query = appendToESQLQuery( esqlBaseQuery, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_numeric_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_numeric_field_stats.ts index 438e756a2d0ad..d12237a05e401 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_numeric_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/esql_requests/get_numeric_field_stats.ts @@ -8,7 +8,7 @@ import type { TimeRange } from '@kbn/es-query'; import type { UseCancellableSearch } from '@kbn/ml-cancellable-search'; import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types'; import { ESQL_ASYNC_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; -import { appendToESQLQuery, getEarliestLatestParams } from '@kbn/esql-utils'; +import { appendToESQLQuery, getStartEndParams } from '@kbn/esql-utils'; import { chunk } from 'lodash'; import pLimit from 'p-limit'; import type { Column } from '../../hooks/esql/use_esql_overall_stats_data'; @@ -69,7 +69,7 @@ const getESQLNumericFieldStatsInChunk = async ({ const numericStatsQuery = '| STATS ' + numericFields.map(({ query }) => query).join(','); const query = appendToESQLQuery(esqlBaseQuery, numericStatsQuery); - const namedParams = getEarliestLatestParams(esqlBaseQuery, timeRange); + const namedParams = getStartEndParams(esqlBaseQuery, timeRange); const request = { params: { query, 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 fb64279019849..2c20902793093 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 13088a0d4e8ec..6b1114a568769 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 b4c8f39c253df..bdae6e0e2853c 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 { }); 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 367fde516ae32..cb72bfd8da245 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 67275a3cf4036..7190a90d56198 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 9ade778c74f31..dd349fca9909e 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/lens/public/datasources/text_based/components/dimension_editor.tsx b/x-pack/plugins/lens/public/datasources/text_based/components/dimension_editor.tsx index d81ad37a22030..9d8d06cee4872 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/components/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/components/dimension_editor.tsx @@ -5,14 +5,14 @@ * 2.0. */ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; -import type { ExpressionsStart, DatatableColumn } from '@kbn/expressions-plugin/public'; +import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; import { fetchFieldsFromESQL } from '@kbn/text-based-editor'; import type { DatasourceDimensionEditorProps, DataType } from '../../../types'; -import { FieldSelect } from './field_select'; +import { FieldSelect, type FieldOptionCompatible } from './field_select'; import type { TextBasedPrivateState } from '../types'; import { isNotNumeric, isNumeric } from '../utils'; @@ -22,7 +22,7 @@ export type TextBasedDimensionEditorProps = }; export function TextBasedDimensionEditor(props: TextBasedDimensionEditorProps) { - const [allColumns, setAllColumns] = useState([]); + const [allColumns, setAllColumns] = useState([]); const query = props.state.layers[props.layerId]?.query; useEffect(() => { @@ -34,43 +34,33 @@ export function TextBasedDimensionEditor(props: TextBasedDimensionEditorProps) { props.expressions ); if (table) { - setAllColumns(table.columns); + const hasNumberTypeColumns = table.columns?.some(isNumeric); + const columns = table.columns.map((col) => { + return { + id: col.id, + name: col.name, + meta: col?.meta ?? { type: 'number' }, + compatible: + props.isMetricDimension && hasNumberTypeColumns + ? props.filterOperations({ + dataType: col?.meta?.type as DataType, + isBucketed: Boolean(isNotNumeric(col)), + scale: 'ordinal', + }) + : true, + }; + }); + setAllColumns(columns); } } } fetchColumns(); - }, [props.expressions, query]); - - const hasNumberTypeColumns = allColumns?.some(isNumeric); - const fields = useMemo(() => { - return allColumns.map((col) => { - return { - id: col.id, - name: col.name, - meta: col?.meta ?? { type: 'number' }, - compatible: - props.isMetricDimension && hasNumberTypeColumns - ? props.filterOperations({ - dataType: col?.meta?.type as DataType, - isBucketed: Boolean(isNotNumeric(col)), - scale: 'ordinal', - }) - : true, - }; - }); - }, [allColumns, hasNumberTypeColumns, props]); + }, [props, props.expressions, query]); const selectedField = useMemo(() => { - const field = fields?.find((column) => column.id === props.columnId); - if (field) { - return { - fieldName: field.name, - meta: field.meta, - columnId: field.id, - }; - } - return undefined; - }, [fields, props.columnId]); + const layerColumns = props.state.layers[props.layerId].columns; + return layerColumns?.find((column) => column.columnId === props.columnId); + }, [props.columnId, props.layerId, props.state.layers]); return ( <> @@ -83,10 +73,10 @@ export function TextBasedDimensionEditor(props: TextBasedDimensionEditorProps) { className="lnsIndexPatternDimensionEditor--padded" > { - const meta = fields?.find((f) => f.name === choice.field)?.meta; + const meta = allColumns?.find((f) => f.name === choice.field)?.meta; const newColumn = { columnId: props.columnId, fieldName: choice.field, diff --git a/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts b/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts index a93cd76e59f39..a337ec4d040f3 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts +++ b/x-pack/plugins/lens/public/datasources/text_based/utils.test.ts @@ -291,7 +291,7 @@ describe('Text based languages utils', () => { const expressionsMock = expressionsPluginMock.createStartContract(); const updatedState = await getStateFromAggregateQuery( state, - { esql: 'FROM my-fake-index-pattern | WHERE time <= ?latest' }, + { esql: 'FROM my-fake-index-pattern | WHERE time <= ?end' }, { ...dataViewsMock, getIdsWithTitle: jest.fn().mockReturnValue( @@ -361,7 +361,7 @@ describe('Text based languages utils', () => { errors: [], index: '4', query: { - esql: 'FROM my-fake-index-pattern | WHERE time <= ?latest', + esql: 'FROM my-fake-index-pattern | WHERE time <= ?end', }, timeField: 'time', }, diff --git a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx index fa69b4a4db046..593e4010285b9 100644 --- a/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/esql_source/esql_source.tsx @@ -14,8 +14,8 @@ import { Adapters } from '@kbn/inspector-plugin/common/adapters'; import { getIndexPatternFromESQLQuery, getLimitFromESQLQuery, - getEarliestLatestParams, - hasEarliestLatestParams, + getStartEndParams, + hasStartEndParams, } from '@kbn/esql-utils'; import { buildEsQuery } from '@kbn/es-query'; import type { Filter, Query } from '@kbn/es-query'; @@ -117,11 +117,11 @@ export class ESQLSource } getApplyGlobalQuery() { - return this._descriptor.narrowByGlobalSearch || hasEarliestLatestParams(this._descriptor.esql); + return this._descriptor.narrowByGlobalSearch || hasStartEndParams(this._descriptor.esql); } async isTimeAware() { - return this._descriptor.narrowByGlobalTime || hasEarliestLatestParams(this._descriptor.esql); + return this._descriptor.narrowByGlobalTime || hasStartEndParams(this._descriptor.esql); } getApplyGlobalTime() { @@ -214,7 +214,7 @@ export class ESQLSource } } - const namedParams = getEarliestLatestParams(this._descriptor.esql, timeRange); + const namedParams = getStartEndParams(this._descriptor.esql, timeRange); if (namedParams.length) { params.params = namedParams; } diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index d06b793f73499..4e9e452ec35af 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 454c1c5ff3a26..0000000000000 --- 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 42a3c10fc48e4..78c42b5ed0871 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 e9d482344a28e..6404fbe2a48a2 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 433e3d934344f..5beb765f3d623 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 79909414e6f96..1ba63b8c43662 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 a5d72e7a8f3a5..0000000000000 --- 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 a92ec9901d7ef..0000000000000 --- 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 74971bfb90162..0000000000000 --- 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 d07343516963d..0000000000000 --- 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 e811b33e3b572..d3a2785709802 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 2ca1ba45e01cb..0000000000000 --- 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 3f32ba4fd5a00..0000000000000 --- 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 ecc5dec40d99e..b1d84eadc8c22 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 d7e4a92fe5a2a..0000000000000 --- 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 70d0c29eeda4b..92381ec3846bc 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 796d96333f1b2..0a8bef2fb8851 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 8864e99bd1ed3..0000000000000 --- 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 bd324c582455f..0000000000000 --- 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 5983f08bb522b..0000000000000 --- 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 b4387fcf88b3c..0000000000000 --- 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 5fbbdc9289b34..0000000000000 --- 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 c74640c9cdc77..0000000000000 --- 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 c57fb62c15a9b..0000000000000 --- 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 c464cd831784f..0000000000000 --- 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 87707620f0985..0000000000000 --- 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 e5376abd3b3ed..0000000000000 --- 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 399e1fa3a1afb..0000000000000 --- 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 10a9c872e3911..0000000000000 --- 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 54c30fa38a588..0000000000000 --- 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 8444e10d11cfd..0000000000000 --- 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 45a3e6b68a5c3..0000000000000 --- 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 c3d2c826021e0..0000000000000 --- 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 4b2056566ea79..6afd236caf3d6 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 cb3eb321bd3fa..29e2354f7454a 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 b20270266602d..0000000000000 --- 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 0227d44f32305..0000000000000 --- 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 274c649ece9dc..be2bcddfca3e6 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 d22fb49ab6627..0000000000000 --- 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 0b9f78ac7a74f..0000000000000 --- 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 de5b5be193c89..0000000000000 --- 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 c5f755ebcb9e4..0000000000000 --- 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 ffebdf330a3f1..0000000000000 --- 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 2dac89716bcf4..f02b37672545a 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 58e18114f08f9..4f6bbac5df419 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 470e1df81ac27..0000000000000 --- 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 d4ddc993a9fc6..0000000000000 --- 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 8f80e64075657..0000000000000 --- 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 a4c8f894cc430..7ebeee21fd1ed 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 e8401d28c67fb..80d598679930f 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 32510c2d342d0..f616a9b48f61b 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 d470e4e85f1cd..cc3ff5507ec40 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 1a88afcb5542c..0000000000000 --- 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 1ac9948818bcc..0000000000000 --- 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 915813bd70216..c8a91c41cb847 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 4190010301a4e..12f6c5fbd0cbb 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 cacbbd243be7d..5a073684ef23d 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 8c823e2e83606..2125c234765cb 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 0d8b63f8c283a..a24992997649f 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 2469d519ad197..9d2f3f521f6c1 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 1bca12d461111..c035fef5af6e4 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 fb00b73e88ffd..949c957bf83c1 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 6d0ecefe8345d..6e1b2a4d6163f 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 b14e007f36754..0908083a30a18 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 77c6fcc180191..b556f81523fc5 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 4a7ea8e77cc92..b90484e4a795f 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 2e32a8b81f035..0848cbcca15c6 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 123d4662768c1..0000000000000 --- 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 545d12d0851f7..0000000000000 --- 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 cfe1e4ec898c4..0000000000000 --- 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 80d0558f5cf55..0000000000000 --- 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 138714693a796..e47d46fcff1e6 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 d8a20904f1b23..ffe7b2dd9668d 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 fae840426cd0b..8a4f911468e4a 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 0763fc3c969a6..0000000000000 --- 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; -} - -
-