From a16b11db24337186f235ef21221f4573a14fcd6d Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Fri, 29 Nov 2024 14:04:58 +0100 Subject: [PATCH] [Security Solution] Add Host/User flyout in One Discover. (#199279) ## Summary Handles https://github.com/elastic/kibana/issues/191998 Follow up work: - https://github.com/elastic/security-team/issues/11112 - https://github.com/elastic/kibana/issues/196667 This PR add below entity flyouts for below entities in One Discover: - host.name - user.name - source.ip - destination.ip In this PR we re-use the security solution code by making use of below model based on `discover-shared` plugin. ```mermaid flowchart TD discoverShared["Discover Shared"] securitySolution["Security Solution"] discover["Discover"] securitySolution -- "registers Features" --> discoverShared discover -- "consume Features" --> discoverShared ``` ## How to Test >[!Note] >This PR adds `security-root-profile` in One discover which is currently in `experimental mode`. All changes below can only be tested when profile is activated. Profile can activated by adding below lines in `config/kibana.dev.yml` > ```yaml > discover.experimental.enabledProfiles: > - security-root-profile > ``` > 1. As mentioned above, adding above experimental flag in `kibana.dev.yml`. 2. Spin up Security Serverless project and add some alert Data. 3. Navigate to Discover and add columns `host.name` and `user.name` in table. Now `host` and `user` flyouts should be available on clicking `host.name`, `user.name`, `source.ip` & `destination.ip`. 4. Flyout should work without any error. 5. Below things are not working and will be tackled in followup PR : - Security Hover actions - Actions such as `Add to Timeline` or `Add to Case` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-unified-data-table/src/types.ts | 7 +- src/plugins/discover/kibana.jsonc | 3 +- .../discover/public/__mocks__/services.ts | 2 + .../context_awareness/__mocks__/index.tsx | 2 + .../accessors/create_app_wrapper_accessor.ts | 17 +++ .../get_cell_renderer_accessor.test.tsx | 57 ++++++++ .../accessors/get_cell_renderer_accessor.tsx | 28 ++++ .../security_root_profile/profile.tsx | 78 ++++++++--- .../public/context_awareness/types.ts | 4 +- src/plugins/discover/public/index.ts | 1 + src/plugins/discover/public/types.ts | 2 +- src/plugins/discover/tsconfig.json | 2 +- src/plugins/discover_shared/public/index.ts | 6 +- .../services/discover_features/types.ts | 29 +++- src/plugins/discover_shared/tsconfig.json | 1 + .../server/services/spaces/agent_policy.ts | 3 +- .../common/types/timeline/cells/index.ts | 4 + x-pack/plugins/security_solution/kibana.jsonc | 4 +- .../discover/add_to_timeline.ts | 2 +- .../add_to_timeline/lens/add_to_timeline.ts | 3 +- .../discover/copy_to_clipboard.ts | 2 +- .../lens/copy_to_clipboard.ts | 3 +- .../app/actions/filter/discover/filter_in.ts | 2 +- .../app/actions/filter/discover/filter_out.ts | 2 +- .../app/actions/filter/lens/create_action.ts | 3 +- .../public/app/actions/utils.ts | 5 - .../public/common/hooks/is_in_security_app.ts | 25 ++++ .../public/one_discover/app_wrapper/index.tsx | 131 ++++++++++++++++++ .../cell_renderers/cell_renderer.test.tsx | 105 ++++++++++++++ .../cell_renderers/cell_renderers.tsx | 71 ++++++++++ .../one_discover/cell_renderers/index.ts | 8 ++ .../public/one_discover/constants.ts | 8 ++ .../public/one_discover/index.tsx | 9 ++ .../public/one_discover/jest.config.js | 19 +++ .../security_solution/public/plugin.tsx | 86 ++++++++++++ .../body/renderers/host_name.test.tsx | 16 --- .../timeline/body/renderers/host_name.tsx | 19 ++- .../body/renderers/user_name.test.tsx | 16 --- .../timeline/body/renderers/user_name.tsx | 15 +- .../plugins/security_solution/public/types.ts | 2 + .../plugins/security_solution/tsconfig.json | 7 +- 41 files changed, 728 insertions(+), 81 deletions(-) create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/security/accessors/create_app_wrapper_accessor.ts create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/security/accessors/get_cell_renderer_accessor.test.tsx create mode 100644 src/plugins/discover/public/context_awareness/profile_providers/security/accessors/get_cell_renderer_accessor.tsx create mode 100644 x-pack/plugins/security_solution/public/common/hooks/is_in_security_app.ts create mode 100644 x-pack/plugins/security_solution/public/one_discover/app_wrapper/index.tsx create mode 100644 x-pack/plugins/security_solution/public/one_discover/cell_renderers/cell_renderer.test.tsx create mode 100644 x-pack/plugins/security_solution/public/one_discover/cell_renderers/cell_renderers.tsx create mode 100644 x-pack/plugins/security_solution/public/one_discover/cell_renderers/index.ts create mode 100644 x-pack/plugins/security_solution/public/one_discover/constants.ts create mode 100644 x-pack/plugins/security_solution/public/one_discover/index.tsx create mode 100644 x-pack/plugins/security_solution/public/one_discover/jest.config.js diff --git a/packages/kbn-unified-data-table/src/types.ts b/packages/kbn-unified-data-table/src/types.ts index 372cff4dd642d..07cef787dd984 100644 --- a/packages/kbn-unified-data-table/src/types.ts +++ b/packages/kbn-unified-data-table/src/types.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { ReactElement } from 'react'; +import type { FunctionComponent } from 'react'; import type { EuiDataGridCellValueElementProps, EuiDataGridColumn } from '@elastic/eui'; import type { DataTableRecord } from '@kbn/discover-utils/src/types'; import type { DataView } from '@kbn/data-views-plugin/common'; @@ -46,10 +46,7 @@ export type DataGridCellValueElementProps = EuiDataGridCellValueElementProps & { isCompressed?: boolean; }; -export type CustomCellRenderer = Record< - string, - (props: DataGridCellValueElementProps) => ReactElement ->; +export type CustomCellRenderer = Record>; export interface CustomGridColumnProps { column: EuiDataGridColumn; diff --git a/src/plugins/discover/kibana.jsonc b/src/plugins/discover/kibana.jsonc index f605d0ae1df95..55adf229aa676 100644 --- a/src/plugins/discover/kibana.jsonc +++ b/src/plugins/discover/kibana.jsonc @@ -32,6 +32,7 @@ "unifiedSearch", "unifiedHistogram", "contentManagement", + "discoverShared" ], "optionalPlugins": [ "dataVisualizer", @@ -59,4 +60,4 @@ "common" ] } -} \ No newline at end of file +} diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index 94a3249bdf271..7b0108c99245a 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -47,6 +47,7 @@ import { urlTrackerMock } from './url_tracker.mock'; import { createElement } from 'react'; import { createContextAwarenessMocks } from '../context_awareness/__mocks__'; import { DiscoverEBTManager } from '../services/discover_ebt_manager'; +import { discoverSharedPluginMock } from '@kbn/discover-shared-plugin/public/mocks'; export function createDiscoverServicesMock(): DiscoverServices { const dataPlugin = dataPluginMock.createStartContract(); @@ -250,6 +251,7 @@ export function createDiscoverServicesMock(): DiscoverServices { profilesManager: profilesManagerMock, ebtManager: new DiscoverEBTManager(), setHeaderActionMenu: jest.fn(), + discoverShared: discoverSharedPluginMock.createStartContract().features, } as unknown as DiscoverServices; } diff --git a/src/plugins/discover/public/context_awareness/__mocks__/index.tsx b/src/plugins/discover/public/context_awareness/__mocks__/index.tsx index 8fb4a0bd769aa..ab179a87778a3 100644 --- a/src/plugins/discover/public/context_awareness/__mocks__/index.tsx +++ b/src/plugins/discover/public/context_awareness/__mocks__/index.tsx @@ -25,6 +25,7 @@ import { ProfileProviderServices } from '../profile_providers/profile_provider_s import { ProfilesManager } from '../profiles_manager'; import { DiscoverEBTManager } from '../../services/discover_ebt_manager'; import { createLogsContextServiceMock } from '@kbn/discover-utils/src/__mocks__'; +import { discoverSharedPluginMock } from '@kbn/discover-shared-plugin/public/mocks'; export const createContextAwarenessMocks = ({ shouldRegisterProviders = true, @@ -181,5 +182,6 @@ export const createContextAwarenessMocks = ({ const createProfileProviderServicesMock = () => { return { logsContextService: createLogsContextServiceMock(), + discoverShared: discoverSharedPluginMock.createStartContract(), } as ProfileProviderServices; }; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/security/accessors/create_app_wrapper_accessor.ts b/src/plugins/discover/public/context_awareness/profile_providers/security/accessors/create_app_wrapper_accessor.ts new file mode 100644 index 0000000000000..42382f088b7a3 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/security/accessors/create_app_wrapper_accessor.ts @@ -0,0 +1,17 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { SecuritySolutionAppWrapperFeature } from '@kbn/discover-shared-plugin/public'; + +export const createAppWrapperAccessor = async ( + appWrapperFeature?: SecuritySolutionAppWrapperFeature +) => { + if (!appWrapperFeature) return undefined; + return appWrapperFeature.getWrapper(); +}; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/security/accessors/get_cell_renderer_accessor.test.tsx b/src/plugins/discover/public/context_awareness/profile_providers/security/accessors/get_cell_renderer_accessor.test.tsx new file mode 100644 index 0000000000000..9774bafdb69b3 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/security/accessors/get_cell_renderer_accessor.test.tsx @@ -0,0 +1,57 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import type { SecuritySolutionCellRendererFeature } from '@kbn/discover-shared-plugin/public'; +import { DataGridCellValueElementProps } from '@kbn/unified-data-table'; +import { createCellRendererAccessor } from './get_cell_renderer_accessor'; +import { render } from '@testing-library/react'; + +const cellRendererFeature: SecuritySolutionCellRendererFeature = { + id: 'security-solution-cell-renderer', + getRenderer: async () => (fieldName: string) => { + if (fieldName === 'host.name') { + return (props: DataGridCellValueElementProps) => { + return
{props.columnId}
; + }; + } + }, +}; + +const mockCellProps = { + columnId: 'host.name', + row: { + id: '1', + raw: {}, + flattened: {}, + }, +} as DataGridCellValueElementProps; + +describe('getCellRendererAccessort', () => { + it('should return a cell renderer', async () => { + const getCellRenderer = await createCellRendererAccessor(cellRendererFeature); + expect(getCellRenderer).toBeDefined(); + const CellRenderer = getCellRenderer?.('host.name') as React.FC; + expect(CellRenderer).toBeDefined(); + const { getByTestId } = render(); + expect(getByTestId('cell-render-feature')).toBeVisible(); + expect(getByTestId('cell-render-feature')).toHaveTextContent('host.name'); + }); + + it('should return undefined if cellRendererFeature is not defined', async () => { + const getCellRenderer = await createCellRendererAccessor(); + expect(getCellRenderer).toBeUndefined(); + }); + + it('should return undefined if cellRendererGetter returns undefined', async () => { + const getCellRenderer = await createCellRendererAccessor(cellRendererFeature); + const cellRenderer = getCellRenderer?.('user.name'); + expect(cellRenderer).toBeUndefined(); + }); +}); diff --git a/src/plugins/discover/public/context_awareness/profile_providers/security/accessors/get_cell_renderer_accessor.tsx b/src/plugins/discover/public/context_awareness/profile_providers/security/accessors/get_cell_renderer_accessor.tsx new file mode 100644 index 0000000000000..9f1d18d4a4d90 --- /dev/null +++ b/src/plugins/discover/public/context_awareness/profile_providers/security/accessors/get_cell_renderer_accessor.tsx @@ -0,0 +1,28 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import type { SecuritySolutionCellRendererFeature } from '@kbn/discover-shared-plugin/public'; +import { DataGridCellValueElementProps } from '@kbn/unified-data-table'; + +export const createCellRendererAccessor = async ( + cellRendererFeature?: SecuritySolutionCellRendererFeature +) => { + if (!cellRendererFeature) return undefined; + const cellRendererGetter = await cellRendererFeature.getRenderer(); + function getCellRenderer(fieldName: string) { + const CellRenderer = cellRendererGetter(fieldName); + if (!CellRenderer) return undefined; + return React.memo(function SecuritySolutionCellRenderer(props: DataGridCellValueElementProps) { + return ; + }); + } + + return getCellRenderer; +}; diff --git a/src/plugins/discover/public/context_awareness/profile_providers/security/security_root_profile/profile.tsx b/src/plugins/discover/public/context_awareness/profile_providers/security/security_root_profile/profile.tsx index 602879125a331..572c86a0e515b 100644 --- a/src/plugins/discover/public/context_awareness/profile_providers/security/security_root_profile/profile.tsx +++ b/src/plugins/discover/public/context_awareness/profile_providers/security/security_root_profile/profile.tsx @@ -7,25 +7,71 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import React, { FunctionComponent, PropsWithChildren } from 'react'; +import { DataGridCellValueElementProps } from '@kbn/unified-data-table'; import { RootProfileProvider, SolutionType } from '../../../profiles'; import { ProfileProviderServices } from '../../profile_provider_services'; import { SecurityProfileProviderFactory } from '../types'; +import { createCellRendererAccessor } from '../accessors/get_cell_renderer_accessor'; +import { createAppWrapperAccessor } from '../accessors/create_app_wrapper_accessor'; + +interface SecurityRootProfileContext { + appWrapper?: FunctionComponent>; + getCellRenderer?: ( + fieldName: string + ) => FunctionComponent | undefined; +} + +const EmptyAppWrapper: FunctionComponent> = ({ children }) => <>{children}; export const createSecurityRootProfileProvider: SecurityProfileProviderFactory< - RootProfileProvider -> = (services: ProfileProviderServices) => ({ - profileId: 'security-root-profile', - isExperimental: true, - profile: { - getCellRenderers: (prev) => (params) => ({ - ...prev(params), - }), - }, - resolve: (params) => { - if (params.solutionNavId === SolutionType.Security) { - return { isMatch: true, context: { solutionType: SolutionType.Security } }; - } + RootProfileProvider +> = (services: ProfileProviderServices) => { + const { discoverShared } = services; + const discoverFeaturesRegistry = discoverShared.features.registry; + const cellRendererFeature = discoverFeaturesRegistry.getById('security-solution-cell-renderer'); + const appWrapperFeature = discoverFeaturesRegistry.getById('security-solution-app-wrapper'); + + return { + profileId: 'security-root-profile', + isExperimental: true, + profile: { + getRenderAppWrapper: (PrevWrapper, params) => { + const AppWrapper = params.context.appWrapper ?? EmptyAppWrapper; + return ({ children }) => ( + + {children} + + ); + }, + getCellRenderers: + (prev, { context }) => + (params) => { + const entries = prev(params); + ['host.name', 'user.name', 'source.ip', 'destination.ip'].forEach((fieldName) => { + entries[fieldName] = context.getCellRenderer?.(fieldName) ?? entries[fieldName]; + }); + return entries; + }, + }, + resolve: async (params) => { + if (params.solutionNavId !== SolutionType.Security) { + return { + isMatch: false, + }; + } + + const getAppWrapper = await createAppWrapperAccessor(appWrapperFeature); + const getCellRenderer = await createCellRendererAccessor(cellRendererFeature); - return { isMatch: false }; - }, -}); + return { + isMatch: true, + context: { + solutionType: SolutionType.Security, + appWrapper: getAppWrapper?.(), + getCellRenderer, + }, + }; + }, + }; +}; diff --git a/src/plugins/discover/public/context_awareness/types.ts b/src/plugins/discover/public/context_awareness/types.ts index 51034e97155b6..70e40df3f8f63 100644 --- a/src/plugins/discover/public/context_awareness/types.ts +++ b/src/plugins/discover/public/context_awareness/types.ts @@ -20,7 +20,7 @@ import type { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import type { OmitIndexSignature } from 'type-fest'; import type { Trigger } from '@kbn/ui-actions-plugin/public'; -import type { PropsWithChildren, ReactElement } from 'react'; +import type { FunctionComponent, PropsWithChildren } from 'react'; import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import type { DiscoverDataSource } from '../../common/data_sources'; import type { DiscoverAppState } from '../application/main/state_management/discover_app_state_container'; @@ -268,7 +268,7 @@ export interface Profile { * @param props The app wrapper props * @returns The custom app wrapper component */ - getRenderAppWrapper: (props: PropsWithChildren<{}>) => ReactElement; + getRenderAppWrapper: FunctionComponent>; /** * Gets default Discover app state that should be used when the profile is resolved diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index b5d4308010f1f..5555b6a787987 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -38,3 +38,4 @@ export { } from './embeddable'; export { loadSharingDataHelpers } from './utils'; export { LogsExplorerTabs, type LogsExplorerTabsProps } from './components/logs_explorer_tabs'; +export type { DiscoverServices } from './build_services'; diff --git a/src/plugins/discover/public/types.ts b/src/plugins/discover/public/types.ts index 2ef380db98703..4b16e3e58df7c 100644 --- a/src/plugins/discover/public/types.ts +++ b/src/plugins/discover/public/types.ts @@ -42,7 +42,7 @@ import type { AiopsPluginStart } from '@kbn/aiops-plugin/public'; import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public'; import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; -import { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; +import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; import { DiscoverAppLocator } from '../common'; import { DiscoverCustomizationContext } from './customizations'; import { type DiscoverContainerProps } from './components/discover_container'; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 1bb3aa10acce0..36655983db13a 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -95,9 +95,9 @@ "@kbn/presentation-containers", "@kbn/observability-ai-assistant-plugin", "@kbn/fields-metadata-plugin", + "@kbn/discover-contextual-components", "@kbn/logs-data-access-plugin", "@kbn/core-lifecycle-browser", - "@kbn/discover-contextual-components", "@kbn/esql-ast", "@kbn/discover-shared-plugin" ], diff --git a/src/plugins/discover_shared/public/index.ts b/src/plugins/discover_shared/public/index.ts index f58a9eaf44f84..4be7a75c817a8 100644 --- a/src/plugins/discover_shared/public/index.ts +++ b/src/plugins/discover_shared/public/index.ts @@ -17,5 +17,9 @@ export type { DiscoverSharedPublicSetup, DiscoverSharedPublicStart } from './typ export type { ObservabilityLogsAIAssistantFeatureRenderDeps, ObservabilityLogsAIAssistantFeature, + SecuritySolutionCellRendererFeature, + SecuritySolutionAppWrapperFeature, DiscoverFeature, -} from './services/discover_features'; + DiscoverFeaturesServiceSetup, + DiscoverFeaturesServiceStart, +} from './services/discover_features/types'; diff --git a/src/plugins/discover_shared/public/services/discover_features/types.ts b/src/plugins/discover_shared/public/services/discover_features/types.ts index cdf78b3335507..a40a4f87a3eb9 100644 --- a/src/plugins/discover_shared/public/services/discover_features/types.ts +++ b/src/plugins/discover_shared/public/services/discover_features/types.ts @@ -7,7 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { DataTableRecord } from '@kbn/discover-utils'; +import type { DataTableRecord } from '@kbn/discover-utils'; +import type { FunctionComponent, PropsWithChildren } from 'react'; +import type { DataGridCellValueElementProps } from '@kbn/unified-data-table'; import { FeaturesRegistry } from '../../../common'; /** @@ -38,8 +40,31 @@ export interface ObservabilityCreateSLOFeature { }) => React.ReactNode; } +/** **************** Security Solution ****************/ + +export interface SecuritySolutionCellRendererFeature { + id: 'security-solution-cell-renderer'; + getRenderer: () => Promise< + (fieldName: string) => FunctionComponent | undefined + >; +} + +export interface SecuritySolutionAppWrapperFeature { + id: 'security-solution-app-wrapper'; + getWrapper: () => Promise<() => FunctionComponent>>; +} + +export type SecuritySolutionFeature = + | SecuritySolutionCellRendererFeature + | SecuritySolutionAppWrapperFeature; + +/** ****************************************************************************************/ + // This should be a union of all the available client features. -export type DiscoverFeature = ObservabilityLogsAIAssistantFeature | ObservabilityCreateSLOFeature; +export type DiscoverFeature = + | ObservabilityLogsAIAssistantFeature + | ObservabilityCreateSLOFeature + | SecuritySolutionFeature; /** * Service types diff --git a/src/plugins/discover_shared/tsconfig.json b/src/plugins/discover_shared/tsconfig.json index 9d2b07eb7aae9..d8bda5214c747 100644 --- a/src/plugins/discover_shared/tsconfig.json +++ b/src/plugins/discover_shared/tsconfig.json @@ -13,5 +13,6 @@ "kbn_references": [ "@kbn/discover-utils", "@kbn/core", + "@kbn/unified-data-table", ] } diff --git a/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts b/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts index e123ca4426654..50f20443c3262 100644 --- a/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/spaces/agent_policy.ts @@ -23,9 +23,10 @@ import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; import { packagePolicyService } from '../package_policy'; import { FleetError, HostedAgentPolicyRestrictionRelatedError } from '../../errors'; -import { isSpaceAwarenessEnabled } from './helpers'; import type { UninstallTokenSOAttributes } from '../security/uninstall_token_service'; +import { isSpaceAwarenessEnabled } from './helpers'; + export async function updateAgentPolicySpaces({ agentPolicyId, currentSpaceId, diff --git a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts index 8435e6ec89845..91c426c24dc78 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts @@ -13,6 +13,10 @@ import type { BrowserFields, TimelineNonEcsData } from '../../../search_strategy /** The following props are provided to the function called by `renderCellValue` */ export type CellValueElementProps = EuiDataGridCellValueElementProps & { + /** + * makes sure that field is not rendered as a plain text + * but according to the renderer. + */ asPlainText?: boolean; browserFields?: BrowserFields; data: TimelineNonEcsData[]; diff --git a/x-pack/plugins/security_solution/kibana.jsonc b/x-pack/plugins/security_solution/kibana.jsonc index 0e713bc095888..f672378c88df8 100644 --- a/x-pack/plugins/security_solution/kibana.jsonc +++ b/x-pack/plugins/security_solution/kibana.jsonc @@ -25,7 +25,6 @@ "dashboard", "data", "dataViews", - "discover", "ecsDataQualityDashboard", "elasticAssistant", "embeddable", @@ -59,7 +58,8 @@ "unifiedDocViewer", "charts", "entityManager", - "inference" + "inference", + "discoverShared" ], "optionalPlugins": [ "encryptedSavedObjects", diff --git a/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.ts b/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.ts index 429d1e1f9db2a..27b309b2acee4 100644 --- a/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.ts +++ b/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/discover/add_to_timeline.ts @@ -6,8 +6,8 @@ */ import type { CellAction, CellActionFactory } from '@kbn/cell-actions'; +import { isInSecurityApp } from '../../../../common/hooks/is_in_security_app'; import type { SecurityAppStore } from '../../../../common/store'; -import { isInSecurityApp } from '../../utils'; import type { StartServices } from '../../../../types'; import { createAddToTimelineCellActionFactory } from '../cell_action/add_to_timeline'; diff --git a/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts b/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts index 3ccbd30efd614..8792d2a6004f5 100644 --- a/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts +++ b/x-pack/plugins/security_solution/public/app/actions/add_to_timeline/lens/add_to_timeline.ts @@ -10,12 +10,13 @@ import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public'; import { createAction } from '@kbn/ui-actions-plugin/public'; import { apiPublishesUnifiedSearch } from '@kbn/presentation-publishing'; import { isLensApi } from '@kbn/lens-plugin/public'; +import { isInSecurityApp } from '../../../../common/hooks/is_in_security_app'; import { KibanaServices } from '../../../../common/lib/kibana'; import type { SecurityAppStore } from '../../../../common/store/types'; import { addProvider } from '../../../../timelines/store/actions'; import type { DataProvider } from '../../../../../common/types'; import { EXISTS_OPERATOR, TimelineId } from '../../../../../common/types'; -import { fieldHasCellActions, isInSecurityApp } from '../../utils'; +import { fieldHasCellActions } from '../../utils'; import { ADD_TO_TIMELINE, ADD_TO_TIMELINE_FAILED_TEXT, diff --git a/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/discover/copy_to_clipboard.ts b/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/discover/copy_to_clipboard.ts index 7a2c39717b342..ba92d46e6eb81 100644 --- a/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/discover/copy_to_clipboard.ts +++ b/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/discover/copy_to_clipboard.ts @@ -6,7 +6,7 @@ */ import type { CellAction, CellActionFactory } from '@kbn/cell-actions'; -import { isInSecurityApp } from '../../utils'; +import { isInSecurityApp } from '../../../../common/hooks/is_in_security_app'; import type { StartServices } from '../../../../types'; import { createCopyToClipboardCellActionFactory } from '../cell_action/copy_to_clipboard'; diff --git a/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/lens/copy_to_clipboard.ts b/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/lens/copy_to_clipboard.ts index 8546f0c3260cc..f4c61c1e7bf7b 100644 --- a/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/lens/copy_to_clipboard.ts +++ b/x-pack/plugins/security_solution/public/app/actions/copy_to_clipboard/lens/copy_to_clipboard.ts @@ -9,8 +9,9 @@ import type { CellValueContext, IEmbeddable } from '@kbn/embeddable-plugin/publi import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public'; import { createAction } from '@kbn/ui-actions-plugin/public'; import copy from 'copy-to-clipboard'; +import { isInSecurityApp } from '../../../../common/hooks/is_in_security_app'; import { KibanaServices } from '../../../../common/lib/kibana'; -import { fieldHasCellActions, isCountField, isInSecurityApp, isLensEmbeddable } from '../../utils'; +import { fieldHasCellActions, isCountField, isLensEmbeddable } from '../../utils'; import { COPY_TO_CLIPBOARD, COPY_TO_CLIPBOARD_ICON, COPY_TO_CLIPBOARD_SUCCESS } from '../constants'; export const ACTION_ID = 'embeddable_copyToClipboard'; diff --git a/x-pack/plugins/security_solution/public/app/actions/filter/discover/filter_in.ts b/x-pack/plugins/security_solution/public/app/actions/filter/discover/filter_in.ts index d0cdaed61f8a2..19ae6cf1f1748 100644 --- a/x-pack/plugins/security_solution/public/app/actions/filter/discover/filter_in.ts +++ b/x-pack/plugins/security_solution/public/app/actions/filter/discover/filter_in.ts @@ -6,8 +6,8 @@ */ import type { CellAction, CellActionFactory } from '@kbn/cell-actions'; +import { isInSecurityApp } from '../../../../common/hooks/is_in_security_app'; import type { SecurityAppStore } from '../../../../common/store'; -import { isInSecurityApp } from '../../utils'; import type { StartServices } from '../../../../types'; import { createFilterInCellActionFactory } from '../cell_action/filter_in'; diff --git a/x-pack/plugins/security_solution/public/app/actions/filter/discover/filter_out.ts b/x-pack/plugins/security_solution/public/app/actions/filter/discover/filter_out.ts index 757b2f41d99b5..77e463c5268d4 100644 --- a/x-pack/plugins/security_solution/public/app/actions/filter/discover/filter_out.ts +++ b/x-pack/plugins/security_solution/public/app/actions/filter/discover/filter_out.ts @@ -6,7 +6,7 @@ */ import type { CellActionFactory, CellAction } from '@kbn/cell-actions'; -import { isInSecurityApp } from '../../utils'; +import { isInSecurityApp } from '../../../../common/hooks/is_in_security_app'; import type { SecurityAppStore } from '../../../../common/store'; import type { StartServices } from '../../../../types'; import { createFilterOutCellActionFactory } from '../cell_action/filter_out'; diff --git a/x-pack/plugins/security_solution/public/app/actions/filter/lens/create_action.ts b/x-pack/plugins/security_solution/public/app/actions/filter/lens/create_action.ts index 966efe9590ecc..e264466767287 100644 --- a/x-pack/plugins/security_solution/public/app/actions/filter/lens/create_action.ts +++ b/x-pack/plugins/security_solution/public/app/actions/filter/lens/create_action.ts @@ -16,8 +16,9 @@ import type { CellValueContext, IEmbeddable } from '@kbn/embeddable-plugin/publi import { createAction } from '@kbn/ui-actions-plugin/public'; import { ACTION_INCOMPATIBLE_VALUE_WARNING } from '@kbn/cell-actions/src/actions/translations'; import { i18n } from '@kbn/i18n'; +import { isInSecurityApp } from '../../../../common/hooks/is_in_security_app'; import { timelineSelectors } from '../../../../timelines/store'; -import { fieldHasCellActions, isInSecurityApp, isLensEmbeddable } from '../../utils'; +import { fieldHasCellActions, isLensEmbeddable } from '../../utils'; import { TimelineId } from '../../../../../common/types'; import { DefaultCellActionTypes } from '../../constants'; import type { SecurityAppStore } from '../../../../common/store'; diff --git a/x-pack/plugins/security_solution/public/app/actions/utils.ts b/x-pack/plugins/security_solution/public/app/actions/utils.ts index d857c54d5091f..3da597db60c0e 100644 --- a/x-pack/plugins/security_solution/public/app/actions/utils.ts +++ b/x-pack/plugins/security_solution/public/app/actions/utils.ts @@ -7,7 +7,6 @@ import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { isLensApi } from '@kbn/lens-plugin/public'; import type { Serializable } from '@kbn/utility-types'; -import { APP_UI_ID } from '../../../common/constants'; // All cell actions are disabled for these fields in Security const FIELDS_WITHOUT_CELL_ACTIONS = [ @@ -17,10 +16,6 @@ const FIELDS_WITHOUT_CELL_ACTIONS = [ 'kibana.alert.reason', ]; -export const isInSecurityApp = (currentAppId?: string): boolean => { - return !!currentAppId && currentAppId === APP_UI_ID; -}; - // @TODO: this is a temporary fix. It needs a better refactor on the consumer side here to // adapt to the new Embeddable architecture export const isLensEmbeddable = (embeddable: IEmbeddable): embeddable is IEmbeddable => { diff --git a/x-pack/plugins/security_solution/public/common/hooks/is_in_security_app.ts b/x-pack/plugins/security_solution/public/common/hooks/is_in_security_app.ts new file mode 100644 index 0000000000000..8714c2129bc6b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/is_in_security_app.ts @@ -0,0 +1,25 @@ +/* + * 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 useObservable from 'react-use/lib/useObservable'; +import { useMemo } from 'react'; +import { APP_UI_ID } from '../../../common'; +import { useKibana } from '../lib/kibana'; + +export const isInSecurityApp = (currentAppId?: string): boolean => { + return !!currentAppId && currentAppId === APP_UI_ID; +}; + +export const useIsInSecurityApp = () => { + const { + services: { application }, + } = useKibana(); + + const currentAppId = useObservable(application.currentAppId$); + + return useMemo(() => isInSecurityApp(currentAppId), [currentAppId]); +}; diff --git a/x-pack/plugins/security_solution/public/one_discover/app_wrapper/index.tsx b/x-pack/plugins/security_solution/public/one_discover/app_wrapper/index.tsx new file mode 100644 index 0000000000000..eb5c325475f2c --- /dev/null +++ b/x-pack/plugins/security_solution/public/one_discover/app_wrapper/index.tsx @@ -0,0 +1,131 @@ +/* + * 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 { ExpandableFlyoutProvider } from '@kbn/expandable-flyout'; +import { Provider as ReduxStoreProvider } from 'react-redux'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { KibanaContextProvider, useKibana } from '@kbn/kibana-react-plugin/public'; +import { NavigationProvider } from '@kbn/security-solution-navigation'; +import type { CoreStart } from '@kbn/core/public'; +import type { SecuritySolutionAppWrapperFeature } from '@kbn/discover-shared-plugin/public'; +import type { DiscoverServices } from '@kbn/discover-plugin/public'; +import { CellActionsProvider } from '@kbn/cell-actions'; +import { APP_ID } from '../../../common'; +import { SecuritySolutionFlyout } from '../../flyout'; +import { StatefulEventContext } from '../../common/components/events_viewer/stateful_event_context'; +import type { SecurityAppStore } from '../../common/store'; +import { ReactQueryClientProvider } from '../../common/containers/query_client/query_client_provider'; +import type { StartPluginsDependencies, StartServices } from '../../types'; +import { MlCapabilitiesProvider } from '../../common/components/ml/permissions/ml_capabilities_provider'; +import { UserPrivilegesProvider } from '../../common/components/user_privileges/user_privileges_context'; +import { DiscoverInTimelineContextProvider } from '../../common/components/discover_in_timeline/provider'; +import { UpsellingProvider } from '../../common/components/upselling_provider'; +import { ConsoleManager } from '../../management/components/console'; +import { AssistantProvider } from '../../assistant/provider'; +import { ONE_DISCOVER_SCOPE_ID } from '../constants'; + +export const createSecuritySolutionDiscoverAppWrapperGetter = ({ + core, + services, + plugins, + store, +}: { + core: CoreStart; + services: StartServices; + plugins: StartPluginsDependencies; + /** + * instance of Security App store that should be used in Discover + */ + store: SecurityAppStore; +}) => { + const getSecuritySolutionDiscoverAppWrapper: Awaited< + ReturnType + > = () => { + return function SecuritySolutionDiscoverAppWrapper({ children }) { + const { services: discoverServices } = useKibana(); + const CasesContext = useMemo(() => plugins.cases.ui.getCasesContext(), []); + + const userCasesPermissions = useMemo(() => plugins.cases.helpers.canUseCases([APP_ID]), []); + + /** + * + * Since this component is meant to be used only in the context of Discover, + * these services are appended/overwritten to the existing services object + * provided by the Discover plugin. + * + */ + const securitySolutionServices: StartServices = useMemo( + () => ({ + ...services, + /* Helps with getting correct instance of query, timeFilter and filterManager instances from discover */ + data: discoverServices.data, + }), + [discoverServices] + ); + + const statefulEventContextValue = useMemo( + () => ({ + // timelineId acts as scopeId + timelineID: ONE_DISCOVER_SCOPE_ID, + enableHostDetailsFlyout: true, + /* behaviour similar to query tab */ + tabType: 'query', + enableIpDetailsFlyout: true, + }), + [] + ); + + return ( + + + + + + {/* ^_^ Needed for notes addition */} + + + {/* ^_^ Needed for Cell Actions since it gives errors when CellActionsContext is used */} + + {/* ^_^ Needed for Alert Preview from Expanded Section of Entity Flyout */} + + + + {/* ^_^ Needed for AlertPreview -> Alert Details Flyout Action */} + + {/* ^_^ Needed for AlertPreview -> Alert Details Flyout Action */} + + {/* ^_^ Needed for Add to Timeline action by `useRiskInputActions`*/} + + + {/* vv below context should not be here and should be removed */} + + {children} + + + + + + + + + + + + + + + + ); + }; + }; + + return getSecuritySolutionDiscoverAppWrapper; +}; diff --git a/x-pack/plugins/security_solution/public/one_discover/cell_renderers/cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/one_discover/cell_renderers/cell_renderer.test.tsx new file mode 100644 index 0000000000000..4bb1eec75cc26 --- /dev/null +++ b/x-pack/plugins/security_solution/public/one_discover/cell_renderers/cell_renderer.test.tsx @@ -0,0 +1,105 @@ +/* + * 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 { DefaultCellRenderer } from '../../timelines/components/timeline/cell_rendering/default_cell_renderer'; +import { render } from '@testing-library/react'; +import { getCellRendererForGivenRecord } from './cell_renderers'; +import type { DataGridCellValueElementProps } from '@kbn/unified-data-table'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; +import { dataViewMock } from '@kbn/discover-utils/src/__mocks__'; +import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks'; + +jest.mock('../../timelines/components/timeline/cell_rendering/default_cell_renderer'); + +const DefaultCellRendererMock = DefaultCellRenderer as unknown as jest.Mock; + +/** + * Mocking DefaultCellRenderer here because it will be renderered + * in Discover's environment and context and we cannot test that here in jest. + * + * Actual working of Cell Renderer will be tested in Discover's functional tests + * + * */ +const mockDefaultCellRenderer = jest.fn((props) => { + return
; +}); + +const mockDataView = dataViewMock; +mockDataView.getFieldByName = jest.fn().mockReturnValue({ type: 'string' } as DataViewField); + +describe('getCellRendererForGivenRecord', () => { + beforeEach(() => { + DefaultCellRendererMock.mockImplementation(mockDefaultCellRenderer); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return cell renderer correctly for allowed fields with correct data format', () => { + const cellRenderer = getCellRendererForGivenRecord('host.name'); + expect(cellRenderer).toBeDefined(); + const props: DataGridCellValueElementProps = { + columnId: 'host.name', + isDetails: false, + isExpanded: false, + row: { + id: '1', + raw: {}, + flattened: { + 'host.name': 'host1', + 'user.name': 'user1', + }, + }, + dataView: mockDataView, + setCellProps: jest.fn(), + isExpandable: false, + rowIndex: 0, + colIndex: 0, + fieldFormats: fieldFormatsMock, + closePopover: jest.fn(), + }; + const CellRenderer = cellRenderer as React.FC; + const { getByTestId } = render(); + expect(getByTestId('mocked-default-cell-render')).toBeVisible(); + expect(mockDefaultCellRenderer).toHaveBeenCalledWith( + { + isDraggable: false, + isTimeline: false, + isDetails: false, + data: [ + { field: 'host.name', value: ['host1'] }, + { field: 'user.name', value: ['user1'] }, + ], + eventId: '1', + scopeId: 'one-discover', + linkValues: undefined, + header: { + id: 'host.name', + columnHeaderType: 'not-filtered', + type: 'string', + }, + asPlainText: false, + context: undefined, + rowRenderers: undefined, + ecsData: undefined, + colIndex: 0, + rowIndex: 0, + isExpandable: false, + isExpanded: false, + setCellProps: props.setCellProps, + columnId: 'host.name', + }, + {} + ); + }); + it('should return undefined for non-allowedFields', () => { + const cellRenderer = getCellRendererForGivenRecord('non-allowed-field'); + expect(cellRenderer).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/one_discover/cell_renderers/cell_renderers.tsx b/x-pack/plugins/security_solution/public/one_discover/cell_renderers/cell_renderers.tsx new file mode 100644 index 0000000000000..7ecc73e404160 --- /dev/null +++ b/x-pack/plugins/security_solution/public/one_discover/cell_renderers/cell_renderers.tsx @@ -0,0 +1,71 @@ +/* + * 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 { DataGridCellValueElementProps } from '@kbn/unified-data-table'; +import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common'; +import type { SecuritySolutionCellRendererFeature } from '@kbn/discover-shared-plugin/public'; +import type { ColumnHeaderType } from '../../../common/types'; +import type { Maybe } from '../../../common/search_strategy'; +import { DefaultCellRenderer } from '../../timelines/components/timeline/cell_rendering/default_cell_renderer'; +import { ONE_DISCOVER_SCOPE_ID } from '../constants'; + +export type SecuritySolutionRowCellRendererGetter = Awaited< + ReturnType +>; + +const ALLOWED_DISCOVER_RENDERED_FIELDS = ['host.name', 'user.name', 'source.ip', 'destination.ip']; + +export const getCellRendererForGivenRecord: SecuritySolutionRowCellRendererGetter = ( + fieldName: string +) => { + if (!ALLOWED_DISCOVER_RENDERED_FIELDS.includes(fieldName)) return undefined; + return function UnifiedFieldRenderBySecuritySolution(props: DataGridCellValueElementProps) { + // convert discover data format to timeline data format + const data: TimelineNonEcsData[] = useMemo( + () => + Object.keys(props.row.flattened).map((field) => ({ + field, + value: Array.isArray(props.row.flattened[field]) + ? (props.row.flattened[field] as Maybe) + : ([props.row.flattened[field]] as Maybe), + })), + [props.row.flattened] + ); + + const header = useMemo(() => { + return { + id: props.columnId, + columnHeaderType: 'not-filtered' as ColumnHeaderType, + type: props.dataView.getFieldByName(props.columnId)?.type, + }; + }, [props.columnId, props.dataView]); + + return ( + + ); + }; +}; diff --git a/x-pack/plugins/security_solution/public/one_discover/cell_renderers/index.ts b/x-pack/plugins/security_solution/public/one_discover/cell_renderers/index.ts new file mode 100644 index 0000000000000..2ec3ff99073df --- /dev/null +++ b/x-pack/plugins/security_solution/public/one_discover/cell_renderers/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 { getCellRendererForGivenRecord } from './cell_renderers'; diff --git a/x-pack/plugins/security_solution/public/one_discover/constants.ts b/x-pack/plugins/security_solution/public/one_discover/constants.ts new file mode 100644 index 0000000000000..f4e779c62cc3d --- /dev/null +++ b/x-pack/plugins/security_solution/public/one_discover/constants.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 const ONE_DISCOVER_SCOPE_ID = 'one-discover'; diff --git a/x-pack/plugins/security_solution/public/one_discover/index.tsx b/x-pack/plugins/security_solution/public/one_discover/index.tsx new file mode 100644 index 0000000000000..a7aefd28551bf --- /dev/null +++ b/x-pack/plugins/security_solution/public/one_discover/index.tsx @@ -0,0 +1,9 @@ +/* + * 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 { getCellRendererForGivenRecord } from './cell_renderers'; +export { createSecuritySolutionDiscoverAppWrapperGetter } from './app_wrapper'; diff --git a/x-pack/plugins/security_solution/public/one_discover/jest.config.js b/x-pack/plugins/security_solution/public/one_discover/jest.config.js new file mode 100644 index 0000000000000..7e4552f72e98f --- /dev/null +++ b/x-pack/plugins/security_solution/public/one_discover/jest.config.js @@ -0,0 +1,19 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/one_discover'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/one_discover', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/public/one_discover/**/*.{ts,tsx}', + ], + moduleNameMapper: require('../../server/__mocks__/module_name_map'), +}; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index f933832264247..497b92637dad5 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -21,6 +21,10 @@ import { AppStatus, DEFAULT_APP_CATEGORIES } from '@kbn/core/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { TriggersAndActionsUIPublicPluginSetup } from '@kbn/triggers-actions-ui-plugin/public'; import { uiMetricService } from '@kbn/cloud-security-posture-common/utils/ui_metrics'; +import type { + SecuritySolutionAppWrapperFeature, + SecuritySolutionCellRendererFeature, +} from '@kbn/discover-shared-plugin/public/services/discover_features'; import { getLazyCloudSecurityPosturePliAuthBlockExtension } from './cloud_security_posture/lazy_cloud_security_posture_pli_auth_block_extension'; import { getLazyEndpointAgentTamperProtectionExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_agent_tamper_protection_extension'; import type { @@ -70,6 +74,7 @@ export class Plugin implements IPlugin, + plugins: SetupPlugins + ) { + const { discoverShared } = plugins; + const discoverFeatureRegistry = discoverShared.features.registry; + const cellRendererFeature: SecuritySolutionCellRendererFeature = { + id: 'security-solution-cell-renderer', + getRenderer: async () => { + const { getCellRendererForGivenRecord } = await this.getLazyDiscoverSharedDeps(); + return getCellRendererForGivenRecord; + }, + }; + + const appWrapperFeature: SecuritySolutionAppWrapperFeature = { + id: 'security-solution-app-wrapper', + getWrapper: async () => { + const [coreStart, startPlugins] = await core.getStartServices(); + + const services = await this.services.generateServices(coreStart, startPlugins); + const subPlugins = await this.startSubPlugins(this.storage, coreStart, startPlugins); + const securityStoreForDiscover = await this.getStoreForDiscover( + coreStart, + startPlugins, + subPlugins + ); + + const { createSecuritySolutionDiscoverAppWrapperGetter } = + await this.getLazyDiscoverSharedDeps(); + + return createSecuritySolutionDiscoverAppWrapperGetter({ + core: coreStart, + services, + plugins: startPlugins, + store: securityStoreForDiscover, + }); + }, + }; + + discoverFeatureRegistry.register(cellRendererFeature); + discoverFeatureRegistry.register(appWrapperFeature); + } + + public async getLazyDiscoverSharedDeps() { + /** + * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. + * See https://webpack.js.org/api/module-methods/#magic-comments + */ + return import( + /* webpackChunkName: "one_discover_shared_deps" */ + './one_discover' + ); + } + /** * SubPlugins are the individual building blocks of the Security Solution plugin. * They are lazily instantiated to improve startup time. @@ -311,6 +372,31 @@ export class Plugin implements IPlugin { + if (!this._securityStoreForDiscover) { + const { createStoreFactory } = await this.lazyApplicationDependencies(); + + this._securityStoreForDiscover = await createStoreFactory( + coreStart, + startPlugins, + subPlugins, + this.storage, + this.experimentalFeatures + ); + } + if (startPlugins.timelines) { + startPlugins.timelines.setTimelineEmbeddedStore(this._securityStoreForDiscover); + } + return this._securityStoreForDiscover; + } + private async registerActions( store: SecurityAppStore, history: H.History, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx index 52344857a07c1..a7769069ff197 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx @@ -12,30 +12,14 @@ import { HostName } from './host_name'; import { TestProviders } from '../../../../../common/mock'; import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context'; -import { createTelemetryServiceMock } from '../../../../../common/lib/telemetry/telemetry_service.mock'; import { TableId } from '@kbn/securitysolution-data-table'; import { createExpandableFlyoutApiMock } from '../../../../../common/mock/expandable_flyout'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -const mockedTelemetry = createTelemetryServiceMock(); const mockOpenRightPanel = jest.fn(); jest.mock('@kbn/expandable-flyout'); -jest.mock('../../../../../common/lib/kibana/kibana_react', () => { - return { - useKibana: () => ({ - services: { - application: { - getUrlForApp: jest.fn(), - navigateToApp: jest.fn(), - }, - telemetry: mockedTelemetry, - }, - }), - }; -}); - jest.mock('../../../../../common/components/draggables', () => ({ DefaultDraggable: () =>
, })); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx index 845b826e5866e..41d403b3f2c5b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx @@ -15,6 +15,7 @@ import { HostDetailsLink } from '../../../../../common/components/links'; import { DefaultDraggable } from '../../../../../common/components/draggables'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { TruncatableText } from '../../../../../common/components/truncatable_text'; +import { useIsInSecurityApp } from '../../../../../common/hooks/is_in_security_app'; interface Props { contextId: string; @@ -45,6 +46,8 @@ const HostNameComponent: React.FC = ({ }) => { const { openRightPanel } = useExpandableFlyoutApi(); + const isInSecurityApp = useIsInSecurityApp(); + const eventContext = useContext(StatefulEventContext); const hostName = `${value}`; const isInTimelineContext = @@ -58,6 +61,10 @@ const HostNameComponent: React.FC = ({ onClick(); } + /* + * if and only if renderer is running inside security solution app + * we check for event and timeline context + * */ if (!eventContext || !isInTimelineContext) { return; } @@ -85,13 +92,21 @@ const HostNameComponent: React.FC = ({ Component={Component} hostName={hostName} isButton={isButton} - onClick={isInTimelineContext ? openHostDetailsSidePanel : undefined} + onClick={isInTimelineContext || !isInSecurityApp ? openHostDetailsSidePanel : undefined} title={title} > {hostName} ), - [Component, hostName, isButton, isInTimelineContext, openHostDetailsSidePanel, title] + [ + Component, + hostName, + isButton, + isInTimelineContext, + openHostDetailsSidePanel, + title, + isInSecurityApp, + ] ); return isString(value) && hostName.length > 0 ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx index 6c3dffc58ce25..bdb53f5850ec3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx @@ -12,30 +12,14 @@ import { TestProviders } from '../../../../../common/mock'; import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; import { UserName } from './user_name'; import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context'; -import { createTelemetryServiceMock } from '../../../../../common/lib/telemetry/telemetry_service.mock'; import { TableId } from '@kbn/securitysolution-data-table'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { createExpandableFlyoutApiMock } from '../../../../../common/mock/expandable_flyout'; -const mockedTelemetry = createTelemetryServiceMock(); const mockOpenRightPanel = jest.fn(); jest.mock('@kbn/expandable-flyout'); -jest.mock('../../../../../common/lib/kibana/kibana_react', () => { - return { - useKibana: () => ({ - services: { - application: { - getUrlForApp: jest.fn(), - navigateToApp: jest.fn(), - }, - telemetry: mockedTelemetry, - }, - }), - }; -}); - jest.mock('../../../../../common/components/draggables', () => ({ DefaultDraggable: () =>
, })); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx index 1f070d52a8de9..31a8424e5ea0c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx @@ -15,6 +15,7 @@ import { DefaultDraggable } from '../../../../../common/components/draggables'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { UserDetailsLink } from '../../../../../common/components/links'; import { TruncatableText } from '../../../../../common/components/truncatable_text'; +import { useIsInSecurityApp } from '../../../../../common/hooks/is_in_security_app'; interface Props { contextId: string; @@ -48,6 +49,8 @@ const UserNameComponent: React.FC = ({ const isInTimelineContext = userName && eventContext?.timelineID; const { openRightPanel } = useExpandableFlyoutApi(); + const isInSecurityApp = useIsInSecurityApp(); + const openUserDetailsSidePanel = useCallback( (e: React.SyntheticEvent) => { e.preventDefault(); @@ -83,13 +86,21 @@ const UserNameComponent: React.FC = ({ Component={Component} userName={userName} isButton={isButton} - onClick={isInTimelineContext ? openUserDetailsSidePanel : undefined} + onClick={isInTimelineContext || !isInSecurityApp ? openUserDetailsSidePanel : undefined} title={title} > {userName} ), - [userName, isButton, isInTimelineContext, openUserDetailsSidePanel, Component, title] + [ + userName, + isButton, + isInTimelineContext, + openUserDetailsSidePanel, + Component, + title, + isInSecurityApp, + ] ); return isString(value) && userName.length > 0 ? ( diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index f4c3cdfc0e4c6..2380c5a7cb08e 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -61,6 +61,7 @@ import type { PluginStartContract } from '@kbn/alerting-plugin/public/plugin'; import type { MapsStartApi } from '@kbn/maps-plugin/public'; import type { IntegrationAssistantPluginStart } from '@kbn/integration-assistant-plugin/public'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; +import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public'; import type { ResolverPluginSetup } from './resolver/types'; import type { Inspect } from '../common/search_strategy'; import type { Detections } from './detections'; @@ -107,6 +108,7 @@ export interface SetupPlugins { ml?: MlPluginSetup; cases?: CasesPublicSetup; data: DataPublicPluginSetup; + discoverShared: DiscoverSharedPublicStart; } /** diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 4a00ef93abe63..4ed7e1cbdd35f 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -15,7 +15,11 @@ "public/**/*.json", "../../../typings/**/*" ], - "exclude": ["target/**/*", "**/cypress/**", "public/management/cypress.config.ts"], + "exclude": [ + "target/**/*", + "**/cypress/**", + "public/management/cypress.config.ts" + ], "kbn_references": [ "@kbn/core", { @@ -228,6 +232,7 @@ "@kbn/core-lifecycle-server", "@kbn/core-user-profile-common", "@kbn/langchain", + "@kbn/discover-shared-plugin", "@kbn/react-hooks", "@kbn/index-adapter", "@kbn/core-http-server-utils"