Skip to content

Commit

Permalink
[Security Solution] expanded flyout - right section - overview tab - …
Browse files Browse the repository at this point in the history
…mitre attack (#152767)
  • Loading branch information
PhilippeOberti authored Mar 10, 2023
1 parent 526edbd commit 6e3a34f
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 {
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_DETAILS,
DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE,
} from '../../../screens/document_expandable_flyout';
import {
expandFirstAlertExpandableFlyout,
openOverviewTab,
} from '../../../tasks/document_expandable_flyout';
import { cleanKibana } from '../../../tasks/common';
import { login, visit } from '../../../tasks/login';
import { createRule } from '../../../tasks/api_calls/rules';
import { getNewRule } from '../../../objects/rule';
import { ALERTS_URL } from '../../../urls/navigation';
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';

// Skipping these for now as the feature is protected behind a feature flag set to false by default
// To run the tests locally, add 'securityFlyoutEnabled' in the Cypress config.ts here https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/config.ts#L50
describe.skip(
'Alert details expandable flyout right panel overview tab',
{ testIsolation: false },
() => {
before(() => {
cleanKibana();
login();
createRule(getNewRule());
visit(ALERTS_URL);
waitForAlertsToPopulate();
expandFirstAlertExpandableFlyout();
openOverviewTab();
});

it('should display mitre attack', () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_DETAILS).should('be.visible');
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
COLLAPSE_DETAILS_BUTTON_TEST_ID,
EXPAND_DETAILS_BUTTON_TEST_ID,
FLYOUT_HEADER_TITLE_TEST_ID,
MITRE_ATTACK_DETAILS_TEST_ID,
MITRE_ATTACK_TITLE_TEST_ID,
} from '../../public/flyout/right/components/test_ids';
import { getDataTestSubjectSelector } from '../helpers/common';

Expand Down Expand Up @@ -90,3 +92,9 @@ export const DOCUMENT_DETAILS_FLYOUT_INVESTIGATIONS_TAB_CONTENT = getDataTestSub
export const DOCUMENT_DETAILS_FLYOUT_HISTORY_TAB_CONTENT = getDataTestSubjectSelector(
HISTORY_TAB_CONTENT_TEST_ID
);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE = getDataTestSubjectSelector(
MITRE_ATTACK_TITLE_TEST_ID
);
export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_DETAILS = getDataTestSubjectSelector(
MITRE_ATTACK_DETAILS_TEST_ID
);
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const Expand: Story<void> = () => {
const panelContextValue = {
eventId: 'eventId',
indexName: 'indexName',
};
} as unknown as RightPanelContext;

return (
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('<ExpandDetailButton />', () => {
const panelContextValue = {
eventId: 'eventId',
indexName: 'indexName',
};
} as unknown as RightPanelContext;

const { getByTestId } = render(
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import type { Story } from '@storybook/react';
import { RightPanelContext } from '../context';
import { MitreAttack } from './mitre_attack';

export default {
component: MitreAttack,
title: 'Flyout/MitreAttack',
};

export const Default: Story<void> = () => {
const contextValue = {
searchHit: {
fields: {
'kibana.alert.rule.parameters': [
{
threat: [
{
framework: 'MITRE ATT&CK',
tactic: {
id: '123',
reference: 'https://attack.mitre.org/tactics/123',
name: 'Tactic',
},
technique: [
{
id: '456',
reference: 'https://attack.mitre.org/techniques/456',
name: 'Technique',
},
],
},
],
},
],
},
},
} as unknown as RightPanelContext;

return (
<RightPanelContext.Provider value={contextValue}>
<MitreAttack />
</RightPanelContext.Provider>
);
};

export const Emtpy: Story<void> = () => {
const contextValue = {
searchHit: {
some_field: 'some_value',
},
} as unknown as RightPanelContext;

return (
<RightPanelContext.Provider value={contextValue}>
<MitreAttack />
</RightPanelContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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 { MitreAttack } from './mitre_attack';
import { RightPanelContext } from '../context';
import { MITRE_ATTACK_DETAILS_TEST_ID, MITRE_ATTACK_TITLE_TEST_ID } from './test_ids';

describe('<MitreAttack />', () => {
it('should render mitre attack information', () => {
const contextValue = {
searchHit: {
fields: {
'kibana.alert.rule.parameters': [
{
threat: [
{
framework: 'MITRE ATT&CK',
tactic: {
id: '123',
reference: 'https://attack.mitre.org/tactics/123',
name: 'Tactic',
},
technique: [
{
id: '456',
reference: 'https://attack.mitre.org/techniques/456',
name: 'Technique',
},
],
},
],
},
],
},
},
} as unknown as RightPanelContext;

const { getByTestId } = render(
<RightPanelContext.Provider value={contextValue}>
<MitreAttack />
</RightPanelContext.Provider>
);

expect(getByTestId(MITRE_ATTACK_TITLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(MITRE_ATTACK_DETAILS_TEST_ID)).toBeInTheDocument();
});

it('should render empty component if missing mitre attack value', () => {
const contextValue = {
searchHit: {
some_field: 'some_value',
},
} as unknown as RightPanelContext;

const { baseElement } = render(
<RightPanelContext.Provider value={contextValue}>
<MitreAttack />
</RightPanelContext.Provider>
);

expect(baseElement).toMatchInlineSnapshot(`
<body>
<div />
</body>
`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import type { FC } from 'react';
import React, { useMemo } from 'react';
import { MITRE_ATTACK_DETAILS_TEST_ID, MITRE_ATTACK_TITLE_TEST_ID } from './test_ids';
import { getMitreComponentParts } from '../../../detections/mitre/get_mitre_threat_component';
import { useRightPanelContext } from '../context';

export const MitreAttack: FC = () => {
const { searchHit } = useRightPanelContext();
const threatDetails = useMemo(() => getMitreComponentParts(searchHit), [searchHit]);

if (!threatDetails || !threatDetails[0]) {
return <></>;
}

return (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem data-test-subj={MITRE_ATTACK_TITLE_TEST_ID}>
<EuiTitle size="xxs">
<h5>{threatDetails[0].title}</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem data-test-subj={MITRE_ATTACK_DETAILS_TEST_ID}>
{threatDetails[0].description}
</EuiFlexItem>
</EuiFlexGroup>
);
};

MitreAttack.displayName = 'MitreAttack';
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export const EXPAND_DETAILS_BUTTON_TEST_ID =
'securitySolutionDocumentDetailsFlyoutHeaderExpandDetailButton';
export const COLLAPSE_DETAILS_BUTTON_TEST_ID =
'securitySolutionDocumentDetailsFlyoutHeaderCollapseDetailButton';
export const MITRE_ATTACK_TITLE_TEST_ID = 'securitySolutionAlertDetailsFlyoutMitreAttackTitle';
export const MITRE_ATTACK_DETAILS_TEST_ID = 'securitySolutionAlertDetailsFlyoutMitreAttackDetails';
53 changes: 51 additions & 2 deletions x-pack/plugins/security_solution/public/flyout/right/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@
* 2.0.
*/

import { css } from '@emotion/react';
import React, { createContext, useContext, useMemo } from 'react';
import type { SearchHit } from '@kbn/es-types';
import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { useTimelineEventsDetails } from '../../timelines/containers/details';
import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers';
import { useSpaceId } from '../../common/hooks/use_space_id';
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
import { SecurityPageName } from '../../../common/constants';
import { SourcererScopeName } from '../../common/store/sourcerer/model';
import { useSourcererDataView } from '../../common/containers/sourcerer';
import type { RightPanelProps } from '.';

export interface RightPanelContext {
Expand All @@ -17,6 +27,10 @@ export interface RightPanelContext {
* Name of the index used in the parent's page
*/
indexName: string;
/**
* The actual raw document object
*/
searchHit: SearchHit<object> | undefined;
}

export const RightPanelContext = createContext<RightPanelContext | undefined>(undefined);
Expand All @@ -29,11 +43,46 @@ export type RightPanelProviderProps = {
} & Partial<RightPanelProps['params']>;

export const RightPanelProvider = ({ id, indexName, children }: RightPanelProviderProps) => {
const currentSpaceId = useSpaceId();
const eventIndex = indexName ? getAlertIndexAlias(indexName, currentSpaceId) ?? indexName : '';
const [{ pageName }] = useRouteSpy();
const sourcererScope =
pageName === SecurityPageName.detections
? SourcererScopeName.detections
: SourcererScopeName.default;
const sourcererDataView = useSourcererDataView(sourcererScope);
const [loading, _, searchHit] = useTimelineEventsDetails({
indexName: eventIndex,
eventId: id ?? '',
runtimeMappings: sourcererDataView.runtimeMappings,
skip: !id,
});

const contextValue = useMemo(
() => (id && indexName ? { eventId: id, indexName } : undefined),
[id, indexName]
() =>
id && indexName
? {
eventId: id,
indexName,
searchHit: searchHit as SearchHit<object>,
}
: undefined,
[id, indexName, searchHit]
);

if (loading) {
return (
<EuiFlexItem
css={css`
align-items: center;
justify-content: center;
`}
>
<EuiLoadingSpinner size="xxl" />
</EuiFlexItem>
);
}

return <RightPanelContext.Provider value={contextValue}>{children}</RightPanelContext.Provider>;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@

import type { FC } from 'react';
import React, { memo } from 'react';
import { EuiText } from '@elastic/eui';
import { OVERVIEW_TAB_CONTENT_TEST_ID } from './test_ids';
import { MitreAttack } from '../components/mitre_attack';

/**
* Overview view displayed in the document details expandable flyout right section
*/
export const OverviewTab: FC = memo(() => {
return <EuiText data-test-subj={OVERVIEW_TAB_CONTENT_TEST_ID}>{'Overview tab'}</EuiText>;
return <MitreAttack />;
});

OverviewTab.displayName = 'OverviewTab';

0 comments on commit 6e3a34f

Please sign in to comment.