Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';