Skip to content

Commit c2f0090

Browse files
PhilippeObertieokoneyo
authored andcommitted
[Security Solution][Threat Intelligence] convert the IOC flyout to expandable flyout (elastic#231959)
> [!WARNING] > The PR is a little on the big side, but a lot of the code was just moved from the `threat_intelligance` folder to the `flyout` folder. ## Summary This PR converts the flyout used in the Intelligence page from a normal EUI flyout to the expandable flyout using the [expandable flyout package](https://github.com/elastic/kibana/tree/main/x-pack/solutions/security/packages/expandable-flyout). > [!NOTE] > The PR does not make any UI changes to the flyout content. The overview, table and json tabs remains identical (outside of the header of the flyout itself). > The PR also should not make any UI or behavior changes to the rest of the application. Here are some benefit of using the expandable flyout: - we persist automatically which tab was opened last (in local storage) and future flyout are reopened on the same tab - we can better interact with all the other existing flyouts - we will have the ability to add an expanded state when necessary - it will facilitate the transition to the new flyout system as it's now using the same api as the other existing flyouts in Security Solution #### Flyout interaction remains unchanged on the Intelligence page https://github.com/user-attachments/assets/bcfe5bfa-2756-46d2-9092-24c38db667bb #### The add to new case functionality still works as before https://github.com/user-attachments/assets/c25b0efa-122a-4b35-a156-5b9530d93db3 #### Interaction with Timeline is also working as before https://github.com/user-attachments/assets/43fbf775-cf3a-48ff-a79d-dcafbb3d40a6 #### Flyout history now has an entry for this new IoC panel https://github.com/user-attachments/assets/5778d0b8-8fff-4896-86f4-65c5bc338133 ## How to test - ingest some threat intelligence data - navigate to the Intelligence page and interact with the flyout - verify all actions in the flyout's take action button - verify the interaction with the cases page ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [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 - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels.
1 parent faa2ad6 commit c2f0090

62 files changed

Lines changed: 1203 additions & 1095 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

x-pack/solutions/security/plugins/security_solution/public/flyout/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import type {
1414
FindingsVulnerabilityPanelExpandableFlyoutPropsNonPreview,
1515
FindingsVulnerabilityPanelExpandableFlyoutPropsPreview,
1616
} from '@kbn/cloud-security-posture';
17-
import { GraphGroupedNodePreviewPanelKey } from '@kbn/cloud-security-posture-graph';
1817
import type { GraphGroupedNodePreviewPanelProps } from '@kbn/cloud-security-posture-graph';
18+
import { GraphGroupedNodePreviewPanelKey } from '@kbn/cloud-security-posture-graph';
1919
import type { GenericEntityDetailsExpandableFlyoutProps } from './entity_details/generic_details_left';
2020
import {
2121
GenericEntityDetailsPanel,
@@ -89,6 +89,10 @@ import { AttackDetailsRightPanelKey } from './attack_details/constants/panel_key
8989
import type { AttackDetailsProps } from './attack_details/types';
9090
import { AttackDetailsProvider } from './attack_details/context';
9191
import { AttackDetailsPanel } from './attack_details';
92+
import type { IOCDetailsProps } from './ioc_details/types';
93+
import { IOCDetailsProvider } from './ioc_details/context';
94+
import { IOCPanel } from './ioc_details';
95+
import { IOCRightPanelKey } from './ioc_details/constants/panel_keys';
9296

9397
const GraphGroupedNodePreviewPanel = React.lazy(() =>
9498
import('@kbn/cloud-security-posture-graph').then((module) => ({
@@ -286,6 +290,14 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
286290
/>
287291
),
288292
},
293+
{
294+
key: IOCRightPanelKey,
295+
component: (props) => (
296+
<IOCDetailsProvider {...(props as IOCDetailsProps).params}>
297+
<IOCPanel path={props.path as IOCDetailsProps['path']} />
298+
</IOCDetailsProvider>
299+
),
300+
},
289301
];
290302

291303
export const SECURITY_SOLUTION_ON_CLOSE_EVENT = `expandable-flyout-on-close-${Flyouts.securitySolution}`;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { render } from '@testing-library/react';
9+
import React from 'react';
10+
import { IndicatorBlock } from './block';
11+
import { generateMockIndicator } from '../../../../common/threat_intelligence/types/indicator';
12+
import { TestProviders } from '../../../common/mock';
13+
14+
const mockIndicator = generateMockIndicator();
15+
16+
describe('IndicatorBlock', () => {
17+
it('should render field and value', () => {
18+
const { getByText } = render(
19+
<TestProviders>
20+
<IndicatorBlock field="threat.indicator.ip" indicator={mockIndicator} />
21+
</TestProviders>
22+
);
23+
24+
expect(getByText('threat.indicator.ip')).toBeInTheDocument();
25+
expect(getByText('0.0.0.0')).toBeInTheDocument();
26+
});
27+
28+
it('should render translated field and value', () => {
29+
const { getByText } = render(
30+
<TestProviders>
31+
<IndicatorBlock field="threat.indicator.name" indicator={mockIndicator} />
32+
</TestProviders>
33+
);
34+
35+
expect(getByText('Indicator')).toBeInTheDocument();
36+
expect(getByText('0.0.0.0')).toBeInTheDocument();
37+
});
38+
});

x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/modules/indicators/components/flyout/block.tsx renamed to x-pack/solutions/security/plugins/security_solution/public/flyout/ioc_details/components/block.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88
import { EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
99
import type { FC } from 'react';
1010
import React from 'react';
11-
import type { Indicator } from '../../../../../../common/threat_intelligence/types/indicator';
12-
import { IndicatorFieldValue } from '../common/field_value';
13-
import { IndicatorFieldLabel } from '../common/field_label';
11+
import type { Indicator } from '../../../../common/threat_intelligence/types/indicator';
12+
import { IndicatorFieldValue } from '../../../threat_intelligence/modules/indicators/components/common/field_value';
13+
import { IndicatorFieldLabel } from '../../../threat_intelligence/modules/indicators/components/common/field_label';
1414
import {
1515
CellActionsMode,
1616
SecurityCellActions,
1717
SecurityCellActionsTrigger,
18-
} from '../../../../../common/components/cell_actions';
19-
import { getIndicatorFieldAndValue } from '../../utils/field_value';
18+
} from '../../../common/components/cell_actions';
19+
import { getIndicatorFieldAndValue } from '../../../threat_intelligence/modules/indicators/utils/field_value';
2020

2121
const panelProps = {
2222
color: 'subdued' as const,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { render } from '@testing-library/react';
9+
import React from 'react';
10+
import { I18nProvider } from '@kbn/i18n-react';
11+
import { IndicatorEmptyPrompt } from './empty_prompt';
12+
13+
describe('IndicatorEmptyPrompt', () => {
14+
it('should render component', () => {
15+
const { getByText } = render(
16+
<I18nProvider>
17+
<IndicatorEmptyPrompt />
18+
</I18nProvider>
19+
);
20+
21+
expect(getByText('Unable to display indicator information')).toBeInTheDocument();
22+
expect(
23+
getByText('There was an error displaying the indicator fields and values.')
24+
).toBeInTheDocument();
25+
});
26+
});

x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/modules/indicators/components/flyout/empty_prompt.tsx renamed to x-pack/solutions/security/plugins/security_solution/public/flyout/ioc_details/components/empty_prompt.tsx

File renamed without changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { render } from '@testing-library/react';
9+
import React from 'react';
10+
import { I18nProvider } from '@kbn/i18n-react';
11+
import { IndicatorEmptyPrompt } from './empty_prompt';
12+
13+
describe('IndicatorEmptyPrompt', () => {
14+
it('should render component', () => {
15+
const { getByText } = render(
16+
<I18nProvider>
17+
<IndicatorEmptyPrompt />
18+
</I18nProvider>
19+
);
20+
21+
expect(getByText('Unable to display indicator information')).toBeInTheDocument();
22+
expect(
23+
getByText('There was an error displaying the indicator fields and values.')
24+
).toBeInTheDocument();
25+
});
26+
});

x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/modules/indicators/components/flyout/fields_table.tsx renamed to x-pack/solutions/security/plugins/security_solution/public/flyout/ioc_details/components/fields_table.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import { FormattedMessage } from '@kbn/i18n-react';
1111
import type { FC } from 'react';
1212
import React, { useMemo } from 'react';
1313
import { css } from '@emotion/react';
14-
import type { Indicator } from '../../../../../../common/threat_intelligence/types/indicator';
15-
import { IndicatorFieldValue } from '../common/field_value';
16-
import { unwrapValue } from '../../utils/unwrap_value';
14+
import type { Indicator } from '../../../../common/threat_intelligence/types/indicator';
15+
import { IndicatorFieldValue } from '../../../threat_intelligence/modules/indicators/components/common/field_value';
16+
import { unwrapValue } from '../../../threat_intelligence/modules/indicators/utils/unwrap_value';
1717
import {
1818
CellActionsMode,
1919
SecurityCellActions,
2020
SecurityCellActionsTrigger,
21-
} from '../../../../../common/components/cell_actions';
21+
} from '../../../common/components/cell_actions';
2222

2323
const euiTableSearchOptions: EuiSearchBarProps = {
2424
box: {

x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/modules/indicators/components/flyout/highlighted_values_table.tsx renamed to x-pack/solutions/security/plugins/security_solution/public/flyout/ioc_details/components/highlighted_values_table.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
* 2.0.
66
*/
77

8-
import React, { useMemo, type FC } from 'react';
8+
import React, { type FC, useMemo } from 'react';
99
import { EuiPanel } from '@elastic/eui';
10-
import type { Indicator } from '../../../../../../common/threat_intelligence/types/indicator';
11-
import { RawIndicatorFieldId } from '../../../../../../common/threat_intelligence/types/indicator';
12-
import { unwrapValue } from '../../utils/unwrap_value';
10+
import type { Indicator } from '../../../../common/threat_intelligence/types/indicator';
11+
import { RawIndicatorFieldId } from '../../../../common/threat_intelligence/types/indicator';
12+
import { unwrapValue } from '../../../threat_intelligence/modules/indicators/utils/unwrap_value';
1313
import { IndicatorFieldsTable } from './fields_table';
1414

1515
/**

x-pack/solutions/security/plugins/security_solution/public/threat_intelligence/modules/indicators/components/flyout/take_action.test.tsx renamed to x-pack/solutions/security/plugins/security_solution/public/flyout/ioc_details/components/take_action.test.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,25 @@
77

88
import { render } from '@testing-library/react';
99
import React from 'react';
10-
import type { Indicator } from '../../../../../../common/threat_intelligence/types/indicator';
11-
import { generateMockFileIndicator } from '../../../../../../common/threat_intelligence/types/indicator';
12-
import { TestProvidersComponent } from '../../../../mocks/test_providers';
13-
import { TakeAction } from './take_action';
14-
import { TAKE_ACTION_BUTTON_TEST_ID } from './test_ids';
10+
import { TAKE_ACTION_BUTTON_TEST_ID, TakeAction } from './take_action';
11+
import { useIOCDetailsContext } from '../context';
12+
import { generateMockIndicator } from '../../../../common/threat_intelligence/types/indicator';
13+
import { TestProviders } from '../../../common/mock';
14+
15+
jest.mock('../context');
1516

1617
describe('TakeAction', () => {
1718
it('should render an EuiContextMenuPanel', () => {
18-
const indicator: Indicator = generateMockFileIndicator();
19+
(useIOCDetailsContext as jest.Mock).mockReturnValue({
20+
indicator: generateMockIndicator(),
21+
});
22+
1923
const { getByTestId, getAllByText } = render(
20-
<TestProvidersComponent>
21-
<TakeAction indicator={indicator} />
22-
</TestProvidersComponent>
24+
<TestProviders>
25+
<TakeAction />
26+
</TestProviders>
2327
);
28+
2429
expect(getByTestId(TAKE_ACTION_BUTTON_TEST_ID)).toBeInTheDocument();
2530
expect(getAllByText('Take action')).toHaveLength(1);
2631
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { memo, useCallback, useMemo, useState } from 'react';
9+
import { EuiButton, EuiContextMenuPanel, EuiPopover, useGeneratedHtmlId } from '@elastic/eui';
10+
import { FormattedMessage } from '@kbn/i18n-react';
11+
import { BlockListFlyout } from '../../../threat_intelligence/modules/block_list/containers/flyout';
12+
import { useIOCDetailsContext } from '../context';
13+
import { canAddToBlockList } from '../../../threat_intelligence/modules/block_list/utils/can_add_to_block_list';
14+
import { AddToBlockListContextMenu } from '../../../threat_intelligence/modules/block_list/components/add_to_block_list';
15+
import { AddToNewCase } from '../../../threat_intelligence/modules/cases/components/add_to_new_case';
16+
import { AddToExistingCase } from '../../../threat_intelligence/modules/cases/components/add_to_existing_case';
17+
import { InvestigateInTimelineContextMenu } from '../../../threat_intelligence/modules/timeline/components/investigate_in_timeline';
18+
19+
export const TAKE_ACTION_BUTTON_TEST_ID = 'tiIndicatorFlyoutTakeActionButton';
20+
export const INVESTIGATE_IN_TIMELINE_TEST_ID = 'tiIndicatorFlyoutInvestigateInTimelineContextMenu';
21+
export const ADD_TO_EXISTING_CASE_TEST_ID = 'tiIndicatorFlyoutAddToExistingCaseContextMenu';
22+
export const ADD_TO_NEW_CASE_TEST_ID = 'tiIndicatorFlyoutAddToNewCaseContextMenu';
23+
export const ADD_TO_BLOCK_LIST_TEST_ID = 'tiIndicatorFlyoutAddToBlockListContextMenu';
24+
25+
/**
26+
* Component rendered at the bottom of the indicators flyout
27+
*/
28+
export const TakeAction = memo(() => {
29+
const { indicator } = useIOCDetailsContext();
30+
31+
const [blockListIndicatorValue, setBlockListIndicatorValue] = useState('');
32+
const [isPopoverOpen, setPopover] = useState(false);
33+
const smallContextMenuPopoverId = useGeneratedHtmlId({
34+
prefix: 'smallContextMenuPopover',
35+
});
36+
37+
const closePopover = useCallback(() => {
38+
setPopover(false);
39+
}, []);
40+
41+
const items = useMemo(
42+
() => [
43+
<InvestigateInTimelineContextMenu
44+
key={'investigateInTime'}
45+
data={indicator}
46+
onClick={closePopover}
47+
data-test-subj={INVESTIGATE_IN_TIMELINE_TEST_ID}
48+
/>,
49+
<AddToExistingCase
50+
key={'attachmentsExistingCase'}
51+
indicator={indicator}
52+
onClick={closePopover}
53+
data-test-subj={ADD_TO_EXISTING_CASE_TEST_ID}
54+
/>,
55+
<AddToNewCase
56+
key={'attachmentsNewCase'}
57+
indicator={indicator}
58+
onClick={closePopover}
59+
data-test-subj={ADD_TO_NEW_CASE_TEST_ID}
60+
/>,
61+
<AddToBlockListContextMenu
62+
key={'addToBlocklist'}
63+
data={canAddToBlockList(indicator)}
64+
onClick={closePopover}
65+
data-test-subj={ADD_TO_BLOCK_LIST_TEST_ID}
66+
setBlockListIndicatorValue={setBlockListIndicatorValue}
67+
/>,
68+
],
69+
[closePopover, indicator]
70+
);
71+
72+
const button = useMemo(
73+
() => (
74+
<EuiButton iconType="arrowDown" iconSide="right" onClick={() => setPopover(!isPopoverOpen)}>
75+
<FormattedMessage
76+
id="xpack.securitySolution.threatIntelligence.indicators.flyout.take-action.button"
77+
defaultMessage="Take action"
78+
/>
79+
</EuiButton>
80+
),
81+
[isPopoverOpen]
82+
);
83+
84+
return (
85+
<>
86+
<EuiPopover
87+
id={smallContextMenuPopoverId}
88+
button={button}
89+
isOpen={isPopoverOpen}
90+
closePopover={closePopover}
91+
panelPaddingSize="none"
92+
anchorPosition="downLeft"
93+
data-test-subj={TAKE_ACTION_BUTTON_TEST_ID}
94+
>
95+
<EuiContextMenuPanel size="s" items={items} />
96+
</EuiPopover>
97+
98+
{blockListIndicatorValue && (
99+
<BlockListFlyout
100+
indicatorFileHash={blockListIndicatorValue}
101+
setBlockListIndicatorValue={setBlockListIndicatorValue}
102+
/>
103+
)}
104+
</>
105+
);
106+
});
107+
108+
TakeAction.displayName = 'TakeAction';

0 commit comments

Comments
 (0)