Skip to content

Commit

Permalink
[Security Solution] Add Host/User flyout in One Discover. (elastic#19…
Browse files Browse the repository at this point in the history
…9279)

## Summary

Handles elastic#191998

Follow up work:
  - elastic/security-team#11112
  - elastic#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 <[email protected]>
  • Loading branch information
2 people authored and CAWilson94 committed Dec 12, 2024
1 parent cb7e47d commit a16b11d
Show file tree
Hide file tree
Showing 41 changed files with 728 additions and 81 deletions.
7 changes: 2 additions & 5 deletions packages/kbn-unified-data-table/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -46,10 +46,7 @@ export type DataGridCellValueElementProps = EuiDataGridCellValueElementProps & {
isCompressed?: boolean;
};

export type CustomCellRenderer = Record<
string,
(props: DataGridCellValueElementProps) => ReactElement
>;
export type CustomCellRenderer = Record<string, FunctionComponent<DataGridCellValueElementProps>>;

export interface CustomGridColumnProps {
column: EuiDataGridColumn;
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/discover/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"unifiedSearch",
"unifiedHistogram",
"contentManagement",
"discoverShared"
],
"optionalPlugins": [
"dataVisualizer",
Expand Down Expand Up @@ -59,4 +60,4 @@
"common"
]
}
}
}
2 changes: 2 additions & 0 deletions src/plugins/discover/public/__mocks__/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -250,6 +251,7 @@ export function createDiscoverServicesMock(): DiscoverServices {
profilesManager: profilesManagerMock,
ebtManager: new DiscoverEBTManager(),
setHeaderActionMenu: jest.fn(),
discoverShared: discoverSharedPluginMock.createStartContract().features,
} as unknown as DiscoverServices;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -181,5 +182,6 @@ export const createContextAwarenessMocks = ({
const createProfileProviderServicesMock = () => {
return {
logsContextService: createLogsContextServiceMock(),
discoverShared: discoverSharedPluginMock.createStartContract(),
} as ProfileProviderServices;
};
Original file line number Diff line number Diff line change
@@ -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();
};
Original file line number Diff line number Diff line change
@@ -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 <div data-test-subj="cell-render-feature">{props.columnId}</div>;
};
}
},
};

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<DataGridCellValueElementProps>;
expect(CellRenderer).toBeDefined();
const { getByTestId } = render(<CellRenderer {...mockCellProps} />);
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();
});
});
Original file line number Diff line number Diff line change
@@ -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 <CellRenderer {...props} />;
});
}

return getCellRenderer;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<PropsWithChildren<{}>>;
getCellRenderer?: (
fieldName: string
) => FunctionComponent<DataGridCellValueElementProps> | undefined;
}

const EmptyAppWrapper: FunctionComponent<PropsWithChildren<{}>> = ({ 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<SecurityRootProfileContext>
> = (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 }) => (
<PrevWrapper>
<AppWrapper>{children}</AppWrapper>
</PrevWrapper>
);
},
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,
},
};
},
};
};
4 changes: 2 additions & 2 deletions src/plugins/discover/public/context_awareness/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<PropsWithChildren<{}>>;

/**
* Gets default Discover app state that should be used when the profile is resolved
Expand Down
1 change: 1 addition & 0 deletions src/plugins/discover/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 1 addition & 1 deletion src/plugins/discover/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/discover/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand Down
6 changes: 5 additions & 1 deletion src/plugins/discover_shared/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -38,8 +40,31 @@ export interface ObservabilityCreateSLOFeature {
}) => React.ReactNode;
}

/** **************** Security Solution ****************/

export interface SecuritySolutionCellRendererFeature {
id: 'security-solution-cell-renderer';
getRenderer: () => Promise<
(fieldName: string) => FunctionComponent<DataGridCellValueElementProps> | undefined
>;
}

export interface SecuritySolutionAppWrapperFeature {
id: 'security-solution-app-wrapper';
getWrapper: () => Promise<() => FunctionComponent<PropsWithChildren<{}>>>;
}

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
Expand Down
1 change: 1 addition & 0 deletions src/plugins/discover_shared/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
"kbn_references": [
"@kbn/discover-utils",
"@kbn/core",
"@kbn/unified-data-table",
]
}
3 changes: 2 additions & 1 deletion x-pack/plugins/fleet/server/services/spaces/agent_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
Loading

0 comments on commit a16b11d

Please sign in to comment.