mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution] expandable flyout - highlighted fields enhancements (#162417)
This commit is contained in:
parent
a44df515f9
commit
8543e8366b
11 changed files with 447 additions and 126 deletions
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { collapseDocumentDetailsExpandableFlyoutLeftSection } from '../../../../tasks/expandable_flyout/alert_details_right_panel';
|
||||
import { DOCUMENT_DETAILS_FLYOUT_INVESTIGATION_TAB_CONTENT } from '../../../../screens/expandable_flyout/alert_details_left_panel_investigation_tab';
|
||||
import {
|
||||
createNewCaseFromExpandableFlyout,
|
||||
|
@ -19,7 +20,6 @@ import {
|
|||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_DETAILS,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_GO_TO_TABLE_LINK,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_HEADER_TITLE,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_CONTENT,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_CORRELATIONS_HEADER,
|
||||
|
@ -42,6 +42,8 @@ import {
|
|||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_SESSION_PREVIEW,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL,
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL,
|
||||
} from '../../../../screens/expandable_flyout/alert_details_right_panel_overview_tab';
|
||||
import {
|
||||
clickCorrelationsViewAllButton,
|
||||
|
@ -61,10 +63,10 @@ import { getNewRule } from '../../../../objects/rule';
|
|||
import { ALERTS_URL } from '../../../../urls/navigation';
|
||||
import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule';
|
||||
import {
|
||||
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_CONTENT,
|
||||
DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_EVENT_TYPE_ROW,
|
||||
} from '../../../../screens/expandable_flyout/alert_details_right_panel_table_tab';
|
||||
import { DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT } from '../../../../screens/expandable_flyout/alert_details_left_panel_entities_tab';
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS,
|
||||
DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS,
|
||||
} from '../../../../screens/expandable_flyout/alert_details_left_panel_entities_tab';
|
||||
|
||||
describe(
|
||||
'Alert details expandable flyout right panel overview tab',
|
||||
|
@ -191,17 +193,29 @@ describe(
|
|||
'be.visible'
|
||||
);
|
||||
|
||||
cy.log('navigate to table tab when clicking on highlighted fields view button');
|
||||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_GO_TO_TABLE_LINK)
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL)
|
||||
.should('be.visible')
|
||||
.click();
|
||||
.and('contain.text', 'host.name');
|
||||
const hostNameCell =
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL('siem-kibana');
|
||||
cy.get(hostNameCell).should('be.visible').and('have.text', 'siem-kibana');
|
||||
|
||||
// the table component is rendered within a dom element with overflow, so Cypress isn't finding it
|
||||
// this next line is a hack that scrolls to a specific element in the table
|
||||
// (in the middle of it vertically) to ensure Cypress finds it
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_EVENT_TYPE_ROW).scrollIntoView();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_CONTENT).should('be.visible');
|
||||
cy.get(hostNameCell).click();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS).scrollIntoView();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_HOST_DETAILS).should('be.visible');
|
||||
|
||||
collapseDocumentDetailsExpandableFlyoutLeftSection();
|
||||
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL)
|
||||
.should('be.visible')
|
||||
.and('contain.text', 'user.name');
|
||||
const userNameCell =
|
||||
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL('test');
|
||||
cy.get(userNameCell).should('be.visible').and('have.text', 'test');
|
||||
|
||||
cy.get(userNameCell).click();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS).scrollIntoView();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_USER_DETAILS).should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ import {
|
|||
ENTITY_PANEL_CONTENT_TEST_ID,
|
||||
ENTITY_PANEL_HEADER_TEST_ID,
|
||||
HIGHLIGHTED_FIELDS_DETAILS_TEST_ID,
|
||||
HIGHLIGHTED_FIELDS_GO_TO_TABLE_LINK,
|
||||
HIGHLIGHTED_FIELDS_TITLE_TEST_ID,
|
||||
INSIGHTS_CORRELATIONS_CONTENT_TEST_ID,
|
||||
INSIGHTS_CORRELATIONS_TITLE_TEST_ID,
|
||||
|
@ -44,6 +43,7 @@ import {
|
|||
REASON_TITLE_TEST_ID,
|
||||
SESSION_PREVIEW_TEST_ID,
|
||||
VISUALIZATIONS_SECTION_HEADER_TEST_ID,
|
||||
HIGHLIGHTED_FIELDS_CELL_TEST_ID,
|
||||
} from '../../../public/flyout/right/components/test_ids';
|
||||
|
||||
/* About section */
|
||||
|
@ -83,8 +83,11 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_HEADER_TITL
|
|||
getDataTestSubjectSelector(HIGHLIGHTED_FIELDS_TITLE_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_DETAILS =
|
||||
getDataTestSubjectSelector(HIGHLIGHTED_FIELDS_DETAILS_TEST_ID);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_GO_TO_TABLE_LINK =
|
||||
getDataTestSubjectSelector(HIGHLIGHTED_FIELDS_GO_TO_TABLE_LINK);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_FIELD_CELL =
|
||||
getDataTestSubjectSelector('fieldCell');
|
||||
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_HIGHLIGHTED_FIELDS_TABLE_VALUE_CELL = (
|
||||
value: string
|
||||
) => getDataTestSubjectSelector(`${value}-${HIGHLIGHTED_FIELDS_CELL_TEST_ID}`);
|
||||
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INVESTIGATION_GUIDE_BUTTON =
|
||||
getDataTestSubjectSelector(INVESTIGATION_GUIDE_BUTTON_TEST_ID);
|
||||
|
||||
|
|
|
@ -22,14 +22,18 @@ import {
|
|||
/**
|
||||
* Expand the left section of the document details expandable flyout by clicking on the expand icon button
|
||||
*/
|
||||
export const expandDocumentDetailsExpandableFlyoutLeftSection = () =>
|
||||
export const expandDocumentDetailsExpandableFlyoutLeftSection = () => {
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_EXPAND_DETAILS_BUTTON).scrollIntoView();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_EXPAND_DETAILS_BUTTON).click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Expand the left section of the document details expandable flyout by clicking on the collapse icon button
|
||||
*/
|
||||
export const collapseDocumentDetailsExpandableFlyoutLeftSection = () =>
|
||||
export const collapseDocumentDetailsExpandableFlyoutLeftSection = () => {
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_COLLAPSE_DETAILS_BUTTON).scrollIntoView();
|
||||
cy.get(DOCUMENT_DETAILS_FLYOUT_COLLAPSE_DETAILS_BUTTON).click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the Overview tab in the document details expandable flyout right section
|
||||
|
|
|
@ -217,7 +217,7 @@ function getFieldsByRuleType(ruleType?: string): EventSummaryField[] {
|
|||
|
||||
/**
|
||||
This function is exported because it is used in the Exception Component to
|
||||
populate the conditions with the Highlighted Fields. Additionally, the new
|
||||
populate the conditions with the Highlighted Fields. Additionally, the new
|
||||
Alert Summary Flyout also requires access to these fields.
|
||||
As the Alert Summary components will undergo changes soon we will go with
|
||||
exporting the function only for now.
|
||||
|
@ -255,7 +255,7 @@ interface EventCategories {
|
|||
* @param data The event details
|
||||
* @returns The event's primary category and all other categories in case there is more than one
|
||||
*/
|
||||
function getEventCategoriesFromData(data: TimelineEventsDetailsItem[]): EventCategories {
|
||||
export function getEventCategoriesFromData(data: TimelineEventsDetailsItem[]): EventCategories {
|
||||
const eventCategoryField = find({ category: 'event', field: 'event.category' }, data);
|
||||
|
||||
let primaryEventCategory: string | undefined;
|
||||
|
|
|
@ -9,97 +9,73 @@ import React from 'react';
|
|||
import { render } from '@testing-library/react';
|
||||
import { RightPanelContext } from '../context';
|
||||
import { HIGHLIGHTED_FIELDS_DETAILS_TEST_ID, HIGHLIGHTED_FIELDS_TITLE_TEST_ID } from './test_ids';
|
||||
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
|
||||
import { HighlightedFields } from './highlighted_fields';
|
||||
import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context';
|
||||
import { useHighlightedFields } from '../hooks/use_highlighted_fields';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
jest.mock('../hooks/use_highlighted_fields');
|
||||
|
||||
describe('<HighlightedFields />', () => {
|
||||
it('should render the component', () => {
|
||||
const mockTheme = getMockTheme({
|
||||
eui: {
|
||||
euiSizeL: '10px',
|
||||
},
|
||||
});
|
||||
const flyoutContextValue = {
|
||||
openRightPanel: jest.fn(),
|
||||
} as unknown as ExpandableFlyoutContext;
|
||||
const panelContextValue = {
|
||||
eventId: 'eventId',
|
||||
indexName: 'indexName',
|
||||
dataFormattedForFieldBrowser: [],
|
||||
browserFields: {},
|
||||
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
|
||||
} as unknown as RightPanelContext;
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue([
|
||||
{
|
||||
field: 'field',
|
||||
description: {
|
||||
field: 'field',
|
||||
values: ['value'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
<HighlightedFields />
|
||||
</RightPanelContext.Provider>
|
||||
</ExpandableFlyoutContext.Provider>
|
||||
</ThemeProvider>
|
||||
<TestProviders>
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
<HighlightedFields />
|
||||
</RightPanelContext.Provider>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId(HIGHLIGHTED_FIELDS_TITLE_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(HIGHLIGHTED_FIELDS_DETAILS_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it(`should render empty component if there aren't any highlighted fields`, () => {
|
||||
const panelContextValue = {
|
||||
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
|
||||
} as unknown as RightPanelContext;
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue([]);
|
||||
|
||||
const { container } = render(
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
<HighlightedFields />
|
||||
</RightPanelContext.Provider>
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render empty component if dataFormattedForFieldBrowser is null', () => {
|
||||
const flyoutContextValue = {} as unknown as ExpandableFlyoutContext;
|
||||
const panelContextValue = {
|
||||
eventId: 'eventId',
|
||||
indexName: 'indexName',
|
||||
dataFormattedForFieldBrowser: null,
|
||||
browserFields: {},
|
||||
} as unknown as RightPanelContext;
|
||||
(useHighlightedFields as jest.Mock).mockReturnValue([
|
||||
{
|
||||
field: 'field',
|
||||
description: {
|
||||
field: 'field',
|
||||
values: ['value'],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { container } = render(
|
||||
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
<HighlightedFields />
|
||||
</RightPanelContext.Provider>
|
||||
</ExpandableFlyoutContext.Provider>
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render empty component if browserFields is null', () => {
|
||||
const flyoutContextValue = {} as unknown as ExpandableFlyoutContext;
|
||||
const panelContextValue = {
|
||||
eventId: 'eventId',
|
||||
indexName: 'indexName',
|
||||
dataFormattedForFieldBrowser: [],
|
||||
browserFields: null,
|
||||
} as unknown as RightPanelContext;
|
||||
|
||||
const { container } = render(
|
||||
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
<HighlightedFields />
|
||||
</RightPanelContext.Provider>
|
||||
</ExpandableFlyoutContext.Provider>
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render empty component if eventId is null', () => {
|
||||
const flyoutContextValue = {} as unknown as ExpandableFlyoutContext;
|
||||
const panelContextValue = {
|
||||
eventId: null,
|
||||
indexName: 'indexName',
|
||||
dataFormattedForFieldBrowser: [],
|
||||
browserFields: {},
|
||||
} as unknown as RightPanelContext;
|
||||
|
||||
const { container } = render(
|
||||
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
<HighlightedFields />
|
||||
</RightPanelContext.Provider>
|
||||
</ExpandableFlyoutContext.Provider>
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
<HighlightedFields />
|
||||
</RightPanelContext.Provider>
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
|
|
|
@ -6,38 +6,61 @@
|
|||
*/
|
||||
|
||||
import type { FC } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import type { EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import { HighlightedFieldsCell } from './highlighted_fields_cell';
|
||||
import {
|
||||
CellActionsMode,
|
||||
SecurityCellActions,
|
||||
SecurityCellActionsTrigger,
|
||||
} from '../../../common/components/cell_actions';
|
||||
import type { UseHighlightedFieldsResult } from '../hooks/use_highlighted_fields';
|
||||
import { HIGHLIGHTED_FIELDS_DETAILS_TEST_ID, HIGHLIGHTED_FIELDS_TITLE_TEST_ID } from './test_ids';
|
||||
import { AlertSummaryView } from '../../../common/components/event_details/alert_summary_view';
|
||||
import { HIGHLIGHTED_FIELDS_TITLE } from './translations';
|
||||
import {
|
||||
HIGHLIGHTED_FIELDS_FIELD_COLUMN,
|
||||
HIGHLIGHTED_FIELDS_TITLE,
|
||||
HIGHLIGHTED_FIELDS_VALUE_COLUMN,
|
||||
} from './translations';
|
||||
import { useRightPanelContext } from '../context';
|
||||
import { RightPanelKey, RightPanelTableTabPath } from '..';
|
||||
import { useHighlightedFields } from '../hooks/use_highlighted_fields';
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<UseHighlightedFieldsResult>> = [
|
||||
{
|
||||
field: 'field',
|
||||
name: HIGHLIGHTED_FIELDS_FIELD_COLUMN,
|
||||
'data-test-subj': 'fieldCell',
|
||||
width: '125px',
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
name: HIGHLIGHTED_FIELDS_VALUE_COLUMN,
|
||||
'data-test-subj': 'valueCell',
|
||||
render: (description: { field: string; values: string[] | null | undefined }) => (
|
||||
<SecurityCellActions
|
||||
data={{
|
||||
field: description.field,
|
||||
value: description.values,
|
||||
}}
|
||||
mode={CellActionsMode.HOVER_RIGHT}
|
||||
triggerId={SecurityCellActionsTrigger.DEFAULT}
|
||||
visibleCellActions={6}
|
||||
>
|
||||
<HighlightedFieldsCell values={description.values} field={description.field} />
|
||||
</SecurityCellActions>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Component that displays the highlighted fields in the right panel under the Investigation section.
|
||||
* It leverages the existing {@link AlertSummaryView} component.
|
||||
* // TODO will require improvements https://github.com/elastic/security-team/issues/6428
|
||||
*/
|
||||
export const HighlightedFields: FC = () => {
|
||||
const { openRightPanel } = useExpandableFlyoutContext();
|
||||
const { eventId, indexName, dataFormattedForFieldBrowser, browserFields, scopeId } =
|
||||
useRightPanelContext();
|
||||
const { dataFormattedForFieldBrowser } = useRightPanelContext();
|
||||
|
||||
const goToTableTab = useCallback(() => {
|
||||
openRightPanel({
|
||||
id: RightPanelKey,
|
||||
path: RightPanelTableTabPath,
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
},
|
||||
});
|
||||
}, [eventId, indexName, openRightPanel, scopeId]);
|
||||
const highlightedFields = useHighlightedFields({ dataFormattedForFieldBrowser });
|
||||
|
||||
if (!dataFormattedForFieldBrowser || !browserFields || !eventId) {
|
||||
if (!dataFormattedForFieldBrowser || highlightedFields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -50,16 +73,7 @@ export const HighlightedFields: FC = () => {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem data-test-subj={HIGHLIGHTED_FIELDS_DETAILS_TEST_ID}>
|
||||
<EuiPanel hasBorder hasShadow={false}>
|
||||
<AlertSummaryView // TODO consider using x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx instead if the link to the table have to be removed
|
||||
data={dataFormattedForFieldBrowser}
|
||||
eventId={eventId}
|
||||
browserFields={browserFields}
|
||||
isDraggable={false}
|
||||
scopeId={scopeId}
|
||||
title={''}
|
||||
isReadOnly={false} // TODO: set properly
|
||||
goToTable={goToTableTab}
|
||||
/>
|
||||
<EuiInMemoryTable items={highlightedFields} columns={columns} compressed />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 {
|
||||
HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID,
|
||||
HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID,
|
||||
HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID,
|
||||
} from './test_ids';
|
||||
import { HighlightedFieldsCell } from './highlighted_fields_cell';
|
||||
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
|
||||
import { RightPanelContext } from '../context';
|
||||
import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
||||
const flyoutContextValue = {
|
||||
openLeftPanel: jest.fn(),
|
||||
} as unknown as ExpandableFlyoutContext;
|
||||
const panelContextValue = {
|
||||
eventId: 'event id',
|
||||
indexName: 'indexName',
|
||||
scopeId: 'scopeId',
|
||||
} as unknown as RightPanelContext;
|
||||
|
||||
const renderHighlightedFieldsCell = (values: string[], field: string) =>
|
||||
render(
|
||||
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
|
||||
<RightPanelContext.Provider value={panelContextValue}>
|
||||
<HighlightedFieldsCell values={values} field={field} />
|
||||
</RightPanelContext.Provider>
|
||||
</ExpandableFlyoutContext.Provider>
|
||||
);
|
||||
|
||||
describe('<HighlightedFieldsCell />', () => {
|
||||
it('should render a basic cell', () => {
|
||||
const { getByTestId } = render(<HighlightedFieldsCell values={['value']} field={'field'} />);
|
||||
|
||||
expect(getByTestId(HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a link cell if field is host.name', () => {
|
||||
const { getByTestId } = renderHighlightedFieldsCell(['value'], 'host.name');
|
||||
|
||||
expect(getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a link cell if field is user.name', () => {
|
||||
const { getByTestId } = renderHighlightedFieldsCell(['value'], 'user.name');
|
||||
|
||||
expect(getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open left panel when clicking on the link within a a link cell', () => {
|
||||
const { getByTestId } = renderHighlightedFieldsCell(['value'], 'user.name');
|
||||
|
||||
getByTestId(HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID).click();
|
||||
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
|
||||
id: LeftPanelKey,
|
||||
path: LeftPanelInsightsTabPath,
|
||||
params: {
|
||||
id: panelContextValue.eventId,
|
||||
indexName: panelContextValue.indexName,
|
||||
scopeId: panelContextValue.scopeId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should render agent status cell if field is agent.status', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<HighlightedFieldsCell values={['value']} field={'agent.status'} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId(HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render agent status component if override field is agent.status', () => {
|
||||
const { container } = render(<HighlightedFieldsCell values={null} field={'field'} />);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { VFC } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiFlexItem, EuiLink } from '@elastic/eui';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { EndpointAgentStatusById } from '../../../common/components/endpoint/endpoint_agent_status';
|
||||
import { useRightPanelContext } from '../context';
|
||||
import {
|
||||
AGENT_STATUS_FIELD_NAME,
|
||||
HOST_NAME_FIELD_NAME,
|
||||
USER_NAME_FIELD_NAME,
|
||||
} from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left';
|
||||
import {
|
||||
HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID,
|
||||
HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID,
|
||||
HIGHLIGHTED_FIELDS_CELL_TEST_ID,
|
||||
HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID,
|
||||
} from './test_ids';
|
||||
|
||||
interface LinkFieldCellProps {
|
||||
/**
|
||||
* Highlighted field's value to display as a EuiLink to open the expandable left panel
|
||||
* (used for host name and username fields)
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* // Currently we can use the same component for both host name and username
|
||||
*/
|
||||
const LinkFieldCell: VFC<LinkFieldCellProps> = ({ value }) => {
|
||||
const { scopeId, eventId, indexName } = useRightPanelContext();
|
||||
const { openLeftPanel } = useExpandableFlyoutContext();
|
||||
|
||||
const goToInsightsEntities = useCallback(() => {
|
||||
openLeftPanel({
|
||||
id: LeftPanelKey,
|
||||
path: LeftPanelInsightsTabPath,
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
},
|
||||
});
|
||||
}, [eventId, indexName, openLeftPanel, scopeId]);
|
||||
|
||||
return (
|
||||
<EuiLink onClick={goToInsightsEntities} data-test-subj={HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID}>
|
||||
{value}
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
export interface HighlightedFieldsCellProps {
|
||||
/**
|
||||
* Highlighted field's name used to know what component to display
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* Highlighted field's value to display
|
||||
*/
|
||||
values: string[] | null | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a component in the highlighted fields table cell based on the field name
|
||||
*/
|
||||
export const HighlightedFieldsCell: VFC<HighlightedFieldsCellProps> = ({ values, field }) => (
|
||||
<>
|
||||
{values != null &&
|
||||
values.map((value, i) => {
|
||||
return (
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key={`${i}-${value}`}
|
||||
data-test-subj={`${value}-${HIGHLIGHTED_FIELDS_CELL_TEST_ID}`}
|
||||
>
|
||||
{field === HOST_NAME_FIELD_NAME || field === USER_NAME_FIELD_NAME ? (
|
||||
<LinkFieldCell value={value} />
|
||||
) : field === AGENT_STATUS_FIELD_NAME ? (
|
||||
<EndpointAgentStatusById
|
||||
endpointAgentId={String(value ?? '')}
|
||||
data-test-subj={HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID}
|
||||
/>
|
||||
) : (
|
||||
<span data-test-subj={HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID}>{value}</span>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
|
@ -53,7 +53,15 @@ export const HIGHLIGHTED_FIELDS_TITLE_TEST_ID =
|
|||
'securitySolutionDocumentDetailsFlyoutHighlightedFieldsTitle';
|
||||
export const HIGHLIGHTED_FIELDS_DETAILS_TEST_ID =
|
||||
'securitySolutionDocumentDetailsFlyoutHighlightedFieldsDetails';
|
||||
export const HIGHLIGHTED_FIELDS_GO_TO_TABLE_LINK = 'summary-view-go-to-table-link';
|
||||
export const HIGHLIGHTED_FIELDS_CELL_TEST_ID =
|
||||
'securitySolutionDocumentDetailsFlyoutHighlightedFieldsCell';
|
||||
export const HIGHLIGHTED_FIELDS_BASIC_CELL_TEST_ID =
|
||||
'securitySolutionDocumentDetailsFlyoutHighlightedFieldsBasicCell';
|
||||
export const HIGHLIGHTED_FIELDS_LINKED_CELL_TEST_ID =
|
||||
'securitySolutionDocumentDetailsFlyoutHighlightedFieldsLinkedCell';
|
||||
export const HIGHLIGHTED_FIELDS_AGENT_STATUS_CELL_TEST_ID =
|
||||
'securitySolutionDocumentDetailsFlyoutHighlightedFieldsAgentStatusCell';
|
||||
|
||||
export const INVESTIGATION_GUIDE_BUTTON_TEST_ID =
|
||||
'securitySolutionDocumentDetailsFlyoutInvestigationGuideButton';
|
||||
|
||||
|
|
|
@ -120,6 +120,16 @@ export const HIGHLIGHTED_FIELDS_TITLE = i18n.translate(
|
|||
{ defaultMessage: 'Highlighted fields' }
|
||||
);
|
||||
|
||||
export const HIGHLIGHTED_FIELDS_FIELD_COLUMN = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.highlightedFields.fieldColumn',
|
||||
{ defaultMessage: 'Field' }
|
||||
);
|
||||
|
||||
export const HIGHLIGHTED_FIELDS_VALUE_COLUMN = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.highlightedFields.valueColumn',
|
||||
{ defaultMessage: 'Value' }
|
||||
);
|
||||
|
||||
/* Insights section */
|
||||
|
||||
export const ENTITIES_TITLE = i18n.translate(
|
||||
|
|
|
@ -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 type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import { find, isEmpty } from 'lodash/fp';
|
||||
import { ALERT_RULE_TYPE } from '@kbn/rule-data-utils';
|
||||
import { isAlertFromEndpointEvent } from '../../../common/utils/endpoint_alert_check';
|
||||
import {
|
||||
getEventCategoriesFromData,
|
||||
getEventFieldsToDisplay,
|
||||
} from '../../../common/components/event_details/get_alert_summary_rows';
|
||||
|
||||
export interface UseHighlightedFieldsParams {
|
||||
/**
|
||||
* An array of field objects with category and value
|
||||
*/
|
||||
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null;
|
||||
}
|
||||
|
||||
export interface UseHighlightedFieldsResult {
|
||||
/**
|
||||
* Highlighted field name (label or if null, falls back to id)
|
||||
*/
|
||||
field: string;
|
||||
description: {
|
||||
/**
|
||||
* Highlighted field name (overrideField or if null, falls back to id)
|
||||
*/
|
||||
field: string;
|
||||
/**
|
||||
* Highlighted field value
|
||||
*/
|
||||
values: string[] | null | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the highlighted fields to display in the right panel under the Investigation section.
|
||||
*/
|
||||
export const useHighlightedFields = ({
|
||||
dataFormattedForFieldBrowser,
|
||||
}: UseHighlightedFieldsParams): UseHighlightedFieldsResult[] => {
|
||||
if (!dataFormattedForFieldBrowser) return [];
|
||||
|
||||
const eventCategories = getEventCategoriesFromData(dataFormattedForFieldBrowser);
|
||||
|
||||
const eventCodeField = find(
|
||||
{ category: 'event', field: 'event.code' },
|
||||
dataFormattedForFieldBrowser
|
||||
);
|
||||
|
||||
const eventCode = Array.isArray(eventCodeField?.originalValue)
|
||||
? eventCodeField?.originalValue?.[0]
|
||||
: eventCodeField?.originalValue;
|
||||
|
||||
const eventRuleTypeField = find(
|
||||
{ category: 'kibana', field: ALERT_RULE_TYPE },
|
||||
dataFormattedForFieldBrowser
|
||||
);
|
||||
const eventRuleType = Array.isArray(eventRuleTypeField?.originalValue)
|
||||
? eventRuleTypeField?.originalValue?.[0]
|
||||
: eventRuleTypeField?.originalValue;
|
||||
|
||||
const tableFields = getEventFieldsToDisplay({
|
||||
eventCategories,
|
||||
eventCode,
|
||||
eventRuleType,
|
||||
});
|
||||
|
||||
return tableFields.reduce<UseHighlightedFieldsResult[]>((acc, field) => {
|
||||
const item = dataFormattedForFieldBrowser.find(
|
||||
(data) => data.field === field.id || (field.legacyId && data.field === field.legacyId)
|
||||
);
|
||||
if (!item || isEmpty(item.values)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// If we found the data by its legacy id we swap the ids to display the correct one
|
||||
if (item.field === field.legacyId) {
|
||||
field.id = field.legacyId;
|
||||
}
|
||||
|
||||
if (
|
||||
field.id === 'agent.id' &&
|
||||
!isAlertFromEndpointEvent({ data: dataFormattedForFieldBrowser })
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
field: field.label ?? field.id,
|
||||
description: {
|
||||
field: field.overrideField ?? field.id,
|
||||
values: item.values,
|
||||
},
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue